Capability Transitions Across execve

When a Linux thread calls execve(2), the kernel does not simply carry its capability sets into the new program — it recomputes every set from a fixed formula that combines the thread’s pre-exec sets with the executable’s file capabilities and the bounding-set mask. This recomputation is the single most security-critical moment in the capability model: it is where privilege is gained (a setcap’d binary acquires CAP_NET_RAW), where it is lost (an ordinary binary drops everything not ambient), and where the legacy “setuid-root means full power” behaviour is grafted on. The whole transformation lives in one function, cap_bprm_creds_from_file() in security/commoncap.c (pinned here to Linux 6.12 LTS). This note walks the canonical formula symbol by symbol and traces the actual kernel code that implements it.

The five per-thread sets and the bounding set’s role as a mask are defined in Capability Sets and the Bounding Set; the storage of file capabilities (the security.capability xattr) and the ambient set’s purpose live in File Capabilities and Ambient Capabilities. This note is the mechanism note: given those inputs, exactly what comes out the other side of execve().

Mental Model — A Pure Function from Old State to New State

Think of the capability transition as a pure function. Its inputs are the thread’s pre-exec sets — permitted P, effective, inheritable I, bounding, ambient A — and the executable’s file sets — file-permitted fP, file-inheritable fI, and the single effective bit fE. Its outputs are the new thread’s sets, written P', I', etc. (the prime denoting “after exec”). The notation throughout: P is the thread set before exec, P' the same set after; F (or fP/fI/fE) is the file set; cap_bset is the bounding set.

flowchart TB
  subgraph IN["Inputs"]
    P["P(permitted)"]
    I["P(inheritable)"]
    A["P(ambient)"]
    B["P(bounding) = cap_bset"]
    FP["F(permitted) fP"]
    FI["F(inheritable) fI"]
    FE["F(effective) fE — one bit"]
  end
  PRIV{{"file privileged?<br/>(file caps OR setuid/setgid)"}}
  A --> PRIV
  PRIV -->|yes| AZ["P'(ambient) = 0"]
  PRIV -->|no| AK["P'(ambient) = P(ambient)"]
  AZ --> PP["P'(permitted) = (P(I) & F(I))<br/>| (F(P) &amp; cap_bset)<br/>| P'(ambient)"]
  AK --> PP
  I --> PP
  FI --> PP
  FP --> PP
  B --> PP
  PP --> PE["P'(effective) = F(E) ? P'(permitted) : P'(ambient)"]
  FE --> PE
  I --> IP["P'(inheritable) = P(inheritable) — unchanged"]
  B --> BP["P'(bounding) = P(bounding) — unchanged"]

The execve capability transformation as a dataflow. What it shows: four of the five outputs are computed; only inheritable and bounding pass through untouched. Ambient is the pivot — it is zeroed first if the file is “privileged,” and then feeds both permitted and effective. The insight to take: privilege on exec flows from exactly three sources — what the file forces (fP & cap_bset), what the thread chose to inherit into a cooperating binary (pI & fI), and what the thread made ambient (pA') — all three masked, ultimately, by the bounding set.

The Canonical Formula, Symbol by Symbol

From capabilities(7), the kernel computes:

P'(ambient)     = (file is privileged) ? 0 : P(ambient)
P'(permitted)   = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient)
P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)
P'(inheritable) = P(inheritable)    [unchanged]
P'(bounding)    = P(bounding)       [unchanged]

Take each line in turn.

P'(ambient) = (file is privileged) ? 0 : P(ambient). “File is privileged” means the executable either carries file capabilities or is setuid/setgid (changes the UID or GID). If so, the entire ambient set is discarded — 0. Otherwise (an ordinary, non-setuid, no-file-caps binary) the ambient set is preserved unchanged. This is the line that makes ambient capabilities a propagation channel for ordinary binaries only: cross any privilege boundary and the channel is reset. Because P'(ambient) is computed first, every later line can refer to it.

P'(permitted) = (P(inheritable) & F(inheritable)) | (F(permitted) & P(bounding)) | P'(ambient). This is the heart of the model. The new permitted set is the union (bitwise OR, |) of three contributions:

  • P(inheritable) & F(inheritable) — the intersection (bitwise AND, &) of what the thread was willing to inherit (pI) and what the file is willing to receive (fI). A capability passes this gate only if both sides list it. This is the term that makes plain inheritable capabilities so often useless: an ordinary binary has fI = 0, so this term is 0. (Cross-reference: this is exactly the trap that ambient capabilities were invented to route around.)
  • F(permitted) & P(bounding) — the file’s forced capabilities fP, masked by the bounding set cap_bset. Whatever the file demands, the bounding set can veto. A capability in fP but absent from cap_bset simply does not appear. This is the term that gives a setcap cap_net_raw+ep binary its CAP_NET_RAW.
  • P'(ambient) — the (possibly zeroed) ambient set, OR’d straight in.

