β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3384

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3384 Β· 7 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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)

  1. v1: single-shot, 1 s stagger, anon read buffer β€” read#1 completed before flush; no interference.
  2. 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.
  3. v3: flush thread spamming BIOCFLUSH continuously across read#1 β€” no hit in 60 attempts.
  4. 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_testable
baseline no→ patch + rebuild →patched clean

Baseline 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']
DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Fri Sep 11 22:24:51 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (fix.diff applied; make nativekernel + installkernel in guest)

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.