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)
PoC verification
Evidence pack
findings/poc/DF-3382 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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:
BIOCSBLEN(0x80000)succeeds (guard passes) βbd_bufsizeis raised to 512 KiB while the kmalloc'd store/free buffers stay 4096 bytes (BPF_DEFAULTBUFSIZE).BIOCSETIF(newif)seesbd_sbuf != NULLand skipsbpf_allocbufs().catchpacket()now believes it has 512 KiB of store buffer and appends packets atbd_sbuf + curlenfor ~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.
DF-3382 VERDICT
Reproduced: YES (first attempt)
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC, INVARIANTS).
Exact sequence (run.log):
ifconfig tun0 create(if_tun is compiled in; bpfattach DLT_NULL, if_tun.c:239).- open
/dev/bpf1,BIOCSETIF("tun0")βbpf_allocbufs()allocates two 4096-byte buffers (bpf.c:1475-1476). ifconfig tun0 destroyβbpfdetach()βbpf_detachd():bd_bif=NULL, buffers retained (bpf.c:320-326).BIOCSBLEN(0x80000)β accepted (bd_bif==NULL, bpf.c:795-805) βbd_bufsize=524288. Program printed[+] BIOCSBLEN -> 524288 (buffers still 4096!).BIOCSETIF("lo0")βbd_sbuf != NULLβ allocation skipped (bpf.c:1128).- 512 loopback UDP packets of 1400 'A' bytes: every
catchpacket()appends atbd_sbuf+curlen; rotation only whencurlen+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 (curlenadvances 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
fixedBaseline: 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']
Confirmed kernel references
- sys/net/bpf.c:794-806
- sys/net/bpf.c:1128-1134
- sys/net/bpf.c:320-326
- sys/net/bpf.c:1580-1583
- sys/net/bpf.c:1418-1463
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.
No comments yet.