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

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 via ar9280SetPowerCalTable: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:

  1. ar9280SetPowerCalTable reads pwr_table_offset from EEPROM at ar9280_olc.c:301 via ath_hal_eepromGet(AR_EEP_PWR_TABLE_OFFSET). The EEPROM getter at ah_eeprom_v14.c:137-142 returns the raw struct byte (pBase->pwr_table_offset, declared uint8_t at ah_eeprom_v14.h:189) reinterpret-cast to int8_t with zero validation β€” the only gate is EEPROM minor version >= 0x15 (21), itself an attacker-controlled byte.

  2. ar9280SetPowerCalTable:376-378 calls ar9280ChangeGainBoundarySettings(ah, ..., pwr_table_offset, &diff) where the caller's diff is uint16_t (line 345) but the formal is int16_t *diff (line 176) β€” a signedness type-mismatch.

  3. Inside ar9280ChangeGainBoundarySettings, when pwr_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.

  1. The return value (int16_t, line 208) is assigned back to the caller's uint16_t diff (line 376), then passed to ar9280AdjustPDADCValues(ah, pwr_table_offset, diff, pdadcValues) at line 399.

  2. Inside ar9280AdjustPDADCValues (line 212), the macro NUM_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 writes pdadcValues[128..129] = 2-byte OOB WRITE past the 128-byte BSS buffer; reads pdadcValues[-2..-1] = 2-byte OOB READ before the buffer.
  • (b) diff=-246 (byte 0x80): NUM_PDADC=374, writes pdadcValues[128..373] = 246-byte OOB WRITE; reads pdadcValues[-246..-1].
  • (c) diff=138 (byte 0x40): NUM_PDADC = 128-138 = -10; (uint16_t)(-10) = 65526; loop writes pdadcValues[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:

  1. A tampered USB ath9k dongle plugged into a workstation or kiosk (hotplug, no special privileges beyond physical access).
  2. PCI/PCIe passthrough of a malicious NIC to a VM guest (the guest kernel parses the device EEPROM at attach).
  3. 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, or 0x80..0xF9), a controlled 2..246-byte overwrite of kernel BSS adjacent to the static pdadcValues buffer β€” 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).

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2002 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict full source-trace narrative + mechanism + fix rationale
↓ download 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)

  1. EEPROM read, no validation β€” sys/dev/netif/ath/ath_hal/ah_eeprom_v14.c:137-142: the AR_EEP_PWR_TABLE_OFFSET case writes *(int8_t *)val = pBase->pwr_table_offset (a raw uint8_t EEPROM field, ah_eeprom_v14.h:189) with the only gate being IS_VERS(>=, AR5416_EEP_MINOR_VER_21) β€” an attacker-controlled EEPROM minor version byte.

  2. Signedness-mismatched diff computation β€” sys/dev/netif/ath/ath_hal/ar9002/ar9280_olc.c:188-192: when pwr_table_offset != AR5416_PWR_TABLE_OFFSET_DB (-5), *diff = (uint16_t)(pwr_table_offset - (-5)); *diff *= 2;. The caller declares uint16_t diff (line 345) but the formal is int16_t *diff (line 176) and the return is int16_t (line 173) β€” a signedness type-mismatch.

  3. 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]; pdadcValues is static uint8_t pdadcValues[AR5416_NUM_PDADC_VALUES] (128 bytes, BSS) declared at ar9280_olc.c:288.

  4. 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, writes pdadcValues[128..129] β†’ 2-byte OOB write past the BSS buffer; reads pdadcValues[-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 on diff at the top of ar9280AdjustPDADCValues (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_testable
baseline no→ patch + rebuild →patched clean

VALIDATED build. Patch applies, ath_hal.ko rebuilds rc=0.

NK_DONE rc=0; ar9280_olc.o recompiled with guard at :227.
↓ fix.diffcombined build rc=0 -Werror

Confirmed kernel references

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.