File Capabilities and Ambient Capabilities

File capabilities are the mechanism that lets an executable carry a subset of root’s power in an extended attribute instead of running with the whole of it via a setuid-root bit. The kernel stores them in the security.capability extended attribute (xattr), where a small binary structure records the file’s permitted and inheritable capability sets plus a single effective bit; on execve() the kernel folds those file sets into the new process’s capability sets. This is why /bin/ping can ship with just CAP_NET_RAW rather than setuid-root — it gets the one privilege it needs to open a raw ICMP socket and nothing else. Ambient capabilities (added in Linux 4.3, October 2015) close a different gap: they let a process pass capabilities across an execve() of an ordinary binary that has no file capabilities at all, so a non-root service manager can hand a privilege down to a child program without painting file caps on every binary or resorting to a setuid wrapper (LWN, “Inheriting capabilities”).

This note covers the two storage-and-propagation halves of the capability model: where file capabilities live on disk and how the namespace-aware xattr format works, and how the ambient set lets capabilities survive an exec without file caps. The exact arithmetic the kernel performs on execve() — the full P'(permitted) = … formula and its symbol-by-symbol walk — lives in the sibling note Capability Transitions Across execve; the per-thread sets (permitted, effective, inheritable, bounding, ambient) and their semantics are defined in Capability Sets and the Bounding Set and POSIX Capabilities. This note assumes those exist as ghost links and stands on its own for the storage and ambient mechanisms.

Version context: all code references are pinned to the Linux 6.12 LTS tree (released 2024-11-17); the common-capability LSM logic lives in security/commoncap.c and the on-disk structures in include/uapi/linux/capability.h, both fetched from the v6.12 tag during research.

Mental Model — Capability State Lives in Two Places

The single most important thing to internalise is that “a capability” can be recorded in two completely different kinds of storage, and the kernel merges them at execve() time:

  • On the file (persistent, on disk) — the security.capability xattr, holding the file’s permitted set fP, inheritable set fI, and effective bit fE. This is what setcap(8) writes.
  • On the thread (transient, in struct cred) — the five live sets the running task carries: permitted pP, effective pE, inheritable pI, bounding, and ambient pA.
flowchart LR
  subgraph DISK["On disk — security.capability xattr"]
    FP["fP (file permitted)<br/>fI (file inheritable)<br/>fE (effective bit)<br/>+ rootid (v3 only)"]
  end
  subgraph THREAD["Calling thread — struct cred"]
    PI["pI (inheritable)"]
    PA["pA (ambient)"]
    PBSET["bounding set"]
  end
  EXEC{{"execve()<br/>cap_bprm_creds_from_file()"}}
  FP --> EXEC
  PI --> EXEC
  PA --> EXEC
  PBSET --> EXEC
  EXEC --> NEW["New process<br/>pP', pE', pI', pA'"]

How a process’s post-exec capabilities are assembled. What it shows: the new credential set is a merge of file-side state (the xattr, written by setcap) and thread-side state (pI, pA, the bounding set the caller already holds). The insight to take: file capabilities and ambient capabilities are two independent inputs to the same merge — file caps let a binary carry privilege regardless of who runs it; ambient caps let a caller carry privilege regardless of what binary they run. They solve mirror-image problems.

File Capabilities — The security.capability Extended Attribute

Before file capabilities existed, the only way to let a normal user run a program with elevated privilege was the setuid bit: chmod u+s on a root-owned binary, and execve() would switch the process’s effective UID to 0, handing it all of root’s power for the duration. That is a blunt instrument — ping needed exactly one privilege (open a raw socket) but got the entire kernel. File capabilities replace that bit with a precise list.

The kernel stores the list in the security.capability extended attribute, a small fixed-layout binary structure defined in include/uapi/linux/capability.h (v6.12). The structures are:

struct vfs_cap_data {
	__le32 magic_etc;            /* Little endian */
	struct {
		__le32 permitted;    /* Little endian */
		__le32 inheritable;  /* Little endian */
	} data[VFS_CAP_U32];
};
 
