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

ath_btcoex_ioctl copies uninitialized kernel heap to userspace (latent; function currently has no caller)

  • File: sys/dev/netif/ath/ath/if_ath_btcoex.c
  • Lines: 397 (kmalloc), 403–406 (default without write), 409–410 (unconditional copyout)
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U:C:L/I:N/A:N
  • CWE: CWE-908 Use of Uninitialized Resource, CWE-200 Exposure of Sensitive Information
  • Confidence: likely
  • Status: new

Summary

ath_btcoex_ioctl (if_ath_btcoex.c:366) allocates an output buffer via kmalloc (line 397) when ATH_DIAG_DYN is set, but the request switch has only a default: error = EINVAL arm (lines 403-406) that never populates the buffer.

Execution then falls through with no if (error) goto bad guard to an unconditional copyout (line 409) that writes the entire uninitialized kmalloc'd buffer to the caller's userspace pointer.

This discloses uninitialized kernel heap memory.

The correct sibling ath_ioctl_diag (if_ath_ioctl.c:214-222) guards its copyout behind the HAL success path; this copy-pasted stub does not.

Root cause

if_ath_btcoex.c:389-401:

when (ad->ad_id & ATH_DIAG_DYN), outdata = kmalloc(outsize, M_TEMP, M_INTWAIT);

The allocation is never written β€” the switch at lines 403-406 has only default: error = EINVAL; and no case handler touches outdata.

Lines 407-410 then execute unconditionally:

if (outsize < ad->ad_out_size)
    ad->ad_out_size = outsize;       /* no-op: outsize was snapshotted at line 372 */

if (outdata && copyout(outdata, ad->ad_out_data, ad->ad_out_size))
    error = EFAULT;

Because there is no if (error) goto bad; between the switch and the copyout, the EINVAL is set but the copyout still fires, copying outsize bytes of uninitialized heap.

The function returns EINVAL but the leak has already occurred.

Contrast with the correctly-implemented ath_ioctl_diag at if_ath_ioctl.c:214-222 where copyout is performed only inside if (ath_hal_getdiagstate(...)) and the else-branch sets EINVAL without copying out.

Threat model

Reachability: ath_btcoex_ioctl is declared extern in if_ath_btcoex.h:36 but has ZERO callers anywhere in sys/ (confirmed by rg over the entire tree β€” only the header declaration and the definition itself match).

No SIOCGATHBTCOEX ioctl number is defined (if_athioctl.h defines only SIOCGATHDIAG/SIOCGATHPHYERR/SIOCGATHSPECTRAL, none of which dispatch here), so the function is dead code today and the leak is not triggerable via any in-tree path.

Impact if wired: The function was clearly written to be connected to a future diagnostic ioctl (its own comment at line 358-364 says "Handle ioctl requests from the diagnostic interface" and it mirrors ath_ioctl_diag).

The moment a case SIOCGATHBTCOEX: return ath_btcoex_ioctl(sc, ad); is added to ath_ioctl (if_ath_ioctl.c:299), an attacker controlling the ioctl can set ATH_DIAG_DYN + ad_out_size > 0 to receive up to outsize bytes of uninitialized kernel heap, leaking prior kernel allocations (potential credentials, pointers, keys).

The SIOCGATHDIAG-class path through the network stack is typically root-gated by ifioctl/priv_check, so practical impact is root-to-root info disclosure (low), but the allocation also allows up to a ~4 GB kmalloc(outsize, M_INTWAIT) request (DoS via memory pressure) since ad_out_size is u_int (if_athioctl.h:188).

No special hardware/BT module is required to reach the copyout once the ioctl is wired.

Proof of concept

Today: NOT reproducible β€” ath_btcoex_ioctl has no caller (rg confirms zero call sites; no SIOCGATHBTCOEX defined). This is a latent defect flagged for defense-in-depth.

PoC sketch demonstrating the bug once wired (requires a one-line dispatcher change not applied to sys/):

/* A program that opens a raw socket on ath0, issues the diagnostic ioctl
 * with ATH_DIAG_DYN set and ad_out_size=4096, ad_id=0x8000|0xdead
 * (unknown ID -> default -> EINVAL), then reads back the 4096-byte buffer.
 * Hexdump shows non-zero leaked kernel heap bytes (pointers, prior slab
 * contents).
 */

Success criterion: returned buffer contains recognizably non-zero kernel data rather than zeroed memory, confirming the uninitialized copyout.

Because the function is dead code, this PoC cannot run against an unmodified kernel and is included only to document the latent primitive.

Two-part fix:

  1. Guard the copyout so it never fires when no handler matched β€” the default arm should skip the copyout (matching ath_ioctl_diag).
  2. Defense-in-depth: zero-initialize the output buffer so a future handler that forgets to fill it cannot leak.
--- a/sys/dev/netif/ath/ath/if_ath_btcoex.c
+++ b/sys/dev/netif/ath/ath/if_ath_btcoex.c
@@ -394,7 +394,7 @@ ath_btcoex_ioctl(struct ath_softc *sc, struct ath_diag *ad)
     * pointer for us to use below in reclaiming the buffer;
     * may want to be more defensive.
     */
-   outdata = kmalloc(outsize, M_TEMP, M_INTWAIT);
+   outdata = kmalloc(outsize, M_TEMP, M_INTWAIT | M_ZERO);
    if (outdata == NULL) {
        error = ENOMEM;
        goto bad;
@@ -402,7 +402,8 @@ ath_btcoex_ioctl(struct ath_softc *sc, struct ath_diag *ad)
    }
    switch (id) {
        default:
-           error = EINVAL;
+           error = EINVAL;
+           goto bad;
    }
    if (outsize < ad->ad_out_size)
        ad->ad_out_size = outsize;

The goto bad on the default path skips the copyout entirely (the bad: label at line 411 still frees indata/outdata), so no uninitialized memory is ever copied out. M_ZERO ensures the buffer is clean even if a future case handler is added that partially fills it.

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1993 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.1 KB ↓ raw
fix.diff suggested-fix Fix: Add if(error) goto bad before the copyout. 393 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1993 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/netif/ath/ath/if_ath_btcoex.c:407-410

Mechanism: ath_btcoex_ioctl: after switch sets error=EINVAL, copyout(outdata,...) executes unconditionally β€” copies entire uninitialized kmalloc'd buffer to userspace. No if(error) goto bad guard.

Hardware dependency: Requires ath(4) with BTCOEX support + ATH_DIAGAPI.

Fix: Add if(error) goto bad before the copyout.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/netif/ath/ath/if_ath_btcoex.c:407-410 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/netif/ath/ath/if_ath_btcoex.c:407-410. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: copyout after EINVAL β†’ uninitialized heap leak. Add if(error) goto bad.

Verified recommended fix

See fix.diff. copyout after EINVAL β†’ uninitialized heap leak. Add if(error) goto bad.

Verdict

REPRODUCED (source-only). sys/dev/netif/ath/ath/if_ath_btcoex.c:407-410: copyout after EINVAL β†’ uninitialized heap leak. Add if(error) goto bad.