For config auditing reasons, I want to be able to search my ext3 filesystem for files which have the immutable attribute set (via chattr +i
). I can't find any options for find
or similar that do this. At this point, I'm afraid I'll have to write my own script to parse lsattr
output for each directory. Is there a standard utility that provides a better way?

- 3,891
-
I should have clarified that in my case, I'm auditing just for config management, not intrusion detection, so I don't have to worry too much about newlines, as I know the file names I'm working with won't have them. Nonetheless, the newline issue is worth keeping in mind, so I'll leave my question as-is. – depquid Jun 03 '14 at 16:11
9 Answers
It can be partially accomplished by piping the lsattr
command through the grep
command.
lsattr -R | grep +i
However, I believe when you mention the entire ext3
file system the search might involve /proc
, /dev
and some other directories which might report some errors that you just want to ignore. You can probably run the command as,
lsattr -R 2>/dev/null | grep -- "-i-"
You might want to make the grep
a bit more strict by using grep
's PCRE facility to more explicitly match the "-i-".
lsattr -R 2>/dev/null | grep -P "(?<=-)i(?=-)"
This will then work for situations such as this:
$ lsattr -R 2>/dev/null afile | grep -P "(?<=-)i(?=-)"
----i--------e-- afile
But is imperfect. If there are additional attributes enabled around the immutable flag, then we'll not match them, and this will be fooled by files whose names happen to match the above pattern as well, such as this:
$ lsattr -R 2>/dev/null afile* | grep -P "(?<=-)i(?=-)"
----i--------e-- afile
-------------e-- afile-i-am
We can tighten up the pattern a bit more like this:
$ lsattr -a -R 2>/dev/null afile* | grep -P "(?<=-)i(?=-).* "
----i--------e-- afile
But it's still a bit too fragile and would require additional tweaking depending on the files within your filesystem. Not to mention as @StephaneChazeles has mentioned in comments that this can be gamed fairly easily by the inclusion of newlines with a files name to bypass the above pattern to grep
.
References
https://groups.google.com/forum/#!topic/alt.os.linux/LkatROg2SlM

- 67,283
- 35
- 116
- 255

