# DF-4321 — VERDICT

**Status: inconclusive at kernel level (no Atheros NIC and no ATH_DIAGAPI
in the guest kernel); the bug itself is certain — proven by exact code
analysis and a bug-faithful replica harness that deterministically
overflows 4064 bytes past a 32-byte buffer (guard-page instrumented,
repeatable).**

## Root cause

`sys/dev/netif/ath/ath_hal/ah.c:848-862`
(`ath_hal_getregdump()`, reached from `HAL_DIAG_REGS` —
`ath_hal_getdiagstate()`, ah.c:882-884):

```c
static u_int
ath_hal_getregdump(struct ath_hal *ah, const HAL_REGRANGE *regs,
	void *dstbuf, int space)                       /* space: SIGNED int */
{
	uint32_t *dp = dstbuf;
	int i;

	for (i = 0; space >= 2*sizeof(uint32_t); i++) {   /* (A) int vs size_t */
		uint32_t r = regs[i].start;
		uint32_t e = regs[i].end;
		*dp++ = r;
		*dp++ = e;
		space -= 2*sizeof(uint32_t);
		do {                                           /* (B) body runs first */
			*dp++ = OS_REG_READ(ah, r);                /*     unchecked write */
			r += sizeof(uint32_t);
			space -= sizeof(uint32_t);
		} while (r <= e && space >= sizeof(uint32_t)); /* (C) int vs size_t */
	}
	return (char *) dp - (char *) dstbuf;
}
```

Two compounding defects:

1. **(C)/(A) signed/unsigned**: `sizeof()` is `size_t`; comparing
   `int space >= sizeof(uint32_t)` converts `space` to unsigned. Once
   `space` is negative it becomes ~2^63 and every space check is
   **permanently true**. The loop is then bounded only by the
   attacker's data: the inner by the user-supplied register-range end
   `e`, the outer by the end of the user-supplied `regs[]` array —
   never by the destination buffer.
2. **(B)**: the do-while executes its first `*dp++` write before any
   space check. An outer iteration that starts with `space == 8`
   consumes the remaining 8 bytes as header and then performs one
   unconditional register-value write, taking `space` to −4 — the
   entry ticket into defect 1.

## Trigger (exact arithmetic, verified by the harness)

`SIOCGATHDIAG` with `ad_id = HAL_DIAG_REGS(13) | ATH_DIAG_IN | ATH_DIAG_DYN`,
`ad_out_size = 32` (kernel `kmalloc(32)` output buffer), indata
`HAL_REGRANGE[]`:
* range 0: `{0x8000, 0x800c}` → header(8) + 4 values(16) = 24 bytes,
  `space` = 8;
* ranges 1..N: `{0x9000, 0x9000}` → each writes header(8)+1 value(4) =
  12 bytes; range 1 leaves `space` = −4; **every** subsequent range is
  pure overflow.

Total written = 24 + 12·N bytes into a 32-byte allocation. N is bounded
only by the ioctl input buffer (`ad_in_size`), so the overflow length is
attacker-chosen; each 4-byte word is `OS_REG_READ(ah, r)` with `r` an
attacker-chosen register — e.g. `AR_STA_ID0` (the NIC MAC address) or
`AR_BSS_ID0` (the associated AP's BSSID), both fully known to a local
attacker → **controlled-length linear kernel-heap overflow with
attacker-known contents** on the no-SMEP/no-SMAP audit-guest class of
machines.

## Evidence

* `run.log` / `run.2.log`: replica harness (exact loop expressions) on
  the DragonFly guest as an unprivileged user —
  `=> WROTE PAST THE GUARD PAGE`, **4064-byte overflow past the 32-byte
  buffer**, deterministic across runs; heap smear
  `a5a5a5a5 00900000 00900000 a5a5a5a5` = interleaved range headers and
  "register values".
* `trace.log` / `run.3.log`: the discovery runs — the same replica with
  a 2-entry regs array walked off into random memory (segfault), which
  is how the unsigned-comparison defect was first caught; the annotated
  trace shows `space` going negative while the loop keeps writing
  (`space=-63688 ... dpoff=63808 ...` until fault).
* Guest: `DragonFly 6.5-DEVELOPMENT #0 ... X86_64_GENERIC`, cc (clang)
  — build/run commands in the logs.

## Why not kernel-reproduced here

The kernel path needs `SIOCGATHDIAG`, which is compiled only with
`options ATH_DIAGAPI` (absent from stock X86_64_GENERIC — `nm` shows no
`ath_ioctl_diag` in if_ath.ko), plus an Atheros NIC (guest is
virtio-only). With that config (and DF-4320's missing gate) this becomes
an unprivileged kernel heap overflow. On such a system the escalation
path is: groom the kmalloc zone of `ad_out_size` → overflow into an
adjacent object with known MAC/BSSID-derived words → target a refcounted
object / function-pointer-bearing structure → code exec → uid=0. Not
developed further here for lack of any ath device to drive the ioctl.

## Fix

`fix.diff` — make both space comparisons signed and check space before
the first value write (`while` instead of `do-while` semantics). The
dump simply returns fewer bytes than requested when the buffer is
short — callers already clamp (`ad->ad_out_size = outsize` in
`ath_ioctl_diag`, if_ath_ioctl.c:215-219).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
