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

Duplicate BO_HANDLES/FENCE/SYNCOBJ_OUT chunks overwrite parser state, permanently leaking bo_list + all BO refs, user-fence BO refs, and syncobj refs

Summary

The chunk loop never rejects a second stateful chunk: a fresh bo_list overwrites p->bo_list, the fence chunk overwrites uf_entry.tv.bo, syncobj-out overwrites the array. parser_fini releases each exactly once, so the first objects leak forever - an unbounded kernel-memory/GTT exhaustion primitive (N attacker-chosen BO refs per ioctl via repeated handles). Reject-duplicates fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4263 Β· 12 files
FileTypeDescriptionSize
README.md β€” 2.7 KB ↓ raw
VERDICT.md β€” 3.7 KB ↓ raw
dup_chunk_leak.c β€” 5.8 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 β€” 839 B view raw
verdict.json β€” 3.5 KB view raw

DF-4263 β€” duplicate CS chunks (BO_HANDLES / FENCE / SYNCOBJ_OUT) leak kernel refs

File: sys/dev/drm/amd/amdgpu/amdgpu_cs.c (pass 2) Severity: Medium (unprivileged local resource-exhaustion DoS) β€” CWE-401 Confidence: certain (static proof; leak arithmetic exact)

Build

cc -O2 -Wall -o dup_chunk_leak dup_chunk_leak.c

Run (as any user on amdgpu hardware)

./dup_chunk_leak

Creates one 1 MiB GTT BO, then submits 1000 CS ioctls whose chunk array holds two AMDGPU_CHUNK_ID_BO_HANDLES chunks, each listing the same handle 64 times. Every CS permanently leaks 64 amdgpu_bo references plus one whole amdgpu_bo_list (kref 1, never dropped). Repeat (or raise NENTRY/ROUNDS) until GTT/kernel-memory exhaustion.

Root cause (path:line)

  • The chunk-dispatch loop amdgpu_cs_parser_init() (amdgpu_cs.c:151-222) never rejects a duplicate stateful chunk:
  • BO_HANDLES (:200-211) β†’ amdgpu_cs_bo_handles_chunk() (:78-101) β†’ amdgpu_bo_list_create(..., &p->bo_list) assigns a new list (kref 1, holds one amdgpu_bo ref per entry β€” amdgpu_bo_list.c:83,103) over p->bo_list without dropping the previous one. amdgpu_cs_parser_fini() puts it exactly once (amdgpu_cs.c:807-808). First list + all its BO refs leak forever.
  • FENCE (:186-198) β†’ amdgpu_cs_user_fence_chunk() (:37-76) does amdgpu_bo_ref() then overwrites p->uf_entry.tv.bo (:52); fini unrefs once (:815-819) β†’ one BO ref leaked per extra FENCE chunk.
  • SYNCOBJ_OUT β€” amdgpu_cs_process_syncobj_out_dep() (:1138-1163) overwrites p->post_dep_syncobjs and resets num_post_dep_syncobjs = 0 (:1148-1151), leaking the previous array and every drm_syncobj reference it holds (drm_syncobj_find takes a ref β€” drm_syncobj.c:83-94); fini releases only the last array (:797-799).
  • The ioctl-level guard that exists (amdgpu_cs.c:587-589) only rejects bo_list_handle+chunk, not chunk+chunk.

