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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0471 Β· 13 files
FileTypeDescriptionSize
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
README.md readme human reproduce guide
↓ download 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:385 IP_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. ipfw3 is optional ipfirewall3; must be kldloaded.
  • 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/run
  • run.log β€” decisive run on unpatched #0 (stops mid-setsockopt β†’ wedge)
  • panic.txt β€” ddb trap from boot.log
  • fix.diff β€” git-apply-able one-hunk fix
  • fix_build.log β€” patched module build (-Werror, rc=0)
  • fix_run.log β€” patched module: EINVAL + regression (4-byte accepted)
  • env.txt β€” guest uname/cc/kldstat/sysctl
  • VERDICT.md β€” full narrative + fix before/after
  • manifest.json β€” machine-readable catalog
VERDICT.md verdict full narrative + fix before/after
↓ download raw

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 reach rip_ctloutput (sys/netinet/raw_ip.c:385 SOPT_SET IP_FW_X β†’ ip_fw3_sockopt sys/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. ipfw3 is optional ipfirewall3 (sys/conf/files:1818); the default X86_64_GENERIC does not compile it. It must be kldloaded. (The 2-line glue ip_fw3_glue.c is optional inet and IS in GENERIC, but only forwards to the module.)

Mechanism (every hop cited)

  1. Attacker (root): setsockopt(s, IPPROTO_IP, IP_FW_X=49, buf[2], 2).
  2. sys_setsockopt (sys/kern/uipc_syscalls.c:1242): sopt_valsize = 2; sopt_val = kmalloc(2); copyin(buf, sopt_val, 2).
  3. kern_setsockopt (:1213): valsize 0 < 2 <= SOMAXOPT_SIZE β†’ accepted.
  4. sosetopt β†’ rip_ctloutput (sys/netinet/raw_ip.c:385): case IP_FW_X β†’ ip_fw3_sockopt (ip_fw3_glue.c:51).
  5. ip_fw3_ctl_x (ip_fw3.c:1038): - :1042 x_header = sopt_val (cast; points into the 2-byte buffer) - :1043 sopt_name = x_header->opcode reads a uint16_t (2 bytes) β€” in bounds for valsize=2. - :1044 sopt_valsize -= 4 β†’ 2 - 4 = 0xfffffffffffffffe (size_t wrap). - :1045 bcopy(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_val in 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

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: baseline wedge; patched EINVAL. Legit 4B still accepted.

BEFORE: wedge db>. AFTER: rc=-1 errno=22.
↓ fix.diff6.5-DEVELOPMENT #0 (module hot-swap ipfw3.ko)

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).