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

bpf_filter() executes unvalidated BPF programs via ipfw3_layer4 check_bpf: BPF_LD|BPF_MEM/BPF_ST/BPF_STX with unchecked pc->k give arbitrary-offset kernel stack read/write and full RIP control

Summary

bpf_filter's four scratch-memory cases index mem[pc->k] with no runtime bound check - the function's own comment states the kernel must validate application filter code or 'a bogus program could easily crash the system'. Every historical caller complies via bpf_validate(), but ipfw3's layer4 module does not: check_bpf runs bpf_filter on a program embedded in an ipfw3 rule, and the rule instruction stream is copied into the kernel verbatim with no validation. Result: arbitrary-offset u32 kernel-stack read (LD|BPF_MEM) and write (ST) with fully controlled value and controlled 32-bit index, plus no termination requirement. VERIFIED on guest: (a) zero-length program -> interpreter walks zeroed heap as endless LD|IMM -> panic; (b) BPF_ST k=0x08000000, A=0xcafebabe -> controlled u32 write at mem+512MB -> panic exactly at the store instruction; (c) BPF_ST k=26/27 -> saved return address overwritten -> CPU jumps to the exact attacker value ('Stopped at 0x414142424343'). With RIP control plus a 16GB positive-offset write window, repeatable per packet, on a no-SMEP/no-KASLR guest this is full ring-0 code execution. Unpriv->uid0 blocked by the entry gate (SYSCAP_NONET_RAW - same accepted hard blocker as DF-2580 on this identical surface); root or a delegated ipfw3-administration mechanism converts trivially. Fix validated (two hunks: runtime BPF_MEMWORDS bounds in bpf_filter.c + bpf_validate gate in check_bpf; nativekernel #1): identical ladder silent+healthy.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3444 Β· 13 files
FileTypeDescriptionSize
poc.c β€” 6.0 KB view raw
build.sh β€” 77 B view raw
run.sh β€” 234 B view raw
build.log β€” 103 B view raw
run.log β€” 2.0 KB view raw
fix_run.log β€” 970 B view raw
panic.txt β€” 1.9 KB view raw
fix.diff β€” 1.5 KB view raw
fix_build.log β€” 5.1 MB ↓ download
env.txt β€” 190 B view raw
VERDICT.md β€” 6.6 KB ↓ raw
manifest.json β€” 1.4 KB view raw
verdict.json β€” 4.8 KB view raw
VERDICT.md
↓ download raw

DF-3444 β€” bpf_filter() executes unvalidated BPF programs (ipfw3_layer4 check_bpf)

Verdict

REPRODUCED on the stock 6.5-DEVELOPMENT #0 INVARIANTS kernel: a root user installs an ipfw3 rule whose O_LAYER4_BPF instruction embeds an arbitrary BPF program; the kernel executes it on every packet with no validation, yielding (a) a deterministic kernel OOB instruction walk, (b) an arbitrary-offset kernel-stack 32-bit write (BPF_ST, value fully controlled), and (c) full RIP control via overwrite of bpf_filter's saved return address (Stopped at 0x414142424343). FIX VALIDATED on a single-fix #1 kernel (fix.diff: runtime BPF_MEMWORDS bounds in bpf_filter.c + a bpf_validate() gate in check_bpf): the identical PoC ladder no longer panics and networking stays healthy.

The escalation chain to uid=0 for an unprivileged user is blocked by the same hard blocker documented for DF-2580 on this attack surface: the only path to installing a rule is setsockopt(raw_ip_socket, IPPROTO_IP, IP_FW_X=49, …) and creating a raw IP socket requires caps_priv_check(SYSCAP_NONET_RAW) (sys/netinet/raw_ip.c:473, rip_attach). A non-root user cannot enter the bug path. For a root user the primitive is already arbitrary kernel code execution: RIP control + a 16 GB positive-offset write window with per-packet repeatability on a no-SMEP/no-KASLR guest (ROP or shellcode-in-rule-memory trivially convert it).

Root cause (line-by-line)

bpf_filter() documents its own contract (sys/net/bpf_filter.c:498-509):

The kernel needs to be able to verify an application's filter code. Otherwise, a bogus program could easily crash the system.

The four memory-access cases index mem[pc->k] with no runtime bound check (bpf_filter.c:365-379), relying entirely on the caller having run bpf_validate():

case BPF_LD|BPF_MEM:
    A = mem[pc->k];          /* line 366 */
case BPF_LDX|BPF_MEM:
    X = mem[pc->k];          /* line 370 */
case BPF_ST:
    mem[pc->k] = A;          /* line 374 */