Reachability: AMDGPU_CS is DRM_AUTH|DRM_RENDER_ALLOW (`amdgpu_kms.c:1206). All chunk types need only well-formed β‰₯min-size kdata (FENCE β‰₯8B, BO_HANDLES β‰₯24B, SYNCOBJ_OUT β‰₯4B β€” enforced at :188/:202 and by floor-division at :1145-1146).

Guest verification (this audit)

Same hardware gate as DF-4262 (no AMD GPU β†’ no /dev/dri): trigger builds (build.log), run exits at open (run.log); baseline + patched in-tree compile of amdgpu_cs.c both rc=0 with -Werror (kmod_baseline_build.log, fix_build.log).

Fix

fix.diff: reject duplicates β€” -EINVAL when p->uf_entry.tv.bo is already set (FENCE), when p->bo_list is already set (BO_HANDLES), and when p->post_dep_syncobjs is already allocated (SYNCOBJ_OUT).

VERDICT.md
↓ download raw

DF-4263 evidence pack β€” verdict

Reproduced? NOT at runtime β€” hardware-gated (no AMD GPU on the audit guest, /dev/dri absent β€” env.txt/run.log). Defect: PROVEN at code level (certain): permanent, unbounded leak of kernel objects + BO references per CS ioctl, by an unprivileged render-node client.

Proof chain

  1. BO_HANDLES (the heavyweight variant): a. amdgpu_cs_parser_init() loop (amdgpu_cs.c:151-222) dispatches every BO_HANDLES chunk (:200-211) to amdgpu_cs_bo_handles_chunk() with no duplicate guard. b. amdgpu_cs_bo_handles_chunk() (:78-101) calls amdgpu_bo_list_create(p->adev, p->filp, info, data->bo_number, &p->bo_list) β€” the list is stored via the out-param, replacing any previous value of p->bo_list without dropping it. c. amdgpu_bo_list_create() (amdgpu_bo_list.c:61-151) returns the list with kref_init = 1 (:83) and holds one amdgpu_bo_ref() per entry (:103). The only put that ever runs for it is amdgpu_cs_parser_fini() (amdgpu_cs.c:807-808, amdgpu_bo_list_put) β€” which fires exactly once, for the LAST list. The first list object (kmalloc'd, sizeof(list) + n*sizeof(entry)) and every BO reference it holds leak permanently. d. Rate: bo_number is user-chosen (bounded only by the kvmalloc in amdgpu_bo_list.c:226); the same GEM handle may appear many times. Each CS ioctl therefore leaks an attacker-chosen number of BO references (each BO β‰₯ PAGE_SIZE of GTT/VRAM + kernel structs) plus one list. GEM handles are created once and reused across CS calls, so N CS ioctls Γ— M entries = NΓ—M leaked refs with only M live BOs β€” unbounded exhaustion without allocating more GPU memory.
  2. FENCE variant: amdgpu_cs_user_fence_chunk() (:37-76) takes amdgpu_bo_ref() (:50) and assigns p->uf_entry.tv.bo = &bo->tbo (:52). A second FENCE chunk overwrites the pointer; fini unrefs once (:815-819). Net +1 BO ref per extra FENCE chunk.
  3. SYNCOBJ_OUT variant: amdgpu_cs_process_syncobj_out_dep() (:1138-1163) allocates a fresh p->post_dep_syncobjs array (:1148) and resets p->num_post_dep_syncobjs = 0 (:1151). A second SYNCOBJ_OUT chunk (the whole amdgpu_cs_dependencies() loop :1170-1188 processes every chunk) orphans the first array together with every drm_syncobj reference taken by drm_syncobj_find() (drm_syncobj.c:83-94 β€” the reference must be released with drm_syncobj_put; fini only puts the last array, amdgpu_cs.c:797-799).
  4. The only existing duplicate guard (amdgpu_cs.c:587-589) rejects ioctl-arg bo_list_handle combined with a BO_HANDLES chunk β€” it does NOT protect chunk-vs-chunk duplication, which is what the PoC uses.

Guest verification performed

  • trigger compiles (build.log); runs to the hardware gate (run.log)
  • baseline amdgpu_cs.c in-tree compile rc=0 with -Werror (kmod_baseline_build.log)
  • stacked fix (DF-4262+4263+4264+4265 diffs) applied via patch -p1 (hunks at 190/209/1155) and recompiled rc=0 with -Werror (fix_build.log) β€” fix compiles; runtime leak measurement needs HW.

Exploit chain: memory-exhaustion DoS only (no memory-safety violation: the leaked objects stay valid, they are simply never freed).

Recommended fix: reject duplicate stateful chunks (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 (three duplicate-rejection guards) applied on the guest stacked with the other amdgpu_cs.c fixes (hunks @190/@209/@1155) and compiled in-tree with -Werror rc=0 (fix_build.log). Runtime leak-measurement before/after impossible (no AMD GPU); compile-only validation.

fix_build.log rc=0; guest patch output 'Hunk #1 succeeded at 190 / #2 at 209 / #3 at 1155'.
↓ fix.diffper-fix-DF-4263

Confirmed kernel references

Detail

Exploit chain

Resource-exhaustion DoS only - leaked objects remain valid, are simply never freed; no memory-safety violation.

Evidence (decisive lines)

dup_chunk_leak.c (two BO_HANDLES chunks x 64 duplicate handles x 1000 rounds); run.log (hardware gate); kmod_baseline_build.log + fix_build.log (in-tree -Werror compile before/after the duplicate-rejection guards).

PoC changes

Trigger written from scratch: embeds the 24-byte drm_amdgpu_bo_list_in as chunk kdata (>=6 dwords passes the :202 size check), entries supplied via bo_info_ptr user pointer, plus one minimal valid IB chunk so the CS survives ib_fill.

Verified recommended fix

Reject duplicate stateful chunks: -EINVAL when p->uf_entry.tv.bo (FENCE), p->bo_list (BO_HANDLES) or p->post_dep_syncobjs (SYNCOBJ_OUT) is already set.

Verdict

Code-certain unprivileged kernel reference leak family: duplicate AMDGPU_CHUNK_ID_BO_HANDLES / _FENCE / _SYNCOBJ_OUT chunks in one CS ioctl overwrite parser state (p->bo_list at amdgpu_cs.c:88-89; p->uf_entry.tv.bo at :52; p->post_dep_syncobjs at :1148-1151) without dropping the previous objects; amdgpu_cs_parser_fini() releases each exactly once (:797-799, :807-808, :815-819), so the first bo_list plus every amdgpu_bo ref it holds (amdgpu_bo_list.c:83,103), the first user-fence bo ref, and the first syncobj array + all its drm_syncobj refs leak permanently. Each CS can leak an attacker-chosen number of BO refs (same GEM handle repeated in the entries array), giving unbounded kernel-memory/GTT exhaustion by an unprivileged render-node client. Runtime reproduction hardware-gated on this guest (no AMD GPU, /dev/dri absent), hence untested; the leak is fully proven at code level.