Skip to content

Why does libbpf skip manual strategy programs during the initial object load, and how does the struct ops map init pass clobber a manual load strategy set via the API?

Because the bulk load pass loads only the programs whose load strategy is AUTO, and it deliberately leaves a MANUAL program unloaded so that program can be loaded or unloaded on its own later. The clobber comes from an earlier pass inside object prepare: the kernel-BTF struct ops map initialization writes DISABLED back onto a program's load strategy unconditionally when a callback slot cannot be resolved, without consulting what the user set. A MANUAL strategy set through the API is therefore silently rewritten to DISABLED, and the later explicit load fails with -EINVAL because the strategy is no longer MANUAL. The autoload adjustment pass that runs immediately afterwards cannot recover the intent, because it protects a program only while that program still reads MANUAL.

The mechanism

  1. Mainline today. The program structure carries a single bool autoload flag, the released header exposes only the autoload accessors, and the object load is one pass. A section name with the SEC("?...") prefix turns autoload off, so that program is skipped at load time and every other program stays on. The released symbol map lists the unload entry point in its original LIBBPF_0.0.1 section, and that mainline unload also frees the program's func_info, line_info, and subprogs arrays; the narrower close-the-descriptor behavior described below is part of the in-flight series, not current mainline.

  2. The in-flight v9 series. The series replaces the autoload boolean with a tri-state load strategy: DISABLED (skipped; the SEC("?...") equivalent), AUTO (loaded during the object load; the default), and MANUAL (skipped by the bulk pass and loaded later through the new program load entry point). The declarative SEC("!...") prefix maps to MANUAL, mirroring the ? convention. The setter is honored only before the object is prepared, and setting a manual strategy on a subprogram is rejected. An explicit load of one program then checks its preconditions in order: an object below the prepared state, a subprogram, or any strategy other than MANUAL each return -EINVAL; a program that is already loaded, meaning it still holds a file descriptor, returns -EBUSY. Two further consequences follow. The generated loader is rejected for an object that contains any manual program, because that loader is finalized during the object load, which skips manual programs. And the module BTF, the vmlinux BTF, and the file-descriptor array are kept alive while a manual program is present and freed only at object close, while manual programs keep their relocation descriptors so the CO-RE verifier log fixup still applies on the later load.

  3. The struct ops break. A struct ops program cannot be loaded or unloaded independently, because the kernel binds the whole struct ops map and the map slots hold the program file descriptors. The series therefore rejects the declarative manual prefix for a struct ops program with -EINVAL while parsing the section, yet the API setter still accepts a manual strategy for those programs, which is exactly the path the open problem sits on. During object prepare, the struct ops map initialization runs immediately before the autoload adjustment pass. Inside it, if a callback member is absent from the running kernel BTF, or if a slot in a struct ops map's shadow structure was cleared or replaced by a different program, the initialization assigns DISABLED to that program's load strategy unconditionally.

How to verify and debug

  1. Read the strategy back. Read the strategy with the getter immediately after setting it; it should still read MANUAL. Read it again after object prepare completes. A struct ops program that now reads DISABLED means the clobber already happened inside the map initialization pass, from a missing callback slot or a cleared or replaced map slot.
  2. Tell the two outcomes apart. The autoload adjustment pass runs next and no longer sees a manual strategy to protect, so one of two things happens. If nothing else references the program, it stays DISABLED and the explicit load returns -EINVAL. If another autocreated struct ops map still references that program, the adjustment pass sets it to AUTO, the program is loaded during the ordinary object load, and no error is reported at all: the manual load simply never takes place. The silent second outcome is the harder one to notice, because the program attaches even though the caller asked to control it.
  3. Check the deferred cleanup boundary. The module BTF, the vmlinux BTF, and the file-descriptor array are released when the object is closed, not when the object load returns, as long as a manual program exists. Do not assume those objects are gone right after the load, and do not assume a manual program's later load can still consult them after the object is closed.