struct vfs_ns_cap_data {
	__le32 magic_etc;
	struct {
		__le32 permitted;    /* Little endian */
		__le32 inheritable;  /* Little endian */
	} data[VFS_CAP_U32];
	__le32 rootid;
};

Walking the fields: magic_etc is a 32-bit word that packs two things — the high bits hold a revision magic number identifying the format version, and the low bit (VFS_CAP_FLAGS_EFFECTIVE, value 0x000001) is the effective bit fE. The data[] array holds the permitted and inheritable masks, one (permitted, inheritable) pair per 32-bit slice of the capability space. VFS_CAP_U32 is 2 (i.e. VFS_CAP_U32_3), so two pairs span the full 64-bit capability range (Linux has ~40 capabilities today, but the masks are sized for 64). The __le32 type and the “Little endian” comments are deliberate: the xattr is stored little-endian on disk regardless of host byte order, so the same filesystem image works on a big-endian machine.

The revision magic numbers are:

#define VFS_CAP_REVISION_1	0x01000000
#define VFS_CAP_REVISION_2	0x02000000
#define VFS_CAP_REVISION_3	0x03000000
#define VFS_CAP_FLAGS_EFFECTIVE	0x000001

VFS_CAP_REVISION_2 vs VFS_CAP_REVISION_3 — Why v3 Adds rootid

Revision 1 was the original 32-bit format (a single permitted/inheritable pair). It is obsolete because the capability space grew past 32 bits.

Revision 2 (since Linux 2.6.25) is the long-standing default: two 32-bit pairs covering the full 64-bit capability mask, no rootid field. Its size is XATTR_CAPS_SZ_2 = sizeof(__le32)*(1 + 2*VFS_CAP_U32_2) = 4 * (1 + 4) = 20 bytes. A v2 file cap is interpreted relative to the initial user namespace: only a process privileged in the host’s namespace (holding CAP_SETFCAP there) could write it, and it grants capabilities to anyone who executes the file.

Revision 3 (since Linux 4.14) adds the trailing __le32 rootid field, making file capabilities user-namespace-aware. Its size is XATTR_CAPS_SZ_3 = sizeof(__le32)*(2 + 2*VFS_CAP_U32_3) = 4 * (2 + 4) = 24 bytes — exactly four bytes more than v2, for the rootid. The rootid records which user namespace’s root the file caps belong to: it is the kuid (kernel UID) that maps to UID 0 in the namespace that wrote the attribute.

The motivation: inside an unprivileged user namespace, a process can be root in that namespace (UID 0) while being some unprivileged UID on the host. Before v3, such a process could not usefully set file capabilities — a v2 cap is host-global, and the kernel will not let a namespaced “root” mint host-wide privilege. With v3, when a process holding CAP_SETFCAP inside a non-initial user namespace writes the xattr, the kernel automatically encodes a v3 attribute stamping the rootid with that namespace’s root kuid (per capabilities(7)). The file caps then take effect only for processes in that namespace (or descendants where the rootid still maps).

The enforcement is in get_vfs_caps_from_disk() in security/commoncap.c (v6.12). When the kernel reads the xattr at exec time it switches on the revision and, for v3, resolves the rootid into the reading namespace:

rootkuid = make_kuid(fs_ns, 0);
switch (magic_etc & VFS_CAP_REVISION_MASK) {
case VFS_CAP_REVISION_1:
	if (size != XATTR_CAPS_SZ_1)
		return -EINVAL;
	break;
case VFS_CAP_REVISION_2:
	if (size != XATTR_CAPS_SZ_2)
		return -EINVAL;
	break;
case VFS_CAP_REVISION_3:
	if (size != XATTR_CAPS_SZ_3)
		return -EINVAL;
	rootkuid = make_kuid(fs_ns, le32_to_cpu(nscaps->rootid));
	break;
default:
	return -EINVAL;
}
 
rootvfsuid = make_vfsuid(idmap, fs_ns, rootkuid);
if (!vfsuid_valid(rootvfsuid))
	return -ENODATA;
 
