DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0573

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0573 · 11 files
FileTypeDescriptionSize
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
README.md readme human-facing reproduction summary
↓ download 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=0nats[-1] — read/write before the array (whatever struct field precedes it in ip_fw3_nat_context) - id>16nats[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).

VERDICT.md verdict narrative: root-only OOB, hardening gap
↓ download raw

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)

  1. Root opens AF_INET/SOCK_RAW/IPPROTO_RAW (requires SYSCAP_NONET_ROOT, gated by rip_attach raw_ip.c:473).
  2. Root issues setsockopt(IPPROTO_IP, IP_FW_X, {opcode=IP_FW_NAT_ADD, ioc.id=17, count=1, ip=10.0.2.15}, sizeof).
  3. The setsockopt lands in rip_ctloutputip_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).
  4. ip_fw3_ctl_nat_add does NOT validate ioc->id before dispatching nat_add_dispatch (ip_fw3_nat.c:731).
  5. nat_add_dispatch indexes nat_ctx->nats[ioc->id - 1] at line 745 — with id=17, this is nats[16], one past nats[NAT_ID_MAX=16] (zero-based indices 0..15).
  6. 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 valid struct cfg_nat * and may dereference/write its fields (nat->id, nat->alias, RB tree heads).
  7. 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 wait messages 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 wait spam.
  • Patched (rebuilt ipfw3_nat.ko): id=0/17/-1 → setsockopt returns EINVAL; id=1..16 → allowed (NAT created correctly via ipfw3 nat N show config confirms). Guest stays up indefinitely.

Kernel references

Fix verification

fixed

validated

see evidence pack
↓ fix.diffn/a (module-level)

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.