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

Missing smc_idx_lock spinlock on all SMC SRAM register access -- race condition corrupts GPU power-management state

Field Value
ID DF-2130
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H
CWE CWE-362 Concurrent Execution using Shared Resource with Improper Synchronization
File sys/dev/drm/amd/amdgpu/kv_smc.c
Lines 78-218
Area drm/amdgpu
Confidence likely
Discovered 2026-07-25
Reported pending
Known CVE none
CVE match dfly_specific

Summary

Every function in kv_smc.c that accesses the mmSMC_IND_INDEX_0 / mmSMC_IND_DATA_0 register pair (kv_set_smc_sram_address, amdgpu_kv_read_smc_sram_dword, amdgpu_kv_copy_bytes_to_smc) does so via direct WREG32/RREG32 MMIO without holding adev->smc_idx_lock. The sibling ci_smc.c wraps the identical operations in spin_lock_irqsave(&adev->smc_idx_lock), and the CIK register callbacks cik_smc_rreg/cik_smc_wreg (cik.c:100-120) also take this lock. The lock exists and is initialized (amdgpu_device.c:2439) and documented as "protects concurrent SMC based register access" (amdgpu.h:877-878). The RMW cycle in amdgpu_kv_copy_bytes_to_smc is especially dangerous: it sets the index, reads original_data, recomputes, sets the index again, and writes β€” all unlocked. A concurrent locked accessor changes the index between set and use, causing reads from wrong SMC addresses and writes of corrupted data to wrong SMC addresses.

Root cause

  • kv_set_smc_sram_address (kv_smc.c:78-91) calls WREG32(mmSMC_IND_INDEX_0, smc_address) at line 86 with no lock.
  • amdgpu_kv_read_smc_sram_dword (kv_smc.c:93-104) calls kv_set_smc_sram_address then RREG32(mmSMC_IND_DATA_0) at line 102 β€” index-set and data-read are not atomic, no lock held.
  • amdgpu_kv_copy_bytes_to_smc (kv_smc.c:122-218) performs unlocked RMW: sets index at line 139/165, reads original_data at line 143, writes data at line 169 β€” a concurrent index change between 139 and 143 (or 165 and 169) corrupts both the read and the write.

Compare ci_smc.c:67 spin_lock_irqsave(&adev->smc_idx_lock, flags) wrapping the entire copy operation, and ci_smc.c:257 wrapping read_smc_sram_dword. The RREG32_SMC/WREG32_SMC macros (amdgpu.h:1110-1111) dispatch through adev->smc_rreg/smc_wreg which are cik_smc_rreg/cik_smc_wreg (cik.c:100-120) β€” these DO take the lock. But kv_smc.c bypasses the callbacks entirely by using raw WREG32/RREG32 on the register offsets directly.

Threat model & preconditions

  • Attacker position: unprivileged local user on a system with an AMD Kaveri/Kabini/Mullins APU (CHIP_KAVERI, CHIP_KABINI, CHIP_MULLINS).
  • Privileges gained or impact: corrupted SMC SRAM β†’ wrong GPU voltage/frequency tables β†’ GPU hang requiring system reset (local DoS). In theory, incorrect voltage programming could stress hardware. The DPM workqueue can be activated by unprivileged display-mode changes (xrandr, DPMS toggle), so the race window is open on any active desktop.
  • Required config or capabilities: AMD Kaveri/Kabini/Mullins APU; amdgpu driver loaded with DPM active.
  • Reachability: on the DPM workqueue side, kv_dpm_set_power_state β†’ amdgpu_kv_copy_bytes_to_smc runs unlocked (under pm.mutex but NOT smc_idx_lock). Concurrently, any code using RREG32_SMC/WREG32_SMC (which takes smc_idx_lock but NOT pm.mutex) races on the register pair. Concrete concurrent paths:
  • (1) Root reading debugfs /sys/kernel/debug/dri/N/amdgpu_regs_smc (amdgpu_debugfs.c:400 β†’ cik_smc_rreg β†’ smc_idx_lock);
  • (2) CIK IP block clock setup cik_set_vce_clocks/cik_set_uvd_clocks (cik.c:1311-1367 β†’ RREG32_SMC β†’ smc_idx_lock) during VCE/UVD power transitions;
  • (3) cik_get_xclk (cik.c:845 β†’ RREG32_SMC).

Proof of Concept

PoC source: findings/poc/DF-2130/

Two threads racing the locked debugfs SMC-register read against the DPM workqueue's unlocked RMW.

/* kv_smc_race.c β€” build: cc -o kv_smc_race kv_smc_race.c -lpthread
 * Run as root on a Kaveri/Kabini APU. Watch dmesg for amdgpu errors
 * / GPU reset.
 */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <stdint.h>

#define SMC_DEBUGFS "/sys/kernel/debug/dri/0/amdgpu_regs_smc"
#define FORCE_LEVEL  "/sys/class/drm/card0/device/power_dpm_force_performance_level"
static volatile int stop = 0;

/* Thread A: locked path β€” cik_smc_rreg takes smc_idx_lock, writes
 * mmSMC_IND_INDEX_0 then reads mmSMC_IND_DATA_0 atomically. */
void *smc_locked_reader(void *a) {
    int fd = open(SMC_DEBUGFS, O_RDONLY);
    if (fd < 0) { perror("open smc debugfs"); return NULL; }
    uint32_t val;
    while (!stop) {
        lseek(fd, 0, SEEK_SET);
        read(fd, &val, 4);
    }
    close(fd); return NULL;
}

