BPF Token and Privilege Delegation

A BPF token is a kernel object — represented as a file descriptor — that lets a privileged process grant a precisely scoped subset of bpf() syscall power to an unprivileged, user-namespaced process, so that BPF workloads can run inside unprivileged containers without ever holding real CAP_BPF on the host. The problem it solves is structural: CAP_BPF and the other BPF-relevant capabilities are checked against the initial user namespace and “cannot be namespaced or sandboxed, as a general rule” (Andrii Nakryiko’s patch set cover letter, LWN), so a process inside a user namespace that nominally “has CAP_BPF” still fails every BPF capability check. The token mechanism, merged in Linux 6.9 (the file kernel/bpf/token.c first appears at the v6.9 tag and is absent at v6.8 — verified by fetching both), breaks this deadlock: a container manager mounts a bpffs (the BPF pseudo-filesystem) with delegate_* mount options that enumerate exactly which commands, map types, program types, and attach types it is willing to delegate; an unprivileged process inside that namespace then issues BPF_TOKEN_CREATE against that mount to obtain a token fd, and passes that fd into subsequent bpf() calls. The kernel then performs its capability checks against the bpffs’s owning user namespace instead of the init namespace, but only for operations the mount whitelisted (token.c, bpf_token_capable/bpf_token_create, v6.12). This note pins every mechanism claim to Linux 6.12 LTS source.

The token is the modern, forward path for the unprivileged-BPF question; it does not re-enable the legacy “any user can load a socket filter” path (see Unprivileged BPF and Its Restrictions) and it is orthogonal to the CAP_BPF capability split — instead of changing what CAP_BPF means, it changes which namespace the check is evaluated in, gated by an explicit, host-administrator-authored delegation policy.

Mental Model

Think of a BPF token as a scoped, delegable capability voucher minted by the host and redeemed by a container. The host administrator (or container manager such as systemd) decides, at bpffs mount time, the full set of BPF operations it is prepared to delegate, and bakes that into four 64-bit bitmasks attached to the filesystem superblock. Any process in the matching user namespace that can reach the mount can ask for a token; the token simply copies those four bitmasks into a refcounted kernel object and hands back a file descriptor. From then on, when a held-token fd is presented to bpf(), the kernel treats the caller as if it held the relevant capability in the bpffs’s user namespace — but it first checks the operation against the token’s bitmask, and silently ignores the token (falling back to ordinary, init-namespace capability checks, which will normally fail for an unprivileged container) if the operation was not delegated.

flowchart TB
  subgraph HOST["Host / init user namespace (privileged)"]
    MGR["Container manager (systemd, etc.)<br/>holds CAP_SYS_ADMIN"]
    MNT["mount -t bpf bpffs /sys/fs/bpf/cont<br/>-o delegate_cmds=prog_load:map_create<br/>-o delegate_progs=xdp -o delegate_maps=ringbuf<br/>-o delegate_attachs=xdp"]
    MGR --> MNT
  end
  subgraph NS["Unprivileged container (own user namespace)"]
    OPEN["open(bpffs root) -> bpffs_fd"]
    TOK["bpf(BPF_TOKEN_CREATE, {bpffs_fd}) -> token_fd"]
    LOAD["bpf(BPF_PROG_LOAD, {<br/>prog_flags|=BPF_F_TOKEN_FD,<br/>prog_token_fd=token_fd})"]
    OPEN --> TOK --> LOAD
  end
  MNT -. "superblock holds<br/>delegate_* bitmasks" .-> TOK
  TOK -. "token copies<br/>4 bitmasks + userns" .-> LOAD
  LOAD --> CHK["kernel: bpf_token_allow_cmd()<br/>+ bpf_token_allow_prog_type()<br/>+ bpf_token_capable(token, CAP_BPF)<br/>checked in bpffs userns"]

The delegation flow end to end. What it shows: the host mounts a bpffs and authors a delegation policy as delegate_* options (which become superblock bitmasks); the unprivileged container opens that mount, calls BPF_TOKEN_CREATE to mint a token fd carrying those bitmasks plus the owning user namespace, then presents the token on each bpf() command via the BPF_F_TOKEN_FD flag and a *_token_fd field. The insight to take: the host never grants blanket CAP_BPF to the container — it grants a specific, enumerated slice, and the kernel re-evaluates capability checks in the container’s user namespace only for that slice. Everything outside the whitelist still hits the old init-namespace checks and is denied.

