Operating Systems · Isolation and Namespaces

Capabilities and the Decomposition of Root Privilege

How the single root check was split into distinct capabilities, how the permitted, effective and inheritable sets interact across execve, and why file capabilities and no_new_privs change the rules.

For most of its history, the kernel asked one question about privilege: whether the caller’s effective user ID was 0. That single bit made it impossible to grant a program the ability to bind a low port without also granting it the ability to load a kernel module, read any file, and reboot the machine. Capabilities replace the bit with several dozen independent flags, and nearly all of the complexity in the mechanism comes from the rules governing what happens to those flags when a process calls execve.

The sets, and which one a check consults

Each process carries five capability sets, all stored in struct cred:

  • Permitted — the ceiling. A capability in permitted may be raised into effective at will.
  • Effective — what the kernel actually checks. Every capable() call tests the effective set of the current credential.
  • Inheritable — carried across execve, and mostly useful for file capabilities rather than for direct checks.
  • Bounding — a per-process mask that limits what may ever be added to permitted. Dropping a bit here is irreversible for that process and its descendants.
  • Ambient — introduced later than the others, and the only set that survives execve for a non-privileged binary without help from a file attribute.

The numbers are stable and defined in the kernel’s uapi header. CAP_CHOWN is 0, CAP_DAC_OVERRIDE is 1, CAP_KILL is 5, CAP_SETUID is 7, CAP_NET_BIND_SERVICE is 10, CAP_NET_ADMIN is 12, CAP_SYS_CHROOT is 18, CAP_SYS_PTRACE is 19, CAP_SYS_ADMIN is 21, CAP_SYS_MODULE is 16, CAP_SYS_TIME is 25, and CAP_BPF is 39. /proc/sys/kernel/cap_last_cap reports the highest number the running kernel supports, so a program that hardcodes numbers rather than consulting the header is fragile across kernel versions.

CAP_SYS_ADMIN is the failure of the decomposition. It gates mount, unshare of most namespace types, sethostname, setdomainname, quota manipulation, pivot_root, and a long list of device-specific ioctls. Because those operations have nothing in common, a process that needs any one of them needs a capability that grants all of them, and the practical result is that a container which must mount a filesystem is nearly as privileged as root. The kernel has moved pieces out of it in recent releases: CAP_BPF and CAP_PERFMON in Linux 5.8, CAP_CHECKPOINT_RESTORE in 5.9. The split is incomplete.

What execve does to the sets

The transformation at execve is where most confusion lives. For a binary with no file capabilities and no setuid bit, the rules are:

  1. If the effective uid is 0, the file is treated as carrying every capability in both its permitted and effective masks, so permitted becomes the union of inheritable, bounding and ambient, and effective becomes permitted. Root keeps its power.
  2. Otherwise, the file’s masks are empty, so permitted becomes the ambient set and effective becomes permitted, which is the ambient set. Inheritable is unchanged.
  3. If no_new_privs is set, the file’s setuid bit and file capabilities are ignored, so the file contributes nothing to any of the sets.

Rule 2 is the reason a non-root process loses its capabilities on exec: the file’s masks are empty, so permitted and effective are rebuilt from the ambient set alone, and a process that never raised anything into ambient finds both empty. A process that has CAP_NET_BIND_SERVICE in effective and then executes a shell finds the capability gone. The ambient set exists precisely to fix this: raising a capability into ambient with prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, ...) requires it to be in both permitted and inheritable, and once there it survives execve for any non-privileged binary.

Rule 3 is the sandbox primitive. PR_SET_NO_NEW_PRIVS is a one-way flag: once set, execve cannot increase privilege, setuid binaries do not elevate, and file capabilities do not apply. It is inherited across fork and cannot be cleared. A container runtime sets it before execing the workload so that a setuid binary inside the image cannot escape the container’s privilege level, and it is a prerequisite for unprivileged use of seccomp filters, discussed in seccomp-BPF and the Filtering of System Calls.

/* Keep CAP_NET_BIND_SERVICE across execve without granting anything else. */
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_BIND_SERVICE, 0, 0);

/* Or drop the ability to ever gain privilege again. */
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);

File capabilities store the grant on disk

