workqueue: Add a new flag to spot the potential UAF error
authorRichard Clark <richard.xnu.clark@gmail.com>
Tue, 13 Dec 2022 04:39:36 +0000 (12:39 +0800)
committerTejun Heo <tj@kernel.org>
Wed, 4 Jan 2023 22:25:29 +0000 (12:25 -1000)
Currently if the user queues a new work item unintentionally
into a wq after the destroy_workqueue(wq), the work still can
be queued and scheduled without any noticeable kernel message
before the end of a RCU grace period.

As a debug-aid facility, this commit adds a new flag
__WQ_DESTROYING to spot that issue by triggering a kernel WARN
message.

Signed-off-by: Richard Clark <richard.xnu.clark@gmail.com>
Reviewed-by: Lai Jiangshan <jiangshanlai@gmail.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
include/linux/workqueue.h
kernel/workqueue.c

index a0143dd..ac551b8 100644 (file)
@@ -335,6 +335,7 @@ enum {
         */
        WQ_POWER_EFFICIENT      = 1 << 7,
 
+       __WQ_DESTROYING         = 1 << 15, /* internal: workqueue is destroying */
        __WQ_DRAINING           = 1 << 16, /* internal: workqueue is draining */
        __WQ_ORDERED            = 1 << 17, /* internal: workqueue is ordered */
        __WQ_LEGACY             = 1 << 18, /* internal: create*_workqueue() */
index 07895de..5b06262 100644 (file)
@@ -1433,9 +1433,13 @@ static void __queue_work(int cpu, struct workqueue_struct *wq,
        lockdep_assert_irqs_disabled();
 
 
-       /* if draining, only works from the same workqueue are allowed */
-       if (unlikely(wq->flags & __WQ_DRAINING) &&
-           WARN_ON_ONCE(!is_chained_work(wq)))
+       /*
+        * For a draining wq, only works from the same workqueue are
+        * allowed. The __WQ_DESTROYING helps to spot the issue that
+        * queues a new work item to a wq after destroy_workqueue(wq).
+        */
+       if (unlikely(wq->flags & (__WQ_DESTROYING | __WQ_DRAINING) &&
+                    WARN_ON_ONCE(!is_chained_work(wq))))
                return;
        rcu_read_lock();
 retry:
@@ -4414,6 +4418,11 @@ void destroy_workqueue(struct workqueue_struct *wq)
         */
        workqueue_sysfs_unregister(wq);
 
+       /* mark the workqueue destruction is in progress */
+       mutex_lock(&wq->mutex);
+       wq->flags |= __WQ_DESTROYING;
+       mutex_unlock(&wq->mutex);
+
        /* drain it before proceeding with destruction */
        drain_workqueue(wq);