Copyright Notice

This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.

This text has appeared in an edited form in SysAdmin/PerformanceComputing/UnixReview magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.

Please read all the information in the table of contents before using this article.

Unix Review Column 18 (January 1998)

Perl has many operations off in the corners and crevices that make certain tasks easier. In this column, let's see how the bit manipulation operations (``bitops'') can help you deal with file permissions.

First, a little background. A file permission (or mode) is normally interpreted as a 12-bit value, grouped into four groups of three bits each. The top four bits are for the setuid, setgid, and sticky bits, which we won't be going in to here. The other three groups control the permissions for the owner of the file (``user''), members of the group-owner of the file (``group''), and anyone else (``other''), respectively. Each group in turn controls the read, write, and execute permissions for that class of users.

Now, we can get the permission number for a particular file using the the stat operator, like so:

    $mode = (stat "/etc/motd")[2];
    print $mode;

But this prints an incomprehensible 33188 on my machine. How can I get this into something reasonable? First, it's easiest to let Perl print the number as an octal value, so that the groups of three become apparent as octal values of 0-7:

    $mode = (stat "/etc/motd")[2];
    printf "%o\n", $mode;

which now prints ``100644'' on my machine. What's that extra 1 out there? That's the code to indicate that /etc/motd is a file, not something like a directory or a device. We don't want that, so here's where the bitops come in. Let's perform a bitwise-and with 07777 on that value, showing just the permissions, and not the encoded type:

    $mode = (stat "/etc/motd")[2];
    printf "%o\n", $mode & 07777;

Ahh! There's the familiar ``644'' that'd we'd be handing to the chmod command. OK, now how can I make that file world writeable? (Not recommended... just for illustration...) We need to or in the bits representing write for user, group, and world, which is most easily specified as 0222 (another octal value):

    $mode = (stat "/etc/motd")[2];
    $newmode = $mode | 0222;
    chmod $newmode, "/etc/motd";

Here, the use of the ``|'' causes the old mode to be or'ed with the extra bits, and we get a writeable message-of-the-day file (if we're root).

How about extracting just the group bits? For that, we need a bit-shift as well as a bit-and, like so:

    $mode = (stat "/etc/motd")[2];
    $groupmode = ($mode & 070) >> 3;
    print "$groupmode\n";

This prints ``4'' on my machine. Notice that I didn't have to use printf, because octal 0-7 prints fine as decimal numbers in the same range.

So, rather than continuing to show piecemeal examples, let's slam this new knowledge into a practical piece of code. This program will walk through your PATH, looking for directories that can be altered by untrusted individuals. It starts like this:

    #!/usr/bin/perl
    use File::stat;

The File::stat module allows us to have named access to some of the fields of stat. Watch for this later. Next we have:

    sub trust_user { $_[0] < 100 }
    sub trust_group { $_[0] < 100 }

These subroutines define trusted user and group ID numbers. It is presumed that a directory writeable by these users is probably safe. Next, split the path into usable components with:

    my @path = split /:/, $ENV{PATH};

And then we need to walk though the path one element at a time:

    for (@path) {
      (my $statinfo = stat $_) or
        (warn "cannot stat $_: $!"), next;

Note that we first put each name into $_, and then try to stat it. The use line above overrides the stat function so that it now returns an object that responds to methods named after the fields of a stat. So the return value here is a scalar, not a list.

Next, we set up some variables:

      my @reasons;
      my $uid = $statinfo->uid;
      my $gid = $statinfo->gid;
      my $mode = $statinfo->mode;

The @reasons variable holds the reasons why this directory is bad. Initially, this is empty. The other three variables hold the owner, group-owner, and permission and type value described above. Next, let's check the owner permissions:

      if ($uid != $<
          and not trust_user($uid)
          and $mode & 0200) {
        push @reasons, "non-owned, write by owner ".
          getpwuid($uid);
      }

Here, if the directory's owner is not the person running the script, and the directory's owner is not a trusted user, then the contents of this directory might possibly be altered by someone else, if they also have permission to write into the directory. So, we check this by looking at the $mode and'ed with 0200 which will be non-zero if the user-write bit is on.

If so, we add the untrustable reason to the list of reasons via the push of a text message. The getpwuid call fetchs the user's name instead of the number.

Next, the same thing (roughly) with respect to the group writeable bits:

      if (not trust_group($gid)
          and $mode & 020) {
        push @reasons, "write by group ".
          getgrgid($gid);
      }

And then with the other-writeable bits:

      if ($mode & 02) {
        push @reasons, "write by world";
      }

Finally, after all the reasons why it might be wrong, it's time to show the errors if we see it:

      if (@reasons) {
        print
          "danger: $_ is dangerous because ",
          "of the following reasons:\n",
          map "  $_\n", @reasons;
      }
    }

