Files
TheRock/rocm-systems/emulation/rocjitsu/tests/daemon_api_test.cpp
T
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

459 lines
17 KiB
C++

// Copyright (c) 2026 Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include "rocjitsu/kmd/linux/kfd_ioctl_utils.h"
#include "rocjitsu/kmd/linux/rpc.h"
#include "rocjitsu/rocjitsu.h"
#include <chrono>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <gtest/gtest.h>
#include <iterator>
#include <string>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
#include <utility>
#include <vector>
namespace {
using namespace rocjitsu;
class TempDirectory {
public:
TempDirectory() {
std::string pattern =
(std::filesystem::temp_directory_path() / "rj-daemon-test-XXXXXX").string();
pattern.push_back('\0');
char *created = mkdtemp(pattern.data());
if (created)
path_ = created;
}
~TempDirectory() {
std::error_code error;
std::filesystem::remove_all(path_, error);
}
[[nodiscard]] const std::filesystem::path &path() const { return path_; }
private:
std::filesystem::path path_;
};
const std::string &daemon_json() {
static const std::string config = [] {
std::ifstream input(std::filesystem::path(CONFIG_DIR) / "gfx950_cdna4_kmd.json");
return std::string(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
}();
return config;
}
class TestDaemon {
public:
explicit TestDaemon(const std::filesystem::path &socket_path)
: TestDaemon(daemon_json().c_str(), socket_path.c_str()) {}
TestDaemon(const char *json, const char *socket_path)
: start_status_(rj_daemon_start(json, socket_path, &daemon_)) {}
~TestDaemon() { (void)stop(); }
TestDaemon(const TestDaemon &) = delete;
TestDaemon &operator=(const TestDaemon &) = delete;
TestDaemon(TestDaemon &&) = delete;
TestDaemon &operator=(TestDaemon &&) = delete;
[[nodiscard]] rj_status_t start_status() const { return start_status_; }
[[nodiscard]] rj_daemon_t *get() const { return daemon_; }
[[nodiscard]] rj_daemon_status_t status() const { return rj_daemon_status(daemon_); }
rj_status_t stop() {
rj_daemon_t *daemon = std::exchange(daemon_, nullptr);
return rj_daemon_stop(daemon);
}
private:
rj_daemon_t *daemon_ = nullptr;
rj_status_t start_status_ = ROCJITSU_STATUS_ERROR;
};
int connect_to(const std::filesystem::path &socket_path) {
const std::string path = socket_path.string();
sockaddr_un address{};
if (path.size() >= sizeof(address.sun_path))
return -1;
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0)
return -1;
const auto length = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
if (connect(fd, reinterpret_cast<const sockaddr *>(&address), length) != 0) {
close(fd);
return -1;
}
return fd;
}
RpcHandshakeResponse handshake(int fd, uint32_t request_id = 1) {
RpcHeader request{};
request.opcode = RPC_HANDSHAKE;
request.request_id = request_id;
EXPECT_TRUE(rpc_send_exact(fd, &request, sizeof(request)));
RpcHeader response{};
EXPECT_TRUE(rpc_recv_exact(fd, &response, sizeof(response)));
EXPECT_EQ(response.request_id, request_id);
EXPECT_EQ(response.result, 0);
EXPECT_GE(response.payload_bytes, sizeof(RpcHandshakeResponse));
RpcHandshakeResponse result{};
EXPECT_TRUE(rpc_recv_exact(fd, &result, sizeof(result)));
const size_t paths_size = response.payload_bytes - sizeof(result);
std::string paths(paths_size, '\0');
if (paths_size > 0) {
EXPECT_TRUE(rpc_recv_exact(fd, paths.data(), paths.size()));
}
return result;
}
void close_session(int fd, uint32_t request_id = 2) {
RpcHeader request{};
request.opcode = RPC_CLOSE;
request.request_id = request_id;
ASSERT_TRUE(rpc_send_exact(fd, &request, sizeof(request)));
RpcHeader response{};
ASSERT_TRUE(rpc_recv_exact(fd, &response, sizeof(response)));
EXPECT_EQ(response.request_id, request_id);
close(fd);
}
bool send_ioctl_request(int fd, uint32_t request_id, uint32_t command, const void *arguments,
size_t argument_size, const void *inline_data = nullptr,
size_t inline_size = 0) {
constexpr size_t max_arguments = UINT32_MAX - sizeof(RpcIoctlRequest);
if (argument_size > max_arguments || inline_size > max_arguments - argument_size)
return false;
const auto args_bytes = static_cast<uint32_t>(argument_size + inline_size);
RpcHeader header{};
header.opcode = RPC_IOCTL;
header.request_id = request_id;
header.payload_bytes = static_cast<uint32_t>(sizeof(RpcIoctlRequest) + args_bytes);
RpcIoctlRequest request{.ioctl_cmd = command, .args_bytes = args_bytes};
return rpc_send_exact(fd, &header, sizeof(header)) &&
rpc_send_exact(fd, &request, sizeof(request)) &&
(argument_size == 0 || rpc_send_exact(fd, arguments, argument_size)) &&
(inline_size == 0 || rpc_send_exact(fd, inline_data, inline_size));
}
void expect_wait_events_payload_rejected(TestDaemon &daemon,
const std::filesystem::path &socket_path,
uint32_t num_events, size_t inline_size) {
const int client = connect_to(socket_path);
ASSERT_GE(client, 0);
ASSERT_EQ(handshake(client).version, kRpcProtocolVersion);
kfd_ioctl_wait_events_args arguments{};
arguments.num_events = num_events;
arguments.timeout = 0;
std::vector<uint8_t> inline_events(inline_size);
ASSERT_TRUE(send_ioctl_request(client, 2, AMDKFD_IOC_WAIT_EVENTS, &arguments, sizeof(arguments),
inline_events.data(), inline_events.size()));
RpcHeader response{};
EXPECT_FALSE(rpc_recv_exact(client, &response, sizeof(response)));
EXPECT_EQ(daemon.status(), RJ_DAEMON_STATUS_RUNNING);
close(client);
}
TEST(DaemonApi, RejectsInvalidArguments) {
TempDirectory directory;
const std::string socket = (directory.path() / "daemon.sock").string();
rj_daemon_t *daemon = reinterpret_cast<rj_daemon_t *>(1);
EXPECT_EQ(rj_daemon_start(nullptr, socket.c_str(), &daemon), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(daemon, nullptr);
EXPECT_EQ(rj_daemon_start(daemon_json().c_str(), nullptr, &daemon),
ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(daemon, nullptr);
EXPECT_EQ(rj_daemon_start(daemon_json().c_str(), socket.c_str(), nullptr),
ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(rj_daemon_start(daemon_json().c_str(), "", &daemon), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(daemon, nullptr);
EXPECT_EQ(rj_daemon_status(nullptr), RJ_DAEMON_STATUS_STOPPED);
EXPECT_EQ(rj_daemon_stop(nullptr), ROCJITSU_STATUS_SUCCESS);
}
TEST(DaemonApi, RejectsInvalidJsonAndLongSocketPath) {
TempDirectory directory;
const std::string socket = (directory.path() / "daemon.sock").string();
TestDaemon empty_json("", socket.c_str());
EXPECT_EQ(empty_json.start_status(), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(empty_json.get(), nullptr);
EXPECT_FALSE(std::filesystem::exists(socket));
TestDaemon invalid_json("not json", socket.c_str());
EXPECT_EQ(invalid_json.start_status(), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(invalid_json.get(), nullptr);
EXPECT_FALSE(std::filesystem::exists(socket));
TestDaemon path_as_json("/does/not/exist.json", socket.c_str());
EXPECT_EQ(path_as_json.start_status(), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(path_as_json.get(), nullptr);
EXPECT_FALSE(std::filesystem::exists(socket));
const std::string long_socket(sizeof(sockaddr_un::sun_path), 'x');
TestDaemon invalid_socket(daemon_json().c_str(), long_socket.c_str());
EXPECT_EQ(invalid_socket.start_status(), ROCJITSU_STATUS_INVALID_ARGUMENT);
EXPECT_EQ(invalid_socket.get(), nullptr);
}
TEST(DaemonApi, PreservesExistingSocketAndNonSocketEntries) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon first(socket_path);
ASSERT_EQ(first.start_status(), ROCJITSU_STATUS_SUCCESS);
struct stat before {};
ASSERT_EQ(lstat(socket_path.c_str(), &before), 0);
TestDaemon second(socket_path);
EXPECT_EQ(second.start_status(), ROCJITSU_STATUS_ERROR);
EXPECT_EQ(second.get(), nullptr);
struct stat after {};
ASSERT_EQ(lstat(socket_path.c_str(), &after), 0);
EXPECT_EQ(after.st_dev, before.st_dev);
EXPECT_EQ(after.st_ino, before.st_ino);
EXPECT_EQ(first.status(), RJ_DAEMON_STATUS_RUNNING);
EXPECT_EQ(first.stop(), ROCJITSU_STATUS_SUCCESS);
std::ofstream(socket_path) << "sentinel";
TestDaemon non_socket(socket_path);
EXPECT_EQ(non_socket.start_status(), ROCJITSU_STATUS_ERROR);
EXPECT_EQ(non_socket.get(), nullptr);
std::ifstream input(socket_path);
std::string contents;
input >> contents;
EXPECT_EQ(contents, "sentinel");
}
TEST(DaemonApi, RecoversAbandonedSocket) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
const std::string path = socket_path.string();
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
const auto length = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
const int stale = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
ASSERT_GE(stale, 0);
ASSERT_EQ(bind(stale, reinterpret_cast<const sockaddr *>(&address), length), 0);
close(stale);
{
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
EXPECT_EQ(daemon.status(), RJ_DAEMON_STATUS_RUNNING);
}
EXPECT_FALSE(std::filesystem::exists(socket_path));
}
TEST(DaemonApi, FullListenerQueueDoesNotBlockOrRemoveSocket) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
const std::string path = socket_path.string();
sockaddr_un address{};
address.sun_family = AF_UNIX;
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
const auto length = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
const int listener = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
ASSERT_GE(listener, 0);
ASSERT_EQ(bind(listener, reinterpret_cast<const sockaddr *>(&address), length), 0);
ASSERT_EQ(listen(listener, 1), 0);
const int first = connect_to(socket_path);
const int second = connect_to(socket_path);
ASSERT_GE(first, 0);
ASSERT_GE(second, 0);
struct stat before {};
ASSERT_EQ(lstat(socket_path.c_str(), &before), 0);
const auto start = std::chrono::steady_clock::now();
TestDaemon daemon(socket_path);
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(5));
EXPECT_EQ(daemon.start_status(), ROCJITSU_STATUS_ERROR);
EXPECT_EQ(daemon.get(), nullptr);
struct stat after {};
ASSERT_EQ(lstat(socket_path.c_str(), &after), 0);
EXPECT_EQ(after.st_dev, before.st_dev);
EXPECT_EQ(after.st_ino, before.st_ino);
close(first);
close(second);
close(listener);
}
TEST(DaemonApi, ServesMultipleClientsAndCleansUp) {
TempDirectory directory;
const auto socket_path = directory.path() / "nested" / "daemon.sock";
{
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
EXPECT_NE(daemon.get(), nullptr);
EXPECT_EQ(daemon.status(), RJ_DAEMON_STATUS_RUNNING);
EXPECT_TRUE(std::filesystem::is_socket(socket_path));
const int first = connect_to(socket_path);
const int second = connect_to(socket_path);
ASSERT_GE(first, 0);
ASSERT_GE(second, 0);
const auto first_handshake = handshake(first, 10);
const auto second_handshake = handshake(second, 20);
EXPECT_EQ(first_handshake.version, kRpcProtocolVersion);
EXPECT_EQ(second_handshake.version, kRpcProtocolVersion);
EXPECT_EQ(first_handshake.gpu_id, second_handshake.gpu_id);
close_session(first, 11);
close_session(second, 21);
}
EXPECT_FALSE(std::filesystem::exists(socket_path));
}
TEST(DaemonApi, StopUnblocksActiveAndPartialClients) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
const int active = connect_to(socket_path);
const int partial = connect_to(socket_path);
ASSERT_GE(active, 0);
ASSERT_GE(partial, 0);
EXPECT_EQ(handshake(active).version, kRpcProtocolVersion);
const uint8_t truncated_header[] = {static_cast<uint8_t>(RPC_HANDSHAKE)};
ASSERT_EQ(send(partial, truncated_header, sizeof(truncated_header), MSG_NOSIGNAL),
static_cast<ssize_t>(sizeof(truncated_header)));
const auto start = std::chrono::steady_clock::now();
EXPECT_EQ(daemon.stop(), ROCJITSU_STATUS_SUCCESS);
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(5));
EXPECT_FALSE(std::filesystem::exists(socket_path));
uint8_t byte = 0;
EXPECT_LE(recv(active, &byte, sizeof(byte), 0), 0);
EXPECT_LE(recv(partial, &byte, sizeof(byte), 0), 0);
close(active);
close(partial);
}
TEST(DaemonApi, RejectsMalformedMessagesWithoutStoppingServer) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
{
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
const int malformed = connect_to(socket_path);
ASSERT_GE(malformed, 0);
EXPECT_EQ(handshake(malformed).version, kRpcProtocolVersion);
RpcHeader bad{};
bad.opcode = RPC_IOCTL;
bad.request_id = 2;
bad.payload_bytes = sizeof(RpcIoctlRequest);
RpcIoctlRequest bad_request{};
bad_request.ioctl_cmd = AMDKFD_IOC_GET_VERSION;
bad_request.args_bytes = UINT32_MAX;
ASSERT_TRUE(rpc_send_exact(malformed, &bad, sizeof(bad)));
ASSERT_TRUE(rpc_send_exact(malformed, &bad_request, sizeof(bad_request)));
RpcHeader response{};
EXPECT_FALSE(rpc_recv_exact(malformed, &response, sizeof(response)));
close(malformed);
const int healthy = connect_to(socket_path);
ASSERT_GE(healthy, 0);
EXPECT_EQ(handshake(healthy).version, kRpcProtocolVersion);
close_session(healthy);
EXPECT_EQ(daemon.status(), RJ_DAEMON_STATUS_RUNNING);
}
EXPECT_FALSE(std::filesystem::exists(socket_path));
}
TEST(DaemonApi, AcceptsValidGetVersionIoctlPayload) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
const int client = connect_to(socket_path);
ASSERT_GE(client, 0);
ASSERT_EQ(handshake(client).version, kRpcProtocolVersion);
kfd_ioctl_get_version_args arguments{};
ASSERT_TRUE(send_ioctl_request(client, 2, AMDKFD_IOC_GET_VERSION, &arguments, sizeof(arguments)));
RpcHeader response{};
ASSERT_TRUE(rpc_recv_exact(client, &response, sizeof(response)));
EXPECT_EQ(response.opcode, RPC_IOCTL);
EXPECT_EQ(response.request_id, 2u);
EXPECT_EQ(response.result, 0);
ASSERT_EQ(response.payload_bytes, sizeof(arguments));
ASSERT_TRUE(rpc_recv_exact(client, &arguments, sizeof(arguments)));
EXPECT_EQ(arguments.major_version, KFD_IOCTL_MAJOR_VERSION);
EXPECT_EQ(arguments.minor_version, KFD_IOCTL_MINOR_VERSION);
EXPECT_EQ(daemon.status(), RJ_DAEMON_STATUS_RUNNING);
close_session(client, 3);
}
TEST(DaemonApi, RejectsWaitEventsInlineArrayOverflowWithoutStoppingServer) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
expect_wait_events_payload_rejected(daemon, socket_path, 2, sizeof(kfd_event_data));
}
TEST(DaemonApi, RejectsWaitEventsInlineArrayOneByteShortWithoutStoppingServer) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
expect_wait_events_payload_rejected(daemon, socket_path, 1, sizeof(kfd_event_data) - 1);
}
TEST(DaemonApi, RejectsWaitEventsInlineArrayWithTrailingByteWithoutStoppingServer) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
expect_wait_events_payload_rejected(daemon, socket_path, 1, sizeof(kfd_event_data) + 1);
}
TEST(DaemonApi, DoesNotRemoveAReplacementSocketEntry) {
TempDirectory directory;
const auto socket_path = directory.path() / "daemon.sock";
{
TestDaemon daemon(socket_path);
ASSERT_EQ(daemon.start_status(), ROCJITSU_STATUS_SUCCESS);
ASSERT_EQ(unlink(socket_path.c_str()), 0);
std::ofstream(socket_path) << "replacement";
}
EXPECT_TRUE(std::filesystem::is_regular_file(socket_path));
std::ifstream input(socket_path);
std::string contents;
input >> contents;
EXPECT_EQ(contents, "replacement");
}
} // namespace