DF-2869 / verdict.json
{ "finding_id": "DF-2869", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Deterministic premature-free UAF proven on the stock INVARIANTS kernel: with two threads servicing one taskqueue (API-supported count>=2), completion of a fast task clears the single tq_running slot while a slow task is still executing, so taskqueue_cancel() returns 0 instead of EBUSY and taskqueue_drain() returns immediately (subr_taskqueue.c:407/:411/:429/:493). The harness then follows the documented cancel/drain-then-free contract, kfree()s the running task's context, forces objcache-LIFO reallocation of the same address (printed alias YES), and the still-running ta_func reads the overwritten memory (magic 0xdeadbeef) -> panic with the backtrace taskqueue_thread_loop -> taskqueue_run -> t1_func, i.e. the taskqueue runner executing on freed-and-reused kernel memory. Primitive: slab-groomable free-while-running (UAF read+write of task-owned context of attacker-chosen consumer objects). No unprivileged in-tree trigger on this guest: the co-runner precondition needs a count>=2 queue (in-tree: vmxnet3 if_vmx.c:990-997, torn down by an admin) or the taskqueue_free() co-run window (subr_taskqueue.c:155); reaching the victim task from userspace requires such a consumer under attack-driven load. That is a reach blocker, not a primitive blocker - the corruption itself is proven. uid0 chain therefore not exercised end-to-end; the natural chain is: drive victim driver's multi-thread queue under load, race its detach drain, free its per-queue softc while the deferred task runs, reallocate same-zone, corrupt function-pointer-bearing state, hijack. fix.diff (per-task TASKQ_RUNNING under TQ_LOCK + wakeup after unlock) validated: cancel returns EBUSY=16, drain blocks until ta_func completes, no panic, module unloads cleanly.", "exploit_chain": "proven: 2-runner queue -> tq_running clobbered -> cancel=0 + drain early-return -> kfree(live ctx) + same-size kmalloc (LIFO alias) -> running ta_func reads/writes foreign object (magic flip observed) -> controlled panic. uid0 route (not exercised, reach-blocked): vmxnet3-style count>=2 consumer torn down under rx/tx load -> premature free of driver softc during detach drain -> slab groom -> function-pointer corruption -> code exec in ring0.", "evidence": [ "run.log/panic.txt: 'taskqueue_cancel(t1) = 0 while t1 running' + 'taskqueue_drain(t1) returned while t1 ta_func still executing'", "run.log/panic.txt: 'freed ctx reallocated at 0xfffff8008d2e0810 (alias YES) magic=deadbeef' + 'UAF CONFIRMED'", "panic.txt: panic trace t1_func+0x9b <- taskqueue_run+0xbb <- taskqueue_thread_loop+0x5d", "run.fixed.log: 'taskqueue_cancel(t1) = 16' + releaser observes drain() blocking + clean unload, no panic" ], "kernel_refs": [ "sys/kern/subr_taskqueue.c:407", "sys/kern/subr_taskqueue.c:411", "sys/kern/subr_taskqueue.c:429", "sys/kern/subr_taskqueue.c:493", "sys/kern/subr_taskqueue.c:512", "sys/kern/subr_taskqueue.c:155", "sys/dev/virtual/vmware/vmxnet3/if_vmx.c:997" ], "poc_changes": "Baseline evidence was produced with the initial harness (identical bug path); the final source adds a releaser thread + clean-shutdown path so the FIXED kernel run terminates without wedging (first fixed-kernel attempt left the controller blocked inside the now-correctly-blocking drain and kldunload crashed - harness flaw, not a fix failure, documented in VERDICT.md). Also: taskqueue_start_threads name must not carry %d (auto-suffixed), DECLARE_MODULE/MODULE_VERSION modules GPF this guest's kldload - DEV_MODULE used instead (see deploy_guest.sh).", "attempts": 4, "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": 6900, "guest_dirty": 0, "build_cmd": "cd /root/poc/tquaf && make -m /usr/share/mk SYSDIR=/usr/src/sys (with device_if.h/bus_if.h symlinks; see build.sh)", "run_cmd": "kldload /root/poc/tquaf/tquaf.ko; sleep 6; dmesg | grep tquaf (see run.sh)", "code_hash": "49dd949d0e1b8fef6c8d07b172513c13fc2bda80a98f0485894ab4d5a68d8a76", "fix_status": "fixed", "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 17:34:00 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Combined fix.diff (per-task TASKQ_RUNNING under TQ_LOCK + wakeup after unlock in taskqueue_run; ACTIVE re-check before worker park; tq_callouts wait in taskqueue_free; timeout_func freer wakeup) built as kernel #1 Wed Sep 2 17:34:00. Baseline bad behavior GONE on every PoC: DF-2869 cancel=EBUSY + drain blocks + no UAF/panic + clean unload; DF-2870 taskqueue_free returns, no wedged threads; DF-2872 timeout task no longer runs after free; DF-2873 hammer does not lose a wakeup (bounded negative). Guest left clean via vm.sh reset with-src. NOTE: one intermediate fixed-kernel run crashed because the module had been rebuilt against the STOCK ABI while struct task grew by ta_flags - module-build logistics, not a fix defect; final run used modules built against the patched /usr/obj.", "fix_evidence": [ "run.fixed.log" ], "notes": "guest reset (vm.sh reset with-src) performed after final run; combined fix.diff shared with DF-2870/2872/2873 packs", "recommended_fix": "Track running tasks with a per-task TASKQ_RUNNING flag set at dequeue and cleared under the queue spinlock (wakeup after unlock); make taskqueue_cancel/taskqueue_drain(_simple) consult the flag instead of the single tq_running slot." } |