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

FillVpdTable/interpolate span never clamped: descending EEPROM curve β†’ (uint16_t)(Pmax-Pmin) wrap β†’ ~131 KB sequential OOB heap WRITE past ar2317State; valid curve spanning >63 half-dB β†’ OOB write past object end with NO wrap (not covered by any family twin)

Summary

Neither the fill loop nor the interpolate loop clamps the span to the 64-wide row or rejects Pmax<Pmin. A descending curve wraps the bound to ~65530 -> up to 131 KB of uint16 stores marching through and past the 2008-byte object into adjacent heap (semi-controlled EEPROM-derived data); a VALID ascending curve spanning 102 half-dB overflows past the last member with no wrap at all. Present verbatim in ar2413/ar2316/ar5413/ar2425 - first filed here. Harness: both modes fault exactly at the struct end; fix (skip inverted/oversized spans) validated. Family-mirror recommended.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4683 Β· 14 files
FileTypeDescriptionSize
README.md β€” 2.6 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
ar2317_harness.c β€” 16.5 KB view raw
build.sh β€” 85 B view raw
run.sh β€” 121 B view raw
fix_validate.sh β€” 2.8 KB view raw
fix.diff β€” 881 B view raw
fixB.diff β€” 699 B view raw
build.log β€” 686 B view raw
run.log β€” 3.0 KB view raw
fix_run.log β€” 8.6 KB view raw
env.txt β€” 844 B view raw
manifest.json β€” 1.1 KB view raw
verdict.json β€” 5.7 KB view raw

DF-4683 β€” ar2317: FillVpdTable/interpolate span never clamped β†’ OOB heap WRITE

past ar2317State (uint16 wrap runaway + valid-oversized-span variants)

NEW (not covered by the ar5413/ar2316/ar2417/ar2413 family twins): neither ar2317FillVpdTable() nor the interpolate loop bounds the pd-gain power span to the vpdTable row width (MAX_PWR_RANGE_IN_HALF_DB = 64, ah_eeprom_v3.h:291) or validates Pmax >= Pmin.

Build

Same harness as DF-4680: cd /root/df2317 && sh build.sh (the ar2317State object is placed with its end exactly at a page boundary and a PROT_NONE guard page after it, emulating the kernel heap allocation)

Run

./ar2317_harness 4     # descending EEPROM curve  -> uint16 wrap runaway
./ar2317_harness 5     # valid curve spanning 102 -> plain missing clamp

Expected (baseline)

--- mode 4 ---
REPRODUCED: SIGSEGV si_addr=0x800476000
  object guard page at 0x800476000 (struct end); si_addr is 2008 bytes past
  struct start (sizeof=2008)
  FillVpdTable loop bound (uint16_t)(Pmax-Pmin) = (uint16_t)(84-90) = 65530
  entries (131060 bytes) into a 64-wide row
MATCH: unbounded OOB WRITE ran off the end of the ar2317State heap object

--- mode 5 ---  (identical guard-page fault, from VALID data, no wrap)
  • Mode 4: Pmin_t2/Pmax_t2 are computed at ar2317.c:372-382 without any monotonicity check; a descending pwr_t4 list gives Pmax < Pmin, so while (ii <= (uint16_t)(Pmax - Pmin)) (ar2317.c:289) wraps to ~65530 and the loop writes up to 131060 bytes from vpdTable_L[0][0] β€” through the whole 2008-byte object and off its end (kernel: adjacent heap objects corrupted / panic).
  • Mode 5: a valid ascending curve spanning 102 half-dB needs no wrap: FillVpdTable writes row 3 indices 64..102 (intra-object spill) and the interpolate loop (ar2317.c:393-398) writes VpdTable_I[3][0..101] β€” vpdTable_I is the last member, so indices β‰₯64 land past the object.

Written values are EEPROM-Vpd-derived (0..255, or 0xFFxx on negative extrapolation): semi-controlled kernel-heap overwrite. Rated Medium (the twins are Low) for the beyond-object write reach.

Fix validation

fix.diff (fixD) skips any pdGain with Pmax < Pmin or span β‰₯ MAX_PWR_RANGE_IN_HALF_DB (continue before the FillVpdTable/interpolate calls). Patched: mode 5 completes with fixD alone (numPdGainsUsed=3); mode 4 completes with fixD+fixB (fixB.diff = DF-4681's early return; skipping the inverted gain leaves zero gains, whose residual path DF-4681's fix covers).

The same defect exists verbatim in ar2413.c:309, ar2316.c:312, ar5413.c:353, ar2425.c:311 β€” recommend fixing all five together.

VERDICT.md
↓ download raw

DF-4683 β€” VERDICT

