git: 79e290d96786 - main - linuxkpi: Fix cancel_delayed_work_sync() return value

From: Vladimir Kondratyev <wulf_at_FreeBSD.org>
Date: Sun, 13 Sep 2026 07:41:06 UTC
The branch main has been updated by wulf:

URL: https://cgit.FreeBSD.org/src/commit/?id=79e290d967862bef1adcf39f0bfcf1b4993a8202

commit 79e290d967862bef1adcf39f0bfcf1b4993a8202
Author:     Ryan Fahy <ryan@rfahy.com>
AuthorDate: 2026-09-13 07:39:48 +0000
Commit:     Vladimir Kondratyev <wulf@FreeBSD.org>
CommitDate: 2026-09-13 07:39:48 +0000

    linuxkpi: Fix cancel_delayed_work_sync() return value
    
    Align behavior between the Linux cancel_delayed_work_sync()
    function return value and the LinuxKPI equivalent.
    
    Linux cancel_delayed_work_sync() returns whether delayed
    work was pending, even if canceled before executing. This
    includes the case where the timer fired and work was queued
    but the callback had not yet started.
    
    The LinuxKPI version used the return value from taskqueue_cancel()
    as the return value of the public facing API, which inverted the
    behavior of two cases, violating the Linux API contract.
    Queued work which was removed before running would return false, and
    work whose callback was already executing would return true.
    
    Track the taskqueue pending count separately from the
    taskqueue_cancel() return value.
    Use the pending count for the public return value.
    Use taskqueue_cancel() return value only to decide whether
    state needs to be re-checked.
    
    This fixes behavior in consumers which use the return value of
    cancel_delayed_work_sync() to distinguish canceled pending
    work from work that was already running or idle.
    
    Signed-off-by:  Ryan Fahy <ryan@rfahy.com>
    Reviewed by:    wulf
    MFC after:      1 month
    Pull Request:   https://github.com/freebsd/freebsd-src/pull/2268
---
 sys/compat/linuxkpi/common/src/linux_work.c | 37 +++++++++++++++++++----------
 1 file changed, 24 insertions(+), 13 deletions(-)

diff --git a/sys/compat/linuxkpi/common/src/linux_work.c b/sys/compat/linuxkpi/common/src/linux_work.c
index 02dfb35d9fd0..bf4c82767621 100644
--- a/sys/compat/linuxkpi/common/src/linux_work.c
+++ b/sys/compat/linuxkpi/common/src/linux_work.c
@@ -507,12 +507,16 @@ linux_cancel_delayed_work(struct delayed_work *dwork)
 }
 
 /*
- * This function cancels the given work structure in a synchronous
- * fashion. It returns true if the work was successfully
- * cancelled. Else the work was already cancelled.
+ * 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.
+ *
+ * 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.
  */
 static bool
-linux_cancel_delayed_work_sync_int(struct delayed_work *dwork)
+linux_cancel_delayed_work_sync_int(struct delayed_work *dwork, u_int *pending,
+    bool *cancelled)
 {
 	static const uint8_t states[WORK_ST_MAX] __aligned(8) = {
 		[WORK_ST_IDLE] = WORK_ST_IDLE,		/* NOP */
@@ -523,7 +527,6 @@ linux_cancel_delayed_work_sync_int(struct delayed_work *dwork)
 	};
 	struct taskqueue *tq;
 	int ret, state;
-	bool cancelled;
 
 	WITNESS_WARN(WARN_GIANTOK | WARN_SLEEPOK, NULL,
 	    "linux_cancel_delayed_work_sync() might sleep");
@@ -536,18 +539,18 @@ linux_cancel_delayed_work_sync_int(struct delayed_work *dwork)
 		return (false);
 	case WORK_ST_TIMER:
 	case WORK_ST_CANCEL:
-		cancelled = (callout_stop(&dwork->timer.callout) == 1);
+		*cancelled = (callout_stop(&dwork->timer.callout) == 1);
 
 		tq = dwork->work.work_queue->taskqueue;
-		ret = taskqueue_cancel(tq, &dwork->work.work_task, NULL);
+		ret = taskqueue_cancel(tq, &dwork->work.work_task, pending);
 		mtx_unlock(&dwork->timer.mtx);
 
 		callout_drain(&dwork->timer.callout);
 		taskqueue_drain(tq, &dwork->work.work_task);
-		return (cancelled || (ret != 0));
+		return (*cancelled || (ret != 0));
 	default:
 		tq = dwork->work.work_queue->taskqueue;
-		ret = taskqueue_cancel(tq, &dwork->work.work_task, NULL);
+		ret = taskqueue_cancel(tq, &dwork->work.work_task, pending);
 		mtx_unlock(&dwork->timer.mtx);
 		if (ret != 0)
 			taskqueue_drain(tq, &dwork->work.work_task);
@@ -558,11 +561,19 @@ linux_cancel_delayed_work_sync_int(struct delayed_work *dwork)
 bool
 linux_cancel_delayed_work_sync(struct delayed_work *dwork)
 {
-	bool res;
+	u_int pending;
+	bool cancelled;
+	bool res = false;
+	bool ret;
+
+	do {
+		pending = 0;
+		cancelled = false;
+		ret = linux_cancel_delayed_work_sync_int(dwork, &pending,
+		    &cancelled);
+		res = res || cancelled || pending != 0;
+	} while (ret);
 
-	res = false;
-	while (linux_cancel_delayed_work_sync_int(dwork))
-		res = true;
 	return (res);
 }