On Tue, Sep 29, 2026 at 05:34:03PM -0300, Thadeu Lima de Souza Cascardo wrote:
> When amdgpu_vm_ptes_update fails, there might be an IB jobs leak.

On 7.2.9 this leak ends with command submission blocked in the kernel
until a reboot, so I tested a backport of the series there.

The leaked job keeps its IB, and that IB never gets a fence, so it
stays in the delayed IB pool for good. drm_suballoc_try_free() stops
at the first entry without a fence, and drm_suballoc_next_hole() can
only skip ahead to an entry that has one. Once a request does not fit
in front of a leaked IB and no fenced entry is left to skip to,
amdgpu_sa_bo_new() sleeps uninterruptibly in drm_suballoc_insert(),
although the rest of the 1 MiB pool may be free. Later page table
updates that need an IB from that pool block the same way.

I hit this on a Framework Desktop (Ryzen AI Max+ 395, Radeon 8060S,
512 MiB VRAM carve-out) running Vulkan compute with 62 GiB in GTT. Page
tables can only be placed in VRAM, which was full at the hang (512 of
512 MiB). That boot logged 239 "Couldn't update BO_VA (-12)", and I
did not trace which allocation failed. The first command submission
after the last of them never returned, with the GPU idle:

  INFO: task qwen38_vk:3765381 blocked for more than 122 seconds.
        Not tainted 7.2.9 #1-NixOS
  Call Trace:
   <TASK>
   __schedule+0x413/0x1780
   schedule+0x27/0xa0
   drm_suballoc_insert+0x5de/0x680 [drm_suballoc_helper]
   drm_suballoc_new+0x47/0x220 [drm_suballoc_helper]
   amdgpu_sa_bo_new+0x1e/0x40 [amdgpu]
   amdgpu_ib_get+0x42/0xa0 [amdgpu]
   amdgpu_job_alloc_with_ib+0x78/0xe0 [amdgpu]
   amdgpu_vm_sdma_prepare+0x4a/0xb0 [amdgpu]
   amdgpu_vm_update_range+0x22f/0x940 [amdgpu]
   amdgpu_vm_bo_update+0x261/0x740 [amdgpu]
   amdgpu_vm_handle_moved+0x60/0x1e0 [amdgpu]
   amdgpu_cs_ioctl+0x1a19/0x2380 [amdgpu]
   drm_ioctl_kernel+0xae/0x100
   drm_ioctl+0x27e/0x5b0
   amdgpu_drm_ioctl+0x4a/0x80 [amdgpu]
   __x64_sys_ioctl+0x97/0xe0
   do_syscall_64+0x134/0x6b0
   entry_SYSCALL_64_after_hwframe+0x77/0x7f

To reproduce it on demand, I limited failslab to the test task and,
with its stack filter, to allocations below amdgpu_vm_ptes_update().
The injected -ENOMEM hits the first slab allocation made while a page
table is created, which need not be where the real failure happened.
The program at the end maps a 4 KiB BO at <count> addresses with
injection on, so every map fails while allocating a page table, after
amdgpu_vm_sdma_prepare() allocated the job. With injection off, one
more map then updates the page tables of all earlier mappings, and the
unmaps clear them again, so both take IBs from the same pool. In
amdgpu_sa_info, an IB without "protected by" belongs to no submitted
job, so after the run it is a leaked one.

Results on 7.2.9 without and with the backported series:

Each kernel ran 64 maps and then 2048 maps in one boot, with nothing
else using the GPU.

Without the series:
  64 maps: 64 injected failures and 64 "Couldn't update BO_VA (-12)".
    64 IBs without a fence stayed in the delayed pool (64 KiB), and
    the program finished.
  2048 maps: after 960 failed maps the program blocked in
    drm_suballoc_insert(), called from amdgpu_vm_sdma_prepare() in
    amdgpu_gem_va_ioctl(). The delayed pool then held 1024 IBs of
    1 KiB without a fence, 64 of them from the first run, which is
    the whole 1 MiB pool. The task stayed in D state, through a
    SIGKILL, until the reboot.

With the series:
  64 maps: 64 injected failures and 64 "Couldn't update BO_VA (-12)",
    no IB without a fence left, all 65 unmaps done.
  2048 maps: 2048 injected failures and 2048 "Couldn't update BO_VA
    (-12)", no IB without a fence left, all 2049 unmaps done, no task
    in D state.

For 7.2.9 I backported both patches. There, struct
amdgpu_vm_update_funcs is in amdgpu_vm.h, the map path is still
amdgpu_vm_update_range() and ends with amdgpu_vm_eviction_unlock(),
and the hunks of patch 1 for amdgpu_vm_unmap_range() and
amdgpu_vm_handle_fault() have no counterpart. The added and removed
lines are the same, only the context differs.

As this blocks command submission until a reboot on 7.2.9, could
patch 2, with patch 1 as its prerequisite, be tagged for stable? The
backport to 7.2.9 needed only the changes above.

Claude Code (Claude Opus 5.5) did the source analysis, the backport,
the test program and script, ran the tests on my machine and drafted
this mail.

Tested-by: Simao Gomes Viana <[email protected]> # 7.2.9 backport

