./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
532 / 546
97.4%
Branches:
128 / 242
52.9%
Functions:
232 / 235
98.7%

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
3/4
float DetourModKit::hook::XmmView::lane<float>(unsigned long long) const:
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 2 times.
unsigned long long DetourModKit::hook::XmmView::lane<unsigned long long>(unsigned long long) const:
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
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
2/2
✓ Branch 2 → 3 taken 2233 times.
✓ Branch 2 → 4 taken 2001 times.
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
5/8
int DetourModKit::hook::Hook::call<int, (anonymous namespace)::CopyOnlyInt>((anonymous namespace)::CopyOnlyInt) const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
int DetourModKit::hook::Hook::call<int, std::unique_ptr<int, std::default_delete<int> > >(std::unique_ptr<int, std::default_delete<int> >) const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
int DetourModKit::hook::Hook::call<int, int>(int) const:
✓ Branch 3 → 4 taken 97517 times.
✓ Branch 3 → 5 taken 16 times.
int DetourModKit::hook::Hook::call<int, int, int>(int, int) const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
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
4/8
int DetourModKit::hook::Hook::call<int, (anonymous namespace)::CopyOnlyInt>((anonymous namespace)::CopyOnlyInt) const:
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
int DetourModKit::hook::Hook::call<int, std::unique_ptr<int, std::default_delete<int> > >(std::unique_ptr<int, std::default_delete<int> >) const:
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 14 not taken.
int DetourModKit::hook::Hook::call<int, int>(int) const:
✓ Branch 6 → 7 taken 16 times.
✗ Branch 6 → 11 not taken.
int DetourModKit::hook::Hook::call<int, int, int>(int, int) const:
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 12 not taken.
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
10/12
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int>() const:
✓ Branch 3 → 4 taken 14 times.
✓ Branch 3 → 7 taken 10 times.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, (anonymous namespace)::CopyOnlyInt>((anonymous namespace)::CopyOnlyInt) const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 1 time.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, std::unique_ptr<int, std::default_delete<int> > >(std::unique_ptr<int, std::default_delete<int> >) const:
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 1 time.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, int>(int) const:
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 1 time.
std::expected<unsigned long, DetourModKit::Error> DetourModKit::hook::Hook::try_call<unsigned long, unsigned long, _XINPUT_STATE*>(unsigned long, _XINPUT_STATE*) const:
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 1 time.
std::expected<void, DetourModKit::Error> DetourModKit::hook::Hook::try_call<void, int>(int) const:
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 1 time.
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/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 13 not taken.
1 reinterpret_cast<void (*)(Args...)>(dispatch.trampoline)(forward_call_argument<Args>(args)...);
468 1 return {};
469 }
470 else
471 {
472
5/10
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int>() const:
✓ Branch 7 → 8 taken 10 times.
✗ Branch 7 → 13 not taken.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, (anonymous namespace)::CopyOnlyInt>((anonymous namespace)::CopyOnlyInt) const:
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 14 not taken.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, std::unique_ptr<int, std::default_delete<int> > >(std::unique_ptr<int, std::default_delete<int> >) const:
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 16 not taken.
std::expected<int, DetourModKit::Error> DetourModKit::hook::Hook::try_call<int, int>(int) const:
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 14 not taken.
std::expected<unsigned long, DetourModKit::Error> DetourModKit::hook::Hook::try_call<unsigned long, unsigned long, _XINPUT_STATE*>(unsigned long, _XINPUT_STATE*) const:
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 15 not taken.
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
2/2
✓ Branch 8 → 9 taken 74968 times.
✓ Branch 8 → 10 taken 22601 times.
97569 if (!gate)
588 74968 return;
589 22601 guard = hook.acquire_call_lock(gate.get());
590
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 22601 times.
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/2
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
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
1/2
✓ Branch 4 → 5 taken 6281 times.
✗ Branch 4 → 15 not taken.
6281 std::unique_ptr<Hook> stored = std::make_unique<Hook>(std::move(hook));
714
1/2
✓ Branch 7 → 8 taken 6281 times.
✗ Branch 7 → 13 not taken.
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
6/14
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::install_detour>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::counted_callback>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 2000 times.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::kept_slot_detour>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 107 times.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::pinned_arm_detour>(DetourModKit::hook::InlineInstallRequest):
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::apply_damage_detour>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 21 times.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::stack_install_detour>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<&(anonymous namespace)::kept_slot_peer_detour>(DetourModKit::hook::InlineInstallRequest):
✓ Branch 6 → 7 taken 100 times.
✗ Branch 6 → 11 not taken.
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
1/4
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<int (int) noexcept>(DetourModKit::hook::InlineInstallRequest, int (*)(int) noexcept):
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 10 not taken.
std::expected<std::reference_wrapper<DetourModKit::hook::Hook>, DetourModKit::Error> DetourModKit::hook::HookStack::install<int (int)>(DetourModKit::hook::InlineInstallRequest, int (*)(int)):
✓ Branch 5 → 6 taken 3 times.
✗ Branch 5 → 10 not taken.
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
2/2
✓ Branch 6 → 4 taken 8356 times.
✓ Branch 6 → 7 taken 12245 times.
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
11/22
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int ((anonymous namespace)::CopyOnlyInt) noexcept>(DetourModKit::hook::InlineRequest, int (*)((anonymous namespace)::CopyOnlyInt) noexcept):
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int (std::unique_ptr<int, std::default_delete<int> >) noexcept>(DetourModKit::hook::InlineRequest, int (*)(std::unique_ptr<int, std::default_delete<int> >) noexcept):
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int (int) noexcept>(DetourModKit::hook::InlineRequest, int (*)(int) noexcept):
✓ Branch 5 → 6 taken 11 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int (int, int) noexcept>(DetourModKit::hook::InlineRequest, int (*)(int, int) noexcept):
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int () noexcept>(DetourModKit::hook::InlineRequest, int (*)() noexcept):
✓ Branch 5 → 6 taken 1121 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<unsigned long (unsigned long, _XINPUT_STATE*) noexcept>(DetourModKit::hook::InlineRequest, unsigned long (*)(unsigned long, _XINPUT_STATE*) noexcept):
✓ Branch 5 → 6 taken 8 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int (int)>(DetourModKit::hook::InlineRequest, int (*)(int)):
✓ Branch 5 → 6 taken 4549 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int (int, int)>(DetourModKit::hook::InlineRequest, int (*)(int, int)):
✓ Branch 5 → 6 taken 1405 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<int ()>(DetourModKit::hook::InlineRequest, int (*)()):
✓ Branch 5 → 6 taken 7 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<unsigned long (unsigned long, _XINPUT_STATE*)>(DetourModKit::hook::InlineRequest, unsigned long (*)(unsigned long, _XINPUT_STATE*)):
✓ Branch 5 → 6 taken 25 times.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<void ()>(DetourModKit::hook::InlineRequest, void (*)()):
✓ Branch 5 → 6 taken 151 times.
✗ Branch 5 → 10 not taken.
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
72/144
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::detour_leaf>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::void_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::wide_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::blend_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::relay_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::split_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::lvalue_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::rvalue_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::scalar_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::shrink_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::unwind_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::vector_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::prologue_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::counted_callback>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2000 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::entry_gap_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::frame_ten_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::large_pod_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::mixed_ten_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::small_pod_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::frame_slot_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<0ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 5 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<100ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<101ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<102ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<10ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<11ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<12ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<13ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<14ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<15ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<16ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<17ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<18ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<19ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<1ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<20ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<21ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<22ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<23ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<24ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<25ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<26ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<27ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<28ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<2ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<3ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<4ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<5ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<6ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<7ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<8ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::pass_count_detour<9ull>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::forward_ten_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::thunk_range_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::frame_return_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::large_result_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<22, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<23, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<25, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<26, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<28, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<29, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int (anonymous namespace)::counted_chain_detour<32, 1000>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(long long (anonymous namespace)::forward_eight_detour<1>(long long, long long, long long, long long, long long, long long, long long, long long))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(long long (anonymous namespace)::forward_eight_detour<2>(long long, long long, long long, long long, long long, long long, long long, long long))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(anonymous namespace)::compute_damage_counted_detour>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 57 times.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<1>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<24>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<27>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<2>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<30>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
std::expected<DetourModKit::hook::Hook, DetourModKit::Error> DetourModKit::hook::inline_at<&(int dmk_test::hook_fixture::parked_detour<31>(int))>(DetourModKit::hook::InlineRequest):
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 11 not taken.
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