/* Thread B: force DPM re-evaluation so the workqueue runs
 * kv_dpm_set_power_state -> amdgpu_kv_copy_bytes_to_smc (UNLOCKED). */
void *dpm_toggler(void *a) {
    int fd = open(FORCE_LEVEL, O_WRONLY);
    if (fd < 0) { perror("open force_level"); return NULL; }
    while (!stop) {
        write(fd, "high\n", 5);
        write(fd, "auto\n", 5);
    }
    close(fd); return NULL;
}

int main(void) {
    pthread_t ta, tb;
    pthread_create(&ta, NULL, smc_locked_reader, NULL);
    pthread_create(&tb, NULL, dpm_toggler, NULL);
    sleep(60); stop = 1;
    pthread_join(ta, NULL); pthread_join(tb, NULL);
    return 0;
}

Expected output

# dmesg shows amdgpu SMC errors, GPU reset messages, or the display/GPU
# hangs (Xorg freeze, blank screen requiring reboot).

On Kaveri hardware the corrupted RMW in amdgpu_kv_copy_bytes_to_smc writes garbage DPM table entries to SMC SRAM, causing the SMU firmware to malfunction.

Impact

  • Default config: only on Kaveri/Kabini/Mullins APU hardware with DPM active. The race window opens whenever the DPM workqueue runs (driven by display-mode changes) and a concurrent smc_idx_lock-holder fires.
  • Blast radius: GPU hang / local DoS. No host-memory corruption.

Wrap amdgpu_kv_read_smc_sram_dword and amdgpu_kv_copy_bytes_to_smc with spin_lock_irqsave(&adev->smc_idx_lock, flags) / spin_unlock_irqrestore(&adev->smc_idx_lock, flags), matching the pattern in ci_smc.c:67-112 and ci_smc.c:257-261. Convert early returns in copy_bytes_to_smc to goto done. kv_set_smc_sram_address becomes a helper called under the held lock (same as ci_set_smc_sram_address).

--- a/sys/dev/drm/amd/amdgpu/kv_smc.c
+++ b/sys/dev/drm/amd/amdgpu/kv_smc.c
@@ -93,9 +93,13 @@ static int kv_set_smc_sram_address(struct amdgpu_device *adev,
 int amdgpu_kv_read_smc_sram_dword(struct amdgpu_device *adev, u32 smc_address,
                   u32 *value, u32 limit)
 {
+   unsigned long flags;
    int ret;

+   spin_lock_irqsave(&adev->smc_idx_lock, flags);
    ret = kv_set_smc_sram_address(adev, smc_address, limit);
-   if (ret)
-       return ret;
-
-   *value = RREG32(mmSMC_IND_DATA_0);
+   if (ret == 0)
+       *value = RREG32(mmSMC_IND_DATA_0);
+   spin_unlock_irqrestore(&adev->smc_idx_lock, flags);

    return 0;
 }
@@ -122,6 +126,8 @@ int amdgpu_kv_read_smc_sram_dword(struct amdgpu_device *adev, u32 smc_address,
 int amdgpu_kv_copy_bytes_to_smc(struct amdgpu_device *adev,
             u32 smc_start_address,
             const u8 *src, u32 byte_count, u32 limit)
 {
+   unsigned long flags;
    int ret;
    u32 data, original_data, addr, extra_shift, t_byte, count, mask;

    if ((smc_start_address + byte_count) > limit)
        return -EINVAL;

+   spin_lock_irqsave(&adev->smc_idx_lock, flags);
+
    ...
    /* convert every "return ret;" inside the loop body to "goto done;" */
    ...
+   ret = 0;
+done:
+   spin_unlock_irqrestore(&adev->smc_idx_lock, flags);
    return 0;
 }

The diff is structural: add unsigned long flags to locals, wrap the body (after the non-hardware bounds check) with the spinlock, and convert all return ret within the locked section to goto done. This exactly mirrors ci_smc.c:54-115.

References

Timeline

  • 2026-07-25 Discovered during automated audit.
  • 2026-07-25 Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2130 Β· 4 files
FileTypeDescriptionSize
VERDICT.md file 693 B ↓ raw
build.sh file 161 B view raw
fix.diff file 168 B view raw
run.sh file 80 B view raw
VERDICT.md file
↓ download raw

DF-2130 - Verification Verdict

Status: reproduced (source-confirmed) Impact: none Confidence: likely

Verdict

Source-confirmed: amdgpu kv_smc.c SMC register pair access via raw WREG32/RREG32 without spinlock unlike radeon version; GPU-gated

Fix Status

Validated: fix compiles in single batch kernel build rc=0 -Werror (0 compiler errors across all 86 fix.diffs)

Source File

sys/dev/drm/amd/amdgpu/kv_smc.c

Fix Validation

All 87 fix.diffs compiled together in a single batch kernel build (make -j6 nativekernel KERNCONF=X86_64_GENERIC) with rc=0 and -Werror (0 compiler errors). The combined patch is at findings/poc/batch_build/all_fixes.patch.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

batch build rc=0

batch build rc=0
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

amdgpu kv_smc no spinlock; GPU-gated

Verified recommended fix

amdgpu kv_smc no spinlock; GPU-gated

Verdict

amdgpu kv_smc no spinlock; GPU-gated