include/DetourModKit/hook.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_HOOK_HPP | ||
| 2 | #define DETOURMODKIT_HOOK_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file hook.hpp | ||
| 6 | * @brief The hooking surface: free verbs that return move-only RAII handles. It hides SafetyHook and Zydis. | ||
| 7 | * @details @ref inline_at, @ref mid_at, and @ref install_all return a disabled hook, and `Hook::enable()` arms it | ||
| 8 | * (`[B-83]`). @ref HookStack::install creates and arms a hook in one process coordinator hold. @ref vmt_for | ||
| 9 | * is live at creation. | ||
| 10 | * | ||
| 11 | * Do not call a hook mutation from DllMain or from a thread that holds the loader lock. Every install, toggle, | ||
| 12 | * batch, VMT creation, and VMT mutation returns @ref ErrorCode::LoaderLockActive before its own object-gate, | ||
| 13 | * ledger, backend, allocation, or protection work. Argument construction at the call site stays the caller's. | ||
| 14 | * The Hook and VmtHook destructors retain unsafe state instead of a wait. | ||
| 15 | * | ||
| 16 | * Participants are the DetourModKit copies in this process that share the process route coordinator in | ||
| 17 | * docs/design/hooking.md. They share backend patch order and route dependencies. Layered hooks on one | ||
| 18 | * target retire in any order (`[B-16]`): the newest layer restores, and a lower layer bypasses its own | ||
| 19 | * detour. A coordinator refusal prevents mutation or retains the route. Foreign libraries outside that | ||
| 20 | * protocol receive no cross-instance lifetime guarantee. | ||
| 21 | */ | ||
| 22 | |||
| 23 | #include "DetourModKit/address.hpp" | ||
| 24 | #include "DetourModKit/detail/counted_entry.hpp" | ||
| 25 | #include "DetourModKit/error.hpp" | ||
| 26 | #include "DetourModKit/scan.hpp" | ||
| 27 | |||
| 28 | #include <array> | ||
| 29 | #include <atomic> | ||
| 30 | #include <chrono> | ||
| 31 | #include <cstddef> | ||
| 32 | #include <cstdint> | ||
| 33 | #include <cstring> | ||
| 34 | #include <functional> | ||
| 35 | #include <memory> | ||
| 36 | #include <mutex> | ||
| 37 | #include <span> | ||
| 38 | #include <string> | ||
| 39 | #include <string_view> | ||
| 40 | #include <type_traits> | ||
| 41 | #include <utility> | ||
| 42 | #include <variant> | ||
| 43 | #include <vector> | ||
| 44 | |||
| 45 | namespace DetourModKit | ||
| 46 | { | ||
| 47 | namespace hook | ||
| 48 | { | ||
| 49 | /** @brief Opaque mid-hook register state. Never define it: the accessor casts need an incomplete type. */ | ||
| 50 | struct MidContext; | ||
| 51 | |||
| 52 | /** | ||
| 53 | * @brief DMK-owned mid-hook detour signature. | ||
| 54 | * @warning The callback must not throw (`[B-84]`). DMK contains an exception that escapes, counts it, and logs | ||
| 55 | * once per site. DMK then treats the callback as complete, with the context as the callback left it. | ||
| 56 | * @note The callback can re-enter the hooked target. | ||
| 57 | * @warning A callback that destroys its own Hook retains the backend. See @ref Hook::~Hook. | ||
| 58 | */ | ||
| 59 | using MidHookFn = void (*)(MidContext &); | ||
| 60 | |||
| 61 | /** @brief Selects a general-purpose register other than rsp and rip for gpr() at a mid-hook site. */ | ||
| 62 | enum class Gpr : std::uint8_t | ||
| 63 | { | ||
| 64 | Rax, | ||
| 65 | Rbx, | ||
| 66 | Rcx, | ||
| 67 | Rdx, | ||
| 68 | Rsi, | ||
| 69 | Rdi, | ||
| 70 | Rbp, | ||
| 71 | R8, | ||
| 72 | R9, | ||
| 73 | R10, | ||
| 74 | R11, | ||
| 75 | R12, | ||
| 76 | R13, | ||
| 77 | R14, | ||
| 78 | R15 | ||
| 79 | }; | ||
| 80 | |||
| 81 | /** @brief Read-only by-value snapshot of one 128-bit XMM register captured at the mid-hook site. */ | ||
| 82 | struct alignas(16) XmmView | ||
| 83 | { | ||
| 84 | std::array<std::byte, 16> bytes; | ||
| 85 | |||
| 86 | /** | ||
| 87 | * @brief Returns the index-th T-sized lane of the captured bytes, or zero for an out-of-range lane. | ||
| 88 | * @tparam T A scalar lane type. The constraint excludes `bool`, because a captured byte is not a valid | ||
| 89 | * `bool` object representation. MidContextXmmViewTest.LaneRejectsBoolAndAdmitsScalars pins the set. | ||
| 90 | * @note Callback-safe: a pure read of the captured context. | ||
| 91 | */ | ||
| 92 | template <typename T> | ||
| 93 | requires(std::is_trivially_copyable_v<T> && !std::is_same_v<T, bool>) | ||
| 94 | 5 | [[nodiscard]] T lane(std::size_t index) const noexcept | |
| 95 | { | ||
| 96 | 5 | T value{}; | |
| 97 | 10 | if (index >= bytes.size() / sizeof(T)) | |
| 98 | { | ||
| 99 | 3 | return value; | |
| 100 | } | ||
| 101 | 2 | std::memcpy(&value, bytes.data() + index * sizeof(T), sizeof(T)); | |
| 102 | 2 | return value; | |
| 103 | } | ||
| 104 | }; | ||
| 105 | |||
| 106 | static_assert(sizeof(void *) == 8, "MidContext register set is Windows x64 only"); | ||
| 107 | |||
| 108 | /** | ||
| 109 | * @brief Returns a mutable reference to a captured general-purpose register. A write survives the resume. | ||
| 110 | * @note Callback-safe: a pure read or write of the captured context. | ||
| 111 | */ | ||
| 112 | [[nodiscard]] std::uintptr_t &gpr(MidContext &ctx, Gpr reg) noexcept; | ||
| 113 | |||
| 114 | /** | ||
| 115 | * @brief Returns the captured stack pointer (rsp) by value. To move the stack, use resume_stack_pointer(). | ||
| 116 | * @note Callback-safe: a pure read of the captured context. | ||
| 117 | */ | ||
| 118 | [[nodiscard]] std::uintptr_t stack_pointer(const MidContext &ctx) noexcept; | ||
| 119 | |||
| 120 | /** | ||
| 121 | * @brief Returns a mutable reference to the stack pointer that the resumed original code runs on. | ||
| 122 | * @note Callback-safe: a pure read or write of the captured context. | ||
| 123 | */ | ||
| 124 | [[nodiscard]] std::uintptr_t &resume_stack_pointer(MidContext &ctx) noexcept; | ||
| 125 | |||
| 126 | /** | ||
| 127 | * @brief Returns a mutable reference to the captured instruction pointer (rip). | ||
| 128 | * @details The resume lands at this value. Write the address of a same-signature function to resume there. | ||
| 129 | * @note Callback-safe: a pure read or write of the captured context. | ||
| 130 | */ | ||
| 131 | [[nodiscard]] std::uintptr_t &instruction_pointer(MidContext &ctx) noexcept; | ||
| 132 | |||
| 133 | /** | ||
| 134 | * @brief Returns a mutable reference to the captured rflags, which the trampoline restores on resume. | ||
| 135 | * @note Callback-safe: a pure read or write of the captured context. | ||
| 136 | */ | ||
| 137 | [[nodiscard]] std::uintptr_t &flags(MidContext &ctx) noexcept; | ||
| 138 | |||
| 139 | /** | ||
| 140 | * @brief Returns a snapshot of XMM register @p index (0 to 15), or a zeroed view for an out-of-range index. | ||
| 141 | * @note Callback-safe: a pure read of the captured context. | ||
| 142 | * @warning The mid-hook frame preserves only XMM0 to XMM15. A detour must not clobber YMM or ZMM upper state, | ||
| 143 | * ZMM16 to ZMM31, opmask registers, x87, MMX, or MXCSR state. | ||
| 144 | */ | ||
| 145 | [[nodiscard]] XmmView xmm(const MidContext &ctx, std::size_t index) noexcept; | ||
| 146 | |||
| 147 | /** | ||
| 148 | * @brief Policy for a target whose first byte is 0xCC or 0xCD (int3 or int n) instead of a function body. | ||
| 149 | * @details @ref Fail refuses the create with @ref ErrorCode::TargetPrologueUnsafe. @ref Relocate logs and | ||
| 150 | * installs anyway. The backend decode decides relocation, so a relative call passes this check. A | ||
| 151 | * backend refusal returns @ref ErrorCode::BackendFailed and logs the reason. Both policies refuse a | ||
| 152 | * target whose settled bytes are not readable, executable, committed memory (see @ref Hook). | ||
| 153 | */ | ||
| 154 | enum class Prologue : std::uint8_t | ||
| 155 | { | ||
| 156 | Fail, | ||
| 157 | Relocate | ||
| 158 | }; | ||
| 159 | |||
| 160 | /** | ||
| 161 | * @brief Per-row policy for a @ref HookSpec in @ref install_all. | ||
| 162 | * @details A @ref Mandatory miss fails the call. A @ref BestEffort miss records its Error in the row outcome | ||
| 163 | * and skips the row. | ||
| 164 | */ | ||
| 165 | enum class Severity : std::uint8_t | ||
| 166 | { | ||
| 167 | BestEffort, | ||
| 168 | Mandatory | ||
| 169 | }; | ||
| 170 | |||
| 171 | /** @brief Per-hook policy for @ref inline_at and @ref mid_at. */ | ||
| 172 | struct Options | ||
| 173 | { | ||
| 174 | /// The policy for a breakpoint first byte. See @ref Prologue. | ||
| 175 | Prologue prologue = Prologue::Fail; | ||
| 176 | |||
| 177 | /** | ||
| 178 | * @brief Refuses the install when the target already appears hooked. | ||
| 179 | * @details A same-kit hook in this instance's ledger returns @ref ErrorCode::TargetAlreadyHookedByThisKit. | ||
| 180 | * Otherwise a foreign jump over the prologue returns | ||
| 181 | * @ref ErrorCode::TargetAlreadyHookedByAnotherModule. The jump forms are E9 rel32, FF25 indirect, | ||
| 182 | * and `mov rax, imm64` then `jmp rax`. The default (false) layers the new hook on top. A vacated | ||
| 183 | * position of an earlier generation counts as a hook, so this option refuses its successor. | ||
| 184 | */ | ||
| 185 | bool fail_if_already_hooked = false; | ||
| 186 | }; | ||
| 187 | |||
| 188 | namespace detail | ||
| 189 | { | ||
| 190 | /// Satisfied only by a pointer-to-function type, the valid cast target for Hook::original. | ||
| 191 | template <typename T> | ||
| 192 | concept FunctionPointer = std::is_pointer_v<T> && std::is_function_v<std::remove_pointer_t<T>>; | ||
| 193 | } // namespace detail | ||
| 194 | |||
| 195 | /// An absolute @ref Address, or a @ref scan::OwnedScanRequest that the install resolves through scan::resolve. | ||
| 196 | using Target = std::variant<Address, scan::OwnedScanRequest>; | ||
| 197 | |||
| 198 | /** | ||
| 199 | * @brief A request to install one inline hook by @ref inline_at. | ||
| 200 | * @details @ref name also identifies the hook's chain position across generations. A later install with the | ||
| 201 | * same name, target, and kind claims a position that an earlier generation vacated (`[B-105]`). Keep | ||
| 202 | * names stable across builds: a renamed hook installs fresh on top. | ||
| 203 | */ | ||
| 204 | struct InlineRequest | ||
| 205 | { | ||
| 206 | std::string name; | ||
| 207 | Target target; | ||
| 208 | Options options{}; | ||
| 209 | }; | ||
| 210 | |||
| 211 | /// A request to install one mid hook by @ref mid_at. @ref InlineRequest states what the name identifies. | ||
| 212 | struct MidRequest | ||
| 213 | { | ||
| 214 | std::string name; | ||
| 215 | Target target; | ||
| 216 | Options options{}; | ||
| 217 | }; | ||
| 218 | |||
| 219 | /** | ||
| 220 | * @brief The return of the game call into a counted hook. A counted detour receives it as its first parameter. | ||
| 221 | * @details The relay jumps into the counted thunk, so the slot is the stack word of the game caller's return | ||
| 222 | * address. A write to the slot changes where the hooked function returns. The counted @ref inline_at | ||
| 223 | * states what the return-address intrinsics name inside the detour. | ||
| 224 | */ | ||
| 225 | struct CallerFrame | ||
| 226 | { | ||
| 227 | /// The stack slot that holds the return address of the call into the hooked function. | ||
| 228 | void **return_address_slot; | ||
| 229 | |||
| 230 | /// Returns the return address of the call into the hooked function. | ||
| 231 | 4 | [[nodiscard]] void *return_address() const noexcept { return *return_address_slot; } | |
| 232 | }; | ||
| 233 | |||
| 234 | class Hook; | ||
| 235 | |||
| 236 | namespace detail | ||
| 237 | { | ||
| 238 | /// The internal access path to a Hook's private state, defined in src/internal/hook_backend.hpp. | ||
| 239 | struct HookAccess; | ||
| 240 | |||
| 241 | /// The non-template inline-install primitive behind the raw @ref inline_at. | ||
| 242 | [[nodiscard]] Result<Hook> inline_at_raw(InlineRequest request, void *detour); | ||
| 243 | |||
| 244 | /// The non-template inline-install primitive behind the counted @ref inline_at. | ||
| 245 | [[nodiscard]] Result<Hook> inline_at_counted(InlineRequest request, CountedInstall install); | ||
| 246 | |||
| 247 | /// Checks the counted detour signature of @ref inline_at and @ref HookStack::install, and names its thunk. | ||
| 248 | 4386 | template <auto Detour> [[nodiscard]] CountedInstall counted_install_for() noexcept | |
| 249 | { | ||
| 250 | // GCC drops the may_alias attribute of a vector type such as __m128 inside a template argument and | ||
| 251 | // warns. The thunk forwards such a value by the ABI of the vector type, which that attribute does not | ||
| 252 | // change. | ||
| 253 | #if defined(__GNUC__) || defined(__clang__) | ||
| 254 | #pragma GCC diagnostic push | ||
| 255 | #pragma GCC diagnostic ignored "-Wignored-attributes" | ||
| 256 | #endif | ||
| 257 | using Signature = CountedSignature<decltype(Detour), CallerFrame>; | ||
| 258 | #if defined(__GNUC__) || defined(__clang__) | ||
| 259 | #pragma GCC diagnostic pop | ||
| 260 | #endif | ||
| 261 | static_assert( | ||
| 262 | Signature::valid, | ||
| 263 | "A counted inline hook needs a noexcept, non-variadic detour whose parameters and result are " | ||
| 264 | "references or trivially copyable. Pass the detour as an argument for another signature." | ||
| 265 | ); | ||
| 266 | static_assert(Detour != nullptr, "A counted inline hook needs a detour"); | ||
| 267 | using Entry = typename Signature::template entry<Detour>; | ||
| 268 | return CountedInstall{ | ||
| 269 | .entry = reinterpret_cast<void *>(&Entry::entry), | ||
| 270 | .block = &Entry::block, | ||
| 271 | 4386 | }; | |
| 272 | } | ||
| 273 | } // namespace detail | ||
| 274 | |||
| 275 | /** | ||
| 276 | * @brief The caller atomic that receives the original of an inline hook from @ref HookStack::install. | ||
| 277 | * @details It converts from a pointer to `std::atomic<Fn>` for any function pointer type, so a designated | ||
| 278 | * initializer names the caller's own atomic. A default slot receives nothing. | ||
| 279 | */ | ||
| 280 | class OriginalSlot | ||
| 281 | { | ||
| 282 | public: | ||
| 283 | 2001 | OriginalSlot() noexcept = default; | |
| 284 | |||
| 285 | /// Selects @p slot. The atomic must outlive every call that can load it. | ||
| 286 | template <detail::FunctionPointer Fn> | ||
| 287 | 2235 | OriginalSlot(std::atomic<Fn> *slot) noexcept : m_slot(slot), m_store(&store_original<Fn>) | |
| 288 | { | ||
| 289 | 2235 | } | |
| 290 | |||
| 291 | private: | ||
| 292 | 2233 | template <class Fn> static void store_original(void *slot, void *original) noexcept | |
| 293 | { | ||
| 294 | 2233 | static_cast<std::atomic<Fn> *>(slot)->store(reinterpret_cast<Fn>(original), std::memory_order_release); | |
| 295 | 2233 | } | |
| 296 | |||
| 297 | /// Stores @p original into the selected atomic with release order. A default slot ignores the call. | ||
| 298 | 4234 | void publish(void *original) const noexcept | |
| 299 | { | ||
| 300 | 4234 | if (m_slot != nullptr) | |
| 301 | { | ||
| 302 | 2233 | m_store(m_slot, original); | |
| 303 | } | ||
| 304 | 4234 | } | |
| 305 | |||
| 306 | void *m_slot{nullptr}; | ||
| 307 | void (*m_store)(void *, void *) noexcept {nullptr}; | ||
| 308 | |||
| 309 | friend struct detail::HookAccess; | ||
| 310 | }; | ||
| 311 | |||
| 312 | /// The request of the inline @ref HookStack::install forms. @ref InlineRequest states what the name identifies. | ||
| 313 | struct InlineInstallRequest | ||
| 314 | { | ||
| 315 | std::string name; | ||
| 316 | Target target; | ||
| 317 | Options options{}; | ||
| 318 | /// Receives the original before the arm. Leave it empty for a detour that never calls the original. | ||
| 319 | OriginalSlot original{}; | ||
| 320 | }; | ||
| 321 | |||
| 322 | #ifdef _MSC_VER | ||
| 323 | #pragma warning(push) | ||
| 324 | // The gate slot storage carries the slot's alignment, so the padding that C4324 reports is intended. | ||
| 325 | #pragma warning(disable : 4324) | ||
| 326 | #endif | ||
| 327 | |||
| 328 | /** | ||
| 329 | * @brief Move-only RAII handle for one inline or mid hook. The destructor restores the prologue, or retires a | ||
| 330 | * layer that cannot restore by bypass. | ||
| 331 | * @note Hooks that share one target, in this copy or in other copies, retire in any order (`[B-16]`). | ||
| 332 | * @note Lock order (`[B-109]`): a toggle takes the per-hook call gate, then the HookLedger target slot, and | ||
| 333 | * then the backend process coordinator. It releases them in reverse order. An install takes its target | ||
| 334 | * reservation and its module reference before the coordinator. A wait line logs while the call holds no | ||
| 335 | * coordinator. Every other log line and lifecycle event follows every release. | ||
| 336 | * @note An install and a public toggle wait while another thread or DMK copy holds the process coordinator. An | ||
| 337 | * install also waits while a patcher outside the coordinator holds the page of an image target without | ||
| 338 | * execute access. No attempt count or time cap applies, and each wait logs one line. Only a coordinator | ||
| 339 | * cause that no retry changes fails at once: BackendFailed for an install, EnableFailed or DisableFailed | ||
| 340 | * for a toggle. | ||
| 341 | */ | ||
| 342 | class Hook | ||
| 343 | { | ||
| 344 | public: | ||
| 345 | Hook(Hook &&other) noexcept; | ||
| 346 | Hook &operator=(Hook &&other) noexcept; | ||
| 347 | Hook(const Hook &) = delete; | ||
| 348 | Hook &operator=(const Hook &) = delete; | ||
| 349 | |||
| 350 | /** | ||
| 351 | * @brief Restores the target when safe and reclaims an idle mid route, or retires the hook as residue. | ||
| 352 | * @details The newest layer over its own patch restores and frees the backend over Original bytes. These | ||
| 353 | * layers retire instead (`[B-104]`): | ||
| 354 | * - a layer under a newer one, | ||
| 355 | * - a layer over foreign or unreadable bytes, | ||
| 356 | * - a layer that a newer participant refuses through the coordinator, | ||
| 357 | * - a claimed position. | ||
| 358 | * | ||
| 359 | * A retirement switches the relay or route off without a target write and drains a mid route. | ||
| 360 | * It then vacates the position for the next generation with the same name. The residue keeps | ||
| 361 | * the target tracked as hooked and releases the module reference. It counts in | ||
| 362 | * @ref diagnostics::LeakSubsystem::HookResidue, whose contract states when a position counts. | ||
| 363 | * | ||
| 364 | * These conditions retain the backend with its detour reachable: | ||
| 365 | * - the loader lock, | ||
| 366 | * - an owned restore that failed, | ||
| 367 | * - a ledger that cannot decide the layer order, | ||
| 368 | * - a process coordinator that refuses or times out, | ||
| 369 | * - a backend that cannot switch its relay off, | ||
| 370 | * - an unproved drain, an unresolved continuation, or a nonlocal exit, | ||
| 371 | * - a route idle proof that fails while a thread can still reach the mid stub, | ||
| 372 | * - a newer retained route that depends on this one, or unwind records that stay registered, | ||
| 373 | * - a full residue budget: more than 16 layers on the target, or fewer than 64 free process | ||
| 374 | * records. | ||
| 375 | * | ||
| 376 | * Such a backend keeps its module reference for the process lifetime, and a mid backend also | ||
| 377 | * keeps its adapter and capacity charge. `[B-73]` attributes each such retention to | ||
| 378 | * @ref diagnostics::LeakSubsystem::HookManager. A restored trampoline that fails its idle proof | ||
| 379 | * stays mapped as HookResidue. So does a restored mid route whose stub no thread can reach | ||
| 380 | * (`[B-108]`). | ||
| 381 | * | ||
| 382 | * A mid hook tombstones its callback before `[B-85]` rundown, so no new callback begins after | ||
| 383 | * destruction returns. Mid teardown waits a bounded time for admitted callbacks and adapter | ||
| 384 | * bodies. The loader lock, a published unload phase, self-destruction, or an unrecorded entrant | ||
| 385 | * prevents that wait. An expired or prevented wait retains the backend, and an admitted callback | ||
| 386 | * can still run after destruction returns. | ||
| 387 | * | ||
| 388 | * A counted inline hook switches its relay off first and waits up to one second for every call | ||
| 389 | * inside its detour to leave (`[B-106]`). A call that stays, or a teardown from inside the | ||
| 390 | * detour, retains the backend. | ||
| 391 | * @warning Quiescence of a raw inline detour is caller-owned for a restore and for a residue. See the raw | ||
| 392 | * @ref inline_at. | ||
| 393 | * @note Setup/control-plane only: teardown mutates the target and can wait for callbacks and continuations. | ||
| 394 | */ | ||
| 395 | ~Hook() noexcept; | ||
| 396 | |||
| 397 | /// True while this handle owns a live hook (false for a moved-from or released handle). | ||
| 398 | [[nodiscard]] explicit operator bool() const noexcept; | ||
| 399 | |||
| 400 | /// The hook's registered name (empty for a moved-from or released handle). | ||
| 401 | [[nodiscard]] std::string_view name() const noexcept; | ||
| 402 | |||
| 403 | /** | ||
| 404 | * @brief True when the hook is armed or conservatively retained as possibly reachable. | ||
| 405 | * @details It reads the published state and backend flag, not target bytes, and never repairs drift. A | ||
| 406 | * toggle that reads Foreign or unreadable bytes after a committed restore keeps it true, because a | ||
| 407 | * newer layer can still reach the trampoline. A toggle that reads Original bytes after the | ||
| 408 | * restore, or a bypass, makes it false. | ||
| 409 | * @note Setup/control-plane only: it waits on the per-hook call gate for an in-flight call or toggle. | ||
| 410 | */ | ||
| 411 | [[nodiscard]] bool is_enabled() const noexcept; | ||
| 412 | |||
| 413 | /** | ||
| 414 | * @brief Returns the unguarded typed trampoline of an inline hook. If teardown can race, use @ref call. | ||
| 415 | * @return The trampoline, or nullptr for a mid hook, a disengaged handle, or a backend miss. | ||
| 416 | * @note Callback-safe: one indirection, with no lock. The caller must keep the hook alive across the call. | ||
| 417 | */ | ||
| 418 | 3667 | template <detail::FunctionPointer Fn> [[nodiscard]] Fn original() const noexcept | |
| 419 | { | ||
| 420 | 3667 | return reinterpret_cast<Fn>(original_address()); | |
| 421 | } | ||
| 422 | |||
| 423 | /** | ||
| 424 | * @brief Calls the original of an inline hook through the trampoline under the per-hook call gate. | ||
| 425 | * @tparam Args The exact by-value parameter types of the original, because a deduced reference rebuilds the | ||
| 426 | * wrong function-pointer ABI. A move-constructible argument moves into the dispatch. | ||
| 427 | * @return The original's return value, or a value-initialized Ret when the hook is inactive or not inline. | ||
| 428 | * @details Teardown can race the call: a late call fails closed, and an in-flight call drains before | ||
| 429 | * teardown frees backend storage. The Hook object's storage must outlive the call. The gate does | ||
| 430 | * not drain a thread that entered the original by another path. | ||
| 431 | * @note Callback-safe: the atomic gate pin is bounded, and no allocation or I/O precedes dispatch. | ||
| 432 | * @warning Concurrent calls through one handle serialize on the recursive gate mutex for the full call. | ||
| 433 | * For a hot target on several threads, use @ref original. | ||
| 434 | */ | ||
| 435 | 97536 | template <typename Ret = void, typename... Args> Ret call(Args... args) const | |
| 436 | { | ||
| 437 | // GuardedDispatch pins the gate and holds its lock through this invocation. | ||
| 438 | 97536 | const GuardedDispatch dispatch{*this}; | |
| 439 | 97536 | if (dispatch.trampoline == nullptr) | |
| 440 | { | ||
| 441 | if constexpr (!std::is_void_v<Ret>) | ||
| 442 | { | ||
| 443 | 97517 | return Ret{}; | |
| 444 | } | ||
| 445 | else | ||
| 446 | { | ||
| 447 | return; | ||
| 448 | } | ||
| 449 | } | ||
| 450 | 19 | return reinterpret_cast<Ret (*)(Args...)>(dispatch.trampoline)(forward_call_argument<Args>(args)...); | |
| 451 | 97536 | } | |
| 452 | |||
| 453 | /** | ||
| 454 | * @brief Works as @ref call, but reports a refused dispatch as an error instead of a value-initialized Ret. | ||
| 455 | * @return The original's return value, or InvalidHookState when the gate refuses dispatch. | ||
| 456 | * @note Callback-safe on the same terms as @ref call. | ||
| 457 | */ | ||
| 458 | 33 | template <typename Ret = void, typename... Args> [[nodiscard]] Result<Ret> try_call(Args... args) const | |
| 459 | { | ||
| 460 | 33 | const GuardedDispatch dispatch{*this}; | |
| 461 | 33 | if (dispatch.trampoline == nullptr) | |
| 462 | { | ||
| 463 | 18 | return std::unexpected(Error{ErrorCode::InvalidHookState, "hook::try_call"}); | |
| 464 | } | ||
| 465 | if constexpr (std::is_void_v<Ret>) | ||
| 466 | { | ||
| 467 | 1 | reinterpret_cast<void (*)(Args...)>(dispatch.trampoline)(forward_call_argument<Args>(args)...); | |
| 468 | 1 | return {}; | |
| 469 | } | ||
| 470 | else | ||
| 471 | { | ||
| 472 | 28 | return reinterpret_cast<Ret (*)(Args...)>(dispatch.trampoline)( | |
| 473 | 3 | forward_call_argument<Args>(args)... | |
| 474 | 14 | ); | |
| 475 | } | ||
| 476 | 33 | } | |
| 477 | |||
| 478 | /** | ||
| 479 | * @brief Arms the hook: patches the target so that the detour runs. | ||
| 480 | * @return Success when the hook is active. Otherwise LoaderLockActive, LayerConflict, BackendFailed, | ||
| 481 | * EnableFailed, DisableFailed, ReentrantCallRejected, or InvalidHookState. | ||
| 482 | * @details The target bytes decide the published state (`[B-97]`), and Foreign or unreadable bytes after a | ||
| 483 | * committed arm keep the hook Active. For a restored hook, Foreign or unreadable bytes before the | ||
| 484 | * write refuse with EnableFailed and write nothing. An Active hook under a newer layer succeeds, | ||
| 485 | * unless a layer on the chain from the target saved this hook's window. That saved window proves | ||
| 486 | * that a restore outside DMK removed this hook's patch. For such a hook, and for a hook that | ||
| 487 | * claims Active over Original bytes, the call first reconciles it to Disabled and arms again | ||
| 488 | * (HookLayeredEnable.*). EnableFailed means that this call published no new arm. A byte refusal | ||
| 489 | * keeps the prior state and bytes, and any other EnableFailed leaves the hook Disabled. | ||
| 490 | * BackendFailed means that the hook is active, but page protection can stay unrestored. | ||
| 491 | * @warning DisableFailed means that this call did not prove a disarm after a rejected or uncertain arm. The | ||
| 492 | * handle stays active, so quiesce or disable it before teardown. | ||
| 493 | * @note A hook that @ref disable bypassed, and a hook that claimed a vacated position, re-arm by one store | ||
| 494 | * with no target write, under any newer layer. Every other arm makes the hook the newest layer of its | ||
| 495 | * target. Over the patch of another layer, local or another participant's, the arm chains to it at | ||
| 496 | * the same trampoline entry (`[B-110]`). Only the newest layer re-arms over its own patch (`[B-16]`). | ||
| 497 | * @note LayerConflict means that the hook cannot chain to the layer on the target, such as a far trampoline | ||
| 498 | * or a jump out of range, that its own patch lies under a newer layer, or that the ledger cannot | ||
| 499 | * decide the layer order. It writes no bytes and keeps the hook's state, but a coordinator refusal | ||
| 500 | * after the reconciliation leaves it Disabled. While another thread runs the trampoline that the arm | ||
| 501 | * rewrites, the call waits, with no attempt count. The wait holds no DMK lock and the hook stays | ||
| 502 | * Disabled, so that thread can call @ref is_enabled, @ref call, or a toggle on the same target. | ||
| 503 | * ReentrantCallRejected means that the calling thread returns into that trampoline, so the wait | ||
| 504 | * cannot end. | ||
| 505 | * @note Idempotent and thread-safe. Publish everything the detour needs before this call (`[B-83]`). | ||
| 506 | * @note Setup/control-plane only: the arm patches the target and serializes on the per-hook call gate. It | ||
| 507 | * waits for a held process coordinator, as @ref Hook states. | ||
| 508 | */ | ||
| 509 | [[nodiscard]] Result<void> enable() noexcept; | ||
| 510 | |||
| 511 | /** | ||
| 512 | * @brief Disarms the hook but leaves it installed. | ||
| 513 | * @return Success when the hook is disabled. Otherwise LoaderLockActive, LayerConflict, BackendFailed, | ||
| 514 | * DisableFailed, or InvalidHookState. | ||
| 515 | * @details An Active layer under a newer layer, local or another participant's, bypasses. So does an | ||
| 516 | * Active layer over foreign or unreadable bytes. One store sends every caller past its detour, no | ||
| 517 | * target byte changes, and the hook publishes Disabled (`[B-97]`, `[B-104]`). A second disable | ||
| 518 | * succeeds. Once the bypassed hook is the newest layer over its own patch again, that call also | ||
| 519 | * restores. A refused process coordinator fails only a restore. | ||
| 520 | * | ||
| 521 | * The newest layer over its own patch restores, and Disabled publishes only once Original bytes | ||
| 522 | * read back. Any other bytes after the restore keep the hook Active, so a retry can disarm after | ||
| 523 | * this hook's exact patch bytes return. If the hook claims Disabled over its exact patch bytes, | ||
| 524 | * the call first reconciles it to Active and retries the restore. DisableFailed means that this | ||
| 525 | * call did not prove a disarm. BackendFailed means that the disarm took effect, but page | ||
| 526 | * protection can stay unrestored. | ||
| 527 | * @note A restored lower layer gets LayerConflict. A restored top layer over foreign or unreadable | ||
| 528 | * bytes gets DisableFailed. A ledger that cannot decide the layer order returns LayerConflict and | ||
| 529 | * changes nothing. | ||
| 530 | * @note Setup/control-plane only: the disarm restores the target and serializes on the per-hook call gate. | ||
| 531 | * The newest local layer waits for a held process coordinator before it reads its bytes, as | ||
| 532 | * @ref Hook states. That includes a hook under a newer layer of another participant. A claimed hook | ||
| 533 | * and a layer under a newer local layer do not wait. | ||
| 534 | */ | ||
| 535 | [[nodiscard]] Result<void> disable() noexcept; | ||
| 536 | |||
| 537 | /** | ||
| 538 | * @brief Detaches the hook from this handle. The hook keeps its state, backend, and ledger record for the | ||
| 539 | * process lifetime. | ||
| 540 | * @note @ref diagnostics::LeakSubsystem::HookManager counts the release. @ref is_target_hooked still | ||
| 541 | * reports the target, and an install with @ref Options::fail_if_already_hooked stays refused. A layer | ||
| 542 | * under this one bypasses on @ref disable and retires as residue. A layer installed after it tears | ||
| 543 | * down normally. | ||
| 544 | * @note Setup/control-plane only. | ||
| 545 | * @warning The detour and everything it reaches must stay mapped for the rest of the process. | ||
| 546 | */ | ||
| 547 | void release() noexcept; | ||
| 548 | |||
| 549 | private: | ||
| 550 | struct Impl; | ||
| 551 | |||
| 552 | template <typename Arg> | ||
| 553 | 26 | [[nodiscard]] static decltype(auto) forward_call_argument(std::remove_reference_t<Arg> &arg) noexcept | |
| 554 | { | ||
| 555 | if constexpr (!std::is_reference_v<Arg> && !std::is_move_constructible_v<Arg>) | ||
| 556 | { | ||
| 557 | 2 | return (arg); | |
| 558 | } | ||
| 559 | else | ||
| 560 | { | ||
| 561 | 24 | return std::forward<Arg>(arg); | |
| 562 | } | ||
| 563 | } | ||
| 564 | |||
| 565 | /// The refcounted call guard in src/internal/hook_backend.hpp. A call pins it, so teardown cannot free it. | ||
| 566 | struct CallGate; | ||
| 567 | Hook(std::unique_ptr<Impl> impl, std::shared_ptr<CallGate> gate) noexcept; | ||
| 568 | |||
| 569 | /// The raw inline trampoline or nullptr, without a guard, behind original<Fn>(). | ||
| 570 | [[nodiscard]] void *original_address() const noexcept; | ||
| 571 | |||
| 572 | /// Copies the atomic call-gate reference into a strong local for @ref call to pin. | ||
| 573 | [[nodiscard]] std::shared_ptr<CallGate> pin_call_gate() const noexcept; | ||
| 574 | |||
| 575 | /// Locks the gate's recursive_mutex for @ref call. A lock failure returns an unowned lock. | ||
| 576 | [[nodiscard]] std::unique_lock<std::recursive_mutex> acquire_call_lock(CallGate *gate) const noexcept; | ||
| 577 | |||
| 578 | /// The gate's published trampoline, or nullptr when inactive. Read it with the call lock held. | ||
| 579 | [[nodiscard]] void *active_trampoline(CallGate *gate) const noexcept; | ||
| 580 | |||
| 581 | /** @brief Pins, locks, and snapshots the call gate. A failed stage leaves @ref trampoline null. */ | ||
| 582 | struct GuardedDispatch | ||
| 583 | { | ||
| 584 | 97569 | explicit GuardedDispatch(const Hook &hook) | |
| 585 | 97569 | { | |
| 586 | 97569 | gate = hook.pin_call_gate(); | |
| 587 | 97569 | if (!gate) | |
| 588 | 74968 | return; | |
| 589 | 22601 | guard = hook.acquire_call_lock(gate.get()); | |
| 590 | 22601 | if (!guard.owns_lock()) | |
| 591 | ✗ | return; | |
| 592 | 22601 | trampoline = hook.active_trampoline(gate.get()); | |
| 593 | } | ||
| 594 | |||
| 595 | std::shared_ptr<CallGate> gate; | ||
| 596 | std::unique_lock<std::recursive_mutex> guard; | ||
| 597 | /// The live trampoline to dispatch through, or nullptr when any gate stage failed closed. | ||
| 598 | void *trampoline = nullptr; | ||
| 599 | }; | ||
| 600 | |||
| 601 | std::unique_ptr<Impl> m_impl; | ||
| 602 | |||
| 603 | /// The atomic holder of the shared call gate. @ref call pins it, so a teardown or move cannot race it. | ||
| 604 | using GateSlot = std::atomic<std::shared_ptr<CallGate>>; | ||
| 605 | |||
| 606 | /** | ||
| 607 | * @brief Never-destroyed gate slot storage (`[B-47]`), so a @ref call that races ~Hook reads a valid null | ||
| 608 | * slot. HookConcurrency.CallRacesDestructorOnRetainedStorage pins the race. | ||
| 609 | */ | ||
| 610 | alignas(GateSlot) unsigned char m_gate_storage[sizeof(GateSlot)]{}; | ||
| 611 | |||
| 612 | [[nodiscard]] GateSlot &gate_slot() noexcept; | ||
| 613 | [[nodiscard]] const GateSlot &gate_slot() const noexcept; | ||
| 614 | |||
| 615 | friend Result<Hook> mid_at(MidRequest request, MidHookFn detour); | ||
| 616 | friend Result<Hook> detail::inline_at_raw(InlineRequest request, void *detour); | ||
| 617 | friend struct detail::HookAccess; | ||
| 618 | }; | ||
| 619 | |||
| 620 | #ifdef _MSC_VER | ||
| 621 | #pragma warning(pop) | ||
| 622 | #endif | ||
| 623 | |||
| 624 | /// How @ref HookStack::retire ended. | ||
| 625 | enum class RetirementOutcome : std::uint8_t | ||
| 626 | { | ||
| 627 | /// The commit ran. The stack is empty, and the counts tell how each hook retired. | ||
| 628 | Retired, | ||
| 629 | /// The call changed nothing, so a retry can succeed later. | ||
| 630 | Busy, | ||
| 631 | /// The call changed nothing, and a retry under the same conditions fails again. | ||
| 632 | Refused | ||
| 633 | }; | ||
| 634 | |||
| 635 | /// The result of @ref HookStack::retire. | ||
| 636 | struct HookRetirement | ||
| 637 | { | ||
| 638 | RetirementOutcome outcome = RetirementOutcome::Retired; | ||
| 639 | /// Top layers that restored their target. | ||
| 640 | std::uint32_t restored = 0; | ||
| 641 | /** | ||
| 642 | * @brief Layers that retired by bypass and reach no image code. | ||
| 643 | * @details Each one vacates its position (`[B-105]`). A vacate that the coordinator refuses keeps the | ||
| 644 | * position Owned. | ||
| 645 | */ | ||
| 646 | std::uint32_t vacated = 0; | ||
| 647 | /// Hooks whose detour can stay reachable. Each one books diagnostics::LeakSubsystem::HookManager. | ||
| 648 | std::uint32_t pinned = 0; | ||
| 649 | /// Counted calls inside a detour at the last read of a Busy drain. | ||
| 650 | std::uint32_t in_flight = 0; | ||
| 651 | /// The reason for Busy or Refused, as the @ref HookStack::retire contract lists. Otherwise ErrorCode::Ok. | ||
| 652 | ErrorCode refusal = ErrorCode::Ok; | ||
| 653 | }; | ||
| 654 | |||
| 655 | /** | ||
| 656 | * @brief Move-only owner of Hook handles that tears them down newest layer first, so each layer restores | ||
| 657 | * (`[B-16]`). The hook that armed last on a target is the newest layer (`[B-110]`). | ||
| 658 | * @details A `std::vector<Hook>` does not guarantee that order, and a lower layer then retires as residue. Use | ||
| 659 | * HookStack for hooks layered on one target. Use @ref retire for a teardown that must not wait in a | ||
| 660 | * destructor. | ||
| 661 | * @note HookStack has no internal synchronization. Build it and tear it down on the setup thread. During | ||
| 662 | * @ref retire, toggle no hook of the stack on another thread. | ||
| 663 | */ | ||
| 664 | class HookStack | ||
| 665 | { | ||
| 666 | public: | ||
| 667 | /** | ||
| 668 | * @brief Constructs an empty hook stack. | ||
| 669 | * @note Setup/control-plane only. | ||
| 670 | */ | ||
| 671 | 11 | HookStack() noexcept = default; | |
| 672 | |||
| 673 | /** | ||
| 674 | * @brief Adopts the hooks of @p other without a teardown. | ||
| 675 | * @note Setup/control-plane only. | ||
| 676 | */ | ||
| 677 | 2 | HookStack(HookStack &&other) noexcept : m_hooks(std::move(other.m_hooks)) { other.m_hooks.clear(); } | |
| 678 | |||
| 679 | /** | ||
| 680 | * @brief Tears down this stack's hooks newest-first, then adopts the hooks of @p other and leaves it empty. | ||
| 681 | * @note Setup/control-plane only. | ||
| 682 | */ | ||
| 683 | 1 | HookStack &operator=(HookStack &&other) noexcept | |
| 684 | { | ||
| 685 | 1 | if (this != &other) | |
| 686 | { | ||
| 687 | 1 | teardown_newest_first(); | |
| 688 | 2 | m_hooks = std::move(other.m_hooks); | |
| 689 | 1 | other.m_hooks.clear(); | |
| 690 | } | ||
| 691 | 1 | return *this; | |
| 692 | } | ||
| 693 | |||
| 694 | HookStack(const HookStack &) = delete; | ||
| 695 | HookStack &operator=(const HookStack &) = delete; | ||
| 696 | |||
| 697 | /** | ||
| 698 | * @brief Restores every owned hook newest-first. | ||
| 699 | * @note Setup/control-plane only: destroy it after the detours and workers that use its hooks quiesce. | ||
| 700 | * @note The destructor can run from loader-lock teardown, where every ~Hook fails closed. | ||
| 701 | */ | ||
| 702 | 2304 | ~HookStack() noexcept { teardown_newest_first(); } | |
| 703 | |||
| 704 | /** | ||
| 705 | * @brief Moves @p hook into the stack. Teardown follows layer order, so the push order does not matter. | ||
| 706 | * @return The stored @ref Hook. The reference stays valid until the stack destroys that hook. | ||
| 707 | * @throws std::bad_alloc If storage growth fails. The stack then destroys @p hook, which restores its | ||
| 708 | * prologue, and keeps the stored hooks. | ||
| 709 | * @note Setup/control-plane only: the push can allocate. | ||
| 710 | */ | ||
| 711 | 6281 | Hook &push(Hook hook) | |
| 712 | { | ||
| 713 | 6281 | std::unique_ptr<Hook> stored = std::make_unique<Hook>(std::move(hook)); | |
| 714 | 12562 | m_hooks.push_back(std::move(stored)); | |
| 715 | 12562 | return *m_hooks.back(); | |
| 716 | 6281 | } | |
| 717 | |||
| 718 | /** | ||
| 719 | * @brief Installs and arms a counted inline hook on top of the stack in one process coordinator hold. | ||
| 720 | * @tparam Detour As the counted @ref inline_at states. | ||
| 721 | * @return The stored @ref Hook, valid until the stack destroys it. Otherwise the Error of the create, as | ||
| 722 | * @ref inline_at returns it, or of the arm, as @ref Hook::enable returns it. | ||
| 723 | * @details Under the hold, the call creates the hook, stores the original into | ||
| 724 | * @ref InlineInstallRequest::original with release order, stores the hook, and arms it (`[B-83]`). | ||
| 725 | * A failed create or arm leaves the stack unchanged. A failed arm destroys the hook through its | ||
| 726 | * teardown and then stores null into the original, unless the teardown pins a reachable detour. | ||
| 727 | * @note Setup/control-plane only: the install waits as @ref Hook states. | ||
| 728 | */ | ||
| 729 | template <auto Detour> | ||
| 730 | 2232 | [[nodiscard]] Result<std::reference_wrapper<Hook>> install(InlineInstallRequest request) | |
| 731 | { | ||
| 732 | 4464 | return install_counted(std::move(request), detail::counted_install_for<Detour>()); | |
| 733 | } | ||
| 734 | |||
| 735 | /** | ||
| 736 | * @brief Works as the counted @ref install, for the raw inline hook of @ref inline_at. | ||
| 737 | * @details The constraint sends a mid callback to the mid form, also from a braced request. A failed arm | ||
| 738 | * whose patch committed pins the hook with its detour reachable and keeps the original, because | ||
| 739 | * no drain covers a raw detour (HookStackTest.InstallRollbackPinsACommittedRawHook). | ||
| 740 | * @warning Quiescence of the raw detour stays caller-owned, as the raw @ref inline_at states. | ||
| 741 | * @note Setup/control-plane only: the install waits as @ref Hook states. | ||
| 742 | */ | ||
| 743 | template <class Fn> | ||
| 744 | requires(!std::is_convertible_v<Fn *, MidHookFn>) | ||
| 745 | 3 | [[nodiscard]] Result<std::reference_wrapper<Hook>> install(InlineInstallRequest request, Fn *detour) | |
| 746 | { | ||
| 747 | static_assert(sizeof(Fn *) == sizeof(void *), "function pointer must be word-sized"); | ||
| 748 | 3 | return install_raw(std::move(request), reinterpret_cast<void *>(detour)); | |
| 749 | } | ||
| 750 | |||
| 751 | /** | ||
| 752 | * @brief Works as the counted @ref install, for the mid hook of @ref mid_at. A mid hook has no original. | ||
| 753 | * @note Setup/control-plane only: the install waits as @ref Hook states. | ||
| 754 | */ | ||
| 755 | [[nodiscard]] Result<std::reference_wrapper<Hook>> install(MidRequest request, MidHookFn detour); | ||
| 756 | |||
| 757 | /** | ||
| 758 | * @brief Reserves slots for @p capacity hooks, so a batch of @ref push calls does not grow the slot list. | ||
| 759 | * @note Setup/control-plane only: the reserve can allocate. | ||
| 760 | */ | ||
| 761 | void reserve(std::size_t capacity) { m_hooks.reserve(capacity); } | ||
| 762 | |||
| 763 | /** | ||
| 764 | * @brief Returns the number of owned hooks. | ||
| 765 | * @note Callback-safe while no thread mutates or destroys this stack. | ||
| 766 | */ | ||
| 767 | 9992 | [[nodiscard]] std::size_t size() const noexcept { return m_hooks.size(); } | |
| 768 | |||
| 769 | /** | ||
| 770 | * @brief Reports whether the stack owns no hooks. | ||
| 771 | * @note Callback-safe while no thread mutates or destroys this stack. | ||
| 772 | */ | ||
| 773 | 24 | [[nodiscard]] bool empty() const noexcept { return m_hooks.empty(); } | |
| 774 | |||
| 775 | /** | ||
| 776 | * @brief Tears down every owned hook newest-first and keeps the capacity. | ||
| 777 | * @note Setup/control-plane only. | ||
| 778 | */ | ||
| 779 | 9940 | void clear() noexcept { teardown_newest_first(); } | |
| 780 | |||
| 781 | /** | ||
| 782 | * @brief Retires every owned hook newest-first, or changes nothing (`[B-106]`). | ||
| 783 | * @param deadline How long the drain can wait for calls inside the detours to leave, and how long the | ||
| 784 | * commit repeats a refused mid route proof. | ||
| 785 | * @return The outcome. Busy and Refused leave every hook, relay, route, and position as it was. | ||
| 786 | * - Busy with LoaderLockActive: the caller holds the loader lock or runs in a loader phase. | ||
| 787 | * - Busy with ErrorCode::Ok: a counted call stayed past @p deadline, a mid callback stayed past the | ||
| 788 | * closing budget, or the process coordinator was unavailable. @ref HookRetirement::in_flight | ||
| 789 | * counts the counted calls. | ||
| 790 | * - Refused with ReentrantCallRejected: the call runs inside a counted detour or a mid callback of | ||
| 791 | * this stack. | ||
| 792 | * - Refused with LayerResidueLimitReached: a fresh layer that the commit cannot restore exceeds the | ||
| 793 | * residue budget of the ~Hook contract. | ||
| 794 | * - Refused with BackendFailed: a relay cannot switch off. OutOfMemory: the proof storage failed. | ||
| 795 | * - Retired: the stack is empty. Each hook restored, vacated its position, or stayed pinned. | ||
| 796 | * @details The call switches off every inline relay, top layers included, and proves each counted detour | ||
| 797 | * idle. It then closes and drains each mid route. Only then does it tear the hooks down | ||
| 798 | * newest-first through the ~Hook contract. A raw inline detour keeps caller-owned quiescence, so a | ||
| 799 | * raw detour must not call this function on its own stack. | ||
| 800 | * | ||
| 801 | * Callers of a closed mid route wait at its gateway, so each route drains for at most the closing | ||
| 802 | * budget of ~Hook. That budget is 4e9 time-stamp counter ticks, about one to four seconds. | ||
| 803 | * | ||
| 804 | * Each idle pass stops every other thread of the process once, for the length of the pass. One | ||
| 805 | * counted drain and one trampoline proof cover all inline hooks, so their pass count does not grow | ||
| 806 | * with the hook count. A restored hook that shares its patch window with another layer adds one | ||
| 807 | * pass, and each mid hook adds two. | ||
| 808 | * @note Setup/control-plane only: the drain can wait up to @p deadline. | ||
| 809 | */ | ||
| 810 | [[nodiscard]] HookRetirement retire(std::chrono::milliseconds deadline) noexcept; | ||
| 811 | |||
| 812 | private: | ||
| 813 | [[nodiscard]] Result<std::reference_wrapper<Hook>> install_raw(InlineInstallRequest request, void *detour); | ||
| 814 | |||
| 815 | [[nodiscard]] Result<std::reference_wrapper<Hook>> | ||
| 816 | install_counted(InlineInstallRequest request, detail::CountedInstall install); | ||
| 817 | |||
| 818 | /// Destroys the owned hooks newest layer first. | ||
| 819 | 12245 | void teardown_newest_first() noexcept | |
| 820 | { | ||
| 821 | 12245 | order_by_layer(); | |
| 822 | 20601 | while (!m_hooks.empty()) | |
| 823 | { | ||
| 824 | 8356 | m_hooks.pop_back(); | |
| 825 | } | ||
| 826 | 12245 | } | |
| 827 | |||
| 828 | /// Orders the owned hooks so that the back holds the newest layer. Allocates nothing. | ||
| 829 | void order_by_layer() noexcept; | ||
| 830 | |||
| 831 | std::vector<std::unique_ptr<Hook>> m_hooks; | ||
| 832 | }; | ||
| 833 | |||
| 834 | /** | ||
| 835 | * @brief Installs a disabled raw inline hook at the request's target. Call @ref Hook::enable to arm it. | ||
| 836 | * @return The @ref Hook with the target unpatched, or an Error. This call reports every install failure, | ||
| 837 | * including a scan miss for a deferred target. It first waits out the transient causes that @ref Hook | ||
| 838 | * lists. | ||
| 839 | * @details Prefer the counted form, which owns quiescence. Use this form for a variadic, `__vectorcall`, or | ||
| 840 | * other signature that the counted thunk does not forward. | ||
| 841 | * @warning The detour must not throw. The patched target calls it directly, so an exception that escapes | ||
| 842 | * terminates the host. | ||
| 843 | * @warning Unlike @ref mid_at and the counted form, quiescence before teardown is caller-owned: DMK cannot wait | ||
| 844 | * for a thread inside the detour. Prove that no thread can execute the detour before the handle dies. | ||
| 845 | * @warning For a Logic DLL detour, stop and join every thread that can reach the target. Then destroy the | ||
| 846 | * handle. Then unmap the provider. DMK cannot detect another order. | ||
| 847 | * @note Setup/control-plane only: the install allocates the trampoline and validates the target. | ||
| 848 | */ | ||
| 849 | 7282 | template <class Fn> [[nodiscard]] Result<Hook> inline_at(InlineRequest request, Fn *detour) | |
| 850 | { | ||
| 851 | static_assert(sizeof(Fn *) == sizeof(void *), "function pointer must be word-sized"); | ||
| 852 | 7282 | return detail::inline_at_raw(std::move(request), reinterpret_cast<void *>(detour)); | |
| 853 | } | ||
| 854 | |||
| 855 | /** | ||
| 856 | * @brief Installs a disabled counted inline hook. DMK counts each call into @p Detour and drains them at | ||
| 857 | * teardown (`[B-106]`). Call @ref Hook::enable to arm it. | ||
| 858 | * @tparam Detour A noexcept, non-variadic function whose parameters and result are references or trivially | ||
| 859 | * copyable. An optional first @ref CallerFrame parameter, by value, receives the game caller's return. | ||
| 860 | * The other parameters match the hooked function. | ||
| 861 | * @return The @ref Hook with the target unpatched, or an Error. InvalidDetourFunction means that the compiled | ||
| 862 | * thunk has a shape that the drain proof cannot cover, and the log names the cause. The call first | ||
| 863 | * waits out the transient causes that @ref Hook lists. | ||
| 864 | * @details A DMK thunk in the consumer image sits in the relay slot and calls @p Detour. @ref HookStack::retire | ||
| 865 | * and ~Hook switch the relay off and wait until no thread runs the thunk or the detour. A Retired | ||
| 866 | * outcome with no pinned hook therefore leaves no counted call in the image. An unwind or nonlocal exit | ||
| 867 | * out of the detour leaves its call counted, so every later retirement of that detour stays Busy. | ||
| 868 | * | ||
| 869 | * Inside @p Detour, `_ReturnAddress()` and `__builtin_return_address(0)` name the thunk, and | ||
| 870 | * `_AddressOfReturnAddress()` names the return slot of the thunk. A detour that identifies its caller | ||
| 871 | * takes @ref CallerFrame first. | ||
| 872 | * @note Hooks that share one detour share its counters, so each drain also waits for the calls of the others. | ||
| 873 | * @note For a target that other DMK copies can hook, use @ref HookStack::install, which arms in the hold of the | ||
| 874 | * create. | ||
| 875 | * @note Setup/control-plane only: the install allocates the trampoline and decodes the thunk. | ||
| 876 | */ | ||
| 877 | 2154 | template <auto Detour> [[nodiscard]] Result<Hook> inline_at(InlineRequest request) | |
| 878 | { | ||
| 879 | 6462 | return detail::inline_at_counted(std::move(request), detail::counted_install_for<Detour>()); | |
| 880 | } | ||
| 881 | |||
| 882 | /** | ||
| 883 | * @brief Installs a disabled mid-function hook at the request's target. Call @ref Hook::enable to arm it. | ||
| 884 | * @return The @ref Hook with the target unpatched, or an Error. @ref ErrorCode::MidHookCapacityExhausted means | ||
| 885 | * that every mid-hook adapter is in use and the call patched nothing. The call first waits out the | ||
| 886 | * transient causes that @ref Hook lists. | ||
| 887 | * @details DMK owns callback exception containment and rundown. @ref Hook::~Hook owns the retention rules. | ||
| 888 | * @note Each hook holds one adapter from a fixed pool until clean teardown. Displaced instructions and their | ||
| 889 | * callees own the route until an ordinary exit completes, so dormant fibers and unresolved exception | ||
| 890 | * continuations retain it at teardown. Exception unwind or a nonlocal exit can abandon ownership and | ||
| 891 | * cause permanent retention. | ||
| 892 | * @note After a displaced call, an instruction with RSP as an explicit destination is unsupported, except ADD | ||
| 893 | * of a nonnegative immediate. Creation then fails before publication with @ref ErrorCode::BackendFailed. | ||
| 894 | * @warning Before teardown, quiesce saved contexts outside the counted displaced execution. These contexts | ||
| 895 | * include entry and exit gaps, copied contexts, and later reuse of a captured instruction pointer. If | ||
| 896 | * quiescence is unproved, keep the Hook and its code providers alive. | ||
| 897 | * @warning A retained route does not authorize provider unload. Every admitted callback and continuation needs | ||
| 898 | * its code providers until it exits. @ref Hook::release also keeps callback dispatch active. | ||
| 899 | * @note Setup/control-plane only: the install claims an adapter and builds the routed chain. | ||
| 900 | */ | ||
| 901 | [[nodiscard]] Result<Hook> mid_at(MidRequest request, MidHookFn detour); | ||
| 902 | |||
| 903 | struct InstallOutcome; | ||
| 904 | |||
| 905 | /// Internal tag that carries the function-to-void* cast of an inline @ref HookSpec. | ||
| 906 | struct InlineDetour | ||
| 907 | { | ||
| 908 | void *fn = nullptr; | ||
| 909 | }; | ||
| 910 | |||
| 911 | /** @brief One row of an @ref install_all table. The @ref inline_hook and @ref mid_hook factories build it. */ | ||
| 912 | class HookSpec | ||
| 913 | { | ||
| 914 | public: | ||
| 915 | /** | ||
| 916 | * @brief Builds an inline-hook row. See @ref Severity and @ref Options for the row policy. | ||
| 917 | * @note Setup/control-plane only: the row can allocate through @p name and @p target. | ||
| 918 | */ | ||
| 919 | template <class Fn> | ||
| 920 | 13 | [[nodiscard]] static HookSpec inline_hook( | |
| 921 | std::string name, | ||
| 922 | scan::OwnedScanRequest target, | ||
| 923 | Fn *detour, | ||
| 924 | Severity severity = Severity::Mandatory, | ||
| 925 | Options options = {} | ||
| 926 | ) | ||
| 927 | { | ||
| 928 | static_assert(sizeof(Fn *) == sizeof(void *), "function pointer must be word-sized"); | ||
| 929 | return HookSpec{ | ||
| 930 | 13 | std::move(name), | |
| 931 | 13 | std::move(target), | |
| 932 | InlineDetour{reinterpret_cast<void *>(detour)}, | ||
| 933 | severity, | ||
| 934 | options | ||
| 935 | 26 | }; | |
| 936 | } | ||
| 937 | |||
| 938 | /** | ||
| 939 | * @brief Builds a mid-hook row. See @ref Severity and @ref Options for the row policy. | ||
| 940 | * @note Setup/control-plane only: the row can allocate through @p name and @p target. | ||
| 941 | */ | ||
| 942 | [[nodiscard]] static HookSpec mid_hook( | ||
| 943 | std::string name, | ||
| 944 | scan::OwnedScanRequest target, | ||
| 945 | MidHookFn detour, | ||
| 946 | Severity severity = Severity::Mandatory, | ||
| 947 | Options options = {} | ||
| 948 | ) | ||
| 949 | { | ||
| 950 | return HookSpec{std::move(name), std::move(target), detour, severity, options}; | ||
| 951 | } | ||
| 952 | |||
| 953 | /// Returns the row name forwarded to the eventual install request. | ||
| 954 | [[nodiscard]] std::string_view name() const noexcept { return m_name; } | ||
| 955 | /// Returns whether this row is mandatory or best-effort. | ||
| 956 | [[nodiscard]] Severity severity() const noexcept { return m_severity; } | ||
| 957 | /// Returns the per-row install policy applied by @ref install_all. | ||
| 958 | [[nodiscard]] const Options &options() const noexcept { return m_options; } | ||
| 959 | |||
| 960 | private: | ||
| 961 | 13 | HookSpec( | |
| 962 | std::string name, | ||
| 963 | scan::OwnedScanRequest target, | ||
| 964 | std::variant<InlineDetour, MidHookFn> detour, | ||
| 965 | Severity severity, | ||
| 966 | Options options | ||
| 967 | ) noexcept | ||
| 968 | 39 | : m_name(std::move(name)), m_target(std::move(target)), m_detour(std::move(detour)), | |
| 969 | 13 | m_severity(severity), m_options(options) | |
| 970 | { | ||
| 971 | 13 | } | |
| 972 | |||
| 973 | std::string m_name; | ||
| 974 | scan::OwnedScanRequest m_target; | ||
| 975 | /// The active alternative selects an inline or a mid hook. | ||
| 976 | std::variant<InlineDetour, MidHookFn> m_detour; | ||
| 977 | Severity m_severity; | ||
| 978 | /// Per-row install policy applied verbatim by @ref install_all. | ||
| 979 | Options m_options; | ||
| 980 | |||
| 981 | friend Result<std::vector<InstallOutcome>> install_all(std::span<const HookSpec> table) noexcept; | ||
| 982 | }; | ||
| 983 | |||
| 984 | /** | ||
| 985 | * @brief Per-row result of @ref install_all, in table order. | ||
| 986 | * @warning Move the successful hooks into a @ref HookStack in table order. @ref HookStack states why. | ||
| 987 | */ | ||
| 988 | struct InstallOutcome | ||
| 989 | { | ||
| 990 | std::string name; | ||
| 991 | Severity severity; | ||
| 992 | /// The installed Hook, or an Error such as NoMatch when @ref install_all skipped the row. | ||
| 993 | Result<Hook> hook; | ||
| 994 | }; | ||
| 995 | |||
| 996 | /** | ||
| 997 | * @brief Installs a table of disabled hooks and returns one outcome per row. | ||
| 998 | * @return The per-row outcomes. LoaderLockActive fails before any row. The first @ref Severity::Mandatory miss | ||
| 999 | * fails the outer Result. An allocation failure that no row reports fails it with OutOfMemory, and any | ||
| 1000 | * other escaped exception fails it with UnknownError. | ||
| 1001 | * @details Every row stays disabled until the call returns, so a rollback has no live hook to disarm. Every | ||
| 1002 | * outer failure removes the installed rows newest-first. To arm a row, call @ref Hook::enable on it | ||
| 1003 | * after you take ownership of the outcomes. | ||
| 1004 | * @warning See the @ref InstallOutcome teardown-order warning. | ||
| 1005 | * @note Setup/control-plane only: the batch resolves scans and allocates per row. | ||
| 1006 | */ | ||
| 1007 | [[nodiscard]] Result<std::vector<InstallOutcome>> install_all(std::span<const HookSpec> table) noexcept; | ||
| 1008 | |||
| 1009 | /** | ||
| 1010 | * @brief Reports whether a hook from this DMK instance owns @p target, or an install in progress reserved it. | ||
| 1011 | * @details The query reads this instance's ledger only, so it does not see foreign hooks or other DMK copies. | ||
| 1012 | * To also refuse foreign hooks, set @ref Options::fail_if_already_hooked on the install. | ||
| 1013 | * @note Setup/control-plane only: the query takes the ledger mutex that installs and teardowns contend on. | ||
| 1014 | */ | ||
| 1015 | [[nodiscard]] bool is_target_hooked(Address target) noexcept; | ||
| 1016 | |||
| 1017 | /** @brief Policy for @ref vmt_for and @ref VmtHook::apply_to. */ | ||
| 1018 | struct VmtOptions | ||
| 1019 | { | ||
| 1020 | /** | ||
| 1021 | * @brief Refuses to clone or apply onto an object whose vptr already points at a clone from this kit. | ||
| 1022 | * @details A clone of a clone treats the hooked methods of the first clone as its originals. | ||
| 1023 | */ | ||
| 1024 | bool fail_if_already_hooked = false; | ||
| 1025 | |||
| 1026 | /** | ||
| 1027 | * @brief Decodes the first byte of the original vtable slot and refuses a slot that is not a function body. | ||
| 1028 | * @details The pre-flight refuses a 0xCC or 0xCD breakpoint pad, a 0x00 byte, and a bare RET (0xC2 or | ||
| 1029 | * 0xC3). It also refuses a same-module `jmp rel8/rel32` stub such as an incremental-link ILT | ||
| 1030 | * entry, and a jump whose slot or target lies in no module. MSVC adjustor thunks pass. Known false | ||
| 1031 | * positives: a /INCREMENTAL consumer routes every function through an ILT stub, and an empty | ||
| 1032 | * virtual body can compile to a bare RET. | ||
| 1033 | */ | ||
| 1034 | bool fail_on_non_function_pointer = false; | ||
| 1035 | }; | ||
| 1036 | |||
| 1037 | class VmtHook; | ||
| 1038 | |||
| 1039 | /** | ||
| 1040 | * @brief Clones the vtable of @p object, swaps @p object onto the clone, and returns the @ref VmtHook. | ||
| 1041 | * @return The @ref VmtHook, or LoaderLockActive, InvalidArg, InvalidObject, HookAlreadyExists, BackendFailed, | ||
| 1042 | * OutOfMemory, SystemCallFailed, or UnknownError. InvalidObject covers an unreadable vtable or RTTI | ||
| 1043 | * header prefix. It also covers an unreadable, non-writable, unaligned, unmapped, reprotected, or | ||
| 1044 | * displaced object word. | ||
| 1045 | * @warning Clone during setup or a host-quiesced window. | ||
| 1046 | * @note Setup/control-plane only: the clone allocates and mutates the vptr of @p object. | ||
| 1047 | */ | ||
| 1048 | [[nodiscard]] Result<VmtHook> vmt_for(std::string name, void *object, VmtOptions options = {}); | ||
| 1049 | |||
| 1050 | /** | ||
| 1051 | * @brief Move-only RAII handle for a cloned vtable applied to one or more live objects. | ||
| 1052 | * @details A @ref hook_method call affects every object on the clone. | ||
| 1053 | * @warning Quiesce virtual dispatch across create, apply, and remove. Keep every applied object alive through | ||
| 1054 | * removal. Guarded vptr access contains faults but is not an ownership protocol. | ||
| 1055 | * @note An object gate serializes the vptr transitions of @ref vmt_for, @ref apply_to, @ref remove_from, and | ||
| 1056 | * teardown, so each duplicate check and swap is one ordered operation. @ref original reads its slot under | ||
| 1057 | * a shared lock and never sees a torn mutation. | ||
| 1058 | */ | ||
| 1059 | class VmtHook | ||
| 1060 | { | ||
| 1061 | public: | ||
| 1062 | VmtHook(VmtHook &&other) noexcept; | ||
| 1063 | VmtHook &operator=(VmtHook &&other) noexcept; | ||
| 1064 | VmtHook(const VmtHook &) = delete; | ||
| 1065 | VmtHook &operator=(const VmtHook &) = delete; | ||
| 1066 | |||
| 1067 | /** | ||
| 1068 | * @brief Restores each applied object's original vptr, unless the handle is released, moved, or outranked. | ||
| 1069 | * @details Each object follows the @ref remove_from rules. An unresolved dependency then retains the clone. | ||
| 1070 | * @ref diagnostics::LeakSubsystem::HookManager counts it, and the log names the hook. Destroy VMT | ||
| 1071 | * hooks newest-first to get the original table back. | ||
| 1072 | * @note Setup/control-plane only: quiesce virtual dispatch first. | ||
| 1073 | */ | ||
| 1074 | ~VmtHook() noexcept; | ||
| 1075 | |||
| 1076 | /// True while this handle owns a live cloned vtable (false for a moved-from or released handle). | ||
| 1077 | [[nodiscard]] explicit operator bool() const noexcept; | ||
| 1078 | |||
| 1079 | /// The hook's registered name (empty for a moved-from or released handle). | ||
| 1080 | [[nodiscard]] std::string_view name() const noexcept; | ||
| 1081 | |||
| 1082 | /** | ||
| 1083 | * @brief Swaps @p object onto the cloned vtable. | ||
| 1084 | * @return Success, or LoaderLockActive, InvalidHookState, InvalidObject, HookAlreadyExists, OutOfMemory, or | ||
| 1085 | * UnknownError. InvalidObject covers an unreadable, non-writable, or unaligned object word, and a | ||
| 1086 | * protection change, displacement, or unmap before publication. Under any @p options, | ||
| 1087 | * HookAlreadyExists also means that this handle cannot name the vptr that it displaces. That covers | ||
| 1088 | * an object that carries this clone but that this handle never applied, or that moved off its | ||
| 1089 | * recorded vptr. A repeat apply of a tracked, published object succeeds as a no-op. | ||
| 1090 | * @warning Apply only while @p object is host-quiesced. The atomic vptr swap does not synchronize dispatch. | ||
| 1091 | * @note Setup/control-plane only: the apply mutates the vptr under the exclusive object gate. | ||
| 1092 | */ | ||
| 1093 | [[nodiscard]] Result<void> apply_to(void *object, VmtOptions options = {}); | ||
| 1094 | |||
| 1095 | /** | ||
| 1096 | * @brief Restores the original vptr on one applied object. | ||
| 1097 | * @return Success, or LoaderLockActive, InvalidObject for a null @p object, InvalidHookState for a | ||
| 1098 | * disengaged handle, or UnknownError when the exclusive object gate is unavailable. | ||
| 1099 | * @details Success does not mean that a restore happened. An untracked object is a no-op. A tracked object | ||
| 1100 | * releases its binding only once its word reads as the recorded original. A writable object on | ||
| 1101 | * this clone swaps back unless a protection change or unmap defeats the swap. An object already at | ||
| 1102 | * the original needs no write and releases its binding, even when its word is not writable. Any | ||
| 1103 | * other or unreadable value stays unchanged and keeps the dependency, because a successor can | ||
| 1104 | * still restore an object to this clone. | ||
| 1105 | * @warning Quiesce @p object before the restore. Fault containment does not drain in-flight dispatch. | ||
| 1106 | * @note Setup/control-plane only: the restore mutates the vptr under the exclusive object gate. | ||
| 1107 | */ | ||
| 1108 | [[nodiscard]] Result<void> remove_from(void *object); | ||
| 1109 | |||
| 1110 | /** | ||
| 1111 | * @brief Points the cloned vtable slot at @p index to @p detour. | ||
| 1112 | * @param index The zero-based index among virtual methods, in declaration order. The ABI vtable header | ||
| 1113 | * (Itanium offset-to-top and RTTI, or the MSVC RTTI locator) is not part of the index. | ||
| 1114 | * @param detour Its ABI must match the method's true signature, with the object pointer as the first | ||
| 1115 | * integer argument (`this` in rcx). DMK cannot validate the signature, and a mismatch silently | ||
| 1116 | * corrupts the ABI. | ||
| 1117 | * @return Success, or LoaderLockActive, InvalidHookState for a disengaged handle, InvalidArg for a null | ||
| 1118 | * @p detour or an out-of-range @p index, MethodAlreadyHooked for an occupied index, BackendFailed, | ||
| 1119 | * or OutOfMemory. | ||
| 1120 | * @warning The detour must not throw (`[B-84]`). The slot calls it directly, so no DMK frame contains an | ||
| 1121 | * exception, and an exception that escapes terminates the host. | ||
| 1122 | * @note Setup/control-plane only: the call mutates the clone under the exclusive write lock. Install all | ||
| 1123 | * method hooks during setup. Do not call it from a detour while another thread reads this handle. | ||
| 1124 | */ | ||
| 1125 | 244 | template <detail::FunctionPointer Fn> [[nodiscard]] Result<void> hook_method(std::size_t index, Fn detour) | |
| 1126 | { | ||
| 1127 | static_assert(sizeof(Fn) == sizeof(void *), "function pointer must be word-sized"); | ||
| 1128 | 244 | return hook_method_raw(index, reinterpret_cast<void *>(detour)); | |
| 1129 | } | ||
| 1130 | |||
| 1131 | /** | ||
| 1132 | * @brief Returns the pre-hook function of the method at @p index, typed as Fn. | ||
| 1133 | * @tparam Fn The full function-pointer type, with the object pointer as the first parameter. | ||
| 1134 | * @return The original, or nullptr for an unhooked @p index or a disengaged handle. The pointer stays valid | ||
| 1135 | * for the hook's lifetime, and a call through it takes no lock. Keep the hook alive for the call. | ||
| 1136 | * @note Callback-safe: the read is a shared-lock snapshot copy. | ||
| 1137 | */ | ||
| 1138 | 241 | template <detail::FunctionPointer Fn> [[nodiscard]] Fn original(std::size_t index) const noexcept | |
| 1139 | { | ||
| 1140 | 241 | return reinterpret_cast<Fn>(method_original_address(index)); | |
| 1141 | } | ||
| 1142 | |||
| 1143 | /** | ||
| 1144 | * @brief Restores the cloned vtable slot at @p index to the original method. | ||
| 1145 | * @return Success, or LoaderLockActive, InvalidHookState for a disengaged handle, or MethodNotFound when | ||
| 1146 | * @p index has no hook on this handle. | ||
| 1147 | * @note Setup/control-plane only: the restore is a bare pointer write under the exclusive write lock. It | ||
| 1148 | * does not protect an in-flight dispatch through the slot. Quiesce the method first. | ||
| 1149 | */ | ||
| 1150 | [[nodiscard]] Result<void> remove_method(std::size_t index); | ||
| 1151 | |||
| 1152 | /** | ||
| 1153 | * @brief Retains the clone for the process lifetime without a vptr restore, and disengages the handle. | ||
| 1154 | * @note @ref diagnostics::total_intentional_leaks counts the release. The clone stays recorded, so | ||
| 1155 | * @ref VmtOptions::fail_if_already_hooked still recognizes it. | ||
| 1156 | * @note Setup/control-plane only. | ||
| 1157 | * @warning Applied objects keep the clone. Each method detour and its callees must stay mapped until | ||
| 1158 | * process exit. DMK holds a module reference on its own module, not on the detour provider, so a | ||
| 1159 | * Logic DLL detour needs that DLL to stay loaded. | ||
| 1160 | */ | ||
| 1161 | void release() noexcept; | ||
| 1162 | |||
| 1163 | private: | ||
| 1164 | struct Impl; | ||
| 1165 | explicit VmtHook(std::unique_ptr<Impl> impl) noexcept; | ||
| 1166 | |||
| 1167 | /// The non-template method-install primitive behind @ref hook_method. | ||
| 1168 | [[nodiscard]] Result<void> hook_method_raw(std::size_t index, void *detour); | ||
| 1169 | |||
| 1170 | /// Copies the original slot for @p index under the shared lock, or returns nullptr (see @ref original). | ||
| 1171 | [[nodiscard]] void *method_original_address(std::size_t index) const noexcept; | ||
| 1172 | |||
| 1173 | std::unique_ptr<Impl> m_impl; | ||
| 1174 | |||
| 1175 | friend Result<VmtHook> vmt_for(std::string name, void *object, VmtOptions options); | ||
| 1176 | }; | ||
| 1177 | } // namespace hook | ||
| 1178 | } // namespace DetourModKit | ||
| 1179 | |||
| 1180 | #endif // DETOURMODKIT_HOOK_HPP | ||
| 1181 |