Lock down the eBPF maps against bpf(2) strangers - #424
Merged
Merged
Conversation
nicholasberlin
marked this pull request as draft
September 14, 2026 20:55
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 14, 2026 21:03
1c3c517 to
5901dc3
Compare
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 15, 2026 19:11
5901dc3 to
a2145d3
Compare
Member
|
@nicholasberlin Ping me if this is ready for review (it's still marked draft) |
nicholasberlin
marked this pull request as ready for review
September 21, 2026 13:52
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 21, 2026 13:52
a2145d3 to
65c6502
Compare
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 22, 2026 14:34
65c6502 to
a0b547d
Compare
Contributor
Author
This is ready for review @christos68k (thanks) |
| * buffer is only ever copied from, by anyone, so it gets the flag too | ||
| * and is frozen below. | ||
| */ | ||
| harden = bpf_map_harden_supported(); |
Contributor
There was a problem hiding this comment.
comment said we want to bump memlock first, then check with probe, but here it's reversed?
Contributor
Author
There was a problem hiding this comment.
Good catch. Force pushed a fix, here's the compare link
christos68k
reviewed
Sep 23, 2026
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 23, 2026 12:29
a0b547d to
d21177e
Compare
christos68k
previously approved these changes
Sep 23, 2026
The trusted pids map is written from userspace and only ever read by the probes, yet nothing stopped a BPF program from writing to it. A privileged process can still update the map through bpf(2), but with BPF_F_RDONLY_PROG it can no longer smuggle a writer in as BPF code: the verifier refuses any program that writes the map, ours or a stranger's. Every other map except the ring buffer is the opposite case: written only by the probes, never by userspace. Freeze them after load with BPF_MAP_FREEZE so no process can plant or erase our state through bpf(2). Without that a stranger could pre-mark its own mprotect transitions as seen in mprotect_seen, or poison sk_to_tgid to misattribute connections. The init buffer is only ever copied from, so it gets both the flag and the freeze. libbpf's internal maps (.rodata) are skipped, libbpf freezes those itself; so are maps that were never created, which still carry a placeholder fd. Both mechanisms arrived in Linux 5.2 and older kernels reject them with EINVAL, so probe with a throwaway map first and leave the maps as they were on kernels that don't know them. Any other probe failure is warned about rather than mistaken for an old kernel, and the memlock bump moves ahead of the probe so a tight RLIMIT_MEMLOCK can't fail it. The RHEL 8.5 CI kernel (4.18.0-348) carries the backports and takes both. Freezing is best effort: the probe only proves a plain hash map freezes and a backport could still refuse another type, so a failed freeze is warned about and that map left as it was, rather than refusing to open. t_trusted_map_rdonly finds the trusted map the way a stranger would, by walking every map id and matching type, sizes and name prefix, then checks the flag is set, that userspace writes still work (both the library calls and a raw bpf(2) update) and that a hand assembled program calling map_lookup_elem loads while the same program calling map_update_elem is refused with EACCES. t_map_freeze checks a raw bpf(2) update and delete on sk_to_tgid and on the init buffer fail with EPERM, and that the init buffer carries BPF_F_RDONLY_PROG. Both skip on kernels without the features. Verified in the krun VMs on Fedora 43, RHEL 8.5, RHEL 9.3 and Ubuntu 22.04. With only the trusted map flag reverted t_trusted_map_rdonly aborts on the map_flags assertion; with only the freeze loop skipped t_map_freeze aborts on the EPERM assertion, so each test pins its half.
nicholasberlin
force-pushed
the
trusted-map-rdonly-prog
branch
from
September 23, 2026 14:18
d21177e to
5e02bd7
Compare
Contributor
Author
|
@christos68k and @stanek-michal thanks for the reviews, the follow-on PRs revealed a bug in the test code. Some stack garbage needing clearing. |
christos68k
approved these changes
Sep 23, 2026
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.
Motivation
The trusted pids map is written from userspace and only ever read by the probes, yet nothing stopped a BPF program from writing to it. A privileged process can still update the map through
bpf(2), but withBPF_F_RDONLY_PROGit can no longer smuggle a writer in as BPF code: the verifier refuses any program that writes the map, ours or a stranger's.Every other map except the ring buffer is the opposite case: written only by the probes, never by userspace. Freezing them with
BPF_MAP_FREEZEmeans no process can plant or erase quark's state throughbpf(2). Without that a stranger could pre-mark its own mprotect transitions as seen inmprotect_seen, or poisonsk_to_tgidto misattribute connections.This does not address the
bpf(2)update route to the trusted map itself, which is the subject of #425.Change
bpf_queue.c:BPF_F_RDONLY_PROGandBPF_MAP_FREEZE. Both arrived in Linux 5.2 and older kernels reject them outright. On such kernels the maps are left as they were, with a debug log line.BPF_F_RDONLY_PROGonelastic_ebpf_events_trusted_pidsand onelastic_ebpf_events_init_buffer, which is only ever copied from.bpf_map_freeze()on every map except the trusted map and the ring buffer. libbpf's internal maps (.rodata) are skipped, libbpf freezes those itself. Maps never created because autocreate is off are skipped by that flag, since libbpf still hands them a placeholder fd.EINVALfrom the probe means an old kernel; anything else is warned about. The memlock bump now runs before the probe so a tightRLIMIT_MEMLOCKcannot silently disable the hardening.CHANGESentry.Test
t_trusted_map_rdonlyfinds the trusted map the way a stranger would, by walking every map id and matching type, sizes, entry count and name prefix (the kernel truncates names to 15 characters, so everyelastic_ebpf_events_*map shares one), then checks:bpf(2)update and deletemap_lookup_elemon the map loads, while the same program callingmap_update_elemis refused withEACCESt_map_freezechecks a rawbpf(2)update and delete onsk_to_tgidfail withEPERM, and that the init buffer both refuses a write and carriesBPF_F_RDONLY_PROG.Both skip on kernels without the features.
Verification
krun VMs,
quark-test -v t_trusted_pid t_trusted_map_rdonly t_map_freeze:Negative: with only the trusted map flag reverted,
t_trusted_map_rdonlyaborts on themap_flagsassertion. With only the freeze loop skipped,t_map_freezeaborts on theEPERMassertion. Each test pins its half.Full eBPF suite (
quark-test -b) passes on Fedora 43.