Report bpf(2) access to our programs and links too - #426
Draft
nicholasberlin wants to merge 1 commit into
Draft
nicholasberlin wants to merge 1 commit into
nicholasberlin wants to merge 1 commit into
Conversation
nicholasberlin
marked this pull request as draft
September 15, 2026 20:16
nicholasberlin
force-pushed
the
tamper-progs-links
branch
from
September 23, 2026 12:29
e47a08a to
fca46f3
Compare
nicholasberlin
force-pushed
the
trusted-map-tamper-events
branch
2 times, most recently
from
September 23, 2026 13:36
e8d115d to
9040906
Compare
nicholasberlin
force-pushed
the
tamper-progs-links
branch
2 times, most recently
from
September 23, 2026 14:18
ba0aeda to
d13e2b2
Compare
nicholasberlin
force-pushed
the
trusted-map-tamper-events
branch
from
September 23, 2026 14:18
9040906 to
b3231bb
Compare
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.
nicholasberlin
force-pushed
the
trusted-map-tamper-events
branch
from
September 23, 2026 15:05
b3231bb to
8a2c26f
Compare
nicholasberlin
force-pushed
the
tamper-progs-links
branch
from
September 23, 2026 15:05
d13e2b2 to
dbebe8c
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #425. Merge that first, then I will retarget this one.
Motivation
The published trusted-map bypass starts by walking programs, matching
module_loadand reading its map ids out ofOBJ_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 onesbpf(2)can detach, throughLINK_DETACH,LINK_UPDATE, or a legacyPROG_DETACHby attach type. Every tracing, raw tracepoint and perf link, the two tamper tracepoints included, has no detach callback and answersEOPNOTSUPP. Both the attempt and the answer are worth seeing.Change
elastic_ebpf_events_prog_idsandelastic_ebpf_events_link_ids, filled like the map set and hardened the same way: read-only for programs, frozen.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 inbpf(2)can reach them anyway.bpf-map,bpf-progorbpf-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: astruct bpf_progkeepsexpected_attach_typewhere astruct bpf_linkkeeps its id, both at offset 8, so abpftool prog showon 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.PROG_GET_FD_BY_ID,LINK_GET_FD_BY_ID,OBJ_GET_INFO_BY_FDon any kind,PROG_TEST_RUN,LINK_DETACH,LINK_UPDATE, andPROG_ATTACH,PROG_DETACH,LINK_CREATE,RAW_TRACEPOINT_OPEN,PROG_BIND_MAPwhen they name one of our programs.PROG_DETACHby attach type alone is no longer reported. Verified in the pinnedkernel/bpf/cgroup.c:cgroup_bpf_link_attach()attaches withBPF_F_ALLOW_MULTI, and in multi modefind_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_tamperlosesmap_idandmap_nameforkind,idandname, with kind one ofQUARK_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 showdoesOBJ_GET_INFO_BY_FDon 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_progsforks a child that walks program ids to our always loadedsched_process_fork, asks for its info, runs it withPROG_TEST_RUN(refused, tracing programs have no runnable test), then walks link ids to its link and callsLINK_DETACH, expectingEOPNOTSUPP. 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_tamperis 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:Negative: with the program id set left empty,
t_tamper_progsaborts 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 fullmakeincluding the Go bindings pass on Fedora 43.