# DF-4321 — ath_hal_getregdump(): signed/unsigned comparison break → unbounded, attacker-length kernel-heap overflow

## Build

```
cc -O2 -Wall -o oob_sim oob_sim.c    # also reproducible at -O0 (exact byte accounting)
```

## Run

```
./oob_sim                            # or: cc -O0 ... && ./oob_sim0
```

## Expected

```
DF-4321: ath_hal_getregdump() signed/unsigned bounds bug
buffer: 32 bytes at ..., guard page at ...
payload: outsize=32; regs[0]={0x8000,0x800c}, regs[1..4095]={0x9000,0x9000}
legitimately writable: 32 bytes (8 header + 6 values)

=> WROTE PAST THE GUARD PAGE (fault at +4KB)
   bytes written before fault: 4096 (buffer was 32) = 4064-byte overflow
first 16 bytes past the 32-byte buffer (offset 32..48): a5a5a5a50090000000900000a5a5a5a5
```

The harness is a bug-faithful replica of the kernel loop
(`sys/dev/netif/ath/ath_hal/ah.c:848-862`): a 32-byte buffer at the
start of a page followed by a PROT_NONE guard page stands in for the
`kmalloc(32)` DYN output buffer; the mock register read stands in for
`OS_REG_READ`. It demonstrates that the loop's `space` accounting never
stops the writes once `space` goes negative — the `int` operand is
converted to `size_t` in `space >= sizeof(uint32_t)` — so the write
length is bounded only by the attacker's regs[] array (ioctl input) and
kernel address space, not by the buffer. Verified deterministic across
runs; the smear pattern `00900000 a5a5a5a5 ...` is the range header +
"register value" being written far past the allocation.

Kernel-level reproduction additionally requires an Atheros NIC and a
kernel built with `options ATH_DIAGAPI` (the ioctl that reaches this
code, SIOCGATHDIAG, is not compiled into stock X86_64_GENERIC; see
DF-4320). The audit guest has neither (virtio-only PCI bus), so the
kernel path could not be exercised there.

## Trigger mapping (harness → kernel ioctl)

| harness                       | kernel (struct ath_diag ioctl)             |
|-------------------------------|---------------------------------------------|
| buffer 32 bytes @ page start  | `ad_out_size = 32` → `kmalloc(32)` DYN buf  |
| regs[0]={0x8000,0x800c}       | indata HAL_REGRANGE #0 (4 regs → 24 bytes)  |
| regs[1..N]={0x9000,0x9000}    | indata ranges #1..N (12 bytes each)         |
| mock 0xA5A5A5A5               | `OS_REG_READ(ah, 0x9000)` — attacker-chosen register (e.g. AR_STA_ID0 = NIC MAC, known) |

## Files

* `oob_sim.c` — the replica harness.
* `run.log` (-O2), `run.2.log` (-O0, exact accounting), `run.3.log`
  (first accidental discovery run at -O0 with a 2-range stack array —
  showed the runaway walking random memory), `trace.log` (annotated
  -O0 trace showing `space` going negative and the loop continuing).
* `fix.diff` — the corrected loop (signed comparisons + checked first write).
