Userland ACLs

C · setuid · xattr · access control

This is a small suite of C programs that implement their own access-control list layer on top of normal Unix permissions, plus a minimal sudo clone. I wrote it to actually understand the setuid() family of system calls rather than just reading about them: how a process changes identity, how the setuid bit hands you the file owner's privileges, and how easy it is to get that wrong.

Every binary is owned by a privileged user with its setuid bit set, so it starts running as that owner regardless of who invokes it. Each program then reads the real user ID with getuid(), decides whether that user is allowed to do what they asked, and only then switches identity to carry out the operation.

What is in it

How the ACLs work

Rather than invent a side-file format, each file carries its own ACL in an extended attribute named user.acl on its inode. setacl takes an identity (-u for a user or -g for a group) and a permission string in rwx form, and stores an entry; -d removes the attribute entirely. The permission string is validated before anything is written, so a malformed mode never lands on disk.

setacl -u larry -p rw- file.txt     # grant larry read/write
setacl -g staff -p r-- file.txt     # grant the staff group read
getacl file.txt                     # print the stored entries
setacl -d file.txt                  # drop the ACL

When fget or fput runs, it looks up the caller in the file's ACL first. If there is an entry for that user or group, the ACL decides. If there is no matching entry, it falls back to the file's normal DAC bits. A new file inherits an ACL equal to its DAC permissions, so behaviour is predictable until you explicitly change it.

The sudo clone

privx is the part that makes the privilege model concrete. It stats the target program, reads the owner UID, and runs the command as that owner. A program owned by root runs as root; a program owned by an ordinary user runs as that user. The child drops to the target identity with setuid() immediately after the fork, before it ever calls execvp(), so the privileged parent never hands a root environment to the executed command.

Getting the security details right

Most of the interesting work was in the failure cases, which is where setuid programs usually go wrong:

It is a learning build, not a replacement for POSIX ACLs, and the trust model is deliberately simple. But writing it is the fastest way I have found to internalise why setuid binaries are such a sensitive part of a Unix system, and why every check has to happen in the right order.