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

taskqgroup_create() heap OOB write: unvalidated cnt writes struct taskqgroup_cpu slots past the fixed tqg_queue[MAXCPU] array

Field Value
ID DF-2848
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:L
CWE CWE-787 Out-of-bounds Write
File sys/kern/subr_gtaskqueue.c
Lines 788-798 (stores :591-597; array :574)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

taskqgroup_create() kmallocs sizeof(struct taskqgroup) (fixed tqg_queue[MAXCPU] array) and taskqgroup_cpu_create() then writes tqg_queue[i] (LIST_INIT NULL store, tgc_taskq heap-pointer store, tgc_cpu int) for every i < cnt with no bound check β€” cnt > MAXCPU writes kernel-controlled data past the allocation (24 bytes per slot at caller-chosen distance). Deterministically reproduced on the guest with a KLD: all readback slots past the M_ZERO'd allocation contained non-NULL kernel heap pointers; the first OOB slots also clobbered the object's own tail (tqg_lock/tqg_name/tqg_cnt). A cnt of MAXCPU+2048 overruns ~48KB. No in-tree caller passes anything but ncpus β€” latent API hazard requiring a root-loaded KLD (DF-0085 family).

Validate and clamp cnt in taskqgroup_create() before allocating (validated fix.diff in findings/poc/DF-2848/).

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_gtaskqueue.c (GLM 5.3); KLD OOB reproduced + fix validated same run.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2848 Β· 14 files
FileTypeDescriptionSize
gtq_oob.c β€” 3.6 KB view raw
gtq_smash.c β€” 906 B view raw
build.sh β€” 201 B view raw
run.sh β€” 189 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 1.9 KB view raw
run.smash.log β€” 882 B view raw
run.fixed.log β€” 744 B view raw
fixbuild.log β€” 220 B view raw
env.txt β€” 1.3 KB view raw
fix.diff β€” 660 B view raw
VERDICT.md β€” 3.4 KB ↓ raw
README.md β€” 2.1 KB ↓ raw
verdict.json β€” 4.0 KB view raw

DF-2848 β€” taskqgroup_create() heap OOB write (cnt unvalidated vs MAXCPU)

WHAT struct taskqgroup embeds a FIXED array struct taskqgroup_cpu tqg_queue[MAXCPU] (sys/kern/subr_gtaskqueue.c:574). The object is kmalloc'd with sizeof(struct taskqgroup) (subr_gtaskqueue.c:788) and taskqgroup_cpu_create() then writes tqg_queue[i] for EVERY i < cnt with no bound check (subr_gtaskqueue.c:793-797): LIST_INIT (8-byte NULL store), tgc_taskq = fresh gtaskqueue* (8-byte heap-pointer store), tgc_cpu = int. cnt > MAXCPU writes past the allocation.

BUILD (on the guest, as root) cd /root/gtq_oob ln -sf /usr/obj/usr/src/sys/X86_64_GENERIC/device_if.h . ln -sf /usr/obj/usr/src/sys/X86_64_GENERIC/bus_if.h . make -m /usr/share/mk SYSDIR=/usr/src/sys # -> gtq_oob.ko

RUN kldload /root/gtq_oob/gtq_oob.ko # deterministic proof dmesg | tail -30 # see run.log ps -ax -o comm | grep -c '^gtq_oob_' # threads created kldunload gtq_oob

Impact variant (silent-corruption stressor): kldload /root/poc/gtq_smash.ko # cnt = MAXCPU+2048

EXPECTED (stock kernel) - 16 slots probed; every slot at offset >= 6252 ("[PAST ALLOC]") contains a NON-NULL kernel heap pointer in tgc_taskq written by taskqgroup_cpu_create() past the end of the kmalloc'd object. - slot[256]/[257] show the qgroup's own tail (tqg_lock/tqg_name) being clobbered (tgc_cpu=-2134490742 garbage, thread names beyond gtq_oob_257 turn to garbage). - Guest stays up: the corruption is silent (no slab redzones). - FIXED kernel: "taskqgroup_create: gtq_oob cnt 272 > MAXCPU 256, clamping" printed, all [PAST ALLOC] slots read zero, exactly 256 gtq_oob_* threads exist.

