BIOCFLUSH racing an in-flight bpfread() breaks the buffer-slot invariants: unconditional bd_fbuf = bd_hbuf after uiomove() loses the hold-buffer pointer (leak) and later leaves bd_sbuf == NULL so catchpacket() writes the bpf header through NULL
Summary
bpfread() ends with uiomove(bd_hbuf...) followed by an unconditional d->bd_fbuf = d->bd_hbuf; d->bd_hbuf = NULL. If the reader blocks inside uiomove (user page fault releases bpf_token) and another thread holding a dup'd descriptor issues BIOCFLUSH, bpf_resetd() parks the hold buffer in bd_fbuf; the resuming reader then clobbers bd_fbuf with the now-NULL bd_hbuf - the buffer pointer is permanently lost (kmalloc leak). After the next immediate-mode read, ROTATE_BUFFERS sets bd_sbuf = bd_fbuf = NULL, and the next tapped packet makes catchpacket() write the bpf header and packet data at address 0 (kernel write to NULL with partially attacker-controlled bh_datalen). Four PoC revisions (incl. flush spam against cold evicted file-backed pages of a 3.5GiB file): the interleaving never landed on this virtualized guest - zero-fill faults complete without yielding the token and virtio faults too short - not_reproduced (timing/hardware), defect likely from the code arithmetic alone; a genuinely blocking fault source would provide the window. The NULL-write consequence is mitigated by the validated DF-3381 fix's bd_sbuf==NULL guard; the leak needs a reader-active flag.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3384 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 952 B | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| trigger.c | β | 3.1 KB | view raw | |
| run.log | β | 450 B | view raw | |
| env.txt | β | 255 B | view raw | |
| code_hash.txt | β | 76 B | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-3384 β BIOCFLUSH concurrent with in-flight bpfread() loses the buffer-slot invariants
bpfread() (sys/net/bpf.c:541-545) clobbers bd_fbuf with bd_hbuf after uiomove(); if the reader blocked in uiomove (page fault -> bpf_token released) and a BIOCFLUSH on a dup'd fd ran bpf_resetd() meanwhile (bpf.c:674-686), the buffer pointer parked in bd_fbuf is lost: permanent kmalloc leak, and after the next immediate-mode read bd_sbuf becomes NULL, so the next tapped packet makes catchpacket() write the bpf header through a NULL store buffer (bpf.c:1455-1458) - kernel write to address 0.
Reproduce attempt
cc -O -o trigger trigger.c -pthread ; ./trigger Three variants tried (timing-scan over anon faults; BIOCFLUSH spam with anon faults; BIOCFLUSH spam with cold file-backed faults). None landed on this virtualized guest - see VERDICT.md for why (faults never yield the token long enough). not_reproduced, defect likely from code arithmetic.
DF-3384 VERDICT
Reproduced: NO (3 strategies, 260+ attempts) β code-certain interleaving, timing not landed on this guest
Claim: bpfread() finishes uiomove() and then unconditionally executes
(bpf.c:541-545):
error = uiomove(d->bd_hbuf, d->bd_hlen, ap->a_uio);
d->bd_fbuf = d->bd_hbuf; /* <-- unconditional clobber */
d->bd_hbuf = NULL;
d->bd_hlen = 0;
If the reader blocks inside uiomove (user page fault), bpf_token is
released (lwkt tokens are dropped on block); a concurrent BIOCFLUSH on a
dup'd descriptor then runs bpf_resetd() (bpf.c:674-686):
if (d->bd_hbuf) {
d->bd_fbuf = d->bd_hbuf; /* parks the hold buffer */
d->bd_hbuf = NULL;
}
and the resuming reader overwrites bd_fbuf with the now-NULL bd_hbuf,
losing the buffer pointer (2-4096 byte... bd_bufsize-byte kmalloc leak
per win; bpf_freed() cannot free it). After the next immediate-mode read
the store slot itself is NULL (ROTATE_BUFFERS sets bd_sbuf = bd_fbuf),
and the next tapped packet makes catchpacket() write the bpf header
through hp = (struct bpf_hdr *)(d->bd_sbuf + curlen) == NULL
(bpf.c:1455-1458) β a kernel write to address 0 (bh_tstamp, then
bh_datalen = pktlen attacker-controlled, then the packet copy at +hdrlen).
Attempts (guest DragonFly 6.5-DEVELOPMENT, INVARIANTS, 4 CPU)
- v1: single-shot, 1 s stagger, anon read buffer β read#1 completed before flush; no interference.
- v2: 200 attempts, 512 KiB hold buffer (128 anon zero-fill page faults inside one uiomove), flush delay scanned 300 Β΅s..20 ms β no hit.
- v3: flush thread spamming
BIOCFLUSHcontinuously across read#1 β no hit in 60 attempts. - v4: read buffer = 128 cold file-backed pages of a 3.5 GiB file (written past RAM size so pages are evicted; hammer2 + virtio disk) β ran 7+ minutes of fault+flush storm without a panic before being stopped; guest stayed up.
Conclusion: on this virtualized guest the copyout fault path never yields
the token for a long enough window for the flushing thread to acquire it
(zero-fill faults complete in-place; host-page-cached virtio disk faults
are too short, and the token ping-pong between a spamming ioctl thread and
a faulting reader doesn't serialize the flush inside the uiomove). The
interleaving is not contradicted by anything in the code β it requires a
genuinely blocking fault (real slow disk, NFS, swap-backed page, memory
pressure) which this guest cannot present. Honest status:
not_reproduced; confidence in the defect remains likely from code
arithmetic alone.
Impact if landed
Kernel memory leak (one bd_bufsize buffer per race win, unrecoverable
until close) + guaranteed NULL-address write (DoS panic). Requires two
threads sharing a bpf fd (dup) β privileged trigger (root-owned fd or
dropped-priv holder). Not a credential-boundary crossing beyond the
bpf-fd holder. The NULL-write is mitigated by the bd_sbuf == NULL guard
in the validated DF-3381 fix.diff; the leak needs a bd_reading flag or
equivalent around uiomove (upstream-grade fix, out of scope here).
kernel refs: sys/net/bpf.c:541-545, 674-686, 475-531, 439-444, 1455-1463.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
(No fix.diff for this item: documented recommendation only - see Fix above.)
Fix verification
not_testableBaseline race never reproduced on this guest (3 strategies), so patched-vs-baseline comparison is not meaningful; the bd_sbuf==NULL catchpacket guard from the validated fix.diff is present in the patched kernel regardless.
['run.log', 'VERDICT.md']
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log: v1/v2/v3/v4 attempt summaries, all survived', 'VERDICT.md: full interleaving derivation and why each variant failed']
PoC changes
Evolved through 4 revisions: single-shot stagger -> timing scan with 512KiB hold buffer (128 faults per uiomove) -> flush-spam thread -> cold file-backed read buffer (3.5GiB file written past RAM to force eviction).
Verified recommended fix
Guard catchpacket against bd_sbuf==NULL (in fix.diff) and stop bpfread from clobbering bd_fbuf when bd_hbuf changed under it (e.g. re-check after uiomove, or a bd_reading flag consulted by bpf_resetd).
Verdict
bpfread() unconditionally executes bd_fbuf = bd_hbuf after uiomove(); if the reader blocks inside uiomove (user page fault releases bpf_token) and a concurrent BIOCFLUSH on a dup'd fd ran bpf_resetd() meanwhile, the hold-buffer pointer parked in bd_fbuf is lost (permanent kmalloc leak), and after the next immediate-mode read the store slot is NULL, making catchpacket() write the bpf header through hp = bd_sbuf+curlen == NULL (kernel write to address 0). Code interleaving is certain from arithmetic on bpf.c:541-545 vs 674-686 and ROTATE_BUFFERS semantics, but it did NOT reproduce on this guest across three strategies (timing scan over 128 anon zero-fill faults x200 attempts; continuous BIOCFLUSH spam x60; flush spam against cold evicted file-backed pages of a 3.5GiB file, 7+ min) - on this virtualized host the copyout fault never yields bpf_token long enough for the flusher to acquire it mid-copy. Impact if landed: one bd_bufsize-byte kernel memory leak per win plus a NULL-address write (DoS); requires two threads sharing a bpf fd (privileged device). not_reproduced (timing), defect likely.
No comments yet.