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)
PoC verification
Evidence pack
findings/poc/DF-3444 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 byrep stosof 8 qwords atrbp-0x60βmembase = rbp-0x60 (bpf_filter+0x1d); - the ST case compiles to
mov %eax,-0x60(%rbp,%rdx,4)withedx = pc->kand no compare (bpf_filter+0x4d0..+0x4d3); - saved return address of
bpf_filteris 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
fixedPatched 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
Confirmed kernel references
- sys/net/bpf_filter.c:365
- sys/net/bpf_filter.c:366
- sys/net/bpf_filter.c:370
- sys/net/bpf_filter.c:374
- sys/net/bpf_filter.c:378
- sys/net/bpf_filter.c:498
- sys/net/ipfw3_layer4/ip_fw3_layer4.c:201
- sys/net/ipfw3/ip_fw3.c:655
- sys/net/ipfw3/ip_fw3.c:951
- sys/net/ipfw3/ip_fw3.c:506
- sys/net/bpf.c:1076
- sys/netgraph/bpf/ng_bpf.c:481
- sys/netinet/raw_ip.c:473
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.
No comments yet.