FILES gtq_oob.c deterministic OOB-write proof module (readback probe) gtq_smash.c large-overrun impact variant (MAXCPU+2048 slots) build.log / run.log / run.smash.log / run.fixed.log / fixbuild.log fix.diff verified fix (clamp + loud kprintf) manifest.json / verdict.json

VERDICT.md
↓ download raw

VERDICT β€” DF-2848 (taskqgroup_create unvalidated cnt -> heap OOB write)

REPRODUCED: YES (deterministic), on the stock INVARIANTS guest kernel (DragonFly 6.5-DEVELOPMENT #0, Thu Jul 2 06:02:54 UTC 2026).

HOW

gtq_oob.ko calls taskqgroup_create("gtq_oob", MAXCPU + 16, 1) and reads the slots [256..271] back through a layout replica (only the leading tqg_queue[] array is probed, which sits at offset 0 of the real opaque struct, so replica offsets are exact on x86_64).

Observed (run.log, stock kernel):

gtq_oob: sizeof(struct taskqgroup) <= 6252 bytes (kmalloc size) gtq_oob: slot[261] off=6264 [PAST ALLOC] tgc_taskq=0xfffff8008bb07198 ... ... (11 consecutive [PAST ALLOC] slots, every one non-NULL) gtq_oob: slot[271] off=6504 [PAST ALLOC] tgc_taskq=0xfffff8008bb075a8 ...

Every [PAST ALLOC] slot holds a freshly allocated struct gtaskqueue * written by taskqgroup_cpu_create() (sys/kern/subr_gtaskqueue.c:592-597) at byte offsets 6264..6527, i.e. 12..384 bytes PAST the end of the kmalloc'd object (allocation upper bound 6252 bytes: 6144-byte array + lock + name + cnt + alignment). The allocation is M_ZERO, so these non-NULL kernel pointers are unambiguously the kernel's own OOB stores. Additionally the first OOB slots clobber the object's own tail: slot[256] readback tgc_cpu=-2134490742 is tqg_lock bytes mixed with the store, and thread naming beyond index 257 degenerates (only 257 well-named threads of 272 created) β€” tqg_name itself gets overwritten by slot 258's tgc_taskq pointer.

Primitive: ~24 bytes per OOB index of kernel-controlled data (heap pointer + int + NULL) written at attacker-chosen distance past a kmalloc'd object (cnt is a caller-supplied int, unvalidated). A large cnt smashes arbitrarily far (run.smash.log: cnt=MAXCPU+2048, ~48KB overrun, 2304 live taskqueue threads β€” guest survived 30s; corruption is silent, DFly slab has no redzones).

WHY NOT uid=0 / no panic coaxed

The only caller of taskqgroup_create() in the tree is the TASKQGROUP_DEFINE macro's SYSINIT (softirq group, cnt=ncpus<=MAXCPU), and the whole gtaskqueue/taskqgroup API has ZERO in-tree consumers beyond that boot glue (verified: no users of GROUPTASK_ENQUEUE, taskqgroup_attach/detach/create/bind/drain_all anywhere in sys/). Reaching the bug requires loading a KLD that calls the API β€” root only. So: certain, deterministic memory-corruption primitive, but no unprivileged trigger exists today; it is a latent API hazard of exactly the DF-0085 family (not re-reported here).

EXPLOIT CHAIN: none (see above β€” root-gated API misuse; no unpriv path).

FIX VALIDATION

fix.diff clamps cnt into [1, MAXCPU] with a loud kprintf. Rebuilt kernel in-guest (#1: Wed Sep 2 09:11:43 UTC 2026, see fixbuild.log: "Kernel install for X86_64_GENERIC completed", BUILD_OK). Re-running the exact PoC on the fixed kernel (run.fixed.log):

taskqgroup_create: gtq_oob cnt 272 > MAXCPU 256, clamping slot[261..271] ... [PAST ALLOC] tgc_taskq=0 tgc_cpu=0 (all zero) FIXED_THREAD_COUNT=256

Baseline OOB writes GONE on the patched kernel. fix_status: fixed.

FILES: run.log (decisive), run.fixed.log, run.smash.log, fixbuild.log, gtq_oob.c, gtq_smash.c, fix.diff, manifest.json, verdict.json. Kernel refs: sys/kern/subr_gtaskqueue.c:574 (array decl), :788-798 (kmalloc + unbounded loop), :585-598 (taskqgroup_cpu_create stores).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in-guest to /usr/src, kernel rebuilt+installed (nativekernel, BUILD_OK, #1 Wed Sep 2 09:11:43 UTC 2026) and the exact PoC re-run: clamp message printed, every [PAST ALLOC] slot reads zero (no kernel writes past the allocation), exactly 256 gtq_oob_* threads created. Baseline OOB writes gone.

["run.fixed.log: 'taskqgroup_create: gtq_oob cnt 272 > MAXCPU 256, clamping'", "run.fixed.log: 'slot[261] off=6264 [PAST ALLOC] tgc_taskq=0 tgc_cpu=0' (all 11 zero)", "run.fixed.log: 'FIXED_THREAD_COUNT=256'", "fixbuild.log: '>>> Kernel install for X86_64_GENERIC completed' + BUILD_OK"]
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Wed Sep 2 09:11:43 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log: 'slot[261] off=6264 [PAST ALLOC] tgc_taskq=0xfffff8008bb07198' (11 consecutive non-NULL past-alloc slots)", "run.log: 'sizeof(struct taskqgroup) <= 6252 bytes (kmalloc size)'", "run.log: 'slot[256] off=6144 tgc_taskq=0xfffff8008bb06f90 tgc_cpu=-2134490742' (tail clobbered)", 'run.smash.log: cnt=MAXCPU+2048 (~48KB overrun) survives 30s - corruption silent', 'run.fixed.log: clamp message + all past-alloc slots zero + FIXED_THREAD_COUNT=256']

PoC changes

Added #include for DEV_MODULE (first build failed); replaced planned struct-deref readback with a 24-byte-slot layout replica of the leading tqg_queue[] array (struct taskqgroup is opaque outside subr_gtaskqueue.c, array is at offset 0 so replica offsets are exact); symlinked generated headers device_if.h/bus_if.h from /usr/obj/usr/src/sys/X86_64_GENERIC for the out-of-tree KLD build.

Verified recommended fix

Clamp/validate cnt in taskqgroup_create(): if (cnt < 1) cnt = 1; if (cnt > MAXCPU) { kprintf(...); cnt = MAXCPU; }

Verdict

Deterministic heap OOB write proven on the stock INVARIANTS kernel: taskqgroup_create() kmallocs sizeof(struct taskqgroup) (<=6252 bytes: fixed tqg_queue[MAXCPU]=6144B plus tail) and taskqgroup_cpu_create() then stores a fresh gtaskqueue heap pointer + int + LIST_INIT NULL into tqg_queue[i] for every i < cnt with no bound check. With cnt=MAXCPU+16 the readback probe shows all 11 slots at offsets 6264..6527 (provably past the allocation, which is M_ZERO) containing non-NULL kernel pointers (0xfffff8008bb07198 etc), plus intra-object clobbering of tqg_lock/tqg_name (slot[256] tgc_cpu=-2134490742, thread names garbage beyond 257). cnt is an unvalidated caller int, so the overwrite distance/size is caller-chosen (smash variant: ~48KB). No panic was coaxed in the observation window (DFly slab has no redzones; corruption is silent), hence impact recorded as dos-class (heap-corruption instability) rather than a demonstrated crash. No unprivileged trigger exists: the API has zero in-tree consumers beyond the ncpus-bounded softirq SYSINIT, so reaching it requires a root-loaded KLD. Same latent-API-hazard family as DF-0085 (not re-reported).