Detaching and pinning: outliving the loader
Open the advanced part with the lifecycle question every earlier chapter dodged: what happens to a program when its loader exits? Learn how BPF objects are reference-counted, pin a program, map, and link to bpffs so they survive the loader, read the pinned map from a separate process, and detach by removing the pin.
Every loader so far has ended the same way: it attaches a program, loops while holding a file descriptor, and when you press Ctrl-C the program detaches and vanishes. That’s the right shape for a tool you run interactively, but it hides a question that matters the moment you operate eBPF for real: what keeps a program alive, and how do you make it outlive the process that loaded it? The answer — reference counting and pinning to bpffs — is the foundation the rest of this part and the operating chapters build on, so we start here.
The code is in examples/48-pin-demo/. ./demo.sh there pins a program and
exits, reads it back from a fresh process, and detaches; its README.md has
the details.
Reference counting, briefly
Every BPF object — a program, a map, a link (the handle representing one attachment) — lives as long as something holds a reference to it. The usual reference is a file descriptor in your loader. When the loader exits, the kernel closes its fds, the reference counts drop to zero, and the objects are freed: the program detaches and the map’s data is gone. That’s why Ctrl-C ends the show.
To keep an object alive past the loader, something else must hold a
reference. The standard mechanism is the BPF filesystem (bpffs, mounted
at /sys/fs/bpf). Pinning an object creates a file there that holds a
reference of its own:
[vm]$ mount | grep bpf # bpffs is usually already mounted at /sys/fs/bpf
[vm]$ ls /sys/fs/bpf # pins show up here as files
A pin is just a named reference. While the pin exists the object stays alive, even with no process holding it; remove the pin and the reference drops.
Pinning with Aya
Three things are worth pinning, and Aya pins each:
- The map, so its data survives and other processes can open it.
- The link, so the attachment survives — without this the program would detach even if the program object itself were pinned.
- The program (optional once the link is pinned, but handy for inspection).
// after load() + attach()
let link_id = prog.attach("syscalls", "sys_enter_execve")?;
let link = prog.take_link(link_id)?; // take ownership of the attachment
let fd_link: FdLink = link.try_into()?; // convert to a pinnable fd-backed link
fd_link.pin("/sys/fs/bpf/ebpf-aya/execs_link")?; // attachment now outlives us
let map: MapData = /* the EXECS map */;
map.pin("/sys/fs/bpf/ebpf-aya/EXECS")?; // data now outlives us
// loader exits here — the program keeps counting execs
Reading the key moves:
take_linkhands you the owned link; converting it to anFdLinkgives a handle backed by a file descriptor, which is the kind Aya can pin.pinwrites the bpffs entry that holds the attachment open.MapData::pindoes the same for the map. The directory must already exist on bpffs (mkdir /sys/fs/bpf/ebpf-aya), since pinning won’t create parent directories.- Once both are pinned, the loader can drop everything and exit. The program stays attached and keeps writing to the map.
Reading a pinned map from elsewhere
Because the map is a named object on bpffs, any process with access can open it — the loader doesn’t need to be running:
let map = MapData::from_pin("/sys/fs/bpf/ebpf-aya/EXECS")?;
let execs: HashMap<_, u32, u64> = HashMap::try_from(Map::HashMap(map))?;
println!("execs so far: {}", execs.get(&0, 0)?);
This decoupling is the whole point: one process installs and pins, a different process (or many, over time) reads — the lifecycle of the eBPF program is no longer tied to any single program. That is exactly how zero-downtime managers operate, a thread Part 9 picks up.
Build, deploy, observe
cd examples/48-pin-demo && ./demo.sh
The demo runs pinctl load (load, attach, pin link + map, exit), then
shows the pins in /sys/fs/bpf with the loader gone, runs pinctl read from a
fresh process a couple of times so you can watch the exec counter climb with no
loader running, and finally pinctl detach to remove the pins.
In the terminal you’ll see the count rise between reads — proof the program
kept working after its loader exited. In Grafana (127.0.0.1:3000), the
read step exports ebpf_pinned_execs_total, so you can graph the persisted
counter that any reader can pick up.
Cross-check
[vm]$ sudo bpftool prog show # the tracepoint program, still loaded
[vm]$ sudo bpftool link show # the pinned link holding the attachment
[vm]$ sudo bpftool map show # the EXECS map
[vm]$ sudo bpftool map dump pinned /sys/fs/bpf/ebpf-aya/EXECS # its contents
[vm]$ ls -l /sys/fs/bpf/ebpf-aya/ # the pins themselves
bpftool prog show listing the program while no loader runs is the proof that
the pin holds it; bpftool map dump pinned reads the same counter your read
command does. Removing the pins (pinctl detach or rm) and re-running
bpftool prog show shows it gone — the reference is what kept it alive.
What you learned
- BPF programs, maps, and links are reference-counted; when the loader’s file descriptors close, the objects are freed and the program detaches.
- Pinning to bpffs (
/sys/fs/bpf) creates a named reference that keeps an object alive past the loader — pin the link to keep the attachment, the map to keep (and share) the data. - A pinned map can be opened by any process, decoupling the program’s lifecycle from its loader; removing the pin drops the reference and detaches.
Next, Chapter 49 builds on persistence with programs that act on syscalls directly, beginning the tour of the modern BPF surface.
Verification status: verified — Fedora 44, kernel 7.1.3. Built and run on the lab VM (Fedora 44, kernel 7.1.3-200.fc44): builds, loads, and attaches cleanly and runs without error. Confirmed on this kernel — attach targets and struct offsets can be version-specific.