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 (asetcap’d binary acquiresCAP_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()insecurity/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) & 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 hasfI = 0, so this term is0. (Cross-reference: this is exactly the trap that ambient capabilities were invented to route around.)F(permitted) & P(bounding)— the file’s forced capabilitiesfP, masked by the bounding setcap_bset. Whatever the file demands, the bounding set can veto. A capability infPbut absent fromcap_bsetsimply does not appear. This is the term that gives asetcap cap_net_raw+epbinary itsCAP_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_setuidis literally!uid_eq(new->euid, old->uid).- The big
if ((is_setid || __cap_gained(...)) && (unsafe || !ptracer_capable))block is theno_new_privs/ unsafe-exec downgrade. If the exec would gain privilege (is_setidor__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 underno_new_privs), the kernel refuses to actually grant the privilege: it forceseuid = 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)— theP'(ambient) = 0when 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— theP'(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()) return—root_privileged()is!issecure(SECURE_NOROOT). If theSECURE_NOROOTsecurebit 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: setSECBIT_NOROOTand 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), socap_bset & ~0 = cap_bsetandpI & ~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 7The 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 viasetuid(), the kernel clears its permitted set (the “setuid fixup”). WithKEEP_CAPSset, 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 anexecve()”, and indeed the kernel does exactly that near the end ofcap_bprm_creds_from_file():new->securebits &= ~issecure_mask(SECURE_KEEP_CAPS);. SoKEEP_CAPSis 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 inhandle_privileged_root(), switches off the legacy grant so UID 0 and setuid-root binaries get no automatic capabilities. -
SECBIT_NO_CAP_AMBIENT_RAISE— forbidsprctl(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 hasfI = 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). execvereturnsEPERMfrom a capability-dumb binary.bprm_caps_from_vfs_caps()fails with-EPERMif 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 betweenhandle_privileged_root()and theis_setidassignment in the v6.12 source (e.g. additional securebits handling). Reason: the function was reconstructed from targeted fetches ofsecurity/commoncap.crather 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: readcap_bprm_creds_from_file()end-to-end at thev6.12tag. uncertain
See Also
- Capability Sets and the Bounding Set — the five per-thread sets and the bounding-set mask this formula consumes
- File Capabilities and Ambient Capabilities — the storage of
fP/fI/fEand the purpose of the ambient set (pA') that feed the transition - POSIX Capabilities — the parent concept of splitting root into grantable bits
- setuid setgid and the Sticky Bit — the legacy mechanism the root/setuid fixup emulates in capability terms
- no_new_privs and Privilege Escalation Control — the flag enforced inside the unsafe-exec downgrade block
- Linux Security MOC — parent map (§B, “Splitting Root — POSIX Capabilities”)