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

IOCATAREQUEST unclamped timeout: 1-tick completion-wait fallback completes and frees a channel-owned ata_request (UAF -> Fatal trap 12 in ata_start)

Summary

The 32-bit user timeout is copied verbatim; timeout=-1 makes the completion wait (timeout*hz*4) wrap to a ~1-tick expiry, so a request still queued behind others hits the 'taskqueue timeout' fallback which runs ata_completed directly in the ioctl thread (which never dequeues it), after which the issuer frees the request while the channel still owns it. REPRODUCED on guest: Fatal trap 12 at ata_start+0x1da after 107 direct-completion warnings, 12s into a 32-thread run on /dev/acd0. Taskqueue-pending completions also use the freed object and a lockuninit'ed done. Operator/root-gated (same class as DF-1643); slab-cached object + DMA-target-after-free corruption primitive. Fix validated: 19200 ioctls, 0 warnings, guest alive.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4475 Β· 7 files
FileTypeDescriptionSize
ata_timeout_uaf.c poc 32-thread IOCATAREQUEST timeout=-1 hammer 4.0 KB view raw
serial_a1_panic.log panic 107 WARNINGs + Fatal trap 12 capture 22.3 KB view raw
run.log run-log baseline summary + panic extract 1.1 KB view raw
fix.diff suggested-fix clamp + wait-dont-complete 3.1 KB view raw
fix_build.log build-log nativekernel rc=0 5.1 MB ↓ download
fix_run.log run-log patched: 19200 ioctls, 0 warnings 177 B view raw
env.txt env guest/kernel info 314 B view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Combined fix (clamp + wait-don't-complete) applied to guest /usr/src, nativekernel build rc=0, installed, rebooted. Identical PoC run: 19,200 ioctls completed, zero 'taskqueue timeout' messages, guest alive. Baseline panic signature gone.

fix_run.log (0 WARNING lines, 'poc: done, 19200 ioctls completed'); fix_build.log tail (build completed rc=0); fix_install.log
↓ fix.diffper-fix-DF-4475

Confirmed kernel references

Detail

Exploit chain

operator/root opens /dev/acd0 -> 32 threads x IOCATAREQUEST(TEST_UNIT_READY, count=512, timeout=-1) -> backlog keeps requests queued past the 1-tick wait -> kernel completes them in the issuer thread and the issuer frees them while linked in ch->ata_queue -> ata_start dequeues a freed request (ata_start+0x1da cmpq (%rax),%r14 on a clobbered TAILQ next) -> Fatal trap 12, panic at db>. Escalation ceiling: heap grooming of the freed ata_request/buf (reallocated via concurrent IOCATAREQUESTs) turns this into kernel memory corruption; not pursued to uid0 in this run (privileged-gated device).

Evidence (decisive lines)

serial_a1_panic.log (full capture: WARNING spam + Fatal trap 12 + Stopped at ata_start+0x1da); fix_run.log (patched kernel: 19200 ioctls, 0 warnings, guest alive); fix.diff; fix_build.log/fix_install.log (nativekernel rc=0, installed, rebooted)

PoC changes

seed PoC was authored fresh for this finding (no prior pack); had to mirror struct ata_ioc_request exactly (64 bytes) and keep count!=0 to avoid DF-1644 overlap

Verified recommended fix

Clamp the ioctl timeout to >= ATA_DEFAULT_TIMEOUT and never complete a channel-owned request from the issuer thread (see fix.diff)

Verdict

IOCATAREQUEST passes the user timeout unclamped (ata-all.c:488); with timeout=-1 the completion wait degenerates to ~1 tick, the ata-queue.c:144-151 fallback directly completes a request that is still channel-owned, and the issuer then frees it (ata-all.c:514-515) -> UAF. Reproduced: 107 'taskqueue timeout' WARNINGs then Fatal trap 12 in ata_start+0x1da (fault 0x0) ~12s into a 32-thread run on /dev/acd0; guest died at db>. Operator/root-gated (device 0600 root:operator), same reachability class as DF-1643. Primitive is a slab-cached ata_request UAF plus a kmalloc'd buf still targeted by device DMA after free.