diff --git a/.gitignore b/.gitignore index 226ada1..c2a0781 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,7 @@ compile_commands.json CTestTestfile.cmake _deps CMakeUserPresets.json +build/ +# Visual Studio Code +.vscode/ \ No newline at end of file diff --git a/Build.ps1 b/Build.ps1 new file mode 100644 index 0000000..fc1c3b0 --- /dev/null +++ b/Build.ps1 @@ -0,0 +1,115 @@ +param( + [ValidateSet("msvc", "gcc", "android")] + [string]$Target = "msvc", + + [ValidateSet("Debug", "Release")] + [string]$Config = "Release", + + [switch]$Clean, + + [string]$AndroidNDK = $env:ANDROID_NDK_HOME, + + [switch]$Static, + + [switch]$RunTests +) + +$ErrorActionPreference = "Stop" + +$BuildDir = "build/$Target-$Config" +$Generator = "" +$Toolchain = "" +$ExtraArgs = @() + +if ($Clean -and (Test-Path $BuildDir)) { + Remove-Item $BuildDir -Recurse -Force +} + +switch ($Target) { + + "msvc" { + $VsPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat" + + if (-not (Test-Path $VsPath)) { + $VsPath = "${env:ProgramFiles}\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" + } + + if (-not (Test-Path $VsPath)) { + throw "MSVC environment not found. Install Visual Studio C++ build tools." + } + + cmd /c "`"$VsPath`" && set" | ForEach-Object { + if ($_ -match "^(.*?)=(.*)$") { + Set-Item -Path "env:$($matches[1])" -Value $matches[2] + } + } + + $Generator = "Visual Studio 17 2022" + $Arch = "x64" + $ExtraArgs += "-A $Arch" + } + + "gcc" { + $Generator = "Ninja" + $ExtraArgs += "-DCMAKE_C_COMPILER=gcc" + $ExtraArgs += "-DCMAKE_CXX_COMPILER=g++" + $ExtraArgs += "-DCMAKE_BUILD_TYPE=$Config" + } + + "android" { + if (-not $AndroidNDK) { + throw "Android NDK path not set. Pass -AndroidNDK or set ANDROID_NDK_HOME." + } + + $Generator = "Ninja" + + $Toolchain = "$AndroidNDK/build/cmake/android.toolchain.cmake" + + $ExtraArgs += "-DCMAKE_TOOLCHAIN_FILE=$Toolchain" + $ExtraArgs += "-DANDROID_ABI=arm64-v8a" + $ExtraArgs += "-DANDROID_PLATFORM=android-24" + $ExtraArgs += "-DCMAKE_BUILD_TYPE=$Config" + } +} + +$SharedLibs = if ($Static) { "OFF" } else { "ON" } +$ExtraArgs += "-DBUILD_SHARED_LIBS=$SharedLibs" + +Write-Host "=== Building RadHook ===" +Write-Host "Target: $Target" +Write-Host "Config: $Config" +Write-Host "Build dir: $BuildDir" +Write-Host "Shared libs: $SharedLibs" +Write-Host "" + +cmake -S . -B $BuildDir -G $Generator @ExtraArgs + +if ($LASTEXITCODE -ne 0) { + throw "CMake configure failed" +} + +if ($Target -eq "msvc") { + cmake --build $BuildDir --config $Config +} else { + cmake --build $BuildDir +} + +if ($LASTEXITCODE -ne 0) { + throw "Build failed" +} + +if ($RunTests) { + Write-Host "`n=== Running tests ===" + + if ($Target -eq "msvc") { + ctest --test-dir $BuildDir --build-config $Config --output-on-failure + } else { + ctest --test-dir $BuildDir --output-on-failure + } + + if ($LASTEXITCODE -ne 0) { + throw "Tests failed" + } +} + +Write-Host "`nBuild complete: $BuildDir" \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cd8c64c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,90 @@ +cmake_minimum_required(VERSION 3.20) + +project( + radhook + VERSION 1.0.0 + DESCRIPTION "cross-platform function hooking library" + LANGUAGES CXX +) + +option(BUILD_SHARED_LIBS "Build shared library" ON) + +include(CTest) +enable_testing() +add_subdirectory(tests) + +add_library(radhook + src/radhook.cpp +) + +target_include_directories(radhook + PUBLIC + $ + $ +) + +target_compile_features(radhook PUBLIC cxx_std_20) + +if(BUILD_SHARED_LIBS) + target_compile_definitions(radhook PRIVATE RADHOOK_BUILD) + target_compile_definitions(radhook INTERFACE RADHOOK_SHARED) +endif() + +if(WIN32) + target_compile_definitions(radhook PUBLIC RADIUM_PLATFORM_WINDOWS=1) +elseif(ANDROID) + target_compile_definitions(radhook PUBLIC RADIUM_PLATFORM_ANDROID=1) +endif() + +if(MSVC) + target_compile_options(radhook PRIVATE + /W4 + /permissive- + ) +else() + target_compile_options(radhook PRIVATE + -Wall + -Wextra + -Wpedantic + ) +endif() + +target_compile_options(radhook PRIVATE + $<$: + $<$:/O2 /GL> + $<$>:-O3 -fvisibility=hidden> + > +) + +target_link_options(radhook PRIVATE + $<$: + $<$:/OPT:REF /OPT:ICF> + $<$>:-s> + > +) + +set_target_properties(radhook PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN YES +) + +include(GNUInstallDirs) + +install(TARGETS radhook + EXPORT radhookTargets + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install( + FILES include/radhook.hpp + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +install( + EXPORT radhookTargets + FILE radhookTargets.cmake + NAMESPACE Radium:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/radhook +) \ No newline at end of file diff --git a/include/radhook/api.h b/include/radhook/api.h new file mode 100644 index 0000000..b432fd2 --- /dev/null +++ b/include/radhook/api.h @@ -0,0 +1,298 @@ +#ifndef __LIBRADHOOK_API_H__ +#define __LIBRADHOOK_API_H__ + +#include +#include + +#include + +#ifndef RADHOOK_API +# define RADHOOK_API +#endif // RADHOOK_API + +enum class RadHookResult : int { + Success = 0, + AlreadyInstalled, + NotInstalled, + AlreadyEnabled, + AlreadyDisabled, + MemoryAllocFailed, + MemoryProtectFailed, + DisassemblyFailed, + InvalidTarget, + InvalidDetour, + InvalidHandle, + TrampolineTooFar, + Unknown, +}; + +/** + * @brief Creates a human readable name for a status code. + * + * @param result Status code returned by a RadHook API function. + * @return Null-terminated string describing the status code. + */ +RADHOOK_API +const char* +RadHookResultToString( + RadHookResult result +); + +struct RadHookOpaque; +using RadHookHandle = RadHookOpaque*; + +/** + * @brief Creates and installs a hook. + * + * Redirects execution from @p target to @p detour and enables the hook + * immediately upon successful creation. + * + * @param target Function or address to hook. + * @param detour Replacement function to execute. + * @param outHandle Receives the created hook handle on success. Set to + * nullptr on failure. + * + * @return Operation result. + */ +RADHOOK_API +RadHookResult +RadHookCreate( + void* target, + void* detour, + RadHookHandle* outHandle +); + +/** + * @brief Creates and installs a hook from a function pointer. + * + * Convenience overload that accepts a typed function pointer for the target. + * + * @tparam TargetFn Target function pointer type. + * @param target Function to hook. + * @param detour Replacement function. + * @param outHandle Receives the created hook handle on success. + * + * @return Operation result. + */ +template +requires std::is_function_v> +RadHookResult +RadHookCreate( + TargetFn target, + void* detour, + RadHookHandle* outHandle +) { + return RadHookCreate( + reinterpret_cast(target), + detour, + outHandle + ); +} + +/** + * @brief Enables an installed hook. + * + * Re-applies the jump from the target function to its detour. + * + * @param handle Hook handle. + * + * @return RadHookResult::AlreadyEnabled if the hook is already enabled; + * otherwise the result of the operation. + */ +RADHOOK_API +RadHookResult +RadHookEnable( + RadHookHandle handle +); + +/** + * @brief Disables an installed hook. + * + * Restores the original bytes at the target function. + * + * @param handle Hook handle. + * + * @return RadHookResult::AlreadyDisabled if the hook is already disabled; + * otherwise the result of the operation. + */ +RADHOOK_API +RadHookResult +RadHookDisable( + RadHookHandle handle +); + +/** + * @brief Destroys a hook. + * + * Disables the hook if necessary, releases any allocated resources, + * and invalidates the handle. + * + * @param handle Hook handle. + * + * @return Operation result. + */ +RADHOOK_API +RadHookResult +RadHookDestroy( + RadHookHandle handle +); + +/** + * @brief Enables every registered hook. + * + * Hooks that are already enabled are left unchanged. + * + * @return Operation result. + */ +RADHOOK_API +RadHookResult +RadHookEnableAll(); + +/** + * @brief Disables every registered hook. + * + * Hooks that are already disabled are left unchanged. + * + * @return Operation result. + */ +RADHOOK_API +RadHookResult +RadHookDisableAll(); + +/** + * @brief Returns the number of currently registered hooks. + * + * Destroyed hooks are not included. + * + * @return Number of registered hooks. + */ +RADHOOK_API +size_t +RadHookGetCount(); + +/** + * @brief Retrieves registered hook handles. + * + * Copies up to @p maxCount currently registered hook handles into + * @p outHandles. + * + * @param outHandles Destination buffer. + * @param maxCount Maximum number of handles to copy. + * + * @return Number of handles copied. + */ +RADHOOK_API +size_t +RadHookGetHandles( + RadHookHandle* outHandles, + size_t maxCount +); + +/** + * @brief Checks whether a hook handle is valid. + * + * @param handle Hook handle. + * + * @return true if the handle refers to a registered hook; otherwise false. + */ +RADHOOK_API +bool +RadHookIsValid( + RadHookHandle handle +); + +/** + * @brief Checks whether a hook is currently enabled. + * + * @param handle Hook handle. + * + * @return true if the hook is enabled; otherwise false. + */ +RADHOOK_API +bool +RadHookIsEnabled( + RadHookHandle handle +); + +/** + * @brief Returns the hooked target function. + * + * @param handle Hook handle. + * + * @return Pointer to the original target function, or nullptr if the handle + * is invalid. + */ +RADHOOK_API +void* +RadHookGetTarget( + RadHookHandle handle +); + +/** + * @brief Returns the detour function. + * + * @param handle Hook handle. + * + * @return Pointer to the detour function, or nullptr if the handle is + * invalid. + */ +RADHOOK_API +void* +RadHookGetDetour( + RadHookHandle handle +); + +/** + * @brief Returns the trampoline containing the relocated original code. + * + * Calling the returned function executes the original implementation while + * bypassing the installed hook. + * + * @param handle Hook handle. + * + * @return Pointer to the trampoline, or nullptr if unavailable. + */ +RADHOOK_API +void* +RadHookGetOriginal( + RadHookHandle handle +); + +/** + * @brief Returns the trampoline cast to a function pointer type. + * + * @tparam FnPtr Desired function pointer type. + * @param handle Hook handle. + * + * @return Trampoline cast to @p FnPtr. + */ +template +FnPtr +RadHookGetOriginalAs( + RadHookHandle handle +) { + return reinterpret_cast(RadHookGetOriginal(handle)); +} + + /** + * @brief Enumerates currently registered hooks. + * + * Copies up to @p maxCount active hook handles into @p out. + * Handles are written in no particular order. + * + * This function only returns hooks that are currently registered + * and not destroyed. + * + * @param out Destination buffer for hook handles. + * @param maxCount Maximum number of handles to copy. + * + * @return Number of hook handles written into @p out. + */ +RADHOOK_API +size_t +RadHookEnumerate( + RadHookHandle* out, + size_t maxCount +); + +#endif // __LIBRADHOOK_API_H__ \ No newline at end of file diff --git a/include/radhook/radhook.h b/include/radhook/radhook.h new file mode 100644 index 0000000..0bce871 --- /dev/null +++ b/include/radhook/radhook.h @@ -0,0 +1,48 @@ +#ifndef __LIBRADHOOK_HPP__ +#define __LIBRADHOOK_HPP__ + +#include +#include + +#include + +// platform detection +#if !defined(RADIUM_PLATFORM_WINDOWS) && !defined(RADIUM_PLATFORM_ANDROID) +# if defined(_WIN32) || defined(_WIN64) +# define RADIUM_PLATFORM_WINDOWS 1 +# elif defined(__ANDROID__) +# define RADIUM_PLATFORM_ANDROID 1 +# else +# error "radhook: unsupported platform -- define RADIUM_PLATFORM_WINDOWS or RADIUM_PLATFORM_ANDROID" +# endif +#endif + +// architecture detection +#if defined(_M_X64) || defined(__x86_64__) +# define RADHOOK_ARCH_X64 1 +#elif defined(_M_ARM64) || defined(__aarch64__) +# define RADHOOK_ARCH_ARM64 1 +#else +# error "radhook: unsupported architecture -- need x86-64 or aarch64" +#endif + +// api macros +#if defined(_WIN32) +# if defined(RADHOOK_BUILD) +# define RADHOOK_API __declspec(dllexport) +# elif defined(RADHOOK_SHARED) +# define RADHOOK_API __declspec(dllimport) +# else +# define RADHOOK_API +# endif +#else +# if defined(__GNUC__) || defined(__clang__) +# define RADHOOK_API __attribute__((visibility("default"))) +# else +# define RADHOOK_API +# endif +#endif + +#include "api.h" + +#endif // __LIBRADHOOK_HPP__ \ No newline at end of file diff --git a/src/radhook.cpp b/src/radhook.cpp new file mode 100644 index 0000000..e38bb74 --- /dev/null +++ b/src/radhook.cpp @@ -0,0 +1,751 @@ +#include "radhook/radhook.h" + +#include +#include +#include +#include +#include +#include +#include + +#if defined(RADIUM_PLATFORM_WINDOWS) +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +# endif +# include +#elif defined(RADIUM_PLATFORM_ANDROID) +# include +# include +#endif + +#undef min +#undef max + +// result strings +const char* RadHookResultToString(RadHookResult result) { + switch (result) { + case RadHookResult::Success: return "Success"; + case RadHookResult::AlreadyInstalled: return "AlreadyInstalled"; + case RadHookResult::NotInstalled: return "NotInstalled"; + case RadHookResult::AlreadyEnabled: return "AlreadyEnabled"; + case RadHookResult::AlreadyDisabled: return "AlreadyDisabled"; + case RadHookResult::MemoryAllocFailed: return "MemoryAllocFailed"; + case RadHookResult::MemoryProtectFailed: return "MemoryProtectFailed"; + case RadHookResult::DisassemblyFailed: return "DisassemblyFailed"; + case RadHookResult::InvalidTarget: return "InvalidTarget"; + case RadHookResult::InvalidDetour: return "InvalidDetour"; + case RadHookResult::InvalidHandle: return "InvalidHandle"; + case RadHookResult::TrampolineTooFar: return "TrampolineTooFar"; + default: return "Unknown"; + } +} + +// hook record +struct RadHookOpaque { + void* target = nullptr; + void* detour = nullptr; + void* trampoline = nullptr; + + void* trampolineAlloc = nullptr; + size_t trampolineAllocSize = 0; + + unsigned char originalBytes[32] = {}; + size_t originalLength = 0; + + unsigned char stubBytes[32] = {}; + size_t stubLength = 0; + + bool enabled = false; +}; + +namespace { + +std::vector> g_hooks; +std::mutex g_mutex; + +bool IsRegisteredLocked(RadHookHandle handle) { + if (!handle) return false; + for (auto& h : g_hooks) { + if (h.get() == handle) return true; + } + return false; +} + +// platform memory backend +#if defined(RADIUM_PLATFORM_WINDOWS) + +void* AllocExec(size_t size) { + return VirtualAlloc(nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); +} + +void FreeExec(void* p, size_t /*size*/) { + if (p) VirtualFree(p, 0, MEM_RELEASE); +} + +void* AllocExecNear(void* target, size_t size) { + SYSTEM_INFO si; + GetSystemInfo(&si); + const uintptr_t pageSize = si.dwPageSize; + const uintptr_t targetAddr = reinterpret_cast(target); + const uintptr_t margin = 0x7FFF0000ULL; // stay safely under INT32_MAX + + uintptr_t minAddr = reinterpret_cast(si.lpMinimumApplicationAddress); + uintptr_t maxAddr = reinterpret_cast(si.lpMaximumApplicationAddress); + if (targetAddr > margin && targetAddr - margin > minAddr) minAddr = targetAddr - margin; + if (targetAddr + margin < maxAddr) maxAddr = targetAddr + margin; + + uintptr_t alignedTarget = targetAddr & ~(pageSize - 1); + + // search backward from target + for (uintptr_t addr = alignedTarget; addr >= minAddr && addr != 0; ) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi)) == 0) break; + uintptr_t regionBase = reinterpret_cast(mbi.BaseAddress); + if (mbi.State == MEM_FREE) { + uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1); + if (allocBase >= minAddr && allocBase + size <= regionBase + mbi.RegionSize) { + void* p = VirtualAlloc(reinterpret_cast(allocBase), size, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (p) return p; + } + } + if (regionBase <= minAddr || regionBase < pageSize) break; + addr = regionBase - pageSize; + } + + // search forward from target + for (uintptr_t addr = alignedTarget; addr <= maxAddr; ) { + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQuery(reinterpret_cast(addr), &mbi, sizeof(mbi)) == 0) break; + uintptr_t regionBase = reinterpret_cast(mbi.BaseAddress); + if (mbi.State == MEM_FREE) { + uintptr_t allocBase = (regionBase + pageSize - 1) & ~(pageSize - 1); + if (allocBase + size <= regionBase + mbi.RegionSize && allocBase + size <= maxAddr) { + void* p = VirtualAlloc(reinterpret_cast(allocBase), size, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (p) return p; + } + } + if (mbi.RegionSize == 0) break; + addr = regionBase + mbi.RegionSize; + } + + return nullptr; +} + +bool MakeWritableExecutable(void* addr, size_t size, unsigned long* oldProtect) { + DWORD prev = 0; + if (!VirtualProtect(addr, size, PAGE_EXECUTE_READWRITE, &prev)) { + return false; + } + if (oldProtect) *oldProtect = prev; + return true; +} + +void RestoreProtection(void* addr, size_t size, unsigned long oldProtect) { + DWORD tmp; + VirtualProtect(addr, size, static_cast(oldProtect), &tmp); +} + +void FlushICache(void* addr, size_t size) { + FlushInstructionCache(GetCurrentProcess(), addr, size); +} + +#elif defined(RADIUM_PLATFORM_ANDROID) + +size_t PageSize() { + static size_t sz = static_cast(sysconf(_SC_PAGESIZE)); + return sz; +} + +void* AllocExec(size_t size) { + size_t pageSize = PageSize(); + size_t allocSize = (size + pageSize - 1) & ~(pageSize - 1); + void* p = mmap(nullptr, allocSize, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return (p == MAP_FAILED) ? nullptr : p; +} + +void FreeExec(void* p, size_t size) { + if (!p) return; + size_t pageSize = PageSize(); + size_t allocSize = (size + pageSize - 1) & ~(pageSize - 1); + munmap(p, allocSize); +} + +#ifndef MAP_FIXED_NOREPLACE +# define MAP_FIXED_NOREPLACE 0x100000 // linux >= 4.17 defined manually for older headers +#endif + +void* AllocExecNear(void* target, size_t size) { + size_t pageSize = PageSize(); + uintptr_t targetAddr = reinterpret_cast(target); + const uintptr_t margin = 0x7FFF0000ULL; + uintptr_t rangeLo = (targetAddr > margin) ? targetAddr - margin : pageSize; + uintptr_t rangeHi = targetAddr + margin; + + FILE* f = fopen("/proc/self/maps", "r"); + if (!f) return nullptr; + + struct Region { uintptr_t start, end; }; + std::vector regions; + char line[512]; + while (fgets(line, sizeof(line), f)) { + unsigned long long s = 0, e = 0; + if (sscanf(line, "%llx-%llx", &s, &e) == 2) { + regions.push_back({ static_cast(s), static_cast(e) }); + } + } + fclose(f); + std::sort(regions.begin(), regions.end(), + [](const Region& a, const Region& b) { return a.start < b.start; }); + + std::vector gaps; + uintptr_t prevEnd = rangeLo; + for (auto& r : regions) { + if (r.start >= rangeHi) break; + uintptr_t gapStart = prevEnd; + uintptr_t gapEnd = std::min(r.start, rangeHi); + if (gapEnd > gapStart) gaps.push_back({ gapStart, gapEnd }); + if (r.end > prevEnd) prevEnd = r.end; + } + if (prevEnd < rangeHi) gaps.push_back({ prevEnd, rangeHi }); + + size_t pageAlignedSize = (size + pageSize - 1) & ~(pageSize - 1); + + void* best = nullptr; + uintptr_t bestDist = static_cast(-1); + for (auto& g : gaps) { + uintptr_t alignedStart = (g.start + pageSize - 1) & ~(pageSize - 1); + uintptr_t alignedEnd = g.end & ~(pageSize - 1); + if (alignedEnd < alignedStart + pageAlignedSize) continue; + + uintptr_t lastValidStart = alignedEnd - pageAlignedSize; // already page-aligned + uintptr_t candidate; + if (targetAddr < alignedStart) candidate = alignedStart; + else if (targetAddr > lastValidStart) candidate = lastValidStart; + else candidate = targetAddr & ~(pageSize - 1); + + // clamp + if (candidate > lastValidStart) candidate = lastValidStart; + if (candidate < alignedStart) candidate = alignedStart; + + uintptr_t dist = (candidate > targetAddr) ? (candidate - targetAddr) : (targetAddr - candidate); + if (dist < bestDist) { + bestDist = dist; + best = reinterpret_cast(candidate); + } + } + if (!best) return nullptr; + + void* p = mmap(best, pageAlignedSize, PROT_READ | PROT_WRITE | PROT_EXEC, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + if (p == MAP_FAILED) return nullptr; + return p; +} + +bool MakeWritableExecutable(void* addr, size_t size, unsigned long* oldProtect) { + if (oldProtect) *oldProtect = 0; + size_t pageSize = PageSize(); + uintptr_t start = reinterpret_cast(addr) & ~(pageSize - 1); + uintptr_t end = (reinterpret_cast(addr) + size + pageSize - 1) & ~(pageSize - 1); + return mprotect(reinterpret_cast(start), end - start, + PROT_READ | PROT_WRITE | PROT_EXEC) == 0; +} + +void RestoreProtection(void* /*addr*/, size_t /*size*/, unsigned long /*oldProtect*/) { + // no-op +} + +void FlushICache(void* addr, size_t size) { + __builtin___clear_cache(reinterpret_cast(addr), + reinterpret_cast(addr) + size); +} + +#endif + +// x86-64 disassembler +#if defined(RADHOOK_ARCH_X64) + +struct X64Insn { + size_t length = 0; + bool ripRelative = false; + size_t dispOffset = 0; + bool relativeControlFlow = false; + bool valid = false; +}; + +bool IsLegacyPrefix(uint8_t b) { + switch (b) { + case 0x66: case 0x67: case 0xF0: case 0xF2: case 0xF3: + case 0x2E: case 0x36: case 0x3E: case 0x26: case 0x64: case 0x65: + return true; + default: + return false; + } +} + +X64Insn DecodeX64(const uint8_t* p, size_t avail) { + X64Insn insn; + size_t i = 0; + bool opSize16 = false; + bool rexW = false; + + while (i < avail && IsLegacyPrefix(p[i])) { + if (p[i] == 0x66) opSize16 = true; + ++i; + } + if (i < avail && (p[i] & 0xF0) == 0x40) { + rexW = (p[i] & 0x08) != 0; + ++i; + } + if (i >= avail) return insn; + + uint8_t opcode = p[i++]; + bool twoByte = false; + if (opcode == 0x0F) { + if (i >= avail) return insn; + twoByte = true; + opcode = p[i++]; + } + + bool hasModRM = false; + int immSize = 0; + bool relBranch = false; + + if (!twoByte) { + uint8_t lo = opcode & 0x0F; + + if (opcode <= 0x3D && (opcode & 0xC0) == 0x00 && lo <= 0x05 && + (opcode & 0x07) <= 0x05) { + if (lo == 0x04) { immSize = 1; } + else if (lo == 0x05) { immSize = opSize16 ? 2 : 4; } + else if (lo <= 0x03) { hasModRM = true; } + else { return insn; } + } else if (opcode >= 0x50 && opcode <= 0x5F) { /* push/pop r64, no operand bytes */ } + else if (opcode == 0x68) { immSize = opSize16 ? 2 : 4; } + else if (opcode == 0x6A) { immSize = 1; } + else if (opcode == 0x69) { hasModRM = true; immSize = opSize16 ? 2 : 4; } + else if (opcode == 0x6B) { hasModRM = true; immSize = 1; } + else if (opcode >= 0x70 && opcode <= 0x7F) { immSize = 1; relBranch = true; } + else if (opcode == 0x80) { hasModRM = true; immSize = 1; } + else if (opcode == 0x81) { hasModRM = true; immSize = opSize16 ? 2 : 4; } + else if (opcode == 0x83) { hasModRM = true; immSize = 1; } + else if (opcode >= 0x84 && opcode <= 0x8F) { hasModRM = true; } + else if (opcode >= 0x90 && opcode <= 0x97) { /* xchg/nop, no operand bytes */ } + else if (opcode == 0x98 || opcode == 0x99) { /* cbw/cwd family */ } + else if (opcode == 0xA8) { immSize = 1; } + else if (opcode == 0xA9) { immSize = opSize16 ? 2 : 4; } + else if (opcode >= 0xB0 && opcode <= 0xB7) { immSize = 1; } + else if (opcode >= 0xB8 && opcode <= 0xBF) { immSize = rexW ? 8 : (opSize16 ? 2 : 4); } + else if (opcode == 0xC0 || opcode == 0xC1) { hasModRM = true; immSize = 1; } + else if (opcode == 0xC2) { immSize = 2; } + else if (opcode == 0xC3) { /* ret */ } + else if (opcode == 0xC6) { hasModRM = true; immSize = 1; } + else if (opcode == 0xC7) { hasModRM = true; immSize = opSize16 ? 2 : 4; } + else if (opcode == 0xC9) { /* leave */ } + else if (opcode == 0xCC) { /* int3 */ } + else if (opcode == 0xCD) { immSize = 1; } + else if (opcode == 0xD0 || opcode == 0xD1 || opcode == 0xD2 || opcode == 0xD3) { hasModRM = true; } + else if (opcode == 0xE8) { immSize = 4; relBranch = true; } + else if (opcode == 0xE9) { immSize = 4; relBranch = true; } + else if (opcode == 0xEB) { immSize = 1; relBranch = true; } + else if (opcode == 0xF6) { hasModRM = true; immSize = 1; } + else if (opcode == 0xF7) { hasModRM = true; immSize = opSize16 ? 2 : 4; } + else if (opcode == 0xFE || opcode == 0xFF) { hasModRM = true; } + else { + return insn; // unrecognized opcode + } + } else { + if (opcode >= 0x80 && opcode <= 0x8F) { immSize = 4; relBranch = true; } + else if (opcode == 0x1E) { immSize = 1; } + else if (opcode == 0x1F) { hasModRM = true; } + else if (opcode == 0x05 || opcode == 0x31 || opcode == 0x34 || opcode == 0x35 || opcode == 0xA2) { /* syscall/rdtsc/cpuid etc */ } + else if (opcode >= 0x40 && opcode <= 0x4F) { hasModRM = true; } // CMOVcc + else if (opcode == 0xAF) { hasModRM = true; } // imul + else if (opcode == 0xB6 || opcode == 0xB7 || opcode == 0xBE || opcode == 0xBF) { hasModRM = true; } // movzx/movsx + else if (opcode >= 0x10 && opcode <= 0x17) { hasModRM = true; } // movups/movaps family + else if (opcode >= 0x28 && opcode <= 0x2F) { hasModRM = true; } + else if (opcode >= 0x54 && opcode <= 0x5F) { hasModRM = true; } + else if (opcode == 0x6E || opcode == 0x6F || opcode == 0x7E || opcode == 0x7F || opcode == 0xD6) { hasModRM = true; } + else { + return insn; + } + } + + if (hasModRM) { + if (i >= avail) return insn; + uint8_t modrm = p[i++]; + uint8_t mod = (modrm >> 6) & 0x3; + uint8_t rm = modrm & 0x7; + + if (mod != 3 && rm == 4) { + if (i >= avail) return insn; + uint8_t sib = p[i++]; + uint8_t base = sib & 0x7; + if (mod == 0 && base == 5) { + i += 4; + } + } + + if (mod == 0 && rm == 5) { + insn.ripRelative = true; + insn.dispOffset = i; + i += 4; + } else if (mod == 1) { + i += 1; + } else if (mod == 2) { + i += 4; + } + } + + i += static_cast(immSize); + if (i > avail) return insn; + + insn.length = i; + insn.relativeControlFlow = relBranch; + insn.valid = true; + return insn; +} + +constexpr size_t kX64StubLen = 14; // FF25 00000000 + imm64 +constexpr size_t kX64MaxStolen = 32; +constexpr size_t kX64MaxRipFixups = 8; + +bool BuildStolenBytesX64(const uint8_t* target, size_t minLen, + uint8_t* outBytes, size_t& outLen, + size_t* outRipOffsets, size_t& outRipCount) { + size_t len = 0; + outRipCount = 0; + while (len < minLen) { + if (len >= kX64MaxStolen) return false; + X64Insn insn = DecodeX64(target + len, kX64MaxStolen - len); + if (!insn.valid) return false; + if (insn.relativeControlFlow) return false; + if (insn.ripRelative) { + if (outRipCount >= kX64MaxRipFixups) return false; + outRipOffsets[outRipCount++] = len + insn.dispOffset; + } + len += insn.length; + } + std::memcpy(outBytes, target, len); + outLen = len; + return true; +} + +#endif // RADHOOK_ARCH_X64 + +// aarch64 prologue check +#if defined(RADHOOK_ARCH_ARM64) + +constexpr size_t kArm64StubLen = 16; + +bool IsArm64PcRelative(uint32_t instr) { + if ((instr & 0x9F000000) == 0x10000000) return true; // ADR + if ((instr & 0x9F000000) == 0x90000000) return true; // ADRP + if ((instr & 0xFC000000) == 0x14000000) return true; // B + if ((instr & 0xFC000000) == 0x94000000) return true; // BL + if ((instr & 0xFF000010) == 0x54000000) return true; // B.cond + if ((instr & 0x7E000000) == 0x34000000) return true; // CBZ/CBNZ + if ((instr & 0x7E000000) == 0x36000000) return true; // TBZ/TBNZ + if ((instr & 0xBF000000) == 0x18000000) return true; // LDR + if ((instr & 0x3B000000) == 0x18000000) return true; // LDR/LDRSW + return false; +} + +void WriteArm64Stub(void* at, uintptr_t targetAddr) { + uint32_t* words = reinterpret_cast(at); + words[0] = 0x58000051; // LDR X17, #8 + words[1] = 0xD61F0220; // BR X17 + std::memcpy(&words[2], &targetAddr, 8); +} + +#endif // RADHOOK_ARCH_ARM64 + +} // anonymous namespace + +namespace { + +RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { + if (!target) return RadHookResult::InvalidTarget; + if (!detour) return RadHookResult::InvalidDetour; + +#if defined(RADHOOK_ARCH_X64) + uint8_t stolen[kX64MaxStolen]; + size_t stolenLen = 0; + size_t ripOffsets[kX64MaxRipFixups]; + size_t ripCount = 0; + + if (!BuildStolenBytesX64(reinterpret_cast(target), kX64StubLen, + stolen, stolenLen, ripOffsets, ripCount)) { + return RadHookResult::DisassemblyFailed; + } + + size_t allocSize = stolenLen + kX64StubLen; + void* trampolineAlloc = AllocExecNear(target, allocSize); + if (!trampolineAlloc) trampolineAlloc = AllocExec(allocSize); // best-effort fallback + if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed; + + std::memcpy(trampolineAlloc, stolen, stolenLen); + + for (size_t k = 0; k < ripCount; ++k) { + size_t off = ripOffsets[k]; + int32_t originalDisp = 0; + std::memcpy(&originalDisp, reinterpret_cast(target) + off, 4); + uintptr_t absTarget = reinterpret_cast(target) + off + 4 + static_cast(originalDisp); + intptr_t newDisp = static_cast(absTarget) - + (reinterpret_cast(trampolineAlloc) + off + 4); + if (newDisp < INT32_MIN || newDisp > INT32_MAX) { + FreeExec(trampolineAlloc, allocSize); + return RadHookResult::TrampolineTooFar; + } + int32_t newDisp32 = static_cast(newDisp); + std::memcpy(reinterpret_cast(trampolineAlloc) + off, &newDisp32, 4); + } + + uint8_t* jumpBack = reinterpret_cast(trampolineAlloc) + stolenLen; + uintptr_t backAddr = reinterpret_cast(target) + stolenLen; + jumpBack[0] = 0xFF; jumpBack[1] = 0x25; + jumpBack[2] = 0; jumpBack[3] = 0; jumpBack[4] = 0; jumpBack[5] = 0; + std::memcpy(jumpBack + 6, &backAddr, 8); + + FlushICache(trampolineAlloc, allocSize); + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(target, stolenLen, &oldProtect)) { + FreeExec(trampolineAlloc, allocSize); + return RadHookResult::MemoryProtectFailed; + } + + std::memcpy(h->originalBytes, target, stolenLen); + h->originalLength = stolenLen; + + uint8_t hookStub[kX64StubLen]; + hookStub[0] = 0xFF; hookStub[1] = 0x25; + hookStub[2] = 0; hookStub[3] = 0; hookStub[4] = 0; hookStub[5] = 0; + uintptr_t detourAddr = reinterpret_cast(detour); + std::memcpy(hookStub + 6, &detourAddr, 8); + std::memcpy(h->stubBytes, hookStub, kX64StubLen); + h->stubLength = kX64StubLen; + + std::memcpy(target, hookStub, kX64StubLen); + RestoreProtection(target, stolenLen, oldProtect); + FlushICache(target, stolenLen); + + h->target = target; + h->detour = detour; + h->trampoline = trampolineAlloc; + h->trampolineAlloc = trampolineAlloc; + h->trampolineAllocSize = allocSize; + h->enabled = true; + return RadHookResult::Success; + +#elif defined(RADHOOK_ARCH_ARM64) + const uint32_t* src = reinterpret_cast(target); + for (size_t k = 0; k < kArm64StubLen / 4; ++k) { + if (IsArm64PcRelative(src[k])) { + return RadHookResult::DisassemblyFailed; + } + } + + size_t allocSize = kArm64StubLen + kArm64StubLen; + void* trampolineAlloc = AllocExec(allocSize); + if (!trampolineAlloc) return RadHookResult::MemoryAllocFailed; + + std::memcpy(trampolineAlloc, target, kArm64StubLen); + uintptr_t backAddr = reinterpret_cast(target) + kArm64StubLen; + WriteArm64Stub(reinterpret_cast(trampolineAlloc) + kArm64StubLen, backAddr); + + FlushICache(trampolineAlloc, allocSize); + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(target, kArm64StubLen, &oldProtect)) { + FreeExec(trampolineAlloc, allocSize); + return RadHookResult::MemoryProtectFailed; + } + + std::memcpy(h->originalBytes, target, kArm64StubLen); + h->originalLength = kArm64StubLen; + + uint8_t hookStub[kArm64StubLen]; + uintptr_t detourAddr = reinterpret_cast(detour); + WriteArm64Stub(hookStub, detourAddr); + std::memcpy(h->stubBytes, hookStub, kArm64StubLen); + h->stubLength = kArm64StubLen; + + std::memcpy(target, hookStub, kArm64StubLen); + RestoreProtection(target, kArm64StubLen, oldProtect); + FlushICache(target, kArm64StubLen); + + h->target = target; + h->detour = detour; + h->trampoline = trampolineAlloc; + h->trampolineAlloc = trampolineAlloc; + h->trampolineAllocSize = allocSize; + h->enabled = true; + return RadHookResult::Success; +#endif +} + +RadHookResult EnableHookInternal(RadHookOpaque* h) { + if (!h->target) return RadHookResult::NotInstalled; + if (h->enabled) return RadHookResult::AlreadyEnabled; + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(h->target, h->originalLength, &oldProtect)) { + return RadHookResult::MemoryProtectFailed; + } + std::memcpy(h->target, h->stubBytes, h->stubLength); + RestoreProtection(h->target, h->originalLength, oldProtect); + FlushICache(h->target, h->originalLength); + + h->enabled = true; + return RadHookResult::Success; +} + +RadHookResult DisableHookInternal(RadHookOpaque* h) { + if (!h->target) return RadHookResult::NotInstalled; + if (!h->enabled) return RadHookResult::AlreadyDisabled; + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(h->target, h->originalLength, &oldProtect)) { + return RadHookResult::MemoryProtectFailed; + } + std::memcpy(h->target, h->originalBytes, h->originalLength); + RestoreProtection(h->target, h->originalLength, oldProtect); + FlushICache(h->target, h->originalLength); + + h->enabled = false; + return RadHookResult::Success; +} + +} // anonymous namespace + +RadHookResult RadHookCreate(void* target, void* detour, RadHookHandle* outHandle) { + if (outHandle) *outHandle = nullptr; + if (!target) return RadHookResult::InvalidTarget; + if (!detour) return RadHookResult::InvalidDetour; + + auto record = std::make_unique(); + RadHookResult result = InstallHook(record.get(), target, detour); + if (result != RadHookResult::Success) { + return result; + } + + std::lock_guard lock(g_mutex); + RadHookOpaque* raw = record.get(); + g_hooks.push_back(std::move(record)); + if (outHandle) *outHandle = raw; + return RadHookResult::Success; +} + +RadHookResult RadHookEnable(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle; + return EnableHookInternal(handle); +} + +RadHookResult RadHookDisable(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle; + return DisableHookInternal(handle); +} + +RadHookResult RadHookDestroy(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle; + + RadHookResult disableResult = RadHookResult::Success; + if (handle->enabled) { + disableResult = DisableHookInternal(handle); + } + + if (handle->trampolineAlloc) { + FreeExec(handle->trampolineAlloc, handle->trampolineAllocSize); + handle->trampolineAlloc = nullptr; + } + + g_hooks.erase( + std::remove_if(g_hooks.begin(), g_hooks.end(), + [handle](const std::unique_ptr& h) { return h.get() == handle; }), + g_hooks.end()); + + return disableResult; +} + +RadHookResult RadHookEnableAll() { + std::lock_guard lock(g_mutex); + RadHookResult last = RadHookResult::Success; + for (auto& h : g_hooks) { + RadHookResult r = EnableHookInternal(h.get()); + if (r != RadHookResult::Success && r != RadHookResult::AlreadyEnabled) { + last = r; + } + } + return last; +} + +RadHookResult RadHookDisableAll() { + std::lock_guard lock(g_mutex); + RadHookResult last = RadHookResult::Success; + for (auto& h : g_hooks) { + RadHookResult r = DisableHookInternal(h.get()); + if (r != RadHookResult::Success && r != RadHookResult::AlreadyDisabled) { + last = r; + } + } + return last; +} + +size_t RadHookGetCount() { + std::lock_guard lock(g_mutex); + return g_hooks.size(); +} + +size_t RadHookGetHandles(RadHookHandle* outHandles, size_t maxCount) { + std::lock_guard lock(g_mutex); + size_t n = std::min(maxCount, g_hooks.size()); + for (size_t i = 0; i < n; ++i) { + outHandles[i] = g_hooks[i].get(); + } + return g_hooks.size(); +} + +bool RadHookIsValid(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + return IsRegisteredLocked(handle); +} + +bool RadHookIsEnabled(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return false; + return handle->enabled; +} + +void* RadHookGetTarget(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return nullptr; + return handle->target; +} + +void* RadHookGetDetour(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return nullptr; + return handle->detour; +} + +void* RadHookGetOriginal(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return nullptr; + return handle->trampoline; +} + +size_t RadHookEnumerate(RadHookHandle* out, size_t maxCount) { + size_t written = 0; + for (auto const& h : g_hooks) { + if (written >= maxCount) break; + out[written++] = h.get(); + } + + return written; +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..0785503 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +file(GLOB TEST_FILES CONFIGURE_DEPENDS "*.cpp") + +foreach(TEST_FILE ${TEST_FILES}) + get_filename_component(TEST_NAME ${TEST_FILE} NAME_WE) + + add_executable(${TEST_NAME} ${TEST_FILE}) + target_link_libraries(${TEST_NAME} PRIVATE radhook) + + add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + + add_custom_command(TARGET ${TEST_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) +endforeach() \ No newline at end of file diff --git a/tests/test_basic.cpp b/tests/test_basic.cpp new file mode 100644 index 0000000..2bd86a7 --- /dev/null +++ b/tests/test_basic.cpp @@ -0,0 +1,18 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +using Fn = int(*)(); + +int main() { + RadHookHandle h = nullptr; + + auto r = RadHookCreate((void*)fn, (void*)detour, &h); + assert(r == RadHookResult::Success); + assert(RadHookIsValid(h)); + + RadHookDestroy(h); + return 0; +} \ No newline at end of file diff --git a/tests/test_enable_disable.cpp b/tests/test_enable_disable.cpp new file mode 100644 index 0000000..db9b203 --- /dev/null +++ b/tests/test_enable_disable.cpp @@ -0,0 +1,24 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +using Fn = int(*)(); + +int main() { + RadHookHandle h = nullptr; + + RadHookCreate((void*)fn, (void*)detour, &h); + + assert(RadHookIsEnabled(h)); + + RadHookDisable(h); + assert(!RadHookIsEnabled(h)); + + RadHookEnable(h); + assert(RadHookIsEnabled(h)); + + RadHookDestroy(h); + return 0; +} \ No newline at end of file diff --git a/tests/test_execution_flow.cpp b/tests/test_execution_flow.cpp new file mode 100644 index 0000000..6c6d818 --- /dev/null +++ b/tests/test_execution_flow.cpp @@ -0,0 +1,30 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +using Fn = int(*)(); + +int main() { + RadHookHandle h = nullptr; + + RadHookCreate((void*)fn, (void*)detour, &h); + + // must trigger detour + assert(fn() == 1337); + + // disable restores original + RadHookDisable(h); + assert(fn() == 42); + + // re-enable restores hook + RadHookEnable(h); + assert(fn() == 1337); + + // trampoline correctness + auto orig = RadHookGetOriginalAs(h); + assert(orig() == 42); + + RadHookDestroy(h); +} \ No newline at end of file diff --git a/tests/test_failure_cases.cpp b/tests/test_failure_cases.cpp new file mode 100644 index 0000000..c95b445 --- /dev/null +++ b/tests/test_failure_cases.cpp @@ -0,0 +1,19 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle h = nullptr; + + // invalid detour + auto r1 = RadHookCreate((void*)fn, nullptr, &h); + assert(r1 != RadHookResult::Success); + + // invalid target + auto r2 = RadHookCreate(nullptr, (void*)detour, &h); + assert(r2 != RadHookResult::Success); + + return 0; +} \ No newline at end of file diff --git a/tests/test_invalid_inputs.cpp b/tests/test_invalid_inputs.cpp new file mode 100644 index 0000000..542220d --- /dev/null +++ b/tests/test_invalid_inputs.cpp @@ -0,0 +1,19 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle h = nullptr; + + // invalid detour + assert(RadHookCreate((void*)fn, nullptr, &h) != RadHookResult::Success); + + // invalid target + assert(RadHookCreate(nullptr, (void*)detour, &h) != RadHookResult::Success); + + // invalid handle operations + assert(RadHookEnable(nullptr) == RadHookResult::InvalidHandle || RadHookEnable(nullptr) != RadHookResult::Success); + assert(RadHookDisable(nullptr) == RadHookResult::InvalidHandle || RadHookDisable(nullptr) != RadHookResult::Success); +} \ No newline at end of file diff --git a/tests/test_lifecycle.cpp b/tests/test_lifecycle.cpp new file mode 100644 index 0000000..0e0f871 --- /dev/null +++ b/tests/test_lifecycle.cpp @@ -0,0 +1,23 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle h = nullptr; + + // destroy before create safety + assert(RadHookDestroy(nullptr) != RadHookResult::Success); + + RadHookCreate((void*)fn, (void*)detour, &h); + + RadHookDestroy(h); + + // use after destroy safety + assert(RadHookEnable(h) != RadHookResult::Success); + assert(RadHookDisable(h) != RadHookResult::Success); + assert(RadHookIsValid(h) == false || true); // depends on implementation + + return 0; +} \ No newline at end of file diff --git a/tests/test_memory_integrity.cpp b/tests/test_memory_integrity.cpp new file mode 100644 index 0000000..fd002be --- /dev/null +++ b/tests/test_memory_integrity.cpp @@ -0,0 +1,21 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle h = nullptr; + + RadHookCreate((void*)fn, (void*)detour, &h); + + for (int i = 0; i < 10000; i++) { + RadHookEnable(h); + assert(fn() == 1337); + + RadHookDisable(h); + assert(fn() == 42); + } + + RadHookDestroy(h); +} \ No newline at end of file diff --git a/tests/test_stability.cpp b/tests/test_stability.cpp new file mode 100644 index 0000000..0755b11 --- /dev/null +++ b/tests/test_stability.cpp @@ -0,0 +1,56 @@ +#include +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle handles[200]; + size_t count = 0; + + // create many hooks + for (int i = 0; i < 200; i++) { + RadHookHandle h = nullptr; + auto r = RadHookCreate( + (void*)fn, + (void*)detour, + &h + ); + + assert(r == RadHookResult::Success); + assert(h != nullptr); + + handles[count++] = h; + } + + // enumerate + RadHookHandle listed[200]; + size_t listedCount = RadHookEnumerate(listed, 200); + assert(listedCount == 200); + + // every created handle must still be valid + for (size_t i = 0; i < count; i++) { + assert(RadHookIsValid(handles[i])); + } + + // destroy half of them + for (size_t i = 0; i < count; i += 2) { + RadHookDestroy(handles[i]); + } + + // validate + for (size_t i = 0; i < count; i++) { + if (i % 2 == 0) { + assert(!RadHookIsValid(handles[i])); + } else { + assert(RadHookIsValid(handles[i])); + } + } + + // final enumeration must not return destroyed hooks + size_t finalCount = RadHookEnumerate(listed, 200); + assert(finalCount == 100); + + return 0; +} \ No newline at end of file diff --git a/tests/test_state_transitions.cpp b/tests/test_state_transitions.cpp new file mode 100644 index 0000000..21af558 --- /dev/null +++ b/tests/test_state_transitions.cpp @@ -0,0 +1,20 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + RadHookHandle h = nullptr; + + assert(RadHookCreate((void*)fn, (void*)detour, &h) == RadHookResult::Success); + + // idempotent enable/disable behavior + assert(RadHookEnable(h) != RadHookResult::Unknown); + assert(RadHookEnable(h) == RadHookResult::AlreadyEnabled || RadHookIsEnabled(h)); + + assert(RadHookDisable(h) != RadHookResult::Unknown); + assert(!RadHookIsEnabled(h) || RadHookDisable(h) == RadHookResult::AlreadyDisabled); + + RadHookDestroy(h); +} \ No newline at end of file diff --git a/tests/test_stress_churn.cpp b/tests/test_stress_churn.cpp new file mode 100644 index 0000000..32b8bd6 --- /dev/null +++ b/tests/test_stress_churn.cpp @@ -0,0 +1,19 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +int main() { + for (int i = 0; i < 1000; i++) { + RadHookHandle h = nullptr; + + auto r = RadHookCreate((void*)fn, (void*)detour, &h); + assert(r == RadHookResult::Success); + + RadHookEnable(h); + RadHookDisable(h); + + RadHookDestroy(h); + } +} \ No newline at end of file diff --git a/tests/test_trampoline.cpp b/tests/test_trampoline.cpp new file mode 100644 index 0000000..ecc3e65 --- /dev/null +++ b/tests/test_trampoline.cpp @@ -0,0 +1,22 @@ +#include +#include "radhook/radhook.h" + +static int fn() { return 42; } +static int detour() { return 1337; } + +using Fn = int(*)(); + +int main() { + RadHookHandle h = nullptr; + + RadHookCreate((void*)fn, (void*)detour, &h); + + auto orig = RadHookGetOriginalAs(h); + assert(orig != nullptr); + + // trampoline must bypass hook + assert(orig() == 42); + + RadHookDestroy(h); + return 0; +} \ No newline at end of file