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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4262 Β· 12 files
FileTypeDescriptionSize
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)

  1. amdgpu_cs.c:114-115 β€” amdgpu_cs_parser_init() returns early success (return 0) when cs->in.num_chunks == 0, before amdgpu_ctx_get() (121), before the chunk loop (151), and before amdgpu_job_alloc() (224) β€” the only place that would have rejected num_ibs == 0 (amdgpu_job.c:55-56).
  2. amdgpu_cs.c:1280 β€” struct amdgpu_cs_parser parser = {}; so parser.job == NULL, parser.entity == NULL, parser.chunks == NULL.
  3. amdgpu_cs.c:1296 β†’ amdgpu_cs_ib_fill(): - loop at amdgpu_cs.c:991 (i < parser->nchunks && ...) is skipped because nchunks == 0 (&& short-circuits before touching parser->job); - amdgpu_cs.c:1048 β€” ring = to_amdgpu_ring(parser->entity->rq->sched); dereferences parser->entity == NULL. rq sits at offset 16 of struct 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.

VERDICT.md
↓ download raw

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

  1. amdgpu_cs_ioctl() (sys/dev/drm/amd/amdgpu/amdgpu_cs.c:1276) zero-initializes the parser at :1280 (struct amdgpu_cs_parser parser = {};) and calls amdgpu_cs_parser_init(&parser, data) at :1290.
  2. 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_array alloc (:117), chunk parse loop (:151), amdgpu_job_alloc() (:224 β€” the only num_ibs == 0 rejection, amdgpu_job.c:55-56). So on this path: parser.ctx == NULL, parser.job == NULL, parser.entity == NULL, parser.nchunks == 0.
  3. amdgpu_cs_ib_fill() (:1296 β†’ :982): - the fill loop :991 for (i = 0, j = 0; i < parser->nchunks && j < parser->job->num_ibs; i++) is skipped (0 < 0 false; && short-circuits so the NULL parser->job is never touched here); - :1048 ring = to_amdgpu_ring(parser->entity->rq->sched); dereferences parser->entity == NULL. rq is the second member of struct 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.
  4. 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.o rc=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_testable
baseline no→ patch + rebuild →patched clean

fix.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'.
↓ fix.diffper-fix-DF-4262

Confirmed kernel references

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.