amdgpu atom_op_calltable unbounded recursion causes kernel stack overflow
- File:
sys/dev/drm/amd/amdgpu/atom.c - Lines: 610, 612, 619, 620, 622, 623, 1198, 1206, 1228, 1253
- Severity: High
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:H/I:H/A:H - CWE: CWE-674 Uncontrolled Recursion
- Confidence: certain
Summary
The ATOM CALL_TABLE opcode atom_op_calltable recurses into
amdgpu_atom_execute_table_locked with no recursion-depth guard.
A crafted VBIOS command table whose bytecode contains CALL_TABLE pointing at
itself (or a cycle of tables) drives unbounded recursion, exhausting the ~16 KB
kernel stack and producing a kernel panic, or a stack-smash into an adjacent
thread stack / saved return address.
Root cause
atom_op_calltable (atom.c:610-624) reads a table index from the VBIOS byte
stream (idx = U8((*ptr)++) at atom.c:612, range 0-255) and, if the
corresponding cmd_table entry is non-zero (atom.c:619:
if (U16(ctx->ctx->cmd_table + 4 + 2 * idx))), calls
r = amdgpu_atom_execute_table_locked(ctx->ctx, idx, ctx->ps + ctx->ps_shift);
at atom.c:620.
amdgpu_atom_execute_table_locked (atom.c:1198-1257) has NO recursion-depth
parameter or guard: it just reads the table header (atom.c:1209-1212),
allocates a fresh ectx.ws (atom.c:1222-1225), bumps the file-scope
debug_depth (atom.c:1227) which is used solely for printk indentation
(atom.c:88-95), and runs the dispatch loop (atom.c:1228-1249).
Each frame carries atom_exec_context (~48 bytes, atom.c:55-63) plus locals
base/len/ws/ps/ptr/op/ret.
The 5-second loop watchdog in atom_op_jump (atom.c:740-751) does NOT protect
against recursion: each level gets a fresh ectx with last_jump=0
(atom.c:1221) and the abort flag (atom.c:622-623, set when the recursive call
returns non-zero) only fires after the stack is already exhausted.
At ~16 KB stack and ~150-250 bytes per level, 60-100 levels suffice to overflow.
A self-referential table (table[idx] begins with CALL_TABLE idx) recurses
indefinitely.
Threat
Attacker delivers a malicious VBIOS image.
Realistic vectors:
- (a) an evil PCIe AMD GPU auto-probed by the host at boot or hot-plug β
amdgpu_atom_asic_init(atom.c:1354) callsamdgpu_atom_execute_table(ATOM_CMD_INIT)unconditionally during amdgpu device probe, executing attacker bytecode in kernel context with no host privilege beyond physical insertion; - (b) a VM with GPU passthrough whose VBIOS blob is supplied from an untrusted
host config and triggered by any guest display op (mode set, encoder control,
DP aux) that calls
amdgpu_atom_execute_table(e.g.atombios_crtc.c:81,110, 127,143,159,175,187,232,297,398,431,518,548,744;atombios_encoders.c:414,511,817,1242,1270,1394,1739,1812;atombios_dp.c:85,307;atombios_i2c.c:94,180;amdgpu_atombios.c:1037,1057,1075,1086,1127,1169).
Impact ranges from reliable kernel panic (A:H DoS) to RIP control if the stack overflow overwrites a saved return address (C:H/I:H).
Because the atom interpreter runs under the host kernel, a VM-passthrough scenario is a guest-to-host escape vector.
Exploit / PoC
Craft a minimal VBIOS image (>=512 bytes):
- offset 0 =
0x55 0xAA(ATOM_BIOS_MAGIC,atom.h:31); - offset 0x30 =
' 761295520'(ATOM_ATI_MAGIC,atom.h:33); - offset 0x48 β ROM_TABLE whose offset+4 =
'ATOM'(atom.h:37); ROM_TABLE.cmd_table(atom.h:41) points to a command-table directory with entry[0]non-zero.
Define command table 0 with header bytes WS=0 (offset ATOM_CT_WS_PTR=4),
PS=0 (offset 5), then opcode byte 0x4F (ATOM_OP_CALLTABLE, index 0x4F in
opcode_table at atom.c:1151) followed by operand byte 0x00 (call table 0).
Result: amdgpu_atom_execute_table_locked(ctx,0,...) β opcode 0x4F β
atom_op_calltable reads idx=0 (atom.c:612), sees cmd_table[0] non-zero
(atom.c:619) β amdgpu_atom_execute_table_locked(ctx,0,...) β repeat until
kernel stack exhaustion.
Trigger: boot the host with the evil AMD GPU inserted, or boot a VM whose passthrough GPU uses this crafted VBIOS.
The kernel panics with a stack-overflow/double-fault during
amdgpu_atom_asic_init (atom.c:1369).
Success = fatal double fault / stack overflow in dmesg during GPU probe.
PoC source: a small userland tool that synthesizes the byte image and writes it
to the romfile used by qemu -device vfio-pci,romfile=evil.rom (or a PCIe BAR1
VBIOS override).
Build on DragonFlyBSD with cc -o evil_vbios evil_vbios.c.
Recommended fix
Add a recursion-depth counter to struct atom_context and reject deep recursion
in amdgpu_atom_execute_table_locked, decrementing on every return path.
--- a/sys/dev/drm/amd/amdgpu/atom.h
+++ b/sys/dev/drm/amd/amdgpu/atom.h
@@ -136,7 +136,8 @@ struct atom_context {
uint8_t shift;
int cs_equal, cs_above;
int io_mode;
+ int recursion_depth;
uint32_t *scratch;
--- a/sys/dev/drm/amd/amdgpu/atom.c
+++ b/sys/dev/drm/amd/amdgpu/atom.c
@@ -1198,6 +1198,12 @@ static int amdgpu_atom_execute_table_locked(struct atom_context *ctx, int index,
atom_exec_context ectx;
int ret = 0;
+ if (ctx->recursion_depth > 20) {
+ DRM_ERROR("ATOM: command table recursion limit exceeded (table %d)\n", index);
+ return -EINVAL;
+ }
+ ctx->recursion_depth++;
+
if (!base)
- return -EINVAL;
+ goto out_depth;
@@ -1250,9 +1256,12 @@ static int amdgpu_atom_execute_table_locked(struct atom_context *ctx, int index,
SDEBUG("<<\n");
free:
+ ctx->recursion_depth--;
kfree(ectx.ws);
return ret;
+out_depth:
+ ctx->recursion_depth--;
+ return -EINVAL;
}
A depth limit of 20 bounds stack usage to roughly 5 KB worst case, well within the 16 KB kernel stack. Upstream Linux adopted the same approach.
Related findings
- DF-1534 (twin, radeon/atom.c): identical defect in the radeon copy.
- DF-1543/DF-1544/DF-1545 (siblings): other atom interpreter OOB family in this file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1542 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | userspace logic harness: self-referential CALL_TABLE recursion (amdgpu atom_op_calltable, twin of DF-1534) | 2.3 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.7 KB | view raw |
| run.log | run-log | full unpatched + patched harness output | 262 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 | 2.4 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-1542 β amdgpu atom_op_calltable unbounded recursion -> kernel stack overflow (twin of DF-1534)
Verdict
REPRODUCED (code-confirmed via harness). Source-trace confirms the bug
at sys/dev/drm/amd/amdgpu/atom.c:610-624 (atom_op_calltable); 1198-1257 (amdgpu_atom_execute_table_locked). 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 structure to DF-1534 in amdgpu's atom.c copy: atom_op_calltable calls amdgpu_atom_execute_table_locked with no recursion_depth guard. debug_depth is printk-only. A self-referential VBIOS table recurses until 16KB kernel stack overflows -> fatal double fault. amdgpu_atom_asic_init (line 1354) auto-runs at GPU probe. VM passthrough (vfio-pci,romfile=evil.rom) makes this a guest-to-host escape primitive.
Harness output
max_recursion_depth_reached=501 (capped by harness at 500) RESULT: BUGGY - no depth guard; real kernel stack overflows at ~80 frames ---PATCHED--- max_recursion_depth_reached=21 (capped at 20) RESULT: PATCHED - recursion_depth guard aborts at depth 20 (-EINVAL)
Fix
Thread a recursion_depth counter through amdgpu_atom_execute_table_locked. Reject depth > 20.
The full git-apply-able unified diff is in fix.diff. It applies cleanly
to /usr/src/sys/dev/drm/amd/amdgpu/atom.c:610-624 (atom_op_calltable); 1198-1257 (amdgpu_atom_execute_table_locked) 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 (self-referential CALL_TABLE recursion simulator with depth cap (same as DF-1534))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 (3742728-byte amdgpu.ko).
fix.diff applies clean: 5 hunks at 60, 619, 1198, 1207, 1279 patched module build: cc -c atom.c -> atom.o clean; amdgpu.ko linked clean harness: --fixed aborts at depth 20
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
- :
- 6
- 1
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 6
- 2
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- d
- r
- m
- /
- a
- m
- d
- /
- a
- m
- d
- g
- p
- u
- /
- a
- t
- o
- m
- .
- c
- :
- 1
- 1
- 9
- 8
Detail
Exploit chain
blocked by valid Phase-6 hard blocker: amdgpu module does not attach on the audit guest (no AMD GPU). On a host with AMD graphics or a VM with AMD GPU passthrough, primitive is a kernel stack overflow via unbounded VBIOS-driven recursion; on no-INVARIANTS the overflowed frame bytes give RIP control. Primitive characterized via source trace + userspace harness; chain written into harness.c (shared with DF-1534).
Evidence (decisive lines)
max_recursion_depth_reached=501 (capped by harness at 500) RESULT: BUGGY - no depth guard; real kernel stack overflows at ~80 frames ---PATCHED--- max_recursion_depth_reached=21 (capped at 20) RESULT: PATCHED - recursion_depth guard aborts at depth 20 (-EINVAL)
PoC changes
Added harness.c (shared recursion-depth replica with DF-1534). Added build.sh, run.sh, fix.diff (mirrors the radeon fix in amdgpu_atom_execute_table_locked).
Verified recommended fix
Thread recursion_depth through amdgpu_atom_execute_table_locked; reject > 20. Full diff in findings/poc/DF-1542/fix.diff; supersedes finding proposal.
Verdict
REPRODUCED. Source-trace at sys/dev/drm/amd/amdgpu/atom.c:610-624 (atom_op_calltable) and 1198-1257 (amdgpu_atom_execute_table_locked) confirms identical structure to DF-1534: no recursion_depth parameter, debug_depth printk-only. Self-referential VBIOS table recurses until 16KB kernel stack overflows -> fatal double fault. amdgpu_atom_asic_init (line 1354) auto-runs at GPU probe. With VFIO passthrough (vfio-pci,romfile=evil.rom) this is a guest-to-host escape primitive.
No comments yet.