BPF Object Pinning and Lifetime

Every loadable BPF entity — a program, a map, a link, a BTF blob — is a reference-counted kernel object that userspace touches only through a file descriptor (fd). The object lives exactly as long as something references it: an open fd, a kernel attachment, a pin in the BPF filesystem, or another BPF object that holds it. When the last reference is dropped, the object’s reference count hits zero and the kernel frees it (per the bpf(2) man page: “An eBPF object is deallocated only after all file descriptors referring to the object have been closed”). This single rule — fd-scoped lifetime, extended by other reference holders — is the foundation of every BPF deployment pattern, from “load, run, exit and clean up” to long-lived detached infrastructure. This note owns the reference-count model and the object reference graph; its sibling Map Pinning and bpffs owns the bpffs filesystem mechanics that pinning rests on.

Mental Model

The unifying idea: a BPF object is like any other refcounted kernel resource. Userspace never holds the object directly — it holds handles (fds) that each count as one reference. The kernel also lets other things hold references: a network subsystem that a program is attached to, a bpffs pin, a link that wraps an attachment, or a program that uses a map. The object’s reference count is just the sum of all these holders. The object survives as long as the count is above zero and is freed the instant it reaches zero. There is no garbage collector, no “owner” with special status — every holder is equal, and the last one out turns off the lights.

This is why the same machinery explains seemingly different behaviors. A self-contained tool that loads a program, attaches it, runs, and exits cleans everything up because exiting closes its fds and detaching drops the attachment reference. A tc filter “stays alive after the process exits” not by magic but because the tc subsystem took its own reference when the program was attached, which outlives the loader’s fd (the bpf(2) man page states this explicitly: “the tc subsystem holds a reference to the eBPF program after the file descriptor has been closed”). And pinning is simply adding one more reference holder — a filesystem entry — so the object outlives the loader even with no attachment. Persistent BPF is not a special object type; it is an ordinary object with an extra reference.

flowchart TB
  subgraph HOLDERS["Reference holders (each = +1 on the count)"]
    FD["open fd(s)<br/>(loader, dup, SCM_RIGHTS)"]
    PIN["bpffs pin<br/>(named, persistent)"]
    ATT["kernel attachment<br/>(tc, xdp, cgroup...)"]
    LINK["BPF link<br/>(owns prog ref)"]
  end
  subgraph GRAPH["Object reference graph"]
    PROG["program<br/>aux->refcnt"]
    MAP["map<br/>refcnt + usercnt"]
    BTF["BTF"]
  end
  FD --> PROG
  FD --> MAP
  PIN --> PROG
  PIN --> MAP
  PIN --> LINK
  ATT --> PROG
  LINK --> PROG
  PROG -->|"uses (verifier-time)"| MAP
  PROG -->|"typed by"| BTF
  MAP -->|"typed by"| BTF

The BPF object reference graph and its holders. What it shows: the upper boxes are the four ways something can hold a reference to a BPF object — an open fd, a bpffs pin, a kernel attachment, or a link. The lower boxes are the objects, with internal edges: a program holds references on every map it uses (taken at verification time), a link holds a reference on its program, and both programs and maps reference the BTF that types them. The insight to take: lifetime is determined by counting all inbound edges to an object; the object dies only when every holder — every fd, every pin, every attachment, every other object pointing at it — has released it. Pinning just adds one durable inbound edge from the filesystem.

The Reference-Count Model, Per Object Type

Each object type carries one or more atomic counters. Reading the v6.12 kernel/bpf/syscall.c source, the three families differ in an important, easy-to-get-wrong way: maps have two counters; programs and links have one.

Programs — one counter, aux->refcnt

A program’s reference count lives in prog->aux->refcnt, an atomic64_t. Taking a reference is bpf_prog_inc() (atomic64_inc(&prog->aux->refcnt)); dropping one is bpf_prog_put()__bpf_prog_put():

static void __bpf_prog_put(struct bpf_prog *prog)
{
	struct bpf_prog_aux *aux = prog->aux;
 
	if (atomic64_dec_and_test(&aux->refcnt)) {   /* dropped to zero? */
		if (in_irq() || irqs_disabled()) {
			INIT_WORK(&aux->work, bpf_prog_put_deferred);
			schedule_work(&aux->work);           /* free from a workqueue */
		} else {
			bpf_prog_put_deferred(&aux->work);   /* free here */
		}
	}
}

