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

amdgpu_bo_placement_from_domain walks up to 6 domain entries into placements[AMDGPU_BO_MAX_PLACEMENTS=3] and BUG_ONs: unprivileged GEM_CREATE with domains=VRAM|GTT|CPU panics the kernel; domains=0x3f writes places[3..5] OOB (over bo->placement/bo->tbo) before the panic

Summary

The placement walk appends one ttm_place per set domain bit into placements[3], storing BEFORE checking BUG_ON(c >= MAX). c==3 (VRAM|GTT|CPU=0x7) is in-bounds yet asserts - panic. c>=4 (any GDS/GWS/OA bit mixed with a standard domain, mask 0x3f accepted at gem.c) writes 12-36 bytes of constants past the array into bo->placement and the head of bo->tbo before the same panic. BUG_ON maps to unconditional panic() in this port, so the corruption never survives - impact ceiling is a reliable unprivileged local DoS (GEM_CREATE is DRM_AUTH|RENDER_ALLOW). Survives in the wild only because Mesa never sends >=3 domain bits. Guard-the-store + relax-the-assert fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4370 Β· 12 files
FileTypeDescriptionSize
README.md β€” 1.9 KB ↓ raw
VERDICT.md β€” 6.6 KB ↓ raw
poc_df4370.c β€” 4.7 KB view raw
build.sh β€” 98 B view raw
run.sh β€” 144 B view raw
build.log β€” 31 B view raw
run.log β€” 22 B view raw
env.txt β€” 378 B view raw
fix.diff β€” 3.7 KB view raw
fix_notes.md β€” 862 B ↓ raw
manifest.json β€” 1.5 KB view raw
verdict.json β€” 3.8 KB view raw

DF-4370 β€” amdgpu placement-array overflow + BUG_ON panic from GEM_CREATE domain mask

  • File: sys/dev/drm/amd/amdgpu/amdgpu_object.c (:139-209, walk), amdgpu_object.h:35 (AMDGPU_BO_MAX_PLACEMENTS 3), reached from amdgpu_gem.c:233,260 and amdgpu_gem.c:716-730
  • Severity: Medium (unprivileged local kernel panic / DoS on amdgpu systems; 12-36 byte constant OOB write inside the amdgpu_bo allocation preceding the panic)
  • Class: CWE-787 / CWE-129 (write past array bound inside heap object) + CWE-757 (BUG() reachable from unprivileged ioctl)

Build & run (guest)

cc -O2 -o /tmp/poc_df4370 /tmp/poc_df4370.c     # build.sh
/tmp/poc_df4370                                  # run.sh  (unprivileged user)

Expected on a vulnerable amdgpu machine: * driver amdgpu v... then the FIRST AMDGPU_IOC_GEM_CREATE (domains=0x7) never returns β€” the console shows panic: BUG in amdgpu_bo_placement_from_domain at amdgpu_object.c:209.

Expected on this audit VM (no GPU): NO_DRM_DEVICES, exit 2 (observed).

Trigger summary

  1. AMDGPU_GEM_CREATE with in.domains = VRAM|GTT|CPU (0x7) β†’ amdgpu_bo_placement_from_domain() appends 3 entries (in-bounds) and then BUG_ON(c >= 3) fires with c==3 β†’ panic.
  2. in.domains = 0x3f (all six domains; mask accepted at amdgpu_gem.c:233) β†’ the walk writes places[3..5] β€” past the 3-element array, over bo->placement and the head of bo->tbo β€” before the same BUG_ON panics.
  3. Secondary path: AMDGPU_GEM_OP SET_PLACEMENT with value 0x7 sets bo->preferred_domains = 0x7 (amdgpu_gem.c:727); the next CS/eviction amdgpu_bo_validate() β†’ same walk β†’ same panic.

Artifacts

  • poc_df4370.c β€” self-contained trigger (no DRM headers required)
  • build.log, run.log, env.txt β€” guest evidence
  • fix.diff β€” bound the walk (if (c < AMDGPU_BO_MAX_PLACEMENTS) guards + BUG_ON(c > ...)), applies cleanly with patch -p1
  • VERDICT.md β€” full analysis
VERDICT.md
↓ download raw

DF-4370 VERDICT β€” amdgpu placement walk overflow / BUG_ON panic

Bottom line

Certain by static proof; not reproduced at runtime because the audit VM has no GPU (no /dev/dri, only QEMU virtio VGA). The PoC builds and runs on the guest and exits NO_DRM_DEVICES (2). On any machine with an amdgpu device node, the first GEM_CREATE with domains=0x7 provably panics the kernel.

Reproduction status

item result
build on guest (DragonFly 6.5-DEVELOPMENT x86_64, gcc) BUILD_OK
run as uid=1001 NO_DRM_DEVICES, exit 2
/dev/dri does not exist on this VM
pciconf only vgapci0@pci0:0:2:0 (virtio-gpu 0x1111:1234), no AMD device
kldstat drm modules none loaded

