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?

  • 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 3, 2014 at 16:11

8 Answers 8


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.



  • 2
    Probably 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 May 31, 2014 at 7:13
  • 1
    Just 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 to i (such as a) are unset.
    – depquid
    Jun 3, 2014 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 3, 2017 at 0:17
  • make that lsattr -R -l | grep " Immutable" (with a space for less filename false-positives for the reason mentioned.)
    – majick
    Mar 3, 2017 at 1:58
  • 2
    Why not simply grep for ^....i? Or at least something like ^[^ ]*i if the i can be in other position than fifth.
    – Ruslan
    Jun 24, 2017 at 13:39

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";;
  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]:
    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
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)
  • 1
    FYI on my system FS_IOC_GETFLAGS is 0x80046601.
    – antonone
    Sep 4, 2015 at 10:25
  • 1
    The value of FS_IOC_GETFLAGS depends on sizeof(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, 2017 at 13:51

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. Nov 1, 2017 at 19:20

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 (/\/\//) {
    } else {
      record = record "\n" $0

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. So lsattr does a lstat() first to check that the file is of the right types and then does open() followed by ioctl() to retrieve the attributes. But it calls open() without O_NOFOLLOW (nor O_NOCTTY). Someone could replace file with a symlink to /dev/watchdog for instance between the lstat() and open() and cause the system to reboot. It should do open(O_PATH|O_NOFOLLOW) followed by fstat(), openat() and ioctl() here to avoid the race conditions.

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);
        perror(("Failed to open file \""+std::string(path)+"\"").c_str());
        return false;
    unsigned long attrs;
        perror(("Failed to get flags for file \""+std::string(path)+"\"").c_str());
        return false;
    return attrs & EXT3_IMMUTABLE_FLAG;

int processPath(const char* path, const struct stat* info, int type, FTW* ftwbuf)
    case FTW_DNR:
        std::cerr << "Failed to read directory: " << path << "\n";
        return 0;
    case FTW_F:
            std::cout << path << '\n';
        return 0;
    return 0;

int main(int argc, char** argv)
        std::cerr << "Usage: " << argv[0] << " dir\n";
        return 1;
    static const int maxOpenFDs=15;
        perror("nftw failed");
        return 1;

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

Use this for find all immutable files:

lsattr -laR | grep "Immutable" | awk {'print $1'}

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
  • Your first code snippet has a typo and your second one doesn't find immutable files on my system.
    – depquid
    Jul 2, 2014 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 3, 2014 at 0:19
  • I didn't notice the second command was only looking in /etc. But both commands incorrectly find a non-immutable file created with touch `"echo -e "bogus\n---------i---e-- changeable"`"
    – depquid
    Jul 3, 2014 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 3, 2014 at 23:25
  • My bad. Try this: touch "`echo -e 'bogus\n---------i---e-- changeable'`"
    – depquid
    Jul 4, 2014 at 3:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.