ip_fw3_ctl_get_modules: bcopy without checking strlen(module_str) <= sopt_valsize: buffer overflow
Summary
ip_fw3_ctl_get_modules(:985-987): bcopy(module_str, sopt_val, strlen(module_str)) without checking strlen<=sopt_valsize. Prior bzero respects sopt_valsize but bcopy does not. If module name list exceeds user buffer -> overflows sopt_val. sopt_valsize then set to strlen(module_str) masking overflow. Compounded by DF-0476 strncpy non-termination feeding unbounded strcat. Kernel/user buffer overflow. Fix: check strlen<=sopt_valsize before bcopy.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0477 · 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| ipfw3_get_modules_overflow.c | trigger-source | parameterized getsockopt(IP_FW_X,IP_FW_MODULE) overflow probe | 2.7 KB | view raw |
| run_detached.sh | repro-driver | unpatched baseline driver (detached; ipfw3 firewalls ssh) | 815 B | view raw |
| run_fixed.sh | repro-driver | patched-module driver | 893 B | view raw |
| run.sh | run-script | standalone repro script | 1.0 KB | view raw |
| build.sh | build-script | cc -O2 compile | 66 B | view raw |
| run.log | run-log | unpatched baseline: returned_len=23 (overflow) | 2.1 KB | view raw |
| run_patched.log | run-log | patched: returned_len=1 (no overflow) | 1.7 KB | view raw |
| env.txt | environment | uname + cc version | 188 B | view raw |
| fix.diff | suggested-fix | bound bcopy by sopt_valsize in ip_fw3_ctl_get_modules | 577 B | view raw |
| VERDICT.md | verdict | full narrative | 3.5 KB | ↓ raw |
| README.md | readme | summary + before/after | 3.6 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-0477 — ip_fw3_ctl_get_modules bcopy heap overflow
Verdict: REPRODUCED (heap overflow confirmed); fix VALIDATED (clean before/after)
The bug
sys/net/ipfw3/ip_fw3.c ip_fw3_ctl_get_modules() (lines 985-987):
bzero(sopt->sopt_val, sopt->sopt_valsize);
bcopy(module_str, sopt->sopt_val, strlen(module_str)); /* NO bounds check */
sopt->sopt_valsize = strlen(module_str);
module_str is built by strcat-ing every loaded ipfw3 submodule name into a
1024-byte stack buffer. sopt->sopt_val is a kmalloc(sopt_valsize, M_TEMP)
buffer whose size is the caller-controlled getsockopt buffer length. The
bcopy uses strlen(module_str) as the length with no check that it is ≤
sopt_valsize, so it writes past the kmalloc'd slab chunk.
Reachability / privilege boundary
Reached via getsockopt(IPPROTO_IP, IP_FW_X=49, {opcode=IP_FW_MODULE=67, ...})
on a raw IP socket. The path: rip_ctloutput → ip_fw3_sockopt →
ip_fw3_ctl_x (strips the 4-byte ip_fw_x_header, reduces sopt_valsize by 4)
→ ip_fw3_ctl → IP_FW_MODULE → ip_fw3_ctl_get_modules.
Privilege: requires a raw IP socket (rip_attach →
caps_priv_check(SYSCAP_NONET_ROOT) = root, sys/netinet/raw_ip.c:473) AND
kldload ipfw3 (root). This is a root→kernel path: there is no
unprivileged→root escalation (root already owns the kernel). It is a
root→kernel hardening gap with a real heap-overflow primitive.
Reproduction (confirmed)
With ipfw3 + submodules basic,layer2,layer4,nat loaded,
module_str = "basic,layer2,layer4,nat" (23 bytes). Issuing the getsockopt
with a 5-byte buffer (4-byte header + 1 payload byte → kmalloc(5, M_TEMP) →
Zone-16 chunk):
bufsize=5 iter 0: getsockopt rc=0 returned_len=23 payload_slot=1 returned_len=23 => OVERFLOW (bcopy exceeded payload slot)
The kernel set sopt_valsize = strlen(module_str) = 23 and bcopy'd 23 bytes
into a 16-byte slab chunk ⇒ 7-byte heap overflow into the adjacent M_TEMP
Zone-16 chunk. The returned_len=23 (≫ payload_slot=1) is the proof the
overflow fired.
On default GENERIC (INVARIANTS ON) this did not immediately panic: in a
tight loop the same two slab chunks are recycled (LIFO), so the corrupted
neighbour stays free and is never re-validated by chunk_mark_allocated. The
corruption is real; a panic is achievable with slab grooming (drain the free
list so the neighbour is a live, validated object) but is moot for impact since
the path is already root-only.
Fix (validated)
fix.diff bounds the bcopy length by sopt_valsize:
size_t mlen = strlen(module_str);
bzero(sopt->sopt_val, sopt->sopt_valsize);
if (mlen > sopt_valsize)
mlen = sopt_valsize;
bcopy(module_str, sopt->sopt_val, mlen);
sopt->sopt_valsize = mlen;
Before / after (clean contrast)
| kernel / module | bufsize=5 returned_len |
overflow? |
|---|---|---|
| unpatched (#0) | 23 | YES (7-byte heap overflow) |
| patched (#1) | 1 (truncated "b") | no |
With a large buffer (256) the patched module still returns the full 23-byte
module_str ⇒ no regression, the fix only bounds the small-buffer case.
Files
ipfw3_get_modules_overflow.c— parameterized trigger (iters, bufsize)run_detached.sh— baseline driver (unpatched; output to /root/df0477.out)run_fixed.sh— patched driver (output to /root/df0477_fixed.out)run.log— unpatched baseline run (returned_len=23 ⇒ overflow)run_patched.log— patched run (returned_len=1 ⇒ no overflow)fix.diff— git-apply-able fixbuild.sh/run.sh— repro scripts
DF-0477 detailed verdict
Verdict: REPRODUCED — heap overflow confirmed; root-only (root→kernel hardening gap)
Mechanism (trigger → primitive → effect)
- Trigger: unprivileged path requires root (raw IP socket +
kldload ipfw3).getsockopt(s, IPPROTO_IP, IP_FW_X=49, {ip_fw_x_header.opcode=IP_FW_MODULE=67}, len=5)onsocket(AF_INET, SOCK_RAW, IPPROTO_RAW). - Dispatch:
rip_ctloutput(sys/netinet/raw_ip.c:335) →ip_fw3_sockopt(sys/net/ipfw3/ip_fw3_glue.c:51) →ip_fw3_ctl_x(sys/net/ipfw3/ip_fw3.c:1038) strips the 4-byteip_fw_x_header:sopt->sopt_valsize -= 4(now 1),bcopy(++x_header, sopt_val, 1), thenip_fw3_ctl→IP_FW_MODULE→ip_fw3_ctl_get_modules. - Primitive (the bug,
ip_fw3.c:985-987):c bcopy(module_str, sopt->sopt_val, strlen(module_str)); /* strlen=23, sopt_val is kmalloc(5)=16-byte slab chunk */sopt_valwaskmalloc(sopt_valsize=5, M_TEMP)insys_getsockopt(sys/kern/uipc_syscalls.c:1332) → Zone-16 chunk. With 4 submodules loadedmodule_str="basic,layer2,layer4,nat"(23 bytes). Thebcopywrites 23 bytes into a 16-byte chunk ⇒ 7-byte heap overflow into the adjacent M_TEMP Zone-16 slab object. - Effect: kernel heap corruption. On default GENERIC (INVARIANTS ON) no
immediate panic — in the tight probe loop the recycled slab chunks mask the
chunk_mark_allocatedcheck (corrupted neighbour stays free). Corruption is real; a panic is reachable via slab grooming but is impact-moot (root-only).
Why no uid=0 chain
The trigger needs a raw socket (SYSCAP_NONET_ROOT, root) and kldload
(root). This is a root→kernel path: root already owns the kernel, so there
is no privilege boundary to cross and no unprivileged→root escalation. It is a
defensive-coding / hardening gap. (Per the audit bright-line rule, a root-only
write primitive is reported as corruption with no privesc claim.)
Reproduction evidence (unpatched #0 kernel)
bufsize=5 iter 0: getsockopt rc=0 returned_len=23 errno=0 payload_slot=1 returned_len=23 => OVERFLOW (bcopy exceeded payload slot) module_str="basic,layer2,layer4,nat" DF-0477: 8000 iterations done, bufsize=5. Kernel still alive.
returned_len=23 ≫ payload_slot=1 proves the kernel bcopy'd 23 bytes into
the 1-payload-slot (5-byte kmalloc) buffer — i.e. the overflow fired 8000×.
PoC changes from the seeded draft
The seeded draft had no PoC. I wrote ipfw3_get_modules_overflow.c
(parameterized: iters + bufsize) and two detached drivers (run_detached.sh
for the unpatched baseline, run_fixed.sh for the patched module) because
kldload ipfw3 enables a default-deny firewall that kills ssh — the driver
runs detached and writes to a file recovered via vm.sh down (force-kill,
preserves disk) + vm.sh up. Also discovered the IP_FW_X extended-opcode
dispatch (the draft assumed a direct IP_FW_MODULE getsockopt; the real path
embeds the opcode in an ip_fw_x_header).
Fix validation (Phase 8) — VALIDATED
Built the ipfw3 module with fix.diff, installed to /boot/kernel, loaded on
the patched #1 kernel, re-ran:
bufsize=5 iter 0: getsockopt rc=0 returned_len=1 (was 23) payload_slot=1 returned_len=1 (no overflow) (was "OVERFLOW") module_str="b" (truncated, bounded)
bufsize=256 still returns the full 23-byte module_str ⇒ no regression. The
fix deterministically bounds the bcopy to sopt_valsize.
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
REPRODUCED (live). ip_fw3_ctl_get_modules bcopy strlen not sopt_valsize -> 7B heap overflow. Root-only. Module fix validated.
No comments yet.