Mechanical Walk-through

Step 1 — the host authors a delegation policy on a bpffs mount

bpffs (mounted with -t bpf, conventionally at /sys/fs/bpf) gained four new mount options whose values are parsed in bpf_parse_param() in kernel/bpf/inode.c: delegate_cmds, delegate_maps, delegate_progs, and delegate_attachs. Each is declared as an fsparam_string and parsed into a u64 bitmask stored on the mount’s options (struct bpf_mount_opts, fields delegate_cmds/delegate_maps/delegate_progs/delegate_attachs).

The value syntax is a colon-separated list, and each element is resolved three ways in priority order (from the parsing loop, while ((p = strsep(&str, ":")))):

  1. the literal string any sets all bits (msk |= ~0ULL) — delegate everything in that category;
  2. a BTF enum constant short name — the parser looks the name up in the kernel’s own BTF, with a category prefix supplied by the option: BPF_ for commands and attach types, BPF_MAP_TYPE_ for maps, BPF_PROG_TYPE_ for programs (find_btf_enum_const(info.btf, enum_t, enum_pfx, p, &val)msk |= 1ULL << val). So delegate_cmds=prog_load resolves against the bpf_cmd enum as BPF_PROG_LOAD, and delegate_progs=xdp resolves against bpf_prog_type as BPF_PROG_TYPE_XDP;
  3. anything that is neither any nor a known enum name is parsed as a raw integer via kstrtou64(p, 0, &msk) — so a literal hex mask like delegate_cmds=0x1 also works (this is the fallback when BTF is unavailable).

Crucially, setting these options is itself privileged: if (msk && !capable(CAP_SYS_ADMIN)) return -EPERM;. Only a CAP_SYS_ADMIN-holding process can author the policy — which is exactly the host/container-manager side. The four masks are independent and additive across repeated options. When the filesystem is later shown via /proc/.../mountinfo, seq_print_delegate_opts() re-renders the bits back into readable enum names (or any, or a residual 0x... for unknown bits).

Step 2 — the unprivileged process mints a token with BPF_TOKEN_CREATE

The container opens the bpffs root directory to get a file descriptor, then issues bpf(BPF_TOKEN_CREATE, &attr, size) with attr.token_create.bpffs_fd set to that fd (the UAPI struct is struct { __u32 flags; __u32 bpffs_fd; } token_create;, include/uapi/linux/bpf.h, v6.12). The kernel handler bpf_token_create() (token.c, v6.12) enforces a chain of guards, each of which is worth understanding:

  • It must be a bpffs root. if (path.dentry != sb->s_root) return -EINVAL; and if (sb->s_op != &bpf_super_ops) return -EINVAL; — you can only create a token from the root of a real bpffs superblock, not from an arbitrary directory or another filesystem. path_permission(&path, MAY_ACCESS) then checks the caller can actually access it.
  • Same user namespace. if (current_user_ns() != userns) return -EPERM; where userns = sb->s_user_ns. The token creator must be in the same user namespace the bpffs was mounted in. The comment in source is explicit: “Enforce that creators of BPF tokens are in the same user namespace as the BPF FS instance. This makes reasoning about permissions a lot easier and we can always relax this later.”
  • CAP_BPF in that namespace. if (!ns_capable(userns, CAP_BPF)) return -EPERM; — the creator needs CAP_BPF relative to the bpffs’s user namespace, which an unprivileged-but-namespaced process typically does hold inside its own user namespace (the whole point: namespace-local capabilities are real inside the namespace, they just don’t reach the init namespace).
  • Not the init namespace. if (current_user_ns() == &init_user_ns) return -EOPNOTSUPP;“Creating BPF token in init_user_ns doesn’t make much sense” (you’d already have real capabilities there).
  • Some delegation must be set. If all four delegate_* masks are zero, return -ENOENT; /* no BPF token delegation is set up */ — a bpffs with no delegation policy cannot mint tokens.

If all guards pass, the kernel allocates a struct bpf_token, pins the owning user namespace (token->userns = get_user_ns(userns)), and copies the four superblock masks into the token:

token->allowed_cmds    = mnt_opts->delegate_cmds;
token->allowed_maps    = mnt_opts->delegate_maps;
token->allowed_progs   = mnt_opts->delegate_progs;
token->allowed_attachs = mnt_opts->delegate_attachs;

It then wraps the token in a pseudo-file (alloc_file_pseudo, fops bpf_token_fops) and installs a fresh O_CLOEXEC fd, returning it. The token is a kernel object with its own refcount; closing the fd drops a reference and the object is freed once the last reference goes (deferred to a workqueue, bpf_token_put_deferred).

Step 3 — the process redeems the token on each bpf() command

To use the token, the caller sets the BPF_F_TOKEN_FD flag (value (1U << 16), uapi bpf.h) in the command’s flags field and puts the token fd in a dedicated per-command field. There are three of these in 6.12: prog_token_fd (for BPF_PROG_LOAD, set prog_flags |= BPF_F_TOKEN_FD), map_token_fd (for BPF_MAP_CREATE, set map_flags |= BPF_F_TOKEN_FD), and btf_token_fd (for BPF_BTF_LOAD, set btf_flags |= BPF_F_TOKEN_FD). There is no single global token field — each command that supports delegation carries its own, because each command consults different subsets of the token’s bitmasks.

On the BPF_PROG_LOAD path in kernel/bpf/syscall.c the sequence is:

if (attr->prog_flags & BPF_F_TOKEN_FD) {
    token = bpf_token_get_from_fd(attr->prog_token_fd);
    if (IS_ERR(token))
        return PTR_ERR(token);
    /* if current token doesn't grant prog loading permissions,
     * then we can't use this token, so ignore it and rely on
     * system-wide capabilities checks
     */
    if (!bpf_token_allow_cmd(token, BPF_PROG_LOAD) ||
        !bpf_token_allow_prog_type(token, attr->prog_type,
                                   attr->expected_attach_type)) {
        bpf_token_put(token);
        token = NULL;
    }
}
bpf_cap = bpf_token_capable(token, CAP_BPF);

Two checks gate whether the token is honored. bpf_token_allow_cmd(token, BPF_PROG_LOAD) tests token->allowed_cmds & BIT_ULL(BPF_PROG_LOAD) (plus an LSM hook, security_bpf_token_cmd). bpf_token_allow_prog_type(token, prog_type, attach_type) tests both token->allowed_progs & BIT_ULL(prog_type) and token->allowed_attachs & BIT_ULL(attach_type) — so loading an XDP program through a token requires the mount to have delegated both delegate_progs=xdp and delegate_attachs=xdp. If either check fails, the token is dropped and the load proceeds without a token, meaning the ordinary capability checks run against the init namespace and an unprivileged container is denied.

The decisive line is bpf_cap = bpf_token_capable(token, CAP_BPF). bpf_token_capable() (token.c) chooses the namespace to check against based on the token: userns = token ? token->userns : &init_user_ns;. With a valid token it evaluates ns_capable(token->userns, CAP_BPF) (via the bpf_ns_capable helper, which also accepts CAP_SYS_ADMIN as a superset) — i.e. it asks “does this process hold CAP_BPF in the bpffs’s user namespace?”, which a namespaced container does. Without a token it falls back to the init namespace and the unprivileged container fails. The same token-aware check is applied for the per-program-type extra capabilities: is_net_admin_prog_type requires bpf_token_capable(token, CAP_NET_ADMIN), is_perfmon_prog_type requires bpf_token_capable(token, CAP_PERFMON). The map-create and BTF-load paths follow the identical pattern with BPF_MAP_CREATE/bpf_token_allow_map_type and BPF_BTF_LOAD respectively.

The net effect is a clean delegation contract: the host enumerates a whitelist; the kernel, for whitelisted operations only, re-roots its capability checks in the container’s user namespace; everything else is denied exactly as before.

Configuration / Code Walk-through

A realistic end-to-end setup. Host side — the container manager mounts a per-container bpffs and authors a tight delegation policy:

# Host, privileged (CAP_SYS_ADMIN). Delegate only what this container needs:
#   - the two commands map_create and prog_load
#   - only RINGBUF maps
#   - only XDP programs, only the XDP attach type
mount -t bpf bpffs /run/container42/bpf \
  -o delegate_cmds=map_create:prog_load \
  -o delegate_maps=ringbuf \
  -o delegate_progs=xdp \
  -o delegate_attachs=xdp

Each option is colon-separated; the strings map_create, prog_load, ringbuf, xdp are BTF enum short-names resolved against bpf_cmd, bpf_map_type, and bpf_prog_type/bpf_attach_type respectively. Reading back /proc/self/mountinfo shows the rendered masks. Because msk && !capable(CAP_SYS_ADMIN) rejects non-admins, an unprivileged container cannot widen its own policy by remounting.

Container side — the unprivileged, user-namespaced process mints and uses a token. With libbpf this is a one-liner at object-open time; the underlying syscalls are:

/* 1. Open the delegated bpffs root and create a token. */
int bpffs_fd = open("/run/container42/bpf", O_RDONLY);
union bpf_attr cattr = {};
cattr.token_create.bpffs_fd = bpffs_fd;
int token_fd = syscall(__NR_bpf, BPF_TOKEN_CREATE, &cattr, sizeof(cattr));
 
/* 2. Create a RINGBUF map *through* the token. */
union bpf_attr mattr = {};
mattr.map_type     = BPF_MAP_TYPE_RINGBUF;
mattr.max_entries  = 1 << 20;
mattr.map_flags    = BPF_F_TOKEN_FD;   /* opt in */
mattr.map_token_fd = token_fd;         /* present the voucher */
int map_fd = syscall(__NR_bpf, BPF_MAP_CREATE, &mattr, sizeof(mattr));
 
/* 3. Load an XDP program through the same token. */
union bpf_attr pattr = {};
pattr.prog_type      = BPF_PROG_TYPE_XDP;
pattr.expected_attach_type = BPF_XDP;
/* ... insns, license, etc. ... */
pattr.prog_flags     = BPF_F_TOKEN_FD;
pattr.prog_token_fd  = token_fd;
int prog_fd = syscall(__NR_bpf, BPF_PROG_LOAD, &pattr, sizeof(pattr));

With libbpf, set LIBBPF_BPF_OBJECT_OPEN_OPTS’s bpf_token_path (or let libbpf auto-detect /sys/fs/bpf delegation) and the loader threads the token fd into every command for you. The conceptual model is unchanged: open bpffs → BPF_TOKEN_CREATE → flag + *_token_fd on each command.

A useful debugging aid: the token fd implements show_fdinfo, so cat /proc/<pid>/fdinfo/<token_fd> prints allowed_cmds, allowed_maps, allowed_progs, allowed_attachs as either any or a hex mask (bpf_token_show_fdinfo, token.c) — the exact slice this token can redeem.

Failure Modes and Common Misunderstandings

  • BPF_TOKEN_CREATE returns -ENOENT. The bpffs has no delegation set up — all four delegate_* masks are zero. Mount with at least one delegated category.
  • -EPERM from BPF_TOKEN_CREATE. Either you are not in the bpffs’s user namespace (current_user_ns() != sb->s_user_ns), or you lack CAP_BPF in that namespace. The token creator is expected to be a namespaced process that holds namespace-local CAP_BPF.
  • -EOPNOTSUPP. You tried to create a token from the init user namespace — that is intentionally disallowed; tokens are only meaningful inside a child user namespace.
  • The token is silently ignored, and the load fails with -EPERM/-EACCES anyway. This is the most confusing case. If the requested command/prog-type/attach-type is not in the token’s bitmask, the kernel does not error on the token — it drops the token (token = NULL) and re-runs the ordinary capability check, which an unprivileged container fails. So a too-narrow delegate_* policy presents as a generic permission denial, not as a “token rejected” message. Diagnose with fdinfo to see what the token actually grants, and compare against the prog/map/attach types you are loading. Remember bpf_token_allow_prog_type needs both delegate_progs and delegate_attachs to include your type.
  • Delegation does not bypass the verifier. A token relaxes who may load, not what may be loaded. The verifier still runs in full, and an unprivileged-equivalent caller is still subject to the stricter verifier mode (e.g. Spectre masking and pointer-leak restrictions) unless the token also delegates the relevant trust — see the cross-link below.
  • Mistaking the token for a global capability. A token is per-fd and per-bitmask. There is no “I have a token, therefore I can do all BPF” — each command checks its own slice, and a token that grants prog_load for XDP grants nothing for, say, kprobe programs or hash maps unless those bits were delegated too.