Reproducer. It needs CONFIG_FAILSLAB, CONFIG_FAULT_INJECTION_DEBUG_FS
and CONFIG_FAULT_INJECTION_STACKTRACE_FILTER, and root. The range of
amdgpu_vm_ptes_update() comes from /proc/kallsyms (readable with
kptr_restrict at most 1), and its end is the next higher symbol
address. The other failslab attributes stay at their defaults.

  cd /sys/kernel/debug/failslab
  echo N > ignore-gfp-wait
  echo Y > task-filter
  echo -1 > times
  echo 0x<start> > require-start
  echo 0x<end> > require-end
  echo 100 > probability

  gcc -O2 -o vm-map-fail vm-map-fail.c \
      $(pkg-config --cflags --libs libdrm_amdgpu)
  ./vm-map-fail /dev/dri/renderD128 64

/*
 * Maps one 4 KiB buffer at <count> GPU addresses 2 MiB apart with
 * /proc/self/make-it-fail set. With failslab limited to that flag and to call
 * chains through amdgpu_vm_ptes_update(), every map fails in its page table
 * allocation after amdgpu_vm_sdma_prepare() allocated the update job, and the
 * kernel logs "Couldn't update BO_VA (-12)" for it.
 *
 * Then, with the flag cleared, one more map retries the page table updates of
 * all <count> earlier mappings, which now allocate their page tables and
 * submit their jobs, and the unmaps clear them again, one job each. Both need
 * IBs from the same pool the failed maps took theirs from.
 *
 * Writing make-it-fail needs CAP_SYS_RESOURCE.
 *
 *   vm-map-fail <render node> <count>
 */
#include <amdgpu.h>
#include <amdgpu_drm.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BO_SIZE 4096ull
#define STRIDE (2ull << 20)

static void check(int r, const char *what)
{
        if (r) {
                fprintf(stderr, "%s: %s\n", what, strerror(-r));
                exit(1);
        }
}

static void set_make_it_fail(const char *value)
{
        int fd = open("/proc/self/make-it-fail", O_WRONLY);

        if (fd < 0 || write(fd, value, 1) != 1) {
                perror("/proc/self/make-it-fail");
                exit(1);
        }
        close(fd);
}

static void va_op_all(amdgpu_bo_handle bo, uint64_t base, unsigned int count,
                      uint32_t op, const char *what)
{
        for (unsigned int i = 0; i < count; i++)
                check(amdgpu_bo_va_op(bo, 0, BO_SIZE, base + i * STRIDE, 0, op),
                      what);
}

int main(int argc, char **argv)
{
        struct amdgpu_bo_alloc_request request = {
                .alloc_size = BO_SIZE,
                .phys_alignment = BO_SIZE,
                .preferred_heap = AMDGPU_GEM_DOMAIN_GTT,
        };
        amdgpu_device_handle dev;
        amdgpu_va_handle va;
        amdgpu_bo_handle bo;
        uint32_t major, minor;
        unsigned int count;
        uint64_t base;
        int fd;

        if (argc != 3 || (count = strtoul(argv[2], NULL, 10)) == 0) {
                fprintf(stderr, "usage: %s <render node> <count>\n", argv[0]);
                return 2;
        }

        fd = open(argv[1], O_RDWR | O_CLOEXEC);
        if (fd < 0) {
                perror(argv[1]);
                return 1;
        }
        check(amdgpu_device_initialize(fd, &major, &minor, &dev),
              "amdgpu_device_initialize");
        check(amdgpu_bo_alloc(dev, &request, &bo), "amdgpu_bo_alloc");
        check(amdgpu_va_range_alloc(dev, amdgpu_gpu_va_range_general,
                                    (count + 1) * STRIDE, STRIDE, 0, &base, &va,
                                    0),
              "amdgpu_va_range_alloc");

        set_make_it_fail("1");
        va_op_all(bo, base, count, AMDGPU_VA_OP_MAP, "map");
        set_make_it_fail("0");
        check(amdgpu_bo_va_op(bo, 0, BO_SIZE, base + count * STRIDE, 0,
                              AMDGPU_VA_OP_MAP),
              "map without failure injection");
        va_op_all(bo, base, count + 1, AMDGPU_VA_OP_UNMAP, "unmap");

        check(amdgpu_va_range_free(va), "amdgpu_va_range_free");
        check(amdgpu_bo_free(bo), "amdgpu_bo_free");
        check(amdgpu_device_deinitialize(dev), "amdgpu_device_deinitialize");
        close(fd);

        printf("%u failed maps from 0x%llx, %llu bytes apart, then 1 map and %u unmaps\n",
               count, (unsigned long long)base, STRIDE, count + 1);
        return 0;
}


Mit freundlichen Grüßen /
best regards,
Logo_Toowoxx.png
 
Simao Gomes Viana Toowoxx IT GmbH
Senior Developer Krumbacher Straße 1
DE-86489 Deisenhausen


E: [email protected]
T: +49 8282 881299-22
W: toowoxx.de
Teams: Simao Gomes Viana | Toowoxx IT GmbH
Standort Ulm
Münchner Straße 15
DE-89073 Ulm
FOLGE UNS:
Firmensitz:               Toowoxx IT GmbH, Krumbacher Straße 1, 86489 Deisenhausen
Geschäftsführer:       Christian Thoma, Oliver Wiemer, Jürgen Mayer
Handelsregister:       Amtsgericht Memmingen HRB 13224
Umsatzsteuer ID:      DE258706918

Footer  TOOWOXXPasskey.png

Reply via email to