if (!rootid_owns_currentns(rootvfsuid))
	return -ENODATA;

Reading this carefully: for v1/v2 the rootkuid stays UID 0 of the filesystem’s namespace; for v3 the kernel decodes the stored rootid into a kuid via make_kuid. It then checks rootid_owns_currentns(rootvfsuid)does the namespace this file cap belongs to own (i.e. is an ancestor of, or equal to) the current user namespace? If not, the function returns -ENODATA, which the caller treats exactly like “this file has no capabilities at all.” That is the whole point: a v3 file cap minted inside one user namespace is silently ignored in an unrelated namespace, so a container cannot forge capabilities that leak to the host or to a sibling container. The size check (size != XATTR_CAPS_SZ_3-EINVAL) also means a truncated or oversized xattr is rejected rather than misparsed.

Uncertain

Verify: the precise semantics of rootid_owns_currentns() — whether it accepts descendant namespaces of the rootid’s namespace, or strictly the same namespace. Reason: the helper body was not fetched verbatim during this task; the man7 text says a v3 cap retrieved “by a process whose namespace matches the stored rootid (or a descendant)” appears as v2, implying descendants are honoured, but the exact predicate in kernel/user_namespace.c/commoncap.c was not read. To resolve: read rootid_owns_currentns() in security/commoncap.c at the v6.12 tag. uncertain

When a v3 attribute is read back from the same namespace that wrote it (the rootid maps to UID 0), getxattr on security.capability presents it to userspace tools as if it were a v2 attribute, so getcap shows the familiar cap_net_raw+ep form without exposing the rootid plumbing.

Worked Example — ping with CAP_NET_RAW

The canonical use is the ping utility. Historically ping was setuid-root because it must open an AF_INET/SOCK_RAW socket to send ICMP echo packets, an operation gated by CAP_NET_RAW. With file capabilities the setuid bit is removed and replaced by:

# setcap cap_net_raw+ep /bin/ping
# getcap /bin/ping
/bin/ping cap_net_raw=ep

The +ep means: put CAP_NET_RAW in the file’s permitted set (p) and set the effective bit (e). On execve() the kernel folds fP = {CAP_NET_RAW} into the new process’s permitted set, and because the effective bit is set, into its effective set too — so ping starts with exactly one capability raised in pE, opens its raw socket, and (if well-written) drops it immediately. A bug in ping now leaks CAP_NET_RAW, not the entire root account (Unix etc., “Linux Capabilities and Ping”). The e flag exists precisely for capability-dumb binaries — programs not rewritten to call libcap and raise their own effective bits via capset(); setting fE makes all the file-permitted capabilities active in pE automatically, mimicking the old setuid-root “everything is already effective” behaviour.

There is one important guard. The function bprm_caps_from_vfs_caps() (v6.12) computes the permitted set and then checks that the process actually obtained everything the file demanded:

/* pP' = (X & fP) | (pI & fI) */
new->cap_permitted.val =
	(new->cap_bset.val & caps->permitted.val) |
	(new->cap_inheritable.val & caps->inheritable.val);
 
if (caps->permitted.val & ~new->cap_permitted.val)
	ret = -EPERM;
 
return *effective ? ret : 0;

Here X is the bounding set (new->cap_bset). If a capability is in the file’s permitted set fP but the bounding set masked it out — so it did not make it into pP' — and the file’s effective bit is set, the comparison caps->permitted.val & ~new->cap_permitted.val is non-zero and execve() fails with EPERM. The rationale: a capability-dumb binary assumes it will run with all its file-permitted caps effective; if the bounding set silently stripped one, the program would run believing it has a privilege it does not, which is more dangerous than refusing to start. (If the effective bit is not set, the file expects a capability-aware program that checks for itself, so the error is suppressed — return *effective ? ret : 0.)

Ambient Capabilities — Passing Privilege Without File Caps

