ath_hal_getregdump(): signed/unsigned comparison break + unchecked do-while first write => unbounded attacker-length kernel-heap overflow with attacker-known register values
Summary
getregdump keeps the remaining size in a signed int but compares against sizeof() (size_t) in both loops: once space goes negative the comparisons convert unsigned and can never fail again. space is forced negative by the do-while's unconditionally-executed first register write whenever an outer iteration begins with space==8. The write length is then bounded only by the attacker's ioctl payload - a controlled-length linear heap overflow whose 4-byte words are register reads of attacker-chosen addresses (MAC/BSSID - known to a local attacker). PROVEN deterministic by a bug-faithful replica harness on the guest: 4064 bytes past a 32-byte buffer before a guard page, -O0 and -O2. Combined with DF-4320's missing gate: no privilege needed on ATH_DIAGAPI kernels. Signed-compare + pre-checked-while fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4321 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| oob_sim.c | β | 5.0 KB | view raw | |
| build.sh | β | 126 B | view raw | |
| run.sh | β | 175 B | view raw | |
| build.log | β | 38 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| run.2.log | β | 515 B | view raw | |
| trace.log | β | 783 B | view raw | |
| env.txt | β | 1003 B | view raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| README.md | β | 2.8 KB | β raw | |
| fix.diff | β | 1.0 KB | view raw |
DF-4321 β ath_hal_getregdump(): signed/unsigned comparison break β unbounded, attacker-length kernel-heap overflow
Build
cc -O2 -Wall -o oob_sim oob_sim.c # also reproducible at -O0 (exact byte accounting)
Run
./oob_sim # or: cc -O0 ... && ./oob_sim0
Expected
DF-4321: ath_hal_getregdump() signed/unsigned bounds bug
buffer: 32 bytes at ..., guard page at ...
payload: outsize=32; regs[0]={0x8000,0x800c}, regs[1..4095]={0x9000,0x9000}
legitimately writable: 32 bytes (8 header + 6 values)
=> WROTE PAST THE GUARD PAGE (fault at +4KB)
bytes written before fault: 4096 (buffer was 32) = 4064-byte overflow
first 16 bytes past the 32-byte buffer (offset 32..48): a5a5a5a50090000000900000a5a5a5a5
The harness is a bug-faithful replica of the kernel loop
(sys/dev/netif/ath/ath_hal/ah.c:848-862): a 32-byte buffer at the
start of a page followed by a PROT_NONE guard page stands in for the
kmalloc(32) DYN output buffer; the mock register read stands in for
OS_REG_READ. It demonstrates that the loop's space accounting never
stops the writes once space goes negative β the int operand is
converted to size_t in space >= sizeof(uint32_t) β so the write
length is bounded only by the attacker's regs[] array (ioctl input) and
kernel address space, not by the buffer. Verified deterministic across
runs; the smear pattern 00900000 a5a5a5a5 ... is the range header +
"register value" being written far past the allocation.
Kernel-level reproduction additionally requires an Atheros NIC and a
kernel built with options ATH_DIAGAPI (the ioctl that reaches this
code, SIOCGATHDIAG, is not compiled into stock X86_64_GENERIC; see
DF-4320). The audit guest has neither (virtio-only PCI bus), so the
kernel path could not be exercised there.
Trigger mapping (harness β kernel ioctl)
| harness | kernel (struct ath_diag ioctl) |
|---|---|
| buffer 32 bytes @ page start | ad_out_size = 32 β kmalloc(32) DYN buf |
| regs[0]={0x8000,0x800c} | indata HAL_REGRANGE #0 (4 regs β 24 bytes) |
| regs[1..N]={0x9000,0x9000} | indata ranges #1..N (12 bytes each) |
| mock 0xA5A5A5A5 | OS_REG_READ(ah, 0x9000) β attacker-chosen register (e.g. AR_STA_ID0 = NIC MAC, known) |
Files
oob_sim.cβ the replica harness.run.log(-O2),run.2.log(-O0, exact accounting),run.3.log(first accidental discovery run at -O0 with a 2-range stack array β showed the runaway walking random memory),trace.log(annotated -O0 trace showingspacegoing negative and the loop continuing).fix.diffβ the corrected loop (signed comparisons + checked first write).
DF-4321 β VERDICT
Status: inconclusive at kernel level (no Atheros NIC and no ATH_DIAGAPI in the guest kernel); the bug itself is certain β proven by exact code analysis and a bug-faithful replica harness that deterministically overflows 4064 bytes past a 32-byte buffer (guard-page instrumented, repeatable).
Root cause
sys/dev/netif/ath/ath_hal/ah.c:848-862
(ath_hal_getregdump(), reached from HAL_DIAG_REGS β
ath_hal_getdiagstate(), ah.c:882-884):
static u_int
ath_hal_getregdump(struct ath_hal *ah, const HAL_REGRANGE *regs,
void *dstbuf, int space) /* space: SIGNED int */
{
uint32_t *dp = dstbuf;
int i;
for (i = 0; space >= 2*sizeof(uint32_t); i++) { /* (A) int vs size_t */
uint32_t r = regs[i].start;
uint32_t e = regs[i].end;
*dp++ = r;
*dp++ = e;
space -= 2*sizeof(uint32_t);
do { /* (B) body runs first */
*dp++ = OS_REG_READ(ah, r); /* unchecked write */
r += sizeof(uint32_t);
space -= sizeof(uint32_t);
} while (r <= e && space >= sizeof(uint32_t)); /* (C) int vs size_t */
}
return (char *) dp - (char *) dstbuf;
}
Two compounding defects:
- (C)/(A) signed/unsigned:
sizeof()issize_t; comparingint space >= sizeof(uint32_t)convertsspaceto unsigned. Oncespaceis negative it becomes ~2^63 and every space check is permanently true. The loop is then bounded only by the attacker's data: the inner by the user-supplied register-range ende, the outer by the end of the user-suppliedregs[]array β never by the destination buffer. - (B): the do-while executes its first
*dp++write before any space check. An outer iteration that starts withspace == 8consumes the remaining 8 bytes as header and then performs one unconditional register-value write, takingspaceto β4 β the entry ticket into defect 1.
Trigger (exact arithmetic, verified by the harness)
SIOCGATHDIAG with ad_id = HAL_DIAG_REGS(13) | ATH_DIAG_IN | ATH_DIAG_DYN,
ad_out_size = 32 (kernel kmalloc(32) output buffer), indata
HAL_REGRANGE[]:
* range 0: {0x8000, 0x800c} β header(8) + 4 values(16) = 24 bytes,
space = 8;
* ranges 1..N: {0x9000, 0x9000} β each writes header(8)+1 value(4) =
12 bytes; range 1 leaves space = β4; every subsequent range is
pure overflow.
Total written = 24 + 12Β·N bytes into a 32-byte allocation. N is bounded
only by the ioctl input buffer (ad_in_size), so the overflow length is
attacker-chosen; each 4-byte word is OS_REG_READ(ah, r) with r an
attacker-chosen register β e.g. AR_STA_ID0 (the NIC MAC address) or
AR_BSS_ID0 (the associated AP's BSSID), both fully known to a local
attacker β controlled-length linear kernel-heap overflow with
attacker-known contents on the no-SMEP/no-SMAP audit-guest class of
machines.
Evidence
run.log/run.2.log: replica harness (exact loop expressions) on the DragonFly guest as an unprivileged user β=> WROTE PAST THE GUARD PAGE, 4064-byte overflow past the 32-byte buffer, deterministic across runs; heap smeara5a5a5a5 00900000 00900000 a5a5a5a5= interleaved range headers and "register values".trace.log/run.3.log: the discovery runs β the same replica with a 2-entry regs array walked off into random memory (segfault), which is how the unsigned-comparison defect was first caught; the annotated trace showsspacegoing negative while the loop keeps writing (space=-63688 ... dpoff=63808 ...until fault).- Guest:
DragonFly 6.5-DEVELOPMENT #0 ... X86_64_GENERIC, cc (clang) β build/run commands in the logs.
Why not kernel-reproduced here
The kernel path needs SIOCGATHDIAG, which is compiled only with
options ATH_DIAGAPI (absent from stock X86_64_GENERIC β nm shows no
ath_ioctl_diag in if_ath.ko), plus an Atheros NIC (guest is
virtio-only). With that config (and DF-4320's missing gate) this becomes
an unprivileged kernel heap overflow. On such a system the escalation
path is: groom the kmalloc zone of ad_out_size β overflow into an
adjacent object with known MAC/BSSID-derived words β target a refcounted
object / function-pointer-bearing structure β code exec β uid=0. Not
developed further here for lack of any ath device to drive the ioctl.
Fix
fix.diff β make both space comparisons signed and check space before
the first value write (while instead of do-while semantics). The
dump simply returns fewer bytes than requested when the buffer is
short β callers already clamp (ad->ad_out_size = outsize in
ath_ioctl_diag, if_ath_ioctl.c:215-219).
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_testablefix.diff converts both space comparisons to signed and moves the value-write check before the first write (while instead of do-while); git-apply clean against sys/; runtime validation impossible on this guest (kernel path needs ath NIC + ATH_DIAGAPI). Replica harness with the fixed loop stops cleanly inside the buffer.
findings/poc/DF-4321/fix.diff; git apply --check passes
Confirmed kernel references
Detail
Exploit chain
on ATH_DIAGAPI + ath NIC: SIOCGATHDIAG{ad_id=HAL_DIAG_REGS(13)|IN|DYN, ad_out_size=32, indata=[{0x8000,0x800c},{X,X}xN]} -> 24+12*N bytes written into kmalloc(32) -> groom the 32-byte kmalloc zone -> overwrite adjacent kernel object with known MAC/BSSID-derived words -> victim object/function pointer -> uid=0 (chain sketched only; no ath hardware to develop it against)
Evidence (decisive lines)
run.log / run.2.log (guard-page instrumented replica: 4064-byte overflow, deterministic); trace.log (discovery trace: space negative while loop keeps writing); oob_sim.c (verbatim kernel loop expressions); VERDICT.md (line-by-line arithmetic); fix.diff (signed comparisons + checked first write, git-apply clean)
PoC changes
initial harness under-modeled the bug (predicted a bounded 4-byte OOB); first guest run segfaulted, exposing the real signed/unsigned conversion; harness rewritten bug-faithful (exact kernel expressions) with guard-page instrumentation and an attacker-style multi-range payload
Verified recommended fix
Signed comparisons for space plus a pre-check before the first register-value write in ath_hal_getregdump()
Verdict
The overflow logic is certain and proven by a bug-faithful replica harness on the DragonFly guest (deterministic across runs and at -O0/-O2): ath_hal_getregdump() compares the signed int space against sizeof(uint32_t) (size_t), so once space goes negative (via the do-while's unconditional first register write when an outer iteration begins with space==8) the comparison converts to ~2^63 and no space check can ever fail again. The write length is then bounded only by the attacker's ioctl payload (register-range ends and the regs[] array length), not by the kmalloc'd output buffer: measured 4064 bytes past a 32-byte buffer before a guard page, smear pattern a5a5a5a5/00900000 = range headers + 'register values'. Every overflowed 4-byte word is OS_REG_READ() of an attacker-chosen register, e.g. AR_STA_ID0 (NIC MAC) or AR_BSS_ID0 (BSSID) - attacker-known contents. In-kernel reproduction requires SIOCGATHDIAG (options ATH_DIAGAPI, absent from stock X86_64_GENERIC) plus an Atheros NIC (guest is virtio-only), so the kernel path itself could not be exercised; on such a config this is an unprivileged-to-root-class heap overflow primitive (see DF-4320 for the gate). Impact recorded 'none' strictly for this guest.
No comments yet.