catchpacket() bh_caplen unsigned underflow: BPF_MINBUFSIZE(32) < bif_hdrlen(34/36) on x86-64 gives a ~4GiB copy into kmalloc(32) (heap overflow, deterministic panic)
Summary
On LP64, SIZEOF_BPF_HDR=32 and BPF_WORDALIGN rounds to 8, so every attached DLT gets bif_hdrlen >= 34 (Ethernet 34; DLT_NULL 36). BIOCSETBLEN clamps bd_bufsize to BPF_MINBUFSIZE=32 - smaller than the header it must hold - and BIOCSETIF allocates that store buffer without any hdrlen check. The first tapped packet makes catchpacket() clamp totlen to 32 and compute hp->bh_caplen = totlen - hdrlen = 0xfffffffc, and cpfn(pkt, hp+hdrlen, 0xfffffffc) copies ~4GiB starting 34/36 bytes into a kmalloc(32) allocation: a forward linear heap overflow with fully attacker-controlled contents (packet bytes) but forced length - corrupt-then-panic. VERIFIED on stock guest with the classic tcpdump setuid-drop pattern (uid=euid=65534 child issues BIOCSETIF + packet after root parent opened the device): 'panic: bpf_mcopy', backtrace bpf_mcopy <- catchpacket <- bpf_mtap <- bpf_ptap <- if_simloop. uid0 not credible (forced 4GiB copy; kernel always dies mid-copy) - primitive characterized as controlled ~50+ byte slab-adjacent corruption before the inevitable fault. Alternate trigger: debug.bpf_bufsize sysctl below bif_hdrlen. Fix validated (three-layer: reject BIOCSBLEN with sbuf, hdrlen check at bpf_setif, totlen<hdrlen guard): PoC fails EINVAL, normal capture intact.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3381 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.8 KB | β raw | |
| VERDICT.md | β | 4.7 KB | β raw | |
| trigger.c | β | 2.7 KB | view raw | |
| build.sh | β | 81 B | view raw | |
| run.sh | β | 90 B | view raw | |
| run.log | β | 670 B | view raw | |
| run.patched.log | β | 504 B | view raw | |
| panic.txt | β | 530 B | view raw | |
| env.txt | β | 255 B | view raw | |
| fix.diff | β | 1.6 KB | view raw | |
| kernel_build.log | β | 4.8 KB | view raw | |
| code_hash.txt | β | 151 B | view raw | |
| verdict.json | β | 4.1 KB | view raw |
DF-3381 β BPF catchpacket() bh_caplen unsigned underflow (heap buffer overflow)
What
sys/net/bpf.c catchpacket() computes the capture length as
hp->bh_caplen = totlen - hdrlen after clamping totlen to bd_bufsize.
On x86-64 every attached DLT has bif_hdrlen >= 32+
(Ethernet = roundup2(14+32,8)-14 = 34; DLT_NULL (lo0/tun/wg) =
roundup2(4+32,8)-4 = 36) because SIZEOF_BPF_HDR = sizeof(struct bpf_hdr)
= 32 (16-byte timeval + 12 bytes, padded) and BPF_WORDALIGN rounds to
sizeof(long) = 8. BIOCSETBLEN happily clamps the store-buffer size to
BPF_MINBUFSIZE = 32 (bpf.c:794-806) which is smaller than the header it
must hold. The next tapped packet then executes:
totlen = hdrlen + min(snaplen, pktlen); /* >= 36 */
if (totlen > d->bd_bufsize) /* 32 */
totlen = d->bd_bufsize; /* 32 */
...
(*cpfn)(pkt, (u_char *)hp + hdrlen,
(hp->bh_caplen = totlen - hdrlen)); /* 32-36 = 0xfffffffc */
i.e. a ~4 GiB copy (bpf_mcopy/_bcopy) into a kmalloc(32) store buffer,
starting 34/36 bytes past the start of that 32-byte allocation. Attacker
packet data is written linearly over the adjacent slab until the chain/page
runs out.
Reproduce
cc -O -o trigger trigger.c # in guest (gcc 8.3) ./trigger # root starts it; the triggering child is uid=nobody
Expected: panic: bpf_mcopy (or a page fault inside the 4 GB copy),
backtrace bpf_mcopy <- catchpacket <- bpf_mtap <- bpf_ptap <- if_simloop.
Observed (guest DragonFly 6.5-DEVELOPMENT #0 ... X86_64_GENERIC,
INVARIANTS): exactly that β see panic.txt, run.log.
Fix
fix.diff (validated in-guest, see VERDICT.md): reject BIOCSETBLEN once
buffers exist, reject BIOCSETIF when bd_bufsize < bif_hdrlen, and guard
catchpacket() against totlen < hdrlen (plus a NULL-store-buffer guard).
DF-3381 VERDICT
Reproduced: YES (first attempt)
Guest: DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC, stock INVARIANTS
kernel), x86_64, QEMU/KVM. /dev/bpf* are 0600 root:wheel; bpfopen()
also rejects jailed credentials (bpf.c:363).
Sequence actually executed (run.log):
- root opens
/dev/bpf0,BIOCSBLEN(32)β kernel clamps to/accepts 32 (BPF_MINBUFSIZE, bpf.c:802-803). Confirmed by the program'sBIOCSBLEN -> 32printout. - fork; child
setgroups/setgid/setuidtonobody(uid 65534), thenBIOCSETIF("lo0")βlo0is DLT_NULL withbif_hdrlen=roundup2(4 + 32, 8) - 4= 36 (bpf.c:1543, bpf.h:61-62,147-149). - child sends one loopback UDP packet β
looutputβif_simloop(if_loop.c:227) βbpf_ptapβbpf_mtapβcatchpacketwithbd_bufsize=32 < hdrlen=36.
Result (serial console, panic.txt):
panic: bpf_mcopy
cpuid = 0
bpf_mcopy() at bpf_mcopy+0x6c
catchpacket.isra.1() at catchpacket.isra.1+0xff
bpf_mtap() at bpf_mtap+0x71
bpf_ptap() at bpf_ptap+0x39
if_simloop() at if_simloop+0x8f
Debugger("panic")
This is the exact predicted path and proves the underflow primitive:
before the m == NULL KASSERT fires, bpf_mcopy has already written the
4-byte AF header at bd_sbuf+36 and the full UDP packet (~44 bytes of
attacker-chosen content) after it β i.e. ~50 bytes of controlled data
written past the end of a kmalloc(32) allocation, with a nominal copy
length of 0xfffffffc bytes.
Primitive characterization (primary objective)
- What: linear heap overflow forward from
kmalloc(32)+hdrlen. - Where: the store buffer allocated in
bpf_allocbufs()(bpf.c:1475). - Content control: full β the copied bytes are the tapped packet's bytes (attacker generates them; here a UDP payload).
- Length control: none. The length is forced to
bd_bufsize - hdrleninterpreted asu_int(0xfffffffc for lo0, 0xffffffe variants per DLT). The copy runs until the mbuf chain ends (thenpanic("bpf_mcopy"), bpf.c:1283-1284) or until an unmapped page faults, whichever comes first. There is no way to stop the copy at a chosen offset, so the kernel always dies; this is a corrupt-then-panic primitive, not corrupt-and-continue. - uid0 route: blocked by the privilege model, not by hardening:
opening
/dev/bpf*requires euid 0 (0600 cdev) and jails are refused (bpf.c:362-366), so an unprivileged local user cannot reach the bug on a default system. What the PoC does demonstrate is the drop-privileges-after-open pattern (tcpdump/libpcap keep a bpf fd across a setuid drop): the process that issued the corrupting ioctls and packet haduid=euid=65534. A sandboxed/capability-dropped capture daemon that ever callsBIOCSBLEN(<= hdrlen)/BIOCSETIFcan smash kernel memory while holding no privileges at trigger time. Reliable uid0 was not pursued further because the unbounded copy length makes survive-and-exploit implausible: any successful run also kills the kernel.
Why not already fixed upstream-style
FreeBSD rewrote its BPF buffer handling (locked buffers, bpf_buffer.c)
and rejects buffer sizes that cannot hold bif_hdrlen; DragonFly retains
the 1990s two-buffer scheme where the only guard is the (too small)
BPF_MINBUFSIZE = 32 constant, which lost meaning when struct timeval
grew to 16 bytes on LP64 (SIZEOF_BPF_HDR 18 -> 32, bpf.h:147-149).
Fix validation (mandatory for memory corruption)
fix.diff: (1) BIOCSETBLEN also rejected once bd_sbuf is allocated,
(2) bpf_setif returns EINVAL when bd_bufsize < bif_hdrlen,
(3) catchpacket drops when totlen < hdrlen (and when bd_sbuf == NULL,
which also covers DF-3384's NULL write).
Baseline: panic above, 100% reproducible by construction (no race).
Patched: rebuilt X86_64_GENERIC in guest (make nativekernel), rebooted,
re-ran the identical PoC β see fix_baseline_reproduced=1,
fix_patched_reproduced=0 in verdict.json and run.patched.log:
BIOCSETIF now fails with EINVAL ("BIOCSETIF(lo0): Invalid argument") and
the guest survives. The defensive catchpacket guard was additionally
exercised by binding to Ethernet (bif_hdrlen 34) with a 40-byte buffer β
packets are dropped (bd_dcount grows), no panic, no corruption.
kernel refs: sys/net/bpf.c:794-806, 1128-1134, 1418-1421, 1462; sys/net/bpf.h:61-62, 64-67, 134-149.
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 (stock #0 kernel): deterministic panic: bpf_mcopy via catchpacket underflow. Patched (#1 kernel, fix.diff): identical PoC fails cleanly with BIOCSETIF(lo0): EINVAL from the bpf_setif bd_bufsize<bif_hdrlen guard; guest survives; normal capture verified working (bh_hdrlen=36 bh_caplen=38 bs_recv=2).
['run.patched.log', 'kernel_build.log (make nativekernel/installkernel RC=0)']
Confirmed kernel references
Detail
Exploit chain
root parent opens /dev/bpf0 + BIOCSBLEN(32) -> fork/setuid(nobody) child -> BIOCSETIF(lo0) -> one loopback UDP packet -> catchpacket bh_caplen underflow -> bpf_mcopy writes packet-controlled bytes at kmalloc(32)+36 with length 0xfffffffc -> heap corruption of adjacent slab objects -> panic (bpf_mcopy chain-end KASSERT). uid0 not pursued: copy length is forced, kernel always dies; trigger requires a bpf fd (privileged open).
Evidence (decisive lines)
["panic.txt: 'panic: bpf_mcopy' + catchpacket/bpf_mtap/bpf_ptap/if_simloop backtrace", "run.log: '[+] BIOCSBLEN -> 32' then guest dies in the setuid(nobody) child phase", 'trigger.c: root opens fd, drops to nobody, binds lo0, fires one UDP packet', 'VERDICT.md: primitive characterization + why uid0 is blocked by the forced 4GiB copy length']
PoC changes
Seed sketch did not exist; PoC written fresh. Key correct details: BIOCSBLEN before BIOCSETIF (only then are buffers allocated); lo0 (DLT_NULL, bif_hdrlen=36) as victim interface; loopback UDP sendto as traffic source (if_simloop->bpf_ptap->bpf_mtap); fork+setuid(nobody) so the corrupting ioctls run unprivileged at trigger time.
Verified recommended fix
Reject BIOCSETBLEN once buffers are allocated, reject BIOCSETIF when bd_bufsize < bif_hdrlen, and drop (count) in catchpacket when totlen < hdrlen (validated fix.diff).
Verdict
catchpacket() computes bh_caplen = totlen - hdrlen after clamping totlen to bd_bufsize; on x86-64 bif_hdrlen is 34 (ether) / 36 (DLT_NULL) > BPF_MINBUFSIZE=32, so BIOCSBLEN(32)+BIOCSETIF makes the subtraction underflow to 0xfffffffc and the tap copy routine writes ~4GiB of packet data through a kmalloc(32) store buffer. Reproduced deterministically on the stock INVARIANTS guest: panic: bpf_mcopy with backtrace catchpacket<-bpf_mtap<-bpf_ptap<-if_simloop; the corrupting ioctls and packet were issued by a uid=nobody child holding the bpf fd opened by its root parent (tcpdump-style privilege drop). Primitive: forward linear heap overflow from kmalloc(32)+hdrlen with fully attacker-controlled contents but uncontrolled (~4GiB, forced) length - corrupt-then-panic, no credible survive-and-exploit path, so impact ceiling is reliable kernel memory corruption/DoS gated on holding an open bpf descriptor (device is 0600 root; jails refused). Fix validated on rebuilt kernel: PoC now fails cleanly with EINVAL at BIOCSETIF and the guest survives.
No comments yet.