A colleague loaded an XDP program onto a production interface from a terminal that has since been closed, and traffic is still being affected. Why do some eBPF attachments survive the process that created them while others vanish with it, and how would you find and remove this one?
answer
- a program lives while something references it
- the fd, the pin, or the attachment
- link fd closes, attachment goes
- the netdev holds the netlink one
- killing the loader changes nothing
basics
~20 sAttachments made through a bpf_link end when the last file descriptor to that link closes, while netlink XDP attachments, tc filters and cgroup attachments hold their own kernel reference and outlive the loader. Those have to be detached explicitly.
solid answer
~50 sA loaded eBPF program is reference-counted: it lives while something holds a reference. Modern attachments create a `bpf_link`, and the link's file descriptor is that reference — so when the loader exits and the fd closes, the attachment is torn down automatically, unless the link was pinned under `/sys/fs/bpf`. The older attach paths do not work that way. An XDP program attached over netlink with `ip link set dev eth0 xdp ...`, a tc filter added with `tc filter add ... bpf`, and a cgroup program attached with `bpf(BPF_PROG_ATTACH)` all leave a kernel-side reference on the netdev or cgroup, which outlives the process entirely. There is no process to kill. You find it with `ip link show dev eth0`, which prints the attached program id, or with `bpftool net show`, then remove it with `ip link set dev eth0 xdp off`.
code
bash · 12 lines# what is attached where?
ip link show dev eth0
sudo bpftool net show
# identify the program behind the id printed above
sudo bpftool prog show id 42
# link-held (self-cleaning) attachments, for contrast
sudo bpftool link show
# detach
sudo ip link set dev eth0 xdp offgo deeper
Know that an eBPF program keeps running after the terminal that loaded it is gone, and that removing it means an explicit detach command rather than killing a process.
Explain the reference model: an open fd, a pin under /sys/fs/bpf, or an attachment can each keep a program alive, and a bpf_link ties the attachment to an fd while netlink XDP, tc and cgroup attachments hold their own kernel reference.
Show the incident habit: enumerate with bpftool net show and ip link show before touching anything, detach through the subsystem that attached, and know that a pin is a second reference you have to clear separately. Say the detach command out loud before you run the attach.
Own the operational contract for eBPF agents in the fleet: whether attachments are expected to be self-cleaning or pinned across agent restarts, who is allowed to attach on the datapath, and how an orphaned attachment is discovered without a human noticing the symptom first.
## Why this is a real incident and not a trivia question The eBPF object model has no owning process in the way people expect. Once a program is loaded and attached, it runs because something in the kernel holds a reference to it, and "the shell that started it" is often not that something. Engineers who assume otherwise kill the loader, see nothing change, and conclude the box is haunted. ## The reference model A program loaded with `bpf(BPF_PROG_LOAD)` returns a file descriptor. While at least one reference exists the program stays loaded; when the last one goes, it is freed. References come from three places: 1. **An open fd** in some process — typically the loader. 2. **A pin** in the bpf filesystem, usually mounted at `/sys/fs/bpf`. A pinned program or link survives every process on the box. 3. **An attachment**, if the attach mechanism takes its own reference. It is the third that produces the surprise, because the answer differs by mechanism. ## bpf_link: the attachment is an object with an fd The modern interface is `BPF_LINK_CREATE`, which returns a **link** fd representing the attachment itself. fentry/fexit, tracepoint, and modern XDP and cgroup attachments all work this way when the loader asks for them. The lifetime rule is simple and pleasant: the attachment lives exactly as long as the link fd. Close it — including by the process exiting or being killed — and the kernel detaches cleanly. This is why running a bpftrace or libbpf tool in the foreground and pressing Ctrl-C leaves nothing behind, and why link-based attachments are self-cleaning by default. Pin the link under `/sys/fs/bpf` and you have deliberately opted out of that. ## The legacy paths: the kernel holds the reference Several widely used attach paths pre-date links and take a reference of their own: - **XDP over netlink.** `ip link set dev eth0 xdp obj prog.o sec xdp` attaches to the net device. The netdev holds the program, and the program keeps running after the loader dies, across the loader's terminal closing, and across the interface going down and up. - **tc classic filters.** A filter added with `tc filter add dev eth0 ingress bpf ...` lives in the qdisc's filter list. - **cgroup attachments** made with `bpf(BPF_PROG_ATTACH)` rather than a cgroup link stay attached to the cgroup. - **Socket filters** attached with `setsockopt(SO_ATTACH_BPF)` are an intermediate case: they are tied to the socket, so they die when the socket is closed, which usually means when the owning process exits. For the first three, there is no process to signal. You must detach through the same subsystem that attached. ## Finding it ```bash # which interfaces carry an XDP program, and which program id? ip link show dev eth0 bpftool net show # what is program id 42 — type, name, when was it loaded? bpftool prog show id 42 # which attachments are held by links (and therefore self-cleaning)? bpftool link show ``` `ip link show` prints an `xdp` marker with the program id for an attached interface. `bpftool net show` is the one command that enumerates XDP and tc attachments across the host's interfaces, and it is the right first move because `bpftool prog show` alone tells you what is *loaded*, not *where it is attached* — a distinction that trips people up under pressure. ## Removing it ```bash ip link set dev eth0 xdp off # detach XDP tc filter del dev eth0 ingress # remove a tc filter bpftool cgroup detach /sys/fs/cgroup/... ... # detach a cgroup program ``` If the program was pinned, the pin under `/sys/fs/bpf` is a separate reference and has to be removed too, or the program stays loaded even after detachment. Note what does *not* work: killing the loader, deleting the ELF object from disk, or flapping the link with `ip link set dev eth0 down && up`. The attachment lives on the netdev, and the object file was only ever the source of the bytecode. ## The habit to take away When you attach anything on a production host, know in advance which of the two lifetimes you have chosen, and write the detach command down before you run the attach. "It stops when I press Ctrl-C" is true for a link-based tool and false for `ip link set ... xdp`, and the difference is only visible if you know to look for it.
- You detach the program and bpftool prog show still lists it. Why is it still loaded?Something else still references it. The usual culprit is a pin in the bpf filesystem — remove the pin file under `/sys/fs/bpf` and the reference goes. Otherwise a process still holds an open fd to the program, or it remains attached somewhere else you have not checked, such as a second interface or a cgroup.
- Why is `bpftool prog show` the wrong first command when hunting an orphaned XDP attachment?Because it enumerates loaded programs, not attachments. It will show the program and its id but not tell you which device it is affecting. `bpftool net show` lists the XDP and tc attachments per interface, and `ip link show` prints the program id on the device — either of those answers the question you actually have.
- How would you deliberately make an attachment survive its loader, and why would you want that?Pin the link, or the program, into the bpf filesystem under `/sys/fs/bpf`, or use one of the attach paths that takes its own kernel reference. You want it for anything that must keep working across a restart of the agent that installed it — a datapath filter or a policy hook — where a self-cleaning link would create a gap every time the agent is upgraded.
saying these in an interview costs you the question
- Assumes killing the loader removes any attachment
- Thinks deleting the object file unloads the program
- Believes bringing the interface down detaches XDP
- Uses bpftool prog show to find where a program is attached
- Says every eBPF attachment is cleaned up automatically