ath_ioctl_phyerr() copies out the UNINITIALIZED M_TEMP result buffer on its error paths β attacker-sized kernel heap disclosure to unprivileged users
Summary
The DYN path kmallocs ad_out_size M_TEMP bytes never initialized, and BOTH error branches (unknown id / short SET_THRESH) still execute the unconditional clamp+copyout - the sane sibling ath_ioctl_diag only copies out on HAL-accept. Attacker-chosen size, unlimited repetitions, no priv check on SIOCGATHPHYERR. Sink-level REPRODUCED on guest: EINVAL returned while 2048B of marker-sprayed heap (255/256 slots) landed in the user buffer, 3 stable runs; error==0 gate A/B-validated. Hardware/config-gated entry (ath NIC + option ATH_DIAGAPI) caps it at Medium.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5024 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df5024.c | β | 5.8 KB | view raw | |
| df5024_fixed.c | β | 5.8 KB | view raw | |
| df5024_trigger.c | β | 3.0 KB | view raw | |
| Makefile | β | 52 B | β download | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 159 B | view raw | |
| build.log | β | 954 B | view raw | |
| run.log | β | 2.2 KB | view raw | |
| run_fixed_ab.log | β | 1.5 KB | view raw | |
| leak_sample.txt | β | 1.9 KB | view raw | |
| env.txt | β | 813 B | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| fix_validation.log | β | 1.8 KB | view raw | |
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| verdict.json | β | 4.3 KB | view raw |
DF-5024 β ath_ioctl_phyerr() uninitialized-heap disclosure on error paths (dfs_null.c)
File: sys/dev/netif/ath/ath_dfs/null/dfs_null.c (pass 2)
Lines: 253-281 (allocation at :253, unconditional copyout at :280)
Class: kernel heap info leak (CWE-908 / CWE-200) β NEW in pass 2;
not a re-report of DF-2051 (GET_THRESH overflow) or DF-2052 (SET_THRESH NULL
deref), which live on the success paths of the two known command ids.
Build (on the DragonFly guest, as root)
sh build.sh # builds df5024.ko (vulnerable-pattern sink) + df5024_trigger
Run
sh run.sh # sprays marker through M_TEMP, then exercises the leak 3x sh run_ab-style: # see run_fixed_ab.log β A/B against df5024_fixed.c (fix applied)
Expected output (decisive lines from run.log)
A:unknown-id: ioctl rc=-1 errno=22(Invalid argument) nonzero=1 marker8=255/256 first64: ffffffffffffffff 4446353032342121 4446353032342121 ... B:short-set: ioctl rc=-1 errno=22(Invalid argument) nonzero=1 marker8=255/256
- The ioctl FAILS with EINVAL β yet the user buffer contains 2048 bytes of
kernel heap:
4446353032342121is the ASCII markerDF5024!!that the sysctl spray wrote intoM_TEMPand freed; the leadingffffffffffffffffqword is malloc-zone chunk metadata. - 255/256 marker slots β the entire
kmalloc(outsize, M_TEMP, M_INTWAIT)block from dfs_null.c:253 crossed the kernel/user boundary despite the command being rejected. - Stable across 3 runs (run.log); with the fix pattern applied
(df5024_fixed.c / run_fixed_ab.log) the same ioctls return EINVAL and the
buffer stays all-zero (
marker8=0/256) β leak eliminated.
Why a sink module
The guest has no Atheros NIC and the stock kernel is built without
ATH_DIAGAPI, so SIOCGATHPHYERR cannot be entered at runtime (env.txt).
Following the DF-4986/DF-5012 house precedent, df5024.c carries the
verbatim ioctl body of ath_ioctl_phyerr() (dfs_null.c:221-288) with only
the two HAL calls elided β they are unreachable on the leaking paths, which
break/default before any sc->sc_ah dereference. The kmalloc/copyout
behavior being demonstrated is entirely kernel-generic (M_TEMP + copyout).
Artifacts
df5024.cβ sink, vulnerable pattern (verbatim structure)df5024_fixed.cβ same sink with the fix'serror == 0gatedf5024_trigger.cβ user trigger (unknown-id + short-SET variants)run.log,run_fixed_ab.log,leak_sample.txt,build.log,env.txtfix.diffβ the real fix againstsys/(error-gated copyout)
DF-5024 Verification Verdict
File: sys/dev/netif/ath/ath_dfs/null/dfs_null.c:253,275-281 Severity: Medium (unprivileged kernel-heap disclosure; hardware/config-gated entry) Method: sink-level reproduction on the stock guest kernel (DF-4986/DF-5012 house precedent) + line-level source trace + behavioral fix A/B + real-module compile validation of the fix.
Reproduced: YES (leak, sink-level; entry path hardware-gated)
The defect (source trace)
ath_ioctl_phyerr() (dfs_null.c:220-288):
dfs_null.c:245-258β when the user setsATH_DIAG_DYN(0x8000) inad_id,outdata = kmalloc(outsize, M_TEMP, M_INTWAIT)is allocated with the attacker-chosenad_out_sizeand never initialized.dfs_null.c:259-277β the command dispatch. Two error paths leaveoutdatauntouched: *default:for any id β {2,3} (DFS_RADARDETECTS=6 from if_athioctl.h:404, or any garbage id) βerror = EINVAL(:275-276); *DFS_SET_THRESHwithad_in_size < sizeof(HAL_PHYERR_PARAM)(60) βerror = EINVAL(:261-264) β this path also works withATH_DIAG_IN|ATH_DIAG_DYNand a 0/short input buffer.dfs_null.c:278-281β unconditionally: clampad_out_size, thenif (outdata && copyout(outdata, ad->ad_out_data, ad->ad_out_size))β the full uninitialized M_TEMP block is published to userspace even though the command was rejected (EINVAL is then returned; the data is already out).
Contrast with the sane sibling ath_ioctl_diag() (if_ath_ioctl.c:214-222):
there the copyout happens only when ath_hal_getdiagstate() accepts the id β
unknown ids never copy out. The null-DFS copy dropped that guard.
Reachability: SIOCGATHPHYERR β ath_ioctl() (if_ath_ioctl.c:301-302,
#ifdef ATH_DIAGAPI) β no privilege check anywhere on the path
(SIOCZATHSTATS two cases above shows what a privileged variant looks like:
caps_priv_check_self, :288). Entry needs an ath interface (hardware) and
a kernel built with option ATH_DIAGAPI. On such a system any local user
holds this leak.
Runtime proof (stock guest kernel, uname in env.txt)
run.log / leak_sample.txt (sink = verbatim ioctl body):
- ioctl returns EINVAL while 2048 bytes of recycled kernel M_TEMP land in
the user buffer: 255/256 slots carry the sysctl-sprayed marker
DF5024!!; the leading qword (ff..ff) is malloc-chunk metadata β kernel provenance proven, not zeros/stale user data. - Both error paths leak (unknown id AND short SET input).
- Stable across 3 runs.
Fix validation
- Behavioral A/B (
run_fixed_ab.log): with the fix'serror == 0gate transplanted into the sink, the same ioctls return EINVAL and the buffer stays all-zero (marker8=0/256,nonzero=0) β the disclosure is gone. - Compile validation:
fix.diffapplied to the guest's/usr/srccopy;sys/dev/netif/ath/ath_dfs/nullmodule rebuilt clean with-Werror(ath_dfs.ko). Sources restored afterward. - Also batch-compiled clean together with the three ips fixes (ips.ko,
-Werror).
Fix
fix.diff β gate the clamp+copyout on error == 0 (mirrors
ath_ioctl_diag). Independent git apply --check clean; no interaction with
the DF-2051/DF-2052 fixes (different lines).
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
fixedfix.diff (error-gated copyout) behaviorally validated at sink level (run_fixed_ab.log: EINVAL with all-zero buffer, marker8=0/256 vs 255/256 baseline) and compile-validated against the guest's /usr/src copy (ath_dfs.ko module rebuilt -Werror clean, fix_validation.log; sources restored). The real ath entry path is hardware-gated on this guest, so the stock-kernel A/B is against the verbatim-pattern sink, not the driver itself.
run_fixed_ab.log (A/B output); fix.diff; fix_validation.log (patched ath_dfs.ko build)
Confirmed kernel references
- sys/dev/netif/ath/ath_dfs/null/dfs_null.c:253
- sys/dev/netif/ath/ath_dfs/null/dfs_null.c:275
- sys/dev/netif/ath/ath_dfs/null/dfs_null.c:278
- sys/dev/netif/ath/ath_dfs/null/dfs_null.c:280
- sys/dev/netif/ath/ath/if_athioctl.h:187
- sys/dev/netif/ath/ath/if_athioctl.h:402
- sys/dev/netif/ath/ath/if_ath_ioctl.c:214
- sys/dev/netif/ath/ath/if_ath_ioctl.c:301
Detail
Evidence (decisive lines)
run.log lines 'A:unknown-id: ioctl rc=-1 errno=22 ... marker8=255/256' and 'B:short-set ...' (leak on both error paths, EINVAL returned); leak_sample.txt hexdump showing ffffffffffffffff allocator qword + 'DF5024!!' (4446353032342121) fill; run_fixed_ab.log '-- FIXED pattern:' showing nonzero=0 marker8=0/256; fix_validation.log patched ath_dfs.ko build; env.txt hardware/config gating probe
PoC changes
authored fresh from source analysis (no seed): sink module replicates the ioctl body line-for-line with the two HAL calls elided (unreachable on the leaking paths); marker-spray sysctl added to prove kernel-heap provenance of the disclosed bytes
Verified recommended fix
Gate the clamp+copyout at dfs_null.c:278-281 on error == 0, mirroring ath_ioctl_diag() (if_ath_ioctl.c:214-222).
Verdict
Sink-level reproduction on the stock guest kernel (DF-4986/DF-5012 house precedent): a kld module carrying the verbatim ioctl body of dfs_null.c ath_ioctl_phyerr() (lines 221-288, HAL calls elided -- they are unreachable on the leaking paths) demonstrates that the unconditional copyout at dfs_null.c:280 publishes the attacker-sized UNINITIALIZED kmalloc(outsize, M_TEMP, M_INTWAIT) block to userspace on BOTH error paths (unknown ioctl id -> default EINVAL; DFS_SET_THRESH with ad_in_size < 60 -> EINVAL). The ioctl returns EINVAL while 2048 bytes of recycled kernel M_TEMP land in the user buffer: 255/256 8-byte slots carry a marker previously sprayed into M_TEMP via sysctl, and the leading qword is malloc-chunk metadata -- kernel-heap provenance proven, stable across 3 runs. The real entry (SIOCGATHPHYERR, no privilege check on the path, if_ath_ioctl.c:301-302) is hardware-gated on this guest (no Atheros NIC, stock kernel lacks ATH_DIAGAPI; env.txt) but is unprivileged on any system with an ath interface and a DIAGAPI kernel -- arbitrary-size kernel heap disclosure (heap contents/pointers, KASLR defeat material). A/B with the fix pattern transplanted into the sink: EINVAL returned, buffer stays all-zero (marker8=0/256) -- leak eliminated; fix.diff also compile-validated against the real sys/ tree (ath_dfs.ko, -Werror clean).
No comments yet.