Access Control Lists From Scratch in C
Setuid binaries, extended attributes and a tiny sudo.
Contents
This is a small suite of C programs that implement their own access control list (ACL) layer on top of normal Unix permissions, plus a minimal sudo clone. I wrote it to properly understand the setuid() family of system calls instead of 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.
Background: Unix permissions and their limits#
Owner, group, other#
Classic Unix permissions are discretionary access control (DAC). Every file has one owner, one group and three sets of rwx bits: one for the owner, one for the group, one for everyone else.
That model is simple and it can’t express some very ordinary policies. “Larry can read and write this file, the staff group can only read it and nobody else can touch it” needs two different named parties with two different permissions and there’s only one group slot. Access control lists fix that by attaching any number of entries to a file, each naming a user or group and giving it its own permissions.
Linux already has POSIX ACLs. This project builds its own, to see how the pieces fit together.
Real and effective user IDs#
Every process carries more than one user ID:
- The real UID is who actually started the process.
- The effective UID is who the kernel treats the process as when checking permissions.
Normally the two are the same. The setuid bit on an executable changes that: when you run a setuid program, its effective UID becomes the file owner’s UID, while the real UID stays yours. That’s how passwd can edit /etc/shadow for an ordinary user. It runs as root, but it knows who called it.
This is powerful and dangerous. A setuid program runs with someone else’s privileges on input chosen by whoever invokes it. Every check it makes has to be right and in the right order.
How the tools work#
Every binary in the suite is owned by a privileged user and has its setuid bit set, so it starts running as that owner whoever invokes it. Each program then:
- Reads the real UID with
getuid()to find out who actually called it. - Decides whether that user is allowed to do what they asked, using the file’s ACL.
- Only then switches identity to carry out the operation.
What’s in it#
| Tool | What it does |
|---|---|
setacl / getacl |
Write and read ACL entries on a file |
fput / fget |
Write to and read from a file, mediated by its ACL |
cd / mkdir |
Directory operations through the same checks |
privx |
A tiny sudo: runs a program as that program’s owner |
Where the ACL lives#
Rather than invent a side file to hold ACLs, each file carries its own ACL in an extended attribute on its inode. Extended attributes (xattrs) are small name-value pairs the filesystem stores alongside a file’s normal metadata. They move with the file when it’s renamed and they disappear when it’s deleted. This project uses one attribute named user.acl.
setacl takes an identity (-u for a user, -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
Because it’s an ordinary extended attribute, you can also inspect it with the standard tools:
getfattr -n user.acl file.txt
How a check is decided#
When fget or fput runs, it looks the caller up in the file’s ACL first:
- If there’s an entry for that user or one of their groups, the ACL decides.
- If there’s no matching entry, it falls back to the file’s normal owner/group/other bits.
A new file starts with an ACL equal to its normal permissions, so behaviour is predictable until you explicitly change it.
The sudo clone#
privx is the part that makes the privilege model concrete. It looks up the target program with stat(), reads the owner’s 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 order of operations is what matters. privx forks and the child drops to the target identity with setuid() immediately, before it calls execvp() to run the program. The privileged parent never hands a root environment to the executed command and there’s no moment where code chosen by the caller runs with more privilege than intended.
There’s also a subtlety in setuid() itself worth knowing. When the effective UID is root, setuid(uid) changes the real, effective and saved UIDs, so the switch can’t be undone. When the effective UID isn’t root, it can only switch between the real and saved UIDs. A program that assumes it has permanently dropped privileges when it hasn’t is a classic source of vulnerabilities.
Getting the security details right#
Most of the interesting work was in the failure cases, which is where setuid programs usually go wrong:
- Order of operations. Privileges are dropped right after the fork and before
exec, so there’s no window where caller-controlled code runs with elevated rights. - Input validation. Permission strings and arguments are checked before use, so a crafted mode or argument can’t slip a bad ACL entry or an injected command through.
- Memory hygiene. Buffers are sized conservatively and freed after use, keeping the common overflow and use-after-free mistakes out of a binary that runs with someone else’s privileges.
What I took from it#
It’s a learning build, not a replacement for POSIX ACLs. The trust model is deliberately simple. But writing it is the fastest way I’ve found to understand why setuid binaries are such a sensitive part of a Unix system and why every check has to happen in exactly the right order.