git: 2ff9182d8921 - main - LinuxKPI: account for delayed work cancelled after taskqueue dequeue

From: Vladimir Kondratyev <wulf_at_FreeBSD.org>
Date: Sun, 18 Oct 2026 15:16:46 UTC
The branch main has been updated by wulf:

URL: https://cgit.FreeBSD.org/src/commit/?id=2ff9182d89215322de1958057bf2345d34a6576e

commit 2ff9182d89215322de1958057bf2345d34a6576e
Author:     JustAnotherHumanBeing <oleglelchuk@gmail.com>
AuthorDate: 2026-10-18 15:15:54 +0000
Commit:     Vladimir Kondratyev <wulf@FreeBSD.org>
CommitDate: 2026-10-18 15:15:54 +0000

    LinuxKPI: account for delayed work cancelled after taskqueue dequeue
    
    I encounter intermittent shutdown/restart hangs with i915/GuC and the
    normal 34 ms scheduling-disable delay. Setting that delay to zero has
    avoided the hang in my testing. This change fixes a reproducible
    LinuxKPI cancellation-accounting defect that can leave the
    corresponding context reference unreleased, rather than changing the
    driver's delay.
    
    Commit 79e290d96786 corrected the ordinary queued and already-executing
    return values of cancel_delayed_work_sync(). There is still a gap
    between native taskqueue dequeue and LinuxKPI callback ownership:
    
    1. In sys/kern/subr_taskqueue.c, taskqueue_run_locked() removes the
       task, clears ta_pending and sets tb_running before calling its
       handler.
    2. Before linux_work_fn() claims the callback,
       linux_cancel_delayed_work_sync_int() atomically changes WORK_ST_TASK
       to WORK_ST_IDLE.
    3. taskqueue_cancel() can now return EBUSY with pending == 0: the
       native task is busy, but the Linux callback has not started.
    4. linux_work_fn() subsequently sees IDLE and skips the callback. The
       cancelling thread drains the task and retries, but returns false
       because neither the timer-stop result nor the native pending count
       records what it cancelled.
    
    The task can also finish consuming IDLE before taskqueue_cancel() is
    called, giving success with pending == 0 and the same incorrect false
    return. This affects zero-delay WORK_ST_TIMER and WORK_ST_CANCEL left
    by a failed nonblocking cancellation as well. A zero-delay self-requeue
    handled by linux_work_fn()'s executor loop can similarly be pending
    without a native queue entry.
    
    Use the old state returned by the atomic transition to account for
    pending work suppressed by this cancellation: TIMER, TASK and CANCEL
    contribute a true result. EXEC does not: the callback has already been
    claimed, or has finished with that sticky state. Keep the native
    pending-count aggregation, drain operations and retry decisions
    unchanged; use a separate local variable for the callout-stop result.
    In particular, EBUSY alone is not treated as successful cancellation,
    which would regress the already-running-callback case fixed by
    79e290d96786.
    
    One affected consumer is guc_request_alloc() in drm-kmod's
    drivers/gpu/drm/i915/gt/uc/intel_guc_submission.c. It calls
    intel_context_sched_disable_unpin() when cancel_delayed_work_sync()
    returns true. Suppressing the delayed callback while returning false
    can leave neither path responsible for releasing that reference,
    retaining the engine wakeref that shutdown later waits for.
    
    Fixes:  79e290d96786 ("Fix cancel_delayed_work_sync() return value")
    Signed-off-by:  JustAnotherHumanBeing <oleglelchuk@gmail.com>
    Differential Revision:  https://reviews.freebsd.org/D60046
---
 sys/compat/linuxkpi/common/src/linux_work.c | 23 ++++++++++++++++-------
 1 file changed, 16 insertions(+), 7 deletions(-)

diff --git a/sys/compat/linuxkpi/common/src/linux_work.c b/sys/compat/linuxkpi/common/src/linux_work.c
index bf4c82767621..8667fe75b948 100644
--- a/sys/compat/linuxkpi/common/src/linux_work.c
+++ b/sys/compat/linuxkpi/common/src/linux_work.c
@@ -507,12 +507,10 @@ linux_cancel_delayed_work(struct delayed_work *dwork)
 }
 
 /*
- * This function cancels the given delayed work structure in a
- * synchronous fashion. It returns true if pending delayed work was
- * cancelled. Else the work was not pending.
+ * Cancel and drain delayed work, reporting cancelled pending work through
+ * cancelled and the taskqueue pending count through pending.
  *
- * If the work restarted itself or was busy while being cancelled,
- * retry_needed is set to true so the caller can re-check the state.
+ * Return true if the caller needs to re-check the state after draining.
  */
 static bool
 linux_cancel_delayed_work_sync_int(struct delayed_work *dwork, u_int *pending,
@@ -527,19 +525,30 @@ linux_cancel_delayed_work_sync_int(struct delayed_work *dwork, u_int *pending,
 	};
 	struct taskqueue *tq;
 	int ret, state;
+	bool stopped;
 
 	WITNESS_WARN(WARN_GIANTOK | WARN_SLEEPOK, NULL,
 	    "linux_cancel_delayed_work_sync() might sleep");
 	mtx_lock(&dwork->timer.mtx);
 
 	state = linux_update_state(&dwork->work.state, states);
+	/*
+	 * A taskqueue worker can have dequeued the task without linux_work_fn()
+	 * having claimed the callback yet.  In that case taskqueue_cancel()
+	 * reports EBUSY with no pending tasks, but our transition to IDLE still
+	 * prevents the callback.  Account for this using the state consumed by
+	 * the atomic transition, not just the native queue's pending count.
+	 * WORK_ST_EXEC, in contrast, means the callback was already claimed.
+	 */
+	*cancelled = (state == WORK_ST_TIMER || state == WORK_ST_TASK ||
+	    state == WORK_ST_CANCEL);
 	switch (state) {
 	case WORK_ST_IDLE:
 		mtx_unlock(&dwork->timer.mtx);
 		return (false);
 	case WORK_ST_TIMER:
 	case WORK_ST_CANCEL:
-		*cancelled = (callout_stop(&dwork->timer.callout) == 1);
+		stopped = (callout_stop(&dwork->timer.callout) == 1);
 
 		tq = dwork->work.work_queue->taskqueue;
 		ret = taskqueue_cancel(tq, &dwork->work.work_task, pending);
@@ -547,7 +556,7 @@ linux_cancel_delayed_work_sync_int(struct delayed_work *dwork, u_int *pending,
 
 		callout_drain(&dwork->timer.callout);
 		taskqueue_drain(tq, &dwork->work.work_task);
-		return (*cancelled || (ret != 0));
+		return (stopped || (ret != 0));
 	default:
 		tq = dwork->work.work_queue->taskqueue;
 		ret = taskqueue_cancel(tq, &dwork->work.work_task, pending);