mirror of
https://git.recroomarchive.org/Radium/libradhook.git
synced 2026-08-07 09:10:59 +00:00
api changes, check README
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
74
README.md
74
README.md
@@ -5,13 +5,16 @@ 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,8 +31,9 @@ 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 |
|
||||
|
||||
@@ -44,6 +48,8 @@ A helper PowerShell script is provided for common build targets:
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Inline hooking
|
||||
```cpp
|
||||
#include <radhook/radhook.h>
|
||||
|
||||
@@ -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,14 +78,59 @@ 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<void***>(&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<Fn>(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 |
|
||||
| `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 |
|
||||
@@ -88,6 +139,23 @@ int main() {
|
||||
| `RadHookGetDetour` | Get the detour function pointer |
|
||||
| `RadHookGetOriginal` / `RadHookGetOriginalAs<T>` | 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.
|
||||
@@ -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 <sal.h> // 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.
|
||||
*
|
||||
@@ -36,11 +55,12 @@ RADHOOK_API
|
||||
const char*
|
||||
RadHookResultToString(
|
||||
RadHookResult result
|
||||
);
|
||||
);
|
||||
|
||||
struct RadHookOpaque;
|
||||
using RadHookHandle = RadHookOpaque*;
|
||||
|
||||
_Success_(return == RadHookResult::Success)
|
||||
/**
|
||||
* @brief Creates and installs a hook.
|
||||
*
|
||||
@@ -57,18 +77,19 @@ using RadHookHandle = RadHookOpaque*;
|
||||
RADHOOK_API
|
||||
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.
|
||||
*
|
||||
@@ -79,9 +100,10 @@ requires std::is_function_v<std::remove_pointer_t<TargetFn>>
|
||||
RadHookResult
|
||||
RadHookCreate(
|
||||
TargetFn target,
|
||||
void* detour,
|
||||
RadHookHandle* outHandle
|
||||
) {
|
||||
_In_ void* detour,
|
||||
_Out_ RadHookHandle* outHandle
|
||||
)
|
||||
{
|
||||
return RadHookCreate(
|
||||
reinterpret_cast<void*>(target),
|
||||
detour,
|
||||
@@ -102,8 +124,8 @@ RadHookCreate(
|
||||
RADHOOK_API
|
||||
RadHookResult
|
||||
RadHookEnable(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Disables an installed hook.
|
||||
@@ -118,8 +140,8 @@ RadHookEnable(
|
||||
RADHOOK_API
|
||||
RadHookResult
|
||||
RadHookDisable(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Destroys a hook.
|
||||
@@ -134,8 +156,8 @@ RadHookDisable(
|
||||
RADHOOK_API
|
||||
RadHookResult
|
||||
RadHookDestroy(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Enables every registered hook.
|
||||
@@ -170,6 +192,7 @@ RADHOOK_API
|
||||
size_t
|
||||
RadHookGetCount();
|
||||
|
||||
_Out_writes_to_(maxCount, return)
|
||||
/**
|
||||
* @brief Retrieves registered hook handles.
|
||||
*
|
||||
@@ -184,9 +207,9 @@ RadHookGetCount();
|
||||
RADHOOK_API
|
||||
size_t
|
||||
RadHookGetHandles(
|
||||
RadHookHandle* outHandles,
|
||||
size_t maxCount
|
||||
);
|
||||
_Out_ RadHookHandle* outHandles,
|
||||
_In_ size_t maxCount
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Checks whether a hook handle is valid.
|
||||
@@ -198,8 +221,8 @@ RadHookGetHandles(
|
||||
RADHOOK_API
|
||||
bool
|
||||
RadHookIsValid(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Checks whether a hook is currently enabled.
|
||||
@@ -211,8 +234,8 @@ RadHookIsValid(
|
||||
RADHOOK_API
|
||||
bool
|
||||
RadHookIsEnabled(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Returns the hooked target function.
|
||||
@@ -225,8 +248,8 @@ RadHookIsEnabled(
|
||||
RADHOOK_API
|
||||
void*
|
||||
RadHookGetTarget(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Returns the detour function.
|
||||
@@ -239,8 +262,8 @@ RadHookGetTarget(
|
||||
RADHOOK_API
|
||||
void*
|
||||
RadHookGetDetour(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Returns the trampoline containing the relocated original code.
|
||||
@@ -255,8 +278,8 @@ RadHookGetDetour(
|
||||
RADHOOK_API
|
||||
void*
|
||||
RadHookGetOriginal(
|
||||
RadHookHandle handle
|
||||
);
|
||||
_In_ RadHookHandle handle
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief Returns the trampoline cast to a function pointer type.
|
||||
@@ -269,12 +292,14 @@ RadHookGetOriginal(
|
||||
template <typename FnPtr>
|
||||
FnPtr
|
||||
RadHookGetOriginalAs(
|
||||
RadHookHandle handle
|
||||
) {
|
||||
_In_ RadHookHandle handle
|
||||
)
|
||||
{
|
||||
return reinterpret_cast<FnPtr>(RadHookGetOriginal(handle));
|
||||
}
|
||||
|
||||
/**
|
||||
_Out_writes_to_(maxCount, return)
|
||||
/**
|
||||
* @brief Enumerates currently registered hooks.
|
||||
*
|
||||
* Copies up to @p maxCount active hook handles into @p out.
|
||||
@@ -291,8 +316,99 @@ RadHookGetOriginalAs(
|
||||
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__
|
||||
125
src/radhook.cpp
125
src/radhook.cpp
@@ -18,6 +18,8 @@
|
||||
# include <unistd.h>
|
||||
#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<LPVOID>(allocBase), size,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
void* p = VirtualAlloc(
|
||||
reinterpret_cast<LPVOID>(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;
|
||||
|
||||
@@ -759,3 +810,73 @@ 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<RadHookOpaque>();
|
||||
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<std::mutex> 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<std::mutex> lock(g_mutex);
|
||||
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||
handle->queuedAction = RadHookQueuedAction::Enable;
|
||||
return RadHookResult::Success;
|
||||
}
|
||||
|
||||
RadHookResult RadHookQueueDisable(RadHookHandle handle) {
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (!IsRegisteredLocked(handle)) return RadHookResult::InvalidHandle;
|
||||
handle->queuedAction = RadHookQueuedAction::Disable;
|
||||
return RadHookResult::Success;
|
||||
}
|
||||
|
||||
RadHookResult RadHookApplyQueued() {
|
||||
std::lock_guard<std::mutex> 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;
|
||||
}
|
||||
68
tests/test_queue.cpp
Normal file
68
tests/test_queue.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include <cassert>
|
||||
#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;
|
||||
}
|
||||
20
tests/test_version.cpp
Normal file
20
tests/test_version.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include <cassert>
|
||||
#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;
|
||||
}
|
||||
68
tests/test_vtable.cpp
Normal file
68
tests/test_vtable.cpp
Normal file
@@ -0,0 +1,68 @@
|
||||
#include <cassert>
|
||||
#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<void***>(&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<Fn>(vtable[0])(&obj) == 777);
|
||||
|
||||
void* originalEntry = RadHookGetTarget(h);
|
||||
assert(originalEntry != nullptr);
|
||||
assert(RadHookGetDetour(h) == reinterpret_cast<void*>(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<Fn>(h);
|
||||
assert(reinterpret_cast<void*>(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;
|
||||
}
|
||||
Reference in New Issue
Block a user