[PATCH 3.12 042/155] workqueue: fix hang involving racing cancel[_delayed]_work_sync()'s for PREEMPT_NONE

From: Jiri Slaby
Date: Tue Apr 07 2015 - 09:27:08 EST

From: Tejun Heo <tj@xxxxxxxxxx>

3.12-stable review patch. If anyone has any objections, please let me know.


commit 8603e1b30027f943cc9c1eef2b291d42c3347af1 upstream.

cancel[_delayed]_work_sync() are implemented using
__cancel_work_timer() which grabs the PENDING bit using
try_to_grab_pending() and then flushes the work item with PENDING set
to prevent the on-going execution of the work item from requeueing

try_to_grab_pending() can always grab PENDING bit without blocking
except when someone else is doing the above flushing during
cancelation. In that case, try_to_grab_pending() returns -ENOENT. In
this case, __cancel_work_timer() currently invokes flush_work(). The
assumption is that the completion of the work item is what the other
canceling task would be waiting for too and thus waiting for the same
condition and retrying should allow forward progress without excessive
busy looping

Unfortunately, this doesn't work if preemption is disabled or the
latter task has real time priority. Let's say task A just got woken
up from flush_work() by the completion of the target work item. If,
before task A starts executing, task B gets scheduled and invokes
__cancel_work_timer() on the same work item, its try_to_grab_pending()
will return -ENOENT as the work item is still being canceled by task A
and flush_work() will also immediately return false as the work item
is no longer executing. This puts task B in a busy loop possibly
preventing task A from executing and clearing the canceling state on
the work item leading to a hang.

task A task B worker

executing work
set work CANCELING
block for work completion
completion, wakes up A
while (forever) {
-ENOENT as work is being canceled
false as work is no longer executing

This patch removes the possible hang by updating __cancel_work_timer()
to explicitly wait for clearing of CANCELING rather than invoking
flush_work() after try_to_grab_pending() fails with -ENOENT.

Link: http://lkml.kernel.org/g/20150206171156.GA8942@xxxxxxxx

v3: bit_waitqueue() can't be used for work items defined in vmalloc
area. Switched to custom wake function which matches the target
work item and exclusive wait and wakeup.

v2: v1 used wake_up() on bit_waitqueue() which leads to NULL deref if
the target bit waitqueue has wait_bit_queue's on it. Use
DEFINE_WAIT_BIT() and __wake_up_bit() instead. Reported by Tomeu

Signed-off-by: Tejun Heo <tj@xxxxxxxxxx>
Reported-by: Rabin Vincent <rabin.vincent@xxxxxxxx>
Cc: Tomeu Vizoso <tomeu.vizoso@xxxxxxxxx>
Tested-by: Jesper Nilsson <jesper.nilsson@xxxxxxxx>
Tested-by: Rabin Vincent <rabin.vincent@xxxxxxxx>
Signed-off-by: Jiri Slaby <jslaby@xxxxxxx>
include/linux/workqueue.h | 3 ++-
kernel/workqueue.c | 56 +++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 54 insertions(+), 5 deletions(-)

diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
index eff358e6945d..0f67a9c82787 100644
--- a/include/linux/workqueue.h
+++ b/include/linux/workqueue.h
@@ -71,7 +71,8 @@ enum {
/* data contains off-queue information when !WORK_STRUCT_PWQ */


* When a work item is off queue, its high bits point to the last
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index d10cc05bfbc6..bb5f920268d7 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -2890,19 +2890,57 @@ bool flush_work(struct work_struct *work)

+struct cwt_wait {
+ wait_queue_t wait;
+ struct work_struct *work;
+static int cwt_wakefn(wait_queue_t *wait, unsigned mode, int sync, void *key)
+ struct cwt_wait *cwait = container_of(wait, struct cwt_wait, wait);
+ if (cwait->work != key)
+ return 0;
+ return autoremove_wake_function(wait, mode, sync, key);
static bool __cancel_work_timer(struct work_struct *work, bool is_dwork)
+ static DECLARE_WAIT_QUEUE_HEAD(cancel_waitq);
unsigned long flags;
int ret;

do {
ret = try_to_grab_pending(work, is_dwork, &flags);
- * If someone else is canceling, wait for the same event it
- * would be waiting for before retrying.
+ * If someone else is already canceling, wait for it to
+ * finish. flush_work() doesn't work for PREEMPT_NONE
+ * because we may get scheduled between @work's completion
+ * and the other canceling task resuming and clearing
+ * CANCELING - flush_work() will return false immediately
+ * as @work is no longer busy, try_to_grab_pending() will
+ * return -ENOENT as @work is still being canceled and the
+ * other canceling task won't be able to clear CANCELING as
+ * we're hogging the CPU.
+ *
+ * Let's wait for completion using a waitqueue. As this
+ * may lead to the thundering herd problem, use a custom
+ * wake function which matches @work along with exclusive
+ * wait and wakeup.
- if (unlikely(ret == -ENOENT))
- flush_work(work);
+ if (unlikely(ret == -ENOENT)) {
+ struct cwt_wait cwait;
+ init_wait(&cwait.wait);
+ cwait.wait.func = cwt_wakefn;
+ cwait.work = work;
+ prepare_to_wait_exclusive(&cancel_waitq, &cwait.wait,
+ if (work_is_canceling(work))
+ schedule();
+ finish_wait(&cancel_waitq, &cwait.wait);
+ }
} while (unlikely(ret < 0));

/* tell other tasks trying to grab @work to back off */
@@ -2911,6 +2949,16 @@ static bool __cancel_work_timer(struct work_struct *work, bool is_dwork)

+ /*
+ * Paired with prepare_to_wait() above so that either
+ * waitqueue_active() is visible here or !work_is_canceling() is
+ * visible there.
+ */
+ smp_mb();
+ if (waitqueue_active(&cancel_waitq))
+ __wake_up(&cancel_waitq, TASK_NORMAL, 1, work);
return ret;


To unsubscribe from this list: send the line "unsubscribe linux-kernel" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at http://vger.kernel.org/majordomo-info.html
Please read the FAQ at http://www.tux.org/lkml/