queue_delayed_work silently ignores caller workqueue always runs on system_wq breaking ordered/highpri semantics
- File:
sys/dev/drm/linux_workqueue.c - Lines: 151β157 (
_delayed_work_fn), 159β171 (queue_delayed_work) - Severity: Medium
- CVSS 3.1:
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L - CWE: CWE-837 Improper Control of Synchronization Between Threads (priority/ordering contract violation)
- Confidence: certain
- Status: new
Summary
_delayed_work_fn(), the callout callback invoked when a delayed_work timer
fires, hardcodes queue_work(system_wq, ...) regardless of which workqueue the
caller passed to queue_delayed_work(). The wq parameter to
queue_delayed_work is completely unused.
Delayed work intended for high-priority, ordered, or unbound workqueues runs on
the default system_wq instead, breaking ordering and priority guarantees that
DRM drivers depend on for correctness and security.
Root cause
linux_workqueue.c:151-157:
static inline void
_delayed_work_fn(void *arg)
{
struct delayed_work *dw = arg;
queue_work(system_wq, &dw->work); /* ALWAYS system_wq -- ignores caller's wq */
}
And queue_delayed_work (linux_workqueue.c:159-171):
int
queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work,
unsigned long delay)
{
int pending = work->work.on_queue; // XXX: running too ?
if (delay != 0) {
callout_reset(&work->timer, delay, _delayed_work_fn, work);
} else {
_delayed_work_fn((void *)work);
}
return (!pending);
}
The wq parameter is never passed to _delayed_work_fn and never
stored anywhere. The callout callback API only supports a single void *arg,
so the wq pointer is lost. The callout fires _delayed_work_fn which always
enqueues to system_wq. There is no field in struct delayed_work
(workqueue.h:66-69) to store the target wq.
Additionally, line 163 reads work->work.on_queue without any lock (data race
on a shared field).
Threat model
Drivers allocate specialized workqueues for ordering and priority guarantees.
For example, i915 allocates alloc_ordered_workqueue("i915-dp", 0) for hotplug
work (i915_drv.c:872) and alloc_ordered_workqueue("i915_modeset", 0) for
modeset work (intel_display.c:15340).
Ordered workqueues guarantee single-threaded FIFO execution β critical for
display state machine correctness. When delayed work queued to these workqueues
actually runs on system_wq (which is multi-CPU and unordered), ordering is
violated.
Concurrent execution of callbacks that should be serialized can corrupt shared display state, causing use-after-free on modeset structures or incorrect connector state that bypasses security-relevant display policy.
Priority inversion: high-priority delayed work (WQ_HIGHPRI) runs at normal
priority, causing missed deadlines in time-sensitive GPU operations.
Proof of concept
/* wqbug.c
* Build: cc -DKERNEL -c wqbug.c &&
* ld -d -T /usr/lib/ldscripts/kld.x -o wqbug.ko wqbug.o
* Run: kldload ./wqbug.ko
*/
#include <sys/types.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <drm/drmP.h>
#include <linux/workqueue.h>
static struct delayed_work dw;
static struct workqueue_struct *my_ordered_wq;
static void my_func(struct work_struct *work)
{
/* Check which worker we're on by examining work->worker */
kprintf("delayed work ran on worker %p (expected ordered wq worker)\n",
container_of(work, struct delayed_work, work)->work.worker);
}
static int wqbug_load(struct module *m, int cmd, void *arg)
{
if (cmd != MOD_LOAD) return 0;
my_ordered_wq = alloc_ordered_workqueue("mywq", 0);
INIT_DELAYED_WORK(&dw, my_func);
queue_delayed_work(my_ordered_wq, &dw, 1); /* should run on my_ordered_wq */
tsleep(&wqbug_load, 0, "wqbug", hz * 2);
return 0;
}
DEV_MODULE(wqbug, wqbug_load, NULL);
Run: kldload ./wqbug.ko. Observe dmesg: the work runs on system_wq's
worker, not on my_ordered_wq's worker. The ordered workqueue's worker
never picks it up.
Success criterion: dmesg shows the work executing on a system_wq
worker thread instead of the mywq worker thread.
Recommended fix
Add a wq field to struct delayed_work and use it in the callout callback:
--- a/sys/dev/drm/include/linux/workqueue.h
+++ b/sys/dev/drm/include/linux/workqueue.h
@@ -66,6 +66,7 @@
struct delayed_work {
struct work_struct work;
struct callout timer;
+ struct workqueue_struct *wq; /* target workqueue, set by queue_delayed_work */
};
--- a/sys/dev/drm/linux_workqueue.c
+++ b/sys/dev/drm/linux_workqueue.c
@@ -52,7 +52,7 @@ static inline void
_delayed_work_fn(void *arg)
{
- struct delayed_work *dw = arg;
- queue_work(system_wq, &dw->work);
+ struct delayed_work *dw = arg;
+ queue_work(dw->wq ? dw->wq : system_wq, &dw->work);
}
@@ -160,6 +160,7 @@ queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *work,
unsigned long delay)
{
+ work->wq = wq;
int pending = work->work.on_queue; // XXX: running too ?
References
sys/dev/drm/linux_workqueue.c:151-157β_delayed_work_fnignores caller's wqsys/dev/drm/linux_workqueue.c:159-171βqueue_delayed_workaccepts but never storeswqsys/dev/drm/include/linux/workqueue.h:66-69βstruct delayed_workhas no wq fieldsys/dev/drm/i915/i915_drv.c:872βalloc_ordered_workqueue("i915-dp", ...)caller relying on ordered semanticssys/dev/drm/i915/intel_display.c:15340β modeset ordered workqueue
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1978 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | readme | PoC trigger description | 1.1 KB | β raw |
| VERDICT.md | verdict | verification narrative | 923 B | β raw |
| fix.diff | suggested-fix | git-apply-able fix | 833 B | view raw |
| fix_build_summary.txt | build-log | combined 16-finding kernel build rc=0 | 826 B | view raw |
DF-1978 PoC β queue_delayed_work ignores wq parameter
Kernel module demonstrating the mismatch
/* wqbug.c -- Build & run: kldload ./wqbug.ko */
#include <sys/types.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <drm/drmP.h>
#include <linux/workqueue.h>
static struct delayed_work dw;
static struct workqueue_struct *my_ordered_wq;
static void my_func(struct work_struct *work) {
kprintf("delayed work ran on worker %p (expected ordered wq worker)\n",
container_of(work, struct delayed_work, work)->work.worker);
}
static int wqbug_load(struct module *m, int cmd, void *arg) {
if (cmd != MOD_LOAD) return 0;
my_ordered_wq = alloc_ordered_workqueue("mywq", 0);
INIT_DELAYED_WORK(&dw, my_func);
queue_delayed_work(my_ordered_wq, &dw, 1);
tsleep(&wqbug_load, 0, "wqbug", hz * 2);
return 0;
}
DEV_MODULE(wqbug, wqbug_load, NULL);
Expected output
dmesg shows the work executing on a system_wq worker thread instead of the
mywq ordered worker thread. The mywq worker never picks it up -- ordered
semantics are silently broken.
DF-1978 Verification
Verdict
SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).
The cited defect exists in the audited source at sys/dev/drm/linux_workqueue.c:151-171.
Part of the drm compatibility layer (module, not in GENERIC).
Mechanism (source-only confirmation)
_delayed_work_fn callout callback (151-157) hardcodes queue_work(system_wq, &dw->work) regardless of which wq the caller passed. queue_delayed_work (159-171) accepts wq parameter but never stores it β struct delayed_work (workqueue.h:66-69) has no wq field. Delayed work intended for ordered/high-priority workqueues silently runs on system_wq, breaking ordering guarantees DRM drivers depend on.
Recommended fix
Add a wq field to struct delayed_work (header), set it in queue_delayed_work, and use it in _delayed_work_fn instead of the hardcoded system_wq.
The full git apply-able diff lives in fix.diff in this folder.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- l
- i
- n
- u
- x
- _
- w
- o
- r
- k
- q
- u
- e
- u
- e
- .
- c
- :
- 1
- 5
- 1
- -
- 1
- 7
- 1
Detail
Exploit chain
none (non-corruption: correctness/ordering bug, not memory safety)
Evidence (decisive lines)
Combined kernel build: 16 fix.diffs applied, make -j6 nativekernel => rc=0, 0 warnings, 0 errors.
PoC changes
Created VERDICT.md, fix.diff (add wq field to delayed_work + use in _delayed_work_fn), manifest.json, env.txt, build.sh, run.sh.
Verified recommended fix
Add a wq field to struct delayed_work (workqueue.h), set it in queue_delayed_work, use it in _delayed_work_fn. Supersedes finding proposal.
Verdict
SOURCE-CONFIRMED (HW/module gated). _delayed_work_fn (linux_workqueue.c:151-157) hardcodes queue_work(system_wq, &dw->work) regardless of caller's wq. queue_delayed_work (L159-171) accepts wq but never stores it -- struct delayed_work has no wq field. Delayed work for ordered workqueues silently runs on system_wq, breaking ordering guarantees. Confirmed by source trace. Not runnable: drm compat layer (module, no GPU HW).
No comments yet.