# DF-0494 PoC: ARP `ar_hln`/`ar_pln` kernel heap+stack memory disclosure

**Status: REPRODUCED + FIX VALIDATED.** Remote-unauthenticated (same-L2)
kernel OOB read via a crafted ARP REQUEST. Read primitive only (info leak /
KASLR defeat); see `VERDICT.md` for the full analysis.

## Vulnerability

`arpintr()` (`sys/netinet/if_ether.c:627`) validates `ar_hrd` and `ar_pro` and
the packet length, but **never validates `ar_hln`/`ar_pln`**. `in_arpreply()`
(:1162) then uses those attacker-controlled byte values as `memcpy` lengths
against a 6-byte source (`IF_LLADDR`, the interface MAC) and a 4-byte source
(`&taddr`, a stack `in_addr_t`), over-reading adjacent kernel **heap** and
**stack** into the ARP REPLY mbuf, which is transmitted to the attacker.

- `sys/netinet/if_ether.c:1183` — heap OOB via `memcpy(ar_sha, IF_LLADDR, ar_hln)`
- `sys/netinet/if_ether.c:1242` — stack OOB via `memcpy(ar_spa, &taddr, ar_pln)`
- (proxy paths :1226, :1237 carry the same heap OOB)

## Files

| file | purpose |
|------|---------|
| `arp_leak.c`     | trigger: tap(4)-based oversized-ARP crafter + REPLY capture + leak analyser |
| `arp_heap_leak.py` | seeded scapy PoC (reference for real-network use; needs same-L2 iface) |
| `build.sh`       | `cc -O2 -o arp_leak arp_leak.c` |
| `run.sh`         | sets up `tap0` (victim IP), injects, captures. args: `[ar_hln] [ar_pln]` |
| `run.log`        | full untrimmed output of the decisive reproduction (heap + stack leaks) |
| `leak_sample.txt`| annotated raw leaked bytes + kernel-pointer scan |
| `env.txt`        | guest uname, cc, cpu, tap0 setup |
| `fix.diff`       | git-apply-able fix (validate ar_hln/ar_pln in arpintr) |
| `fix_build.log`  | full patched-kernel build log |
| `fix_run.log`    | patched-kernel PoC run (leak gone) |
| `VERDICT.md`     | full narrative + before/after |
| `manifest.json`  | artifact catalog |

## Reproduce

The PoC simulates a same-L2 remote attacker inside the guest using `tap0` as a
private L2 island (QEMU user-mode/slirp doesn't let the host inject into the
guest's L2). Root is needed only to create `tap0` / open `/dev/tap0`; the bug
itself needs **no privilege** — a real remote attacker just sends one ARP frame.

```sh
# on the DragonFly guest (as root):
cd poc/DF-0494
./build.sh                       # cc -O2 -o arp_leak arp_leak.c
./run.sh                         # default: ar_hln=200 ar_pln=200 (auto-clamped)
./run.sh 100 4                   # heap leak:  94 OOB bytes past 6-byte MAC
./run.sh 6 100                   # stack leak: 96 OOB bytes past 4-byte &taddr
./run.sh 6 4                     # sanity: a LEGAL arp -> clean reply, no leak
```

### Expected (bug present, unpatched `#0` kernel)

- `./run.sh 100 4` → ARP REPLY whose `ar_sha` = victim MAC + **94 bytes of
  kernel heap** (kernel residue; the request's `0xCC` filler is NOT echoed).
- `./run.sh 6 100` → ARP REPLY whose `ar_spa` = victim IP + **96 bytes of
  kernel stack** containing ~10 canonical kernel pointers (kernel-`.text`
  return addresses `0xffffffff80??????`, kernel-virtual `0xfffff800????????`).
- `./run.sh 6 4`   → clean 6-byte MAC / 4-byte IP REPLY, 0 OOB bytes (sanity).

### Expected (FIXED kernel, `fix.diff` applied)

- `./run.sh 100 4` and `./run.sh 6 100` → **NO ARP REPLY** (the oversized
  REQUEST is dropped by the new `arpintr()` guard). `./run.sh 6 4` still works.

## Network setup (why tap0)

QEMU user-mode (slirp) networking gives the guest `vtnet0` behind NAT; the host
cannot inject raw L2 frames into it. The PoC therefore creates a `tap0`
interface with a private IP (`10.99.99.1`), writes a crafted Ethernet/ARP frame
to `/dev/tap0` (== frame arriving on the wire → `ether_input` → `arpintr`), and
reads the REPLY back from `/dev/tap0` (== frame the kernel emitted). This
exercises the exact kernel code path a remote same-L2 attacker would.

**Harness caveat:** the `tapwrite()` allocator builds an mbuf *chain*, so the
demonstrable leak is capped where all four ARP variable fields fit in the first
mbuf (`ar_hln ≤ ~106`, `ar_pln ≤ ~103`). On real hardware (single contiguous
RX cluster mbuf) the full attacker range — `ar_hln`/`ar_pln` up to 255, i.e. up
to ~249 heap / ~251 stack leaked bytes — applies, exactly as the finding
states. The mechanism and the leak-size proportionality are identical.
