DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0477

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0477 · 13 files
FileTypeDescriptionSize
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
README.md readme summary + before/after
↓ download 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_ctloutputip_fw3_sockoptip_fw3_ctl_x (strips the 4-byte ip_fw_x_header, reduces sopt_valsize by 4) → ip_fw3_ctlIP_FW_MODULEip_fw3_ctl_get_modules.

Privilege: requires a raw IP socket (rip_attachcaps_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_strno 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 fix
  • build.sh / run.sh — repro scripts
VERDICT.md verdict full narrative
↓ download raw

DF-0477 detailed verdict

Verdict: REPRODUCED — heap overflow confirmed; root-only (root→kernel hardening gap)

Mechanism (trigger → primitive → effect)

  1. 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) on socket(AF_INET, SOCK_RAW, IPPROTO_RAW).
  2. 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-byte ip_fw_x_header: sopt->sopt_valsize -= 4 (now 1), bcopy(++x_header, sopt_val, 1), then ip_fw3_ctlIP_FW_MODULEip_fw3_ctl_get_modules.
  3. 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_val was kmalloc(sopt_valsize=5, M_TEMP) in sys_getsockopt (sys/kern/uipc_syscalls.c:1332) → Zone-16 chunk. With 4 submodules loaded module_str="basic,layer2,layer4,nat" (23 bytes). The bcopy writes 23 bytes into a 16-byte chunk ⇒ 7-byte heap overflow into the adjacent M_TEMP Zone-16 slab object.
  4. 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_allocated check (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=23payload_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

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sat Jul 18 17:19:50 UTC 2026

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.