Status: REPRODUCED (guest, verbatim-extraction harness, modes 4+5) Β· impact: dos (kernel heap corruption) Β· confidence: certain Β· fix: fixed (validated)

Root cause

Two loops write into priv->vpdTable_{L,R,I}[pdGainIdx][ii] β€” rows are MAX_PWR_RANGE_IN_HALF_DB = 64 uint16 entries (ah_eeprom_v3.h:291) inside the heap-allocated struct ar2317State (last three members; vpdTable_I is the final member):

  1. ar2317FillVpdTable() β€” while (ii <= (uint16_t)(Pmax - Pmin)) retVpdList[pdGainIdx][ii] = kk; (ar2317.c:289-306). The only sanity check inside is numIntercepts < 2 (:286); the span Pmax - Pmin is never validated against the row width, and Pmax < Pmin is never rejected.
  2. the interpolate loop β€” for (kk = 0; kk < (uint16_t)(Pmax_t2[..] - Pmin_t2[..]); kk++) VpdTable_I[numPdGainsUsed][kk] = ... (ar2317.c:393-398), same missing clamp.

Pmin_t2/Pmax_t2 derive straight from EEPROM bytes at :372-382 (pwr_t4 quarters of dBm, /2 β†’ half dB): descending lists give Pmax < Pmin (wrap trigger), wide lists give spans up to 127 (> 64; plain clamp trigger). No sorting/monotonicity validation exists anywhere on the v3-EEPROM raw-data path.

Verified behavior (run.log)

The ar2317State is emulated as the kernel heap allocation: mmap'd, struct end placed exactly at a page boundary, PROT_NONE guard page after.

  • Mode 4 (wrap) β€” gain slot 3 with pwr_t4 {180,176,172,168} β†’ Pmin_t2=90, Pmax_t2=84 β†’ (uint16_t)(84-90) = 65530 β†’ the loop starts storing at vpdTable_L[0][0] and marches; SIGSEGV on the STORE at exactly the struct end (si_addr == guard base; the faulting instruction is the retVpdList[...] = kk store β€” the reads at that point target the mapped dataset). Proven escape of a would-be 131060-byte sequential overwrite with Vpd-derived values.
  • Mode 5 (clamp, no wrap) β€” gain slot 0 with pwr_t4 {0,68,136,204} (perfectly valid ascending data, span 102): FillVpdTable row-3 indices 64..102 spill intra-object (vpdTable_R/vpdTable_I heads β€” observed canary clobber), then the interpolate loop writes VpdTable_I[3][64..101], i.e. past the object end; SIGSEGV at exactly &vpdTable_I[3][64] == struct end. Valid EEPROM data, no integer wrap, pure missing clamp.

In-kernel: writes continue into adjacent kernel heap objects (ath_hal_malloc = kmalloc) for up to ~128 KB (mode 4) β€” adjacent-object corruption with semi-controlled content, or immediate panic on unmapped KVA. This is an OOB write beyond the heap object, a class stronger than the intra-object spill (DF-4680) and wild reads (DF-4681/4682) filed for the family β€” hence Medium, memcorrupt bucket.

Why the family twins did not cover it

DF-4619/4620/4622 (ar5413), DF-4648/4649/4650 (ar2316), DF-4660/4661/4662 (ar2413) each filed the three specified twins only. The span defect is verbatim-present in all five ar5212 RF files (ar2317.c:289 == ar2413.c:309 == ar2316.c:312 == ar5413.c:353 == ar2425.c:311); this is its first filing (on ar2317, as instructed). Recommend mirroring the fix to the other four.

Fix validation (fix_run.log)