P'(effective) = F(effective) ? P'(permitted) : P'(ambient). The file’s effective bit fE is a single bit, not a set. If it is set, all of the new permitted capabilities are also raised in effective — the binary starts ready to use them without calling capset() (this is the capability-dumb case, for programs not rewritten to manage their own effective set). If fE is clear, only the ambient capabilities are effective; a capability-aware program is expected to raise the rest itself from P'(permitted) via libcap.

P'(inheritable) = P(inheritable) and P'(bounding) = P(bounding). Both pass through unchanged. Inheritable is a deliberate choice the thread made before exec and the kernel does not touch it; the bounding set is a one-way-shrinking mask (you can drop from it but never add) that the kernel likewise leaves intact across exec. Their constancy is why the other formulas can reference P(inheritable) and P(bounding) as stable inputs.

The Bounding Set as the Master Mask

The bounding set cap_bset is the ceiling on what any exec can grant. It appears explicitly in F(permitted) & P(bounding) — file-permitted caps are AND’d with it — so a capability dropped from the bounding set can never be regained by execing a file that lists it, no matter how privileged. This is what makes dropping bounding-set bits a durable sandbox: it shrinks the universe of grantable capabilities for the process and all its descendants. The bounding set is detailed in Capability Sets and the Bounding Set; here the key fact is its role as the &-mask in the permitted-set formula and its invariance (P'(bounding) = P(bounding)).

There is a subtlety the formula above hides: the bounding set also constrains the (P(inheritable) & F(inheritable)) term indirectly. You can only have a capability in your inheritable set if it was permitted to put there, and the bounding set limited what you could ever raise — so in practice every contribution to P'(permitted) is bounded.

Walking the Kernel Code — cap_bprm_creds_from_file()

The man-page formula is the specification; cap_bprm_creds_from_file() (security/commoncap.c, v6.12) is the implementation. The function entry and file-cap extraction:

int cap_bprm_creds_from_file(struct linux_binprm *bprm, const struct file *file)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;
	bool effective = false, has_fcap = false, is_setid;
	int ret;
	kuid_t root_uid;
 
	if (WARN_ON(!cap_ambient_invariant_ok(old)))
		return -EPERM;
 
	ret = get_file_caps(bprm, file, &effective, &has_fcap);
	if (ret < 0)
		return ret;
 
	root_uid = make_kuid(new->user_ns, 0);
	handle_privileged_root(bprm, has_fcap, &effective, root_uid);

get_file_caps() reads the security.capability xattr and (via bprm_caps_from_vfs_caps()) sets new->cap_permitted = (cap_bset & fP) | (pI & fI), sets effective from fE, and sets has_fcap if the file had any caps. That single line already realises the first two terms of P'(permitted). handle_privileged_root() then layers the legacy root fixup on top (next section). After that, the new effective and ambient sets are finalised:

	/* if we have fs caps, clear dangerous personality flags */
	if (__cap_gained(permitted, new, old))
		bprm->per_clear |= PER_CLEAR_ON_SETID;
 
	is_setid = __is_setuid(new, old) || __is_setgid(new, old);
 
	if ((is_setid || __cap_gained(permitted, new, old)) &&
	    ((bprm->unsafe & ~LSM_UNSAFE_PTRACE) ||
	     !ptracer_capable(current, new->user_ns))) {
		/* downgrade; they get no more than they had, and maybe less */
		if (!ns_capable(new->user_ns, CAP_SETUID) ||
		    (bprm->unsafe & LSM_UNSAFE_NO_NEW_PRIVS)) {
			new->euid = new->uid;
			new->egid = new->gid;
		}
		new->cap_permitted = cap_intersect(new->cap_permitted,
						   old->cap_permitted);
	}
 
	new->suid = new->fsuid = new->euid;
	new->sgid = new->fsgid = new->egid;
 
	/* File caps or setid cancels ambient. */
	if (has_fcap || is_setid)
		cap_clear(new->cap_ambient);
 
	/*
	 * Now that we've computed pA', update pP' to give:
	 *   pP' = (X & fP) | (pI & fI) | pA'
	 */
	new->cap_permitted = cap_combine(new->cap_permitted, new->cap_ambient);
 
	if (effective)
		new->cap_effective = new->cap_permitted;
	else
		new->cap_effective = new->cap_ambient;

Reading this against the formula:

  • is_setid = __is_setuid(new, old) || __is_setgid(new, old) — detects whether the UID or GID changed (a setuid/setgid binary). __is_setuid is literally !uid_eq(new->euid, old->uid).
  • The big if ((is_setid || __cap_gained(...)) && (unsafe || !ptracer_capable)) block is the no_new_privs / unsafe-exec downgrade. If the exec would gain privilege (is_setid or __cap_gained(permitted, …) — the new permitted is not a subset of the old) and the exec is “unsafe” (e.g. being ptraced without permission, or running under no_new_privs), the kernel refuses to actually grant the privilege: it forces euid = uid, egid = gid, and intersects the new permitted set with the old one (cap_intersect), so “they get no more than they had, and maybe less.” This is the mechanism by which [[no_new_privs and Privilege Escalation Control|no_new_privs]] guarantees a child can never exceed its parent — it neuters both setuid and file-cap escalation at this exact point.
  • if (has_fcap || is_setid) cap_clear(new->cap_ambient) — the P'(ambient) = 0 when file-privileged rule, verbatim.
  • new->cap_permitted = cap_combine(new->cap_permitted, new->cap_ambient) — OR the (now possibly zeroed) ambient set into permitted: the | pA' term.
  • effective ? new->cap_permitted : new->cap_ambient — the P'(effective) = fE ? P'(permitted) : P'(ambient) line, exactly.

Note what is absent: there is no line touching new->cap_inheritable or new->cap_bset. They are inherited from the credential the exec started with — P'(inheritable) = P(inheritable), P'(bounding) = P(bounding) — confirming the “unchanged” lines of the formula by omission.

The Legacy root / setuid-to-root Fixup

The capability model has to coexist with decades of software that predates it and assumes the old UNIX rule: UID 0 (or a setuid-root binary) gets everything. handle_privileged_root() implements that compatibility shim:

static void handle_privileged_root(struct linux_binprm *bprm,
				   bool has_fcap, bool *effective, kuid_t root_uid)
{
	const struct cred *old = current_cred();
	struct cred *new = bprm->cred;
 
	if (!root_privileged())
		return;
 
	if (has_fcap && __is_suid(root_uid, new)) {
		warn_setuid_and_fcaps_mixed(bprm->filename);
		return;
	}
 
	if (__is_eff(root_uid, new) || __is_real(root_uid, new)) {
		/* pP' = (cap_bset & ~0) | (pI & ~0) */
		new->cap_permitted = cap_combine(old->cap_bset,
						 old->cap_inheritable);
	}
 
	if (__is_eff(root_uid, new))
		*effective = true;
}

Walking it:

  • if (!root_privileged()) returnroot_privileged() is !issecure(SECURE_NOROOT). If the SECURE_NOROOT securebit is set, the legacy root grant is disabled entirely and this function does nothing. That is how you build a “root is not special” environment: set SECBIT_NOROOT and even UID 0 gets only the capabilities explicitly assigned.
  • The setuid-root-and-file-caps case warns and bails (file caps win; do not also apply the legacy grant).
  • if (__is_eff(root_uid, new) || __is_real(root_uid, new)) — if the new effective UID or the real UID is 0 (i.e. a setuid-root binary, or root running anything), the legacy grant fires: new->cap_permitted = old->cap_bset | old->cap_inheritable. The comment spells out the meaning — it is the general formula (cap_bset & fP) | (pI & fI) with the file sets treated as all-ones (fP = fI = ~0), so cap_bset & ~0 = cap_bset and pI & ~0 = pI. In other words, a setuid-root program behaves as if every file capability bit were set. That is precisely the old “root gets everything (within the bounding set)” rule, re-expressed in capability terms.
  • if (__is_eff(root_uid, new)) *effective = true — and if the effective UID is 0, set the effective flag, so all those permitted caps are also effective. A setuid-root binary thus starts with full effective capabilities, exactly as before capabilities existed.

The crucial detail is the bounding set still applies: the legacy grant is old->cap_bset | …, so even root cannot exceed the bounding set. Dropping a bit from the bounding set restricts root too — which is what makes the bounding set a real confinement primitive.

Securebits — Overriding the Default Transitions

The transition rules above can be tuned per-process by the securebits flags in include/uapi/linux/securebits.h (v6.12). Each is a bit position, paired with a _LOCKED companion one position higher that latches the setting irreversibly:

#define issecure_mask(X)		(1 << (X))
#define SECURE_NOROOT			0
#define SECURE_NOROOT_LOCKED		1
#define SECURE_NO_SETUID_FIXUP		2
#define SECURE_NO_SETUID_FIXUP_LOCKED	3
#define SECURE_KEEP_CAPS		4
#define SECURE_KEEP_CAPS_LOCKED		5
#define SECURE_NO_CAP_AMBIENT_RAISE	6
#define SECURE_NO_CAP_AMBIENT_RAISE_LOCKED 7

The two that most affect execve and the surrounding credential changes (per capabilities(7)):

  • SECBIT_KEEP_CAPS (SECURE_KEEP_CAPS) — normally, when a process with UID 0 switches all its UIDs to non-zero via setuid(), the kernel clears its permitted set (the “setuid fixup”). With KEEP_CAPS set, the permitted set is retained across that UID switch — the idiom for a root process that wants to drop UID but keep specific capabilities. The man page notes it is “always cleared on an execve(), and indeed the kernel does exactly that near the end of cap_bprm_creds_from_file(): new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);. So KEEP_CAPS is a one-shot, pre-exec tool.

  • SECBIT_NO_SETUID_FIXUP (SECURE_NO_SETUID_FIXUP) — disables the kernel’s automatic adjustment of the permitted, effective, and ambient sets when the effective/filesystem UID transitions between zero and non-zero. Used by programs that manage their own capabilities and do not want the legacy “switching UID changes your caps” behaviour interfering.

  • SECBIT_NOROOT (SECURE_NOROOT) — as seen in handle_privileged_root(), switches off the legacy grant so UID 0 and setuid-root binaries get no automatic capabilities.

  • SECBIT_NO_CAP_AMBIENT_RAISE — forbids prctl(PR_CAP_AMBIENT_RAISE); affects the ambient set’s manipulation rather than the exec transition directly (see File Capabilities and Ambient Capabilities).