The limitation

  • None of the load strategy enum, the single-program load entry point, or the manual SEC("!...") prefix is in mainline. The released header and symbol map carry only the autoload flag accessors and the unload entry point, and the newest released symbol version in the map is still an empty section.
  • Struct ops programs cannot be deferred in practice: the kernel binds the whole struct ops map, so one such program cannot be loaded or unloaded on its own.
  • The clobber is an open review finding on the current revision, graded medium: the suggested guard belongs in the map initialization pass, which is the pass that writes DISABLED.
  • The generated loader is incompatible with any manual program and fails with -EOPNOTSUPP, and setting up that loader after the prepare step is rejected as well, so a manual strategy and a generated loader cannot be combined.

References

Community discussion today

The watchlist for this run covers six communities: two chat workspaces with ten channels, two archive-backed chat workspaces with three channels, one mailing list, and one practitioner forum. Two of them carried real technical traffic in this window: the public bpf mailing list, read through its public archive pages, and the practitioner forum, read through its ordinary public listing pages. The three archive-backed channels were queried through the approved read-only archive for the 24-hour and 7-day windows and returned no messages at all, so they were quiet rather than skipped. The ten channels in the two chat workspaces were not covered this run, because reading them needs an authenticated browser session that was not available; they are recorded as not covered, not quiet.

The manual loading series arrived inside this window and carries the whole measured motivation for a manual strategy: one large tool has around 250 potentially loadable programs on any given system but loads only around 90 by default, and shrinking the default loaded set cut load time by roughly sixty percent in the author's own measurements, with unload time scaling with the number of loaded programs. The review traffic on it is machine-generated so far: the automated reviewer replied twice, one reply on the manual load patch with the strategy-overwrite finding above, and as of this run no human reply had appeared. The reviewer concerns quoted in the cover letter's change notes belong to earlier revisions, so the boundary this answer describes is genuinely still open rather than merely unsettled.

The rest of the mailing-list window is dominated by verifier and loader work rather than by new attach APIs. A large series that reworks how verified instruction patches are applied — stripping no-operation instructions, blinding constants, remapping verifier state, and widening a backward bounded-loop jump when a patch pushes it out of range — drew a long review exchange in this window, and the loop-jump case is the practical one to watch: a patch that lengthens the code can invalidate a jump distance the verifier had already accepted. A separate series proposes a query interface over the kernel symbol table plus a loader path that resolves untyped kernel symbols through it; the review concentrated on the interface shape rather than on the idea. A series on reading and mapping user memory from kernel functions, and two socket-map cleanups — annotating the teardown data races, and covering a concurrent update and delete with a test — were also under review in this window, alongside transmit-side checksum and buffer-release fixes for the zero-copy packet path. Together these say the current effort is going into verifier patching and safer access to memory that eBPF programs did not previously reach cleanly, not into more attach mechanisms.

On the practitioner forum the only item inside the 24-hour window was a production-experience post about operating eBPF at scale; the newest technical thread there is a few days old and continues a multi-day report on a packet-crafting scanner built on the zero-copy packet socket API in its copy mode. The wall-clock problem turned out to be one transmit syscall per packet on a single transmit ring, and batching up to sixty-four frames per call recovered most of the gap to a conventional scanner, while a busy-poll loop pinned to a core actively hurt throughput on a loaded virtual machine because the spinning thread competed for the vCPU. Two questions there remain open and are the ones an engineer hits the same way: how much a kernel-wakeup flag buys when the producer would otherwise poll, and whether anything beyond batching helps when there is only one transmit ring. The binding flag that exposes that wakeup mechanism, and the ring layout behind it, are specified in the kernel AF_XDP documentation; whether the wakeup helps is workload-dependent and the thread has no measurement to settle it. A separate recent post asked which security tooling actually uses eBPF today after its author encountered instrumentation through observability agents, which is a discovery-pattern signal worth noting: new practitioners arrive from observability products and then look for the security use case.

This run's archive-backed sources produced no messages in either window, so the question above was sourced from the permitted public bpf mailing list and verified against its patch thread and the released kernel sources; the practitioner forum supplied the operational themes in this section.