File capabilities answer “how does this binary get a privilege regardless of who runs it?” They do not answer the mirror question: “how does this caller pass a privilege to whatever ordinary binary it execs?” Before Linux 4.3 the only thread-side tool for that was the inheritable set pI — and inheritable capabilities have a notorious trap.

Why Inheritable Capabilities Alone Fail

A capability in the caller’s inheritable set pI is only carried into the child’s permitted set if the executed file also lists it in its own inheritable set fI. The relevant term in the execve formula is (pI & fI) — the intersection of thread-inheritable and file-inheritable (the full formula is walked in Capability Transitions Across execve). An ordinary binary — /bin/bash, your own helper program — has an empty security.capability xattr, so fI is empty, so pI & fI is empty, so the child gets nothing. As LWN put it: “Without putting file capabilities on many different binaries throughout the system, there is no way to pass CAP_NET_RAW (or others …) down to child processes or new programs started with execve().” You would have to setcap an fI bit onto every binary you might exec — unworkable, and itself a privilege-granting operation requiring CAP_SETFCAP.

The concrete pain, from the LWN thread: Christoph Lameter ran a user-space network stack needing raw-socket privilege that “also may run arbitrary binary programs” and wanted to “restrict what those programs can do” while still granting a specific capability — without setuid and without tattooing file caps onto unknown binaries.

How the Ambient Set Fixes It

The ambient set pA (Linux 4.3, by Andy Lutomirski, commit 58319057b784, per kernelnewbies Linux_4.3) is a fifth per-thread capability set whose defining property is: it is preserved across an execve() of a non-privileged binary, and is added to the child’s permitted and effective sets. In formula terms, the ambient set feeds the execve transformation as P'(permitted) = … | P'(ambient) and P'(effective) = fE ? P'(permitted) : P'(ambient) — so even when the executed file contributes nothing, the ambient caps land in the child’s pP' and pE'. No file capabilities required.

Crucially, the ambient set obeys a strict invariant: a capability can be ambient only if it is in both the permitted set pP and the inheritable set pI. From capabilities(7): “The ambient capability set obeys the invariant that no capability can ever be ambient if it is not both permitted and inheritable.” This is what keeps ambient from being a privilege-escalation hole: you can only make ambient something you already hold (permitted) and already intended to pass down (inheritable). Dropping a cap from pP or pI automatically drops it from pA. The kernel enforces this everywhere — there is even a cap_ambient_invariant_ok() check asserted at the start and end of cap_bprm_creds_from_file().

The prctl(PR_CAP_AMBIENT, …) Interface

A thread manipulates its own ambient set through prctl(2) (per capabilities(7) and the documentation patch, LWN 662980):

/* Raise CAP_NET_RAW into the ambient set (must already be in pP and pI). */
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_RAW, 0, 0);
 
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_LOWER, CAP_NET_RAW, 0, 0);  /* remove one */
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_IS_SET, CAP_NET_RAW, 0, 0); /* query: 1/0 */
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);        /* wipe pA */

PR_CAP_AMBIENT_RAISE fails with EPERM if the capability is not in both pP and pI, or if the SECBIT_NO_CAP_AMBIENT_RAISE securebit is set (a one-way latch a process can flip to forbid any further ambient raising — useful right before execing untrusted code). PR_CAP_AMBIENT_LOWER and ..._CLEAR_ALL never fail on privilege grounds; lowering is always allowed.

A typical capability-only service launcher does: drop to a non-root UID, ensure CAP_NET_RAW is in pP and pI, prctl(PR_CAP_AMBIENT_RAISE, CAP_NET_RAW), then execve("/usr/bin/my-helper"). The helper — an ordinary binary with no file caps — runs with CAP_NET_RAW effective. This is the “capability-only service without a setuid wrapper” pattern the feature was built for.

When Ambient Is Wiped

The ambient set is cleared to zero by any execve() that crosses a privilege boundary — specifically, executing a setuid/setgid binary (one that changes UID/GID) or a binary carrying any file capabilities. The rule in cap_bprm_creds_from_file() (v6.12) is blunt:

