git: 2ff9182d8921 - main - LinuxKPI: account for delayed work cancelled after taskqueue dequeue
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
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);