# DF-2256 — VERDICT

**Status: reproduced** (impact: none directly — correctness defect that
feeds uninitialized kernel-stack bytes into the parser and amplifies
DF-2255; no demonstrated userspace leak) · **Fix: validated** ·
Contributor: df-bsd-reviewer, 2026-10-17.

## What was claimed

`ar9300_calibration_data_read_array()` ignores per-byte read failures
(`(void)` cast, unconditional `AH_TRUE`, ar9300_eeprom.c:3696-3706), leaving
destination slots uninitialized; the stale bytes then flow into the
blank-check, header unpack, checksum and mchecksum logic in
`ar9300_eeprom_restore_internal_address()`, and can synthesize a length that
drives the DF-2255 overflow.

## How it was verified

Same rig as DF-2255 (byte-identical sha256-gated extraction compiled into
the guest kernel; fake EEPROM device; sentinel-filled stack; see the
DF-2255 pack for the rig construction and build/boot mapping in `env.txt`).

### 1. API level — `arrfail` (baseline kernel, serial console)

`ar9300_calibration_data_read_array(ah, addr=2, buf[16], many=16)` on a
working 4 KB device: addresses 2,1,0 succeed (`buf[0..2] = 11 22 33`),
addresses -1..-13 are rejected by the `address < 0` check inside the
verbatim `ar9300_calibration_data_read_eeprom` (ar9300_eeprom.c:3616) —
13/13 of those slots still hold the pre-call 0xCC sentinel, and the
function returns `AH_TRUE`:

```
DFH: arrfail: buf after call: 11 22 33 cc cc cc cc cc cc cc cc cc cc cc cc cc
DFH: arrfail: ... 13/13 slots still hold the pre-call sentinel, yet the
      function returned AH_TRUE: *** DF-2256 CONFIRMED ***
```

### 2. Amplifier — `uninit` (baseline kernel)

Groom: one successful parse leaves the crafted header (`length=2047`) in the
dead `word[]` stack slots (models any prior kernel stack user). Then the
device goes dark (`fake_fail_all_reads=1`, i.e. absent/unreadable EEPROM —
exactly the `eeprom_size()==0` / dead-OTP situation the driver handles at
attach). Second call at the same stack depth:

* the 4-byte header read fails; `word[0..3]` keep the stale groom bytes;
* the blank-check compares that garbage (`0x60 != 0xff`);
* `ar9300_compression_header_unpack` derives `length = 2047` from
  **uninitialized kernel stack**;
* the payload `read_array` is asked for **2053 bytes into `word[2048]`** —
  the counter trace shows 4104 failed device reads = the 2053-byte attempt
  (plus headers) on iteration 0 and the cptr-walked retry:

```
DFH: uninit: ... unpack derived length=2047 from UNINITIALIZED stack, payload
      read_array of 2053 bytes into word[2048] (5 past the end) ...
      *** DF-2256 AMPLIFIER CONFIRMED: uninit stack drove many > MOUTPUT ***
```

This is precisely the finding text amplifier: with DF-2255 unfixed, stale
stack content can push `many` past the buffer even when the card data is
unreadable.

### 3. Partial failure — `otp2k` (baseline kernel, 2 KB device)

`cptr=2047, length=2047`: the five highest-index reads (i=2048..2052) are
rejected (negative addresses), so `word[2048..2052]` keep the pre-call
sentinel — 5/5 in the final harness run — and those **uninitialized bytes
are then read** by `ar9300_compression_checksum(&word[4], 2047)` (covers
word[2048..2050]) and by the `mchecksum` load at `word[2051],word[2052]`
(ar9300_eeprom.c:3926-3930). No OOB write happens on a 2 KB device (that
requires the reads to succeed — DF-2255 territory).

### Leak sub-claim (assessed, not reproduced to userland)

The in-RAM `ah_eeprom` image can only absorb uninitialized bytes via
`_compress_none` (`length == mdata_size == 1088` real struct size, measured)
with a partially-failing payload read, which on an OTP-fallback cptr=1023
leaves the top ~66 image bytes stale; the checksum gate must then
accidentally match garbage-derived values (~2^-16 per attempt, content fixed
at manufacture). The only in-tree readout of the restored struct is
`HAL_DIAG_EEPROM` (ar9300_misc.c:1207-1210, returns `&ahp->ah_eeprom`),
which is compiled only under `AH_PRIVATE_DIAG` and reached only via
`SIOCGATHDIAG` under `ATH_DIAGAPI` (if_ath_ioctl.c:299) — neither option is
in the stock kernel config (LINT64 only). The readout ioctl itself carries
no privilege check, which is worth noting for hardening, but on any default
build there is no unprivileged path. Verdict: data-flow real, exposure
theoretical — impact recorded as `none` for this finding on its own.

## Fix validation

`fix.diff` (three hunks): propagate the per-byte failure in
`ar9300_calibration_data_read_array`, and honor the return at both call
sites in `ar9300_eeprom_restore_internal_address` (`break` — never parse a
partially-filled `word[]`). Applied on top of the DF-2255 fix in-guest
(source sha `952817c6…`, extraction log `logs/fix2256_extract.log` shows the
liveness probe flip to "read failure handling present"), rebuilt
(`fix_build.log`), rebooted (kernel #9):

* `arrfail`: retval `AH_FALSE`, 13/13 slots never written —
  `FIX EFFECTIVE: failure propagated`;
* `uninit`: aborts after **1** failed read — `FIX EFFECTIVE, no
  garbage-driven parse` (amplifier dead);
* `chk` (legitimate path, all reads succeed): still restores the 16/16
  marker payload — no behavior change for working cards;
* differential (kernel #7, DF-2255 fix only): `arrfail` still `AH_TRUE` +
  stale slots — the two findings are independent.

Fix status: **fixed**.

## Upstream divergence

FreeBSD head 2026-10-17 (`logs/freebsd_head_ar9300_eeprom.c`, sha256
`ae1c326d…`) still has the `(void)ar9300_calibration_data_read(...)` pattern
(line 3759) and both `(void) ar9300_calibration_data_read_array(...)` call
sites (3959/3979). No upstream fix exists to diverge from; `fix.diff` is
the upstream-contributable patch (contrib code — upstream owns the fix,
DragonFly should carry it locally and offer it up).

## Notes

* No panic, no guest instability from any DF-2256 trigger.
* `guest_dirty = 0` — the guest was reverted to the `with-src` snapshot
  after the run matrix.

## 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.