The _LOCKED variant of each, once set, cannot be unset, so a process can drop into (say) a no-root-privilege regime and guarantee neither it nor its children can climb back out. SECURE_ALL_BITS and SECURE_ALL_LOCKS are the composite masks of all four bits and all four locks respectively.

Failure Modes and Common Misunderstandings

  • “I set inheritable, why did my privilege vanish?” Because P'(permitted) includes (pI & fI), and an ordinary binary has fI = 0. The inheritable set is nearly inert without either file-inheritable bits on the target or an ambient raise. This is the most common capability confusion and the reason ambient capabilities exist (LWN).
  • execve returns EPERM from a capability-dumb binary. bprm_caps_from_vfs_caps() fails with -EPERM if the file’s effective bit is set but the bounding set masked out a capability the file demanded (caps->permitted & ~new->cap_permitted). The binary cannot start because it would run believing it has a privilege it does not. Either add the cap to the bounding set or do not mark the file effective.
  • Surprise privilege drop under ptrace. The unsafe-exec downgrade block intersects new permitted with old and resets euid/egid if the exec is being traced without permission or under no_new_privs. A debugger attaching to a setuid binary will see it run without the privilege escalation — by design, to stop a tracer from hijacking elevated credentials.
  • Ambient silently gone after exec. Expected: any setuid/setgid or file-cap exec clears pA. If you need a capability to survive into a privileged child, ambient is the wrong channel.