/* File caps or setid cancels ambient. */
if (has_fcap || is_setid)
	cap_clear(new->cap_ambient);

The reasoning is a defence against confused-deputy escalation: if you exec a setuid-root program, the ambient caps you were carrying must not silently combine with the privileges the setuid transition grants — the new program might not expect them. So ambient is an “ordinary binaries only” channel; the moment a privileged exec happens, it is reset. This clearing is one of the conditions that also zeroes P'(ambient) in the canonical execve formula — see Capability Transitions Across execve for the full set-by-set derivation.

Failure Modes and Common Misunderstandings

  • setcap then getcap shows nothing. The most common cause is a filesystem mounted nosuid or one that does not support the security.* xattr namespace. cap_bprm_creds_from_file()’s helper get_file_caps() bails early on !mnt_may_suid(file->f_path.mnt) — a nosuid mount makes file capabilities (like setuid) a no-op. tmpfs, network filesystems, and overlay upper layers vary in xattr support.

  • File caps “disappear” on copy. Extended attributes are not preserved by cp without --preserve=xattr (or cp -a), by most archive tools without explicit flags, or across many filesystem-to-filesystem transfers. A deploy pipeline that copies a binary will strip its security.capability xattr and silently revert it to powerless. This is a frequent “works on my machine” bug.

  • Mixing setuid-root and file caps. If a binary is both setuid-root and carries file capabilities, the kernel prints a warning (warn_setuid_and_fcaps_mixed) and the legacy root path does not grant full capabilities — the file caps win. This combination is almost always a mistake; pick one model.

  • Expecting inheritable to “just work.” Setting pI and execing an ordinary binary and expecting the cap to survive is the classic trap dissected above — without fI on the file or an ambient raise, pI & fI is empty and the child gets nothing. Reach for ambient capabilities, not inheritable, when the target binary has no file caps.

  • v3 file caps ignored in a container. A file cap set inside one user namespace is -ENODATA (treated as absent) when read from an unrelated namespace, by design (rootid_owns_currentns). Operators sometimes set caps in one namespace and are surprised they have no effect in another. This is the security feature working, not a bug.

Alternatives and When to Choose Them

  • setuid-root bit — the legacy mechanism. Choose it only for genuinely capability-unaware programs where no finer model exists, and prefer file caps wherever the program needs a bounded privilege. setuid hands over all of root; file caps hand over a list. See setuid setgid and the Sticky Bit.
  • File capabilities (fP/fI/fE) — when a specific binary should always run with a fixed privilege regardless of caller (a network ping tool, a packet-capture helper). The privilege travels with the file.
  • Ambient capabilities — when a specific caller (a non-root daemon, a container init, a service manager) wants to grant a privilege to arbitrary child binaries it execs, without touching those binaries’ xattrs. The privilege travels with the process tree.
  • Inheritable capabilities — rarely used alone today; mostly meaningful in concert with file fI for a curated set of cooperating binaries.

Production Notes

Red Hat–family distributions ship ping with cap_net_raw+ep file caps rather than setuid by default; Debian/Ubuntu historically shipped it setuid and have moved toward file caps, with the exact state varying by release (per the search-cited Atomic/Debian discussions). systemd exposes ambient capabilities directly through the AmbientCapabilities= unit directive, which is the mainstream way administrators grant a daemon a capability without making it root or setcap’ing its binary — systemd raises the listed caps into pA before execve()ing the service. Container runtimes (runc, crun) configure the ambient and permitted sets from the OCI spec’s process.capabilities block; getting the ambient set right is what lets a non-root container process keep, say, CAP_NET_BIND_SERVICE.

Uncertain

Verify: the current default packaging state of ping (setuid vs file caps) on specific distro releases (Debian 12, Ubuntu 24.04, Fedora 40+). Reason: distro packaging is volatile and the search results describe historical states without a single authoritative current snapshot. To resolve: check the actual getcap/ls -l of /usr/bin/ping on each target distro release, or the package changelog. uncertain

See Also