fix.diff (fixD): continue (skip gain) when Pmax_t2 < Pmin_t2 or (Pmax_t2 - Pmin_t2) >= MAX_PWR_RANGE_IN_HALF_DB, placed after the Pmin/Pmax computation and before the FillVpdTable/interpolate calls. - Mode 5 with fixD alone: completes, numPdGainsUsed=3 (oversized gain skipped), no fault. - Mode 4 with fixD+fixB (fixB = DF-4681's numPdGainsUsed==0 early return, included as fixB.diff): completes, ret=0, sentinel minPower β€” the residual zero-gain path created by skipping the only gain is covered by DF-4681's guard, as documented. git apply --check clean against sys/; never applied to sys/.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Validated on the guest: fix.diff (skip any pdGain whose span is inverted (Pmax= MAX_PWR_RANGE_IN_HALF_DB via 'continue' before the FillVpdTable/interpolate calls) applied to a copy of the pristine guest ar2317.c. Mode 5 completes with fixD alone ('no SIGSEGV', numPdGainsUsed=3 -- the oversized gain is skipped). Mode 4: fixD alone leaves numPdGainsUsed==0 (all other gains unused), reaching the DF-4681 wild-read path, so fixB.diff (DF-4681's early return, also in this pack) is applied with it: mode 4 then completes with ret=0 and sentinel minPower. fix.diff is git-apply-able against the pristine tree and was never applied to sys/.

fix_run.log ('===== fixD' and '===== fixD + fixB' sections: PATCHES_APPLIED, FIXED_BUILD_OK, both modes 'no SIGSEGV: function completed'); run.log modes 4/5 for the baseline guard-page faults.
↓ fix.diffper-fix-DF-4683

Confirmed kernel references

Detail

Exploit chain

crafted v3 EEPROM on AR2317 card: (a) descending pwr_t4 curve -> (uint16_t)(Pmax-Pmin) wrap -> ~131 KB sequential uint16 overwrite from vpdTable_L through and past the ath_hal_malloc'd ar2317State into adjacent kernel heap objects -> corruption/panic; or (b) valid curve spanning >63 half-dB -> up to 128 bytes past the object end via VpdTable_I[3][64..]. Values are Vpd-derived (0..255 / 0xFFxx). No uid0 route demonstrated (gated on attacker-supplied AR2317 hardware); in-kernel ceiling is adjacent-heap corruption with semi-controlled data.

Evidence (decisive lines)

run.log mode 4: 'REPRODUCED: SIGSEGV si_addr=0x800476000' (== guard == struct end, 'si_addr is 2008 bytes past struct start'), 'FillVpdTable loop bound (uint16_t)(84-90) = 65530 entries (131060 bytes) into a 64-wide row', 'MATCH: unbounded OOB WRITE ran off the end of the ar2317State heap object'; run.log mode 5: same guard fault from the valid 102-half-dB curve; fix_run.log: fixD-only mode 5 completes (numPdGainsUsed=3), fixB+fixD mode 4 completes (ret=0, sentinel minPower).

PoC changes

New modes 4/5 added to the DF-4680 harness: object placed at a page end with a PROT_NONE guard page so OOB WRITEs fault at exactly struct end; modes 1-3 unchanged from the twin harness.

Verified recommended fix

Reject pd-gain curves with Pmax_t2 < Pmin_t2 or span >= MAX_PWR_RANGE_IN_HALF_DB before calling ar2317FillVpdTable / the interpolate loop (fix.diff; mirror to ar2413/ar2316/ar5413/ar2425).

Verdict

REPRODUCED on the DragonFly guest (verbatim-extraction harness, modes 4 and 5) -- a heap OOB WRITE one class stronger than the ar2317 twins and NOT covered by them: neither ar2317FillVpdTable's loop nor the interpolate loop clamps the pd-gain power span to the vpdTable row width (MAX_PWR_RANGE_IN_HALF_DB=64, ah_eeprom_v3.h:291) or validates Pmax>=Pmin. (Mode 4, wrap variant) a DESCENDING EEPROM pwr_t4 curve yields Pmax_t2=84 < Pmin_t2=90 (ar2317.c:372-382 performs no monotonicity check), so while (ii <= (uint16_t)(Pmax - Pmin)) at ar2317.c:289 wraps to ii<=65530 and the loop writes retVpdList[pdGainIdx][ii] for ii=0..65530 -- up to 131060 bytes marching from vpdTable_L[0][0] through the whole 2008-byte ar2317State and off its end; the harness (object placed with its end at a page boundary and a guard page after) faulted on the STORE at exactly the struct end, proving the write escaped the heap object. (Mode 5, clamp variant) a perfectly VALID ascending curve spanning 102 half-dB (>64) needs no integer wrap: FillVpdTable writes vpdTable_L/R row 3 indices 64..102 (intra-object spill) and the interpolate loop at ar2317.c:393-398 writes VpdTable_I[3][0..101] -- vpdTable_I is the LAST member, so indices >=64 land past the object end; faulted at exactly &vpdTable_I[3][64] == struct end. Written values are EEPROM-Vpd-derived (0..255, or 0xFFxx from negative extrapolation), so in-kernel this is a semi-controlled kernel-heap overwrite of up to ~128 KB followed by adjacent-object corruption or an immediate panic. The family pass-2s (DF-4619..4662) filed only the kk/gain-count/flat-curve siblings; this span defect is present in all five ar5212 RF files (ar2317.c:289 == ar2413.c:309 == ar2316.c:312 == ar5413.c:353 == ar2425.c:311) but is filed here first. Fix validated: fixD (reject inverted/oversized spans via 'continue') makes mode 5 complete with fixD alone; mode 4 completes with fixD+fixB (skipping the inverted gain leaves zero gains -- DF-4681's early return covers that residual).