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

SIOCGATHDIAG/SIOCGATHPHYERR dispatched with no privilege check: unprivileged users get arbitrary device MMIO read/write (incl. plaintext WEP/TKIP keycache dump), EEPROM reads, and NULL-deref panics on ATH_DIAGAPI kernels

Summary

ath_ioctl gates SIOCZATHSTATS with caps_priv_check but dispatches SIOCGATHDIAG/SIOCGATHPHYERR straight through - no privilege check anywhere on the path (ifioctl default -> ieee80211 pass-through -> ath_ioctl). FreeBSD upstream gates the identical cases with PRIV_DRIVER; the DF port kept only the SIOCZATHSTATS check. Behind the missing gate: HAL_DIAG_SETREGS (arbitrary device MMIO writes), HAL_DIAG_REGS (register dump including the keytable at 0x9800+ = 802.11 key material disclosure), EEPROM reads, and NULL-deref panics. Gated by options ATH_DIAGAPI (not in stock GENERIC - verified by symbol absence on the guest). caps_priv_check_self-mirror fix (filed against ar5212_misc.c where the deferred diag surface lives).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4320 Β· 10 files
FileTypeDescriptionSize
poc.c β€” 4.4 KB view raw
build.sh β€” 80 B view raw
run.sh β€” 86 B view raw
build.log β€” 38 B view raw
run.log β€” 1.5 KB view raw
static_check.log β€” 1.8 KB view raw
env.txt β€” 1003 B view raw
VERDICT.md β€” 5.4 KB ↓ raw
README.md β€” 2.5 KB ↓ raw
fix.diff β€” 690 B view raw

DF-4320 β€” Missing privilege check on SIOCGATHDIAG exposes the full HAL diagnostic interface (arbitrary device MMIO read/write, 802.11 keycache disclosure, NULL-deref panics)

Build

cc -O2 -Wall -o poc poc.c

(Any DragonFly host; plain POSIX userland, no privileges needed.)

Run

./poc            # probes every interface with SIOCGATHDIAG as the invoking user

Expected

  • Vulnerable system (kernel built with options ATH_DIAGAPI, an ath(4) wlan interface present, run as a plain user): prints SETREGS : SUCCESS (an arbitrary device register write was accepted from an unprivileged uid) and/or REGS-dump: SUCCESS (device register dump, including the WEP/TKIP keycache registers at 0x9800+, handed to an unprivileged uid). No EPERM is ever produced β€” the DragonFly source has no privilege gate anywhere on the dispatch path (socket β†’ ifioctl default β†’ ieee80211_ioctl default β†’ ath_ioctl case SIOCGATHDIAG β†’ ath_ioctl_diag).
  • Stock X86_64_GENERIC (no ATH_DIAGAPI): every probe returns ENOTTY/EINVAL β€” the ioctl case is not compiled into the kernel (verified: nm /boot/kernel/if_ath.ko | grep ath_ioctl_diag β†’ empty). On the audit QEMU guest additionally no Atheros NIC exists (virtio-only PCI bus), so the wlan dispatch is never reached.

Static proof (decisive on any host)

  • sys/dev/netif/ath/ath/if_ath_ioctl.c:288 β€” the neighboring ioctl SIOCZATHSTATS does check caps_priv_check_self(SYSCAP_NODRIVER).
  • sys/dev/netif/ath/ath/if_ath_ioctl.c:299-302 β€” SIOCGATHDIAG / SIOCGATHPHYERR call ath_ioctl_diag/ath_ioctl_phyerr directly with no check. FreeBSD upstream gates the identical cases with priv_check(curthread, PRIV_DRIVER).
  • sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516-3520 β€” unknown ioctls are passed to the driver ("Pass unknown ioctls first to the driver") with no privilege check.
  • sys/dev/netif/ath/ath_hal/ah.c:878-935 β€” what the gate would be protecting: HAL_DIAG_SETREGS (arbitrary addr,value MMIO writes, unconditional), HAL_DIAG_REGS (arbitrary register-range dump β€” includes keycache), HAL_DIAG_EEREAD, plus (under AH_PRIVATE_DIAG) SETKEY/RESETKEY/EEWRITE.

Files

  • poc.c β€” the probe (struct layouts mirrored 1:1 from if_athioctl.h).
  • run.log β€” guest run (default kernel: ENOTTY/EINVAL as predicted).
  • static_check.log β€” guest kernel/module symbol + PCI evidence.
  • env.txt β€” guest environment.
  • fix.diff β€” the one-hunk fix.
VERDICT.md
↓ download raw

DF-4320 β€” VERDICT

