Skip to content

libxdp: Synthesise BTF for component programs supplied as instructions - #592

Closed
simonmorley wants to merge 1 commit into
xdp-project:mainfrom
simonmorley:libxdp-synthesise-component-btf
Closed

simonmorley wants to merge 1 commit into
xdp-project:mainfrom
simonmorley:libxdp-synthesise-component-btf

Conversation

@simonmorley

Copy link
Copy Markdown

Closes #591.

A dispatcher component has to be loaded as BPF_PROG_TYPE_EXT, and the kernel only accepts an EXT program that carries BTF describing its own function signature — check_attach_btf_id() compares the replacement's prototype against the target function's via btf_check_func_type_match(). Since a program's BTF is fixed at BPF_PROG_LOAD time, a caller holding an already-loaded BTF-less program cannot add it afterwards, and xdp_multiprog__link_prog() correctly refuses it.

That excludes any project emitting BPF bytecode directly rather than compiling it with clang. Such a program can only take the whole interface, which in turn locks every libxdp user out of that interface.

Every XDP component has the same int (struct xdp_md *) signature, so the BTF is fully determined and libxdp can generate it. This adds insns/insns_cnt to struct xdp_program_opts:

struct bpf_insn insns[] = {
	BPF_MOV64_IMM(BPF_REG_0, XDP_PASS),
	BPF_EXIT_INSN(),
};
DECLARE_LIBXDP_OPTS(xdp_program_opts, opts,
		    .prog_name = "my_program",
		    .insns = insns,
		    .insns_cnt = 2);

prog = xdp_program__create(&opts);

Approach

xdp_elfgen.c synthesises a minimal ELF object — the instructions, a license section, the BTF, and a .BTF.ext func_info record — which is then opened with bpf_object__open_mem(). Wrapping the instructions in an ELF rather than loading them directly means the result is an ordinary struct bpf_object, so the attach, EXT conversion and chain linking paths need no special case at all.

Two details worth flagging for review:

  • The struct xdp_md in the generated BTF is deliberately opaque (vlen = 0). btf_check_func_type_match() compares the struct name and never its members, so a component need not agree with the kernel on the layout. That keeps the BTF at ~127 bytes and independent of kernel version.
  • libelf references the image rather than copying it, so the buffer is owned by the xdp_program and freed in xdp_program__close(), after bpf_object__close() has stopped reading it.

No new exported symbol, so libxdp.map is unchanged — xdp_program__create() has been exported since LIBXDP_1.3.0 and the opts struct is extended through the existing sz mechanism.

Testing

New test_synth_insns creates a program from a raw instruction stream and checks it attaches as a component of a real dispatcher, rather than falling back to a legacy single-program attach. Full suite on 6.12.107, x86-64:

     [test_link_so]                PASS
     [test_link_a]                 PASS
     [test_old_dispatcher]         PASS
     [test_xdp_devbound]           PASS
     [test_xdp_frags]              PASS
     [test_xsk_prog_refcnt_bpffs]  PASS
     [test_xsk_prog_refcnt_legacy] PASS
     [test_xsk_non_privileged]     SKIPPED
     [test_link_detach]            PASS
     [test_xsk_umem_flags]         PASS
     [test_xsk_map_leak]           PASS
     [test_synth_insns]            PASS

libxdp.3 was updated by hand rather than regenerated, as emacs was not available here; it renders correctly under groff but is worth a check against the org export.

🤖 Generated with Claude Code

A dispatcher component has to be loaded as BPF_PROG_TYPE_EXT, and the
kernel only accepts an EXT program that carries BTF describing its own
function signature: check_attach_btf_id() compares the replacement's
prototype against the target function's via btf_check_func_type_match().
A program's BTF is fixed at BPF_PROG_LOAD time, so a caller holding an
already-loaded BTF-less program cannot add it afterwards, and
xdp_multiprog__link_prog() correctly refuses such a program.

This excludes any project that emits BPF bytecode directly instead of
compiling it with clang. Such a program can only take the whole
interface, which in turn locks every libxdp user out of that interface.

Every XDP component has the same int (struct xdp_md *) signature, so the
BTF is fully determined and libxdp can generate it. Add insns/insns_cnt
to struct xdp_program_opts and synthesise a minimal ELF object - the
instructions, a license section, the BTF, and a .BTF.ext func_info
record - which is then opened with bpf_object__open_mem() and handled
like any other object. Wrapping the instructions in an ELF rather than
loading them directly means the attach, EXT conversion and chain linking
paths need no special case.

Two details worth noting. The struct xdp_md in the generated BTF is
deliberately opaque: btf_check_func_type_match() compares the struct name
and never its members, so a component need not agree with the kernel on
the layout, which keeps the BTF small and version-independent. And
libelf references the image rather than copying it, so the buffer is
owned by the xdp_program and freed in xdp_program__close(), after
bpf_object__close() has stopped reading it.

Add a test that creates a program from a raw instruction stream and
checks that it attaches as a component of a real dispatcher rather than
falling back to a legacy single-program attach.

Signed-off-by: Simon Morley <simon@nullrabbit.ai>
@tohojo tohojo closed this Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dispatcher component programs require BTF that libxdp could synthesise (~127 bytes)

2 participants