CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
735 lines
28 KiB
C++
735 lines
28 KiB
C++
// Copyright (c) 2026 Advanced Micro Devices, Inc.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
/// @file interposer_dup_test.cpp
|
|
/// @brief LD_PRELOAD regression tests for the interposer's KFD fd/backend
|
|
/// bookkeeping across dup / dup2 / dup3 / fcntl(F_DUPFD).
|
|
///
|
|
/// @details These run with librocjitsu.so preloaded (see the ENVIRONMENT set in
|
|
/// tests/CMakeLists.txt) so that open("/dev/kfd") is serviced by the simulated
|
|
/// KFD driver. AMDKFD_IOC_GET_VERSION is used purely as a routing probe: it
|
|
/// succeeds (returns 0 and fills the version) only when the fd is routed to a
|
|
/// KFD backend, and fails when the fd falls through to the real (non-KFD)
|
|
/// descriptor. The SimulatedDriver-level unit tests (KfdIoctlTest) cannot catch
|
|
/// these because they exercise the driver object directly, bypassing the fd
|
|
/// tracking that lives in the interposer.
|
|
|
|
#include "rocjitsu/base/rj_compiler.h"
|
|
RJ_DIAGNOSTIC_PUSH
|
|
RJ_DIAGNOSTIC_IGNORE_PEDANTIC
|
|
#include "drm/amdgpu_drm.h"
|
|
#include "drm/drm.h"
|
|
#include "linux/uapi/kfd_ioctl.h"
|
|
RJ_DIAGNOSTIC_POP
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <atomic>
|
|
#include <cerrno>
|
|
#include <chrono>
|
|
#include <fcntl.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/mman.h>
|
|
#include <thread>
|
|
#include <unistd.h>
|
|
|
|
namespace {
|
|
|
|
// Issue AMDKFD_IOC_GET_VERSION on fd. Returns true if the fd routed to a KFD
|
|
// backend (ioctl succeeded and reported the expected major version).
|
|
bool kfd_version_ok(int fd) {
|
|
kfd_ioctl_get_version_args args{};
|
|
int rc = ioctl(fd, AMDKFD_IOC_GET_VERSION, &args);
|
|
return rc == 0 && args.major_version == KFD_IOCTL_MAJOR_VERSION;
|
|
}
|
|
|
|
int open_kfd() { return open("/dev/kfd", O_RDWR | O_CLOEXEC); }
|
|
|
|
} // namespace
|
|
|
|
// A plain dup() of the KFD fd must keep routing KFD ioctls to the driver, and
|
|
// closing the original primary fd must not tear the process down while the dup
|
|
// still holds a reference (dup keeps the backend alive).
|
|
TEST(InterposerDupTest, DupKeepsKfdRoutingAfterPrimaryClose) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
|
|
int dup_fd = dup(kfd);
|
|
ASSERT_GE(dup_fd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(dup_fd));
|
|
|
|
// Close the primary; the dup must still route to the live KFD backend.
|
|
EXPECT_EQ(close(kfd), 0);
|
|
EXPECT_TRUE(kfd_version_ok(dup_fd));
|
|
|
|
EXPECT_EQ(close(dup_fd), 0);
|
|
}
|
|
|
|
// fcntl(F_DUPFD_CLOEXEC) is the dup path libdrm uses; it must also preserve KFD
|
|
// routing on the duplicate.
|
|
TEST(InterposerDupTest, FcntlDupfdKeepsKfdRouting) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
|
|
int dup_fd = fcntl(kfd, F_DUPFD_CLOEXEC, 0);
|
|
ASSERT_GE(dup_fd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(dup_fd));
|
|
|
|
EXPECT_EQ(close(dup_fd), 0);
|
|
// Original still routes.
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// dup2 that OVERWRITES the primary KFD fd number must invalidate the old primary
|
|
// identity: after dup2(other, kfd) the kfd number now names 'other', so KFD
|
|
// ioctls on it must NOT be routed to the (now-replaced) KFD backend, and closing
|
|
// it must behave like closing a normal fd.
|
|
TEST(InterposerDupTest, Dup2OverPrimaryInvalidatesKfdIdentity) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
|
|
// A plain pipe fd to overwrite the primary KFD fd number with.
|
|
int pipefd[2];
|
|
ASSERT_EQ(pipe(pipefd), 0);
|
|
|
|
// Overwrite the KFD primary number with the read end of the pipe.
|
|
ASSERT_EQ(dup2(pipefd[0], kfd), kfd);
|
|
|
|
// The kfd number now refers to the pipe, not the KFD backend: a KFD ioctl must
|
|
// no longer succeed against it.
|
|
EXPECT_FALSE(kfd_version_ok(kfd));
|
|
|
|
// Cleanup. Closing the overwritten number closes the pipe read-end dup.
|
|
EXPECT_EQ(close(kfd), 0);
|
|
EXPECT_EQ(close(pipefd[0]), 0);
|
|
EXPECT_EQ(close(pipefd[1]), 0);
|
|
}
|
|
|
|
// dup2 of the KFD fd ONTO a fresh number must make the target route KFD ioctls,
|
|
// and the reference bookkeeping must let both fds be closed without prematurely
|
|
// destroying the backend.
|
|
TEST(InterposerDupTest, Dup2OntoFreshFdRoutesKfd) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
|
|
// Reserve a target fd number with a pipe end, then dup2 the KFD fd onto it.
|
|
int pipefd[2];
|
|
ASSERT_EQ(pipe(pipefd), 0);
|
|
int target = pipefd[0];
|
|
|
|
ASSERT_EQ(dup2(kfd, target), target);
|
|
EXPECT_TRUE(kfd_version_ok(target));
|
|
|
|
// Close the primary; the dup2 target keeps the backend alive.
|
|
EXPECT_EQ(close(kfd), 0);
|
|
EXPECT_TRUE(kfd_version_ok(target));
|
|
|
|
EXPECT_EQ(close(target), 0);
|
|
EXPECT_EQ(close(pipefd[1]), 0);
|
|
}
|
|
|
|
// dup3 of the KFD fd onto a fresh number must route KFD ioctls to the target,
|
|
// and dup3(fd, fd, flags) must fail with EINVAL without disturbing tracking (the
|
|
// interposer's reserve/reconcile path must roll back cleanly on that failure).
|
|
TEST(InterposerDupTest, Dup3RoutesKfdAndRejectsSameFd) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
|
|
// dup3(fd, fd, ...) is required to fail with EINVAL and leave fd untouched.
|
|
errno = 0;
|
|
EXPECT_EQ(dup3(kfd, kfd, O_CLOEXEC), -1);
|
|
EXPECT_EQ(errno, EINVAL);
|
|
// The primary must still route after the rejected dup3 (no tracking disturbed).
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
|
|
// dup3 onto a fresh number routes KFD, and both fds close cleanly.
|
|
int pipefd[2];
|
|
ASSERT_EQ(pipe(pipefd), 0);
|
|
int target = pipefd[0];
|
|
ASSERT_EQ(dup3(kfd, target, O_CLOEXEC), target);
|
|
EXPECT_TRUE(kfd_version_ok(target));
|
|
|
|
EXPECT_EQ(close(kfd), 0);
|
|
EXPECT_TRUE(kfd_version_ok(target)); // dup3 target keeps the backend alive.
|
|
|
|
EXPECT_EQ(close(target), 0);
|
|
EXPECT_EQ(close(pipefd[1]), 0);
|
|
}
|
|
|
|
// Overwriting the primary KFD fd number via dup2 while a dup keeps the backend
|
|
// alive must (a) leave the dup routing, and (b) let a fresh open("/dev/kfd")
|
|
// return a valid, routable KFD fd. This is the reopen-after-overwrite path that
|
|
// re-mints the primary fd number (remote: reissue_synthetic_kfd_fd under
|
|
// remote_mutex_; local: ensure_fd_created under process_mutex_) without
|
|
// disturbing the still-live backend the dup holds. Runs identically on the local
|
|
// and daemon (remote) harnesses.
|
|
TEST(InterposerDupTest, ReopenAfterPrimaryOverwriteKeepsBackend) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
EXPECT_TRUE(kfd_version_ok(kfd));
|
|
|
|
// A dup keeps the backend alive across the primary's overwrite.
|
|
int keeper = dup(kfd);
|
|
ASSERT_GE(keeper, 0);
|
|
EXPECT_TRUE(kfd_version_ok(keeper));
|
|
|
|
// Overwrite the primary fd number with an unrelated pipe end. After dup2 the
|
|
// kfd number aliases the pipe read-end, so pipefd[0] is redundant and must be
|
|
// closed to avoid leaking a descriptor across the other tests in this process.
|
|
int pipefd[2];
|
|
ASSERT_EQ(pipe(pipefd), 0);
|
|
ASSERT_EQ(dup2(pipefd[0], kfd), kfd);
|
|
EXPECT_EQ(close(pipefd[0]), 0);
|
|
// The overwritten number now names the pipe, not the KFD backend.
|
|
EXPECT_FALSE(kfd_version_ok(kfd));
|
|
// The dup still routes: the backend stayed alive.
|
|
EXPECT_TRUE(kfd_version_ok(keeper));
|
|
|
|
// A fresh open must return a valid, routable KFD fd (re-minted primary).
|
|
int kfd2 = open_kfd();
|
|
ASSERT_GE(kfd2, 0) << "reopen after primary overwrite returned " << kfd2;
|
|
EXPECT_TRUE(kfd_version_ok(kfd2));
|
|
|
|
EXPECT_EQ(close(kfd2), 0);
|
|
EXPECT_EQ(close(keeper), 0);
|
|
EXPECT_EQ(close(kfd), 0); // closes the pipe-read dup installed over the number
|
|
EXPECT_EQ(close(pipefd[1]), 0);
|
|
}
|
|
|
|
// Serialized reopen-after-overwrite under contention. The interposer keeps a
|
|
// single primary KFD fd slot, so a distinct dup (keeper) holds the backend alive
|
|
// while the main thread repeatedly overwrites the primary fd number and reopens
|
|
// "/dev/kfd" — always exactly one primary at a time, matching how a real client
|
|
// uses /dev/kfd. A background thread churns dup/close on the keeper so backend
|
|
// retain/release runs concurrently with invalidation + reopen. With invalidation
|
|
// and open serialized on the same lock (remote_mutex_ / process_mutex_), every
|
|
// reopen must return a routable KFD fd — never -1 or a reused non-KFD descriptor
|
|
// (the ENOTTY case the review reproduced). This is the invariant asserted here,
|
|
// and it holds on both the local (process_mutex_) and daemon/remote
|
|
// (remote_mutex_) harnesses. Note: the keeper's own routability is deliberately
|
|
// not asserted after the loop — on the local backend a reopen rebinds the shared
|
|
// backing fd and untracks existing dups (clear_dups), which is expected
|
|
// local-only behavior unrelated to the reopen-routability invariant under test.
|
|
TEST(InterposerDupTest, SerializedReopenUnderContentionStaysRoutable) {
|
|
int primary = open_kfd();
|
|
ASSERT_GE(primary, 0);
|
|
ASSERT_TRUE(kfd_version_ok(primary));
|
|
int keeper = dup(primary); // distinct number; holds the backend across reopens
|
|
ASSERT_GE(keeper, 0);
|
|
ASSERT_TRUE(kfd_version_ok(keeper));
|
|
|
|
constexpr int kIters = 500;
|
|
std::atomic<bool> stop{false};
|
|
|
|
// Background churn: dup the keeper and close it, exercising backend
|
|
// retain/release concurrently with the reopen loop below. A short yield/sleep
|
|
// between iterations keeps the contention window open without spinning a full
|
|
// core (which would add CI flakiness/timeouts under parallel test runs).
|
|
std::thread churn([&] {
|
|
while (!stop.load(std::memory_order_relaxed)) {
|
|
int d = dup(keeper);
|
|
if (d >= 0)
|
|
close(d);
|
|
std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
}
|
|
});
|
|
|
|
// Use non-fatal checks (EXPECT_*/break) rather than ASSERT_* inside the loop:
|
|
// the churn thread is joinable here, so a fatal assertion that returned early
|
|
// would skip stop/join and terminate the process. Any setup failure breaks to
|
|
// the shutdown path below.
|
|
int bad = 0;
|
|
bool setup_failed = false;
|
|
for (int i = 0; i < kIters; ++i) {
|
|
// Overwrite the current primary fd number with a pipe end, releasing the
|
|
// primary's reference (keeper keeps the backend alive). This drives
|
|
// invalidate_overwritten_kfd_fd() concurrently with the churn thread. After
|
|
// dup2 the primary number aliases the pipe read-end, so close both the
|
|
// now-redundant pipefd[0] and the aliased primary number, plus pipefd[1].
|
|
int pipefd[2];
|
|
if (pipe(pipefd) != 0) {
|
|
setup_failed = true;
|
|
break;
|
|
}
|
|
if (dup2(pipefd[0], primary) != primary) {
|
|
setup_failed = true;
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
break;
|
|
}
|
|
close(primary); // now the pipe-read dup installed over the number
|
|
close(pipefd[0]);
|
|
close(pipefd[1]);
|
|
|
|
// Reopen: the slot was cleared, so this must re-mint a fresh, routable
|
|
// primary (a distinct number from the still-open keeper).
|
|
primary = open_kfd();
|
|
if (primary < 0) {
|
|
++bad;
|
|
primary = dup(keeper); // recover so the loop can continue overwriting
|
|
if (primary < 0) { // recovery also failed under fd pressure: stop.
|
|
setup_failed = true;
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (!kfd_version_ok(primary))
|
|
++bad;
|
|
}
|
|
|
|
stop.store(true);
|
|
churn.join();
|
|
|
|
EXPECT_FALSE(setup_failed) << "pipe()/dup2() failed under resource pressure";
|
|
EXPECT_EQ(bad, 0) << "a reopen returned -1 or a non-routable fd under contention";
|
|
if (primary >= 0) {
|
|
EXPECT_EQ(close(primary), 0);
|
|
}
|
|
EXPECT_EQ(close(keeper), 0);
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Open the synthetic DRM render node the interposer exposes for the simulated GPU
|
|
// (render minor 128 in the KMD test configs). Requires the KFD driver to be up, so
|
|
// callers open /dev/kfd first.
|
|
int open_drm_render() { return open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC); }
|
|
|
|
// Create an mmap-able, sized stand-in for a dmabuf export fd. PRIME_FD_TO_HANDLE
|
|
// fstats the fd for the BO size and later MAP mmaps it, so the fd must be a real
|
|
// sized, mappable object; a memfd satisfies both without a KFD allocation.
|
|
int make_sized_memfd(size_t size) {
|
|
int fd = memfd_create("rocjitsu_gem_test", MFD_CLOEXEC);
|
|
if (fd < 0)
|
|
return -1;
|
|
if (ftruncate(fd, static_cast<off_t>(size)) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
return fd;
|
|
}
|
|
|
|
// Mint a stable GEM handle for a dmabuf fd via PRIME_FD_TO_HANDLE on the DRM fd.
|
|
bool prime_import(int drm_fd, int dmabuf_fd, uint32_t *handle) {
|
|
drm_prime_handle prime{};
|
|
prime.fd = dmabuf_fd;
|
|
if (ioctl(drm_fd, DRM_IOCTL_PRIME_FD_TO_HANDLE, &prime) != 0)
|
|
return false;
|
|
*handle = prime.handle;
|
|
return true;
|
|
}
|
|
|
|
// DRM_AMDGPU_GEM_VA is a DRM_COMMAND-relative ioctl; build the request number the
|
|
// same way libdrm_amdgpu does. Wrapped in a function so the test reads cleanly.
|
|
unsigned long DRM_AMDGPU_GEM_VA_request() {
|
|
return DRM_IOWR(DRM_COMMAND_BASE + DRM_AMDGPU_GEM_VA, drm_amdgpu_gem_va);
|
|
}
|
|
|
|
int gem_close(int drm_fd, uint32_t handle) {
|
|
drm_gem_close gc{};
|
|
gc.handle = handle;
|
|
return ioctl(drm_fd, DRM_IOCTL_GEM_CLOSE, &gc);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// A dmabuf export fd number that is closed and then recycled by a second export
|
|
// must resolve to a DISTINCT, stable GEM handle — never one derived from the fd
|
|
// number. Two concurrently-live BOs whose export fds happened to reuse the same
|
|
// integer must keep independent handles and independent GPU mappings, and closing
|
|
// one handle must not disturb the other. This pins the fix for the old
|
|
// handle = dmabuf_fd + 1 scheme, under which a recycled fd tore down a live BO.
|
|
TEST(InterposerGemTest, ReusedDmabufFdMintsDistinctHandles) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBoSize = 0x1000;
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va_a = 0x1000000000ULL;
|
|
const uint64_t va_b = 0x1000100000ULL;
|
|
|
|
// First BO: import and map it (the export fd must stay open across MAP, which
|
|
// lazily mmaps the backing pages — mirrors ROCr, which closes the export fd only
|
|
// AFTER access setup). Then close the export fd so its number becomes free.
|
|
int dmabuf_a = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf_a, 0);
|
|
uint32_t handle_a = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_a, &handle_a));
|
|
ASSERT_NE(handle_a, 0u);
|
|
|
|
drm_amdgpu_gem_va map_a{};
|
|
map_a.handle = handle_a;
|
|
map_a.operation = AMDGPU_VA_OP_MAP;
|
|
map_a.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map_a.va_address = va_a;
|
|
map_a.map_size = kBoSize;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map_a), 0);
|
|
|
|
const int reused_number = dmabuf_a;
|
|
ASSERT_EQ(close(dmabuf_a), 0); // A's mapping stays live; the handle owns it now.
|
|
|
|
// Second BO: force a fresh memfd onto the SAME fd number A's export used, then
|
|
// import + map it. Under the old handle = dmabuf_fd + 1 scheme this PRIME would
|
|
// collide with A's still-live handle and tear down A's BO; with stable handles it
|
|
// must mint a distinct handle and leave A untouched.
|
|
int tmp = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(tmp, 0);
|
|
int dmabuf_b = reused_number;
|
|
// If make_sized_memfd already recycled the freed number for `tmp`, it is already
|
|
// the reused number — dup2 onto itself then close would leave it closed, so just
|
|
// use it directly. Otherwise move it onto the reused number.
|
|
if (tmp != dmabuf_b) {
|
|
ASSERT_EQ(dup2(tmp, dmabuf_b), dmabuf_b);
|
|
ASSERT_EQ(close(tmp), 0);
|
|
}
|
|
uint32_t handle_b = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_b, &handle_b));
|
|
ASSERT_NE(handle_b, 0u);
|
|
EXPECT_NE(handle_a, handle_b)
|
|
<< "a recycled dmabuf fd number must not collide with a live handle";
|
|
|
|
drm_amdgpu_gem_va map_b{};
|
|
map_b.handle = handle_b;
|
|
map_b.operation = AMDGPU_VA_OP_MAP;
|
|
map_b.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map_b.va_address = va_b;
|
|
map_b.map_size = kBoSize;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map_b), 0);
|
|
close(dmabuf_b); // B's mapping stays live via its handle.
|
|
|
|
// A must still be fully live despite B reusing its export fd number: A's own
|
|
// UNMAP through A's handle must still succeed (proving B's PRIME did not tear
|
|
// down A's mapping).
|
|
drm_amdgpu_gem_va unmap_a{};
|
|
unmap_a.handle = handle_a;
|
|
unmap_a.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_a.va_address = va_a;
|
|
unmap_a.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_a), 0)
|
|
<< "reusing A's export fd number for B must not tear down A's mapping";
|
|
|
|
// Closing A's handle must not disturb B: B's UNMAP through its own handle must
|
|
// still succeed afterward.
|
|
EXPECT_EQ(gem_close(drm, handle_a), 0);
|
|
drm_amdgpu_gem_va unmap_b{};
|
|
unmap_b.handle = handle_b;
|
|
unmap_b.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_b.va_address = va_b;
|
|
unmap_b.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_b), 0)
|
|
<< "closing the recycled-fd sibling handle must not tear down this BO's mapping";
|
|
|
|
EXPECT_EQ(gem_close(drm, handle_b), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// UNMAP with a handle that does not own the exact range must fail rather than
|
|
// tear down another handle's PTEs or report a phantom success.
|
|
TEST(InterposerGemTest, UnmapWithWrongHandleFails) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBoSize = 0x1000;
|
|
int dmabuf_a = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf_a, 0);
|
|
int dmabuf_b = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf_b, 0);
|
|
uint32_t handle_a = 0, handle_b = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_a, &handle_a));
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_b, &handle_b));
|
|
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va_a = 0x1000000000ULL;
|
|
drm_amdgpu_gem_va map_a{};
|
|
map_a.handle = handle_a;
|
|
map_a.operation = AMDGPU_VA_OP_MAP;
|
|
map_a.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map_a.va_address = va_a;
|
|
map_a.map_size = kBoSize;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map_a), 0);
|
|
|
|
// UNMAP of A's range through B's handle must fail (B does not own it) and leave
|
|
// A's mapping intact, so A's own UNMAP then succeeds.
|
|
drm_amdgpu_gem_va bad_unmap{};
|
|
bad_unmap.handle = handle_b;
|
|
bad_unmap.operation = AMDGPU_VA_OP_UNMAP;
|
|
bad_unmap.va_address = va_a;
|
|
bad_unmap.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &bad_unmap), -1);
|
|
|
|
drm_amdgpu_gem_va good_unmap{};
|
|
good_unmap.handle = handle_a;
|
|
good_unmap.operation = AMDGPU_VA_OP_UNMAP;
|
|
good_unmap.va_address = va_a;
|
|
good_unmap.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &good_unmap), 0)
|
|
<< "A's mapping must survive a wrong-handle UNMAP attempt";
|
|
|
|
EXPECT_EQ(gem_close(drm, handle_a), 0);
|
|
EXPECT_EQ(gem_close(drm, handle_b), 0);
|
|
EXPECT_EQ(close(dmabuf_a), 0);
|
|
EXPECT_EQ(close(dmabuf_b), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// PRIME_FD_TO_HANDLE dups the export fd internally, so the caller may close its
|
|
// export fd BEFORE GEM_VA MAP and the deferred lazy backing mmap must still succeed
|
|
// (the handle owns a private dup of the dmabuf that outlives the caller's fd).
|
|
TEST(InterposerGemTest, MapSucceedsAfterExportFdClosed) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBoSize = 0x1000;
|
|
int dmabuf = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf, 0);
|
|
uint32_t handle = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf, &handle));
|
|
ASSERT_NE(handle, 0u);
|
|
|
|
// Close the export fd BEFORE mapping. Under the old scheme (store the raw fd for a
|
|
// later lazy mmap) this would either fail or map an unrelated recycled fd. Force a
|
|
// recycle of the number so a lingering raw-fd dependency would be caught.
|
|
const int reused_number = dmabuf;
|
|
ASSERT_EQ(close(dmabuf), 0);
|
|
int filler = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(filler, 0);
|
|
if (filler != reused_number) {
|
|
ASSERT_EQ(dup2(filler, reused_number), reused_number);
|
|
ASSERT_EQ(close(filler), 0);
|
|
}
|
|
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va = 0x1000000000ULL;
|
|
drm_amdgpu_gem_va map{};
|
|
map.handle = handle;
|
|
map.operation = AMDGPU_VA_OP_MAP;
|
|
map.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map.va_address = va;
|
|
map.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &map), 0)
|
|
<< "GEM_VA MAP must succeed via the handle's private dmabuf dup after the "
|
|
"caller closed (and recycled) its export fd";
|
|
|
|
drm_amdgpu_gem_va unmap{};
|
|
unmap.handle = handle;
|
|
unmap.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap.va_address = va;
|
|
unmap.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap), 0);
|
|
EXPECT_EQ(gem_close(drm, handle), 0);
|
|
EXPECT_EQ(close(reused_number), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// AMDGPU_VA_OP_REPLACE at a VA that overlaps an existing mapping of a DIFFERENT size
|
|
// must evict the old mapping (by its own extent), not silently install overlapping
|
|
// PTEs. After a REPLACE, the old owner's stale range must be gone: its handle's UNMAP
|
|
// of the original range must fail, and no double-unmap can occur.
|
|
TEST(InterposerGemTest, ReplaceEvictsOverlappingDifferentSizeRange) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBigBo = 0x4000;
|
|
constexpr size_t kSmallBo = 0x1000;
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va = 0x1000000000ULL;
|
|
|
|
int dmabuf_a = make_sized_memfd(kBigBo);
|
|
ASSERT_GE(dmabuf_a, 0);
|
|
int dmabuf_b = make_sized_memfd(kSmallBo);
|
|
ASSERT_GE(dmabuf_b, 0);
|
|
uint32_t handle_a = 0, handle_b = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_a, &handle_a));
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_b, &handle_b));
|
|
|
|
// A maps a large range at va.
|
|
drm_amdgpu_gem_va map_a{};
|
|
map_a.handle = handle_a;
|
|
map_a.operation = AMDGPU_VA_OP_MAP;
|
|
map_a.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map_a.va_address = va;
|
|
map_a.map_size = kBigBo;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map_a), 0);
|
|
|
|
// B REPLACEs a SMALLER range at the same base va. This overlaps A's range but is
|
|
// not identical; it must still evict A's mapping.
|
|
drm_amdgpu_gem_va replace_b{};
|
|
replace_b.handle = handle_b;
|
|
replace_b.operation = AMDGPU_VA_OP_REPLACE;
|
|
replace_b.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
replace_b.va_address = va;
|
|
replace_b.map_size = kSmallBo;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &replace_b), 0)
|
|
<< "REPLACE overlapping a different-size range must succeed and evict it";
|
|
|
|
// A's original range is gone: A's UNMAP of it must now fail (the record was evicted
|
|
// by the overlap-aware REPLACE, so A cannot double-unmap B's new PTEs).
|
|
drm_amdgpu_gem_va unmap_a{};
|
|
unmap_a.handle = handle_a;
|
|
unmap_a.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_a.va_address = va;
|
|
unmap_a.map_size = kBigBo;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_a), -1)
|
|
<< "A's overlapping range must have been evicted by B's REPLACE";
|
|
|
|
// B's new range is live and its UNMAP succeeds exactly once.
|
|
drm_amdgpu_gem_va unmap_b{};
|
|
unmap_b.handle = handle_b;
|
|
unmap_b.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_b.va_address = va;
|
|
unmap_b.map_size = kSmallBo;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_b), 0);
|
|
|
|
EXPECT_EQ(gem_close(drm, handle_a), 0);
|
|
EXPECT_EQ(gem_close(drm, handle_b), 0);
|
|
EXPECT_EQ(close(dmabuf_a), 0);
|
|
EXPECT_EQ(close(dmabuf_b), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// After B REPLACEs A's range, closing A must NOT tear down B's PTEs: the REPLACE
|
|
// transferred ownership of the VA to B, so A's teardown has nothing to unmap there
|
|
// and B's mapping stays live (its own UNMAP still succeeds afterward).
|
|
TEST(InterposerGemTest, ReplaceTransfersOwnershipAcrossOldOwnerClose) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBoSize = 0x1000;
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va = 0x1000000000ULL;
|
|
|
|
int dmabuf_a = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf_a, 0);
|
|
int dmabuf_b = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf_b, 0);
|
|
uint32_t handle_a = 0, handle_b = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_a, &handle_a));
|
|
ASSERT_TRUE(prime_import(drm, dmabuf_b, &handle_b));
|
|
|
|
drm_amdgpu_gem_va map_a{};
|
|
map_a.handle = handle_a;
|
|
map_a.operation = AMDGPU_VA_OP_MAP;
|
|
map_a.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map_a.va_address = va;
|
|
map_a.map_size = kBoSize;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map_a), 0);
|
|
|
|
drm_amdgpu_gem_va replace_b = map_a;
|
|
replace_b.handle = handle_b;
|
|
replace_b.operation = AMDGPU_VA_OP_REPLACE;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &replace_b), 0);
|
|
|
|
// Ownership of the VA transferred to B: A must no longer own the range, so A's
|
|
// UNMAP of it fails. This is the load-bearing assertion — gem_va_unmap() reports
|
|
// success purely on process existence, not PTE presence, so without this negative
|
|
// check the later "B's UNMAP succeeds" alone could pass even if REPLACE had failed
|
|
// to evict A's record (A's close would then quietly tear down the shared PTE while
|
|
// B's bookkeeping stayed intact).
|
|
drm_amdgpu_gem_va unmap_a{};
|
|
unmap_a.handle = handle_a;
|
|
unmap_a.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_a.va_address = va;
|
|
unmap_a.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_a), -1)
|
|
<< "A must not own the range after REPLACE transferred it to B";
|
|
|
|
// Close A entirely. Its GEM_CLOSE reap must not unmap va — B owns it now.
|
|
EXPECT_EQ(gem_close(drm, handle_a), 0);
|
|
EXPECT_EQ(close(dmabuf_a), 0);
|
|
|
|
// B's mapping is still live: its UNMAP through its own handle succeeds.
|
|
drm_amdgpu_gem_va unmap_b{};
|
|
unmap_b.handle = handle_b;
|
|
unmap_b.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap_b.va_address = va;
|
|
unmap_b.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap_b), 0)
|
|
<< "B's mapping must survive A's close after REPLACE transferred ownership";
|
|
|
|
EXPECT_EQ(gem_close(drm, handle_b), 0);
|
|
EXPECT_EQ(close(dmabuf_b), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|
|
|
|
// AMDGPU_VA_OP_CLEAR tears down a range's PTEs and updates the owning handle's
|
|
// bookkeeping, so a later GEM_CLOSE of that handle does not double-unmap the range.
|
|
// A UNMAP of the cleared range must fail (it is gone), and GEM_CLOSE must still
|
|
// succeed cleanly.
|
|
TEST(InterposerGemTest, ClearUpdatesOwnerBookkeeping) {
|
|
int kfd = open_kfd();
|
|
ASSERT_GE(kfd, 0);
|
|
ASSERT_TRUE(kfd_version_ok(kfd));
|
|
int drm = open_drm_render();
|
|
if (drm < 0)
|
|
GTEST_SKIP() << "synthetic DRM render node unavailable in this configuration";
|
|
|
|
constexpr size_t kBoSize = 0x1000;
|
|
const unsigned long gem_va = DRM_AMDGPU_GEM_VA_request();
|
|
const uint64_t va = 0x1000000000ULL;
|
|
|
|
int dmabuf = make_sized_memfd(kBoSize);
|
|
ASSERT_GE(dmabuf, 0);
|
|
uint32_t handle = 0;
|
|
ASSERT_TRUE(prime_import(drm, dmabuf, &handle));
|
|
|
|
drm_amdgpu_gem_va map{};
|
|
map.handle = handle;
|
|
map.operation = AMDGPU_VA_OP_MAP;
|
|
map.flags = AMDGPU_VM_PAGE_READABLE | AMDGPU_VM_PAGE_WRITEABLE;
|
|
map.va_address = va;
|
|
map.map_size = kBoSize;
|
|
ASSERT_EQ(ioctl(drm, gem_va, &map), 0);
|
|
|
|
// CLEAR is handle-agnostic; it uses handle 0 and tears down whatever owns the VA.
|
|
drm_amdgpu_gem_va clear{};
|
|
clear.handle = 0;
|
|
clear.operation = AMDGPU_VA_OP_CLEAR;
|
|
clear.va_address = va;
|
|
clear.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &clear), 0) << "CLEAR of a mapped range must succeed";
|
|
|
|
// The range is gone from the owner's bookkeeping: an UNMAP now fails.
|
|
drm_amdgpu_gem_va unmap{};
|
|
unmap.handle = handle;
|
|
unmap.operation = AMDGPU_VA_OP_UNMAP;
|
|
unmap.va_address = va;
|
|
unmap.map_size = kBoSize;
|
|
EXPECT_EQ(ioctl(drm, gem_va, &unmap), -1) << "CLEAR must have removed the range record";
|
|
|
|
// GEM_CLOSE must not double-unmap the already-cleared range.
|
|
EXPECT_EQ(gem_close(drm, handle), 0);
|
|
EXPECT_EQ(close(dmabuf), 0);
|
|
EXPECT_EQ(close(drm), 0);
|
|
EXPECT_EQ(close(kfd), 0);
|
|
}
|