Unvalidated ioc->id used as nats[] index: OOB read/write of pointer array
Summary
ioc->id from privileged IP_FW_NAT_ADD sockopt used directly as nats[id-1] in nat_add_dispatch(:745) nat_del_dispatch(:797) nat_state_add_dispatch(:712) check_nat(:161). NO check id in [1,NAT_ID_MAX=16]. id==0 index -1 OOB before nats[]. id>16 OOB after. NULL check may pass if OOB slot holds non-NULL -> foreign pointer treated as cfg_nat type confusion. nat_state_add_dispatch also derefs nat/alias without NULL checks. Local-root OOB array access cfg_nat type confusion kernel R/W. Fix: if(id<1||id>NAT_ID_MAX) return EINVAL.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0573 · 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | direct setsockopt bypassing userland id validation | 2.5 KB | view raw |
| build.sh | build-script | cc -o trigger trigger.c | 55 B | view raw |
| run.sh | run-script | ./trigger 17 | 223 B | view raw |
| fix.diff | suggested-fix | bounds-check ioc->id in nat_add/nat_del | 991 B | view raw |
| run.log | run-log | baseline wedge + patched EINVAL contrast | 1.6 KB | view raw |
| fix_run.log | fix-validation-log | patched module: id=17 returns EINVAL | 293 B | view raw |
| env.txt | environment | uname, cc, module info | 238 B | view raw |
| VERDICT.md | verdict | narrative: root-only OOB, hardening gap | 4.5 KB | ↓ raw |
| README.md | readme | human-facing reproduction summary | 2.3 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-0573 — ipfw3_nat ioc->id unvalidated array index (OOB R/W)
Bug
ioc->id from the privileged IP_FW_NAT_ADD/IP_FW_NAT_DEL sockopts
is used directly as nats[id-1] in:
- nat_add_dispatch (ip_fw3_nat.c:745, :764)
- nat_del_dispatch (ip_fw3_nat.c:797, :847)
- nat_state_add_dispatch (ip_fw3_nat.c:712)
- check_nat (ip_fw3_nat.c:161)
with NO validation that id ∈ [1, NAT_ID_MAX=16]. The userland ipfw3
tool validates this (sbin/ipfw3/ipfw3nat.c:104-106), but the kernel
trusts the value. The array nats[] is cfg_nat *nats[NAT_ID_MAX]
(ip_fw3_nat.h:139), so:
- id=0 → nats[-1] — read/write before the array (whatever struct
field precedes it in ip_fw3_nat_context)
- id>16 → nats[16+] — read/write past the array end
The subsequent if (nat == NULL) check at line 162/745 does NOT catch
the OOB — if the OOB slot happens to hold non-NULL (which is true for
slots just past the end where adjacent struct fields live), the kernel
treats random memory as a struct cfg_nat *, dereferences it, and
writes to its fields → type confusion → silent memory corruption.
Setup
Same as DF-0571: ipfw3 modules loaded with
net.filters_default_to_accept=1.
Reproduce
The trigger bypasses the userland ipfw3 validation by calling
setsockopt(IPPROTO_IP, IP_FW_X, ...) directly with an embedded
IP_FW_NAT_ADD opcode and ioc.id=17 (or 0).
./build.sh ./run.sh # as root; sends id=17
On default GENERIC (#0 baseline): setsockopt returns success (the OOB
write happens silently), then the kernel wedges within seconds
(callout/softclock spinlock indefinite-wait messages, then full hang).
Trigger
- The setsockopt path requires
SYSCAP_NONET_RAW(raw IP socket). So the trigger is root-only. This is a root→kernel OOB R/W hardening gap, NOT an unpriv→root escalation. - An unprivileged user CANNOT trigger this: raw IP socket creation
requires
SYSCAP_NONET_RAW(verified:socket(AF_INET, SOCK_RAW, IPPROTO_RAW)→ EPERM as uid=1001 on this guest).
Fix
Add bounds checks in ip_fw3_ctl_nat_add and ip_fw3_ctl_nat_del
(the sockopt entry points) rejecting id outside [1, NAT_ID_MAX].
See fix.diff. Validates correctly: id=0/17/-1 → EINVAL; id=1..16 →
allowed (matches userland behavior, but enforced in kernel).
DF-0573 — Verdict: REPRODUCED (root→kernel OOB; DoS)
Verdict
REPRODUCED as a root-reachable OOB array index in
sys/net/ipfw3_nat/ip_fw3_nat.c. Triggering requires a raw IP socket
(SYSCAP_NONET_RAW, i.e. root), so this is a root→kernel OOB / type
confusion hardening gap, NOT an unprivileged escalation. The bug
itself is real and confirmed; the exploitability ceiling on this guest
is DoS (kernel wedge) on default GENERIC.
Mechanism (trigger → primitive → effect)
- Root opens
AF_INET/SOCK_RAW/IPPROTO_RAW(requiresSYSCAP_NONET_ROOT, gated byrip_attachraw_ip.c:473). - Root issues
setsockopt(IPPROTO_IP, IP_FW_X, {opcode=IP_FW_NAT_ADD, ioc.id=17, count=1, ip=10.0.2.15}, sizeof). - The setsockopt lands in
rip_ctloutput→ip_fw3_sockopt(sys/net/ipfw3/ip_fw3_glue.c:51) →ip_fw3_ctl_x(ip_fw3.c:1039) →ip_fw3_ctl(ip_fw3.c:1054) →ip_fw3_ctl_nat_sockopt(ip_fw3_nat.c:881) →ip_fw3_ctl_nat_add(ip_fw3_nat.c:770). ip_fw3_ctl_nat_adddoes NOT validateioc->idbefore dispatchingnat_add_dispatch(ip_fw3_nat.c:731).nat_add_dispatchindexesnat_ctx->nats[ioc->id - 1]at line 745 — withid=17, this isnats[16], one pastnats[NAT_ID_MAX=16](zero-based indices 0..15).- The OOB slot is read first; if it holds non-NULL garbage, the kernel
enters the
if (nat_ctx->nats[ioc->id - 1] == NULL)true branch is NOT taken — instead the kernel proceeds to treat the garbage as a validstruct cfg_nat *and may dereference/write its fields (nat->id,nat->alias, RB tree heads). - Observed on default GENERIC (#0 baseline): setsockopt returns 0
(silent OOB write), then within ~10s the kernel begins emitting
spin_lock_ex: softclock_handler, indefinite waitmessages as lock structures get corrupted; the system wedges.
Threat model / reachability ceiling
- Trigger requires root (raw IP socket). On this guest an
unprivileged user (uid=1001) cannot reach this path:
socket(AF_INET, SOCK_RAW, IPPROTO_RAW)→ EPERM. - This is a root→kernel OOB / type confusion, which is a real hardening/correctness bug (kernel input from a privileged userland API must be validated), but it is NOT an unpriv→root escalation.
- Realistic impact on this guest: DoS (kernel wedge) via OOB memory corruption. No memory corruption primitive was developed into a priv-esc chain because there is no unpriv trigger path.
Why no priv-esc chain was developed
This is one of the valid hard blockers listed in the procedure: the
write is reachable only from an already-root context (raw IP socket
requires SYSCAP_NONET_RAW). Root→kernel game-over is by definition;
there is no privilege boundary to cross. The honest report is:
demonstrated DoS via root-triggered OOB; no unpriv escalation path
exists.
Fix validation (Phase 8)
Built fix.diff which adds bounds checks in ip_fw3_ctl_nat_add and
ip_fw3_ctl_nat_del:
- sopt_valsize minimum-size check (defense in depth)
- id ∈ [1, NAT_ID_MAX] check (matches userland validation, now
enforced in kernel)
Rebuilt the single ipfw3_nat.ko module with make in
/usr/src/sys/net/ipfw3_nat, installed to /boot/kernel/, rebooted.
- Baseline (#0 kernel, unpatched module): id=17 → setsockopt
returns 0 (OOB write happens), kernel wedges within ~10s with
spin_lock_ex: indefinite waitspam. - Patched (rebuilt ipfw3_nat.ko): id=0/17/-1 → setsockopt returns
EINVAL; id=1..16 → allowed (NAT created correctly via
ipfw3 nat N show configconfirms). Guest stays up indefinitely.
Kernel references
- sys/net/ipfw3_nat/ip_fw3_nat.c:745 —
nats[ioc->id - 1]write in nat_add_dispatch (confirmed unbounded) - sys/net/ipfw3_nat/ip_fw3_nat.c:764 — second
nats[ioc->id - 1]write - sys/net/ipfw3_nat/ip_fw3_nat.c:797 —
nats[msg->id - 1]read in nat_del_dispatch (confirmed unbounded) - sys/net/ipfw3_nat/ip_fw3_nat.c:712 —
nats[msg->nat_id - 1]in nat_state_add_dispatch (confirmed unbounded, also dereferences nat and alias without NULL check) - sys/net/ipfw3_nat/ip_fw3_nat.c:161 —
nats[nat_id - 1]read in check_nat (confirmed unbounded, only NULL-checked not range-checked) - sys/net/ipfw3_nat/ip_fw3_nat.h:40 —
NAT_ID_MAX = 16 - sys/net/ipfw3_nat/ip_fw3_nat.h:139 —
cfg_nat *nats[NAT_ID_MAX] - sys/netinet/raw_ip.c:473 —
caps_priv_check(SYSCAP_NONET_RAW)gate on raw IP socket creation (root-only) - sbin/ipfw3/ipfw3nat.c:104-106 — userland validation that the kernel is missing
Fix verification
fixedvalidated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
REPRODUCED (live). ipfw3_nat nats[id-1] no bounds -> OOB write -> kernel wedge. Root-only. Module fix: EINVAL.
No comments yet.