atomic64_dec_and_test decrements and returns true exactly when the result is zero — the canonical “last reference out” idiom. Only then does the kernel tear the program down: bpf_prog_put_deferred removes the program’s id, audits the unload, and schedules the final free behind an RCU grace period (call_rcu, or call_rcu_tasks_trace for sleepable programs) so that any CPU still executing the program finishes first. The deferral to a workqueue when called from IRQ/atomic context exists because freeing may sleep. The takeaway: a program is not freed the moment you close its fd — it is freed when the count reaches zero, possibly much later, and even then only after in-flight executions drain.

A link (the modern fd that owns an attachment) has link->refcnt. bpf_link_inc() bumps it; bpf_link_put() drops it:

void bpf_link_put(struct bpf_link *link)
{
	if (!atomic64_dec_and_test(&link->refcnt))
		return;                          /* still referenced, do nothing */
	INIT_WORK(&link->work, bpf_link_put_deferred);
	schedule_work(&link->work);          /* last ref: free via workqueue */
}

When a link’s count hits zero, bpf_link_free() runs: it detaches the program from its hook (ops->release(link)) and then drops the link’s reference on the program with bpf_prog_put(link->prog). This is the key graph edge: a link holds a reference on the program it attaches. So a link keeps its program alive, and closing a link both detaches it and releases that program reference — which may or may not free the program depending on who else holds it.

Maps — two counters, refcnt and usercnt

Maps are the subtle case. A struct bpf_map carries two atomic64_t counters, and confusing them is a classic source of bugs and misunderstanding:

  • refcnt — the object lifetime count. When it reaches zero, the map is freed.
  • usercnt — the userspace-reference count. When it reaches zero, the kernel calls map->ops->map_release_uref(map) (a per-type hook used by, e.g., maps that hold fds or that must notify on losing their last user-visible reference). It does not free the map.

At creation (map_create), both are initialized to one: atomic64_set(&map->refcnt, 1); atomic64_set(&map->usercnt, 1);. The two are taken and dropped by paired helpers:

void bpf_map_inc(struct bpf_map *map)            { atomic64_inc(&map->refcnt); }
void bpf_map_inc_with_uref(struct bpf_map *map)  { atomic64_inc(&map->refcnt);
                                                   atomic64_inc(&map->usercnt); }
 
void bpf_map_put(struct bpf_map *map)            /* drops refcnt; frees at 0 */
{
	if (atomic64_dec_and_test(&map->refcnt)) {
		bpf_map_free_id(map);
		... /* schedule free (RCU / workqueue) */
	}
}
void bpf_map_put_with_uref(struct bpf_map *map)  /* drops usercnt then refcnt */
{
	bpf_map_put_uref(map);                        /* if usercnt -> 0: map_release_uref */
	bpf_map_put(map);
}

Who uses which? An open map fd holds both counts: a userspace fd is a user reference, so closing it calls bpf_map_put_with_uref (you can see this in bpf_map_release, the fd’s ->release handler). A program that uses a map holds only refcnt, not usercnt — the program is a kernel-internal user, not a userspace handle, so it takes bpf_map_inc alone. This distinction is exactly why a map can keep existing (for a still-loaded program) after every userspace fd is closed: refcnt > 0 keeps it alive even though usercnt has hit zero. The man page captures the program-holds-map edge from the other side: “During verification, the kernel increments reference counts for each of the maps that the eBPF program uses, so that the attached maps can’t be removed until the program is unloaded” (bpf(2)).

Only maps have the dual counter. Programs use a single aux->refcnt; links use a single refcnt. When you reason about map lifetime, always ask which counter — the object dies on refcnt, but the “last userspace user left” callback fires on usercnt.

The Object Reference Graph

Putting the per-type rules together gives a directed graph of who keeps whom alive:

  • fd → object. Every open fd is one reference. For a map it is a with_uref reference (both counters); for a program or link it is the single counter. Closing the fd (or the process exiting) drops it.
  • program → map. A program references every map it uses; the reference is taken when the program is loaded/verified (bpf_map_inc, refcnt only) and released when the program is freed. A map cannot be freed while a program that uses it is loaded.
  • link → program. A link references the program it attaches; bpf_link_free does bpf_prog_put(link->prog) on the last link reference. A program cannot be freed while a link to it exists.
  • attachment → program. Some attach paths (notably classic tc/xdp attaches that predate links) make the subsystem hold a program reference directly, independent of any fd — this is why a tc-attached program survives the loader’s exit.
  • pin → object. A bpffs pin holds one reference to whatever it pins (prog, map, or link). For a map it is a with_uref reference (see below); for a prog/link the single counter.
  • object → BTF. Programs and maps reference the BTF that describes their types; that BTF is freed (btf_put) only when its last holder goes.

