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)
PoC verification
Evidence pack
findings/poc/DF-4370 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 fromamdgpu_gem.c:233,260andamdgpu_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
AMDGPU_GEM_CREATEwithin.domains = VRAM|GTT|CPU (0x7)βamdgpu_bo_placement_from_domain()appends 3 entries (in-bounds) and thenBUG_ON(c >= 3)fires with c==3 β panic.in.domains = 0x3f(all six domains; mask accepted at amdgpu_gem.c:233) β the walk writesplaces[3..5]β past the 3-element array, overbo->placementand the head ofbo->tboβ before the same BUG_ON panics.- Secondary path:
AMDGPU_GEM_OPSET_PLACEMENT with value 0x7 setsbo->preferred_domains = 0x7(amdgpu_gem.c:727); the next CS/evictionamdgpu_bo_validate()β same walk β same panic.
Artifacts
poc_df4370.cβ self-contained trigger (no DRM headers required)build.log,run.log,env.txtβ guest evidencefix.diffβ bound the walk (if (c < AMDGPU_BO_MAX_PLACEMENTS)guards +BUG_ON(c > ...)), applies cleanly withpatch -p1VERDICT.mdβ full analysis
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
-
sys/dev/drm/amd/amdgpu/amdgpu_object.h:35#define AMDGPU_BO_MAX_PLACEMENTS 3and:81struct ttm_place placements[3];insidestruct amdgpu_bo. -
sys/dev/drm/amd/amdgpu/amdgpu_object.c:131-216amdgpu_bo_placement_from_domain(abo, domain)appends one placement per set domain bit among VRAM/GTT/CPU/GDS/GWS/OA (up to 6), writingplaces[c].fpfn/.lpfn/.flagsfirst 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, thenBUG_ON(3 >= 3)is TRUE βpanic("BUG in ...")(sys/dev/drm/include/asm/bug.h:33-39: BUG/BUG_ON = unconditionalpanic()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.
-
struct amdgpu_bolayout (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'dstruct amdgpu_bo(no cross-object heap corruption), andBUG_ONpanics 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). -
Reachability (unprivileged, no capabilities needed): *
amdgpu_kms.c:1197βAMDGPU_GEM_CREATEis registeredDRM_AUTH|DRM_RENDER_ALLOWβ reachable from any render node. *amdgpu_gem.c:233β domains only validated againstAMDGPU_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_SHIFTat :435-437 first β 4 bytes β 16 KiB β still far below gart size). * Secondary path:amdgpu_gem_op_ioctlSET_PLACEMENT (amdgpu_gem.c:716-732) setsrobj->preferred_domains = value & (VRAM|GTT|CPU)with no β€2-domain constraint; any lateramdgpu_bo_validate()(amdgpu_object.c:674-694, driven by CS validationamdgpu_cs.c:393,514or eviction) re-runs the walk onbo->preferred_domains == 7β same panic. -
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
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:139-216 (walk + BUG_ON at :209)
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:493 (call site in do_create)
- sys/dev/drm/amd/amdgpu/amdgpu_object.h:35,77-108 (MAX=3, struct layout)
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:233,260 (mask + pass-through)
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:716-732 (SET_PLACEMENT secondary path)
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1197 (DRM_RENDER_ALLOW)
- sys/dev/drm/include/asm/bug.h:33-39 (BUG_ON = panic, unconditional)
- sys/dev/drm/include/drm/ttm/ttm_placement.h:85-89 (ttm_place, 12 bytes)
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.
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):
- guard every store with
if (c < AMDGPU_BO_MAX_PLACEMENTS)so the walk can never write pastplacements[2], and - 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_testablefix.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'
Confirmed kernel references
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:209
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:139
- sys/dev/drm/amd/amdgpu/amdgpu_object.c:493
- sys/dev/drm/amd/amdgpu/amdgpu_object.h:35
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:233
- sys/dev/drm/amd/amdgpu/amdgpu_gem.c:727
- sys/dev/drm/amd/amdgpu/amdgpu_kms.c:1197
- sys/dev/drm/include/asm/bug.h:33
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.
No comments yet.