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

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) calls amdgpu_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.

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.

  • 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1542 Β· 9 files
FileTypeDescriptionSize
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
VERDICT.md verdict human-readable narrative with mechanism + fix
↓ download 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 commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_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
↓ fix.diffn/a (module-bound bug; guest has no AMD GPU)

Confirmed kernel references

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.