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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0472 Β· 17 files
FileTypeDescriptionSize
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
README.md readme human reproduce guide
↓ download 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).

VERDICT.md verdict full narrative + fix before/after
↓ download raw

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:

  1. leaked back to userland by ip_fw3_ctl_get_rules (:1026, bcopy(rule->cmd, ioc->cmd, ioc->cmd_len*4) via IP_FW_GET) β€” CWE-125/200;
  2. used as filter_funcs[module][opcode] indices by ip_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:385 rip_ctloutput β†’ ip_fw3_sockopt (ip_fw3_glue.c:51) β†’ ip_fw3_ctl_x (ip_fw3.c:1038) strips the 4-byte x_header β†’ ip_fw3_ctl β†’ ip_fw3_ctl_sockopt (:1138) case IP_FW_ADD β†’ ip_fw3_ctl_add_rule (:951).
  • Missing validation (:956-965): only size ∈ [40,1020] is checked; no check that cmd_len is consistent with size. 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_rules copies rule->cmd (1020 bytes) back to userland.
  • OOB call (:506): when the firewall is enabled and a packet is evaluated, ip_fw3_chk iterates the rule's cmds and calls (filter_funcs[cmd->module][cmd->opcode])(...). filter_funcs is [10][100]; attacker module=0x80, opcode=0x80 indexes 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline leak 42B; patched EINVAL no leak. Legit rule accepted.

BEFORE: 42B leak. AFTER: EINVAL no leak.
↓ fix.diff6.5-DEVELOPMENT #0 (module hot-swap ipfw3.ko)

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=IOC_RULESIZE(ioc_rule) in ip_fw3_ctl_add_rule. Full diff in findings/poc/DF-0472/fix.diff.

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.