# DF-0569 — VERDICT

## Verdict: REPRODUCED + FIX VALIDATED

The heap OOB write via byte-swapped `alias_port` used as array index is **real, confirmed, and fixable**. Two independent lines of evidence prove the bug at runtime; the fix eliminates all OOB writes.

---

## Root-cause mechanism (line-by-line)

### The store (line 439)
```c
// sys/net/ipfw3_nat/ip_fw3_nat.c:439
s->alias_port = htons(krandom() % ALIAS_RANGE + ALIAS_BEGIN);
```
`pick_alias_port()` generates a host-order port in [1024, 65534], then stores it as **network byte order** via `htons()`. On little-endian x86-64, this byte-swaps the two bytes.

### The mis-indexed write (line 423, same-CPU path)
```c
// sys/net/ipfw3_nat/ip_fw3_nat.c:423
alias->tcp_in[s->alias_port - ALIAS_BEGIN] = s2;
```
`s->alias_port` is read as a `uint16_t` — on little-endian, its VALUE is the byte-swapped result. This is used **directly** as an array index without `ntohs()`. The C integer promotion rules make `s->alias_port - ALIAS_BEGIN` a signed `int`. When the byte-swapped value < 1024, the result is negative.

### Same bug at 7 more sites
- **Line 425:** `alias->udp_in[s->alias_port - ALIAS_BEGIN] = s2;` (UDP write, same-CPU)
- **Line 721:** `alias->tcp_in[msg->alias_port - ALIAS_BEGIN] = s2;` (TCP write, cross-CPU)
- **Line 723:** `alias->udp_in[msg->alias_port - ALANS_BEGIN] = s2;` (UDP write, cross-CPU)
- **Line 204:** `s2 = alias->tcp_in[*old_port - ALIAS_BEGIN];` (TCP read, return path)
- **Line 209:** `s2 = alias->udp_in[*old_port - ALIAS_BEGIN];` (UDP read, return path)
- **Line 326:** `alias->icmp_in[s->alias_port] = s2;` (ICMP write)
- **Line 215:** `s2 = alias->icmp_in[*old_port];` (ICMP read)

### When it fires
The OOB fires when the low byte of the host-order random port is **0, 1, 2, or 3** (probability 4/256 = 1.5625%). Example:
- host value `0x0401` (1025) → `htons` → `0x0104` (260) → index `260 - 1024 = -764`
- Writes `s2` (8-byte pointer) at `tcp_in[-764]`, i.e., 6112 bytes before `tcp_in[0]`

### OOB direction (correcting the finding claim)
The finding claims the index "wraps unsigned to ~64772, past `tcp_in[64511]`". This is **incorrect** — C integer promotion makes `uint16_t - int` → `int`, yielding a **negative** index in `[-1020, -1]`. The OOB write goes **before** the array, not past its end. The impact is the same (heap corruption), but the direction is opposite.

For the **UDP path** specifically, `udp_in[]` immediately follows `tcp_in[]` in `struct cfg_alias`, so `udp_in[negative_index]` writes into the **tail of `tcp_in[]`** — still wrong, but within the same allocation.

For the **TCP path**, `tcp_in[negative_index]` writes before the array: into `struct cfg_alias`'s `ip`/`next` fields (indices -1 to -3) or into kernel heap before the allocation (indices -4 to -1020).

---

## Evidence

### 1. Deterministic arithmetic proof (`alias_port_oob_proof.c`)
Scanned all 64511 possible host-order ports:
- **1008 values (1.56%)** produce OOB indices
- OOB index range: **[-1020, -1]** (all negative)
- Maximum offset: **8160 bytes** before `tcp_in[0]`

### 2. Runtime state-table evidence (5000 UDP flows through NAT)
After driving 5000 UDP flows through ipfw3 NAT on the unpatched `#0` kernel:

| Metric | Value |
|--------|-------|
| Total NAT states | 15497 |
| UDP entries | 15395 |
| **TCP entries** | **102** ← impossible with UDP-only traffic! |

All 102 TCP entries are UDP `s2` structs misplaced into `tcp_in[]` by the byte-swap OOB. Every one has a host-order `dst_port` with **low byte exactly 0–3**:

| Low byte | Count |
|----------|-------|
| 0 | 27 |
| 1 | 25 |
| 2 | 26 |
| 3 | 24 |
| ≥4 | **0** |

This distribution is statistically perfect: uniform across {0,1,2,3} and zero outside — exactly the byte-swap OOB signature.

### 3. Panic (raw SYN packets)
Sending raw TCP SYN packets through the NAT caused a kernel panic:
```
delayed m_pullup, m->len: 40  off: 32084  p: 6
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x10
Stopped at in_delayed_cksum+0x71:  movq 0x10(%r12),%rax
```

