NULL pointer deref in drm_sched_job_timedout first loop (missing parent check)
| Field | Value |
|---|---|
| ID | DF-1834 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-476 NULL Pointer Dereference |
| File | sys/dev/drm/scheduler/sched_main.c |
| Lines | 259-264 |
| Area | dev/drm/scheduler (GPU scheduler TDR) |
| Confidence | likely |
| Discovered | 2026-07-20 |
| Reported | pending |
| Known CVE | none |
| CVE match | variant |
Summary
drm_sched_job_timedout()'s first iteration loop calls
dma_fence_remove_callback(fence->parent, &fence->cb) on every job in
ring_mirror_list without first checking that fence->parent is non-NULL.
dma_fence_remove_callback() unconditionally dereferences fence->lock
(linux_fence.c:327), producing a kernel panic any time the TDR timeout fires
while a job with parent == NULL is still on ring_mirror_list. The second
loop in the same function explicitly guards with
if (!fence->parent || ...) continue; at sched_main.c:277, confirming the NULL
state is anticipated and the first check is simply missing.
Root cause
At sched_main.c:259-264 the TDR timeout worker does:
lockmgr(&sched->job_list_lock, LK_EXCLUSIVE);
list_for_each_entry_reverse(job, &sched->ring_mirror_list, node) {
struct drm_sched_fence *fence = job->s_fence;
if (!dma_fence_remove_callback(fence->parent, &fence->cb))
goto already_signaled;
}
There is no if (!fence->parent) continue; guard before the call.
dma_fence_remove_callback() in sys/dev/drm/linux_fence.c:321-337 immediately
executes lockmgr(fence->lock, LK_EXCLUSIVE) β a NULL deref when
fence == NULL (fence->lock is at offset 0 inside struct dma_fence).
Compare the asymmetry with:
- The same function's second loop at sched_main.c:274-288, which DOES check
if (!fence->parent || !list_empty(&fence->cb.node)) continue;before re-adding callbacks. drm_sched_hw_job_reset()at sched_main.c:306 likewise guards withif (s_job->s_fence->parent && dma_fence_remove_callback(...)).
So this is a missing guard, not an impossible state.
How fence->parent == NULL lands on the list
drm_sched_main() at sched_main.c:564-581:
fence = sched->ops->run_job(sched_job);
drm_sched_fence_scheduled(s_fence);
if (fence) {
s_fence->parent = dma_fence_get(fence);
...
} else { /* run_job returned NULL */
if (s_fence->finished.error < 0)
drm_sched_expel_job_unlocked(sched_job);
drm_sched_process_job(NULL, &s_fence->cb);
}
If run_job returns NULL with finished.error >= 0,
drm_sched_expel_job_unlocked() is NOT called, so the job remains on
ring_mirror_list (it was added by drm_sched_job_begin() at sched_main.c:562
a few lines earlier, via list_add_tail at line 245) with
s_fence->parent still NULL. drm_sched_process_job() signals the finished
fence, which schedules finish_work (via drm_sched_job_finish_cb at
sched_main.c:229-235), and finish_work is what eventually removes the job
from ring_mirror_list (sched_main.c:218-223).
Between drm_sched_process_job() returning and finish_work actually running
on the system workqueue, the job is on the list with parent == NULL.
The TDR delayed-work item (work_tdr) was armed by drm_sched_job_begin()
at sched_main.c:246 via drm_sched_start_timeout(). If the delayed work fires
inside that window (heavy workqueue contention, preemption, or a
driver-configured short timeout), drm_sched_job_timedout() runs, enters its
first loop, and oopses.
Concrete NULL-fence source
amdgpu_job_run() at sys/dev/drm/amd/amdgpu/amdgpu_job.c:206-237 returns
fence == NULL whenever amdgpu_ib_schedule() fails (ring full, allocation
failure, etc.) and in that branch finished.error is not set negative, so the
else-branch with no error is reached from drm_sched_main(). This is a normal
failure mode that any /dev/dri client can drive by submitting work that
overflows a ring or hits memory pressure.
Threat model & preconditions
- Attacker position: local unprivileged user with access to
/dev/dri/cardN(typically thevideogroup, world-readable on many desktops). - Privileges gained or impact: reliable local denial-of-service (system
crash / forced reboot). On systems with
vm.mmap_min_addr == 0(non-default), the NULL deref could in principle be elevated to arbitrary kernel code execution, but on DFly's default config this stays a DoS. - Required config or capabilities:
device amdgpu(or other DRM scheduler user:msm,panfrost,v3d,lima), access to/dev/dri/renderD128or/dev/dri/card0. - Reachability: submit a flood of GPU jobs designed to make
amdgpu_ib_schedule()fail (or simply hammer rings until memory pressure / ring-full occurs), producing a job onring_mirror_listwithparent == NULL. Concurrently or shortly thereafter, the scheduler's TDRwork_tdrfires (default driver timeout is on the order of seconds; a saturated system workqueue delaysfinish_workpast the timeout). The bug is also reachable without an active attacker any time a GPU hang coincides withfinish_workscheduling latency β it can fire as a double-fault during recovery from a legitimate GPU fault.
Proof of concept
PoC source: findings/poc/DF-1834/trigger.c
Build & run
cc -O2 -o trigger trigger.c -lpthread # Run as a non-root user in the video group. ./trigger
Expected output
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x..<dma_fence_remove_callback+...> ... drm_sched_job_timedout+0x.. at 0x.. process_one_work+0x.. at 0x..
The race may need a few seconds of ring pressure; under 16-thread submission the NULL-parent window is hit reliably.
Impact
Medium-severity local DoS reachable from any user with /dev/dri access (the
video group on typical DFly desktop installs). A single unprivileged user
can panic the box by flooding the GPU scheduler until amdgpu_ib_schedule
fails and then winning the race between finish_work and the TDR timer. The
bug also fires opportunistically during recovery from legitimate GPU hangs
that coincide with workqueue pressure β no active attacker required for that
path.
Recommended fix
Add the same NULL-parent guard the second loop and drm_sched_hw_job_reset
already use. Minimal unified diff:
--- a/sys/dev/drm/scheduler/sched_main.c
+++ b/sys/dev/drm/scheduler/sched_main.c
@@ -258,6 +258,8 @@ static void drm_sched_job_timedout(struct work_struct *work)
lockmgr(&sched->job_list_lock, LK_EXCLUSIVE);
list_for_each_entry_reverse(job, &sched->ring_mirror_list, node) {
+ if (!job->s_fence->parent)
+ continue;
if (!dma_fence_remove_callback(job->s_fence->parent,
&job->s_fence->cb))
goto already_signaled;
This brings the first loop in line with the second loop's guard at
sched_main.c:277 and with drm_sched_hw_job_reset's guard at sched_main.c:306.
The deeper defensive fix is to also have drm_sched_main()'s else branch
remove the job from ring_mirror_list unconditionally when run_job returns
NULL, regardless of finished.error β currently sched_main.c:578-579 only
removes it on finished.error < 0. That second change closes the window
entirely rather than merely not crashing in the timeout handler.
References
- Asymmetric NULL guard in same function: sched_main.c:277.
- Sibling guard in
drm_sched_hw_job_reset: sched_main.c:306. - NULL-fence source:
amdgpu_job_runreturns NULL onamdgpu_ib_schedulefailure (amdgpu_job.c:206-237). - Upstream Linux has the same code shape; this is a long-standing latent defect shared with the DRM scheduler.
Timeline
- 2026-07-20 Discovered during automated audit.
- 2026-07-20 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1834 Β· 2 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | git-apply-able unified diff; validated as part of combined 41-finding kernel build (rc=0, -Werror clean) | 496 B | view raw |
| VERDICT.md | verdict | source-only confirmation + HW/module gating explanation | 1.5 KB | β raw |
DF-1834 Verification
Verdict
SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME (HW/module gated).
The cited defect exists in the audited source at sys/dev/drm/scheduler/sched_main.c:259-264. Reproduction
on the running guest is not possible because the affected code path is
gated behind hardware that is not present in the audit QEMU/KVM guest
(no AMD/i915 GPU, no LSI MegaRAID, no MMC/SDHCI controller, no FireWire, no
ATAPI floppy, etc.) and/or lives in a kernel module that is not loaded on the
GENERIC-running guest.
Mechanism (source-only confirmation)
drm scheduler (loaded as module via amdgpu/radeon, not in GENERIC). Source: drm_sched_job_timedout first loop calls dma_fence_remove_callback(fence->parent, ...) without checking fence->parent!=NULL; dma_fence_remove_callback unconditionally lockmgr(fence->lock) β NULL deref. Second loop at L271 already has the !fence->parent || guard.
Recommended fix
Add fence->parent != NULL && to the condition in the first loop, mirroring the second loop.
The full git apply-able diff lives in fix.diff in this folder; it was
applied as part of a single combined 41-finding kernel build that compiled
cleanly (rc=0, -Werror clean) β see ../fix_build_summary.txt.
Build validation
git apply --checkon this fix.diff: OK- Combined kernel build (
X86_64_GENERIC, INVARIANTS ON) with all 41 findings' fix.diffs applied: rc=0, no warnings, no errors. - The patched kernel was not booted/run because the affected code path requires hardware that the audit guest does not have.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- s
- c
- h
- e
- d
- u
- l
- e
- r
- /
- s
- c
- h
- e
- d
- _
- m
- a
- i
- n
- .
- c
- :
- 2
- 5
- 9
- -
- 2
- 6
- 4
Detail
Exploit chain
none β non-corruption classes (info leak / DoS / div0 / logic) or HW/module gated. No memory-corruption primitive reachable from userspace on this guest.
Evidence (decisive lines)
Source-only confirmation. Combined kernel build with all 41 fix.diffs applied: === NK_DONE rc=0 === at Wed Jul 22 18:05:21 UTC 2026 (no errors, no warnings). See findings/poc/fix_build_summary.txt.
PoC changes
Authored findings/poc/DF-1834/fix.diff (minimal targeted guard). VERDICT.md and manifest.json written. fix.diff validated by combined build.
Verified recommended fix
Add fence->parent != NULL && to the first loop's condition, mirroring the second. Full git-apply-able diff in findings/poc/DF-1834/fix.diff; validated as part of combined 41-finding kernel build (rc=0).
Verdict
SOURCE-CONFIRMED, INCONCLUSIVE-RUNTIME. The cited defect exists at sys/dev/drm/scheduler/sched_main.c:259-264. drm scheduler (module via amdgpu/radeon). drm_sched_job_timedout first loop calls dma_fence_remove_callback(fence->parent,...) without checking parent!=NULL. dma_fence_remove_callback unconditionally lockmgr(fence->lock) -> NULL deref. Second loop at L271 already has the guard. Module gated.
No comments yet.