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

Stale undersized store buffers: BIOCSBLEN after bpfdetach (bd_bif==NULL, bd_sbuf allocated) desynchronizes bd_bufsize from the kmalloc'd buffers -> ~512KiB controlled overflow past kmalloc(4096)

Summary

BIOCSETBLEN's only guard is bd_bif != NULL while bpf_setif() (re)allocates buffers only when bd_sbuf == NULL. bpfdetach() (ifconfig destroy) leaves the descriptor with bd_bif == NULL but buffers retained. In that window BIOCSBLEN(0x80000) is accepted, raising bd_bufsize to 512KiB over the still-4096-byte buffers; the next BIOCSETIF skips bpf_allocbufs() and catchpacket() then appends packets at bd_sbuf+curlen for up to 512KiB - hundreds of KiB of attacker packet data past a 4096-byte allocation. Unlike DF-3381 the per-packet copy lengths are legitimate, so the kernel keeps running after the corruption (corrupt-and-continue). VERIFIED on guest: tun0 create/SETIF/destroy/BIOCSBLEN(512KiB)/SETIF(lo0)/512 loopback packets -> program exited 0, kernel died seconds later with 'Fatal trap 9: general protection fault' at cache_nlookup dereferencing a corrupted namecache object - cross-subsystem heap corruption. Strongest primitive of this audit (survive-and-corrupt): slab-groom + victim overwrite documented; uid0 chain not completed (destroy step needs root; time budget went to fix validation). Fix validated (reject BIOCSBLEN once buffers exist): EINVAL, guest survives.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3382 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.7 KB ↓ raw
VERDICT.md β€” 3.6 KB ↓ raw
trigger.c β€” 2.5 KB view raw
build.sh β€” 81 B view raw
run.sh β€” 114 B view raw
run.log β€” 1003 B view raw
run.patched.log β€” 469 B view raw
panic.txt β€” 276 B view raw
env.txt β€” 255 B view raw
fix.diff β€” 1.6 KB view raw
code_hash.txt β€” 151 B view raw
verdict.json β€” 3.6 KB view raw

DF-3382 β€” stale undersized BPF store buffers after interface destroy + BIOCSBLEN

What

BIOCSETBLEN guards only against d->bd_bif != NULL (bpf.c:795), but the buffer allocation in bpf_setif() guards on d->bd_sbuf == NULL (bpf.c:1128). After bpfdetach() (interface destroyed) a bound descriptor keeps its allocated buffers with bd_bif == NULL (bpf_detachd, bpf.c:320-326 via bpfdetach bpf.c:1580-1583). In that state:

  1. BIOCSBLEN(0x80000) succeeds (guard passes) β€” bd_bufsize is raised to 512 KiB while the kmalloc'd store/free buffers stay 4096 bytes (BPF_DEFAULTBUFSIZE).
  2. BIOCSETIF(newif) sees bd_sbuf != NULL and skips bpf_allocbufs().
  3. catchpacket() now believes it has 512 KiB of store buffer and appends packets at bd_sbuf + curlen for ~512 KiB β€” writing hundreds of KiB of attacker packet data past the 4096-byte allocation.

Reproduce (in guest)

ifconfig tun0 create
cc -O -o trigger trigger.c
./trigger

Expected: mass kernel heap corruption β†’ panic/fatal trap shortly after the loopback flood begins.

Observed: all 512 packets sent (lo0 tap appends ~1.4 KiB each into a 4096-byte buffer with bd_bufsize=524288), process printed "survived", and the kernel died seconds later in the namecache path (see panic.txt):

Fatal trap 9: general protection fault while in kernel mode
cpuid = 0; lapic id = 0
instruction pointer = 0x8:0xffffffff806dea20
Stopped at cache_nlookup_maybe_shared+0x100: cmpq %rax,0x48(%rbx)

The GPF'd cmpq was reading a corrupted namecache object β€” the overflow had crossed into kernel slab memory filled with 0x41 ('A') payload bytes.

Fix

Same fix.diff as DF-3381: BIOCSETBLEN is rejected once bd_sbuf != NULL.

VERDICT.md
↓ download raw

DF-3382 VERDICT

Reproduced: YES (first attempt)

Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS).

Exact sequence (run.log):

  1. ifconfig tun0 create (if_tun is compiled in; bpfattach DLT_NULL, if_tun.c:239).
  2. open /dev/bpf1, BIOCSETIF("tun0") β†’ bpf_allocbufs() allocates two 4096-byte buffers (bpf.c:1475-1476).
  3. ifconfig tun0 destroy β†’ bpfdetach() β†’ bpf_detachd(): bd_bif=NULL, buffers retained (bpf.c:320-326).
  4. BIOCSBLEN(0x80000) β†’ accepted (bd_bif==NULL, bpf.c:795-805) β†’ bd_bufsize=524288. Program printed [+] BIOCSBLEN -> 524288 (buffers still 4096!).
  5. BIOCSETIF("lo0") β†’ bd_sbuf != NULL β†’ allocation skipped (bpf.c:1128).
  6. 512 loopback UDP packets of 1400 'A' bytes: every catchpacket() appends at bd_sbuf+curlen; rotation only when curlen+totlen > 512KiB, so the write stream runs ~731 KiB through a 4096-byte allocation.

