DF-4606 / verdict.json
{ "finding_id": "DF-4606", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "ata_timeout() (ata-queue.c:539-562) is a lock-aided callout callback that drops ch->state_mtx mid-execution and re-acquires it at the end, while leaving ch->running set and ch->state == ATA_ACTIVE. Any late ata_interrupt() reaching ata_end_transactions callout_cancel (ata-lowlevel.c:442), or ata_reinit() (ata-all.c:208), or ata_fail_requests() (ata-queue.c:578), all holding state_mtx, then blocks forever: _callout_cancel_or_stop(sync=1) issues LK_CANCEL_BEG and ssleep(\"costp\")s waiting for the callback to return while the callback is blocked re-acquiring the very lock the canceller holds (LKC_CANCEL does not abort a non-CANCELABLE acquisition, kern_lock.c). Deterministic KLD staging of the exact interleaving on the stock guest kernel reproduced the permanent circular wait (sysctl progress probe shows the softclock callback entered the re-acquire lockmgr and never returned; canceller parked on wchan costp; kldunload EBUSY), the loss of the wedged CPUs callwheel (userspace sleep RUN_RC=124), and the death of orderly shutdown (stuck at syncing disks... 0, force-kill required, twice). In-situ ATA trigger additionally needs a >=10 s channel stall then boundary completion, which the emulated PIIX3 cannot produce from inside the guest; window and entry paths proven by source trace. If the callback instead wins the re-acquire race, the same window yields a duplicate completion (second ATA_LF_UNLOCK + second ata_finish). Local DoS from unprivileged IO; no uid=0 route for this class.", "exploit_chain": "unprivileged IO -> request armed with callout_init_lk(&request->callout, &ch->state_mtx) -> device stalls >= request->timeout (10 s) -> callout fires ata_timeout which releases state_mtx mid-callback -> late completion interrupt takes state_mtx and calls callout_cancel in ata_end_transaction -> canceller sleeps on costp holding state_mtx; callback blocks re-acquiring state_mtx -> permanent kernel deadlock: boot-disk channel dead + one softclock thread (and its CPUs callwheel) lost -> system wedge (timers and orderly shutdown hang).", "evidence": "findings/poc/DF-4606/run.log (progress=3 probe, wchan costp, kldunload EBUSY, RUN_RC=124 sleep, stuck syncing disks... 0); wedge-serial.log (full serial console of run 3 incl. failed shutdown); VERDICT.md (run-by-run narrative incl. run-1 lockmgr_cancel_beg KKASSERT proving the INPROG cancel path); cancelrace.c (deterministic staging of the cited interleaving using the same callout_init_lk/LK_RELEASE/LK_EXCLUSIVE/callout_cancel API sequence)", "kernel_refs": [ "sys/dev/disk/nata/ata-queue.c:557", "sys/dev/disk/nata/ata-queue.c:560", "sys/dev/disk/nata/ata-queue.c:578", "sys/dev/disk/nata/ata-queue.c:93", "sys/dev/disk/nata/ata-lowlevel.c:442", "sys/dev/disk/nata/ata-all.c:208", "sys/dev/disk/nata/ata-all.c:333", "sys/kern/kern_timeout.c:609", "sys/kern/kern_timeout.c:888", "sys/kern/kern_timeout.c:920", "sys/kern/kern_lock.c:160" ], "poc_changes": "Seed sketch did not exist (new finding); authored from scratch. Three harness iterations were needed: (1) canceller originally forgot lockmgr(LK_EXCLUSIVE) before callout_cancel -> exposed the LK_CANCEL_BEG lock must be held KKASSERT instead; (2) callback slept with a timeout inside softclock (illegal: serviced by itself) -> replaced with busy rendezvous; (3) added hw.df4606_progress sysctl probe because kprintf from the post-wedge softclock context does not drain.", "attempts": 3, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 1500, "guest_dirty": 0, "build_cmd": "cd /tmp/df4606 && make (KMOD=df4606 SRCS=df4606.c, bsd.kmod.mk against X86_64_GENERIC)", "run_cmd": "kldload /tmp/df4606/df4606.ko; sysctl hw.df4606_progress; ps -axH -o state,wchan,comm | grep df4606c", "code_hash": "2f068cfdb3c2f7d7b359ed02b808b6ae5da40b4496b9d11a2c88de24d1a1a7b6", "notes": "Mechanism-level deterministic reproduction on the stock kernel (the KLD only exercises the public callout/lockmgr API in the same sequence ata uses); in-situ staging impossible on QEMU PIIX3 (no stall dial; 512MB completes in 0.69s). Guest was reset with-src after each wedged run; final state clean.", "recommended_fix": "In ata_timeout(), set ch->running = NULL while still holding ch->state_mtx (before LK_RELEASE) -- the comment at ata-queue.c:551-553 already claims this; it closes both the callout_cancel deadlock and the duplicate-completion variant.", "fix_status": "not_testable", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff authored (ch->running = NULL under state_mtx). Not validatable on this guest: baseline requires the in-situ timeout+boundary-completion interleave the emulated hardware cannot stage, and the KLD reproduces the generic primitive, which the ata-queue.c patch cannot affect by construction.", "fix_evidence": "findings/poc/DF-4606/fix.diff" } |