β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-1978

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.

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1978 Β· 4 files
FileTypeDescriptionSize
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
README.md readme PoC trigger description
↓ download 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.

VERDICT.md verdict verification narrative
↓ download raw

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.

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

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).