ar9300_calibration_data_read_array silently ignores read failures leaving stack buffer uninitialized
Summary
ar9300_calibration_data_read_array iterates many single-byte reads but casts each return to (void) and unconditionally returns AH_TRUE. When per-byte read fails (address underflows below 0 on OTP/2KB-EEPROM systems) destination buffer slot left with uninitialized kernel-stack contents. In ar9300_eeprom_restore_internal_address uninitialized bytes flow into compression-header unpack checksum computation and potentially OS_MEMCPY into live EEPROM struct. On OTP-only systems (AR_SREV_HORNET/POSEIDON base_address 512) header read succeeds 4 bytes but if resulting length is large payload read partially fails checksum computed over uninitialized stack data. If stack garbage produces matching checksum (low probability non-zero across boots) uninitialized data memcpy into ahp->ah_eeprom readable via SIOCGATHDIAG/HAL_DIAG_EEPROM leaking ~1KB prior kernel stack. Also amplifier for DF-2255: if initial 4-byte header read fails word[0..3] fully uninitialized word[0]==blank check at :3905 uses garbage producing arbitrary length driving overflow. CONTRIBUTED vendored code upstream owns fix. [VERIFIED pass2: reproduced - 13/13 failed-read slots keep stale sentinel while AH_TRUE returned; amplifier proven - stale bytes derive length 2047 and drive the DF-2255-shaped 2053-byte read; 2KB OTP scope killed 5/5 slots consumed by checksum; leak sub-claim theoretical only; fix-validated - failure propagation aborts parse, legit path unchanged]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2256 Β· 22 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 6.2 KB | β raw | |
| df2255_harness.c | β | 34.1 KB | view raw | |
| extract.sh | β | 3.8 KB | view raw | |
| df2255_verbatim_fix2256.inc | β | 10.5 KB | β download | |
| build.sh | β | 179 B | view raw | |
| run.sh | β | 215 B | view raw | |
| build.log | β | 615.9 KB | β download | |
| run.log | β | 4.6 KB | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| fix_build.log | β | 619.0 KB | β download | |
| env.txt | β | 2.8 KB | view raw | |
| verdict.json | β | 4.9 KB | view raw | |
| logs/extract_baseline.log | β | 1.8 KB | view raw | |
| logs/build_baseline.log | β | 615.9 KB | β download | |
| logs/fix1_build.log | β | 620.0 KB | β download | |
| logs/fix2_build.log | β | 619.0 KB | β download | |
| logs/fix2256_extract.log | β | 1.7 KB | view raw | |
| logs/run_baseline.log | β | 6.4 KB | view raw | |
| logs/run_fix1_fullbattery.log | β | 21.3 KB | view raw | |
| logs/run_fix2.log | β | 6.3 KB | view raw | |
| logs/freebsd_head_ar9300_eeprom.c | β | 163.1 KB | view raw |
DF-2256 β ar9300_calibration_data_read_array ignores read failures, leaves destination uninitialized
File: sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c (CONTRIB, vendored QCA HAL)
Severity: Low Β· Verdict: REPRODUCED (failure-ignoring API + uninitialized
kernel-stack bytes consumed by the parser + amplifier for DF-2255) Β· Fix: VALIDATED
The bug
ar9300_calibration_data_read_array() (ar9300_eeprom.c:3696-3706) loops
single-byte reads but casts each result to (void) and unconditionally
returns AH_TRUE. When a per-byte read fails (address underflow below 0 on
OTP/1-2KB-EEPROM systems, or a dead/absent device), the destination slot is
left with whatever was on the kernel stack, and every caller proceeds as if
the buffer were valid:
- the blank-check
word[0..3] == blank(ar9300_eeprom.c:3905) compares garbage; ar9300_compression_header_unpack()derivescode/length/...from that garbage β a stale header can synthesizelength = 2047and drive the 2053-byte read of DF-2255 (the amplifier);- the checksum loop and the
mchecksumread atword[4+length], word[5+length]consume uninitialized slots (on 2 KB devices the five highest slots are always unreadable).
Rig
Same verbatim-code kernel-compiled rig as DF-2255 (shared harness, sha256
gate, fake 4 KB/2 KB EEPROM device, sentinel-filled kernel stack). Triggers:
sysctl -w kern.df2255.rig=arrfail|uninit|otp2k (root); evidence prefixed
DFH: on console/dmesg.
Reproduce
cd /root/df2255 && sh build.sh && reboot # rig kernel (see build.sh) sysctl -w kern.df2255.rig=arrfail # API-level: failure ignored sysctl -w kern.df2255.rig=uninit # stale header drives length sysctl -w kern.df2255.rig=otp2k # 2KB device: uninit consumed dmesg | grep DFH:
Expected (baseline):
arrfail β 13/13 failed-address slots keep the pre-call sentinel while the
function returns AH_TRUE (*** DF-2256 CONFIRMED ***);
uninit β a device that goes dark leaves word[0..3] stale, the parser
unpacks length=2047 from the uninitialized stack and issues a 2053-byte
read into word[2048] (*** DF-2256 AMPLIFIER CONFIRMED ***);
otp2k β on a 2 KB device word[2048..2052] stay uninitialized and are
consumed by the checksum/mchecksum compare.
Fix validation
fix.diff: read_array propagates the first per-byte failure as AH_FALSE,
and both call sites in ar9300_eeprom_restore_internal_address() break out
of the block scan on failure (never parse a partially-filled word[]).
After the fix (kernel #9): arrfail returns AH_FALSE with the slots never
written, uninit aborts after 1 failed read (no garbage-driven parse), and
the legitimate path is untouched (chk still restores 16/16 marker bytes).
Differential: with only the DF-2255 fix applied, DF-2256 stays live β the
findings are independent.
Threat model (honest)
Same input class as DF-2255 (card EEPROM/OTP at attach): no unprivileged
local path. The claimed userspace readout of uninit-stained data via
HAL_DIAG_EEPROM requires a non-default kernel (AH_PRIVATE_DIAG +
ATH_DIAGAPI, not in stock configs) AND a ~2^-16 checksum coincidence over
garbage, so the leak remains theoretical; the verified impact is
correctness/uninitialized-data-consumption plus the DF-2255 amplifier.
Low severity is correct.
Upstream
FreeBSD head (2026-10-17, copy in logs/) still ships the (void) pattern
at its lines 3759/3959/3979 β no divergent upstream fix; fix.diff is
novel and upstream-contributable.
Files
Same layout as the DF-2255 pack (shared rig): df2255_harness.c,
extract.sh, build.sh, run.sh, build.log, run.log, fix.diff,
fix_build.log, env.txt, logs/, VERDICT.md, manifest.json,
verdict.json.
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_unpackderiveslength = 2047from uninitialized kernel stack;- the payload
read_arrayis asked for 2053 bytes intoword[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: retvalAH_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):
arrfailstillAH_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 thewith-srcsnapshot 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.
Fix verification
not_testableConfirmed kernel references
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3697
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3703
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3705
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3903
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3905
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3923
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_eeprom.c:3929
- sys/contrib/dev/ath/ath_hal/ar9300/ar9300_misc.c:1207
Detail
Evidence (decisive lines)
run.log: baseline arrfail buf dump 11 22 33 cc x13 with retval AH_TRUE (*** DF-2256 CONFIRMED ***); uninit amplifier line (length=2047 from UNINITIALIZED stack, 2053-byte read, *** DF-2256 AMPLIFIER CONFIRMED ***); otp2k 5/5 sentinel slots consumed by checksum/mchecksum; post-fix section shows FIX EFFECTIVE (AH_FALSE propagated, abort after 1 read) with chk 16/16 unchanged; differential section (fix2255-only kernel #7) shows DF-2256 still live. logs/extract_baseline.log: sha256 gate 75771111... env.txt: kernel/boot mapping.
PoC changes
Shared rig with DF-2255 (see that pack): authored the sha256-gated verbatim extractor and kernel-compiled harness, fake EEPROM device with per-word failure control, stack groom between two same-depth verbatim calls to model prior stack users, sentinel window capture. Same 8 harness iterations; DF-2256-specific cases needed the iteration-1 header replant and stub-level vs pre-stub rejection accounting (13 negative-address rejects happen above the device stub).
Verified recommended fix
Make ar9300_calibration_data_read_array return AH_FALSE on the first per-byte read failure and have both call sites in ar9300_eeprom_restore_internal_address break out of the block scan instead of parsing a partially-filled word[].
Verdict
Reproduced through the byte-identical code compiled into the guest kernel. (1) ar9300_calibration_data_read_array(ah, 2, buf, 16) on a working device: the 13 negative-address reads are rejected inside the verbatim read_eeprom, their slots keep the pre-call sentinel, and the function still returns AH_TRUE - the (void) cast at ar9300_eeprom.c:3703 plus unconditional AH_TRUE at :3705 is exactly as claimed. (2) Amplifier: after a successful parse leaves a length=2047 header in the dead word[] stack slots, a device that goes dark makes the next header read fail; the blank-check and ar9300_compression_header_unpack then derive length=2047 from the uninitialized stack and the payload read_array is asked for 2053 bytes into word[2048] (counter trace 4104 failed device reads) - stale stack drives many beyond the buffer, feeding DF-2255. (3) On a 2KB device word[2048..2052] stay uninitialized and are consumed by the checksum loop and the mchecksum loads at word[4+length],word[5+length]. Direct impact is a correctness/uninitialized-data defect (Low): the claimed userspace readout needs a non-default AH_PRIVATE_DIAG+ATH_DIAGAPI kernel and a ~2^-16 checksum coincidence over garbage, so it stays theoretical; no unprivileged local input path exists (card EEPROM/OTP is the input). fix.diff (propagate per-byte failure as AH_FALSE plus break at both call sites) validated on a rebuilt kernel: arrfail returns AH_FALSE with slots never written, uninit aborts after 1 failed read, and the legitimate all-reads-succeed path still restores 16/16 marker bytes.
No comments yet.