Result: the process itself completed ("survived", exit 0) but the kernel died moments later with a general protection fault in cache_nlookup_maybe_shared+0x100 while dereferencing a corrupted namecache link (panic.txt from the serial console). That is heap corruption consuming unrelated kernel subsystems β€” a stronger demonstration than a direct panic: ~727 KiB of controlled 0x41 data was written past the victim allocation.

Primitive characterization

  • What: effectively unbounded linear heap overflow (up to bd_bufsize = 512 KiB per store-buffer cycle) with fully controlled content (packet bytes) and start offset (curlen advances by captured packet sizes).
  • Survivability: unlike DF-3381 the copy length is legitimate per packet; the kernel kept running for seconds after corrupting ~727 KiB of adjacent memory β€” a corrupt-and-continue primitive, strictly more exploitable than DF-3381's fixed-4GiB copy.
  • Privilege gate: reaching step 3 requires destroying an interface (root). Steps 4-6 require only the open bpf descriptor, so like DF-3381 the ioctls and traffic can come from a privilege-dropped fd holder; the destroy itself needs root, making the practical model root-configured / unprivileged-triggered.
  • uid0 route: not completed. A credible chain would spray kmalloc(4096) victim objects (e.g. file-table entries, kqueue structures) around the store buffer, overflow with forged objects, and resume execution β€” the guest has no SMAP/SMEP/KASLR β€” but the trigger needs a root-orchestrated interface destroy, so a real-world privilege boundary is only crossed in the dropped-daemon-holding-fd scenario. Documented as the ceiling; not pursued to a uid0 shell within this run (time budget spent on fix validation, which is mandatory).

Fix validation

fix.diff (shared with DF-3381): BIOCSETBLEN now also rejects when d->bd_sbuf != NULL. On the patched kernel the PoC fails cleanly at step 4 (BIOCSBLEN: Invalid argument, run.patched.log), guest survives, and the identical sequence cannot desynchronize bd_bufsize from the allocated buffers. fix_baseline_reproduced=1, fix_patched_reproduced=0.

kernel refs: sys/net/bpf.c:794-806 (BIOCSBLEN guard), 1128-1134 (setif alloc guard), 320-326 (bpf_detachd keeps buffers), 1580-1583 (bpfdetach loop), 1418-1463 (catchpacket appends).

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline: Fatal trap 9 GPF in cache_nlookup_maybe_shared after ~727KiB overflow. Patched: identical sequence fails cleanly at BIOCSBLEN: EINVAL (bd_sbuf != NULL guard); guest survives.

['run.patched.log']
↓ fix.diffDragonFly 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

Exploit chain

ifconfig tun0 create -> open /dev/bpf1 + BIOCSETIF(tun0) [alloc 2x4096] -> ifconfig tun0 destroy [bd_bif=NULL, buffers kept] -> BIOCSBLEN(512K) [accepted] -> BIOCSETIF(lo0) [alloc skipped] -> 512 loopback packets -> catchpacket writes ~727KiB of controlled data past kmalloc(4096) -> namecache corruption -> GPF panic. uid0 chain (slab groom + victim overwrite) not completed: trigger's destroy step is root-only; time budget spent on mandatory fix validation.

Evidence (decisive lines)

['panic.txt: Fatal trap 9 GPF at cache_nlookup_maybe_shared+0x100', "run.log: full stdout incl. 'BIOCSBLEN -> 524288 (buffers still 4096!)' and 512 packets sent", 'VERDICT.md: primitive characterization and corrupt-and-continue analysis']

PoC changes

Written fresh: uses tun0 (if_tun is compiled into X86_64_GENERIC) as the destroyable bpf-attached interface; floods lo0 with 1400-byte UDP packets after rebinding; disc(4) rejected because it is not clone-destroyable.

Verified recommended fix

Reject BIOCSETBLEN when bd_sbuf != NULL (validated in fix.diff).

Verdict

BIOCSETBLEN guards only bd_bif!=NULL while bpf_setif() allocates buffers only when bd_sbuf==NULL; after ifconfig destroy detaches the descriptor (bd_bif=NULL, buffers retained), BIOCSBLEN raises bd_bufsize to 512KiB over the still-4096-byte kmalloc buffers and the next BIOCSETIF skips bpf_allocbufs, so catchpacket() appends ~512KiB of packets into the 4096-byte store buffer. Reproduced on the stock guest: 512 controlled 1.4KiB 'A' packets written ~727KiB past the allocation; kernel survived the flood then died with 'Fatal trap 9: general protection fault' in cache_nlookup_maybe_shared+0x100 dereferencing a corrupted namecache object. This is a corrupt-AND-continue primitive (kernel ran seconds after the overflow) with fully controlled content - the strongest of the four findings; a uid0 chain (spray kmalloc(4096) victims around the store buffer) is plausible on this no-SMAP/SMEP/KASLR guest but the destroy-interface step requires root, so no privilege boundary beyond the bpf-fd holder is crossed; documented as ceiling. Fix validated on rebuilt kernel: BIOCSBLEN now returns EINVAL once buffers exist, guest survives.