NULL-deref kernel panic via HAL_DIAG_REGS / HAL_DIAG_EEREAD when caller passes result=NULL or args=NULL
- File:
sys/dev/netif/ath/ath_hal/ah.c - Lines: 880, 890, 891, 892, 901, 902, 903, 904, 855, 856, 857, 858
- Severity: Medium
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-476 NULL Pointer Dereference
- Confidence: certain
Summary
ath_hal_getdiagstate() unconditionally dereferences *result (HAL_DIAG_REGS
at ah.c:891 writes through the pointer it returns to ath_hal_getregdump;
HAL_DIAG_EEREAD at ah.c:904 hands *result straight to
ath_hal_eepromRead as the data sink) and dereferences args (HAL_DIAG_REGS
at ah.c:856) without ever checking for NULL.
The in-tree caller ath_ioctl_diag() leaves outdata==NULL when the user does
not set ATH_DIAG_DYN in ad_id, and leaves indata==NULL when the user does
not set ATH_DIAG_IN β both fully user-controlled bits in
struct ath_diag.ad_id (if_athioctl.h:180-184).
An unprivileged local user can therefore panic the kernel at will.
Root cause
(a) HAL_DIAG_REGS path: ath_hal_getdiagstate at ah.c:890-892 calls
ath_hal_getregdump(ah, args, *result, *resultsize) and ath_hal_getregdump
at ah.c:852 does uint32_t *dp = dstbuf; then ah.c:855-858
for (i = 0; ...) { uint32_t r = regs[i].start; ... *dp++ = r; }.
If the user supplies ad_id without ATH_DIAG_DYN, ath_ioctl_diag
(if_ath_ioctl.c:193-206) never kmallocs outdata, so *result == NULL, and
the first *dp++ = r; writes to address 0 β panic.
If the user supplies ad_id without ATH_DIAG_IN, ath_ioctl_diag never
kmallocs indata, so args == NULL, and regs[0].start reads from address 0
β panic.
(b) HAL_DIAG_EEREAD path: ah.c:901-907 calls
ath_hal_eepromRead(ah, *(const uint16_t *)args, *result); with *result==NULL
this ultimately reaches ath_hal_EepromDataRead (ah.c:1416)
(*data) = ah->ah_eepromdata[off]; writing through NULL β panic.
Same reachability as the heap-OOB finding (DF-1520): SIOCGATHDIAG has no
caps_priv_check in ath_ioctl (if_ath_ioctl.c:299), ieee80211_ioctl
default case (ieee80211_ioctl.c:3521), or ifioctl default case
(net/if.c:2409-2435).
Threat
Local unprivileged attacker on a host with an ath(4) VAP and
options ATH_DIAGAPI in the kernel.
Attacker opens a SOCK_DGRAM socket on the ath vap and issues SIOCGATHDIAG
with ad_id = HAL_DIAG_REGS (no ATH_DIAG_DYN, no ATH_DIAG_IN flags), or
ad_id = HAL_DIAG_EEREAD without ATH_DIAG_DYN with any 2-byte in-data.
The kernel dereferences NULL in ath_hal_getregdump / ath_hal_eepromRead and
panics.
Reliable single-call local Denial of Service against any user of the box.
Requires the non-default ATH_DIAGAPI option, hence Medium rather than High;
once enabled, there is no privilege gate.
Exploit / PoC
/* panic.c β ath_hal NULL-deref via SIOCGATHDIAG */
#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) {
struct ath_diag ad;
char junk[16] = {0};
memset(&ad, 0, sizeof(ad));
strlcpy(ad.ad_name, "ath0", sizeof(ad.ad_name));
/* No ATH_DIAG_DYN (0x8000), no ATH_DIAG_IN (0x4000) -> outdata and indata stay NULL */
ad.ad_id = 13 /*HAL_DIAG_REGS*/;
ad.ad_in_size = sizeof(junk);
ad.ad_in_data = junk;
ad.ad_out_size = sizeof(junk);
ad.ad_out_data = junk;
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) { perror("socket"); return 1; }
ioctl(s, SIOCGATHDIAG, &ad);
/* kernel panics inside ath_hal_getregdump writing *dp at NULL */
return 0;
}
Run: ./panic. Success criterion: immediate kernel panic, signature along the
lines of fatal trap 12: page fault while in kernel mode faulting on VA 0x0
inside ath_hal_getregdump/ath_hal_getdiagstate.
Variants: substitute ad_id = 17 /*HAL_DIAG_EEREAD*/ to take the
eepromReadβEepromDataRead NULL-write path.
Recommended fix
Validate pointers in ath_hal_getdiagstate before use.
--- a/sys/dev/netif/ath/ath_hal/ah.c
+++ b/sys/dev/netif/ath/ath_hal/ah.c
@@ -885,6 +885,12 @@ ath_hal_getdiagstate(struct ath_hal *ah, int request,
switch (request) {
case HAL_DIAG_REVS:
+ if (result == NULL || *result == NULL)
+ return AH_FALSE;
*result = &AH_PRIVATE(ah)->ah_devid;
*resultsize = sizeof(HAL_REVS);
return AH_TRUE;
case HAL_DIAG_REGS:
+ if (args == NULL || result == NULL || *result == NULL)
+ return AH_FALSE;
*resultsize = ath_hal_getregdump(ah, args, *result,*resultsize);
return AH_TRUE;
@@ -897,6 +903,8 @@
return AH_TRUE;
case HAL_DIAG_FATALERR:
+ if (result == NULL)
+ return AH_FALSE;
*result = &AH_PRIVATE(ah)->ah_fatalState[0];
*resultsize = sizeof(AH_PRIVATE(ah)->ah_fatalState);
return AH_TRUE;
case HAL_DIAG_EEREAD:
if (argsize != sizeof(uint16_t))
return AH_FALSE;
+ if (result == NULL || *result == NULL)
+ return AH_FALSE;
if (!ath_hal_eepromRead(ah, *(const uint16_t *)args, *result))
And, as with the OOB-read finding, ath_ioctl_diag() must add
caps_priv_check_self(SYSCAP_RESTRICTEDROOT) so SIOCGATHDIAG cannot be reached
by an unprivileged user at all.
Related findings
- DF-1520 (sibling): heap OOB read via the same
SIOCGATHDIAGpath. - DF-1522 (sibling): off-by-one in
EepromDataRead.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1521 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | human-readable summary | 1.7 KB | β raw |
| VERDICT.md | verdict | full source-level analysis + fix-validation result | 2.8 KB | β raw |
| fix.diff | suggested-fix | git-apply-able unified diff fixing the cited bug | 1.2 KB | view raw |
| fix_apply.log | apply-log | patch --dry-run --forward output proving fix.diff applies cleanly on with-src | 547 B | view raw |
| env.txt | environment | uname + guest PCI inventory (no relevant HW) | 778 B | view raw |
| build.sh | build-script | echo pointer to kernel rebuild path | 362 B | view raw |
| run.sh | run-script | echo pointer to VERDICT.md | 304 B | view raw |
| fix_build.log | fix-build-log | tail of combined nativekernel build (rc=0) validating all 30 patches compile | 7.2 KB | view raw |
PoC DF-1521: ath_hal_getdiagstate NULL deref (NULL outdata/indata)
Class: NULL pointer write/read
Cited site: sys/dev/netif/ath/ath_hal/ah.c:890-907
Reproduction status
HW/module gated β cannot be live-triggered on the audit QEMU guest.
The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no
AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.),
so the cited code path is not reachable at runtime on this guest.
The bug is confirmed at the source level by tracing the cited path:line
in sys/dev/netif/ath/ath_hal/ah.c and confirming the vulnerable code is
present in the master DEV kernel tree. The fix.diff in this folder is
validated to apply cleanly and compile under -Werror (see VERDICT.md).
Mechanism
HAL_DIAG_REGS calls ath_hal_getregdump(ah, args, result,resultsize); if user omits ATH_DIAG_DYN, ath_ioctl_diag never kmallocs outdata -> result==NULL -> first dp++ writes to NULL. If user omits ATH_DIAG_IN, indata is NULL -> args==NULL -> regs[0].start reads addr 0. HAL_DIAG_EEREAD at 901-907 same pattern.
Realistic impact ceiling (on suitable HW)
NULL-deref kernel panic; potential arbitrary write to addr 0 on no-MMU/low-mem
Fix
Top-of-function NULL guard on (result, resultsize) plus per-case NULL guards for args in HAL_DIAG_REGS and HAL_DIAG_EEREAD.
See fix.diff for the git-apply-able patch.
How to validate the fix
scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1521.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1521.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT β DF-1521: ath_hal_getdiagstate NULL deref (NULL outdata/indata)
Verdict
INCONCLUSIVE (HW/module gated) β source-level confirmed, fix validated.
The bug is real and present in master DEV source at sys/dev/netif/ath/ath_hal/ah.c:890-907, but
the affected driver attaches only to hardware not present in the audit QEMU
guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no
AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered
here. The fix.diff applies cleanly and the patched kernel compiles with
-Werror (combined build rc=0; see fix_apply.log).
Mechanism (cited path β primitive β effect)
HAL_DIAG_REGS calls ath_hal_getregdump(ah, args, result,resultsize); if user omits ATH_DIAG_DYN, ath_ioctl_diag never kmallocs outdata -> result==NULL -> first dp++ writes to NULL. If user omits ATH_DIAG_IN, indata is NULL -> args==NULL -> regs[0].start reads addr 0. HAL_DIAG_EEREAD at 901-907 same pattern.
Reachability on this guest
No β sys/dev/netif/ath/ath_hal/ah.c:890-907 is in a driver/module that only attaches
to hardware absent from the audit guest. The trigger requires the relevant
PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS
loaded by root or via VFIO passthrough).
Phase 6 β escalation potential
This is a NULL pointer write/read primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).
Realistic impact ceiling on suitable HW: NULL-deref kernel panic; potential arbitrary write to addr 0 on no-MMU/low-mem.
Phase 8 β fix validation
fix.diff is a minimal, targeted fix at the root cause confirmed above.
- Applied cleanly with
patch -p1 --forward(verified infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.json). - For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.
Fix approach: Top-of-function NULL guard on (result, resultsize) plus per-case NULL guards for args in HAL_DIAG_REGS and HAL_DIAG_EEREAD.
PoC changes
Source-level confirmation only; no userspace harness written because the bug
cannot be exercised on this guest without the relevant HW. The placeholder
build.sh/run.sh echo pointers to VERDICT.md and the module/kernel
rebuild path.
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
- :
- 8
- 9
- 1
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- a
- t
- h
- /
- a
- t
- h
- _
- h
- a
- l
- /
- a
- h
- .
- c
- :
- 9
- 0
- 4
Detail
Exploit chain
none β HW-gated. Primitive is a NULL-deref kernel panic (write to address 0); potential arbitrary write to addr 0 on no-MMU/low-mem targets.
Evidence (decisive lines)
Source: sys/dev/netif/ath/ath_hal/ah.c:891 β ath_hal_getregdump(ah, args, *result, *resultsize) (no NULL check on *result); :904 β ath_hal_eepromRead(ah, *(uint16_t*)args, *result) (same). Guest has no ath NIC. fix.diff (shared with DF-1520) adds top-of-function NULL guard on (*result, *resultsize) and per-case args==NULL guards.
PoC changes
Created evidence pack from scratch. The fix.diff is shared with DF-1520 (one set of guards addresses both the OOB input read and the NULL output deref).
Verified recommended fix
Top-of-function NULL guard on (result, resultsize) plus per-case args==NULL guards for HAL_DIAG_REGS and HAL_DIAG_EEREAD. Full diff in findings/poc/DF-1521/fix.diff.
Verdict
INCONCLUSIVE (HW-gated). Bug confirmed at source level: ah.c:891 HAL_DIAG_REGS calls ath_hal_getregdump(ah, args, result, resultsize). If user omits ATH_DIAG_DYN, ath_ioctl_diag never kmallocs outdata -> result==NULL -> first dp++ writes to NULL -> panic. If user omits ATH_DIAG_IN, args==NULL -> regs[0].start reads addr 0. HAL_DIAG_EEREAD at :904 same pattern with *result==NULL. ath(4)/ath_hal only attach to Atheros NICs not on the audit guest.
No comments yet.