Skip to content

Report bpf(2) access to our programs and links too - #426

Draft
nicholasberlin wants to merge 1 commit into
trusted-map-tamper-eventsfrom
tamper-progs-links
Draft

nicholasberlin wants to merge 1 commit into
trusted-map-tamper-eventsfrom
tamper-progs-links

Conversation

@nicholasberlin

@nicholasberlin nicholasberlin commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #425. Merge that first, then I will retarget this one.

Motivation

The published trusted-map bypass starts by walking programs, matching module_load and reading its map ids out of OBJ_GET_INFO_BY_FD. #425 covers maps, so its third step was loud and its first step silent. Links are the other half. Verified against the pinned kernel: the seven cgroup attachments are the only ones bpf(2) can detach, through LINK_DETACH, LINK_UPDATE, or a legacy PROG_DETACH by attach type. Every tracing, raw tracepoint and perf link, the two tamper tracepoints included, has no detach callback and answers EOPNOTSUPP. Both the attempt and the answer are worth seeing.

Change

  • Two more id sets, elastic_ebpf_events_prog_ids and elastic_ebpf_events_link_ids, filled like the map set and hardened the same way: read-only for programs, frozen.
  • The fill and the freeze move from after load to after bpf_probes__attach(), since links exist only then. The probes run unfrozen for the microseconds in between. Links attached the legacy way on old kernels have no id and are skipped; nothing in bpf(2) can reach them anyway.
  • The probe classifies the object behind an fd by the name of its anonymous inode's dentry, bpf-map, bpf-prog or bpf-link, then reads the id from the matching struct and looks it up in that kind's set. Review fix: an earlier version read the id at all three struct offsets and argued a wrong-kind read was random. It is not: a struct bpf_prog keeps expected_attach_type where a struct bpf_link keeps its id, both at offset 8, so a bpftool prog show on a host with any other fentry, raw tracepoint or cgroup program would have reported quark links as touched. The dentry name is exact and needs no kernel symbol.
  • New commands reported: PROG_GET_FD_BY_ID, LINK_GET_FD_BY_ID, OBJ_GET_INFO_BY_FD on any kind, PROG_TEST_RUN, LINK_DETACH, LINK_UPDATE, and PROG_ATTACH, PROG_DETACH, LINK_CREATE, RAW_TRACEPOINT_OPEN, PROG_BIND_MAP when they name one of our programs.
  • Review fix: a legacy PROG_DETACH by attach type alone is no longer reported. Verified in the pinned kernel/bpf/cgroup.c: cgroup_bpf_link_attach() attaches with BPF_F_ALLOW_MULTI, and in multi mode find_detach_entry() only matches an entry whose program and link both match, so a legacy detach cannot remove a link-attached program. Reporting it would only have echoed systemd's own cgroup housekeeping on hosts with legacy IP accounting.
  • quark_tamper loses map_id and map_name for kind, id and name, with kind one of QUARK_TAMPER_MAP, PROG, LINK. A link is named after the program it attaches. No consumer had used the old fields. Static asserts tie the flag and kind values to the probe's.

Not deduplicated. bpftool prog show does OBJ_GET_INFO_BY_FD on every program on the host, so one invocation now produces an event per program of ours on top of the ones per map from #425. The right key for collapsing reconnaissance depends on how the consumer uses the event, so that is left for when there is one.

Test

t_tamper_progs forks a child that walks program ids to our always loaded sched_process_fork, asks for its info, runs it with PROG_TEST_RUN (refused, tracing programs have no runnable test), then walks link ids to its link and calls LINK_DETACH, expecting EOPNOTSUPP. Only reconnaissance may appear on other objects and every reported object must be nameable. Kernels without link ids skip the link half and say so. t_tamper is adjusted to the new field names.

Verification

krun VMs, quark-test -v t_tamper t_tamper_progs t_map_freeze t_trusted_map_rdonly t_trusted_pid:

Kernel Result
Fedora 43, 7.2.5 ok
RHEL 8.5, 4.18.0-348 ok
RHEL 9.3, 5.14.0-362 ok
Ubuntu 22.04, 6.8.0-138 ok

