# DF-0570 — VERDICT

## Verdict: REPRODUCED + FIX VALIDATED

The inbound-path OOB read is **real, reachable by a remote unauthenticated
attacker, and proven at runtime** by an instrumented module that prints the
out-of-bounds array index the kernel actually computes from the attacker's
packet. The fix (host-order conversion + bounds validation before indexing)
eliminates every OOB access.

This is a **pure out-of-bounds READ** primitive. By Phase-6 rules a read-only
primitive has no escalation chain to `uid=0`; the impact ceiling is **remote
kernel-heap information disclosure and remote DoS (panic)**.

---

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

### The vulnerable inbound path (`ip_fw3_nat`, `args->oif == NULL`)

```c
// sys/net/ipfw3_nat/ip_fw3_nat.c:190  (inbound packet branch)
if (args->oif == NULL) {
    old_addr = &ip->ip_dst;
    ...
    LIST_FOREACH(alias, &nat->alias, next)
        if (alias->ip.s_addr == ntohl(args->f_id.dst_ip)) break;   // alias IP match
    switch (ip->ip_p) {
    case IPPROTO_TCP:
        old_port = &L3HDR(struct tcphdr, ip)->th_dport;            // :203 network order
        s2 = alias->tcp_in[*old_port - ALIAS_BEGIN];               // :204 *** OOB ***
    case IPPROTO_UDP:
        old_port = &L3HDR(struct udphdr, ip)->uh_dport;            // :208
        s2 = alias->udp_in[*old_port - ALIAS_BEGIN];               // :209 *** OOB ***
    case IPPROTO_ICMP:
        old_port = &L3HDR(struct icmp, ip)->icmp_id;               // :214
        s2 = alias->icmp_in[*old_port];                            // :215 *** OOB, no -ALIAS_BEGIN ***
    }
    if (s2 == NULL) goto oops;                                     // :221 guard is AFTER the read
}
```

*old_port points at the **raw network-order** port/icmp_id bytes from the
inbound packet.  It is used **directly** as the array index with **no
`ntohs()`** and **no bounds check**.  The `s2 == NULL` guard at :221 runs
*after* the OOB access, so it cannot prevent it.

`tcp_in[]`/`udp_in[]` have `ALIAS_RANGE` (64511) entries indexed from
`ALIAS_BEGIN` (1024); `icmp_in[]` has 64511 entries indexed from 0.  An
attacker who can deliver a packet to the NAT alias IP controls the index
fully:

| proto | index expression                 | attacker-reachable range | OOB? |
|-------|----------------------------------|--------------------------|------|
| TCP   | `*old_port - 1024`               | [-1024 .. 64511]         | yes  |
| UDP   | `*old_port - 1024`               | [-1024 .. 64511]         | yes  |
| ICMP  | `*old_port`                      | [0 .. 65535] (array 64511)| yes |

### Why the byte-swap matters

`*old_port` is read as a little-endian `uint16_t` from the network-order
field.  An attacker sending on-wire destination port P produces
`*old_port = bswap(P)`.  So the attacker controls the index through both the
port value AND the byte order, making the OOB index fully attacker-chosen.

---

## Runtime evidence (instrumented module — the smoking gun)

Because DragonFly's kernel **direct-maps all physical RAM**, an OOB read of
adjacent (mapped) pages does not fault — so a plain trigger produces no
crash and the externally-observable behaviour (packet dropped) is identical
with/without the bug.  To prove the OOB access actually executes with an
attacker-controlled index, a diagnostic build adds a `kprintf` right after
each inbound array read that prints the computed index and the value
returned whenever the index is out of bounds.

### Before (unpatched module) — OOB reads execute
```
DF0570 icmp OOB idx=65531 s2=0
DF0570 icmp OOB idx=64764 s2=0
DF0570 icmp OOB idx=65020 s2=0
DF0570 icmp OOB idx=65276 s2=0
DF0570 icmp OOB idx=65532 s2=0
... (idx ranges over the attacker-chosen 64511..65535 set)
```
The inbound ICMP path executes `alias->icmp_in[idx]` for `idx` in
**[64511..65535]** — every one is past the 64511-element array.  The read
returned `0` (NULL) here because the adjacent heap pages were zeroed
(fresh `cfg_alias`, `M_ZERO`); with non-zero residue the non-NULL garbage
would be dereferenced at `:335` (`s2->alias_addr`) and either leak kernel
memory or panic (see DF-0569's cleanup-callout crash for the non-NULL case:
`nat_cleanup_func_dispatch` dereferenced `s2=0x1af`).

Reachability is also confirmed by the firewall counter: 1027 inbound ICMP
packets with OOB `icmp_id` matched the `nat 1 icmp ... in` rule and entered
`ip_fw3_nat`.

### After (fixed module) — bounds check denies before the read
```
DF0570FIX icmp BOUNDS-DENIED in_port=64511
DF0570FIX icmp BOUNDS-DENIED in_port=64512
DF0570FIX icmp BOUNDS-DENIED in_port=64513
...
TOTAL BOUNDS-DENIED: 1025          (one per OOB packet)
new OOB reads: 0
```
The fix converts to host order, validates `ALIAS_BEGIN <= port <
ALIAS_BEGIN+ALIAS_RANGE` (ICMP: `port < ALIAS_RANGE`), and `goto oops`
**before** the array access.  No OOB read executes.

---

## Exploit chain / impact ceiling

The primitive is a **pure out-of-bounds READ** of kernel heap, attacker-
controlled index, triggered by an **unauthenticated remote** packet to the
NAT alias IP.  Per Phase-6 rules a read-only primitive has **no escalation
chain to `uid=0`** (valid hard blocker).  Its realistic ceiling is:

* **Remote kernel-heap information disclosure** — when the OOB slot holds
  non-NULL heap residue, `s2->alias_addr`/`s2->src_addr` (:335-339) read
  kernel addresses and the resulting packet is rewritten/delivered carrying
  that data.
* **Remote DoS (panic)** — when the OOB slot holds a non-canonical /
  unmapped pointer, the deref at `:335` faults (the DF-0569 cleanup crash
  demonstrates this non-NULL-garbage path).

On this clean-heap, kmem-direct-map guest the OOB slots read NULL, so the
*observed* effect this run is silent packet drop (no bytes extracted, no
crash) — but the primitive is proven and the leak/DoS follow from non-NULL
heap residue, which a real NAT under load readily provides.

---

## PoC changes

Authored the entire evidence pack from scratch:
* `df0570_oob_trigger.c` — raw ICMP echo sender with attacker-chosen
  `icmp_id` sweep [64511..65535] (runs as root on the box only to simulate
  the unauthenticated remote attacker; raw ICMP sockets need local
  privilege, but the *exploit itself* is unprivileged from the attacker's
  side).
* `fix.diff` — adds `ntohs()` + bounds validation at :204/:209/:215.
* `diag_dmesg_icmp.txt` / `fix_dmesg.txt` — before/after instrumented output.

## Fix

`fix.diff` introduces a host-order `in_port` and bounds-checks it before
each inbound array index, `goto oops`-ing on any OOB value.  It supersedes
the DF-0569 read-side `ntohs()` changes at the same lines (:204/:209/:215)
because it adds the bounds check DF-0569's fix lacks.
