ip_fw3_ctl_x: size_t underflow in sopt_valsize when <4 causes unbounded bcopy heap corruption/panic
Summary
ip_fw3_ctl_x(:1038-1047): sopt->sopt_valsize -= sizeof(ip_fw_x_header)=4(:1044) no check valsize>=4. sopt_valsize is size_t(unsigned). valsize<4 wraps to ~SIZE_MAX. bcopy(++x_header, sopt_val, sopt_valsize)(:1045) copies ~2^64 bytes -> massive heap corruption before fault. x_header->opcode read(:1043) from buffer <4 bytes is OOB read. No priv_check on path (raw_ip.c:335 -> ip_fw3_glue.c:51 -> ip_fw3_ctl_x). Raw IP socket requires root to create. Fix: if(sopt_valsize<sizeof(ip_fw_x_header)) return EINVAL.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0471 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uflow.c | trigger-source | minimal trigger: setsockopt(IPPROTO_IP, IP_FW_X, buf[2], 2) -> size_t underflow bcopy | 3.2 KB | view raw |
| build.sh | build-script | cc -Wall -o uflow uflow.c | 112 B | view raw |
| run.sh | run-script | kldload ipfw3 (default-to-accept) + ./uflow | 604 B | view raw |
| run.log | run-log | decisive run on unpatched #0 (stops mid-setsockopt -> guest wedges) | 248 B | view raw |
| panic.txt | panic-signature | ddb trap 'Stopped at systimer_add+0x179 ... db>' from boot.log | 127 B | view raw |
| fix.diff | suggested-fix | one-hunk: reject sopt_valsize<sizeof(ip_fw_x_header) in ip_fw3_ctl_x | 706 B | view raw |
| fix_build.log | build-log | patched ipfw3.ko build (-Werror, rc=0) | 1.8 KB | view raw |
| fix_run.log | run-log | patched module: EINVAL on short payload + 4-byte regression accepted | 643 B | view raw |
| env.txt | environment | uname, cc version, kldstat, sysctl, module sha256 | 364 B | view raw |
| README.md | readme | human reproduce guide | 2.6 KB | β raw |
| VERDICT.md | verdict | full narrative + fix before/after | 5.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-0471 β ip_fw3_ctl_x size_t underflow β unbounded bcopy
Bug
sys/net/ipfw3/ip_fw3.c:1038-1047 β ip_fw3_ctl_x() does
sopt->sopt_valsize -= sizeof(ip_fw_x_header) (4) with no bounds check.
sopt_valsize is size_t (unsigned). A setsockopt(IPPROTO_IP, IP_FW_X, buf, n)
with n β {1,2,3} passes the generic setsockopt plumbing (which only rejects
n==0 and n > SOMAXOPT_SIZE) and arrives here with sopt_valsize < 4, so the
subtract wraps to ~SIZE_MAX and the next line's
bcopy(++x_header, sopt->sopt_val, sopt_valsize) runs an unbounded copy through
kernel heap β memory corruption β guest wedges in DDB.
Threat model (read this)
- Root-only trigger. The path needs a raw IP socket
(
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) βraw_ip.c:385IP_FW_Xβip_fw3_glue.c:51βip_fw3_ctl_x. Raw sockets require root. No unprivilegedβroot escalation. Relevance: compromised-root process, setuid ipfw3 front-end, jail escape. - Module not in GENERIC.
ipfw3isoptional ipfirewall3; must bekldloaded. - Impact: kernel memory-corruption β hard DoS (ddb wedge). Not an LPE.
Reproduce
# as root, on the audit guest (vm.sh up)
sysctl net.filters_default_to_accept=1 # keep ssh alive after kldload
kldload ipfw3
cd findings/poc/DF-0471
./build.sh # cc -Wall -o uflow uflow.c
./run.sh # ./uflow -> on the BUGGY kernel the guest wedges in DDB
Expected on the buggy #0 module: uflow prints the setsockopt banner,
then the guest becomes unresponsive; serial console shows
Stopped at systimer_add+0x179 ... db>. vm.sh status β down.
Expected on the fixed module: uflow exits 0 with
[+] kernel correctly rejected short payload (EINVAL) and the guest stays up.
A 4-byte IP_FW_X payload is still accepted (no regression).
Fix
fix.diff β one guard in ip_fw3_ctl_x before the unsigned subtraction:
if (sopt->sopt_valsize < sizeof(ip_fw_x_header)) return EINVAL;
Rebuild only ipfw3.ko (make KERNBUILDDIR=.../X86_64_GENERIC in
sys/net/ipfw3) and hot-swap.
Files
uflow.cβ minimal trigger (2-byte IP_FW_X β size_t underflow bcopy)build.sh/run.shβ exact build/runrun.logβ decisive run on unpatched #0 (stops mid-setsockopt β wedge)panic.txtβ ddb trap from boot.logfix.diffβ git-apply-able one-hunk fixfix_build.logβ patched module build (-Werror, rc=0)fix_run.logβ patched module: EINVAL + regression (4-byte accepted)env.txtβ guest uname/cc/kldstat/sysctlVERDICT.mdβ full narrative + fix before/aftermanifest.jsonβ machine-readable catalog
DF-0471 β Verdict
Verdict: REPRODUCED (kernel memory-corruption β hard DoS / ddb wedge). FIX VALIDATED.
Summary
ip_fw3_ctl_x() (sys/net/ipfw3/ip_fw3.c:1038-1047) strips the 4-byte
ip_fw_x_header from a setsockopt(IPPROTO_IP, IP_FW_X, ...) payload with no
bounds check:
int
ip_fw3_ctl_x(struct sockopt *sopt)
{
ip_fw_x_header *x_header;
x_header = (ip_fw_x_header *)(sopt->sopt_val);
sopt->sopt_name = x_header->opcode; /* :1043 */
sopt->sopt_valsize -= sizeof(ip_fw_x_header); /* :1044 NO check */
bcopy(++x_header, sopt->sopt_val, sopt->sopt_valsize); /* :1045 */
return ip_fw3_ctl(sopt);
}
sopt->sopt_valsize is size_t (unsigned). The setsockopt plumbing
(sys/kern/uipc_syscalls.c:1250 sopt.sopt_valsize = uap->valsize;
kern_setsockopt at :1221 rejects only valsize==0 and :1223 only
valsize > SOMAXOPT_SIZE) lets valsize β {1,2,3} through. With e.g.
valsize=2, line 1044 computes 2 - 4 = 0xfffffffffffffffe and line 1045 issues
bcopy(sopt_val+4, sopt_val, 0xfffffffffffffffe) β an unbounded forward copy
that walks straight off the option buffer through kernel heap, corrupting
everything in its path. The guest wedges: on the next timer interrupt the
corrupted heap trips the kernel into DDB.
Reachability / threat model (honest)
- Trigger is root-only. The control path needs a raw IP socket
(
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) to reachrip_ctloutput(sys/netinet/raw_ip.c:385SOPT_SETIP_FW_Xβip_fw3_sockoptsys/net/ipfw3/ip_fw3_glue.c:51βip_fw3_ctl_x), and raw sockets require root. No unprivileged privilege boundary is crossed. Relevance: a compromised-root process, a setuid ipfw3 front-end, or a jail escape. - Module not in GENERIC.
ipfw3isoptional ipfirewall3(sys/conf/files:1818); the defaultX86_64_GENERICdoes not compile it. It must bekldloaded. (The 2-line glueip_fw3_glue.cisoptional inetand IS in GENERIC, but only forwards to the module.)
Mechanism (every hop cited)
- Attacker (root):
setsockopt(s, IPPROTO_IP, IP_FW_X=49, buf[2], 2). sys_setsockopt(sys/kern/uipc_syscalls.c:1242):sopt_valsize = 2;sopt_val = kmalloc(2);copyin(buf, sopt_val, 2).kern_setsockopt(:1213): valsize0 < 2 <= SOMAXOPT_SIZEβ accepted.sosetoptβrip_ctloutput(sys/netinet/raw_ip.c:385): caseIP_FW_Xβip_fw3_sockopt(ip_fw3_glue.c:51).ip_fw3_ctl_x(ip_fw3.c:1038): -:1042x_header = sopt_val(cast; points into the 2-byte buffer) -:1043sopt_name = x_header->opcodereads auint16_t(2 bytes) β in bounds for valsize=2. -:1044sopt_valsize -= 4β2 - 4 = 0xfffffffffffffffe(size_t wrap). -:1045bcopy(sopt_val+4, sopt_val, 0xfffffffffffffffe)β unbounded copy.
Evidence
Reproduction (unpatched #0 kernel)
./run.sh (load ipfw3 with filters_default_to_accept=1 so ssh survives,
then run ./uflow). The PoC opens a raw socket and issues
setsockopt(IPPROTO_IP, IP_FW_X, buf[2], 2). Output stops mid-call; the guest
becomes unresponsive. Serial console (dfbsd-qemu/boot.log):
ipfw3 initialized, default to accept Stopped at systimer_add+0x179: cmpq %r13,0x18(%rsi) db>
The unbounded bcopy corrupted kernel heap (including data used by the periodic
systimer); on the next tick the kernel faulted into DDB at systimer_add+0x179.
vm.sh status β down (guest wedged in the debugger). Full run transcript in
run.log; the ddb trap in panic.txt.
Impact ceiling (honest β NOT an LPE)
- Root-only trigger β no privilege boundary crossed (valid hard blocker for an escalation chain).
- Write content is not attacker-controlled β the bcopy copies whatever
follows
sopt_valin kernel heap, with an unbounded size that faults/hangs rather than landing a precise controlled value. (Valid hard blocker.) - Net demonstrated impact: kernel memory corruption β hard DoS (wedge in ddb). The value is the memory-corruption primitive reachable from a root / compromised-root context (CWE-787 OOB write via unsigned wrap), not an unprivileged escalation.
The fix (fix.diff)
One hunk β reject short payloads before the unsigned subtraction, in
ip_fw3_ctl_x:
if (sopt->sopt_valsize < sizeof(ip_fw_x_header))
return EINVAL;
sizeof(ip_fw_x_header) == 4 (uint16 opcode + uint16 pad, ip_fw3.h:366).
This is the minimal targeted fix at the root cause. It supersedes the finding
markdown's proposal (which suggested the same guard) with a tested, line-accurate
implementation.
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 (cp /boot/kernel/ipfw3.ko, kldload), no kernel
rebuild/reboot required.
| test | unpatched #0 module |
patched module |
|---|---|---|
./uflow (2-byte IP_FW_X) |
guest WEDGES in ddb at systimer_add+0x179 |
setsockopt β errno=22 (EINVAL), guest stays up |
regression: 4-byte IP_FW_X |
accepted (reaches ip_fw3_ctl) |
accepted rc=0 (gets past the new check, no regression) |
| guest after test | down (wedged) | up, responsive |
Clean before/after: the bad behaviour (unbounded bcopy β ddb wedge) is present on
the unpatched #0 module and gone on the single-fix module. A legitimate
β₯4-byte IP_FW_X payload is still accepted (rc=0). fix_status = fixed.
Fix verification
fixedVALIDATED: baseline wedge; patched EINVAL. Legit 4B still accepted.
BEFORE: wedge db>. AFTER: rc=-1 errno=22.
Confirmed kernel references
Detail
Exploit chain
none -- root-only + uncontrolled write content. DoS/hardening gap.
Evidence (decisive lines)
BEFORE: setsockopt(2B) -> guest wedge Stopped at systimer_add. AFTER: EINVAL, guest up.
PoC changes
uflow.c (2B IP_FW_X trigger), fix.diff (if(sopt_valsize<sizeof(ip_fw_x_header)) return EINVAL), VERDICT.md, manifest.json.
Verified recommended fix
Add if(sopt->sopt_valsize<sizeof(ip_fw_x_header)) return EINVAL before subtraction at ip_fw3.c:1044. Full diff in findings/poc/DF-0471/fix.diff.
Verdict
REPRODUCED. ip_fw3_ctl_x ip_fw3.c:1044 sopt_valsize-=sizeof(ip_fw_x_header) unsigned underflow when valsize<4 -> bcopy ~SIZE_MAX -> guest wedge. ipfw3 KLD module. Root-only (raw socket).
No comments yet.