DF-5176 / verdict.json
{ "finding_id": "DF-5176", "status": "reproduced", "reproduced": true, "impact": "kernel panic (local DoS) on INVARIANTS kernels via KKASSERT in taskqueue_enqueue_locked; unlocked cross-CPU task-state race on non-INVARIANTS kernels; triggered from drm hrtimer usage (i915_pmu/intel_uncore/i915_perf/amdgpu cross-CPU restart of a timer whose previous expiry task is still pending)", "confidence": "certain", "verdict": "The LinuxKPI hrtimer shim enqueues the single shared timer->task on the FIRING cpus taskqueue_thread[mycpuid] (linux_hrtimer.c:37) while (re-)arming the underlying systimer on the CALLING cpu (linux_hrtimer.c:115 / kern_systimer.c:368), so restarting one hrtimer from two different cpus migrates the same struct task between per-cpu taskqueues \u2014 explicitly forbidden by subr_taskqueue.c:185-188 because tasks are not locked. Deterministically reproduced on the guest with a KLD driving the exact sequence (block q0, start from cpu0, fire, start from cpu1, fire): panic assertion \"queue == task->ta_queue\" failed in taskqueue_enqueue_locked at /usr/src/sys/kern/subr_taskqueue.c:206 with __hrtimer_function in the backtrace, on the first attempt. Distinct from known DF-2174 (epilogue active-flag clobber) and DF-2175 (re-init of an ARMED systimer): here the previous systimer already fired, the bzero+add is queue-safe, and the defect is the cross-queue task migration. fix.diff (pin timer->tq at hrtimer_init; enqueue on the pinned queue) rebuilt as drm.ko inside the guest eliminates the panic (5 attempts, FAILED to trigger - no panic, guest stays up).", "exploit_chain": "unpriv user on a GPU machine (drm.ko + i915/amdgpu loaded): cause the same hrtimer to be restarted from two different cpus while the first expirys task is still pending on the first cpus taskqueue_thread (e.g. i915 pmu sampling timer started from gt-unpark/engine-enable paths on cpu A and again on cpu B under load, intel_uncore.c:70-78 forcewake re-arms, or i915_perf poll timer) -> fire on cpu B -> taskqueue_enqueue_locked KKASSERT(queue == task->ta_queue) -> kernel panic (DoS). On non-INVARIANTS kernels the same path does an unlocked ta_pending++ against a task linked into another cpus queue, a state-corruption window rather than a clean panic. Root escalation not developed (panic-class primitive).", "evidence": "panic.txt + run.log.full: [df5176] task ta_pending=1 ta_queue == q0 then after re-arm from cpu1 panic: assertion \"queue == task->ta_queue\" failed ... taskqueue_enqueue_locked+0x159 <- taskqueue_enqueue+0x45 <- __hrtimer_function+0x21 <- systimer_intr+0xd6 <- pcpu_timer_process_oncpu+0x82; run.fix.log: post-fix task ta_pending=0 ta_queue != q0 ... FAILED to trigger - no panic ... GUEST-STILL-UP; env.txt: INVARIANTS=1, X86_64_GENERIC, 6 cpus, drm.ko loaded", "kernel_refs": [ "sys/dev/drm/linux_hrtimer.c:33-40", "sys/dev/drm/linux_hrtimer.c:87-119", "sys/kern/subr_taskqueue.c:185-188", "sys/kern/subr_taskqueue.c:205-209", "sys/kern/kern_systimer.c:359-371", "sys/dev/drm/i915/i915_pmu.c:130-135", "sys/dev/drm/i915/intel_uncore.c:70-78", "sys/dev/drm/i915/intel_uncore.c:393", "sys/dev/drm/i915/i915_perf.c:1914", "sys/dev/drm/amd/amdgpu/dce_virtual.c:709" ], "poc_changes": "PoC written from scratch (no seed): KLD with MODULE_DEPEND(drm); v1 migrated the kldload user thread via lwkt_setcpu_self which wedged the guest scheduler, v2 uses dedicated per-cpu kernel threads created with lwkt_create(..., TDF_NOSTART, cpuid) + lwkt_schedule; DFs DECLARE_MODULE requires a moduledata_t VARIABLE (passing the handler directly makes the kernel strcmp() machine-code bytes as the module name \u2014 fixed by following the selffree.c pattern); taskqueue_block(taskqueue_thread[0]) keeps the fired task deterministically pending so no race is needed", "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": 1, "build_cmd": "scp df5176.c Makefile dfbsd:/root/df5176/ ; ssh dfbsd cd /root/df5176 && make SYSDIR=/usr/src/sys ; fix phase: cd /usr/src && patch -p1 < /root/fix.diff && cd sys/dev/drm/drm && make SYSDIR=/usr/src/sys && cp drm.ko /boot/kernel && sync", "run_cmd": "ssh dfbsd kldload drm; kldload /root/df5176/df5176.ko (expect immediate panic; see panic.txt)", "code_hash": "d5daa5d749cf6b23449902a719698f4cc1d157b2c2fcdf4f9cbf74f995532c21", "notes": "Known pair DF-2174/DF-2175 NOT re-reported; this is the taskqueue-side contract violation (task shared across per-cpu queues), which fires even when the systimer-side re-init is queue-safe (previous timer already fired). Two harness-only crashes during development (DECLARE_MODULE metadata misuse -> strcmp GPF in module_lookupbyname; user-thread lwkt_setcpu_self migration -> guest wedge) were diagnosed and eliminated \u2014 they are not kernel findings. Guest reset (with-src) performed after validation.", "recommended_fix": "Pin the tasks home queue for the timers lifetime: add struct taskqueue *tq to struct hrtimer, set timer->tq = taskqueue_thread[mycpuid] in hrtimer_init, and make __hrtimer_function enqueue on timer->tq instead of the firing cpus queue (see fix.diff)", "fix_status": "fixed", "fix_baseline_reproduced": true, "fix_patched_reproduced": false, "fix_verdict": "fix.diff applied inside guest /usr/src (patch -p1 clean), drm.ko rebuilt from patched source and installed to /boot/kernel, guest rebooted into it, PoC rebuilt against the patched header: task stayed on its pinned home queue (ta_queue != q0, ta_pending=0 coalescing), 5 attempts, NO panic, guest up \u2014 the baseline panic is gone", "fix_evidence": "run.fix.log ([df5176] task ta_pending=0 ta_queue != q0 ... FAILED to trigger - no panic ... GUEST-STILL-UP) vs panic.txt/run.log.full (baseline KKASSERT panic through __hrtimer_function)" } |