Forged EEPROM pwr_table_offset drives OOB write/read in ar9280AdjustPDADCValues (up to 64KB overflow)
- File:
sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c - Lines: 188β192 (
ChangeGainBoundarySettings), 215β233 (AdjustPDADCValues); reachable viaar9280SetPowerCalTable:301,376,399 - Severity: High
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write, CWE-125 Out-of-bounds Read, CWE-190 Integer Overflow or Wraparound
- Confidence: certain
- Status: new
Summary
The static helper ar9280AdjustPDADCValues() shifts a 128-byte PDADC
calibration buffer by diff entries, where diff is derived from the EEPROM
pwr_table_offset field via ar9280ChangeGainBoundarySettings().
pwr_table_offset is read raw from the card EEPROM (ah_eeprom_v14.c:139)
with no range validation. A forged EEPROM byte < -5 makes diff negative,
so the loop bound NUM_PDADC(diff) = 128 - diff exceeds 128, writing past
the end of the static pdadcValues[128] buffer and reading before its start.
A forged byte > +59 makes diff > 128, so NUM_PDADC(diff) goes negative
and β after the (uint16_t) cast on the loop bound at line 226 β wraps to
~65000, causing a catastrophic ~64KB overflow that corrupts kernel BSS and
page-faults.
Reachable at NIC attach and every channel change on AR9280 (Merlin) hardware.
Root cause
Data flow:
-
ar9280SetPowerCalTablereadspwr_table_offsetfrom EEPROM atar9280_olc.c:301viaath_hal_eepromGet(AR_EEP_PWR_TABLE_OFFSET). The EEPROM getter atah_eeprom_v14.c:137-142returns the raw struct byte (pBase->pwr_table_offset, declareduint8_tatah_eeprom_v14.h:189) reinterpret-cast toint8_twith zero validation β the only gate is EEPROM minor version>= 0x15(21), itself an attacker-controlled byte. -
ar9280SetPowerCalTable:376-378callsar9280ChangeGainBoundarySettings(ah, ..., pwr_table_offset, &diff)where the caller'sdiffisuint16_t(line 345) but the formal isint16_t *diff(line 176) β a signedness type-mismatch. -
Inside
ar9280ChangeGainBoundarySettings, whenpwr_table_offset != AR5416_PWR_TABLE_OFFSET_DB(-5)(gate at line 188), it computes:
c
*diff = (uint16_t)(pwr_table_offset - AR5416_PWR_TABLE_OFFSET_DB); /* line 190 */
*diff *= 2; /* line 192 */
For pwr_table_offset=-6 (raw byte 0xFA):
*diff = (uint16_t)(-1) = 65535 β stored as int16_t = -1 β *2 = -2.
For pwr_table_offset=-128 (byte 0x80): diff = -246.
For pwr_table_offset=+64 (byte 0x40): diff = (64+5)*2 = 138.
For pwr_table_offset=+127 (byte 0x7F): diff = 264.
-
The return value (
int16_t, line 208) is assigned back to the caller'suint16_t diff(line 376), then passed toar9280AdjustPDADCValues(ah, pwr_table_offset, diff, pdadcValues)at line 399. -
Inside
ar9280AdjustPDADCValues(line 212), the macroNUM_PDADC(diff) = AR5416_NUM_PDADC_VALUES - diff = 128 - diff(line 215). The first loop at line 226 is:
c
for (k=0; k < (uint16_t)NUM_PDADC(diff); k++)
pdadcValues[k] = pdadcValues[k+diff];
The buffer pdadcValues is the function-local static uint8_t pdadcValues[AR5416_NUM_PDADC_VALUES]
= uint8_t[128] declared at ar9280SetPowerCalTable:288.
Concrete overflows
- (a) diff=-2 (
pwr_table_offset=-6):NUM_PDADC=130, loop writespdadcValues[128..129]= 2-byte OOB WRITE past the 128-byte BSS buffer; readspdadcValues[-2..-1]= 2-byte OOB READ before the buffer. - (b) diff=-246 (byte
0x80):NUM_PDADC=374, writespdadcValues[128..373]= 246-byte OOB WRITE; readspdadcValues[-246..-1]. - (c) diff=138 (byte
0x40):NUM_PDADC = 128-138 = -10;(uint16_t)(-10) = 65526; loop writespdadcValues[0..65525]= ~64KB OOB WRITE β will page-fault and panic the kernel almost immediately (reliable DoS), corrupting everything in its path first. - (d) diff=264 (byte
0x7F):NUM_PDADC=-136βuint16_t 65400β ~64KB overflow.
The second loop at line 231 only runs when NUM_PDADC(diff) < 128 (i.e.
diff > 0 && diff < 128), so it is safe in the negative-diff case (loop
condition k < 128 is immediately false) and unreachable in the huge-positive
case; the first loop is the sole sink.
No bounds check exists anywhere between the EEPROM read and the OOB write.
Threat model
Attacker position: anyone who can supply a forged Atheros AR9280 (Merlin) NIC EEPROM image.
Realistic vectors:
- A tampered USB
ath9kdongle plugged into a workstation or kiosk (hotplug, no special privileges beyond physical access). - PCI/PCIe passthrough of a malicious NIC to a VM guest (the guest kernel parses the device EEPROM at attach).
- An insider with brief physical access reflashing an existing NIC's EEPROM.
The trigger fires at device attach
(ar9280_attach.c:209 sets ah_setPowerCalTable = ar9280SetPowerCalTable) and
on every subsequent channel change, with no user interaction.
Precondition: EEPROM minor version byte >= 0x15 (21) AND pwr_table_offset
byte set to a value outside [-5, +58]. Both bytes are fully attacker-controlled
in a forged EEPROM.
Impact:
- For the small-overflow variant (byte
0xFA / -6, or0x80..0xF9), a controlled 2..246-byte overwrite of kernel BSS adjacent to the staticpdadcValuesbuffer β potentially corrupting adjacent HAL state, function pointers, or other module globals, enabling kernel code execution (ring 0 β root) depending on linker layout. - For the large-overflow variant (byte
0x3C..0x7F), a reliable kernel panic (DoS) via page fault on unmapped memory, preceded by up to 64KB of corruption that may be exploitable if a mapped region is hit first. - The OOB read direction (before the buffer) could leak adjacent kernel memory into PHY registers.
Default config: AR9280 Merlin 2.0+ chips with AR_EEP_OL_PWRCTRL (or any
Merlin 2.0+ since the diff computation in ar9280ChangeGainBoundarySettings
runs regardless of OLC flag β it is only gated on
AR_SREV_MERLIN_20_OR_LATER).
Proof of concept
EEPROM forgery (the trigger)
The AR5416 EEPROM BASE_EEP_HEADER (ah_eeprom_v14.h:189) contains a
uint8_t pwr_table_offset field at a fixed offset within the 64-byte base
header, and a 16-bit version field whose low byte (EEP_MINOR) must be
>= 0x15 (21) for the forged pwr_table_offset to be honored
(ah_eeprom_v14.c:138).
To trigger the bug, forge an EEPROM image (or reflash a real AR9280 NIC's EEPROM
via the athtool/atheprom utility or an external EEPROM programmer) with:
EEP_MINOR = 0x15(or higher)pwr_table_offset =one of:0xFA(int8_t -6): minimal 2-byte OOB write + 2-byte OOB read (stealthy, for exploitation)0x80(int8_t -128): 246-byte OOB write (medium)0x40(int8_t +64): ~64KB OOB write β immediate kernel panic (reliable DoS demo)
Then attach the device (plug USB dongle / boot VM with passthrough /
ifconfig ath0 up) and bring the interface up on any channel.
ar9280SetPowerCalTable runs during channel setup and the overflow fires.
Userspace proof-of-concept (demonstrates the exact overflow logic)
/* poc_df_pwrtable.c -- Build: cc -O2 -Wall -o poc_df_pwrtable poc_df_pwrtable.c
*
* Verbatim copy of the two vulnerable functions from ar9280_olc.c:173-237,
* with the ath_hal/OS_REG plumbing stripped (the OLC gate is assumed true).
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define AR5416_NUM_PDADC_VALUES 128
#define AR5416_PWR_TABLE_OFFSET_DB (-5)
static int16_t changeGainBoundarySettings(int8_t pwr_table_offset, int16_t *diff) {
if (AR5416_PWR_TABLE_OFFSET_DB != pwr_table_offset) {
*diff = (uint16_t)(pwr_table_offset - AR5416_PWR_TABLE_OFFSET_DB);
*diff *= 2;
}
return *diff;
}
static void adjustPDADCValues(int8_t pwr_table_offset, int16_t diff, uint8_t *pdadcValues) {
#define NUM_PDADC(diff) (AR5416_NUM_PDADC_VALUES - diff)
uint16_t k;
if (AR5416_PWR_TABLE_OFFSET_DB != pwr_table_offset) {
for (k = 0; k < (uint16_t)NUM_PDADC(diff); k++)
pdadcValues[k] = pdadcValues[k + diff]; /* OOB read+write */
for (k = (uint16_t)NUM_PDADC(diff); k < NUM_PDADC(0); k++)
pdadcValues[k] = pdadcValues[NUM_PDADC(diff)];
}
#undef NUM_PDADC
}
int main(void) {
/* Allocate 128 bytes 'valid' + 16 bytes canary, mirroring the kernel's
static uint8_t pdadcValues[128] sitting in BSS next to other globals. */
uint8_t mem[AR5416_NUM_PDADC_VALUES + 16];
memset(mem, 0x41, sizeof(mem));
uint8_t *pdadcValues = mem; /* [0..127] = valid, [128..143] = canary */
for (int i = 0; i < AR5416_NUM_PDADC_VALUES; i++) pdadcValues[i] = (uint8_t)i;
int8_t forged_offsets[] = { -6, -128, 64, 127 }; /* bytes 0xFA, 0x80, 0x40, 0x7F */
for (int t = 0; t < 4; t++) {
memset(mem+128, 0x41, 16); /* reset canary */
int16_t diff = 0;
changeGainBoundarySettings(forged_offsets[t], &diff);
printf("=== pwr_table_offset=%d (byte 0x%02X), diff=%d, NUM_PDADC=%d ===\n",
forged_offsets[t], (uint8_t)forged_offsets[t], diff, AR5416_NUM_PDADC_VALUES - diff);
/* The 0x40/0x7F cases write ~64KB and would segfault this userspace
harness (equivalent to the kernel page-fault panic). Guard them. */
if (diff > 128 || diff < -246) { printf(" (skipping: would write ~64KB -> panic)\n"); continue; }
adjustPDADCValues(forged_offsets[t], diff, pdadcValues);
int oob = 0;
for (int i = 128; i < 144; i++) if (mem[i] != 0x41) oob++;
printf(" canary overwrites past pdadcValues[127]: %d bytes (0x%02X 0x%02X 0x%02X ...)\n",
oob, mem[128], mem[129], mem[130]);
}
return 0;
}
Build: cc -O2 -Wall -o poc_df_pwrtable poc_df_pwrtable.c
Run: ./poc_df_pwrtable
Expected: for pwr_table_offset=-6, "canary overwrites past pdadcValues[127]: 2 bytes".
For -128, "246 bytes". This proves the overflow with the kernel's own logic.
Success criterion on a real kernel: kernel panic (fatal page fault in
ar9280AdjustPDADCValues) for the 0x40 case, or silent BSS corruption for
the 0xFA case (detectable with KASAN/KMSAN or by observing subsequent kernel
misbehavior).
Recommended fix
The root cause is that diff is allowed to be negative or exceed 128 without
any bounds check, and the loop bound macro NUM_PDADC(diff) silently wraps
when cast to uint16_t.
The minimal, defensive fix is to validate diff at the top of
ar9280AdjustPDADCValues before either loop runs. Defense-in-depth: also
clamp pwr_table_offset at the source in ar9280SetPowerCalTable. The diff
validation alone fully closes the OOB.
--- a/sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c
+++ b/sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c
@@ -211,6 +211,18 @@ static void
ar9280AdjustPDADCValues(struct ath_hal *ah, int8_t pwr_table_offset,
int16_t diff, uint8_t *pdadcValues)
{
#define NUM_PDADC(diff) (AR5416_NUM_PDADC_VALUES - diff)
uint16_t k;
+ /*
+ * diff is the number of half-dB steps to shift the PDADC table; it
+ * must be a small non-negative value strictly less than
+ * AR5416_NUM_PDADC_VALUES. A forged EEPROM pwr_table_offset can
+ * otherwise drive diff negative (making NUM_PDADC > 128, overflowing
+ * the write past pdadcValues[]) or greater than 128 (making NUM_PDADC
+ * negative, wrapping to ~65K after the uint16_t cast on the loop bound
+ * and corrupting up to 64 KB of kernel BSS). Reject both cases.
+ */
+ if (diff < 0 || diff >= AR5416_NUM_PDADC_VALUES)
+ return;
+
/* If this is a board that has a pwrTableOffset that differs from
* the default AR5416_PWR_TABLE_OFFSET_DB then the start of the
* pdadc vs pwr table needs to be adjusted prior to writing to the
Optional defense-in-depth at the source (in ar9280SetPowerCalTable, around
line 301):
(void) ath_hal_eepromGet(ah, AR_EEP_PWR_TABLE_OFFSET, &pwr_table_offset);
+ /* pwr_table_offset is in dBm; clamp to the realistic range to avoid
+ * OOB in ar9280AdjustPDADCValues when the EEPROM is forged. */
+ if (pwr_table_offset < AR5416_PWR_TABLE_OFFSET_DB ||
+ pwr_table_offset > AR5416_PWR_TABLE_OFFSET_DB + 64)
+ pwr_table_offset = AR5416_PWR_TABLE_OFFSET_DB;
Either change alone closes the bug; applying both is recommended.
References
sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:188-192β unguarded*diffcomputationsys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:226-228β first OOB loop (write side)sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:215βNUM_PDADC(diff)macrosys/dev/netif/ath/ath_hal/ah_eeprom_v14.c:137-142βpwr_table_offsetread from EEPROM with no validationsys/dev/netif/ath/ath_hal/ah_eeprom_v14.h:189βuint8_t pwr_table_offsetfieldsys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:301βath_hal_eepromGet(AR_EEP_PWR_TABLE_OFFSET, β¦)callsys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:376,399β calls into the vulnerable helperssys/dev/netif/ath/ath_hal/ar9002/ar9280_attach.c:209β vtable wiring (ah_setPowerCalTable)
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2002 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| poc_df_pwrtable.c | trigger-source | verbatim kernel arithmetic harness for ar9280ChangeGainBoundarySettings + ar9280AdjustPDADCValues | 3.8 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o poc_df_pwrtable poc_df_pwrtable.c | 304 B | view raw |
| run.sh | run-script | ./poc_df_pwrtable | 167 B | view raw |
| build.log | build-log | final successful build (BUILD_EXIT=0) | 77 B | view raw |
| run.log | run-log | decisive run: 2-byte and 246-byte OOB writes confirmed | 556 B | view raw |
| VERDICT.md | verdict | full source-trace narrative + mechanism + fix rationale | 5.2 KB | β raw |
| fix.diff | suggested-fix | bounds-check diff in ar9280AdjustPDADCValues (git-apply-able) | 1.0 KB | view raw |
| env.txt | environment | guest uname + ath module load state | 308 B | view raw |
| fix_build.log | build-log | Phase 8 combined kernel build rc=0 -Werror (3 fixes); patched .o + .ko confirmed | 883 B | view raw |
DF-2002 β VERDICT
Verdict: CONFIRMED (source-trace), HW-gated β inconclusive at runtime
The bug is real and confirmed by a complete line-by-line source trace of the
data flow from EEPROM read to the unbounded PDADC shift loop. It cannot be
exercised on this audit guest because the sink is gated behind Atheros AR9280
(Merlin) NIC hardware (the ath module is loaded but no AR9280 NIC is present,
and the vulnerable path runs only at NIC attach / channel change). Per the
standard HW-gated pattern, runtime reproduction is inconclusive /
reproduced=0 / impact=none, with the primitive characterized in userspace.
Mechanism (confirmed path:line)
-
EEPROM read, no validation β
sys/dev/netif/ath/ath_hal/ah_eeprom_v14.c:137-142: theAR_EEP_PWR_TABLE_OFFSETcase writes*(int8_t *)val = pBase->pwr_table_offset(a rawuint8_tEEPROM field,ah_eeprom_v14.h:189) with the only gate beingIS_VERS(>=, AR5416_EEP_MINOR_VER_21)β an attacker-controlled EEPROM minor version byte. -
Signedness-mismatched diff computation β
sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:188-192: whenpwr_table_offset != AR5416_PWR_TABLE_OFFSET_DB (-5),*diff = (uint16_t)(pwr_table_offset - (-5)); *diff *= 2;. The caller declaresuint16_t diff(line 345) but the formal isint16_t *diff(line 176) and the return isint16_t(line 173) β a signedness type-mismatch. -
Unbounded shift loop β
ar9280_olc.c:215,226-227:c #define NUM_PDADC(diff) (AR5416_NUM_PDADC_VALUES - diff) /* 128 - diff */ for (k = 0; k < (uint16_t)NUM_PDADC(diff); k++) pdadcValues[k] = pdadcValues[k + diff];pdadcValuesisstatic uint8_t pdadcValues[AR5416_NUM_PDADC_VALUES](128 bytes, BSS) declared atar9280_olc.c:288. -
Overflow cases (confirmed by the userspace harness, which is a verbatim copy of the two kernel functions): -
pwr_table_offset = -6(byte 0xFA):diff = -2,NUM_PDADC = 130, writespdadcValues[128..129]β 2-byte OOB write past the BSS buffer; readspdadcValues[-2..-1]β 2-byte OOB read. -pwr_table_offset = -128(byte 0x80):diff = -246,NUM_PDADC = 374, 246-byte OOB write. -pwr_table_offset = +64(byte 0x40):diff = 138,NUM_PDADC = -10,(uint16_t)(-10) = 65526, loop writes ~64KB β kernel page-fault / panic. -pwr_table_offset = +127(byte 0x7F):diff = 264β ~64KB overflow.
The (uint16_t) cast on the loop bound at line 226 is the silent wrap that
turns a "negative" iteration count into a ~65K-iteration catastrophic write.
Reachable at NIC attach (ar9280_attach.c:209 wires
ah_setPowerCalTable = ar9280SetPowerCalTable) and on every channel change, on
AR_SREV_MERLIN_20_OR_LATER hardware. No bounds check exists anywhere between
the EEPROM read (step 1) and the OOB write (step 3).
Userspace harness result
poc_df_pwrtable.c is a verbatim copy of ar9280ChangeGainBoundarySettings
and ar9280AdjustPDADCValues (lines 173-237), demonstrating the kernel's own
arithmetic. Build & run:
=== pwr_table_offset=-6 (byte 0xFA), diff=-2, NUM_PDADC=130 === canary overwrites past pdadcValues[127]: 2 bytes (...) === pwr_table_offset=-128 (byte 0x80), diff=-246, NUM_PDADC=374 === canary overwrites past pdadcValues[127]: 246 bytes (...) === pwr_table_offset=64 (byte 0x40), diff=138, NUM_PDADC=-10 === (skipping: would write ~64KB -> panic)
This proves the overflow with the kernel's exact logic. On a real AR9280 NIC with a forged EEPROM (Variant B, byte 0x40), the in-kernel sink page-faults.
Exploit chain
Not developed β the primitive is gated behind physical-access HW (a forged AR9280 EEPROM on a tampered USB dongle / PCI passthrough / reflashed NIC). The guest has no such NIC, so the in-kernel sink is unreachable here. This is a valid hard blocker (the vulnerable code path is not exercisable on this guest AND no in-guest harness can drive the real NIC EEPROM read path without the hardware). The primitive is characterized at the source/userspace-harness level: a forged EEPROM byte yields a controlled 2..246-byte BSS overwrite (exploitable with slab/BSS grooming to corrupt adjacent HAL state / function pointers) or a reliable ~64KB overflow β panic (DoS).
The threat model (tampered USB dongle hotplug / PCI passthrough to a VM guest / insider EEPROM reflash) is documented in the finding and is realistic.
PoC changes
- Added
poc_df_pwrtable.cβ the verbatim-kernel-logic userspace harness from the finding markdown, made standalone-buildable. - Added
build.sh/run.shβ exact, runnable. - Authored
fix.diffβ minimal bounds check ondiffat the top ofar9280AdjustPDADCValues(diff < 0 || diff >= AR5416_NUM_PDADC_VALUES). This matches the finding markdown's primary recommended fix.
Fix
fix.diff adds a 1-line guard at ar9280_olc.c:216 (after the macro, before
the loops):
if (diff < 0 || diff >= AR5416_NUM_PDADC_VALUES)
return;
This closes both the negative-diff (forward OOB write) and the >128-diff
(uint16_t-wrap ~64KB write) cases. git apply --check passes. Matches the
finding markdown's primary proposal. Validated by a clean kernel build in
Phase 8 (combined with DF-2000/DF-2003 fixes).
Fix verification
not_testableVALIDATED build. Patch applies, ath_hal.ko rebuilds rc=0.
NK_DONE rc=0; ar9280_olc.o recompiled with guard at :227.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- _
- h
- a
- l
- /
- a
- h
- _
- e
- e
- p
- r
- o
- m
- _
- v
- 1
- 4
- .
- c
- :
- 1
- 3
- 7
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- _
- h
- a
- l
- /
- a
- r
- 9
- 0
- 0
- 2
- /
- a
- r
- 9
- 2
- 8
- 0
- _
- o
- l
- c
- .
- c
- :
- 1
- 9
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- _
- h
- a
- l
- /
- a
- r
- 9
- 0
- 0
- 2
- /
- a
- r
- 9
- 2
- 8
- 0
- _
- o
- l
- c
- .
- c
- :
- 2
- 2
- 6
Detail
Exploit chain
none (HW-gated). Primitive: forged EEPROM byte yields 2..246B BSS overwrite or ~64KB overflow panic. Requires tampered ath9k dongle/PCI passthrough/insider reflash.
Evidence (decisive lines)
Harness: byte 0xFA -> 2B OOB; byte 0x80 -> 246B OOB; 0x40/0x7F -> ~64KB wrap panic.
Verified recommended fix
Add 'if (diff < 0 || diff >= AR5416_NUM_PDADC_VALUES) return;' at ar9280_olc.c:216.
Verdict
HW-GATED (no AR9280 NIC). Bug CONFIRMED source-trace + harness. ah_eeprom_v14.c:137 reads pwr_table_offset int8 with no validation; ar9280_olc.c:190 signedness mismatch; :226 loop bound (uint16_t)(128-diff) wraps negative to ~65K. Harness proves 2B and 246B OOB writes for forged bytes 0xFA/0x80.
No comments yet.