Status: inconclusive at kernel level (feature absent on the audit guest); the missing privilege gate itself is certain from source, fully traced.

What the finding claims

SIOCGATHDIAG (the ath(4) HAL diagnostic ioctl) is dispatched to ath_ioctl_diag() without any privilege check on DragonFly:

sys/dev/netif/ath/ath/if_ath_ioctl.c:285-297   SIOCZATHSTATS *does* check
                                              caps_priv_check_self(SYSCAP_NODRIVER)   :288
sys/dev/netif/ath/ath/if_ath_ioctl.c:299-300   case SIOCGATHDIAG:
                                                return (ath_ioctl_diag(sc, data));  <-- NO check
sys/dev/netif/ath/ath/if_ath_ioctl.c:301-302   case SIOCGATHPHYERR: same             <-- NO check

The full unprivileged dispatch chain (traced, no gate anywhere):

  1. ioctl(2) on a datagram socket β†’ sys/net/if.c:2409-2415 default β†’ so_pru_control_direct (unknown driver ioctl passed through);
  2. sys/netproto/802_11/wlan/ieee80211_ioctl.c:3516-3520 default β€” "Pass unknown ioctls first to the driver" β†’ ic->ic_ioctl = ath_ioctl;
  3. ath_ioctl case SIOCGATHDIAG β†’ ath_ioctl_diag (if_ath_ioctl.c:170-235) β€” copyin of attacker ad_in_size/ad_out_size buffers, then ath_hal_getdiagstate(ah, id, indata, insize, &outdata, &outsize);
  4. sys/dev/netif/ath/ath_hal/ah.c:878-935 β€” unconditional handlers: * HAL_DIAG_SETREGS (33): ath_hal_setregs() β€” arbitrary device MMIO writes with attacker-supplied addr,value pairs, no validation at all (ah.c:888-891); * HAL_DIAG_REGS (13): ath_hal_getregdump() β€” arbitrary register range dump returned to userland; the keycache registers (AR_KEYTABLE_KEYn, 0x9800+, ar5212reg.h) hold installed WEP/TKIP key material in plaintext β†’ 802.11 key disclosure to any local user; * HAL_DIAG_EEREAD (17): arbitrary EEPROM word read. Plus NULL-deref panics with the ATH_DIAG_IN/ATH_DIAG_DYN flag bits unset: getregdump(ah, NULL, NULL, attacker_outsize) / ath_hal_setregs(ah, NULL, argsize) dereference NULL (ah.c:848-856/865-870 with indata == NULL) β€” trivial unprivileged panic on a vulnerable config. The ar5212-specific handlers behind ar5212GetDiagState (ar5212_misc.c:1049-1126) add HAL_DIAG_ANI_CMD β†’ ar5212AniControl with unchecked cmd, which NULL-derefs params->maxNoiseImmunityLevel when no ANI state exists (ar5212_ani.c:230-243).

FreeBSD upstream gates the identical cases with priv_check(curthread, PRIV_DRIVER); DragonFly's port kept the check for SIOCZATHSTATS only (the caps_priv_check_self at :288) and dropped it for the two DIAGAPI ioctls.

