CS ioctl with num_chunks==0 returns early success, amdgpu_cs_ib_fill dereferences NULL parser->entity β unprivileged kernel panic
Summary
amdgpu_cs_parser_init returns 0 for num_chunks==0 before ctx/job/chunk processing, leaving the parser zero-initialized; amdgpu_cs_ib_fill's loop is short-circuited (0<0) and the line loads parser->entity->rq at address 0x10 -> fatal trap. ONE ioctl from an unprivileged render-node user; no objects or context needed. Hardware-gated on this guest; defect proven by straight-line code trace with no device-dependent branch. One-token -EINVAL fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4262 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 3.5 KB | β raw | |
| cs_zero_chunks.c | β | 3.1 KB | view raw | |
| build.sh | β | 65 B | view raw | |
| run.sh | β | 89 B | view raw | |
| build.log | β | 782 B | view raw | |
| run.log | β | 869 B | view raw | |
| env.txt | β | 516 B | view raw | |
| kmod_baseline_build.log | β | 4.0 KB | view raw | |
| fix_build.log | β | 4.0 KB | view raw | |
| fix.diff | β | 279 B | view raw | |
| verdict.json | β | 3.6 KB | view raw |
DF-4262 β amdgpu CS ioctl with num_chunks == 0 β NULL-deref kernel panic
File: sys/dev/drm/amd/amdgpu/amdgpu_cs.c (pass 2)
Severity: Medium (unprivileged local DoS/panic) β CWE-476
Confidence: certain (static proof, line-cited)
Build
cc -O2 -Wall -o cs_zero_chunks cs_zero_chunks.c
(self-contained: no drm headers needed β guest /usr/include has none)
Run (as any user, on a system with amdgpu hardware)
./cs_zero_chunks # opens /dev/dri/renderD128 (or card0), issues
# DRM_IOCTL_AMDGPU_CS with in.num_chunks = 0
Expected result on amdgpu hardware
Immediate kernel panic (fatal trap 12, page fault in kernel mode, read of
parser->entity->rq β address 0x10) inside the ioctl β the machine goes
down before the syscall returns.
What was verified on THIS audit's QEMU guest
The guest has no AMD GPU (vgapci0 ... chip=0x11111234 = QEMU VGA;
amdgpu.ko loads but binds nothing, /dev/dri does not exist), so the
ioctl path cannot be entered at runtime:
- trigger builds on the guest:
build.log(cc 8.3, all four DF-4262..65 triggers compile) - trigger run as unprivileged
maxx:run.log(open fails with ENOENT β hardware gate documented) env.txt: uname, pciconf, kldstat evidence- in-tree kernel compile of the vulnerable file (baseline) and of the
fixed file:
kmod_baseline_build.log/fix_build.log(make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC amdgpu_cs.o,-Werror, both rc=0)
Root cause (path:line)
amdgpu_cs.c:114-115βamdgpu_cs_parser_init()returns early success (return 0) whencs->in.num_chunks == 0, beforeamdgpu_ctx_get()(121), before the chunk loop (151), and beforeamdgpu_job_alloc()(224) β the only place that would have rejectednum_ibs == 0(amdgpu_job.c:55-56).amdgpu_cs.c:1280βstruct amdgpu_cs_parser parser = {};soparser.job == NULL,parser.entity == NULL,parser.chunks == NULL.amdgpu_cs.c:1296βamdgpu_cs_ib_fill(): - loop atamdgpu_cs.c:991(i < parser->nchunks && ...) is skipped becausenchunks == 0(&&short-circuits before touchingparser->job); -amdgpu_cs.c:1048βring = to_amdgpu_ring(parser->entity->rq->sched);dereferencesparser->entity == NULL.rqsits at offset 16 ofstruct drm_sched_entity(sys/dev/drm/include/drm/gpu_scheduler.h:81) β kernel load from 0x10 β fatal trap β panic.
Reachability: AMDGPU_CS is DRM_AUTH|DRM_RENDER_ALLOW
(amdgpu_kms.c:1206) β any local user with /dev/dri/renderD*.
Fix
fix.diff (reject zero-chunk submissions): return -EINVAL at the
num_chunks == 0 check. Nothing has been allocated at that point, and
amdgpu_cs_parser_fini() already tolerates the all-NULL parser.
DF-4262 evidence pack β verdict
Reproduced? NOT at runtime β hardware-gated on the audit guest. Defect: PROVEN at code level (certain), full line-cited chain below.
Chain of proof
amdgpu_cs_ioctl()(sys/dev/drm/amd/amdgpu/amdgpu_cs.c:1276) zero-initializes the parser at :1280 (struct amdgpu_cs_parser parser = {};) and callsamdgpu_cs_parser_init(&parser, data)at :1290.amdgpu_cs_parser_init()at :114-115: if (cs->in.num_chunks == 0) return 0; This returns SUCCESS before any of:amdgpu_ctx_get()(:121),chunk_arrayalloc (:117), chunk parse loop (:151),amdgpu_job_alloc()(:224 β the onlynum_ibs == 0rejection, amdgpu_job.c:55-56). So on this path:parser.ctx == NULL,parser.job == NULL,parser.entity == NULL,parser.nchunks == 0.amdgpu_cs_ib_fill()(:1296 β :982): - the fill loop :991for (i = 0, j = 0; i < parser->nchunks && j < parser->job->num_ibs; i++)is skipped (0 < 0false;&&short-circuits so the NULLparser->jobis never touched here); - :1048ring = to_amdgpu_ring(parser->entity->rq->sched);dereferencesparser->entity == NULL.rqis the second member ofstruct drm_sched_entity(offset 16 β sys/dev/drm/include/drm/gpu_scheduler.h:81), i.e. the kernel loads from address 0x10 in kernel mode β fatal trap 12 β panic.- Reachability: AMDGPU_CS is DRM_AUTH|DRM_RENDER_ALLOW (sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1206). An unprivileged user with /dev/dri/renderD* triggers it with a single ioctl; no GEM objects, no context, no chunks are needed (num_chunks=0 skips even ctx lookup).
Why not reproduced at runtime
The audit guest is QEMU with a QEMU/Boches VGA (chip 0x11111234, vendor 0x1234 β env.txt). amdgpu.ko exists on the guest and even loads (kldload succeeded; it registers and binds nothing), but with no AMD PCI device there is no drm device instance and /dev/dri does not exist β the trigger's open() fails with ENOENT (run.log). The ioctl cannot be entered on this machine; this is a hardware gate, not a code-path dead-end: every line of the chain above is straight-line code with no device-dependent branch between :114 and :1048.
Verification actually performed on the guest
- trigger compiles (cc 8.3) β build.log
- trigger run as unprivileged user maxx β run.log (absent device)
- baseline in-tree kernel compile of amdgpu_cs.c with -Werror:
make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC amdgpu_cs.orc=0 (kmod_baseline_build.log) - fix.diff applied to the guest /usr/src copy (patch -p1, hunk at 112) and recompiled with -Werror rc=0 (fix_build.log) β the fix compiles cleanly; runtime before/after comparison is impossible without AMD HW.
Exploit chain: N/A β the class is a NULL-deref panic (local DoS). The faulting address (0x10) is in the unmapped null page; on DragonFly the null page cannot be mapped by user processes (VM_MIN_ADDRESS), so this is not convertible to code execution.
Recommended fix: reject num_chunks == 0 with -EINVAL (fix.diff).
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff (one-line -EINVAL) applied to guest /usr/src via patch -p1 (hunk @112) and the patched amdgpu_cs.c compiled in-tree with -Werror rc=0 (fix_build.log). Runtime before/after comparison impossible on this guest (no AMD GPU, baseline PoC cannot execute), so fix validation is compile-only; the change is provably sufficient by inspection: the early return is the sole path that leaves parser->entity NULL into amdgpu_cs_ib_fill().
fix_build.log (patched compile rc=0); kmod_baseline_build.log (unpatched compile rc=0); guest patch output in task log: 'Hunk #1 succeeded at 112'.
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:114
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:1280
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:1296
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:991
- sys/dev/drm/amd/amdgpu/amdgpu_cs.c:1048
- sys/dev/drm/amd/amdgpu/amdgpu_job.c:55
- sys/dev/drm/include/drm/gpu_scheduler.h:81
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1206
Detail
Exploit chain
N/A - NULL-page deref (offset 0x10) is not mappable by userland on DragonFly; impact ceiling is local DoS/panic.
Evidence (decisive lines)
cs_zero_chunks.c (self-contained trigger); run.log (open /dev/dri fails ENOENT - absent HW); env.txt (pciconf/kldstat); kmod_baseline_build.log + fix_build.log (amdgpu_cs.c compiles rc=0 with -Werror before and after fix.diff); VERDICT.md has the full line-cited chain.
PoC changes
Trigger written from scratch (no seed): self-contained UAPI structs because the guest has no drm user headers; ioctl nr computed via _IOWR('d', 0x40+0x04, 24-byte union).
Verified recommended fix
Reject CS submissions with zero chunks: return -EINVAL at amdgpu_cs.c:114 instead of early success.
Verdict
Code-certain unprivileged kernel NULL-deref panic: amdgpu_cs_parser_init() returns early SUCCESS for num_chunks==0 (amdgpu_cs.c:114-115) before job/ctx/entity are set, so amdgpu_cs_ib_fill() falls straight through its skipped loop to to_amdgpu_ring(parser->entity->rq->sched) (amdgpu_cs.c:1048) with parser->entity==NULL (zero-init at amdgpu_cs.c:1280) -> kernel load from 0x10 -> fatal trap. AMDGPU_CS is DRM_AUTH|DRM_RENDER_ALLOW (amdgpu_kms.c:1206) so any local render-node user triggers it with one ioctl. Runtime reproduction on this audit guest is impossible: QEMU has no AMD GPU (vgapci chip 0x11111234; amdgpu.ko loads but binds nothing; /dev/dri absent - env.txt/run.log), so status=untested (hardware-gated, not a code dead-end: no device-dependent branch between :114 and :1048). Fix compiles in-tree with -Werror.
No comments yet.