From 35f52a41156f3eff3c8de1e108e640a235b409ae Mon Sep 17 00:00:00 2001 From: nexusverypro Date: Fri, 3 Jul 2026 22:00:03 +0100 Subject: [PATCH] api changes, check README --- CMakeLists.txt | 2 +- README.md | 112 +++++++++++++++---- include/radhook/api.h | 242 ++++++++++++++++++++++++++++++----------- src/radhook.cpp | 125 ++++++++++++++++++++- tests/test_queue.cpp | 68 ++++++++++++ tests/test_version.cpp | 20 ++++ tests/test_vtable.cpp | 68 ++++++++++++ 7 files changed, 549 insertions(+), 88 deletions(-) create mode 100644 tests/test_queue.cpp create mode 100644 tests/test_version.cpp create mode 100644 tests/test_vtable.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d992f76..681a6ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.21) project( radhook - VERSION 1.0.0 + VERSION 1.1.0 DESCRIPTION "cross-platform function hooking library" LANGUAGES CXX ) diff --git a/README.md b/README.md index 2724256..1d86ee6 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,20 @@ # libradhook -A minimal, cross-platform inline function hooking library for x86-64 and AArch64, -targeting Windows and Android, built as a lightweight alternative to libraries like +A minimal, cross-platform inline function hooking library for x86-64 and AArch64, +targeting Windows and Android, built as a lightweight alternative to libraries like MinHook and Dobby with a flat, C-style, handle-based API. ## Features - Inline hooking on x86-64 and AArch64 +- Vtable hooking via direct slot swapping, no trampoline required - Windows and Android support - Flat C API with opaque handles - Typed convenience overload of `RadHookCreate` for function pointers - Trampolines to call the original function from within a detour - Enable / disable / destroy individual hooks, or all hooks at once +- Deferred enable/disable via a queue, applied atomically in one pass - Hook enumeration and introspection (target, detour, trampoline, enabled state) - Human-readable status strings +- Runtime version query for ABI sanity checks - Builds as a static or shared library via CMake ## Requirements @@ -28,10 +31,11 @@ cmake --build build ``` Useful options: -| Option | Default | Description | -|------------------------|------------------------|--------------------------------------------------------| -| `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static | -| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite | + +| Option | Default | Description | +|------------------------|------------------------|---------------------------------------------------------| +| `BUILD_SHARED_LIBS` | `ON` | Build radhook as a shared library instead of static | +| `RADHOOK_BUILD_TESTS` | `ON` when top-level | Build the test suite | ### Build.ps1 A helper PowerShell script is provided for common build targets: @@ -44,6 +48,8 @@ A helper PowerShell script is provided for common build targets: ``` ## Usage + +### Inline hooking ```cpp #include @@ -56,7 +62,7 @@ int main() { // create and install the hook RadHookResult result = RadHookCreate(TargetFunction, (void*)DetourFunction, &handle); if (result != RadHookResult::Success) { - // handle error + // handle error, RadHookResultToString(result) for a message } // call through the trampoline @@ -72,22 +78,84 @@ int main() { } ``` +### Vtable hooking +```cpp +struct IFoo { + virtual int Value() { return 7; } + virtual ~IFoo() = default; +}; + +int Detour(IFoo* self) { return 777; } + +IFoo obj; +void** vtable = *reinterpret_cast(&obj); + +RadHookHandle handle = nullptr; +RadHookVTableCreate(vtable, /*index=*/0, (void*)Detour, &handle); + +// no relocation is needed for vtable hooks +using Fn = int(*)(IFoo*); +Fn original = RadHookGetOriginalAs(handle); +original(&obj); + +RadHookDestroy(handle); +``` + +### Deferred apply +Stage several enable/disable transitions and flip them together in one pass, +instead of one hook at a time: + +```cpp +RadHookQueueEnable(handleA); +RadHookQueueDisable(handleB); + +// nothing has changed yet +Foo(); + +// then apply +RadHookApplyQueued(); // both transitions land together +``` + +If a handle is queued more than once before `RadHookApplyQueued` runs, the +most recent call wins. + ## API overview -| Function Name | Description | -|---------------------------------------------------|------------------------------------------------------------| -| `RadHookCreate` | Create and install a hook, enabled by default | -| `RadHookEnable` | Enable an installed hook | -| `RadHookDisable` | Disable an installed hook, restoring original bytes | -| `RadHookDestroy` | Disable, release, and invalidate a hook | -| `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook | -| `RadHookGetCount` | Number of currently registered hooks | -| `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles | -| `RadHookIsValid` | Check whether a handle refers to a registered hook | -| `RadHookIsEnabled` | Check whether a hook is currently enabled | -| `RadHookGetTarget` | Get the hooked target function pointer | -| `RadHookGetDetour` | Get the detour function pointer | -| `RadHookGetOriginal` / `RadHookGetOriginalAs` | Get the trampoline to call the original implementation | -| `RadHookResultToString` | Human-readable name for a `RadHookResult` | + +| Function Name | Description | +|-----------------------------------------------------|-----------------------------------------------------------------| +| `RadHookCreate` | Create and install a hook, enabled by default | +| `RadHookVTableCreate` | Create and install a hook on a vtable slot | +| `RadHookEnable` | Enable an installed hook | +| `RadHookDisable` | Disable an installed hook, restoring original bytes | +| `RadHookDestroy` | Disable, release, and invalidate a hook | +| `RadHookEnableAll` / `RadHookDisableAll` | Enable or disable every registered hook | +| `RadHookQueueEnable` / `RadHookQueueDisable` | Stage a hook's enable/disable state without applying it | +| `RadHookApplyQueued` | Apply every staged enable/disable transition at once | +| `RadHookGetCount` | Number of currently registered hooks | +| `RadHookGetHandles` / `RadHookEnumerate` | Enumerate registered hook handles | +| `RadHookIsValid` | Check whether a handle refers to a registered hook | +| `RadHookIsEnabled` | Check whether a hook is currently enabled | +| `RadHookGetTarget` | Get the hooked target function pointer | +| `RadHookGetDetour` | Get the detour function pointer | +| `RadHookGetOriginal` / `RadHookGetOriginalAs` | Get the trampoline to call the original implementation | +| `RadHookResultToString` | Human-readable name for a `RadHookResult` | +| `RadHookGetVersion` | Query the library's major/minor/patch version | + +All fallible operations return a `RadHookResult`, including granular error +codes such as `TrampolineTooFar`, `DisassemblyFailed`, and +`MemoryProtectFailed`. + +## Testing +Tests are built with CTest and cover creation, enable/disable and lifecycle +transitions, trampoline behavior, vtable hooking, queued apply, invalid +input handling, failure cases, memory integrity, and stress/churn +scenarios. + +```sh +cmake -S . -B build -DRADHOOK_BUILD_TESTS=ON +cmake --build build +ctest --test-dir build --output-on-failure +``` ## License Licensed under the GNU Lesser General Public License v3 (LGPLv3). See [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/include/radhook/api.h b/include/radhook/api.h index b432fd2..159fd17 100644 --- a/include/radhook/api.h +++ b/include/radhook/api.h @@ -10,6 +10,24 @@ # define RADHOOK_API #endif // RADHOOK_API +// versioning +#define RADHOOK_VERSION_MAJOR 1 +#define RADHOOK_VERSION_MINOR 1 +#define RADHOOK_VERSION_PATCH 0 + +#if _MSC_VER +# include // for source code annotations +#else +# define _In_ +# define _In_opt_ +# define _Out_ +# define _Out_opt_ +# define _Outptr_ +# define _Ret_z_ +# define _Success_(expr) +# define _Out_writes_to_(size, count) +#endif + enum class RadHookResult : int { Success = 0, AlreadyInstalled, @@ -26,6 +44,7 @@ enum class RadHookResult : int { Unknown, }; +_Ret_z_ /** * @brief Creates a human readable name for a status code. * @@ -33,14 +52,15 @@ enum class RadHookResult : int { * @return Null-terminated string describing the status code. */ RADHOOK_API -const char* +const char* RadHookResultToString( RadHookResult result -); + ); struct RadHookOpaque; using RadHookHandle = RadHookOpaque*; +_Success_(return == RadHookResult::Success) /** * @brief Creates and installs a hook. * @@ -55,20 +75,21 @@ using RadHookHandle = RadHookOpaque*; * @return Operation result. */ RADHOOK_API -RadHookResult +RadHookResult RadHookCreate( - void* target, - void* detour, - RadHookHandle* outHandle -); + _In_ void* target, + _In_ void* detour, + _Out_ RadHookHandle* outHandle + ); +_Success_(return == RadHookResult::Success) /** * @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 target Target function to hook. * @param detour Replacement function. * @param outHandle Receives the created hook handle on success. * @@ -76,12 +97,13 @@ RadHookCreate( */ template requires std::is_function_v> -RadHookResult +RadHookResult RadHookCreate( TargetFn target, - void* detour, - RadHookHandle* outHandle -) { + _In_ void* detour, + _Out_ RadHookHandle* outHandle + ) +{ return RadHookCreate( reinterpret_cast(target), detour, @@ -100,10 +122,10 @@ RadHookCreate( * otherwise the result of the operation. */ RADHOOK_API -RadHookResult +RadHookResult RadHookEnable( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Disables an installed hook. @@ -116,10 +138,10 @@ RadHookEnable( * otherwise the result of the operation. */ RADHOOK_API -RadHookResult +RadHookResult RadHookDisable( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Destroys a hook. @@ -132,10 +154,10 @@ RadHookDisable( * @return Operation result. */ RADHOOK_API -RadHookResult +RadHookResult RadHookDestroy( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Enables every registered hook. @@ -145,7 +167,7 @@ RadHookDestroy( * @return Operation result. */ RADHOOK_API -RadHookResult +RadHookResult RadHookEnableAll(); /** @@ -156,7 +178,7 @@ RadHookEnableAll(); * @return Operation result. */ RADHOOK_API -RadHookResult +RadHookResult RadHookDisableAll(); /** @@ -167,9 +189,10 @@ RadHookDisableAll(); * @return Number of registered hooks. */ RADHOOK_API -size_t +size_t RadHookGetCount(); +_Out_writes_to_(maxCount, return) /** * @brief Retrieves registered hook handles. * @@ -182,11 +205,11 @@ RadHookGetCount(); * @return Number of handles copied. */ RADHOOK_API -size_t +size_t RadHookGetHandles( - RadHookHandle* outHandles, - size_t maxCount -); + _Out_ RadHookHandle* outHandles, + _In_ size_t maxCount + ); /** * @brief Checks whether a hook handle is valid. @@ -196,10 +219,10 @@ RadHookGetHandles( * @return true if the handle refers to a registered hook; otherwise false. */ RADHOOK_API -bool +bool RadHookIsValid( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Checks whether a hook is currently enabled. @@ -209,10 +232,10 @@ RadHookIsValid( * @return true if the hook is enabled; otherwise false. */ RADHOOK_API -bool +bool RadHookIsEnabled( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Returns the hooked target function. @@ -223,10 +246,10 @@ RadHookIsEnabled( * is invalid. */ RADHOOK_API -void* +void* RadHookGetTarget( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Returns the detour function. @@ -237,10 +260,10 @@ RadHookGetTarget( * invalid. */ RADHOOK_API -void* +void* RadHookGetDetour( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Returns the trampoline containing the relocated original code. @@ -253,10 +276,10 @@ RadHookGetDetour( * @return Pointer to the trampoline, or nullptr if unavailable. */ RADHOOK_API -void* +void* RadHookGetOriginal( - RadHookHandle handle -); + _In_ RadHookHandle handle + ); /** * @brief Returns the trampoline cast to a function pointer type. @@ -267,32 +290,125 @@ RadHookGetOriginal( * @return Trampoline cast to @p FnPtr. */ template -FnPtr +FnPtr RadHookGetOriginalAs( - RadHookHandle handle -) { + _In_ 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. - */ +_Out_writes_to_(maxCount, return) +/** + * @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 -); + _Out_ RadHookHandle* out, + _In_ size_t maxCount + ); + +/** + * @brief Returns the library version. + * + * Any of the output parameters may be nullptr if that component is not + * needed. Useful for verifying ABI compatibility when radhook is loaded + * as a shared library independently of the consuming module. + * + * @param outMajor Receives the major version, or nullptr. + * @param outMinor Receives the minor version, or nullptr. + * @param outPatch Receives the patch version, or nullptr. + */ +RADHOOK_API +void +RadHookGetVersion( + _Out_opt_ int* outMajor, + _Out_opt_ int* outMinor, + _Out_opt_ int* outPatch + ); + +_Success_(return == RadHookResult::Success) +/** + * @brief Creates and installs a hook on a vtable slot. + * + * @param vtable Pointer to the base of the vtable. + * @param index Index of the slot to hook. + * @param detour Replacement function. + * @param outHandle Receives the created hook handle on success. Set to nullptr on failure. + * + * @return InvalidTarget if @p vtable is nullptr or the slot + * at @p index is empty; otherwise the result of the operation. + */ +RADHOOK_API +RadHookResult +RadHookVTableCreate( + _In_ void** vtable, + _In_ size_t index, + _In_ void* detour, + _Out_ RadHookHandle* outHandle + ); + +/** + * @brief Queues a hook to be enabled on the next RadHookApplyQueued call. + * + * Does not modify the hook itself. If the same handle is queued for both + * enable and disable before RadHookApplyQueued runs, the most recent call + * wins. + * + * @param handle Hook handle. + * + * @return RadHookResult::InvalidHandle if the handle is not registered; + * otherwise RadHookResult::Success. + */ +RADHOOK_API +RadHookResult +RadHookQueueEnable( + _In_ RadHookHandle handle + ); + +/** + * @brief Queues a hook to be disabled on the next RadHookApplyQueued call. + * + * Does not modify the hook itself. If the same handle is queued for both + * enable and disable before RadHookApplyQueued runs, the most recent call + * wins. + * + * @param handle Hook handle. + * + * @return RadHookResult::InvalidHandle if the handle is not registered; + * otherwise RadHookResult::Success. + */ +RADHOOK_API +RadHookResult +RadHookQueueDisable( + _In_ RadHookHandle handle + ); + +/** + * @brief Applies all queued enable/disable operations. + * + * Every hook with a pending queued action has that action applied and its + * queue cleared, regardless of individual outcomes. Hooks with no queued + * action are left untouched. + * + * @return The result of the first queued operation that did not succeed + * (ignoring RadHookResult::AlreadyEnabled / RadHookResult::AlreadyDisabled), + * or RadHookResult::Success if all applied cleanly. + */ +RADHOOK_API +RadHookResult +RadHookApplyQueued(); #endif // __LIBRADHOOK_API_H__ \ No newline at end of file diff --git a/src/radhook.cpp b/src/radhook.cpp index cc71c9b..97dacbe 100644 --- a/src/radhook.cpp +++ b/src/radhook.cpp @@ -18,6 +18,8 @@ # include #endif +// stupid macros messing up compilation because it conflicts with +// std::min and std::max #undef min #undef max @@ -40,6 +42,17 @@ const char* RadHookResultToString(RadHookResult result) { } } +enum class RadHookKind : int { + Inline, + VTable, +}; + +enum class RadHookQueuedAction : int { + None, + Enable, + Disable, +}; + // hook record struct RadHookOpaque { void* target = nullptr; @@ -56,6 +69,12 @@ struct RadHookOpaque { size_t stubLength = 0; bool enabled = false; + + RadHookKind kind = RadHookKind::Inline; + void** vtableSlot = nullptr; + void* vtableOriginalEntry = nullptr; + + RadHookQueuedAction queuedAction = RadHookQueuedAction::None; }; namespace { @@ -121,8 +140,10 @@ void* AllocExecNear(void* target, size_t size) { 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); + void* p = VirtualAlloc( + reinterpret_cast(allocBase), size, + MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE + ); if (p) return p; } } @@ -598,6 +619,21 @@ RadHookResult InstallHook(RadHookOpaque* h, void* target, void* detour) { } RadHookResult EnableHookInternal(RadHookOpaque* h) { + if (h->kind == RadHookKind::VTable) { + if (!h->vtableSlot) return RadHookResult::NotInstalled; + if (h->enabled) return RadHookResult::AlreadyEnabled; + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) { + return RadHookResult::MemoryProtectFailed; + } + *h->vtableSlot = h->detour; + RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect); + + h->enabled = true; + return RadHookResult::Success; + } + if (!h->target) return RadHookResult::NotInstalled; if (h->enabled) return RadHookResult::AlreadyEnabled; @@ -614,6 +650,21 @@ RadHookResult EnableHookInternal(RadHookOpaque* h) { } RadHookResult DisableHookInternal(RadHookOpaque* h) { + if (h->kind == RadHookKind::VTable) { + if (!h->vtableSlot) return RadHookResult::NotInstalled; + if (!h->enabled) return RadHookResult::AlreadyDisabled; + + unsigned long oldProtect = 0; + if (!MakeWritableExecutable(h->vtableSlot, sizeof(void*), &oldProtect)) { + return RadHookResult::MemoryProtectFailed; + } + *h->vtableSlot = h->vtableOriginalEntry; + RestoreProtection(h->vtableSlot, sizeof(void*), oldProtect); + + h->enabled = false; + return RadHookResult::Success; + } + if (!h->target) return RadHookResult::NotInstalled; if (!h->enabled) return RadHookResult::AlreadyDisabled; @@ -758,4 +809,74 @@ size_t RadHookEnumerate(RadHookHandle* out, size_t maxCount) { } return written; +} + +void RadHookGetVersion(int* outMajor, int* outMinor, int* outPatch) { + if (outMajor) *outMajor = RADHOOK_VERSION_MAJOR; + if (outMinor) *outMinor = RADHOOK_VERSION_MINOR; + if (outPatch) *outPatch = RADHOOK_VERSION_PATCH; +} + +RadHookResult RadHookVTableCreate(void** vtable, size_t index, void* detour, RadHookHandle* outHandle) { + if (outHandle) *outHandle = nullptr; + if (!vtable) return RadHookResult::InvalidTarget; + if (!detour) return RadHookResult::InvalidDetour; + + void** slot = vtable + index; + void* original = *slot; + if (!original) return RadHookResult::InvalidTarget; + + auto record = std::make_unique(); + record->kind = RadHookKind::VTable; + record->vtableSlot = slot; + record->vtableOriginalEntry = original; + record->target = original; + record->detour = detour; + record->trampoline = original; + + RadHookResult result = EnableHookInternal(record.get()); + 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 RadHookQueueEnable(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle; + handle->queuedAction = RadHookQueuedAction::Enable; + return RadHookResult::Success; +} + +RadHookResult RadHookQueueDisable(RadHookHandle handle) { + std::lock_guard lock(g_mutex); + if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle; + handle->queuedAction = RadHookQueuedAction::Disable; + return RadHookResult::Success; +} + +RadHookResult RadHookApplyQueued() { + std::lock_guard lock(g_mutex); + RadHookResult last = RadHookResult::Success; + for (auto& h : g_hooks) { + RadHookQueuedAction action = h->queuedAction; + if (action == RadHookQueuedAction::None) continue; + h->queuedAction = RadHookQueuedAction::None; + + RadHookResult r = (action == RadHookQueuedAction::Enable) + ? EnableHookInternal(h.get()) + : DisableHookInternal(h.get()); + + if (r != RadHookResult::Success && + r != RadHookResult::AlreadyEnabled && + r != RadHookResult::AlreadyDisabled) { + last = r; + } + } + return last; } \ No newline at end of file diff --git a/tests/test_queue.cpp b/tests/test_queue.cpp new file mode 100644 index 0000000..8ab4c32 --- /dev/null +++ b/tests/test_queue.cpp @@ -0,0 +1,68 @@ +#include +#include "radhook/radhook.h" + +static int fnA() { return 1; } +static int detourA() { return 100; } + +static int fnB() { return 2; } +static int detourB() { return 200; } + +static int fnC() { return 3; } +static int detourC() { return 300; } + +int main() { + RadHookHandle hA = nullptr; + RadHookHandle hB = nullptr; + + assert(RadHookCreate((void*)fnA, (void*)detourA, &hA) == RadHookResult::Success); + assert(RadHookCreate((void*)fnB, (void*)detourB, &hB) == RadHookResult::Success); + + // start both from a known, disabled state + assert(RadHookDisable(hA) == RadHookResult::Success); + assert(RadHookDisable(hB) == RadHookResult::Success); + assert(fnA() == 1); + assert(fnB() == 2); + + // queueing must not apply immediately + assert(RadHookQueueEnable(hA) == RadHookResult::Success); + assert(RadHookQueueEnable(hB) == RadHookResult::Success); + assert(!RadHookIsEnabled(hA)); + assert(!RadHookIsEnabled(hB)); + assert(fnA() == 1); + assert(fnB() == 2); + + // apply enables both together + assert(RadHookApplyQueued() == RadHookResult::Success); + assert(RadHookIsEnabled(hA)); + assert(RadHookIsEnabled(hB)); + assert(fnA() == 100); + assert(fnB() == 200); + + // last queued action before apply wins + assert(RadHookQueueDisable(hA) == RadHookResult::Success); + assert(RadHookQueueEnable(hA) == RadHookResult::Success); + assert(RadHookApplyQueued() == RadHookResult::Success); + assert(RadHookIsEnabled(hA)); + + // an apply with nothing queued is a no-op + assert(RadHookApplyQueued() == RadHookResult::Success); + + // queueing a redundant transition (already enabled) still applies cleanly + assert(RadHookQueueEnable(hA) == RadHookResult::Success); + assert(RadHookApplyQueued() == RadHookResult::Success); + assert(RadHookIsEnabled(hA)); + + // invalid handles + assert(RadHookQueueEnable(nullptr) == RadHookResult::InvalidHandle); + assert(RadHookQueueDisable(nullptr) == RadHookResult::InvalidHandle); + + // a destroyed hook can no longer be queued + RadHookHandle hC = nullptr; + assert(RadHookCreate((void*)fnC, (void*)detourC, &hC) == RadHookResult::Success); + RadHookDestroy(hC); + assert(RadHookQueueEnable(hC) == RadHookResult::InvalidHandle); + + RadHookDestroy(hA); + RadHookDestroy(hB); + return 0; +} \ No newline at end of file diff --git a/tests/test_version.cpp b/tests/test_version.cpp new file mode 100644 index 0000000..9fa68c5 --- /dev/null +++ b/tests/test_version.cpp @@ -0,0 +1,20 @@ +#include +#include "radhook/radhook.h" + +int main() { + int major = -1, minor = -1, patch = -1; + RadHookGetVersion(&major, &minor, &patch); + + assert(major == RADHOOK_VERSION_MAJOR); + assert(minor == RADHOOK_VERSION_MINOR); + assert(patch == RADHOOK_VERSION_PATCH); + + // individual out params may be omitted independently + int onlyMajor = -1; + RadHookGetVersion(&onlyMajor, nullptr, nullptr); + assert(onlyMajor == RADHOOK_VERSION_MAJOR); + + // must not crash + RadHookGetVersion(nullptr, nullptr, nullptr); + return 0; +} \ No newline at end of file diff --git a/tests/test_vtable.cpp b/tests/test_vtable.cpp new file mode 100644 index 0000000..67a8859 --- /dev/null +++ b/tests/test_vtable.cpp @@ -0,0 +1,68 @@ +#include +#include "radhook/radhook.h" + +struct IFoo { + virtual int Value() { return 7; } + virtual ~IFoo() = default; +}; + +static int DetourValue(IFoo* /*self*/) { return 777; } + +using Fn = int(*)(IFoo*); + +int main() { + IFoo obj; + void** vtable = *reinterpret_cast(&obj); + + RadHookHandle h = nullptr; + assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h) == RadHookResult::Success); + assert(RadHookIsValid(h)); + assert(RadHookIsEnabled(h)); + + // handle is created enabled, so the swap must already be visible + assert(obj.Value() == 777); + assert(reinterpret_cast(vtable[0])(&obj) == 777); + + void* originalEntry = RadHookGetTarget(h); + assert(originalEntry != nullptr); + assert(RadHookGetDetour(h) == reinterpret_cast(DetourValue)); + + // disable restores the original vtable entry + assert(RadHookDisable(h) == RadHookResult::Success); + assert(!RadHookIsEnabled(h)); + assert(obj.Value() == 7); + assert(vtable[0] == originalEntry); + + // re-enable re-applies the swap + assert(RadHookEnable(h) == RadHookResult::Success); + assert(obj.Value() == 777); + + // no relocation is needed for vtable hooks -- the trampoline is the + // original entry itself and can be called directly while the hook is live + auto original = RadHookGetOriginalAs(h); + assert(reinterpret_cast(original) == originalEntry); + assert(original(&obj) == 7); + + RadHookDestroy(h); + assert(!RadHookIsValid(h)); + assert(obj.Value() == 7); + + // queueing works uniformly across inline and vtable hook kinds + RadHookHandle h2 = nullptr; + assert(RadHookVTableCreate(vtable, 0, (void*)DetourValue, &h2) == RadHookResult::Success); + assert(RadHookQueueDisable(h2) == RadHookResult::Success); + assert(RadHookIsEnabled(h2)); // not applied yet + assert(RadHookApplyQueued() == RadHookResult::Success); + assert(!RadHookIsEnabled(h2)); + assert(obj.Value() == 7); + RadHookDestroy(h2); + + // invalid inputs + RadHookHandle bad = nullptr; + assert(RadHookVTableCreate(nullptr, 0, (void*)DetourValue, &bad) == RadHookResult::InvalidTarget); + assert(bad == nullptr); + assert(RadHookVTableCreate(vtable, 0, nullptr, &bad) == RadHookResult::InvalidDetour); + assert(bad == nullptr); + + return 0; +} \ No newline at end of file