DF-4625 / verdict.json
{ "finding_id": "DF-4625", "status": "reproduced", "reproduced": 1, "impact": "dos", "confidence": "certain", "verdict": "REPRODUCED on the DragonFly guest with a KLD that compiles the drm workqueue shim functions VERBATIM (build.sh seds process_all_work/wq_worker_thread/queue_work/_delayed_work_fn/queue_delayed_work/_cancel_work/cancel_delayed_work* out of the guests /usr/src/sys/dev/drm/linux_workqueue.c) against the REAL kernel primitives (lockmgr, lksleep, callout_reset/callout_cancel, lwkt threads), then races a self-rearming delayed_work exactly the way output_poll_execute does (drm_probe_helper.c:653-654). Decisive dmesg lines (run.log): BASELINE -- canceller: cancel_delayed_work_sync() RETURN (timer pending=1 on_queue=0 running=0) proving cancel_sync returns while the callback-armed timer is STILL PENDING and the work is idle (the structural miss: _cancel_work linux_workqueue.c:348-385 breaks on idle and never sees the timer; the initial callout_cancel at :421 ran before the callback re-armed), followed 4 ticks later by EXEC #2 entered -> DF-4625 REPRODUCED: work executed AFTER cancel_delayed_work_sync() returned (exec_count 1 -> 2). When the surviving timer fires, queue_work (linux_workqueue.c:135) clears work->canceled, so the full poll cycle re-executes. In the drm context this means drm_kms_helper_poll_disable()/poll_fini() (drm_probe_helper.c:695-739, cancel_delayed_work_sync at :699/:737) return while output_poll_execute will run once more up to DRM_OUTPUT_POLL_PERIOD=10s later: on the amdgpu/radeon/i915 suspend paths (amdgpu_device.c:985/:2813, radeon_device.c:1233/:1620, i915_drv.c:1983) that is a full connector-detect cycle against a suspended GPU (MMIO on powered-down hardware -> hang/panic), and on the teardown paths (i915_drv.c:1663, amdgpu dce_v8/10/11/virtual, radeon_display.c:1652, drm_modeset_helper.c:185) it is a use-after-free on the freed drm_device. FIXED build (only cancel_delayed_work_sync replaced by the converging loop of fix.diff): RETURN (timer pending=0 on_queue=0 running=0) and FIX VALIDATED: no execution after cancel_delayed_work_sync() returned. Realistic ceiling on stock systems is a suspend/resume or driver-teardown crash/DoS (needs a drm-driven GPU and the poll worker mid-cycle during cancel; poll period 10s, probe cycles run hundreds of ms, so the overlap is a per-event probability that compounds over suspend/resume cycles).", "exploit_chain": "cancel_delayed_work_sync(dwork) while dworks callback is executing -> :421 callout_cancel runs on an idle timer (consumed/cleared, kern_timeout.c _callout_cancel_or_stop) -> _cancel_work parks in lksleep waiting for running==false -> callback tail schedule_delayed_work(dwork, 10s) (drm_probe_helper.c:654) arms a FRESH timer via callout_reset -> callback returns, _cancel_work sees on_queue==0/running==0 and breaks -> cancel_delayed_work_sync returns with the timer pending -> 10s later timer fires, _delayed_work_fn -> queue_work clears canceled (linux_workqueue.c:135) and re-queues -> output_poll_execute runs full detect cycle after the caller believed it dead -> mid-suspend MMIO (panic/hang) or post-teardown UAF of drm_device/mode_config. No attacker-controlled write primitive; crash/DoS-grade with UAF flavor.", "evidence": "run.log lines: canceller: cancel_delayed_work_sync() RETURN (timer pending=1 on_queue=0 running=0) (baseline) then EXEC #2 entered and DF-4625 REPRODUCED: work executed AFTER cancel_delayed_work_sync() returned (exec_count 1 -> 2); fixed section: RETURN (timer pending=0 on_queue=0 running=0) + FIX VALIDATED: no execution.... build.log shows the verbatim extraction ranges; df4625.c contains the exact scenario (poll_emul_fn re-arms exactly like drm_probe_helper.c:653-654).", "kernel_refs": [ "sys/dev/drm/drm_probe_helper.c:206", "sys/dev/drm/drm_probe_helper.c:611", "sys/dev/drm/drm_probe_helper.c:653", "sys/dev/drm/drm_probe_helper.c:654", "sys/dev/drm/drm_probe_helper.c:695", "sys/dev/drm/drm_probe_helper.c:699", "sys/dev/drm/drm_probe_helper.c:735", "sys/dev/drm/drm_probe_helper.c:737", "sys/dev/drm/linux_workqueue.c:135", "sys/dev/drm/linux_workqueue.c:151", "sys/dev/drm/linux_workqueue.c:159", "sys/dev/drm/linux_workqueue.c:348", "sys/dev/drm/linux_workqueue.c:364", "sys/dev/drm/linux_workqueue.c:385", "sys/dev/drm/linux_workqueue.c:415", "sys/dev/drm/linux_workqueue.c:421", "sys/kern/kern_timeout.c:1032", "sys/dev/drm/i915/i915_drv.c:1663", "sys/dev/drm/i915/i915_drv.c:1983", "sys/dev/drm/amd/amdgpu/amdgpu_device.c:985", "sys/dev/drm/radeon/radeon_device.c:1233" ], "poc_changes": "no seed PoC; KLD written from scratch: verbatim sed-extraction of the shim compiled against real kernel primitives; the scenario forces the deterministic interleaving (callback holds in lksleep until the canceller has passed callout_cancel and parked inside _cancel_work) that suspend/teardown hits statistically; fixed variant needed distinct module id (df4625fx) because DECLARE_MODULE/moduledata-string/MODULE_VERSION names collide", "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": 420, "guest_dirty": 0, "build_cmd": "sh build.sh (sed ranges from /usr/src/sys/dev/drm/linux_workqueue.c into df4625_extracted.inc; make -DNO_WERROR SYSDIR=/usr/src/sys for df4625.ko and fixed/df4625fix.ko)", "run_cmd": "sh run.sh (kldload ./df4625.ko; kldload ./fixed/df4625fix.ko; dmesg | grep df4625) -- fresh boot required, the modules cannot unload (EBUSY by design, worker thread loops forever)", "code_hash": "793cf5a0ea3b8f00b4cc0f8aeb28941e8f4b0649ec448dcf7a430335efcab670", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 x86_64 (same-kernel KLD-level validation: verbatim shim vs shim-with-fixed-cancel_delayed_work_sync, real lockmgr/lksleep/callout primitives; full kernel rebuild hardware-gated: guests X86_64_GENERIC has no drm)", "fix_verdict": "fix.diff replaces cancel_delayed_work_syncs one-shot sequence with a converging loop (_cancel_work until callout_cancel returns 0 AND work idle). On the fixed KLD run: cancel returns with timer pending=0 on_queue=0 running=0 and no execution occurs in the post-cancel window, vs the baselines pending=1 + post-cancel EXEC. The loop converges because any re-arm happens before the callback returns (before running==false is observed), and callout_cancels sync wait (kern_timeout.c CALLOUT_INPROG) covers a timer firing mid-loop.", "fix_evidence": "run.log FIXED build section: RETURN (timer pending=0 on_queue=0 running=0) + FIX VALIDATED: no execution after cancel_delayed_work_sync() returned within the window (exec_count=1)", "notes": "The broken primitive lives in linux_workqueue.c (pass-1 findings DF-1976..1980 did not cover this self-rearm/cancel miss); it is filed here because drm_probe_helper.cs poll machinery is the in-tree victim and its drm_kms_helper_poll_disable/poll_fini contract is what gets violated. Consumer-side hardening alternative: drm_kms_helper_poll_disable could clear mode_config.poll_enabled and re-cancel. The guest kernel has no drm (X86_64_GENERIC, optional drm), so the real in-kernel poll path is untestable on this guest; the KLD reproduces the exact code + primitives.", "recommended_fix": "Converging cancel_delayed_work_sync loop in linux_workqueue.c (see fix.diff); optionally also make drm_kms_helper_poll_disable/fini clear poll_enabled before cancelling." } |