ip_fw3_ctl_add_rule missing cmd_len validation: heap over-read + info leak + OOB function pointer call
Summary
ip_fw3_ctl_add_rule(:950) validates total buffer 32..1020 but never cmd_len vs data provided. krealloc to IPFW_RULE_SIZE_MAX*4=1020(:950), extra bytes uninitialized heap. add_rule_dispatch(:655) bcopy(ioc_rule->cmd, rule->cmd, cmd_len*4) with user cmd_len=255 reads 1020 bytes from offset ~40 -> ~40-byte heap over-read past allocation. Garbage opcodes become rule. ip_fw3_ctl_get_rules(:1026) copies garbage back via IP_FW_GET -> heap info leak. ip_fw3_chk iterates garbage -> filter_funcs[module][opcode] OOB func call. Fix: validate cmd_len<=IPFW_RULE_SIZE_MAX-header, act_ofs<cmd_len, set<32.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0472 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| leak.c | trigger-source | info-leak / heap-over-read trigger via IP_FW_X ADD (cmd_len=255) + GET | 7.8 KB | view raw |
| panic.c | trigger-source | OOB function-pointer call trigger (cmd[0] module=0x80 opcode=0x80) -> ip_fw3_chk panic | 3.8 KB | view raw |
| build.sh | build-script | cc -Wall -o leak leak.c; cc -Wall -o panic panic.c | 198 B | view raw |
| run.sh | run-script | kldload ipfw3; fw3.enable=0; ./leak | 721 B | view raw |
| panic.sh | run-script | runs ./panic (crashes guest) | 260 B | view raw |
| build.log | build-log | final successful leak/panic build | 234 B | view raw |
| run.log | run-log | decisive leak run on unpatched #0 (paths + kernel pointer) | 4.8 KB | view raw |
| leak_sample.txt | leak-sample | captured leaked bytes across runs with annotations (variance + 0xfffff8008db3b000 ptr) | 1.2 KB | view raw |
| panic.txt | panic-signature | fatal trap 9 GP fault at ip_fw3_chk+0x1a4 from boot.log | 2.3 KB | view raw |
| env.txt | environment | uname, cc version, kldstat, sysctl state | 390 B | view raw |
| fix.diff | suggested-fix | cmd_len/act_ofs/size validation in ip_fw3_ctl_add_rule (git-apply-able) | 1.0 KB | view raw |
| fix_build.log | build-log | patched ipfw3.ko module build (-Werror, rc=0) | 932 B | view raw |
| fix_run.log | run-log | leak+panic PoC on patched module: EINVAL, no panic | 144 B | view raw |
| README.md | readme | human reproduce guide | 5.2 KB | β raw |
| VERDICT.md | verdict | full narrative + fix before/after | 7.7 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0472 β ip_fw3_ctl_add_rule missing cmd_len validation
Heap over-read (CWE-125) + kernel heap info leak (CWE-200) + OOB function-pointer call (CWE-787)
sys/net/ipfw3/ip_fw3.c:951 (ip_fw3_ctl_add_rule), :655 (add_rule_dispatch),
:1026 (ip_fw3_ctl_get_rules), :506 (ip_fw3_chk).
Status
REPRODUCED (both the info leak and the OOB-call panic), FIX VALIDATED.
Reachability is root-only: the ipfw3 control path
(rip_ctloutput β ip_fw3_sockopt β ip_fw3_ctl_x β ip_fw3_ctl_add_rule) is
reached through an AF_INET SOCK_RAW socket, which requires root. ipfw3 is a
loadable module (/boot/kernel/ipfw3.ko), not compiled into X86_64_GENERIC,
so it must first be kldloaded. This is a rootβkernel primitive β relevant to a
compromised root process, a setuid ipfw3 front-end, or a jail-escape context that
can open a raw socket and load the module. It is not an unprivileged LPE.
The bug
ip_fw3_ctl_add_rule validates only that the total sopt_valsize is in
[sizeof(ioc_rule)-sizeof(ipfw_insn), 1020] and then krealloc()s the buffer to
1020 bytes (sizeof(uint32_t)*IPFW_RULE_SIZE_MAX, no M_ZERO). It never
validates ioc_rule->cmd_len against the data actually supplied. A caller sends
52 bytes (4-byte IP_FW_X header + a 48-byte ioc_rule) but sets
cmd_len = IPFW_RULE_SIZE_MAX = 255.
add_rule_dispatch then does:
rule->cmd_len = ioc_rule->cmd_len; /* 255 */
bcopy(ioc_rule->cmd, rule->cmd, rule->cmd_len * 4); /* 255*4=1020 */
ioc_rule->cmd lives at offset 36 of a 1020-byte (kmalloc-1024 slab) buffer, so
this bcopy reads bytes [36, 1056):
| region | bytes | source |
|---|---|---|
| our 1 real cmd | [36, 44) |
attacker data |
| krealloc tail | [44, 1024) |
~976 bytes uninitialized heap (slab residue) |
| over-read | [1024,1056) |
32 bytes past the slab object (neighbour heap) |
That garbage becomes the rule. ip_fw3_ctl_get_rules copies it straight back to
userland via IP_FW_GET β kernel heap info leak. ip_fw3_chk then iterates
the garbage and at :506 does
(filter_funcs[cmd->module][cmd->opcode])(...);
filter_funcs is [MAX_MODULE=10][MAX_OPCODE_PER_MODULE=100]; attacker-chosen
module/opcode index out of bounds β OOB indirect function call.
Files
| file | purpose |
|---|---|
leak.c |
info-leak / heap-over-read trigger (IP_FW_X ADD with cmd_len=255, then GET) |
panic.c |
OOB-call trigger (installs a rule with cmd[0].module=0x80 opcode=0x80, enables firewall, sends a packet) |
build.sh |
cc -Wall -o leak leak.c; cc -Wall -o panic panic.c |
run.sh |
loads ipfw3, disables packet filtering, runs ./leak |
panic.sh |
runs ./panic (crashes the guest) |
leak_sample.txt |
captured leaked bytes across runs (path strings + kernel pointer) |
panic.txt |
fatal-trap-9 signature from dfbsd-qemu/boot.log |
run.log |
full decisive leak run (unpatched) |
fix_build.log |
module build log for the patched ipfw3.ko |
fix_run.log |
leak + panic PoC on the patched module (EINVAL, no panic) |
env.txt |
guest uname / cc / sysctl state |
fix.diff |
git apply-able one-hunk fix |
VERDICT.md |
full narrative + fix before/after |
Reproduce (on the audit guest)
# as root (ssh dfbsd), guest already booted on the with-src (#0) kernel:
cd /root/poc/DF-0472 && cc -Wall -o leak leak.c && cc -Wall -o panic panic.c
sysctl net.filters_default_to_accept=1 # keep ssh alive if firewall hooks
kldload ipfw3
sysctl net.inet.ip.fw3.enable=0 # do NOT evaluate rules on live traffic
./leak # expect: rc=0, non-zero "leaked" bytes
# OOB-call panic (crashes guest β capture in dfbsd-qemu/boot.log):
# ./panic
Expected (bug present)
./leak prints setsockopt(IP_FW_ADD) rc=0 and a hex dump whose tail contains
leaked kernel heap β observed: ASCII path strings ("/root/poc/DF-0472/leak",
"/home/m/r/root/poc/DF-0472/root") and a kernel pointer
0xfffff8008db3b000. The byte count/offsets vary run-to-run (genuine residue).
./panic enables the firewall and sends one UDP packet; the guest panics:
Fatal trap 9: general protection fault while in kernel mode instruction pointer = 0x8:0xffffffff826001a4 (ipfw3.ko+0x1a4) Stopped at ip_fw3_chk+0x1a4: ret
Expected (after fix.diff)
./leak and ./panic both get setsockopt(IP_FW_ADD) rc=-1 errno=22 (EINVAL);
no rule is installed, nothing is leaked, no panic. A legitimate rule
(cmd_len=2, full data) is still accepted (rc=0).
DF-0472 β Verdict
Verdict: REPRODUCED (info leak + OOB-call panic). FIX VALIDATED.
Summary
ip_fw3_ctl_add_rule (sys/net/ipfw3/ip_fw3.c:951) accepts a firewall rule
from a raw socket (IP_FW_X / IP_FW_ADD) and validates only the total
sopt_valsize ([sizeof(ioc_rule)-sizeof(ipfw_insn) .. 1020]), never
ioc_rule->cmd_len / act_ofs. It krealloc()s the buffer to 1020 bytes
without M_ZERO, then add_rule_dispatch (:655) does
bcopy(ioc_rule->cmd, rule->cmd, cmd_len*4). With cmd_len=255 but only one
8-byte instruction actually supplied, that bcopy reads 1020 bytes from offset
36 of a 1020-byte (kmalloc-1024) buffer: ~976 bytes of uninitialized krealloc
tail plus ~32 bytes over-read into the neighbouring slab object. The garbage
becomes the rule and is:
- leaked back to userland by
ip_fw3_ctl_get_rules(:1026,bcopy(rule->cmd, ioc->cmd, ioc->cmd_len*4)viaIP_FW_GET) β CWE-125/200; - used as
filter_funcs[module][opcode]indices byip_fw3_chk(:506) when the firewall evaluates a packet β CWE-787 OOB indirect call.
Mechanism (every hop cited)
- Trigger (root):
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)βsetsockopt(IPPROTO_IP, IP_FW_X=49, [x_hdr.opcode=IP_FW_ADD=50][ioc_rule], 52).raw_ip.c:385rip_ctloutputβip_fw3_sockopt(ip_fw3_glue.c:51) βip_fw3_ctl_x(ip_fw3.c:1038) strips the 4-bytex_headerβip_fw3_ctlβip_fw3_ctl_sockopt(:1138) caseIP_FW_ADDβip_fw3_ctl_add_rule(:951). - Missing validation (
:956-965): onlysize β [40,1020]is checked; no check thatcmd_lenis consistent withsize.krealloc(sopt_val, 1020, M_TEMP, M_WAITOK)(:962) grows the 52-byte buffer to 1020 bytes; the tail[48,1020)is uninitialized heap. - Over-read (
:655):rule->cmd_len = ioc_rule->cmd_len(=255);bcopy(ioc_rule->cmd, rule->cmd, 255*4=1020)reads[36,1056)of the 1020-byte buffer β[1024,1056)is past the slab object (neighbour heap). - Info leak (
:1026):IP_FW_GETβip_fw3_ctl_get_rulescopiesrule->cmd(1020 bytes) back to userland. - OOB call (
:506): when the firewall is enabled and a packet is evaluated,ip_fw3_chkiterates the rule's cmds and calls(filter_funcs[cmd->module][cmd->opcode])(...).filter_funcsis[10][100]; attackermodule=0x80, opcode=0x80indexes entry 12928, far past the 1000-entry array.
Evidence
Info leak (deterministic, varies run-to-run)
./leak on the unpatched #0 kernel returns the garbage rule with non-zero
leaked bytes whose count/offset vary across fresh runs (12 / 5 / 28 non-zero
bytes), proving genuine uninitialized heap rather than deterministic output.
Observed leaked content includes ASCII path strings from kernel namecache /
vnode-path buffers and a kernel virtual address 0xfffff8008db3b000
(bytes 00 b0 b3 8d 00 f8 ff ff, little-endian). Full hex in leak_sample.txt.
OOB-call panic (trap 9)
./panic installs a rule with cmd[0].module=0x80 opcode=0x80, enables the
firewall, and sends one UDP packet. The guest panics (full signature in
panic.txt):
Fatal trap 9: general protection fault while in kernel mode instruction pointer = 0x8:0xffffffff826001a4 (ipfw3.ko+0x1a4) current process = Idle Stopped at ip_fw3_chk+0x1a4: ret
The faulting RIP is inside ipfw3.ko (loaded at 0xffffffff82600000); ddb
symbolises it as ip_fw3_chk+0x1a4, i.e. the filter_funcs[module][opcode]
indirect call site (:506). The wild call ran a few instructions off the
corrupted pointer/stack and faulted on ret β proof the attacker-controlled
module/opcode reached the indirect call.
Impact (honest)
- Root-only trigger. No privilege boundary is crossed from an unprivileged user: the ctl path needs a raw socket (root) and the module must be loaded. Relevance: compromised root process, setuid ipfw3 front-end, jail escape.
- Info leak: discloses ~1 KB of neighbouring kernel heap per call, including
kernel pointers (
0xfffff800........). On a KASLR-enabled build this defeats KASLR; here KASLR is off so the ceiling is heap-content disclosure. - OOB call: a kernel memory-corruption primitive (attacker-influenced
indirect call). On this guest SMEP/SMAP are OFF, so a heap-grooming chain that
lands a chosen value at
filter_funcs[0x80][0x80]could redirect execution to userspace shellcode (commit_creds(prepare_kernel_cred(0))) for rootβkernel code execution; demonstrated here at the panic (DoS) level. Because the trigger is already root, the LPE chain is moot β the value is the memory-corruption primitive itself (CWE-787) plus the info leak.
The fix (fix.diff)
One hunk in ip_fw3_ctl_add_rule, inserted after ioc_rule = sopt->sopt_val;
and before ip_fw3_add_rule(ioc_rule);:
if (ioc_rule->cmd_len > IPFW_RULE_SIZE_MAX -
((sizeof(*ioc_rule) - sizeof(ipfw_insn)) / sizeof(uint32_t)) ||
ioc_rule->act_ofs >= ioc_rule->cmd_len ||
size < IOC_RULESIZE(ioc_rule)) {
return EINVAL;
}
This rejects a rule when:
- cmd_len exceeds the words that fit in the 255-word buffer after the 10-word
header (cmd_len <= 245) β closes the slab over-read;
- act_ofs >= cmd_len β closes the ACTION_PTR OOB;
- the supplied size is smaller than IOC_RULESIZE(ioc_rule) (= 40 +
cmd_len*4) β closes the uninitialized-krealloc-tail read.
A legitimate rule (cmd_len=2, full data, act_ofs=0) is still accepted
(verified: rc=0).
Fix validation (Phase 8)
Because ipfw3 is a loadable module, the fix was validated by rebuilding only
ipfw3.ko (make KERNBUILDDIR=.../X86_64_GENERIC in sys/net/ipfw3,
-Werror, rc=0) and hot-swapping it (kldunload/cp/kldload), no kernel
rebuild/reboot required.
| test | unpatched #0 module |
patched module |
|---|---|---|
./leak ADD |
rc=0, rule installed |
rc=-1 errno=22 (EINVAL) |
./leak leaked bytes |
12β41 non-zero (paths + 0xfffff800β¦ ptr) |
n/a (ADD rejected, nothing to read back) |
./panic |
trap 9 panic at ip_fw3_chk+0x1a4 |
ADD rejected (EINVAL), guest stays up |
legitimate cmd_len=2 rule |
accepted rc=0 |
accepted rc=0 (no regression) |
Clean before/after: the bad behaviour is present on the unpatched #0 module
and gone on the single-fix module. fix_status = fixed.
Re-verification (2026-07-16, #0 with-src)
Re-confirmed on a fresh with-src snapshot (#0 unpatched kernel + unpatched
ipfw3.ko), then re-validated the fix by rebuilding only ipfw3.ko and
hot-swapping.
| test | unpatched #0 module |
patched module |
|---|---|---|
./leak ADD (cmd_len=255) |
rc=0 (rule installed) |
rc=-1 errno=22 (EINVAL) |
./leak leaked bytes |
42 non-zero incl. /root/.ssh/authorized/root + ptrs 0xffffffff81115a60/0x...5a20 |
n/a (ADD rejected, nothing to read back) |
legitimate rule (cmd_len=1) |
accepted rc=0 |
accepted rc=0 (no regression) |
ipfw3.ko build |
n/a | -Werror, rc=0 |
Clean before/after holds on the current guest: bad behaviour present on the
unpatched module, gone on the single-fix module, legit rules unaffected.
fix_status = fixed. (Full transcripts: run.log, run.2.log, run.3.log,
leak_sample.txt, fix_build.log, fix_run.log, env.txt.)
Fix verification
fixedVALIDATED: baseline leak 42B; patched EINVAL no leak. Legit rule accepted.
BEFORE: 42B leak. AFTER: EINVAL no leak.
Confirmed kernel references
Detail
Exploit chain
none -- root-only. Info leak ~1KB + root->kernel OOB call primitive.
Evidence (decisive lines)
BEFORE: leak 42/1012B non-zero incl KVA. AFTER: ADD EINVAL no leak. Legit cmd_len=1 still accepted.
PoC changes
leak.c (cmd_len=255 info leak), panic.c (OOB filter_funcs call), fix.diff (validate cmd_len/act_ofs/supplied size), VERDICT.md, manifest.json.
Verified recommended fix
Validate cmd_len<=245, act_ofs
Verdict
REPRODUCED. ip_fw3_ctl_add_rule ip_fw3.c:951 no cmd_len validation -> bcopy cmd_len*4 reads past supplied data -> krealloc tail + slab OOB read. Leaked 42B incl KVA ptrs 0xffffffff81115a60 + /root/.ssh path. OOB-call variant: filter_funcs[0x80][0x80] panic. ipfw3 KLD. Root-only.
No comments yet.