A binary that needs a capability without being setuid root carries it in the security.capability extended attribute. The attribute holds a revision number, a permitted set, an inheritable set, and, from revision 3 onward, a root uid that scopes the grant to a user namespace. Revision 2 carries the two sets as 64-bit masks, giving a 20-byte attribute; revision 3 adds the root user ID of the namespace that created it, for 24 bytes. The kernel computes the file’s capability sets at execve as the intersection of the file’s permitted set with the process’s bounding set, then applies the same rules as for setuid: effective gets the file’s effective bits, permitted gets the file’s permitted bits.

Two properties follow. First, a file capability is only as strong as the bounding set: dropping CAP_NET_RAW from the bounding set makes a file capability for it inert. Second, writing the attribute requires CAP_SETFCAP (capability 31, which exists only to control the assignment of other capabilities), and a thread in a non-initial user namespace must additionally have the file’s user and group IDs mapped in its own namespace before the write is permitted. Removing CAP_SETFCAP from a container’s bounding set prevents the container from minting new privileged binaries inside its own filesystem.

The security model around the attribute is the reason nosuid mounts matter. A filesystem mounted MS_NOSUID ignores the setuid bit, and it also ignores file capabilities, so an attacker who can write to a mounted image cannot create a privileged binary that will be honoured when executed. This is why container runtimes mount most of the image read-only and why /proc, /sys and /dev are mounted with nosuid,nodev,noexec.

Dropping privilege in the right order

The ordering rules for dropping are not symmetric with the rules for raising. Effective can always be lowered. Permitted can be lowered, but the new effective set must remain a subset of the new permitted set, so a capability still present in effective cannot be removed from permitted in the same call; the two are lowered together by passing the reduced masks to capset. Removing a bit from the bounding set requires CAP_SETPCAP in the effective set, and it is one-way: a capability dropped from the bounding set cannot be put back into it. The ambient set has its own constraint: a capability cannot be raised into ambient unless it is in both permitted and inheritable, and lowering a capability out of permitted or inheritable automatically clears the ambient bit.

capset performs the first two operations in one call; prctl(PR_CAPBSET_DROP) does the third. The order is forced by the capability each step needs: the bounding drop has to happen while CAP_SETPCAP is still effective, so a container init drops the bounding set first and only then hands capset masks with nothing set. Doing it the other way round leaves no CAP_SETPCAP to authorise the drop, and PR_CAPBSET_DROP fails with EPERM. Their effects compose: a bit removed from the bounding set can no longer be conferred by any binary the process executes, even one that carries that capability on disk.

struct __user_cap_header_struct hdr = {
    .version = _LINUX_CAPABILITY_VERSION_3, .pid = 0
};
struct __user_cap_data_struct data[2] = {{0}};  /* all sets empty */

/* The bounding drop needs CAP_SETPCAP, so it must run first. */
for (int cap = 0; cap <= CAP_LAST_CAP; cap++)
        if (prctl(PR_CAPBSET_DROP, cap, 0, 0, 0) < 0)
                die("capbset drop");

/* Then clear effective, permitted and inheritable together. */
if (capset(&hdr, data) < 0)
        die("capset");

What the model does not cover

Capabilities are checked at the syscall boundary, and the kernel’s own objects are not uniformly protected by them. A process with CAP_DAC_OVERRIDE bypasses file permission checks entirely, so a container that retains it can read any file reachable through its mount namespace regardless of mode bits. A process with CAP_SYS_PTRACE can attach to any process in its user namespace, which includes reading and writing memory. These are not leaks in the mechanism; they are the mechanism, and a container’s capability set is the real statement of what it may do.

The user namespace changes the meaning of a capability without changing its number. A process holding CAP_SYS_ADMIN in a container’s user namespace holds it only over objects owned by that namespace, as described in User Namespaces and the Mapping of UIDs. The same bit appears set in CapEff in /proc/<pid>/status whether the process is a real root or a namespaced one, and the difference is only visible through the owning namespace of the object being operated on.

A final limitation is that capabilities gate operations, not data. A process with no capabilities at all can still read every world-readable file, consume CPU, allocate memory within its cgroup limit, and send network traffic. The capability set is the authority axis; the resource axis is cgroups, and the visibility axis is namespaces. None of the three subsumes the others.

References

  1. capabilities(7) — Linux manual page — man7.org
  2. execve(2) — Linux manual page — man7.org
  3. prctl(2) — Linux manual page — man7.org