output_poll_execute's self-rearm structurally survives drm_kms_helper_poll_disable/poll_fini on the DragonFly workqueue shim β poll work executes up to 10s AFTER cancel_delayed_work_sync returned (mid-suspend connector probe on a suspended GPU, or UAF on a torn-down drm_device)
Summary
output_poll_execute re-arms itself whenever any polled connector exists; the shim's cancel_delayed_work_sync cancels the callout BEFORE waiting out the running callback, its idle predicate ignores a pending re-armed timer, and the late queue_work CLEARS work->canceled - so cancel returns 'work dead' while it is guaranteed to run once more up to 10s later. Suspend callers get a full connector-detect cycle against a suspending GPU; teardown callers get the poll on a drm_device being freed (UAF of mode_config + connector walks; fixed code, crash/hang ceiling). VERIFIED in-kernel on guest via a KLD compiling the shim functions verbatim against the real primitives ('timer pending=1' at cancel return, then EXEC #2; FIXED build converges). Make-cancel-converge fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4625 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| df4625.c | β | 8.2 KB | view raw | |
| build.sh | β | 66 B | view raw | |
| run.sh | β | 126 B | view raw | |
| fix.diff | β | 1.7 KB | view raw | |
| build.log | β | 10.4 KB | view raw | |
| run.log | β | 1.3 KB | view raw | |
| env.txt | β | 275 B | view raw | |
| df4625_extracted.inc | β | 3.9 KB | β download | |
| verdict.json | β | 7.5 KB | view raw |
DF-4625 β poll worker's self-rearm survives cancel_delayed_work_sync β post-cancel execution (mid-suspend MMIO / teardown UAF)
What
output_poll_execute() (sys/dev/drm/drm_probe_helper.c:570-655) re-arms
itself at :653-654 (if (repoll) schedule_delayed_work(delayed_work,
DRM_OUTPUT_POLL_PERIOD) = 10 s). Upstream Linux relies on
cancel_delayed_work_sync() reliably killing self-rearming work. The
DragonFly drm shim's cancel_delayed_work_sync()
(sys/dev/drm/linux_workqueue.c:415-424) does not:
:420-421it setscanceledand runscallout_cancel(&dwork->timer)first. If the callback is currently executing, the timer is idle βcallout_cancel(kern_timeout.c:1032) finds nothing, consumes/clears the CANCEL state and returns._cancel_work(:340-388) then parks inlksleepwaiting forrunning == false.- The running callback (our poll worker) reaches :654 and calls
schedule_delayed_work()βcallout_reset()arms a fresh timer (nothing blocks a reset; the CANCEL flag was consumed in step 1) β then returns. _cancel_workwakes, seeson_queue == 0 && running == 0and breaks β a pending timer is invisible to it.cancel_delayed_work_syncreturns with the timer armed.- Up to 10 s later the timer fires β
_delayed_work_fnβqueue_work(), which clearswork->canceled(linux_workqueue.c:135) and re-queues β the full poll cycle executes after its caller was promised the work is dead.
Victim contract: drm_kms_helper_poll_disable() (:695-700, cancel_delayed_work_sync
at :699) and drm_kms_helper_poll_fini() (:735-739) β called from
amdgpu suspend (amdgpu_device.c:985, :2813), radeon suspend
(radeon_device.c:1233, :1620), i915 (i915_drv.c:1983), and teardown
(i915_drv.c:1663, amdgpu dce_v8/10/11/virtual, radeon_display.c:1652,
drm_modeset_helper.c:185).
Impact: a full connector-detect cycle racing device suspend (MMIO against
a powered-down GPU β hang/panic) or executing on a torn-down drm_device
(use-after-free of dev->mode_config / freed connectors).
Build / run (DragonFly guest, fresh boot)
sh build.sh # verbatim-extracts the shim from /usr/src, builds
# df4625.ko (baseline) and fixed/df4625fix.ko
sh run.sh # kldload both; verdicts land in dmesg
The KLD compiles process_all_work, wq_worker_thread, queue_work,
_delayed_work_fn, queue_delayed_work, _cancel_work, and the
cancel_* trio verbatim against the real kernel primitives
(lockmgr / lksleep / callout_reset / callout_cancel / lwkt), then runs a
poll-worker-shaped self-rearming delayed work and cancels it mid-callback.
The modules deliberately refuse to unload (the verbatim worker thread
loops forever β the already-filed DF-1977 wart); reboot after use.
Expected output (dmesg)
baseline: df4625: canceller: cancel_delayed_work_sync() RETURN (timer pending=1 on_queue=0 running=0) df4625: EXEC #2 entered df4625: DF-4625 REPRODUCED: work executed AFTER cancel_delayed_work_sync() returned (exec_count 1 -> 2) fixed: df4625: canceller: cancel_delayed_work_sync() RETURN (timer pending=0 on_queue=0 running=0) df4625: FIX VALIDATED: no execution after cancel_delayed_work_sync() returned within the window (exec_count=1)
The timer pending=1 at cancel-return is the structural miss made
directly observable; EXEC #2 is the post-cancel execution.
Files
df4625.c KLD: environment + scenario (fixed variant via -DDF4625_FIXED) build.sh / run.sh exact build/run fix.diff converging cancel_delayed_work_sync (linux_workqueue.c) df4625_extracted.inc the verbatim shim extraction build.log / run.log / env.txt full untrimmed evidence
DF-4625 VERDICT β REPRODUCED (in-kernel KLD, verbatim shim + real primitives)
Status: reproduced / impact: dos (post-cancel execution β mid-suspend probe or teardown UAF) / confidence: certain.
Root cause (traced line by line)
- drm_probe_helper.c:653-654 β
output_poll_execute()unconditionally re-arms itself (repollis true whenever any polled connector exists). - drm_probe_helper.c:695-700 / :735-739 β
drm_kms_helper_poll_disable()/poll_fini()delegate the kill tocancel_delayed_work_sync(). - linux_workqueue.c:415-424 β the shim cancels the callout before
waiting out the running callback; when the callback then re-arms
(callout_reset arms a fresh timer; DFBSD callouts only refuse resets
while a CANCEL request is still queued β kern_timeout.c:293 checks
CALLOUT_CANCELat wheel-processing time, and_callout_cancel_or_stopon an idle timer consumes/clears the request immediately at kern_timeout.c:1032), nothing re-cancels afterwards. - linux_workqueue.c:348-385 β
_cancel_work()breaks as soon ason_queue == 0 && running == 0; a pending timer is not part of its idle predicate. - linux_workqueue.c:135 β when the surviving timer fires,
queue_work()clearswork->canceled, so the full callback re-executes.
Net effect: cancel_delayed_work_sync() returns while the work is
guaranteed to run once more (up to DRM_OUTPUT_POLL_PERIOD = 10 s later,
drm_probe_helper.c:206). Consumers:
- suspend: amdgpu_device.c:985/:2813, radeon_device.c:1233/:1620, i915_drv.c:1983 β full connector detect (i2c/aux reads + MMIO) against a suspending/suspended GPU β hang/panic.
- teardown: i915_drv.c:1663, amdgpu dce_v8/10/11/virtual fini,
radeon_display.c:1652, drm_modeset_helper.c:185 β the work executes on
a drm_device being freed β UAF (
dev->mode_configreads, connector walks via kref-protected iters on freed memory).
How it was proven (guest, DragonFly 6.5-DEVELOPMENT, real kernel)
A KLD (df4625.c) that sed-extracts the shim functions verbatim
from /usr/src/sys/dev/drm/linux_workqueue.c (ranges in build.sh) and
compiles them against the real lockmgr/lksleep/callout_reset/
callout_cancel/lwkt-thread kernel primitives. The scenario emulates the
poll worker: a callback that re-arms itself with
schedule_delayed_work() while a canceller thread runs
cancel_delayed_work_sync() mid-callback β the exact interleaving a
suspend or poll_fini() hits when it races the 10 s poll cycle (probe
cycles run for hundreds of ms, so the overlap is a per-event probability
that compounds over suspend/resume cycles).
Decisive dmesg (run.log):
BASELINE: canceller: cancel_delayed_work_sync() RETURN (timer pending=1 on_queue=0 running=0) EXEC #2 entered DF-4625 REPRODUCED: work executed AFTER cancel_delayed_work_sync() returned FIXED (fix.diff): canceller: cancel_delayed_work_sync() RETURN (timer pending=0 on_queue=0 running=0) FIX VALIDATED: no execution after cancel_delayed_work_sync() returned
timer pending=1 at cancel-return is the structural miss observed
directly in-kernel, not inferred. (The four "interface/module already
present" lines in run.log are earlier failed-load attempts of the fixed
module before its module id was renamed β kept for honesty.)
Why dos-grade and not higher
The post-cancel execution runs fixed kernel code (the poll worker) on a
freed/suspended object; there is no attacker-controlled write primitive.
Realistic ceiling: repeatable suspend/resume or driver-teardown crash /
hang (needs a drm-driven GPU and timing overlap). The guest's
X86_64_GENERIC has no drm compiled in (optional drm), so the real poll
path could not be exercised on it β hence the KLD reproduces the exact
code and primitives rather than the driver itself.
Fix validation
fix.diff replaces cancel_delayed_work_sync()'s one-shot sequence with
a converging loop (_cancel_work until callout_cancel() returns 0 and
the work is idle). Convergence argument: any re-arm happens before the
callback returns (i.e., before running == false is observed), so the
loop's callout_cancel always observes the last-armed timer; a timer
firing mid-loop is covered by callout_cancel's synchronous wait on
CALLOUT_INPROG (kern_timeout.c). Validated on the same kernel: fixed
module returns with pending=0 and no post-cancel execution.
Filed against drm_probe_helper.c (the victim whose
drm_kms_helper_poll_disable/poll_fini contract is violated); the
causal patch lands in linux_workqueue.c β a file whose pass-1 findings
(DF-1976..1980) did not cover this self-rearm cancel miss.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedfix.diff replaces cancel_delayed_work_sync's 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 baseline's pending=1 + post-cancel EXEC. The loop converges because any re-arm happens before the callback returns (before running==false is observed), and callout_cancel's sync wait (kern_timeout.c CALLOUT_INPROG) covers a timer firing mid-loop.
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)'
Confirmed kernel references
- 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
Detail
Exploit chain
cancel_delayed_work_sync(dwork) while dwork's 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 (decisive lines)
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).
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
Verified 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.
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 guest's /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).
No comments yet.