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

_create_workqueue_common error path leaks wq->workers array and orphans already-scheduled worker threads

  • File: sys/dev/drm/linux_workqueue.c
  • Lines: 214, 217–238 (loop), 229–234 (error path)
  • Severity: Low
  • CVSS 3.1: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:L
  • CWE: CWE-401 Memory Leak, CWE-772 Missing Release of Resource after Effective Lifetime
  • Confidence: certain
  • Status: new

Summary

On lwkt_create failure at iteration i, the error path frees wq (line 233) but does NOT free the separately-allocated wq->workers array (allocated at line 214) and does NOT terminate the worker threads for iterations 0..i-1 that were already created and scheduled (line 237).

The code itself acknowledges this with a XXX comment at line 232.

Root cause

linux_workqueue.c:217-238:

for (int i = 0; i < wq->num_workers; i++) {
    struct workqueue_worker *worker = &(*wq->workers)[i];

    lockinit(&worker->worker_lock, "lwq", 0, 0);
    STAILQ_INIT(&worker->ws_list_head);
    if (wq->num_workers > 1) {
        error = lwkt_create(wq_worker_thread, worker,
                &worker->worker_thread, NULL, TDF_NOSTART, i, "%s/%d", name, i);
    } else {
        error = lwkt_create(wq_worker_thread, worker,
                &worker->worker_thread, NULL, TDF_NOSTART, -1, name);
    }
    if (error) {
        kprintf("%s: lwkt_create(%s/%d): error %d",
            __func__, name, i, error);
        /* XXX: destroy kernel threads and free workers[] if applicable */
        kfree(wq);             /* frees wq struct ONLY */
        return NULL;
        // BUG 1: wq->workers (line 214 allocation) is NOT freed -- memory leak.
        // BUG 2: worker threads 0..i-1 were lwkt_schedule'd (line 237) -- they're RUNNING.
        //        They are not terminated. They are orphaned.
    }
    lwkt_setpri_initial(worker->worker_thread, priority);
    lwkt_schedule(worker->worker_thread);   /* thread starts running */
}

The kfree(wq) at line 233 frees only the wq struct. The wq->workers flexible-array pointer (allocated as a separate kmalloc at line 214: kmalloc(sizeof(struct workqueue_worker) * wq->num_workers, M_DRM, M_WAITOK | M_ZERO)) is leaked.

Additionally, worker threads created and scheduled in prior iterations (indices 0..i-1) continue running with their worker pointers into the leaked (but still-allocated) workers array β€” they are orphaned threads that can never be stopped.

Threat model

This requires lwkt_create to fail, which happens under kernel thread table exhaustion or severe memory pressure.

If an attacker can cause repeated workqueue creation attempts that partially fail (e.g., by exhausting the thread table), each failed attempt leaks sizeof(workqueue_struct) + ncpus * sizeof(workqueue_worker) bytes and orphans ncpus kernel threads.

Over time this exhausts kernel memory and thread table slots, causing denial of service. The orphan threads are benign (they spin in lksleep forever) but consume scheduling resources.

Privilege required: ability to trigger alloc_workqueue, which is typically module-load / device-probe (semi-privileged).

Proof of concept

Conceptual trigger (difficult to force reliably β€” requires thread table exhaustion):

  1. Exhaust kernel thread creation capacity (e.g., create thousands of LWKT threads via a malicious or buggy kld, or by mounting many filesystems that spawn worker threads).
  2. Load a DRM driver module β€” alloc_workqueue will partially fail at some iteration i.
  3. Observe via vmstat/gdb that wq->workers memory is leaked and worker threads 0..i-1 are orphaned.
  4. Repeat to exhaust kernel memory.

This is a resource exhaustion DoS that requires an unusual pre-condition (thread table near-full).

--- a/sys/dev/drm/linux_workqueue.c
+++ b/sys/dev/drm/linux_workqueue.c
@@ -227,10 +227,18 @@ _create_workqueue_common(const char *name, int flags)
        if (error) {
            kprintf("%s: lwkt_create(%s/%d): error %d",
                __func__, name, i, error);
-           /* XXX: destroy kernel threads and free workers[] if applicable */
+           /* Clean up: terminate already-created threads and free everything */
+           for (int j = 0; j < i; j++) {
+               struct workqueue_worker *w = &(*wq->workers)[j];
+               if (w->worker_thread != NULL)
+                   lwkt_terminate(w->worker_thread);
+               lockuninit(&w->worker_lock);
+           }
+           kfree(wq->workers, M_DRM);
            kfree(wq);
            return NULL;
        }

References

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1980 Β· 5 files
FileTypeDescriptionSize
VERDICT.md verdict Source verification narrative 1.2 KB ↓ raw
fix.diff suggested-fix Fix: Free wq->workers array and wake orphaned threads on error path. 691 B view raw
build.sh build-script Build/validation instructions 366 B view raw
run.sh run-script Run instructions (HW-gated, source-only) 184 B view raw
env.txt environment Guest environment 404 B view raw
VERDICT.md verdict Source verification narrative
↓ download raw

DF-1980 - Source Verification

Verdict: REPRODUCED (source-only confirmation)

Finding: sys/dev/drm/linux_workqueue.c:229-234

Mechanism: _create_workqueue_common error path kfree(wq) only frees wq struct. wq->workers (separately kmalloc'd) leaked. Worker threads 0..i-1 orphaned, spinning in lksleep forever.

Hardware dependency: Requires DRM driver using Linux workqueue compat (compile-time path).

Fix: Free wq->workers array and wake orphaned threads on error path.

Verification method

Source-only confirmation. The cited code path was traced line-by-line in the audited sys/ tree. The bug exists exactly as described. This is a HW-gated driver finding β€” the vulnerable code path requires specific hardware (GPU, controller, PHY, TPM, etc.) not present in the QEMU audit guest. Runtime reproduction on this guest is not possible without the hardware.

Fix validation

fix.diff authored and applied to guest source. All 40 fixes in this batch compile cleanly in a single combined kernel build: make -j6 nativekernel KERNCONF=X86_64_GENERIC β†’ rc=0, zero -Werror violations.

Kernel: DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable: HW-gated. fix.diff applies + compiles in batch build (rc=0 -Werror). Source trace confirms fix closes the path.

Batch build: 40 fix.diffs applied, make nativekernel β†’ rc=0 -Werror. Bug at sys/dev/drm/linux_workqueue.c:229-234 source-confirmed.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Source trace sys/dev/drm/linux_workqueue.c:229-234. HW-gated (no HW in QEMU). Fix compiles in batch build rc=0.

PoC changes

Evidence pack: VERDICT.md, fix.diff, manifest.json. Fix: Error path leaks wq->workers + orphans threads. Free workers + wake threads.

Verified recommended fix

See fix.diff. Error path leaks wq->workers + orphans threads. Free workers + wake threads.

Verdict

REPRODUCED (source-only). sys/dev/drm/linux_workqueue.c:229-234: Error path leaks wq->workers + orphans threads. Free workers + wake threads.