⬢ DragonFlyBSD Kernel Audit
DF-4625 / fix.diff
← back to finding ↓ download raw
# DF-4625 fix: make cancel_delayed_work_sync() converge for self-rearming
# delayed work.  The shim currently (a) cancels the callout BEFORE waiting
# out the running callback -- a callback that re-arms itself (drm's
# output_poll_execute, drm_probe_helper.c:653-654) then arms a fresh timer
# nothing cancels, (b) _cancel_work() breaks as soon as the work is idle,
# so the pending timer is invisible to it, and (c) queue_work() clears
# work->canceled (linux_workqueue.c:135) when the timer eventually fires,
# so the full callback re-executes up to DRM_OUTPUT_POLL_PERIOD (10 s)
# AFTER cancel_delayed_work_sync() returned -- i.e. after
# drm_kms_helper_poll_disable()/poll_fini() promised the work is dead
# (mid-suspend MMIO probe, or UAF on a torn-down drm_device).
#
# Validated on the guest: with this loop, cancel returns with
# timer pending=0 and no post-cancel execution (see run.log, FIXED build).
--- a/sys/dev/drm/linux_workqueue.c
+++ b/sys/dev/drm/linux_workqueue.c
@@ -416,11 +416,24 @@ bool
 cancel_delayed_work_sync(struct delayed_work *dwork)
 {
 	struct work_struct *work = &dwork->work;
+	bool ret;
 
-	work->canceled = true;
-	callout_cancel(&dwork->timer);
+	/*
+	 * Self-rearming delayed work (e.g. drm's output_poll_execute) must
+	 * not survive this call: the callback can re-arm the timer AFTER
+	 * our callout_cancel() but BEFORE _cancel_work() observes the work
+	 * idle, and queue_work() clears work->canceled when the timer
+	 * eventually fires.  Loop until the timer is dead AND the work is
+	 * neither queued nor running.
+	 */
+	for (;;) {
+		ret = _cancel_work(work, true);
+		if (!callout_cancel(&dwork->timer) &&
+		    !work->on_queue && !work->running)
+			break;
+	}
 
-	return _cancel_work(work, true);
+	return ret;
 }
 
 bool