Negative: with the program id set left empty, t_tamper_progs aborts on its ordering assertion when the link events arrive without the program events before them, so it pins the new half. All four kernels above had link ids, so the link half ran everywhere, RHEL 8.5 included.

Full eBPF suite (quark-test -b) and a full make including the Go bindings pass on Fedora 43.

@nicholasberlin
nicholasberlin requested a review from a team as a code owner September 15, 2026 20:15
@nicholasberlin
nicholasberlin marked this pull request as draft September 15, 2026 20:16
@nicholasberlin
nicholasberlin force-pushed the trusted-map-tamper-events branch 2 times, most recently from e8d115d to 9040906 Compare September 23, 2026 13:36
@nicholasberlin
nicholasberlin force-pushed the tamper-progs-links branch 2 times, most recently from ba0aeda to d13e2b2 Compare September 23, 2026 14:18
@nicholasberlin
nicholasberlin force-pushed the trusted-map-tamper-events branch from 9040906 to b3231bb Compare September 23, 2026 14:18
The published trusted-map bypass starts by walking programs, matching
module_load and reading its map ids out of OBJ_GET_INFO_BY_FD. Maps
were covered, programs were not, so the first step stayed silent.
Links are the other half: the seven cgroup attachments are the only
ones bpf(2) can detach (LINK_DETACH, LINK_UPDATE, or a legacy
PROG_DETACH by attach type), and every tracing, raw tracepoint and
perf link, the two tamper tracepoints included, has no detach op and
answers EOPNOTSUPP. Both the attempt and the answer are worth seeing.

Two more id sets, elastic_ebpf_events_prog_ids and _link_ids, filled
like the map set and hardened the same way. Since links exist only
after attach, the fill and the freeze now run after bpf_probes__attach()
instead of after load; the probes run unfrozen for the microseconds in
between. A link attached the legacy way has no id and is skipped,
nothing in bpf(2) can reach it anyway.

The probe classifies the object behind an fd by the name of its
anonymous inode's dentry, "bpf-map", "bpf-prog" or "bpf-link", then
reads the id from the matching struct and looks it up in that kind's
set. Guessing from the layout is not an option: a program keeps its
attach type where a link keeps its id, so a bpftool run on a host with
any other fentry or cgroup program would have reported our links as
touched. New commands: PROG_GET_FD_BY_ID, LINK_GET_FD_BY_ID,
OBJ_GET_INFO_BY_FD on any kind, PROG_TEST_RUN, LINK_DETACH,
LINK_UPDATE, PROG_ATTACH, PROG_DETACH, LINK_CREATE,
RAW_TRACEPOINT_OPEN and PROG_BIND_MAP with one of our programs. A
legacy PROG_DETACH by attach type alone is not reported: our cgroup
programs are link attached with BPF_F_ALLOW_MULTI, and in multi mode
the kernel only detaches entries whose program and link both match,
so such a detach cannot remove ours and would only have echoed
systemd's own cgroup housekeeping.

quark_tamper loses map_id and map_name for kind, id and name, where
kind is QUARK_TAMPER_{MAP,PROG,LINK}. A link is named after the
program it attaches. No consumer had used the old fields. Static
asserts tie the flag and kind values to the probe's.

Not deduplicated: bpftool prog show does OBJ_GET_INFO_BY_FD on every
program, so one invocation produces an event per program of ours on
top of the ones per map. The right key for collapsing reconnaissance
depends on how the consumer uses the event, so that is left for when
there is one.

t_tamper_progs forks a child that walks program ids to our always
loaded sched_process_fork, asks for its info, runs it with
PROG_TEST_RUN (refused, tracing programs have no runnable test), then
walks link ids to its link and calls LINK_DETACH, expecting
EOPNOTSUPP. Only reconnaissance may appear on other objects and every
reported object must be nameable. Kernels without link ids skip the
link half. t_tamper is adjusted to the new field names.

Verified in the krun VMs on Fedora 43, RHEL 8.5, RHEL 9.3 and Ubuntu
22.04, all four with link ids. With the program id set left empty,
t_tamper_progs aborts on its ordering assertion when the link events
arrive without the program events before them, so it pins the new
half.

This branch has not been deployed

No deployments
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.

1 participant