# DF-2255 — Stack OOB write in ar9300_eeprom_restore_internal_address (verbatim-code kernel rig)

**File:** `sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c` (CONTRIB, vendored QCA HAL)
**Severity:** Medium · **Verdict:** REPRODUCED (controlled 1–5 byte kernel-stack OOB write,
attacker-known content, through the byte-identical real parse code) · **Fix:** VALIDATED

## The bug

`ar9300_eeprom_restore_internal_address()` parses the compressed calibration
blocks that sit at the top of an AR9300 card EEPROM/OTP at driver attach
(ar9300_eeprom.c:4186-4188 calls it with `cptr = ar9300_eeprom_base_address()`
= 4095 on a 4 KB EEPROM, `blank=0xff`):

* each block header carries an 11-bit `length` field, max **2047**
  (`ar9300_compression_header_unpack`, ar9300_eeprom.c:3848);
* the whole block is read into `u_int8_t word[MOUTPUT]` = `word[2048]`
  (ar9300_eeprom.c:3892, ar9300eep.h:41) with
  `ar9300_calibration_data_read_array(ah, cptr, word,
  compression_header_length + osize + compression_checksum_length)`
  = **4 + length + 2 = up to 2053 bytes** (ar9300_eeprom.c:3922-3925);
* the original `length >= 1024` guard is dead code inside
  `#ifdef DONTUSE` (ar9300_eeprom.c:3915-3921);
* `ar9300_calibration_data_read_eeprom` only range-checks the *source*
  address against `AR9300_EEPROM_SIZE` 16 KB (ar9300_eeprom.c:3616) and has
  no knowledge of the destination size.

`length >= 2043` ⇒ 1–5 bytes past `word[2047]` are written with
EEPROM-controlled data. On a 4 KB EEPROM every byte read succeeds
(`cptr - 2052 = 2043 >= 0`), so the overflow always fires; the 16-bit
additive checksum gate is trivially computable by whoever crafts the image
(`chk` case proves the gate passes and `_compress_block` applies the payload).
On 1/2 KB devices the highest 5 slots read at negative addresses and fail —
those become the DF-2256 uninitialized-slots case instead.

## Rig (rig class: verbatim-code kernel-compiled harness)

`ath` attach is hardware-gated (no Atheros NIC in the QEMU guest), so per the
established contrib rig the eight target functions are extracted **byte
identical** from the live `/usr/src` copy by `extract.sh` (sha256-gated:
source sha `75771111c80e7ebc…`, extraction logged in `extract.log` /
`logs/extract_baseline.log`) into `df2255_verbatim.inc`, `#include`d by
`df2255_harness.c`, compiled **into the kernel** (`sys/conf/files`:
`kern/df2255_harness.c standard`; runtime kldload GPFs on this guest).
The public symbols are aliased `df2255_*` via preprocessor `#define`s because
the stock kernel already links the contrib `ath_hal` — the extracted text
itself is untouched. The harness fabricates the HAL state exactly as the
driver presents it at attach (`calibration_data_source =
calibration_data_eeprom`, `ah_eepromRead` method → fake 4 KB/2 KB serial
EEPROM device) and calls the real `ar9300_eeprom_restore_internal_address()`
through a volatile function pointer with the kernel stack below the frame
pre-filled with an 0xA5 sentinel. Trigger (root): `sysctl kern.df2255.rig=…`
(`info|arrfail|uninit|otp2k|chk|ovf <len>|ovfall|diff`), evidence on
console/dmesg with the `DFH:` prefix.

## Reproduce

```
# guest, root:  build the rig kernel (see build.sh)
cd /root/df2255 && sh build.sh          # extract (sha256 gate) + nativekernel + installkernel
reboot
sh run.sh                                # or: sysctl -w kern.df2255.rig=ovfall / diff / …
```

**Expected (baseline kernel):** for `ovfall`, five lines
`*** OOB WRITE CONFIRMED: eeprom-controlled kernel-stack bytes past
word[2047] ***` — `word[2048..2052]` = `e0 e1 e2 e3 e4` (the crafted EEPROM
tail) with the 0xA5 sentinel intact at `word[2053..2055]`; `diff` shows
exactly `+2048(e0->d0) … +2052(e4->d4)` when only the five tail EEPROM bytes
change. See `run.log` (authentic serial-console capture).

## Fix validation

`fix.diff` replaces the dead `#ifdef DONTUSE` guard with a live bound check
`length > MOUTPUT - compression_header_length - compression_checksum_length`
(> 2042 ⇒ skip block). Applied to the in-guest `/usr/src`, rebuilt
(`logs/fix1_build.log`), rebooted: all five `ovf` lengths report
`word[2048..2052]` = full sentinel — `no eeprom bytes landed past word[2047]
(sentinel intact -- FIX EFFECTIVE)` — and `diff` shows **0** tracked OOB
bytes (`logs/run_fix1.log`, `fix_run.log`). The legitimate path still works
(`chk`: 16/16 marker bytes restored), and DF-2256 is still live under this
single fix (differential proof, `logs/run_fix1_fullbattery.log`).

## Threat model (honest reachability)

* Input is the **on-card EEPROM/OTP content**: set at manufacture, or
  rewritable only by a privileged HAL_DIAG_EEWRITE on ATH_DIAGAPI builds.
  The attacker is therefore a **malicious/defective AR9300-series card**
  (PCI/PCIe) or an attacker who already has root — NOT an unprivileged
  local user and NOT a remote peer. No unprivileged→root path exists on
  this guest (no ath hardware at all) or on any stock deployment.
* Demonstrated primitive: 1–5 contiguous, fully attacker-controlled bytes
  written past a 2048-byte kernel-stack buffer, repeatable up to
  MSTATE(100) times per attach at chosen contents. On this gcc8
  `-fno-stack-protector` layout the bytes land on intra-frame slack
  (adjacent content at `+2056..2059` unchanged); on SSP kernels gcc places
  arrays against the canary, so the same write smashes canary bytes ⇒
  reliable **panic on attach/hotplug**.
* Realistic impact ceiling: local DoS from crafted peripheral; theoretical
  code execution needs a layout where a saved register/pointer sits exactly
  at `word+2048..2052`. Severity Medium is correct.

## Upstream status (contrib — fix owner is upstream)

Current FreeBSD head `sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c`
(fetched 2026-10-17, sha256 `ae1c326d…`, copy in `logs/`) **still has both
the dead `#ifdef DONTUSE` guard (its line 3971) and the `(void)` read
pattern** — no divergent upstream fix exists; `fix.diff` is a novel,
upstream-contributable patch (QCA license header preserved).

## Files

| file | purpose |
|---|---|
| `df2255_harness.c` | kernel-compiled rig (verbatim include + fake HAL/EEPROM + sysctl trigger) |
| `extract.sh` | byte-identical extraction + sha256 gate + liveness probes |
| `df2255_verbatim_fix2256.inc` | sample generated include (fixed tree) |
| `build.sh` / `run.sh` | exact build & run |
| `build.log` / `run.log` | baseline kernel build + decisive baseline run (serial console) |
| `fix.diff` | the validated fix (git-apply-able vs `sys/`) |
| `fix_build.log` / `fix_run.log` | fix kernel build + before/after run |
| `logs/` | all raw logs: extractions, builds, per-kernel runs, FreeBSD head copy |
| `env.txt` | guest/uptime/kernel mapping |
| `VERDICT.md`, `manifest.json`, `verdict.json` | verdict + machine records |
