Linux: port the emulator to a working state on Linux (#117)

Linux: port the emulator to a working state
This commit is contained in:
Stefanos Costa
2026-07-29 01:25:58 +03:00
committed by GitHub
parent 3dc793d859
commit 2b9cba457e
30 changed files with 2320 additions and 381 deletions
+185 -1
View File
@@ -18,6 +18,13 @@
#include <windows.h>
#undef min
#undef max
#else
#include <csignal>
#include <map>
#include <sys/mman.h>
#include <sys/wait.h>
#include <ucontext.h>
#include <unistd.h>
#endif
namespace {
@@ -39,6 +46,123 @@ void Check(bool value, const char *text) {
}
}
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
// POSIX shims for the shared test body.
using DWORD = uint32_t;
constexpr uint32_t PAGE_NOACCESS = 1;
constexpr uint32_t PAGE_READONLY = 2;
constexpr uint32_t PAGE_READWRITE = 3;
constexpr uint32_t MEM_RESERVE = 0;
constexpr uint32_t MEM_COMMIT = 0;
constexpr uint32_t MEM_RELEASE = 0;
int ToHostProt(uint32_t protection) {
switch (protection) {
case PAGE_NOACCESS:
return PROT_NONE;
case PAGE_READONLY:
return PROT_READ;
default:
return PROT_READ | PROT_WRITE;
}
}
uint32_t Protection(const void *address) {
const auto addr = reinterpret_cast<uintptr_t>(address);
std::FILE *maps = std::fopen("/proc/self/maps", "r");
Check(maps != nullptr, "open /proc/self/maps failed");
char line[512];
uint32_t result = 0; // 0 => not mapped at all
while (std::fgets(line, sizeof(line), maps) != nullptr) {
unsigned long start = 0;
unsigned long end = 0;
char perms[8]{};
if (std::sscanf(line, "%lx-%lx %7s", &start, &end, perms) != 3) {
continue;
}
if (addr >= start && addr < end) {
result = perms[1] == 'w' ? PAGE_READWRITE
: perms[0] == 'r' ? PAGE_READONLY
: PAGE_NOACCESS;
break;
}
}
std::fclose(maps);
return result;
}
bool IsWritable(const void *address) { return Protection(address) == PAGE_READWRITE; }
// munmap needs a length where VirtualFree's callers pass 0, so sizes are remembered here.
std::map<void *, size_t> &AllocationSizes() {
static std::map<void *, size_t> sizes;
return sizes;
}
void *VirtualAlloc(void *address, size_t size, DWORD /*type*/, uint32_t protection) {
const int extra = address != nullptr ? MAP_FIXED_NOREPLACE : 0;
void *raw = ::mmap(address, size, ToHostProt(protection),
MAP_PRIVATE | MAP_ANONYMOUS | extra, -1, 0);
if (raw == MAP_FAILED) {
return nullptr;
}
AllocationSizes()[raw] = size;
return raw;
}
int VirtualFree(void *address, size_t /*size*/, DWORD /*type*/) {
auto &sizes = AllocationSizes();
auto it = sizes.find(address);
if (it == sizes.end()) {
return 0;
}
const int ok = ::munmap(address, it->second) == 0 ? 1 : 0;
sizes.erase(it);
return ok;
}
[[maybe_unused]] int VirtualProtect(void *address, size_t size, uint32_t protection,
DWORD *old_protection) {
if (old_protection != nullptr) {
*old_protection = Protection(address);
}
return ::mprotect(address, size, ToHostProt(protection)) == 0 ? 1 : 0;
}
// Two views of one anonymous shared object.
class SharedPage final {
public:
SharedPage(uintptr_t address, uint64_t size) : size_(static_cast<size_t>(size)) {
fd_ = static_cast<int>(::syscall(SYS_memfd_create, "kyty-shared-page", 0u));
Check(fd_ >= 0, "memfd_create failed");
Check(::ftruncate(fd_, static_cast<off_t>(size)) == 0, "ftruncate failed");
guest = static_cast<uint8_t *>(::mmap(reinterpret_cast<void *>(address), size_,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED_NOREPLACE, fd_, 0));
Check(guest == reinterpret_cast<void *>(address), "fixed shared view failed");
backing = static_cast<uint8_t *>(
::mmap(nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
Check(backing != MAP_FAILED && backing != nullptr, "shared backing view failed");
}
~SharedPage() {
Check(::munmap(backing, size_) == 0, "shared backing unmap failed");
Check(::munmap(guest, size_) == 0, "shared guest unmap failed");
Check(::close(fd_) == 0, "shared mapping close failed");
}
SharedPage(const SharedPage &) = delete;
SharedPage &operator=(const SharedPage &) = delete;
uint8_t *guest = nullptr;
uint8_t *backing = nullptr;
private:
size_t size_ = 0;
int fd_ = -1;
};
#endif
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
bool IsWritable(const void *address) {
MEMORY_BASIC_INFORMATION info{};
@@ -84,6 +208,8 @@ class SharedPage final {
private:
HANDLE mapping_ = nullptr;
};
#endif
#if 1
bool DummyFault(void *, PageFaultAccess, uint64_t, uint64_t, PageFaultPhase) noexcept {
return true;
@@ -246,6 +372,7 @@ void UnmapContended() noexcept {
g_unmap_contended.store(true, std::memory_order_release);
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
LONG CALLBACK NativeTrackerFaultHandler(EXCEPTION_POINTERS *exception) {
if (exception == nullptr || exception->ExceptionRecord == nullptr ||
exception->ExceptionRecord->ExceptionCode != EXCEPTION_ACCESS_VIOLATION) {
@@ -266,6 +393,45 @@ LONG CALLBACK NativeTrackerFaultHandler(EXCEPTION_POINTERS *exception) {
? EXCEPTION_CONTINUE_EXECUTION
: EXCEPTION_CONTINUE_SEARCH;
}
#else
// SIGSEGV stands in for the vectored exception handler.
void NativeTrackerFaultHandler(int signal_number, siginfo_t *info, void *native_context) {
auto *context = static_cast<ucontext_t *>(native_context);
const auto error_code = static_cast<uint64_t>(context->uc_mcontext.gregs[REG_ERR]);
const auto access = (error_code & 0x10u) != 0 ? PageFaultAccess::Execute
: (error_code & 0x02u) != 0 ? PageFaultAccess::Write
: PageFaultAccess::Read;
auto *page_manager = g_native_page_manager.load(std::memory_order_acquire);
if (page_manager != nullptr) {
g_native_fault_entered.store(true, std::memory_order_release);
if (page_manager->HandleFault(access, reinterpret_cast<uint64_t>(info->si_addr))) {
return;
}
}
struct sigaction restore {};
restore.sa_handler = SIG_DFL;
sigemptyset(&restore.sa_mask);
::sigaction(signal_number, &restore, nullptr);
}
struct sigaction g_saved_segv_action {};
void *AddVectoredExceptionHandler(unsigned long /*first*/,
void (*handler)(int, siginfo_t *, void *)) {
struct sigaction action {};
action.sa_sigaction = handler;
sigemptyset(&action.sa_mask);
action.sa_flags = SA_SIGINFO;
if (::sigaction(SIGSEGV, &action, &g_saved_segv_action) != 0) {
return nullptr;
}
return reinterpret_cast<void *>(handler);
}
int RemoveVectoredExceptionHandler(void * /*token*/) {
return ::sigaction(SIGSEGV, &g_saved_segv_action, nullptr) == 0 ? 1 : 0;
}
#endif
void TestPendingFaultBlocksUploadConsumption() {
constexpr uintptr_t base = 0x0000000200010000ull;
@@ -978,14 +1144,17 @@ void TestCrossRegionUpload() {
}
void TestFatalPaths() {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
char path[MAX_PATH]{};
Check(GetModuleFileNameA(nullptr, path, MAX_PATH) != 0,
"GetModuleFileName failed");
#endif
for (const char *name : {"gpu-dirty-fault", "gpu-dirty-read", "virtual-gpu-read",
"gpu-dirty-explicit-cpu",
"unmapped", "reentrant-upload",
"writable-upload-race", "gpu-dirty-unmap-race",
"missing-download-bytes"}) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
std::string command = std::string("\"") + path + "\" --death " + name;
std::vector<char> mutable_command(command.begin(), command.end());
mutable_command.push_back('\0');
@@ -1004,6 +1173,21 @@ void TestFatalPaths() {
"MemoryTracker death path used the wrong exit");
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
#else
const pid_t pid = ::fork();
Check(pid >= 0, "fork failed");
if (pid == 0) {
::execl("/proc/self/exe", "MemoryTrackerTests", "--death", name, nullptr);
std::_Exit(0x7e);
}
int status = 0;
Check(::waitpid(pid, &status, 0) == pid, "waitpid failed");
// Exit status carries only the low 8 bits.
const bool fatal_exit = WIFEXITED(status) && (WEXITSTATUS(status) == (321 & 0xff) ||
WEXITSTATUS(status) == (322 & 0xff));
Check(fatal_exit || WIFSIGNALED(status),
"MemoryTracker death path used the wrong exit");
#endif
}
}
#endif
@@ -1011,7 +1195,7 @@ void TestFatalPaths() {
} // namespace
int main(int argc, char **argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#if 1
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
}