Status: not_reproduced β€” target device absent (impact none on this guest). Real-world impact: unprivileged kernel panic (local DoS) on any system exposing an amdgpu render/card node, plus a deterministic 12-36 byte constant-data write past placements[3] inside the same heap object before the panic.

The bug, line by line

  1. sys/dev/drm/amd/amdgpu/amdgpu_object.h:35 #define AMDGPU_BO_MAX_PLACEMENTS 3 and :81 struct ttm_place placements[3]; inside struct amdgpu_bo.

  2. sys/dev/drm/amd/amdgpu/amdgpu_object.c:131-216 amdgpu_bo_placement_from_domain(abo, domain) appends one placement per set domain bit among VRAM/GTT/CPU/GDS/GWS/OA (up to 6), writing places[c].fpfn/.lpfn/.flags first and checking the bound after:

c if (domain & AMDGPU_GEM_DOMAIN_OA) { places[c].fpfn = 0; /* <-- write happens here */ places[c].lpfn = 0; places[c].flags = TTM_PL_FLAG_UNCACHED | AMDGPU_PL_FLAG_OA; c++; } ... BUG_ON(c >= AMDGPU_BO_MAX_PLACEMENTS); /* amdgpu_object.c:209 */

  • c == 3 (e.g. VRAM|GTT|CPU): three in-bounds writes, then BUG_ON(3 >= 3) is TRUE β†’ panic("BUG in ...") (sys/dev/drm/include/asm/bug.h:33-39: BUG/BUG_ON = unconditional panic() in this port).
  • c >= 4 (any GDS/GWS/OA bit mixed with β‰₯1 standard domain): places[3] (and [4], [5]) are stored past the end of the 3-element array before the BUG_ON fires.
  1. struct amdgpu_bo layout (amdgpu_object.h:77-108), 64-bit: preferred_domains@0, allowed_domains@4, placements@8..43, struct ttm_placement placement@48 (num_placement@48, placement ptr@56, num_busy_placement@64, busy_placement ptr@72), struct ttm_buffer_object tbo@80. So the OOB stores for c=6 scribble: * places[3] (GDS) β†’ placement.num_placement = 0 * places[4] (GWS) β†’ placement.placement = NULL, num_busy_placement = TTM_PL_FLAG_UNCACHED|AMDGPU_PL_FLAG_GWS * places[5] (OA) β†’ busy_placement = (flags << 32) non-canonical garbage All values are constants; all land inside the same kzalloc'd struct amdgpu_bo (no cross-object heap corruption), and BUG_ON panics immediately after, so the write is never dereferenced. Practical impact = the panic; the OOB write is the memory-corruption component that makes this more than a logic bug (and would become directly dangerous if BUG() were ever compiled out).

  2. Reachability (unprivileged, no capabilities needed): * amdgpu_kms.c:1197 β€” AMDGPU_GEM_CREATE is registered DRM_AUTH|DRM_RENDER_ALLOW β†’ reachable from any render node. * amdgpu_gem.c:233 β€” domains only validated against AMDGPU_GEM_DOMAIN_MASK = 0x3f (all six bits accepted). * amdgpu_gem.c:260-262 β†’ amdgpu_gem_object_create β†’ amdgpu_bo_create (amdgpu_object.c:586) β†’ amdgpu_bo_do_create (amdgpu_object.c:419) β†’ amdgpu_bo_placement_from_domain(bo, bp->domain) at amdgpu_object.c:493 β€” before any TTM allocation, i.e. the bug fires on the first ioctl, regardless of memory pressure. * Size checks pass trivially: amdgpu_bo_validate_size (amdgpu_object.c:382-417) passes when GTT is in the mask and the size is small (for 0x3f, size <<= PAGE_SHIFT at :435-437 first β€” 4 bytes β†’ 16 KiB β€” still far below gart size). * Secondary path: amdgpu_gem_op_ioctl SET_PLACEMENT (amdgpu_gem.c:716-732) sets robj->preferred_domains = value & (VRAM|GTT|CPU) with no ≀2-domain constraint; any later amdgpu_bo_validate() (amdgpu_object.c:674-694, driven by CS validation amdgpu_cs.c:393,514 or eviction) re-runs the walk on bo->preferred_domains == 7 β†’ same panic.

  3. Why c==3 is enough: the guard is >= not >. c==3 writes only indices 0..2 (legal), yet asserts. So even the reasonable request "VRAM|GTT|CPU" (a normal "anywhere" allocation) panics. VRAM|GTT (c=2) β€” the combination Mesa actually uses β€” is unaffected, which is why this survived in the wild: no mainstream client sends β‰₯3 domain bits.

Exploitability toward uid=0

