Files
TheRock/rocm-systems/projects/hip-tests/catch/unit/oob/oob_module.cc
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

360 lines
12 KiB
C++

#include <hip_test_common.hh>
#include <hip/hiprtc.h>
#include "hip_test_filesystem.hh"
#include <cstdint>
#include <cstring>
#include <fstream>
#include <string>
#include <string_view>
#include <vector>
#if HT_AMD
// POSIX-only helpers for the fat-binary readable-size bounds tests below. This
// directory (unit/oob) is gated to UNIX, so these headers are always available.
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdlib>
#include <system_error>
#endif
// Only the valid code object is shipped alongside the test binary (cwd-relative
// basename). The malformed variants that exercise getElfSize's rejection paths
// are synthesized here at runtime instead of being generated and installed as
// fixtures: installing corrupt ELFs broke DEB packaging, because dh_makeshlibs
// runs `objdump -p` on every installed ELF and aborts on a bad header. The
// mutation recipes below are ported from the former gen_evil_elfs.py.
constexpr std::string_view kValidModule = "oob_kernel.code";
namespace {
using Bytes = std::vector<char>;
// Elf64_Ehdr field offsets we mutate.
constexpr size_t kEShoff = 40; // e_shoff (u64)
constexpr size_t kEShentsize = 58; // e_shentsize (u16)
constexpr size_t kEShnum = 60; // e_shnum (u16)
constexpr char kElfMagic[4] = {0x7f, 'E', 'L', 'F'};
Bytes ReadFile(std::string_view path) {
std::ifstream f(std::string(path), std::ios::binary | std::ios::ate);
REQUIRE(f.good());
std::streamsize size = f.tellg();
REQUIRE(size >= 0);
f.seekg(0);
Bytes buf(static_cast<size_t>(size));
f.read(buf.data(), size);
REQUIRE(f.good());
return buf;
}
// AMDGPU is little-endian and so are all supported hosts, so a plain copy matches
// the '<' packing the original Python used.
uint16_t Rd16(const Bytes& b, size_t o) {
uint16_t v;
std::memcpy(&v, b.data() + o, sizeof(v));
return v;
}
uint64_t Rd64(const Bytes& b, size_t o) {
uint64_t v;
std::memcpy(&v, b.data() + o, sizeof(v));
return v;
}
void Wr16(Bytes& b, size_t o, uint16_t v) { std::memcpy(b.data() + o, &v, sizeof(v)); }
void Wr32(Bytes& b, size_t o, uint32_t v) { std::memcpy(b.data() + o, &v, sizeof(v)); }
void Wr64(Bytes& b, size_t o, uint64_t v) { std::memcpy(b.data() + o, &v, sizeof(v)); }
// Extract the first AMDGPU ELF from a clang offload bundle (pass a raw ELF through).
// Keeps arch aligned with whatever oob_kernel.code was just compiled for.
Bytes ExtractElf(const Bytes& data) {
if (data.size() >= 4 && std::memcmp(data.data(), kElfMagic, 4) == 0) {
return data;
}
static constexpr char kBundleMagic[] = "__CLANG_OFFLOAD_BUNDLE__";
// Magic (24) + entry count (8) must be present before any header read.
REQUIRE(data.size() >= 32);
REQUIRE(std::memcmp(data.data(), kBundleMagic, 24) == 0);
const uint64_t total = data.size();
size_t off = 24;
uint64_t num = Rd64(data, off);
off += 8;
for (uint64_t i = 0; i < num; ++i) {
// Each entry header is 24 bytes (offset, size, id_len) + a variable id.
REQUIRE(off + 24 <= total);
uint64_t entry_off = Rd64(data, off);
uint64_t entry_size = Rd64(data, off + 8);
uint64_t id_len = Rd64(data, off + 16);
REQUIRE(id_len <= total - (off + 24));
off += 24 + id_len;
// Reject an entry whose payload range overflows or spills past the buffer.
if (entry_size >= 4 && entry_off <= total && entry_size <= total - entry_off &&
std::memcmp(data.data() + entry_off, kElfMagic, 4) == 0) {
return Bytes(data.begin() + entry_off, data.begin() + entry_off + entry_size);
}
}
FAIL("no ELF entry in bundle");
return {};
}
// e_shnum past the buffer.
Bytes MakeHugeShnum(const Bytes& elf) {
Bytes b = elf;
Wr16(b, kEShnum, 0xFFFF);
return b;
}
// e_shoff: section table start past end of file.
Bytes MakeBadShoff(const Bytes& elf) {
Bytes b = elf;
Wr64(b, kEShoff, uint64_t(1) << 40);
return b;
}
// e_shoff + e_shnum: table starts in bounds but its end spills past the file.
Bytes MakeTableSpill(const Bytes& elf) {
Bytes b = elf;
Wr64(b, kEShoff, elf.size() - 10);
Wr16(b, kEShnum, 1);
return b;
}
// sh_offset + sh_size on one section overflows a uint64.
Bytes MakeShOverflow(const Bytes& elf) {
Bytes b = elf;
uint64_t shoff = Rd64(elf, kEShoff);
uint16_t shentsize = Rd16(elf, kEShentsize);
size_t s5 = shoff + 5 * shentsize;
REQUIRE(s5 + 40 <= b.size());
Wr32(b, s5 + 4, 1); // sh_type = SHT_PROGBITS
Wr64(b, s5 + 24, ~uint64_t(0)); // sh_offset -> overflow on + sh_size
Wr64(b, s5 + 32, 0x10); // sh_size
return b;
}
// Writes a byte buffer to a uniquely-named temp .code file and removes it on scope
// exit, so the file-backed loader path (hipModuleLoad) sees an exact file size.
class TempCodeObject {
public:
TempCodeObject(std::string_view name, const Bytes& data) {
// Address of the source buffer gives a unique-enough name per run without an
// OS-specific call (getpid), matching the pattern in hrr_workload_test.cc.
path_ = fs::temp_directory_path() /
(std::string("oob_") + std::string(name) + "_" +
std::to_string(reinterpret_cast<uintptr_t>(data.data())) + ".code");
std::ofstream f(path_, std::ios::binary);
f.write(data.data(), static_cast<std::streamsize>(data.size()));
f.close();
REQUIRE(f.good());
}
~TempCodeObject() {
std::error_code ec;
fs::remove(path_, ec);
}
TempCodeObject(const TempCodeObject&) = delete;
TempCodeObject& operator=(const TempCodeObject&) = delete;
std::string path() const { return path_.string(); }
private:
fs::path path_;
};
} // namespace
// File-backed path: hipModuleLoad(fname) - image_size_ is the exact file size.
HIP_TEST_CASE(OOB_hip_module_load_over) {
Bytes elf = ExtractElf(ReadFile(kValidModule));
SECTION("valid - sanity") {
hipModule_t module{};
HIP_CHECK(hipModuleLoad(&module, std::string(kValidModule).c_str()));
HIP_CHECK(hipModuleUnload(module));
}
SECTION("huge shnum") {
TempCodeObject fixture("huge_shnum", MakeHugeShnum(elf));
hipModule_t module{};
HIP_CHECK_ERROR(hipModuleLoad(&module, fixture.path().c_str()), hipErrorInvalidImage);
}
SECTION("bad shoff") {
TempCodeObject fixture("bad_shoff", MakeBadShoff(elf));
hipModule_t module{};
HIP_CHECK_ERROR(hipModuleLoad(&module, fixture.path().c_str()), hipErrorInvalidImage);
}
SECTION("table spill") {
TempCodeObject fixture("table_spill", MakeTableSpill(elf));
hipModule_t module{};
HIP_CHECK_ERROR(hipModuleLoad(&module, fixture.path().c_str()), hipErrorInvalidImage);
}
SECTION("sh overflow") {
TempCodeObject fixture("sh_overflow", MakeShOverflow(elf));
hipModule_t module{};
HIP_CHECK_ERROR(hipModuleLoad(&module, fixture.path().c_str()), hipErrorInvalidImage);
}
}
// In-memory path: hipModuleLoadData carries no length, so only malformations
// detectable without one can be asserted. Length-dependent cases (e.g. a
// corrupt e_shoff) are covered against the file path in OOB_hip_module_load_over.
HIP_TEST_CASE(OOB_hip_module_load_data_over) {
Bytes elf = ExtractElf(ReadFile(kValidModule));
SECTION("sh overflow in-memory") {
Bytes buf = MakeShOverflow(elf);
hipModule_t module{};
HIP_CHECK_ERROR(hipModuleLoadData(&module, buf.data()), hipErrorInvalidImage);
}
}
// Runtime-compiled code objects live in an anonymous heap buffer and must still
// load through hipModuleLoadData after the bounds hardening.
HIP_TEST_CASE(OOB_hiprtc_roundtrip_loads) {
static constexpr char kSource[] = "extern \"C\" __global__ void nop() {}\n";
hipDeviceProp_t props;
HIP_CHECK(hipGetDeviceProperties(&props, 0));
std::string arch = std::string("--offload-arch=") + props.gcnArchName;
const char* options[] = {arch.c_str()};
hiprtcProgram prog;
HIPRTC_CHECK(hiprtcCreateProgram(&prog, kSource, "nop.cu", 0, nullptr, nullptr));
HIPRTC_CHECK(hiprtcCompileProgram(prog, 1, options));
size_t code_size = 0;
HIPRTC_CHECK(hiprtcGetCodeSize(prog, &code_size));
std::vector<char> code(code_size);
HIPRTC_CHECK(hiprtcGetCode(prog, code.data()));
HIPRTC_CHECK(hiprtcDestroyProgram(&prog));
hipModule_t module{};
HIP_CHECK(hipModuleLoadData(&module, code.data()));
HIP_CHECK(hipModuleUnload(module));
}
// ---------------------------------------------------------------------------
// Size-bounds tests for fat-binary parsing. Only file-backed loads have an
// exact size, so bounds are asserted there; pointer inputs carry no length.
// ---------------------------------------------------------------------------
#if HT_AMD
namespace {
// Magic string for ROCm compressed clang offload bundles.
constexpr char kCompressedBundleMagic[] = "CCOB";
// Builds a `size`-byte compressed offload bundle: the "CCOB" magic with
// `total_size` written to the totalSize field (offset 8); remaining bytes zero.
Bytes MakeCompressedBundle(size_t size, uint32_t total_size) {
REQUIRE(size >= 12); // magic (4) through totalSize (offset 8, 4 bytes)
Bytes image(size, 0);
std::memcpy(image.data(), kCompressedBundleMagic, 4);
std::memcpy(image.data() + 8, &total_size, sizeof(total_size));
return image;
}
// Read-only file mapping used by the valid-image false-positive guard.
class FileBackedMapping {
public:
explicit FileBackedMapping(const char* path) {
fd_ = open(path, O_RDONLY);
REQUIRE(fd_ >= 0);
struct stat st {};
REQUIRE(fstat(fd_, &st) == 0);
REQUIRE(st.st_size > 0);
size_ = static_cast<size_t>(st.st_size);
data_ = mmap(nullptr, size_, PROT_READ, MAP_PRIVATE, fd_, 0);
REQUIRE(data_ != MAP_FAILED);
}
~FileBackedMapping() {
if (data_ != nullptr && data_ != MAP_FAILED) {
munmap(data_, size_);
}
if (fd_ >= 0) {
close(fd_);
}
}
FileBackedMapping(const FileBackedMapping&) = delete;
FileBackedMapping& operator=(const FileBackedMapping&) = delete;
const void* data() const { return data_; }
size_t size() const { return size_; }
private:
int fd_ = -1;
void* data_ = nullptr;
size_t size_ = 0;
};
} // namespace
/**
* Loads a valid file-backed compressed module and runs its kernel, guarding
* against the bounds checks regressing into false positives.
*/
HIP_TEST_CASE(OOB_hipModuleLoadData_Positive_ValidFileBackedImage) {
FileBackedMapping mapping("oob_copyKernelCompressed.code");
hipModule_t module = nullptr;
HIP_CHECK(hipModuleLoadData(&module, mapping.data()));
REQUIRE(module != nullptr);
hipFunction_t kernel = nullptr;
HIP_CHECK(hipModuleGetFunction(&kernel, module, "copy_ker"));
REQUIRE(kernel != nullptr);
constexpr unsigned int kLen = 64;
constexpr size_t kBytes = kLen * sizeof(int);
std::vector<int> host_in(kLen), host_out(kLen, 0);
for (unsigned int i = 0; i < kLen; ++i) {
host_in[i] = static_cast<int>(i);
}
int* device_in = nullptr;
int* device_out = nullptr;
HIP_CHECK(hipMalloc(&device_in, kBytes));
HIP_CHECK(hipMalloc(&device_out, kBytes));
HIP_CHECK(hipMemcpy(device_in, host_in.data(), kBytes, hipMemcpyHostToDevice));
struct {
void* Ad;
void* Bd;
size_t size;
} args{device_in, device_out, kLen};
size_t args_size = sizeof(args);
void* config[] = {HIP_LAUNCH_PARAM_BUFFER_POINTER, &args, HIP_LAUNCH_PARAM_BUFFER_SIZE,
&args_size, HIP_LAUNCH_PARAM_END};
HIP_CHECK(hipModuleLaunchKernel(kernel, 1, 1, 1, kLen, 1, 1, 0, nullptr, nullptr, config));
HIP_CHECK(hipDeviceSynchronize());
HIP_CHECK(hipMemcpy(host_out.data(), device_out, kBytes, hipMemcpyDeviceToHost));
for (unsigned int i = 0; i < kLen; ++i) {
REQUIRE(host_out[i] == host_in[i]);
}
HIP_CHECK(hipFree(device_in));
HIP_CHECK(hipFree(device_out));
HIP_CHECK(hipModuleUnload(module));
}
/**
* Loads a file whose compressed header declares a totalSize larger than the
* file. hipModuleLoad must reject it with hipErrorInvalidImage.
*/
HIP_TEST_CASE(OOB_hipModuleLoad_Negative_OutOfBoundsTotalSize) {
// Full header, but totalSize exceeds the file size.
const Bytes payload = MakeCompressedBundle(32, 0xFFFFFFFFu);
TempCodeObject image("fatbin_bounds", payload);
hipModule_t module = nullptr;
HIP_CHECK_ERROR(hipModuleLoad(&module, image.path().c_str()), hipErrorInvalidImage);
}
#endif // HT_AMD