β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2256

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2256 Β· 22 files
FileTypeDescriptionSize
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() derives code/length/... from that garbage β€” a stale header can synthesize length = 2047 and drive the 2053-byte read of DF-2255 (the amplifier);
  • the checksum loop and the mchecksum read at word[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.

VERDICT.md
↓ download raw

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.

Fix verification

not_testable
↓ fix.diffper-fix-DF-2256

Confirmed kernel references

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.