Lifetime is then a simple invariant: an object is freed exactly when its inbound reference count reaches zero, i.e. when every fd is closed, every link/attachment is gone, every using-program is unloaded, and every pin is removed. Miss any one holder and the object stays resident.

How Pinning Fits the Model

Pinning is the cleanest demonstration that the lifetime rule is just “count the holders.” When userspace runs BPF_OBJ_PIN (see Map Pinning and bpffs for the bpffs path mechanics), the kernel’s bpf_obj_do_pin path creates a filesystem inode whose i_private points at the object, and the act of pinning takes a reference via bpf_any_get:

static void *bpf_any_get(void *raw, enum bpf_type type)
{
	switch (type) {
	case BPF_TYPE_PROG: bpf_prog_inc(raw);          break;  /* +1 prog refcnt */
	case BPF_TYPE_MAP:  bpf_map_inc_with_uref(raw); break;  /* +1 BOTH map counters */
	case BPF_TYPE_LINK: bpf_link_inc(raw);          break;  /* +1 link refcnt */
	}
	return raw;
}

Note the map case: pinning a map takes a with_uref reference — it bumps both refcnt and usercnt, because a pin is treated as a user-visible handle, equivalent to holding an open fd. So after pinning, the loader can close its own fd (dropping one with_uref) and the map’s counts stay at one each, held by the pin alone. BPF_OBJ_GET (re-opening the pin) likewise calls bpf_any_get to take another reference, handing back a fresh fd to the same object.

The symmetric half — what drops the pin’s reference — is the BPF superblock’s inode-eviction path. bpf_super_ops sets .drop_inode = generic_delete_inode, which means when a pin’s last directory link is removed (an rm/unlink), the inode is deleted immediately rather than cached. Its .free_inode = bpf_free_inode then runs:

static void bpf_free_inode(struct inode *inode)
{
	enum bpf_type type;
	if (S_ISLNK(inode->i_mode))
		kfree(inode->i_link);
	if (!bpf_inode_type(inode, &type))
		bpf_any_put(inode->i_private, type);   /* release the held reference */
	free_inode_nonrcu(inode);
}

bpf_any_put mirrors bpf_any_get — for a map it calls bpf_map_put_with_uref, dropping both counters; for a prog/link the single counter. So removing a pin drops exactly the reference the pin took. If that was the last reference, the object is now freed (after its RCU grace period). This closes the loop: pinning adds one inbound edge; unpinning removes it; the object dies only when that and every other edge is gone.

Configuration / Code: Observing Lifetime in Practice

A short walk through the lifecycle from userspace, with bpftool and libbpf:

# 1. Load a program+maps and pin them. The loader process exits immediately,
#    but the pins hold references, so nothing is freed.
bpftool prog loadall prog.o /sys/fs/bpf/myapp pinmaps /sys/fs/bpf/myapp
 
# 2. The program and maps are still resident with no process holding fds:
bpftool prog show           # the prog is listed
bpftool map show            # its maps are listed
ls /sys/fs/bpf/myapp        # the pins that keep them alive
 
# 3. A wholly separate process re-opens a map by path (BPF_OBJ_GET) and reads it:
bpftool map dump pinned /sys/fs/bpf/myapp/events
 
# 4. Removing the pins drops the references; with no other holder, the
#    objects are freed (after RCU grace).
rm -rf /sys/fs/bpf/myapp
bpftool prog show           # the prog is gone (if nothing else held it)

From C, the libbpf docs make the reference semantics explicit: bpf_program__pin() “increments the programs reference count, allowing it to stay loaded after the process which loaded it has exited”, and bpf_program__unpin() “decrements program’s in-kernel reference count” (per libbpf API reference). The same wording applies to bpf_map__pin/unpin and bpf_link__pin/unpin. A subtlety the libbpf docs call out: the pin file “can also be unlinked by a different process,” so a later __unpin may fail with an error even though the in-kernel reference was already released by the eviction path — the kernel reference and the userspace pin-path bookkeeping can diverge if someone rms the pin behind libbpf’s back.

