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).

A flow chart of generic_permission. Is fsuid equal to the inode's i_uid? Yes: use the owner bits. No: is i_gid among the process's groups? Yes: use the group bits. No: use the other bits. Then: do the chosen bits have the wanted r, w or x? Yes: allowed. No: does the process hold CAP_DAC_OVERRIDE, valid for this inode? Yes: allowed. No: EACCES.
The kernel picks one class of bits, and only if those say no does it ask for a capability.

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:

FieldWhat it isalice's shell
real uid (ruid)who started the process1000
effective uid (euid)whose rights it uses; changed by setuid programs1000
filesystem uid (fsuid)what file checks really compare; follows euid1000
egid and supplementary groupsthe groups it is in1000 (alice), 1002 (dev)
capability setspieces of root's power; CapEff is what counts nowempty
user namespacewhich uid numbers it lives inthe 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 ls -l string -rwsr-xr-x of /usr/bin/passwd split into parts: the file type dash, then owner rws, group r-x, other r-x. Below each triple its octal digit: 7, 5, 5, and in front a 4 for the setuid bit, giving 4755. The s in the owner triple means x plus setuid.
-rwsr-xr-x is 4755: setuid, then rwx for the owner, r-x for the group and r-x for everyone else.

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 usedBitsResult
alice (uid 1000)ownerrw-✓ fd 3 (Demo: alice reads her own file)
bob (uid 1001, in dev)groupr--✓ 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:

CapabilityLets the process
CAP_DAC_OVERRIDEignore the rwx bits of files
CAP_DAC_READ_SEARCHread any file and search any directory
CAP_CHOWN, CAP_FOWNERchange any file's owner; act as the owner of any file
CAP_NET_BIND_SERVICEbind ports below 1024
CAP_KILLsend signals to any process
CAP_SYS_ADMINa 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.

Left: setuid. alice's bash has ruid 1000 and euid 1000; after execve of /usr/bin/passwd, which has the setuid bit and owner root, the new process has ruid 1000 and euid 0. Right: a user namespace. Container uids 0 to 65535 map to host uids 100000 to 165535; container root is host uid 100000. A host file owned by host uid 0 is outside the map, so inside the container it shows as nobody (65534) and container root's capabilities do not apply to it.
setuid changes which uid a process acts as; a user namespace changes what uid 0 means.

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.