More Filesystem Flexibility: chattr and lsattr

This column covers utilities that set and list ext2/ext3 file attributes.

You probably already know about permission bits and chmod(1); they're available on all Linux filesystems. The ext2 and ext3 filesystems also support file attributes. If you haven't used attributes before, they might surprise you. For instance, chattr +a myfile makes myfile append-only: you can add to the end of it, but you can't change its contents. Many file attributes have been planned but aren't implemented yet. Still, the ones that work now are worth knowing about!

The user-level programs chattr(1) and lsattr(1) change and list attributes. (They're part of e2fsprogs package, from http://e2fsprogs.sourceforge.net/. Version 1.40.8 was released in March, 2008.) The chattr manpage describes the attributes pretty well, so we'll see just a couple of examples. However, the lsattr manpage gives almost no information -- so we'll check the source code to figure out how it works.

Supported filesystems

Attributes are supported on ext2 and ext3 filesystems (and the ext4 filesystem, under development). Posix and BSD machines with those filesystems also support file attributes.

(On BSD systems, also see the chflags(1) utility and the command ls -l -o. These set and list file flags.)

If you get an error like lsattr: Inappropriate ioctl for device While reading flags on filesystem, then check /etc/mtab to find a locally-mounted ext[234] filesystem. (You can't check or set attributes on an NFS-mounted filesystem, for example.)

The attributes

The attributes include:

Not all of the planned attributes are supported yet, and some attributes can be set only by the superuser (root) or by processes with certain capabilities. The current chattr(1) manpage lists those limitations, including:

As we'll see later (the section "Checking lsattr output"), a few other attributes are defined but not documented in chattr(1).

Example: append-only files

Listing One has an example of file attributes: setting a file's append-only (a) attribute, then trying to overwrite or remove the file. First, ls -l shows that the log file has 132 characters and lsattr shows that it has no attributes set. After setting the a attribute with chattr +a, lsattr shows the attribute set. Then we can't replace the file with the shell's > operator or the cp command -- or remove it with rm. An append operation (with >>) succeeds, though; ls -l shows that the file now has 143 characters. Removing the attribute with chattr -a lets us overvrite the file; now it has just 15 characters.

Listing One: Append-only file

# ls -l log
-rw-r--r--  1 root root 132 2008-04-22 11:41 log
# lsattr log
----------------- log
# chattr +a log
# lsattr log
-----a----------- log
# echo the first line > log
bash: log: Operation not permitted
# echo a new line >> log
# ls -l log
-rw-r--r--  1 root root 143 2008-04-22 11:44 log
# cp /var/log/messages log
cp: cannot create regular file `log': Operation not permitted
# rm log
rm: cannot remove `log': Operation not permitted
# chattr -a log
# echo the first line > log
# ls -l log
-rw-r--r--  1 root root 15 2008-04-22 11:45 log

The append-only flag can help to prevent accidental changes in log files. Like many attributes, it can only be set or cleared by the superuser or by a process with that privilege.

Example: Preventing atime changes

The A attribute prevents changes to a file's last-access time. As the chattr manpage says, this can prevent some disk I/O on laptop computers and other filesystems -- which can be handy for files that are accessed often. It's also useful whenever you want to preserve a file's atime: use chattr +A to prevent changes, then use chattr -A once the atime can change again.

Although most attributes must be set or removed by the superuser or a specially-privileged process, any user can set the A attribute.

Listing Two has examples with two files, busyfile and otherfile. After setting the A attribute on busyfile, we show both files' atimes. Next we read each file (and throw the text away into /dev/null) and check both files again: otherfile's atime has changed, but busyfile's has not. After removing the A attribute, checking the atimes again shows no change.

Listing Two: Preventing atime changes

$ ls -l *file
-rw------- 1 jpeek users 776 Apr 23 11:37 busyfile
-rw------- 1 jpeek users 776 Apr 23 11:39 otherfile
$ chattr +A busyfile
$ lsattr *file
-------A---------- busyfile
------------------ otherfile
$ ls -lu *file
-rw------- 1 jpeek users 776 Apr 23 11:37 busyfile
-rw------- 1 jpeek users 776 Apr 23 11:39 otherfile
$ cat *file >/dev/null
$ ls -lu *file
-rw------- 1 jpeek users 776 Apr 23 11:37 busyfile
-rw------- 1 jpeek users 776 Apr 23 11:40 otherfile
$ chattr -A busyfile
$ ls -lu *file
-rw------- 1 jpeek users 776 Apr 23 11:37 busyfile
-rw------- 1 jpeek users 776 Apr 23 11:40 otherfile

More about lsattr

Documentation on lsattr(1) is much less detailed than for chattr. There's also an undocumented but useful -l option. Let's dig in.

lsattr gives one line of information for each file. It lists files and directories named on its command line; by default, it lists all files in the current directory:

$ lsattr /root/tmp
-----a----------- /root/tmp/log
----i------------ /root/tmp/uneditable
------dA--------- /root/tmp/activefile

The undocumented lsattr -l option (it's a lowercase "L") lists the long name of each attribute. This can be handy when you don't remember what each letter stands for:

$ cd /root/tmp
$ lsattr -l
./log              Append_Only
./uneditable       Immutable
./activefile       No_Dump, No_Atime

Version/generation numbers

Each file has a version/generation number, which you can list with lsattr -v. This comes from the ext2 filesystem inode, the unsigned long variable i_generation (also called i_version in some versions). It's especially useful with NFS, for systems that have an open filehandle for a file on another machine that's deleted and later the inode is re-used while the NFS filehandle is still open. Changing the inode's version/generation number can show that the file has changed.

As Listing Three shows, lsattr displays the version number for the target of a symbolic link and for any hard links, too. You can set this number with chattr -v, but that's probably not advisable without a good reason...

Listing Three: File version/generation numbers

$ ls -l
total 8
-rw-r--r--  2 jpeek users 119 2008-04-22 13:09 afile
-rw-r--r--  2 jpeek users 119 2008-04-22 13:09 hardlink
lrwxrwxrwx  1 jpeek users   5 2008-04-22 13:07 symlink -> afile
$ lsattr -v afile
1260086426 ----------------- afile
$ chattr -v 1234 afile
$ lsattr -v
 1234 ----------------- ./hardlink
 1234 ----------------- ./afile
 1234 ----------------- ./symlink

Checking lsattr output

Most Linux utilties don't recognize attributes. For instance, find(1) can't search for files by their attributes. But you can probably get what you want in the old-fashioned way: by hacking the output of lsattr with a shell alias, function, or script. Because the order of attributes from lsattr isn't documented, we'll have to check the source code.

Listing Four has the relevant part of e2fs_lib.c. The function print_flags(), which isn't shown here, iterates through flags_array:

Listing Four: How lsattr lists attributes

/* Print file attributes on an ext2 file system */
struct flags_name {
  unsigned long   flag;
  const char      *short_name;
  const char      *long_name;
};

static const struct flags_name flags_array[] = {
  { EXT2_SECRM_FL, "s", "Secure_Deletion" },
  { EXT2_UNRM_FL, "u" , "Undelete" },
  { EXT2_SYNC_FL, "S", "Synchronous_Updates" },
  { EXT2_DIRSYNC_FL, "D", "Synchronous_Directory_Updates" },
  { EXT2_IMMUTABLE_FL, "i", "Immutable" },
  { EXT2_APPEND_FL, "a", "Append_Only" },
  { EXT2_NODUMP_FL, "d", "No_Dump" },
  { EXT2_NOATIME_FL, "A", "No_Atime" },
  { EXT2_COMPR_FL, "c", "Compression_Requested" },
#ifdef ENABLE_COMPRESSION
  { EXT2_COMPRBLK_FL, "B", "Compressed_File" },
  { EXT2_DIRTY_FL, "Z", "Compressed_Dirty_File" },
  { EXT2_NOCOMPR_FL, "X", "Compression_Raw_Access" },
  { EXT2_ECOMPR_FL, "E", "Compression_Error" },
#endif
  { EXT3_JOURNAL_DATA_FL, "j", "Journaled_Data" },
  { EXT2_INDEX_FL, "I", "Indexed_direcctory" },
  { EXT2_NOTAIL_FL, "t", "No_Tailmerging" },
  { EXT2_TOPDIR_FL, "T", "Top_of_Directory_Hierarchies" },
  { 0, NULL, NULL }
};

Hacking lsattr output

You can extend lsattr by filtering its output through other utilities. For instance, to find all files in the current directory with the i ("immutable") attribute set:

lsattr | grep '^-*i-* '

As we saw earlier, that will actually include symbolic links -- which don't actually have the attribute set. To exclude symlinks (and the current directory itself), use find with its -maxdepth 1 option and xargs. (The find -print0 and xargs -0 options handle "special" filenames reliably; see the article “Filename Trouble”.)

find . -maxdepth 1 ! -name . ! -type l -print0 | xargs -0 lsattr | grep '^-*i-* '

To find all files in the current directory and below that have an attribute set, try piping lsattr output through egrep -v to omit lines that are empty, the names of directories (lines ending with a colon character), error messages, and lines for files with no attributes (lines that start with all dash characters followed by a space). The egrep operator | (a vertical bar) is the pattern alternation ("OR") operator. The lsattr option -R does a recursive search, and the shell operator 2>&1 merges standard error onto standard output (so egrep will filter everything):

lsattr -R 2>&1 | egrep -v '^$|:$|^lsattr: |^-+ '

To find files with any arbitrary attribute set, a script like the one in Listing Five could do the job. The script uses grep to search for any line of lsattr output that starts with zero or more of the valid attribute letters or a dash, followed by the attribute letter you're looking for, followed by zero or more other attributes or dashes, followed by a space character. The attribute letters were copied from the flags_array in e2fs_lib.c; since some of these attributes are unsupported or not normally used, you may want to shorten the list.

Listing Five: findattr script

#!/bin/bash
# findattr - find files in current directory 
#   with a certain attribute
# Usage: findattr attribute

attributes='suSDiadAcBZXEjItT'

case "$1" in
[$attributes])
  lsattr | grep "^[-$attributes]*$1[-$attributes]* "
  ;;
*)
  echo "${0##*/}: unknown attribute '$1'" 1>&2
  exit 1
  ;;
esac

Those are just some examples. Cook up your own to get the most out of file attributes.

More...

There's more information at http://www.securityfocus.com/infocus/1407, http://e2fsprogs.sourceforge.net/, the section "Advanced" Ext2fs features of the paper at http://e2fsprogs.sourceforge.net/ext2intro.html#section:ext2fs, the section ``History'' of http://www.win.tue.nl/~aeb/linux/lk/lk-7.html#ss7.2, and in the article "Programming Linux 2.6" at http://www.linux-mag.com/id/1684.

Jerry Peek is a freelance writer and instructor who has used Unix and Linux for more than 25 years. He's happy to hear from readers; see http://www.jpeek.com/contact.html.

[Read previous article] [Read next article]
[Read Jerry’s other Linux Magazine articles]