Failure Modes and Common Misunderstandings

  • “I closed the fd, why is my program still loaded?” Because something else holds a reference: a pin, a link, or a subsystem attachment (tc/xdp). Closing one fd only drops one reference. Use bpftool prog show and check for pins under /sys/fs/bpf and for attached links (bpftool link show).
  • “I removed the pin, why is my map still there?” A program using the map holds a refcnt reference taken at verification time. The map cannot be freed until that program is unloaded too. Unpinning is necessary but not always sufficient.
  • Confusing usercnt with refcnt. Code that wants “free this map when the last userspace user goes” must not assume that hitting usercnt == 0 frees anything — it only fires map_release_uref. The map persists as long as refcnt > 0 (e.g. a loaded program is still using it). Conversely, refcnt reaching zero is the only thing that frees the map.
  • Use-after-free assumptions about freeing. Objects are not freed synchronously when the count hits zero — programs, maps, and links all defer the final free behind RCU (and sometimes a workqueue). Reasoning that “the count is zero so the memory is gone” is wrong; in-flight executions and RCU readers are allowed to finish first. This is deliberate and is what makes BPF safe to detach under load.
  • Leaked pins = leaked objects. Because a pin is a hard reference, a forgotten pin keeps its object (and, transitively, every map that object uses) resident forever. Orphaned pins under /sys/fs/bpf are a slow, silent leak; auditing them is an operational task.
  • Mismatched put on partial init. In the kernel, a failed pin must drop the reference it took (bpf_obj_pin_user calls bpf_any_put on error) — getting this wrong is exactly the class of refcount bug that leaks or double-frees objects. Userspace code that opens-by-fd and pins should likewise be careful to balance every inc with a put.

Alternatives and When to Choose Them

The “how do I keep this object alive” question has a small menu, each a different reference holder:

  • Keep the fd open (keep the loader running): simplest, zero filesystem footprint, but the object dies with the process. Right for self-contained, long-lived agents that own everything.
  • Attach it (link or classic attach): the attachment becomes the reference holder, so the object lives as long as it is attached, even if the loader exits. Right when the program is supposed to run indefinitely at a hook (the link is the modern, fd-owned form of this).
  • Pin it into bpffs: a named, persistent reference independent of process and attachment. Right when the object must be discoverable by path, shared across unrelated processes, or survive both the loader exiting and being detached. See Map Pinning and bpffs.
  • Pass the fd over a UNIX socket (SCM_RIGHTS) or inherit via fork: shares the object between cooperating, co-running processes without any persistence. Right for transient sharing where neither pinning nor attachment is wanted.

These compose: a real deployment often pins a map (persistence + sharing), holds a link (keeps the attachment), and passes fds at runtime (live sharing) all at once — three independent reference holders on the same objects.

Production Notes

The reference model is what makes graceful BPF upgrades possible. Tools like Cilium pin their maps and hold links; on agent restart the new process re-opens the pins (the maps were never freed because the pins held them) and re-attaches, so datapath state survives the upgrade with no packet loss. The same model is why a crashing loader does not necessarily tear down its BPF: if the program was pinned or attached, those references outlive the crash, which is both a feature (resilience) and a hazard (orphaned, un-owned BPF that must be cleaned up out-of-band). Observability tooling leans on the model directly: bpftool prog show / map show / link show enumerate live objects by walking the kernel’s id tables, and the presence of an object with no obvious owner almost always means a pin or a subsystem attachment is the hidden holder. When debugging “why won’t this object free,” the discipline is to enumerate every possible reference holder from the graph above — fds (including dup’d and SCM-passed ones), pins, links, attachments, and using-programs — and confirm each is gone.

See Also

  • Map Pinning and bpffs — the bpffs filesystem and the BPF_OBJ_PIN/BPF_OBJ_GET path mechanics that this note’s reference model rests on
  • BPF Links and Attachment Lifecycle — links as the fd that owns an attachment and holds a program reference; the modern attach-lifetime primitive
  • BPF Maps — what maps are; their two-counter lifetime is detailed here
  • BPF Program Types — programs as refcounted objects and how attachment makes the subsystem a reference holder
  • The bpf() Syscall — the syscall whose commands create the fds that begin every object’s life
  • BTF (BPF Type Format) — the type objects that programs and maps reference
  • Linux eBPF MOC — parent map of content