case BPF_STX:
    mem[pc->k] = X;          /* line 378 */

Compliant callers: bpf_setf (sys/net/bpf.c:1076, BIOCSETF/32/WF) and ng_bpf (sys/netgraph/bpf/ng_bpf.c:481, sys/netgraph7/bpf/ng_bpf.c:560).

The non-compliant caller: check_bpf (sys/net/ipfw3_layer4/ip_fw3_layer4.c:194-206):

slen = bpf_filter(bpf->bf_insn, (u_char *)m, m_lengthm(m, NULL), 0);  /* :201 */

The program reaches the kernel unvalidated because ipfw3 rule installation copies the instruction stream verbatim: ip_fw3_ctl_add_rule (ip_fw3.c:951, only a size sanity check) β†’ ip_fw3_add_rule β†’ add_rule_dispatch β†’ bcopy(ioc_rule->cmd, rule->cmd, rule->cmd_len * 4) (ip_fw3.c:655) β€” no opcode walk, no BPF validation, no bf_len check. At match time ip_fw3_check dispatches filter_funcs[cmd->module][cmd->opcode] (ip_fw3.c:506) β€” with the ipfw3_layer4 module loaded, module=3, opcode=4 reaches check_bpf, which runs bpf->bf_insn directly.

Primitive on this kernel (X86_64_GENERIC #0, gcc 8.3)

From objdump -d /boot/kernel/kernel:

  • prologue: mem[] is zeroed by rep stos of 8 qwords at rbp-0x60 β†’ mem base = rbp-0x60 (bpf_filter+0x1d);
  • the ST case compiles to mov %eax,-0x60(%rbp,%rdx,4) with edx = pc->k and no compare (bpf_filter+0x4d0..+0x4d3);
  • saved return address of bpf_filter is at rbp+8 β†’ BPF_ST k=26 (low dword), k=27 (high dword).

So one BPF_LD|BPF_IMM + two BPF_STs give RIP = any chosen 64-bit value. BPF_LD|BPF_MEM with the same k-range gives the matching read primitive; and because unvalidated programs need not terminate, even an all-zero "program" walks kernel memory as an endless BPF_LD|BPF_IMM stream (code 0) until it faults.

Reproduction (baseline #0)

# sysctl net.filters_default_to_accept=1
# kldload ipfw3; kldload ipfw3_basic; kldload ipfw3_layer4
# cc -O -o poc3444 poc3444.c
# ./poc3444 safe 0x12345678            # terminating in-bounds program: survives
# ./poc3444 empty                      # zero instructions:
Fatal trap 12 ... instruction pointer = 0xffffffff8072ac60
Stopped at bpf_filter+0x50: movzwl 0x8(%rdi),%r8d        # OOB instruction walk
# ./poc3444 store 0xcafebabe 0x08000000   # mem[0x08000000]=0xcafebabe:
--- trap 000000000000000c, rip = ffffffff8072b0e3 ---
bpf_filter() at bpf_filter+0x4d3                        # the OOB store itself
# ./poc3444 riptake 0x42424343 0x00004141 # ST k=26/27 -> saved RIP:
FStopped at 0x414142424343: kernel: type 12 trap, code=0

Each panic is deterministic (fresh vm.sh reset with-src, single run). Full signatures in panic.txt; runs in run.log.

Fix validation (patched #1 kernel)

fix.diff (applies clean with git apply): 1. sys/net/bpf_filter.c β€” runtime if (pc->k >= BPF_MEMWORDS) return 0; in all four MEM cases: bpf_filter is now safe even for unvalidated programs (defense in depth; decisive hunk). 2. sys/net/ipfw3_layer4/ip_fw3_layer4.c β€” check_bpf refuses to run any program that fails bpf_validate(bpf->bf_insn, (int)bpf->bf_len).

vm.sh reset with-src β†’ apply β†’ make nativekernel && make installkernel (full log: fix_build.log, 14896 lines, BUILD-RC=0) β†’ reboot into #1 β†’ identical ladder:

# ./poc3444 empty                            -> survives
# ./poc3444 store 0xcafebabe 0x08000000      -> survives
# ./poc3444 riptake 0x42424343 0x00004141    -> survives
# ping -c1 10.0.2.2                          -> 0.218 ms, ssh alive

Baseline corrupted/panicked on every run; patched kernel: zero panics, networking healthy. fix_status = fixed.

Privilege analysis (hard blocker for unpriv→root)

Identical entry gating to DF-0668/DF-2580 (same sockopt surface): rip_ctloutput is only reachable through a raw IP socket, and rip_attach (sys/netinet/raw_ip.c:460-475) gates on caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW | …). An unprivileged user cannot obtain the socket, cannot install the rule, and there is no secondary unvalidated path to bpf_filter (bpf.c and both ng_bpf implementations validate). Documented as a valid hard blocker, consistent with the accepted DF-2580 precedent on this exact surface.

Residual severity for a root attacker (and for any future in-kernel BPF consumer that forgets validation): full ring-0 compromise of kernel integrity β€” RIP control demonstrated on the no-SMEP/no-KASLR guest.

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

Patched kernel #1 built with fix.diff (nativekernel, fix_build.log BUILD-RC=0): all three panic modes gone, packets flow, ssh alive. Baseline #0 panicked deterministically on every mode.

fix_run.log, fix_build.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Sep 12 21:38:29 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

root: socket(AF_INET,SOCK_RAW,IPPROTO_RAW) -> setsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=50 IP_FW_ADD, rule{cmd_len, cmd=ipfw_insn{module=3,opcode=4 O_LAYER4_BPF}+bf_str+bf_len+bf_insn[]}}) -> add_rule_dispatch bcopy (no validation) -> packet -> pfil -> ip_fw3_check -> filter_funcs[3][4]=check_bpf -> bpf_filter(bf_insn) -> BPF_LD|BPF_IMM (A=value) + BPF_ST k=26/27 -> saved RIP = value -> retq jumps to attacker address. Value fully controlled per 32-bit half; repeatable per packet; multiple STs per program (up to ~14 insns per bpf cmd, multiple cmds/rules). BPF_LD|BPF_MEM with arbitrary k is the matching read. uid0-from-unprivileged: blocked (no raw socket without SYSCAP_NONET_RAW).

