michael orlitzky

Problems with POSIX ACLs and common utilities

posted 2012-08-13

Update 2018-03-06
a more better fix is described in Fix busted ACLs faster with libadacl.
Update 2013-01-30
I've written an update to this article with a new fix: Fixing POSIX ACLs in Common Utilities.

Introduction

POSIX Access Control Lists (ACLs) introduce fine-grained permissions to many common POSIX systems, such as Linux and BSD. If you're used to Windows filesystem permissions, POSIX ACLs work similarly: there are named users and groups, and you can grant them a certain level of access to files and directories independently of the standard RWX permission bits.

There's one novel aspect of POSIX ACLs, though—the “mask”. The mask permissions are an upper bound on the permissions that any other user or group can have. If the mask on a file is r-x, no user or group will be able to write to it, even if you grant them write access.

The ACL proposals were never accepted into POSIX, but they have been widely implemented nonetheless. For more on POSIX ACLs, see,

The Problem

Much like with NTFS on Windows, you can set a default ACL for a directory, causing all newly-created files and directories within that directory to inherit a set of permissions (ACL entries).

This works for some simple operations, but fails with a few common utilities:

A Simple Example

user $ mkdir acl

user $ cd acl

user $ setfacl -d -m user:apache:rwx .

All newly-created files in this directory should be rwx for the apache user.

user $ cp /etc/profile ./

user $ getfacl profile

# file: profile

# owner: mjo

# group: mjo user::rw-

user:apache:rwx #effective:r--

group::r-x #effective:r--

mask::r--

other::r--

From the user:apache:rwx #effective:r-- line, we see that the apache user only has read access to the file. This is because cp preserves the source file's group bits.

Once an ACL exists, these group bits no longer specify the group permissions: they now represent the mask entry which is an upper bound on all permissions. Therefore, no named entry can write to the new file, regardless of the default ACL.

A Real-life Example

We host a bunch of Drupal sites. The Drupal code lies under a public folder, and the site-specific configuration lies under public/sites/default. Everyone in the developers group should have read/write access to all of this stuff.

We'd like to upgrade some modules, and update the main installation of Drupal for www.example.com.

user $ cd public/sites/default/modules/

user $ getfacl .

# file: .

# owner: root

# group: root

user::rwx

user:www.example.com:r-x

group::---

group:developers:rwx

mask::rwx

other::---

default:user::rwx

default:user:www.example.com:r-x

default:group::---

default:group:developers:rwx

default:mask::rwx

default:other::---

All of the permissions are currently how we want them. Notice the default ACL for the developers group. We'll download and replace the ctools module.

user $ rm -rf ctools/

user $ wget -q https://ftp.drupal.org/files/projects/ctools-7.x-1.1.tar.gz

user $ tar -xf ctools-7.x-1.1.tar.gz

What does happen:

user $ getfacl ctools

# file: ctools

...

group:developers:rwx #effective:r-x

mask::r-x

...

So, whoever upgrades the module now needs to dig through the ctools directory and fix the mask on every file/directory contained therein. What should have happened:

user $ getfacl ctools

# file: ctools

# owner: mjo

# group: mjo

user::rwx

user:www.example.com:r-x

group::---

group:developers:rwx

mask::rwx

other::---

default:user::rwx

default:user:www.example.com:r-x

default:group::---

default:group:developers:rwx

default:mask::rwx

default:other::--

Now we'll upgrade Drupal as well.

user $ cd /var/www/example.com/www/

user $ ls

total 16K

drwxrwx---+ 9 root root 4.0K 2012-06-19 13:16 public

drwxrwx---+ 2 root root 4.0K 2012-06-17 10:32 tmp

user $ getfacl .

# file: .

# owner: root

# group: root

user::rwx

user:www.example.com:--x

group::---

group:developers:rwx

mask::rwx

other::---

default:user::rwx

default:user:www.example.com:r-x

default:group::---

default:group:developers:rwx

default:mask::rwx

default:other::---

Again, the default ACLs specify that developers should be able to read/write/execute anything under the current directory, and the www.example.com user should be able to read only.

user $ cp -r ~/drupal-7.15/ ./

user $ mv public/sites/ drupal-7.15/

user $ rm -rf public/

user $ mv drupal-7.15/ public

This completes the upgrade, but the default ACLs aren't in effect because cp -r copied the source group permissions into our mask entry.

user $ getfacl public/

...

group:developers:rwx #effective:r-x

mask::r-x

...

Again, this is what it should look like:

user $ getfacl public/

# file: public/

# owner: mjo

# group: mjo

user::rwx

user:www.example.com:r-x

group::---

group:developers:rwx

mask::rwx

other::---

default:user::rwx

default:user:www.example.com:r-x

default:group::---

default:group:developers:rwx

default:mask::rwx

default:other::---

To require every developer to go through the entire public folder (which contains some other special ACLs) and fix all of the masks is unrealistic. The public/sites/default/files directory needs to be writable by the web user, so it's not as simple as just recreating the ACLs on everything under public.

Proposed Fix

Note: This algorithm is outdated. In Fixing POSIX ACLs in Common Utilities, I propose a new fix that uses apply-default-acl, and so the default ACL application happens according to the apply-default-acl algorithm.

The utilities in question should be patched to reapply the default ACL when creating new files. For backwards-compatibility, an environment variable—GNU_REAPPLY_DEFAULT_ACL—should be set to “true” to request the new behavior.

The reapplication algorithm is as follows:

  1. Are we dealing with a regular file or directory? If not, exit..
  2. Does the parent directory have a default ACL? If not, exit.
  3. Determine whether or not the mask's execute bit should be set. If any existing ACL entry (including the mode bits) allows execute, the resulting mask should permit it as well.
  4. Wipe all existing access ACLs on the target.
  5. If the target is a directory, replace its default ACL with that of the parent.
  6. Loop through each entry in the parent's default ACL. For each entry:
    1. If this is a mask, user, group, or other entry, potentially mask the execute bit as calculated earlier.
    2. Update or create the resulting entry in the target's access ACL.

Code

Note: I've written an update to this article with a new fix: Fixing POSIX ACLs in Common Utilities.

Todo

  1. If portability matters, acl_get_perm can be reimplemented.

Relevant Bugs