Security Properties and the Spectre Tie-In

The token’s safety rests on the host’s policy being the only way to widen scope: only CAP_SYS_ADMIN can set delegate_* at mount time, and the unprivileged side can only ever narrow (it cannot remount to add bits). The design is also explicitly composable with LSMs — every allow-check (bpf_token_allow_cmd, bpf_token_capable, bpf_token_create) calls a corresponding security_bpf_token_* hook, so a BPF-LSM or SELinux policy can further restrict (never widen) what a token permits (token.c; design rationale in LWN).

A subtle and important interaction: because bpf_token_capable() re-roots capability checks in the token’s user namespace, a token that delegates enough trust can change how hardening is applied. The verifier derives its speculative-execution-mitigation posture from bpf_bypass_spec_v1(token) and bpf_bypass_spec_v4(token), which resolve to bpf_token_capable(token, CAP_PERFMON) (include/linux/bpf.h, v6.12). So a token whose owning namespace grants CAP_PERFMON can let a program skip the Spectre-v1 pointer masking that an unprivileged program would otherwise receive — meaning delegation is not only an access-control decision but, indirectly, a hardening decision. The mechanics of those mitigations live in BPF and Spectre Hardening; the point here is that the token is the object that carries the trust level into that decision.

Alternatives and When to Choose Them

  • Run the workload as real root / with CAP_BPF on the host. Simplest, but it gives the container blanket BPF power over the whole host — the exact outcome the token avoids. Appropriate only for fully trusted system daemons; see CAP_BPF and BPF Privilege Model.
  • Enable unprivileged BPF (unprivileged_bpf_disabled=0). The legacy path — lets any user load a restricted set of program types (socket filters, cgroup/skb) with heavy verifier restrictions. It is disabled by default on modern kernels after a string of Spectre-class verifier-bypass CVEs, and it is not namespaced or scopeable. Tokens are strictly more controllable. See Unprivileged BPF and Its Restrictions.
  • Load BPF in a privileged sidecar / agent and hand fds to the container. A common pre-token pattern: a privileged helper loads and pins the programs/maps, and the unprivileged container only uses the resulting fds. Still valid, but it forces all loading logic into the privileged component; tokens let the container’s own code do the loading under a scoped grant, which is more flexible for self-contained BPF applications.
  • LSM-only delegation. Before tokens, proposals leaned on LSM hooks alone to permit unprivileged loads. The cover letter notes tokens are “not changing anything about LSM approach, but can be combined with LSM hooks for very fine-grained security policy” (LWN) — tokens give the coarse, mount-time whitelist; LSM gives the fine, per-event policy on top.

Production Notes

BPF tokens are the enabling mechanism for running BPF-using workloads (observability agents, custom networking, security tooling) inside unprivileged, user-namespaced containers — the design was driven explicitly by the container-management use case (“delegate a subset of BPF functionality from privileged system-wide daemon (e.g., systemd or any other container manager) to a trusted unprivileged application”, LWN). Because the feature is relatively new (6.9, May 2024), adoption is gated on userspace support: libbpf gained token plumbing (bpf_token_path/auto-detection in object-open options) and bpftool/runtime support landed alongside it. As of the 6.12/6.18 LTS era the three delegating commands are BPF_PROG_LOAD, BPF_MAP_CREATE, and BPF_BTF_LOAD (the *_token_fd fields present in the UAPI); other commands continue to follow the ordinary capability model.

Uncertain

Verify: the exact userspace tooling versions (libbpf, systemd, container-runtime) that consume BPF tokens, and whether additional bpf() commands beyond BPF_PROG_LOAD/BPF_MAP_CREATE/BPF_BTF_LOAD gained *_token_fd fields in 6.18. Reason: this note pins the kernel mechanism to 6.12 source but did not fetch the 6.18 UAPI header or the userspace release notes. To resolve: diff include/uapi/linux/bpf.h between v6.12 and v6.18 for new *_token_fd fields, and check libbpf/systemd changelogs. uncertain

See Also