Alternatives and When to Choose Them

The execve transition is not a thing you choose; it is the universal mechanism. What you choose is which input to use: file capabilities (privilege travels with the binary — File Capabilities and Ambient Capabilities), ambient capabilities (privilege travels with the process tree), or the legacy setuid-root bit (all-or-nothing — setuid setgid and the Sticky Bit). The transition formula then determines the outcome deterministically. The no_new_privs flag and securebits are how you constrain the transition to be non-escalating.

Production Notes

Understanding this transition is what separates a working least-privilege daemon from a silently-broken one. A typical hardened service: drop the bounding set to just the capabilities needed (prctl(PR_CAPBSET_DROP, …) for each unwanted cap), set the needed caps in inheritable and permitted, raise them into ambient, set no_new_privs, then execve() the (capability-unaware) service binary — which inherits exactly those caps via the | pA' term while every escalation path is masked by the bounding set and no_new_privs. systemd’s CapabilityBoundingSet=, AmbientCapabilities=, and NoNewPrivileges= directives drive precisely these knobs around the exec. Container runtimes compute the OCI capability sets into the same fields before execve()ing the container entrypoint.

Uncertain

Verify: that no relevant lines of cap_bprm_creds_from_file() were elided between handle_privileged_root() and the is_setid assignment in the v6.12 source (e.g. additional securebits handling). Reason: the function was reconstructed from targeted fetches of security/commoncap.c rather than read top-to-bottom in one pass; the quoted blocks are verbatim but their exact ordering and any short intervening lines should be confirmed. To resolve: read cap_bprm_creds_from_file() end-to-end at the v6.12 tag. uncertain

See Also