Not viable: the OOB write targets a fixed offset with constant data inside the object's own allocation, and BUG_ON is an unconditional panic() (sys/dev/drm/include/asm/bug.h:33-37) in this tree β€” execution never resumes after the corruption. Ceiling = reliable local DoS (C:N/I:N/A:H).

Fix validation

fix.diff (guard every store with if (c < AMDGPU_BO_MAX_PLACEMENTS), assert c > MAX instead of >=) was generated mechanically and verified with patch -p1 --dry-run against a pristine copy β€” APPLIES_CLEAN (scratch copy only; sys/ untouched). Runtime fix validation is impossible on this VM (no amdgpu kernel path to patch into a testable state), hence fix_status: not_testable. Post-fix semantics: a 0x3f request keeps the first three placements (VRAM, GTT, CPU) and silently drops the on-chip pools; a 0x7 request succeeds normally instead of panicking.

kernel_refs

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_notes.md
↓ download raw

DF-4370 fix

amdgpu_bo_placement_from_domain() can be driven by any unprivileged render-node client (AMDGPU_GEM_CREATE domains, or AMDGPU_GEM_OP SET_PLACEMENT + a CS that validates the BO) to append more placement entries than abo->placements[AMDGPU_BO_MAX_PLACEMENTS == 3] can hold, and the BUG_ON(c >= AMDGPU_BO_MAX_PLACEMENTS) fires even for the legal in-bounds case c == 3 (VRAM|GTT|CPU).

Fix (behavior-preserving except for dropping entries that previously panicked the kernel):

  1. guard every store with if (c < AMDGPU_BO_MAX_PLACEMENTS) so the walk can never write past placements[2], and
  2. change the assertion to BUG_ON(c > AMDGPU_BO_MAX_PLACEMENTS) so a full (3-entry) placement set is accepted instead of panicking.

Apply with: (cd /usr/src && patch -p1) < fix.diff (against the read-only audit tree: git apply fix.diff)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored post-verification; applies cleanly (patch -p1 --dry-run) to a scratch copy of amdgpu_object.c; runtime before/after comparison impossible on this VM (no AMD GPU, no /dev/dri) β€” flagged not_testable rather than guessed

findings/poc/DF-4370/fix.diff; dry-run output 'checking file sys/dev/drm/amd/amdgpu/amdgpu_object.c / APPLIES_CLEAN'
↓ fix.diffper-fix-DF-4370

Confirmed kernel references

Detail

Evidence (decisive lines)

VERDICT.md (full line-by-line proof incl. struct layout math); poc_df4370.c (self-contained trigger, both variants); build.log (BUILD_OK); run.log (NO_DRM_DEVICES EXIT=2); env.txt (uname 6.5-DEVELOPMENT, no /dev/dri, virtio-vga only); fix.diff verified APPLIES_CLEAN via patch -p1 --dry-run on a scratch copy

PoC changes

No seed present; PoC written from scratch. Self-contained ioctl definitions (DRM_IOCTL_VERSION and AMDGPU GEM_CREATE as _IOWR(0x40,0x00,uint64_t[4])) to avoid userspace header dependency; scans renderD128..130/card0..2, verifies driver==amdgpu via VERSION before firing. One iteration: fixed device-scan bookkeeping that mis-reported NO_AMDGPU when no device opened.

Verified recommended fix

Guard each placement store with if (c < AMDGPU_BO_MAX_PLACEMENTS) and relax BUG_ON(c >= AMDGPU_BO_MAX_PLACEMENTS) to c > AMDGPU_BO_MAX_PLACEMENTS (see fix.diff)

Verdict

Certain by static proof: amdgpu_bo_placement_from_domain() (amdgpu_object.c:139-209) appends one ttm_place per set domain bit into placements[AMDGPU_BO_MAX_PLACEMENTS=3] and checks BUG_ON(c >= 3) only AFTER storing. Any unprivileged render-node client can pass domains=VRAM|GTT|CPU (0x7) through AMDGPU_GEM_CREATE (mask 0x3f accepted at amdgpu_gem.c:233, DRM_RENDER_ALLOW at amdgpu_kms.c:1197): c==3 is in-bounds but BUG_ON(3>=3) panics the kernel. domains=0x3f additionally writes places[3..5] (36 bytes of constants) over bo->placement/bo->tbo before the panic; a second path sets preferred_domains=0x7 via AMDGPU_GEM_OP SET_PLACEMENT (amdgpu_gem.c:727) and panics on the next validate. Not reproduced at runtime because the audit VM has no GPU (no /dev/dri; only QEMU virtio VGA): PoC builds (BUILD_OK) and runs, printing NO_DRM_DEVICES, exit 2. Real-world impact ceiling: reliable local kernel panic (DoS) on amdgpu systems; the OOB write is intra-allocation with constant data and BUG_ON is an unconditional panic (asm/bug.h:33), so no escalation path.