The idea: the kernel compares two sets of numbers
Every time a program opens a file, runs a program or binds a port, the kernel decides on the spot: allowed, or EACCES (Permission denied). It does not look at names. It compares numbers from the process (its user id, group ids and capabilities) with numbers on the object (the file's owner, group and mode bits).
The page has one machine with three users (alice, bob, eve), a group dev that alice and bob are in, root, a web server running as www-data, and a container. Three files: alice's notes.txt, /etc/shadow and /usr/bin/passwd. Pick a process and an action, and watch the check run row by row.
The process side: credentials
Each task points to a struct cred. The fields that matter here:
| Field | What it is | alice's shell |
|---|---|---|
real uid (ruid) | who started the process | 1000 |
effective uid (euid) | whose rights it uses; changed by setuid programs | 1000 |
filesystem uid (fsuid) | what file checks really compare; follows euid | 1000 |
egid and supplementary groups | the groups it is in | 1000 (alice), 1002 (dev) |
| capability sets | pieces of root's power; CapEff is what counts now | empty |
| user namespace | which uid numbers it lives in | the host's (init) |
You can see them in /proc/<pid>/status: the lines Uid: and Gid: show real, effective, saved and filesystem ids, Groups: the supplementary groups, CapEff: a 64-bit mask. The id command prints the same thing by name.
The file side: owner, group and mode bits
The inode stores an owner i_uid, a group i_gid and 12 mode bits. Nine of them are three sets of rwx: for the owner, for the group, and for other (everyone else). The other three are setuid, setgid and sticky.
The rule that surprises people: the kernel uses exactly one of the three sets. If fsuid == i_uid, it uses the owner bits, even if the group or other bits would give more. Else, if egid or any supplementary group equals i_gid, the group bits. Else the other bits. A file with mode ----rw-rw- can be written by everyone except its owner.
Who reads notes.txt (-rw-r----- alice dev) | Class used | Bits | Result |
|---|---|---|---|
| alice (uid 1000) | owner | rw- | ✓ fd 3 (Demo: alice reads her own file) |
| bob (uid 1001, in dev) | group | r-- | ✓ fd 3 (Demo: bob reads it through group dev) |
| eve (uid 1003) | other | --- | ✗ EACCES (Demo: eve is denied) |
| root (uid 0) | other | --- | ✓ by CAP_DAC_OVERRIDE (Demo: root) |
Capabilities: root, split into pieces
Root is not special to the bit check: uid 0 is compared like any other uid, so root reading notes.txt lands on the other bits ---. What root has is capabilities. When the bits say no, generic_permission() asks capable_wrt_inode_uidgid(CAP_DAC_OVERRIDE), which lets the access through (for reading also CAP_DAC_READ_SEARCH; for running a file, only if at least one x bit is set).
Linux splits root's power into about 41 capabilities, so a program can get one piece without the rest. A few common ones:
| Capability | Lets the process |
|---|---|
CAP_DAC_OVERRIDE | ignore the rwx bits of files |
CAP_DAC_READ_SEARCH | read any file and search any directory |
CAP_CHOWN, CAP_FOWNER | change any file's owner; act as the owner of any file |
CAP_NET_BIND_SERVICE | bind ports below 1024 |
CAP_KILL | send signals to any process |
CAP_SYS_ADMIN | a large grab bag: mount, many ioctls, and more |
Ports below net.ipv4.ip_unprivileged_port_start (1024) need CAP_NET_BIND_SERVICE. A web server running as www-data gets EACCES from bind() on port 80. Instead of running it as root, give the binary a file capability: setcap cap_net_bind_service+ep /usr/sbin/nginx stores it in the security.capability extended attribute, and execve() puts it in the process's effective set (Demo: port 80 needs CAP_NET_BIND_SERVICE). systemd's AmbientCapabilities= does the same without touching the file.
setuid: how passwd writes /etc/shadow
Password hashes live in /etc/shadow, mode -rw-r----- root shadow. alice can't even read it. Yet she can change her password with passwd. The trick is the setuid bit on /usr/bin/passwd (-rwsr-xr-x root): when execve() runs a file with S_ISUID, it sets the effective uid (and the saved uid) to the file's owner. alice's passwd has ruid 1000 and euid 0, so the owner bits of /etc/shadow apply and the write is allowed (Demo: passwd is setuid root). The real uid tells passwd whose password it may change.
Because a setuid-root program runs with all of root's power on behalf of any user, every bug in it is a way to become root. That is why there are few of them (passwd, sudo, su, mount), why the kernel ignores setuid on scripts and on nosuid mounts, and why many tools moved to file capabilities instead.
User namespaces: root in a container
A user namespace gives a process its own uid numbers. A rootless container's /proc/self/uid_map might be 0 100000 65536: uids 0 to 65535 inside are 100000 to 165535 on the host. The kernel always checks the host number (the kuid). Container root has all capabilities, but only in its own namespace: capable_wrt_inode_uidgid() accepts them only for inodes whose uid and gid are mapped there. A host file owned by host root (uid 0) is not, so container root reading the host's /etc/shadow lands on the other bits and gets EACCES (Demo: container root is uid 100000 on the host). In the same container it can bind port 80, because its network namespace belongs to its user namespace. What a Container Really Is shows the other namespaces and cgroups.
Directories: x to pass, w to create
On a directory the same bits mean other things: r lists the names, x lets you pass through it to a name inside, and w (with x) lets you create, rename or delete entries. Opening /home/alice/notes.txt needs x on /, /home and /home/alice before the file's own bits are checked; deleting a file needs w on its directory, not on the file. The path walk is on What open() Does: Linux VFS Path Lookup, and the system call itself on A Linux System Call, Step by Step.
What the page leaves out
The page checks only the file itself, not the directories on its path. It leaves out POSIX ACLs (extra user and group entries, the mask), setgid and the sticky bit, the saved uid and setresuid(), the permitted, inheritable, bounding and ambient capability sets and how execve() computes them, no_new_privs and securebits, the umask, Linux Security Modules (SELinux, AppArmor), seccomp, idmapped mounts, read-only mounts (EROFS), and that open() checks once and later read() calls do not. Capabilities are drawn as "all" or one name; the real masks have 41 bits.