- 39,297
-
2Probably not good for auditing as one can fake or hide an immutable file by have newline characters in the file name with that approach. Also, it's not uncommon for filenames to have
-i-
in its name (there are 34 on the system I'm currently logged on). You'll probably want the-a
option as well – Stéphane Chazelas May 31 '14 at 07:13 -
1Just out of curiosity, what is the
+i
supposed to be for in the first example? It doesn't work for me. Also, grepping for-i-
assumes that the attributes that appear adjacent toi
(such asa
) are unset. – depquid Jun 03 '14 at 16:25 -
I decided the -l switch to make this all easier:
lsattr -R -l | grep "Immutable"
(okay, "Immutable" might be in a filename but it's highly doubtful) – majick Mar 03 '17 at 00:17 -
make that
lsattr -R -l | grep " Immutable"
(with a space for less filename false-positives for the reason mentioned.) – majick Mar 03 '17 at 01:58 -
2Why not simply grep for
^....i
? Or at least something like^[^ ]*i
if thei
can be in other position than fifth. – Ruslan Jun 24 '17 at 13:39 -
This pattern can not matched be like this:
----ia-------e-- /usr/local/cpanel/cpkeyclt
– Nabi K.A.Z. May 04 '20 at 22:09
Given that the purpose of the script is auditing, it is especially important to deal correctly with arbitrary file names, e.g. with names containing newlines. This makes it impossible to use lsattr
on multiple files simultaneously, since the output of lsattr
can be ambiguous in that case.
You can recurse with find
and call lsattr
on one file at a time. It'll be pretty slow though.
find / -xdev -exec sh -c '
for i do
attrs=$(lsattr -d "$i"); attrs=${attrs%% *}
case $attrs in
*i*) printf "%s\0" "$i";;
esac
done' sh {} +
I recommend using a less cranky language such as Perl, Python or Ruby and doing the work of lsattr
by yourself. lsattr
operates by issuing a FS_IOC_GETFLAGS
ioctl syscall and retrieving the file's inode flags. Here's a Python proof-of-concept.
#!/usr/bin/env python2
import array, fcntl, os, sys
S_IFMT = 0o170000
S_IFDIR = 0o040000
S_IFREG = 0o100000
FS_IOC_GETFLAGS = 0x80086601
EXT3_IMMUTABLE_FL = 0x00000010
count = 0
def check(filename):
mode = os.lstat(filename).st_mode
if mode & S_IFMT not in [S_IFREG, S_IFDIR]:
return
fd = os.open(filename, os.O_RDONLY)
a = array.array('L', [0])
fcntl.ioctl(fd, FS_IOC_GETFLAGS, a, True)
if a[0] & EXT3_IMMUTABLE_FL:
sys.stdout.write(filename + '\0')
global count
count += 1
os.close(fd)
for x in sys.argv[1:]:
for (dirpath, dirnames, filenames) in os.walk(x):
for name in dirnames + filenames:
check(os.path.join(dirpath, name))
if count != 0: exit(1)

- 829,060
-
1
-
1The value of
FS_IOC_GETFLAGS
depends onsizeof(long)
. See e.g. the following bash command to find out what the macro expands to in C:gcc -E - <<< $'#include <linux/fs.h>\nFS_IOC_GETFLAGS' | tail -n1
. I got from it the following expression:(((2U) << (((0 +8)+8)+14)) | ((('f')) << (0 +8)) | (((1)) << 0) | ((((sizeof(long)))) << ((0 +8)+8)))
, which simplifies to(2U << 30) | ('f' << 8) | 1 | (sizeof(long) << 16)
. – Ruslan Jun 24 '17 at 13:51
To deal with arbitrary file names (including those containing newline characters), the usual trick is to find files inside .//.
instead of .
. Because //
cannot normally occur while traversing the directory tree, you're sure that a //
signals the start of a new filename in the find
(or here lsattr -R
) output.
lsattr -R .//. | awk '
function process() {
i = index(record, " ")
if (i && index(substr(record,1,i), "i"))
print substr(record, i+4)
}
{
if (/\/\//) {
process()
record=$0
} else {
record = record "\n" $0
}
}
END{process()}'
Note that the output will still be newline separated. If you need to post-process it, you'll have to adapt it. For instance, you could add a -v ORS='\0'
to be able to feed it to GNU's xargs -r0
.
Also note that lsattr -R
(at least 1.42.13) cannot report the flags of files whose path is larger than PATH_MAX (usually 4096), so someone could hide such an immutable file by moving its parent directory (or any of the path components that lead to it, except itself as it's immutable) into a very deep directory.
A work around would be to use find
with -execdir
:
find . -execdir sh -c '
a=$(lsattr -d "$1") &&
case ${a%% *} in
(*i*) ;;
(*) false
esac' sh {} \; -print0
Now, with -print0
, that's post-processable, but if you intend to do anything with those paths, note that any system call on file paths greater than PATH_MAX would still fail and directory components could have be renamed in the interval.
If we're to get a reliable report on a directory tree that's potentially writable by others, there are a few more issues inherent to the lsattr
command itself that we'd need to mention:
- the way
lsattr -R .
traverses the directory tree, it is subject to race conditions. One can make it descend to directories outside of the directory tree routed at.
by replacing some directories with symlinks at the right moment. - even
lsattr -d file
has a race condition. Those attributes are only applicable to regular files or directories. Solsattr
does alstat()
first to check that the file is of the right types and then doesopen()
followed byioctl()
to retrieve the attributes. But it callsopen()
withoutO_NOFOLLOW
(nor O_NOCTTY). Someone could replacefile
with a symlink to/dev/watchdog
for instance between thelstat()
andopen()
and cause the system to reboot. It should doopen(O_PATH|O_NOFOLLOW)
followed byfstat()
,openat()
andioctl()
here to avoid the race conditions.

- 544,893
Thanks to Ramesh, slm and Stéphane for pointing me in the right direction (I was missing the -R
switch for lsattr
). Unfortunately, none of the answers so far worked correctly for me.
I came up with the following:
lsattr -aR .//. | sed -rn '/i.+\.\/\/\./s/\.\/\///p'
This protects against newlines being used to make a file appear as being immutable when it is not. It does not protect against files that are set as immutable and have newlines in their filenames. But since such a file would have to be made that way by root, I can be confident that such files don't exist on my filesystem for my use case. (This method is not suitable for intrusion detection in cases where the root user may be compromised, but then neither is using the same system's lsattr
utility which is also owned by the same root user.)
-
Only root can add the immutable bit to a file, but potentially other users can later rename path components that lead to those files, so the file path may contain newline. Also a user can create a file path (not immutable) that would fool your script into thinking another file is immutable. – Stéphane Chazelas Nov 01 '17 at 19:20
Using find -exec
is too slow, parsing output of lsattr
is unreliable similarly to that of ls
, using Python as in the answer by Gilles requires to choose the constant for ioctl
depending on whether the Python interpreter is 32- or 64-bit...
The problem at hand is more or less low-level, so let's go lower level: C++ is not that bad as a scripting language :) As a bonus, it has access to system C headers with full power of the C preprocessor.
The following program searches for immutable files, staying within one filesystem, i.e. never crosses mount points. To search the apparent tree, crossing mount points as needed, remove FTW_MOUNT
flag in the nftw
call. Also it doesn't follow symlinks. To do follow them, remove FTW_PHYS
flag.
#define _FILE_OFFSET_BITS 64
#include <iostream>
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <sys/stat.h>
#include <ftw.h>
bool isImmutable(const char* path)
{
static const int EXT3_IMMUTABLE_FLAG=0x10;
const int fd=open(path,O_RDONLY|O_NONBLOCK|O_LARGEFILE);
if(fd<=0)
{
perror(("Failed to open file \""+std::string(path)+"\"").c_str());
return false;
}
unsigned long attrs;
if(ioctl(fd,FS_IOC_GETFLAGS,&attrs)==-1)
{
perror(("Failed to get flags for file \""+std::string(path)+"\"").c_str());
close(fd);
return false;
}
close(fd);
return attrs & EXT3_IMMUTABLE_FLAG;
}
int processPath(const char* path, const struct stat* info, int type, FTW* ftwbuf)
{
switch(type)
{
case FTW_DNR:
std::cerr << "Failed to read directory: " << path << "\n";
return 0;
case FTW_F:
if(isImmutable(path))
std::cout << path << '\n';
return 0;
}
return 0;
}
int main(int argc, char** argv)
{
if(argc!=2)
{
std::cerr << "Usage: " << argv[0] << " dir\n";
return 1;
}
static const int maxOpenFDs=15;
if(nftw(argv[1],processPath,maxOpenFDs,FTW_PHYS|FTW_MOUNT))
{
perror("nftw failed");
return 1;
}
}

- 3,370
Use this for find all immutable files:
lsattr -laR | grep "Immutable" | awk {'print $1'}

- 160
Probably a bit late to add , but I created three different files with immutable bits in different sub-folders of my /etc directory.
Here's how I found them :
# lsattr -aR /etc 2>/dev/null | awk '{ print $1 " " $2 }' | grep -w i

- 1
Disclaimer: I am the current author of rawhide (rh) which is used in this answer (see https://github.com/raforg/rawhide).
If you are able to install rawhide (rh), then immutable files can be found with:
rh / immutable

- 171
Instead of piping the output to grep, why not just use awk to only match the 'i' in the first field of the output?
lsattr -Ra 2>/dev/null /|awk '$1 ~ /i/ && $1 !~ /^\// {print}'
In fact, I run this daily via cron to scan the /etc directory on hundreds of servers and send the output to syslog. I can then generate a daily report via Splunk:
lsattr -Ra 2>/dev/null /etc|awk '$1 ~ /i/ && $1 !~ /^\// {print "Immutable_file="$2}'|logger -p local0.notice -t find_immutable

- 1
-
Your first code snippet has a typo and your second one doesn't find immutable files on my system. – depquid Jul 02 '14 at 21:19
-
Fixed typo in first command. Perhaps the second one isn't finding any immutable files because there aren't any? – Rootdev Jul 03 '14 at 00:19
-
I didn't notice the second command was only looking in
/etc
. But both commands incorrectly find a non-immutable file created withtouch `"echo -e "bogus\n---------i---e-- changeable"`"
– depquid Jul 03 '14 at 15:27 -
It says in my original post that I am running that via cron to scan the /etc directory. I can't help it if you didn't read either the post or the command before running it.
And yes, you can probably construct an edge case to fool just about any search if you feel like it, but since you were so quick to point out the typo in my original command (missing the last '), I'll point out that your command doesn't work as written so won't create anything! :-)
– Rootdev Jul 03 '14 at 23:25 -
My bad. Try this:
touch "`echo -e 'bogus\n---------i---e-- changeable'`"
– depquid Jul 04 '14 at 03:33