**Important caveat:** This panic also occurs with the patched module (same crash signature). Investigation shows the crash is caused by the NAT code setting `CSUM_TCP` on raw packets whose mbuf metadata isn't set up for delayed checksum processing — a **separate bug** from DF-0569. The DF-0569 evidence is the UDP state-table OOB entries, not this crash.

---

## Exploit chain (INVARIANTS hard blocker)

The primitive is an **8-byte pointer heap OOB write** via the `nat_state2 *` store. On this guest, **INVARIANTS is enabled**, which blocks slab-grooming escalation:

- The write stores a **valid** `nat_state2 *` pointer at a negative index. The pointer value is not attacker-controlled (it's a freshly `kmalloc`'d `nat_state2`), but the *destination offset* is determined by the random alias port's byte-swap.
- For the TCP path (indices -4 to -1020), the write corrupts kernel heap before `cfg_alias`. This could overwrite a victim object's function pointer, `ucred *`, refcount, or data pointer. With no SMAP/SMEP/KASLR, redirecting a corrupted pointer to userspace shellcode would give `uid=0`.
- **However**, INVARIANTS (enabled on this guest) catches many slab-level corruptions during allocation/free, making reliable slab grooming infeasible without first defeating the INVARIANTS checks.
- For the UDP path (indices -1 to -1020 from `udp_in`, which maps into the tail of `tcp_in[]`), the write stays within the `cfg_alias` struct — no useful victim corruption.

**Conclusion:** The OOB write is a genuine memory-corruption primitive that could be escalated to `uid=0` on a non-INVARIANTS kernel (the standard DragonFly release config). On this audit guest (INVARIANTS on), reliable escalation is blocked. The remote-trigger nature (any traffic through a NAT'd interface) makes this a high-severity finding regardless.

---

## Fix validation

### Fix: `fix.diff`
Adds `ntohs()` at all 8 array index sites, ensuring the host-order value is always used for indexing. The fix is minimal (8 single-line changes) and doesn't change the storage format of `alias_port`.

### Before/after comparison (identical workload: 5000 UDP flows)

| Metric | Unpatched `#0` kernel | Patched module |
|--------|----------------------|----------------|
| TCP entries in state table | **102** (OOB writes confirmed) | **0** (no OOB) |
| OOB write rate | 1.56% of flows | 0% |
| All OOB ports have low byte 0–3 | ✓ | N/A |
| Module hash | 28b50c63... | 400941d5... |

The fix completely eliminates the OOB writes. NAT continues to function correctly (9828 valid UDP states created and displayed).

### Note on raw SYN crash
The raw SYN crash at `in_delayed_cksum` persists with the fix. This is a **separate bug**: the NAT code sets `m->m_pkthdr.csum_flags = CSUM_TCP` on raw packets whose mbuf metadata isn't properly initialized for delayed checksum processing. This is NOT DF-0569 and is not addressed by the byte-swap fix. It should be reported as a separate finding.

---

## PoC changes

Authored the entire evidence pack from scratch (no seeded PoC folder existed):
- `alias_port_oob_proof.c` — deterministic arithmetic proof
- `nat_oob_trigger.c` — UDP flow driver (primary trigger, avoids SSH breakage)
- `nat_oob_trigger_tcp.c` — TCP connection driver (slow, for reference)
- `raw_syn_trigger.c` — raw SYN packet sender (fast TCP path, but hits separate checksum bug)
- `run.sh` — automated NAT setup + driver
- `build.sh` — build script
- `fix.diff` — 8-site `ntohs()` fix

---

## Re-verification (2026-07-16, same #0 guest, 6 CPUs)

Re-confirmed on the current master DEV guest. After driving 4000 outbound UDP
flows the state table again showed the byte-swap OOB signature:
- 7812 UDP entries + **55 TCP entries** (impossible with UDP-only traffic),
- every misplaced TCP entry's host-order `dst_port` has low byte exactly 0–3.

Additionally, under sustained NAT traffic the **cleanup callout panicked** on
the heap corrupted by the OOB writes (`panic_cleanup.txt`):
```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x1c7
Stopped at nat_cleanup_func_dispatch+0x1bf: subq 0x18(%rdi),%rdx
```
`0x1c7 = s2(0x1af) + 0x18` = `nat_state2->timestamp`: the cleanup found a
**garbage non-NULL `nat_state2` pointer (0x1af)** in one of the alias arrays
(corrupted by the OOB writes) and dereferenced it. This is direct runtime
proof the OOB writes corrupt adjacent heap and the corruption is fatal.
