diff --git a/lib/tinykvm/common.hpp b/lib/tinykvm/common.hpp index d0252ed5..44c928c6 100644 --- a/lib/tinykvm/common.hpp +++ b/lib/tinykvm/common.hpp @@ -57,6 +57,12 @@ namespace tinykvm }; struct MachineOptions { + enum class IRelativeMode { + BestEffort = 0, + StrictFail = 1, + ExecuteResolver = 2, + }; + uint64_t max_mem = 16ULL << 20; /* 16MB */ uint32_t max_cow_mem = 0; uint32_t stack_size = 1600UL << 10; /* 1600KB */ @@ -100,6 +106,8 @@ namespace tinykvm bool relocate_fixed_mmap = true; /* Make heap executable, to support JIT. */ bool executable_heap = false; + /* How to handle R_X86_64_IRELATIVE during bootstrap relocation. */ + IRelativeMode irelative_mode = IRelativeMode::StrictFail; /* Enable file-backed memory mappings for large files */ bool mmap_backed_files = false; /* Enable VM snapshot by file-mapping all physical memory diff --git a/lib/tinykvm/machine.cpp b/lib/tinykvm/machine.cpp index b243fd6f..544998d8 100644 --- a/lib/tinykvm/machine.cpp +++ b/lib/tinykvm/machine.cpp @@ -32,6 +32,7 @@ Machine::Machine(std::string_view binary, const MachineOptions& options) : m_forked {false}, m_just_reset {false}, m_relocate_fixed_mmap {options.relocate_fixed_mmap}, + m_irelative_mode {options.irelative_mode}, memory { vMemory::New(*this, options, options.vmem_base_address, options.vmem_base_address + 0x100000, options.max_mem) }, @@ -65,6 +66,7 @@ Machine::Machine(std::string_view binary, const MachineOptions& options) } this->setup_long_mode(options); + this->execute_pending_irelative_resolvers(options); /* We need to adjust BRK if the kernel end address is above the default BRK start address. */ @@ -94,6 +96,7 @@ Machine::Machine(const Machine& other, const MachineOptions& options) m_forked {true}, m_just_reset {true}, m_relocate_fixed_mmap {options.relocate_fixed_mmap}, + m_irelative_mode {options.irelative_mode}, m_binary {options.binary.empty() ? other.m_binary : options.binary}, memory {*this, options, other.memory}, m_image_base {other.m_image_base}, @@ -170,6 +173,7 @@ void Machine::reset_to(std::string_view binary, const MachineOptions& options) this->remote_disconnect(); this->m_mmap_cache = {}; + this->m_irelative_mode = options.irelative_mode; this->m_mt.reset(nullptr); this->m_signals.reset(nullptr); this->m_fds.reset(nullptr); @@ -178,6 +182,7 @@ void Machine::reset_to(std::string_view binary, const MachineOptions& options) this->vcpu.init(0, *this, options); this->setup_long_mode(options); + this->execute_pending_irelative_resolvers(options); struct tinykvm_regs regs {}; /* Store the registers, so that Machine is ready to go */ this->setup_registers(regs); diff --git a/lib/tinykvm/machine.hpp b/lib/tinykvm/machine.hpp index 73910827..61b7a50b 100644 --- a/lib/tinykvm/machine.hpp +++ b/lib/tinykvm/machine.hpp @@ -323,6 +323,7 @@ struct Machine void elf_loader(std::string_view binary, const MachineOptions&); void elf_load_ph(std::string_view binary, const MachineOptions&, const void*); void dynamic_linking(std::string_view binary, const MachineOptions&); + void execute_pending_irelative_resolvers(const MachineOptions&); bool relocate_section(const char* section_name, const char* sym_section); bool relocate_relr_section(const char* section_name); void setup_long_mode(const MachineOptions&); @@ -349,9 +350,15 @@ struct Machine bool m_verbose_system_calls = false; bool m_verbose_mmap_syscalls = false; bool m_verbose_thread_syscalls = false; + MachineOptions::IRelativeMode m_irelative_mode = MachineOptions::IRelativeMode::StrictFail; void* m_userdata = nullptr; std::string_view m_binary; + struct PendingIRelative { + address_t target_addr; + address_t resolver_addr; + }; + std::vector m_pending_irelative; vMemory memory; // guest memory diff --git a/lib/tinykvm/machine_elf.cpp b/lib/tinykvm/machine_elf.cpp index ac2f7f4c..d094fdb7 100644 --- a/lib/tinykvm/machine_elf.cpp +++ b/lib/tinykvm/machine_elf.cpp @@ -374,10 +374,25 @@ bool Machine::relocate_section(const char* section_name, const char* sym_section if (rtype == R_X86_64_RELATIVE) { *(address_t*) memory.safely_at(addr, sizeof(address_t)) = this->m_image_base + rela_addr[i].r_addend; } else { - /* Best-effort bootstrap handling for IFUNC relocations. - A full implementation must evaluate the resolver and write - its return value. */ - *(address_t*) memory.safely_at(addr, sizeof(address_t)) = this->m_image_base + rela_addr[i].r_addend; + switch (this->m_irelative_mode) + { + case MachineOptions::IRelativeMode::StrictFail: + throw MachineException( + "R_X86_64_IRELATIVE encountered in strict mode", rela_addr[i].r_offset); + case MachineOptions::IRelativeMode::BestEffort: + /* Bootstrap fallback for IFUNC relocations. + Full correctness requires executing the guest resolver and + storing its return value. */ + *(address_t*) memory.safely_at(addr, sizeof(address_t)) = this->m_image_base + rela_addr[i].r_addend; + break; + case MachineOptions::IRelativeMode::ExecuteResolver: + /* Defer resolver execution until long mode/register setup is complete. */ + this->m_pending_irelative.push_back(PendingIRelative{ + addr, + this->m_image_base + rela_addr[i].r_addend, + }); + break; + } } } else { if constexpr (VERBOSE_LOADER) { @@ -388,6 +403,44 @@ bool Machine::relocate_section(const char* section_name, const char* sym_section return true; } +void Machine::execute_pending_irelative_resolvers(const MachineOptions& options) +{ + if (this->m_pending_irelative.empty()) { + return; + } + + if (this->m_irelative_mode != MachineOptions::IRelativeMode::ExecuteResolver) { + this->m_pending_irelative.clear(); + return; + } + + for (const auto& pending : this->m_pending_irelative) + { + if (!memory.safely_within(pending.target_addr, sizeof(address_t))) { + throw MachineException("IRELATIVE target is outside guest memory", pending.target_addr); + } + if (!memory.safely_within(pending.resolver_addr, 1)) { + throw MachineException("IRELATIVE resolver is outside guest memory", pending.resolver_addr); + } + + /* Execute resolver in guest context and write returned function address. */ + this->timed_vmcall(pending.resolver_addr, 0.25f); + const auto regs = this->registers(); + const address_t resolved = regs.rax; + + *(address_t*) memory.safely_at(pending.target_addr, sizeof(address_t)) = resolved; + + if (options.verbose_loader) { + printf("* IRELATIVE resolver %p -> %p written to %p\n", + (void*) pending.resolver_addr, + (void*) resolved, + (void*) pending.target_addr); + } + } + + this->m_pending_irelative.clear(); +} + bool Machine::relocate_relr_section(const char* section_name) { const auto* relr = section_by_name(m_binary, section_name); @@ -446,6 +499,7 @@ void Machine::dynamic_linking(std::string_view binary, const MachineOptions& opt { (void)binary; (void)options; + this->m_pending_irelative.clear(); this->relocate_relr_section(".relr.dyn"); this->relocate_section(".rela.dyn", ".dynsym"); //this->relocate_section(".rela.plt", ".dynsym"); diff --git a/tests/unit/elf.cpp b/tests/unit/elf.cpp index ebb7e58c..fcfbd0fb 100644 --- a/tests/unit/elf.cpp +++ b/tests/unit/elf.cpp @@ -2,36 +2,158 @@ #include #include +#include +#include +#include +#include extern std::vector load_file(const std::string& filename); +extern std::vector build_and_load(const std::string& code); static const uint64_t MAX_MEMORY = 8ul << 20; /* 8MB */ static const std::vector env{ "LC_TYPE=C", "LC_ALL=C", "USER=root"}; static const std::vector ld_linux_x86_64_so = load_file("/lib64/ld-linux-x86-64.so.2"); -TEST_CASE("Initialize KVM", "[Initialize]") +static std::string current_dir_path() { - tinykvm::Machine::init(); + char cwd[PATH_MAX]; + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + throw std::runtime_error("Failed to resolve current directory"); + } + return std::string(cwd); } -TEST_CASE("Verify dynamic Rust ELF", "[ELF]") +static std::string rust_elf_path() { - std::string guest_filename - = std::string(get_current_dir_name()) + "/../unit/elf/rust.elf"; - // Make filename absolute + std::string guest_filename = current_dir_path() + "/../unit/elf/rust.elf"; char abs_path[PATH_MAX]; realpath(guest_filename.c_str(), abs_path); - guest_filename = abs_path; + return std::string(abs_path); +} + +static Elf64_Shdr* section_by_name_mut(std::vector& elf, const char* name) +{ + if (elf.size() < sizeof(Elf64_Ehdr)) { + throw std::runtime_error("ELF too small for header"); + } + auto* ehdr = reinterpret_cast(elf.data()); + if (ehdr->e_shoff + ehdr->e_shnum * sizeof(Elf64_Shdr) > elf.size()) { + throw std::runtime_error("ELF section table outside binary"); + } + auto* shdr = reinterpret_cast(elf.data() + ehdr->e_shoff); + if (ehdr->e_shstrndx >= ehdr->e_shnum) { + throw std::runtime_error("Invalid ELF shstrndx"); + } + const auto& shstrtab = shdr[ehdr->e_shstrndx]; + if (shstrtab.sh_offset + shstrtab.sh_size > elf.size()) { + throw std::runtime_error("ELF shstrtab outside binary"); + } + const char* strings = reinterpret_cast(elf.data() + shstrtab.sh_offset); + for (uint16_t i = 0; i < ehdr->e_shnum; i++) + { + const char* shname = strings + shdr[i].sh_name; + if (strcmp(shname, name) == 0) { + return &shdr[i]; + } + } + return nullptr; +} + +static std::vector make_malformed_relr_size(std::vector elf) +{ + auto* relr = section_by_name_mut(elf, ".relr.dyn"); + if (relr == nullptr) { + throw std::runtime_error("ELF is missing .relr.dyn section"); + } + // Force non-word-sized section length to trigger strict malformed check. + relr->sh_size += 1; + return elf; +} + +static std::vector make_malformed_relr_sequence(std::vector elf) +{ + auto* relr = section_by_name_mut(elf, ".relr.dyn"); + if (relr == nullptr) { + throw std::runtime_error("ELF is missing .relr.dyn section"); + } + if (relr->sh_size < sizeof(Elf64_Addr)) { + throw std::runtime_error("ELF .relr.dyn section is too small"); + } + if (relr->sh_offset + relr->sh_size > elf.size()) { + throw std::runtime_error("ELF .relr.dyn payload outside binary"); + } + auto* relr_entries = reinterpret_cast(elf.data() + relr->sh_offset); + // First entry as bitmap (LSB=1) is invalid because there is no base address yet. + relr_entries[0] = 1; + return elf; +} + +static std::vector make_relr_oob_target(std::vector elf) +{ + auto* relr = section_by_name_mut(elf, ".relr.dyn"); + if (relr == nullptr) { + throw std::runtime_error("ELF is missing .relr.dyn section"); + } + if (relr->sh_size < sizeof(Elf64_Addr)) { + throw std::runtime_error("ELF .relr.dyn section is too small"); + } + if (relr->sh_offset + relr->sh_size > elf.size()) { + throw std::runtime_error("ELF .relr.dyn payload outside binary"); + } + auto* relr_entries = reinterpret_cast(elf.data() + relr->sh_offset); + // Force direct RELR relocation target far beyond guest VM memory range. + relr_entries[0] = 0x4000000000000000ULL; + return elf; +} + +static std::vector make_rela_too_many(std::vector elf) +{ + auto* rela = section_by_name_mut(elf, ".rela.dyn"); + if (rela == nullptr) { + throw std::runtime_error("ELF is missing .rela.dyn section"); + } + // Trigger relocate_section guard before payload traversal. + rela->sh_size = (600001ULL * sizeof(Elf64_Rela)); + return elf; +} + +TEST_CASE("Initialize KVM", "[Initialize]") +{ + tinykvm::Machine::init(); +} + +TEST_CASE("Verify static ELF without dynamic relocation", "[ELF][no-reloc]") +{ + const auto binary = build_and_load(R"M( +int main(int argc, char** argv) { + (void)argc; + (void)argv; + return 123; +} +)M"); + + tinykvm::Machine machine{binary, {.max_mem = MAX_MEMORY}}; + machine.setup_linux({"program"}, env); + machine.run(2.0f); + + REQUIRE(machine.return_value() == 123); +} + +TEST_CASE("Verify dynamic Rust ELF relocation support", "[ELF][reloc]") +{ + const std::string guest_filename = rust_elf_path(); tinykvm::Machine machine { ld_linux_x86_64_so, { .max_mem = MAX_MEMORY, .verbose_loader = true, .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, .mmap_backed_files = true } }; // Allow opening all files (for dynamic linker) machine.fds().set_open_readable_callback( [&] (std::string& path) -> bool { + (void)path; return true; }); // Load the dynamic linker instead of the program @@ -62,7 +184,7 @@ TEST_CASE("Verify dynamic Rust ELF", "[ELF]") REQUIRE(machine.return_value() == 231); } -TEST_CASE("Verify dynamic Rust ELF (himem)", "[ELF]") +TEST_CASE("Verify dynamic Rust ELF relocation support (himem)", "[ELF][reloc]") { const uint64_t HIMEM = 128ULL << 30; /* 128GB */ tinykvm::Machine machine{ld_linux_x86_64_so, { @@ -71,6 +193,7 @@ TEST_CASE("Verify dynamic Rust ELF (himem)", "[ELF]") .vmem_base_address = HIMEM, .master_direct_memory_writes = true, .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, .mmap_backed_files = true }}; // Use constrained working memory @@ -78,12 +201,13 @@ TEST_CASE("Verify dynamic Rust ELF (himem)", "[ELF]") // Allow opening all files (for dynamic linker) machine.fds().set_open_readable_callback( [&] (std::string& path) -> bool { + (void)path; return true; }); // Load the dynamic linker instead of the program std::vector args; args.push_back("/lib64/ld-linux-x86-64.so.2"); - args.push_back(std::string(get_current_dir_name()) + "/../unit/elf/rust.elf"); + args.push_back(rust_elf_path()); // We need to create a Linux environment for runtimes to work well machine.setup_linux(args, env); REQUIRE(machine.entry_address() > HIMEM); @@ -93,3 +217,170 @@ TEST_CASE("Verify dynamic Rust ELF (himem)", "[ELF]") REQUIRE(machine.return_value() == 231); } + +TEST_CASE("IRELATIVE strict-fail mode rejects dynamic Rust ELF", "[ELF][reloc]") +{ + const std::string guest_filename = rust_elf_path(); + + bool threw = false; + try { + tinykvm::Machine machine { ld_linux_x86_64_so, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::StrictFail, + .mmap_backed_files = true, + } }; + machine.fds().set_open_readable_callback( + [&] (std::string& path) -> bool { + (void)path; + return true; + }); + std::vector args; + args.push_back("/lib64/ld-linux-x86-64.so.2"); + args.push_back(guest_filename); + machine.setup_linux(args, env); + machine.run(4.0f); + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("R_X86_64_IRELATIVE") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("IRELATIVE default mode is strict-fail", "[ELF][reloc]") +{ + const std::string guest_filename = rust_elf_path(); + + bool threw = false; + try { + tinykvm::Machine machine { ld_linux_x86_64_so, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .mmap_backed_files = true, + } }; + machine.fds().set_open_readable_callback( + [&] (std::string& path) -> bool { + (void)path; + return true; + }); + std::vector args; + args.push_back("/lib64/ld-linux-x86-64.so.2"); + args.push_back(guest_filename); + machine.setup_linux(args, env); + machine.run(4.0f); + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("R_X86_64_IRELATIVE") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("Malformed RELR section size fails hard", "[ELF][reloc]") +{ + const auto malformed_ld = make_malformed_relr_size(ld_linux_x86_64_so); + + bool threw = false; + try { + tinykvm::Machine machine { malformed_ld, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, + .mmap_backed_files = true, + } }; + (void)machine; + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("Malformed RELR section") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("Malformed RELR sequence fails hard", "[ELF][reloc]") +{ + const auto malformed_ld = make_malformed_relr_sequence(ld_linux_x86_64_so); + + bool threw = false; + try { + tinykvm::Machine machine { malformed_ld, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, + .mmap_backed_files = true, + } }; + (void)machine; + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("Malformed RELR sequence") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("RELR out-of-bounds target fails hard", "[ELF][reloc]") +{ + const auto malformed_ld = make_relr_oob_target(ld_linux_x86_64_so); + + bool threw = false; + try { + tinykvm::Machine machine { malformed_ld, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, + .mmap_backed_files = true, + } }; + (void)machine; + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("RELR relocation target out of bounds") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("RELA excessive relocation count fails hard", "[ELF][reloc]") +{ + const auto malformed_ld = make_rela_too_many(ld_linux_x86_64_so); + + bool threw = false; + try { + tinykvm::Machine machine { malformed_ld, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::BestEffort, + .mmap_backed_files = true, + } }; + (void)machine; + } catch (const tinykvm::MachineException& ex) { + threw = true; + REQUIRE(std::string(ex.what()).find("Too many relocations") != std::string::npos); + } + + REQUIRE(threw); +} + +TEST_CASE("IRELATIVE execute-resolver mode runs dynamic Rust ELF", "[ELF][reloc]") +{ + const std::string guest_filename = rust_elf_path(); + + tinykvm::Machine machine { ld_linux_x86_64_so, { + .max_mem = MAX_MEMORY, + .executable_heap = true, + .irelative_mode = tinykvm::MachineOptions::IRelativeMode::ExecuteResolver, + .mmap_backed_files = true, + } }; + machine.fds().set_open_readable_callback( + [&] (std::string& path) -> bool { + (void)path; + return true; + }); + std::vector args; + args.push_back("/lib64/ld-linux-x86-64.so.2"); + args.push_back(guest_filename); + machine.setup_linux(args, env); + machine.run(4.0f); + + REQUIRE(machine.return_value() == 231); +}