Here if @reasons is non-empty, then it's time to squawk. The map turns the individual reasons into an indented message, which is then preceded by the top error message. And an extra close-curly to match up the foreach loop started at the top.

So, if you stick this into a file and run it, you'll be seeing all the places you could get hurt. Be sure to adjust the trusted users and groups for your particular configuration.

And now, let's look at one other small program. Here, I wanted to do the equivalent of:

    chmod -R go=u-w /some/dir /other/dir

which I do frequently, because it sets all the permissions correct on a ``published'' tree regardless if they are an executable or whatever.

But I was annoyed that chmod didn't have a ``verbose'' option to let me know which ones were wrong and therefore being fixed. I also wanted to mark some directories as ``don't touch this'', which is another option that the standard chmod didn't have.

But never fear... a quick Perl hack to the rescue! This little program starts with:

    #!/usr/bin/perl
    $| = 1;
    use File::Find;

These lines unbuffer STDOUT, and also pull in the File::Find module. Next, we have the meat of the top-level code. Just one line:

    find \&wanted, @ARGV;

This line calls the find function defined by File::Find, passing it the subroutine defined below and the command-line arguments. I described File::Find in an earlier column. The &wanted subroutine will be called with each qualified pathname, and starts like this:

    sub wanted {
      if (-d && -e "$_/.PRIVATE") {
        print "## skipping contents of $File::Find::name\n";
        return $File::Find::prune = 1;
      }

First, we rule out any directories that contain a file or directory named .PRIVATE. This is how I can get my ``no touch'' areas. Setting the $File::Find::prune variable to 1 causes File::Find::find to abort descending further into this directory. Next, we skip over any symlinks or non files or directories:

      return if -l;
      return unless -f or -d;

And also rule out anything we can't stat:

      my @stat = stat or return;

Next we grab various bits of the mode:

      my $mode = $stat[2] & 07777;
      my $user_rwx = ($mode & 00700) >> 6;
      my $target_bits = $user_rwx & 05;
      my $group_rwx = ($mode & 00070) >> 3;
      my $other_rwx = ($mode & 00007);

Here... the $user_rwx is the user-bits, while $target_bits is what we want the group and other bits to be. Let's see if they are:

      if ($target_bits != $group_rwx
          or $target_bits != $other_rwx) {

Well, if this fails, then we need to compute a new mode:

        my $new_mode = (($mode & 07700) |
                        ($target_bits << 3) |
                        ($target_bits));

Note the use if bit-or here to create a new mode from the pieces of the bits. Finally, let's say what we're doing, and do it:

        printf
          "chmod %o %s # was %o\n",
          $new_mode, $File::Find::name, $mode;
        chmod $new_mode, $_;
      }
    }

And the final curly brace closes off the outer subroutine definition.

So, bitwise operators can be your friend. Get to know them and use them. And perhaps the next time you curse at the standard Unix tools for not providing that verbose option, you can just hack out a 10-line Perl program to do the job instead, at nearly the same speed. Enjoy!


Randal L. Schwartz is a renowned expert on the Perl programming language (the lifeblood of the Internet), having contributed to a dozen top-selling books on the subject, and over 200 magazine articles. Schwartz runs a Perl training and consulting company (Stonehenge Consulting Services, Inc of Portland, Oregon), and is a highly sought-after speaker for his masterful stage combination of technical skill, comedic timing, and crowd rapport. And he's a pretty good Karaoke singer, winning contests regularly.

Schwartz can be reached for comment at merlyn@stonehenge.com or +1 503 777-0095, and welcomes questions on Perl and other related topics.