Evidence (decisive lines)

["panic.txt: three deterministic panic signatures (bpf_filter+0x50 walk; bpf_filter+0x4d3 store with rip=ffffffff8072b0e3; 'Stopped at 0x414142424343')", 'run.log: baseline ladder (safe survives; empty/store/riptake panic)', 'fix_run.log: identical ladder on patched #1 kernel - all survive, ping 0.218ms', 'fix_build.log: full nativekernel build log (BUILD-RC=0)', 'VERDICT.md: full narrative incl. objdump frame math (mem at rbp-0x60 via rep stos at bpf_filter+0x1d; ST compiles to mov %eax,-0x60(%rbp,%rdx,4))']

PoC changes

PoC authored fresh (no seed): raw-socket IP_FW_X=49/opcode-50 rule builder mirroring kernel structs (ipfw_ioc_rule, ipfw_insn{module=3,opcode=4}, ipfw_insn_bpf with bf_insn at +140). Fixed two self-inflicted PoC bugs found during bring-up: program was emitted before the payload memset (produced an accidental all-zeros program - itself a valid repro of the no-termination facet), and the 'empty' mode initially still emitted a trailing RET.

Verified recommended fix

bpf_filter.c: runtime 'if (pc->k >= BPF_MEMWORDS) return 0;' in LD|MEM/LDX|MEM/ST/STX; plus bpf_validate(bf_insn, bf_len) gate in ipfw3_layer4 check_bpf (see fix.diff)

Verdict

REPRODUCED on stock 6.5-DEVELOPMENT #0 (INVARIANTS): an ipfw3 rule with an O_LAYER4_BPF instruction executes its embedded BPF program with zero validation (ip_fw3.c:655 verbatim bcopy; check_bpf ip_fw3_layer4.c:201 calls bpf_filter directly). Demonstrated: (1) zero-length 'program' walks kernel memory as endless BPF_LD|BPF_IMM until unmapped page (panic bpf_filter+0x50); (2) BPF_ST with k=0x08000000 writes a controlled u32 at mem+512MB (panic exactly at the store, bpf_filter+0x4d3, mov %eax,-0x60(%rbp,%rdx,4), edx=pc->k unchecked); (3) BPF_ST k=26/27 overwrites bpf_filter's saved return address -> CPU jumps to the exact attacker value 0x414142424343 ('Stopped at 0x414142424343'). FIX VALIDATED on single-fix kernel #1 (fix.diff: BPF_MEMWORDS runtime bounds in bpf_filter.c:365-379 + bpf_validate gate in check_bpf): identical ladder survives, no panic, networking healthy. Unpriv->uid0 is blocked by the entry gate (raw IP socket requires SYSCAP_NONET_RAW, sys/netinet/raw_ip.c:473) - same accepted hard blocker as DF-2580 on this surface; for a root attacker the demonstrated RIP control is full ring-0 code execution on this no-SMEP/no-KASLR guest.