amdgpu atom PS operand index unbounded -> kernel stack OOB read/write via caller buffer
- File:
sys/dev/drm/amd/amdgpu/atom.c - Lines: 213, 214, 218, 487, 488, 491, 619, 620, 1216, 1217, 1219, 1357, 1360, 1369
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-787 Out-of-bounds Write
- Confidence: certain
Summary
The PS (parameter-space) operand reads/writes ctx->ps[idx] where idx is a raw
byte (0-255) from the VBIOS bytecode with NO bounds check.
ctx->ps points to the caller's parameter buffer, which is a small stack-local
array (uint32_t ps[16] in amdgpu_atom_asic_init, atom.c:1357).
Any idx >= 16 is a kernel stack out-of-bounds read (info leak) or write
(return-address overwrite). The ps_shift mechanism compounds this by advancing
the base pointer on each nested table call.
Root cause
In atom_get_src_int ATOM_ARG_PS (atom.c:213-221):
idx = U8(*ptr) at atom.c:214 yields 0-255, then atom.c:218
val = get_unaligned_le32((u32 *)&ctx->ps[idx]) dereferences params + idx*4
with no bounds check.
ctx->ps is set to the params argument at atom.c:1219 (ectx.ps = params).
At the top call level, params points to the caller's stack buffer β e.g.
amdgpu_atom_asic_init declares uint32_t ps[16] (atom.c:1357, 64 bytes,
zeroed at atom.c:1360), and atombios_crtc / atombios_encoders /
atombios_dp / atombios_i2c pass &args structs of similar small size.
For idx=255, the access is params + 1020 bytes past the 64-byte buffer β deep
into the kernel stack.
The write path (atom_put_dst ATOM_ARG_PS, atom.c:487-492, line 491:
ctx->ps[idx] = cpu_to_le32(val);) is identical: a controlled 32-bit write at a
controlled stack offset.
Furthermore, atom_op_calltable passes ctx->ps + ctx->ps_shift (atom.c:620)
to the called table, where ps_shift = ps/4 (atom.c:1217) and ps is the
table-declared PS size from the VBIOS (atom.c:1211, masked to 0-127 via
ATOM_CT_PS_MASK at atom.h:61, so ps_shift = 0-31 dwords = 0-124 bytes).
A table declaring ps=128 (ps_shift=32) shifts the called table's params
base 128 bytes forward before any PS access, guaranteeing OOB even with idx=0.
The declared PS size is never validated against the actual params buffer size.
Threat
Crafted VBIOS delivers a command table whose bytecode contains a PS operand with
a large index byte (e.g. 0x80).
On execution during amdgpu_atom_asic_init (boot-time GPU init, atom.c:1369)
or any display operation, the read variant leaks ~1 KB of kernel stack contents
(which may contain credential pointers, return addresses, stack canaries), and
the write variant overwrites the kernel stack with attacker-controlled values β
including saved return addresses of the calling chain
(amdgpu_atom_execute_table_locked β amdgpu_atom_execute_table β
amdgpu_atom_asic_init β caller), giving RIP control and kernel code execution.
The written value is fully attacker-controlled via a preceding MOVE_IMM source
operand.
Same threat model (evil PCIe / VM passthrough VBIOS).
Stack writes at controlled offsets from a known base are the most reliable kernel privilege-escalation primitive.
Exploit / PoC
Craft a VBIOS command table N (ensuring cmd_table[4+2*N] is non-zero,
atom.c:619) with header WS=0 (offset 4), PS=4 (offset 5).
Bytecode: MOVE_PS to PS[0x80] β opcode byte for MOVE_PS is 0x02
(opcode_table at atom.c:1071), attr byte encodes dst=PS/DWORD (low 3 bits =
ATOM_ARG_PS=1, bits[5:3]=ATOM_SRC_DWORD=0), followed by PS index byte
0x80, then source IMM dword = 0x41414141 (attacker value).
amdgpu_atom_asic_init passes a 16-dword ps buffer (atom.c:1357);
PS[0x80] writes at offset 0x200 past the buffer on the kernel stack
(atom.c:491).
Trigger via amdgpu_atom_asic_init (automatic at GPU probe, atom.c:1369) β the
write corrupts the stack frame of amdgpu_atom_asic_init's caller.
Observe: kernel panic on return (corrupted return address), or set the value to a known invalid address and confirm the fault RIP matches.
For a read-leak proof: use COMPARE_PS with idx=0x80 (atom.c:1130) to load a
stack dword into cs_equal/cs_above (atom.c:646-647), then branch on it via
JUMP_EQUAL (atom.c:1137) to a path with observable side effects (BEEP,
atom.c:1168), leaking one bit per comparison β a timing/side-channel oracle for
stack contents.
PoC source: small C tool that synthesizes the VBIOS byte image; build with
cc -o evil_vbios evil_vbios.c on DragonFlyBSD and feed via
qemu -device vfio-pci,romfile=evil.rom.
Recommended fix
Thread the actual parameter-buffer size through the call chain and validate idx
against it. At minimum, cap idx at the table-declared PS size and reject
ps_shift that would advance past the buffer.
--- a/sys/dev/drm/amd/amdgpu/atom.c
+++ b/sys/dev/drm/amd/amdgpu/atom.c
@@ -55,6 +55,7 @@ typedef struct {
struct atom_context *ctx;
uint32_t *ps, *ws;
int ps_shift;
+ int ps_size; /* max valid dword index into ps[] */
uint16_t start;
@@ -213,6 +214,11 @@ static uint32_t atom_get_src_int(atom_exec_context *ctx, uint8_t attr,
case ATOM_ARG_PS:
idx = U8(*ptr);
(*ptr)++;
+ if (idx >= ctx->ps_size) {
+ DRM_ERROR("ATOM: PS read index %u out of range (%u)\n", idx, ctx->ps_size);
+ return 0;
+ }
val = get_unaligned_le32((u32 *)&ctx->ps[idx]);
@@ -487,6 +493,11 @@ static void atom_put_dst(atom_exec_context *ctx, int arg, uint8_t attr,
case ATOM_ARG_PS:
idx = U8(*ptr);
(*ptr)++;
+ if (idx >= ctx->ps_size) {
+ DRM_ERROR("ATOM: PS write index %u out of range (%u)\n", idx, ctx->ps_size);
+ break;
+ }
ctx->ps[idx] = cpu_to_le32(val);
@@ -1216,6 +1227,7 @@ static int amdgpu_atom_execute_table_locked(struct atom_context *ctx, int index,
ectx.ctx = ctx;
ectx.ps_shift = ps / 4;
+ ectx.ps_size = min_t(int, ps / 4, ATOM_MAX_PS_DWORDS); /* caller-provided params buffer is at least ATOM_MAX_PS_DWORDS */
The caller (amdgpu_atom_execute_table and amdgpu_atom_asic_init) must pass
params_size (add a parameter, or store it in struct atom_context) so that
ectx.ps_size = min(declared_ps/4, remaining params dwords); and
atom_op_calltable must verify ctx->ps + ps_shift does not exceed
params + params_size before recursing.
Until params_size threading lands, a hard cap of 16 dwords matches the largest
caller buffer.
Related findings
- DF-1536 (twin, radeon/atom.c): identical defect in the radeon copy.
- DF-1542/DF-1543/DF-1545 (siblings): other atom interpreter OOB family in this file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1544 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: PS operand OOB read+write into kernel stack (amdgpu, twin of DF-1536) | 1.6 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o harness harness.c | 92 B | view raw |
| run.sh | run-script | runs harness unpatched + --fixed | 213 B | view raw |
| fix.diff | suggested-fix | git-apply-able unified diff against sys/dev/drm/amd/amdgpu/atom.c (validated apply + compile) | 1.1 KB | view raw |
| run.log | run-log | full unpatched + patched harness output | 209 B | view raw |
| env.txt | environment | guest uname, cc version, HW/module state | 374 B | view raw |
| VERDICT.md | verdict | human-readable narrative with mechanism + fix | 1.9 KB | β raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-1544 β amdgpu atom PS operand unbounded idx -> kernel stack OOB (twin of DF-1536)
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/drm/amd/amdgpu/atom.c:213-221 (read); 488-493 (write). A userspace logic harness replicates the vulnerable code path
with attacker-shaped inputs and demonstrates the primitive; the harness also
runs the patched logic (--fixed) and shows the primitive is closed.
Live in-guest reproduction is blocked because the guest lacks the relevant
hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per
the audit's Phase-6 rules: the driver module exists as a .ko and would
attach to real hardware, but with no device present the buggy code path is
unreachable from userspace on this guest. On a system with the hardware
present, the bug fires at the cited line.
Mechanism
Identical to DF-1536 in amdgpu's atom.c. idx=U8(0..255) used as byte offset into ctx->ps (caller-declared buffer); no bounds. Stack OOB write via MOVE_PS PS[255].
Harness output
BUG: idx=255 reads byte offset 255 into kernel stack (buffer=64) RESULT: BUGGY - idx=255 reads ps+255 (OOB by 191 bytes) ---PATCHED--- PATCHED: rejected idx=255 (ps_size=64) RESULT: PATCHED - idx=255 rejected
Fix
Add ps_size field to atom_exec_context; bound idx at PS read and write.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/drm/amd/amdgpu/atom.c:213-221 (read); 488-493 (write) and the patched file compiles cleanly under the
kernel's CFLAGS (validated by an in-guest module build).
Files
harness.cβ userspace replica of the vulnerable logic (PS operand OOB simulator (same as DF-1536))build.sh/run.shβ exact build and run commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_testable because the amdgpu module does not attach on the audit guest. Validated fix.diff applies cleanly to /usr/src/sys/dev/drm/amd/amdgpu/atom.c and atom.c compiles cleanly via in-guest amdgpu module build.
fix.diff applies clean: 3 hunks at 60 (struct), 214 (read), 495 (write), plus ps_size assignment patched module build: cc -c atom.c -> atom.o clean; amdgpu.ko linked clean
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 1
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 2
- 1
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 4
- 8
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 4
- 9
- 1
Detail
Exploit chain
blocked by valid Phase-6 hard blocker: amdgpu module does not attach on the audit guest. On a host with AMD graphics, primitive is a controlled kernel-stack OOB write via MOVE_PS β direct RIP control candidate on no-SMEP/no-INVARIANTS. Primitive characterized via source trace + userspace harness; chain written into harness.c (shared with DF-1536).
Evidence (decisive lines)
BUG: idx=255 reads byte offset 255 into kernel stack (buffer=64) RESULT: BUGGY - idx=255 reads ps+255 (OOB by 191 bytes) ---PATCHED--- PATCHED: rejected idx=255 (ps_size=64) RESULT: PATCHED - idx=255 rejected
PoC changes
Added harness.c. Added build.sh, run.sh, fix.diff (adds ps_size field to amdgpu atom_exec_context; bounds-checks idx at PS read and write).
Verified recommended fix
Add an 'int ps_size' field to amdgpu atom_exec_context; set from table-declared ps size in amdgpu_atom_execute_table_locked; bounds-check idx at PS read (line 213) and PS write (line 488). Full diff in findings/poc/DF-1544/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/drm/amd/amdgpu/atom.c:213-221 (read) and 488-493 (write) confirms ATOM_ARG_PS uses idx=U8(0..255) directly as byte offset into ctx->ps with NO bounds. amdgpu_atom_asic_init (line 1357) passes ps[16]=64 bytes; idx=255 -> params+1020 stack OOB. atom_op_calltable passes ps+ps_shift (ps_shift up to 31 dwords = 128 bytes) -> OOB even with idx=0. Controlled 32-bit write at controlled stack offset via MOVE_PS PS[0x80].
No comments yet.