Why it is not kernel-reproducible on THIS guest (and what was verified instead)

  • The guest kernel is X86_64_GENERIC (DragonFly 6.5-DEVELOPMENT #0 ... /usr/obj/usr/src/sys/X86_64_GENERIC), which does not include options ATH_DIAGAPI (sys/config/X86_64_GENERIC β€” no ATH_DIAGAPI; only LINT64 lists it at :1121). The whole #ifdef ATH_DIAGAPI block β€” including ath_ioctl_diag β€” is not compiled: nm /boot/kernel/if_ath.ko | grep ath_ioctl_diag β†’ no symbol (see static_check.log).
  • The guest has no Atheros NIC (virtio-only PCI bus, pciconf -l in env.txt), so even a recompiled kernel could not be driven into the HAL path without hardware.

What WAS verified on the guest:

  • poc (unprivileged, uid=1001) issued real SIOCGATHDIAG ioctls with a HAL_DIAG_SETREGS payload and a HAL_DIAG_REGS keycache-dump payload on every interface: non-wlan interfaces answer EINVAL (ether layer), i.e. no privilege gate was hit β€” and none exists in the source path (run.log).
  • Static evidence that the HAL side handlers are compiled unconditionally into ath_hal.ko (disassembly of ath_hal_getdiagstate at +0x67b0 shows comparisons against diag codes 13 (REGS), 17 (EEREAD), 29 (FATALERR), 30 (11NCOMPAT), 33 (SETREGS)) β€” the only missing piece on a stock system is the ioctl case in if_ath.ko, which any options ATH_DIAGAPI kernel build turns on.

Exploit chain (on a kernel built with options ATH_DIAGAPI + ath NIC)

unprivileged local user
  β†’ socket(AF_INET,SOCK_DGRAM) + ioctl(SIOCGATHDIAG, {ad_id=HAL_DIAG_REGS|IN|DYN,
      ad_in_data=[{0x9800,0x983f},...], ad_out_size=4096})
  β†’ copyout of keycache registers β†’ recover WEP/TKIP group/pairwise keys
  β†’ (or) ad_id=HAL_DIAG_SETREGS, write arbitrary device registers
  β†’ (or) ad_id=HAL_DIAG_ANI_CMD, NULL-deref panic
  β†’ (or) HAL_DIAG_REGS with crafted sizes β†’ DF-4321 heap overflow β†’ code exec

Bottom line

Certain source-level vulnerability (CWE-862), unconditionally missing gate whose sibling ioctl demonstrates the intended pattern in the same switch. Exploitation requires a kernel built with options ATH_DIAGAPI (not in default GENERIC) and an ath(4) device β€” therefore Medium severity per the project rubric ("requires unusual config"), with High impact on such configurations. Not reproducible on the audit guest (feature absent); fix.diff adds the gate mirroring :288.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff adds caps_priv_check_self(SYSCAP_NODRIVER) to the SIOCGATHDIAG/SIOCGATHPHYERR cases, mirroring SIOCZATHSTATS at :288; validated as git-apply-clean against sys/; runtime validation impossible on this guest (no ATH_DIAGAPI kernel, no ath NIC).

findings/poc/DF-4320/fix.diff; git apply --check passes
↓ fix.diffper-fix-DF-4320

Confirmed kernel references

Detail

Exploit chain

unprivileged socket(AF_INET,SOCK_DGRAM) -> ioctl(SIOCGATHDIAG,{ad_id=HAL_DIAG_REGS|IN|DYN, ranges=[{0x9800,0x983f},...], ad_out_size=4096}) -> copyout of keycache registers -> WEP/TKIP key recovery; or ad_id=HAL_DIAG_SETREGS -> arbitrary device register write; or unset IN/DYN -> NULL deref panic (requires options ATH_DIAGAPI + ath NIC; not present on audit guest)

Evidence (decisive lines)

VERDICT.md (full path trace); static_check.log (nm: no ath_ioctl_diag in guest if_ath.ko; ath_hal_getdiagstate disasm showing unconditional REGS=13/SETREGS=33 handlers in ath_hal.ko); run.log (unprivileged probe, no EPERM possible); fix.diff (priv gate, git-apply clean)

PoC changes

struct ath_diag mirrored 1:1 from if_athioctl.h:178-192 (ad_id u16, ad_in_size u16 before pointers); probe issues HAL_DIAG_SETREGS and HAL_DIAG_REGS payloads on every interface

Verified recommended fix

Gate SIOCGATHDIAG/SIOCGATHPHYERR with caps_priv_check_self(SYSCAP_NODRIVER) as FreeBSD does with PRIV_DRIVER

Verdict

The missing privilege gate is certain from fully-traced source: SIOCGATHDIAG/SIOCGATHPHYERR dispatch to ath_ioctl_diag()/ath_ioctl_phyerr() (if_ath_ioctl.c:299-302) with no caps/priv check anywhere on the path (ifioctl default net/if.c:2409 -> ieee80211_ioctl default wlan/ieee80211_ioctl.c:3516 -> ath_ioctl), while the neighboring SIOCZATHSTATS case checks caps_priv_check_self(SYSCAP_NODRIVER) at :288 and FreeBSD upstream gates the same DIAGAPI ioctls with PRIV_DRIVER. On an ATH_DIAGAPI kernel with an ath NIC this hands any local user HAL_DIAG_SETREGS (arbitrary device MMIO write), HAL_DIAG_REGS (arbitrary register dump incl. plaintext WEP/TKIP keycache at 0x9800+), HAL_DIAG_EEREAD, plus NULL-deref panics via unset ATH_DIAG_IN/DYN flags. Kernel-level reproduction was impossible on the audit guest: stock X86_64_GENERIC has no options ATH_DIAGAPI (nm shows no ath_ioctl_diag in if_ath.ko) and the guest has no Atheros NIC (virtio-only PCI). The unprivileged probe ran on the guest (run.log): non-wlan interfaces answer EINVAL; no privilege gate exists in the source path. Impact recorded as 'none' strictly for this guest; on ATH_DIAGAPI configs the ceiling is 802.11 key disclosure + device takeover (and DF-4321 heap overflow).