./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
18 / 18
100.0%
Branches:
-
-%
Functions:
6 / 6
100.0%

src/internal/hook_backend.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_HOOK_BACKEND_HPP
2 #define DETOURMODKIT_INTERNAL_HOOK_BACKEND_HPP
3
4 /**
5 * @file internal/hook_backend.hpp
6 * @brief Backend-coupled pimpl bodies and the per-instance allocator for the hook subsystem.
7 * @details hook.hpp holds each backend-owning handle's nested Impl behind std::unique_ptr. This header completes those
8 * bodies with safetyhook.hpp visible. scripts/check_header_hygiene.py owns and gates BACKEND_SOURCE_ISLANDS.
9 * docs/design/hooking.md ("Backend confinement") describes the islands. hook.hpp owns the rule that
10 * hook::MidContext stays incomplete, so no definition belongs here.
11 */
12
13 #include "DetourModKit/diagnostics.hpp"
14 #include "DetourModKit/hook.hpp"
15
16 #include "internal/srw_shared_mutex.hpp"
17
18 #include <safetyhook.hpp>
19
20 #include <array>
21 #include <atomic>
22 #include <chrono>
23 #include <cstddef>
24 #include <cstdint>
25 #include <expected>
26 #include <functional>
27 #include <memory>
28 #include <mutex>
29 #include <optional>
30 #include <span>
31 #include <string>
32 #include <string_view>
33 #include <unordered_map>
34 #include <utility>
35 #include <variant>
36 #include <vector>
37
38 namespace DetourModKit
39 {
40 namespace hook
41 {
42 /**
43 * @enum HookState
44 * @brief Internal enable/disable state machine for a managed Hook (never public).
45 * @details The intermediate Enabling / Disabling states let an atomic compare-exchange publish a terminal
46 * state only after the backend toggle returns, so a concurrent reader never observes a speculative
47 * Active/Disabled while the backend call is mid-flight.
48 */
49 enum class HookState : std::uint8_t
50 {
51 Active,
52 Disabled,
53 Enabling,
54 Disabling
55 };
56
57 /**
58 * @brief The refcounted call gate that outlives the backend it fronts, so a late @ref Hook::call is safe.
59 * @details A caller pins the gate before locking, and teardown nulls @ref callable under the same mutex before
60 * freeing the trampoline. The recursive mutex permits a detour to re-enter @ref Hook::call. During
61 * loader-lock teardown the backend and its module reference are intentionally leaked, so callable
62 * remains valid. Every access to callable holds the mutex.
63 */
64 struct Hook::CallGate
65 {
66 std::recursive_mutex mutex;
67 void *callable{nullptr};
68 };
69
70 /// The return-site lists that the calls of a counted thunk outside its counted window can fill.
71 inline constexpr std::size_t COUNTED_HELPER_SITE_LISTS = 4;
72
73 /**
74 * @brief The thunk of a counted hook: its counters and the code that its drain proof scans (`[B-106]`).
75 * @details The range is the thunk's unchained RUNTIME_FUNCTION. A helper site is the return into the thunk
76 * of a call that runs before the enter count or after the exit count. An unoptimized consumer build
77 * can emit such a call, and only a stack read finds a thread inside it.
78 */
79 struct CountedThunk
80 {
81 detail::CountedBlock *block{nullptr};
82 safetyhook::AddressRange range{};
83 std::array<safetyhook::ReturnSites, COUNTED_HELPER_SITE_LISTS> helper_sites{};
84 std::size_t helper_site_lists{0};
85 };
86
87 /**
88 * @brief The complete backend state behind a @ref Hook handle.
89 * @details Holds one backend hook, its state, identity, target, and ledger token. The call mutex lives in the
90 * separate refcounted @ref Hook::CallGate. Atomic state makes this type non-movable.
91 */
92 struct Hook::Impl
93 {
94 std::variant<safetyhook::InlineHook, safetyhook::MidHook> backend;
95 std::atomic<HookState> status;
96 std::string name;
97 std::uintptr_t target{0};
98 std::uint64_t ledger_id{0};
99 bool is_inline{false};
100 // Index into the mid-hook adapter pool, or SIZE_MAX for an inline hook. A bare index keeps the pool out of
101 // this header. A pinned Impl keeps its slot claimed, so a reachable stub never reaches reused storage.
102 std::size_t mid_slot{static_cast<std::size_t>(-1)};
103 // Counted reference on the module that holds the detour and adapter code, taken while the module is mapped
104 // and before publication. It protects only image code that a retained chain can reach, so ~Hook releases
105 // it once no path reaches that code (`[B-44]`). A void* keeps HMODULE out of this header.
106 void *self_ref{nullptr};
107 // True while the switch word sends every caller past the detour and the target patch stays in the chain.
108 // Written under the call gate mutex. A claimed backend starts bypassed.
109 bool bypassed{false};
110 // The layer order of this kit: the ledger id, and a fresh ledger stamp after each arm that made the hook
111 // the newest layer. HookStack tears its hooks down by descending stamp (`[B-110]`).
112 std::uint64_t layer_stamp{0};
113 // The thunk of a counted inline hook, or empty for a raw inline or a mid hook.
114 std::optional<CountedThunk> counted;
115
116 #if defined(DMK_ENABLE_TEST_SEAMS)
117 ~Impl() noexcept;
118 #endif
119
120 11586 Impl(
121 safetyhook::InlineHook hook,
122 std::string hook_name,
123 std::uintptr_t hook_target,
124 std::uint64_t ledger,
125 HookState initial_state
126 )
127 34758 : backend(std::move(hook)), status(initial_state), name(std::move(hook_name)), target(hook_target),
128 11586 ledger_id(ledger), is_inline(true), layer_stamp(ledger)
129 {
130 11586 }
131
132 5257 Impl(
133 safetyhook::MidHook hook,
134 std::string hook_name,
135 std::uintptr_t hook_target,
136 std::uint64_t ledger,
137 HookState initial_state
138 )
139 21028 : backend(std::move(hook)), status(initial_state), name(std::move(hook_name)), target(hook_target),
140 5257 ledger_id(ledger), is_inline(false), layer_stamp(ledger)
141 {
142 5257 }
143 };
144
145 /**
146 * @brief The complete backend state behind a @ref VmtHook handle.
147 * @details SafetyHook clones a private snapshot surrogate, so the backend holds no host object pointer. DMK
148 * publishes and restores every binding through guarded stores. method_count equals the backend-owned
149 * allocation and bounds every unchecked VmHook slot access.
150 *
151 * Each VmHook restores its clone slot on destruction, so an erase or the map teardown restores the
152 * method. The map dies before `backend`, which keeps the clone allocation alive for every VmHook.
153 *
154 * method_mutex gives @ref VmtHook::original a shared read and every mutation an exclusive write, so
155 * a reader never sees a map or an apply in mid-mutation. It wraps SRWLOCK, because the winpthreads
156 * rwlock corrupts under reader contention. Its noexcept operations keep @ref VmtHook::original
157 * noexcept.
158 */
159 struct VmtHook::Impl
160 {
161 struct ObjectBinding
162 {
163 void *object{nullptr};
164 std::uintptr_t original_vptr{0};
165 };
166
167 safetyhook::VmtHook backend;
168 std::string name;
169 std::uintptr_t cloned_vptr_base{0};
170 std::size_t method_count{0};
171 std::uint64_t ledger_id{0};
172 // Counted reference on the module this clone's code lives in, taken before the clone is published;
173 // released on clean teardown, left outstanding with the leaked Impl on a leak branch. Holds an HMODULE;
174 // acquire/release live in hook.cpp.
175 void *self_ref{nullptr};
176 // Complete restoration state for objects whose dependency on this clone has not been safely released.
177 // Guarded by the process-wide VMT object gate.
178 std::vector<ObjectBinding> object_bindings;
179 mutable DetourModKit::detail::SrwSharedMutex method_mutex;
180
181 /**
182 * @brief One hooked slot: the original pointer read from the clone and the backend's slot patch.
183 * @details The node exists before the backend stores the slot, so @ref VmtHook::original resolves from
184 * the instant a detour can run. VmtHookFaultProof.MethodMapNodeAllocatesBeforeSlotStore pins it.
185 */
186 struct MethodHook
187 {
188 void *original{nullptr};
189 safetyhook::VmHook backend;
190 };
191 std::unordered_map<std::size_t, MethodHook> method_hooks;
192
193 111 Impl(
194 safetyhook::VmtHook hook,
195 std::string hook_name,
196 std::uintptr_t base,
197 std::size_t methods,
198 std::uint64_t ledger
199 )
200 333 : backend(std::move(hook)), name(std::move(hook_name)), cloned_vptr_base(base), method_count(methods),
201 111 ledger_id(ledger)
202 {
203 111 }
204 };
205
206 /// How the teardown of one Hook ended. @ref HookRetirement counts each value.
207 enum class TeardownOutcome : std::uint8_t
208 {
209 /// The handle owned no hook.
210 Empty,
211 /// The hook restored its target.
212 Restored,
213 /// The hook retired by bypass as residue and reaches no image code.
214 Vacated,
215 /// The backend leaked with its detour possibly reachable. HookManager counts it.
216 Pinned,
217 /// The hook restored its target, and HookAccess::finish_deferred_restore reports how it ends.
218 Deferred,
219 };
220
221 namespace detail
222 {
223 /// The counted drain proof that a teardown relies on.
224 enum class CountedDrainProof : std::uint8_t
225 {
226 /// The teardown switches the relay off and drains the counted detour itself.
227 Required,
228 /// HookStack::retire switched every relay of its stack off and drained every counted detour.
229 ProvenByRetire,
230 /// As Required. No caller proves a raw inline detour idle, so one whose patch committed pins.
231 InstallRollback,
232 };
233
234 /// An inline or mid hook target that the HookLedger reserved for one install.
235 struct ReservedTarget
236 {
237 std::uintptr_t address{0};
238 std::uint64_t ledger_id{0};
239 /// True when this kit already hooks the target, so the install layers on its own hook.
240 bool preexisting{false};
241 };
242
243 /// The access path of hook.cpp and hook_retirement.cpp to the private state of a Hook.
244 struct HookAccess
245 {
246 /**
247 * @brief A restored hook whose trampoline or mid route waits for the idle proof that HookStack::retire
248 * runs after its last restore.
249 * @details The teardown released the ledger entry. The Impl keeps the trampoline or the route, its
250 * process record, the adapter slot, and the module reference until @ref
251 * finish_deferred_restore.
252 */
253 struct DeferredRestore
254 {
255 std::unique_ptr<Hook::Impl> impl;
256 std::string name;
257 bool was_active{false};
258 };
259
260 /// Returns the backend state of @p hook, or nullptr for a disengaged handle.
261 2502 [[nodiscard]] static Hook::Impl *impl(const Hook &hook) noexcept { return hook.m_impl.get(); }
262
263 /// Builds the handle that an install verb returns.
264 [[nodiscard]] static Hook
265 17812 make(std::unique_ptr<Hook::Impl> impl, std::shared_ptr<Hook::CallGate> gate) noexcept
266 {
267 35624 return Hook(std::move(impl), std::move(gate));
268 }
269
270 /**
271 * @brief Runs the ~Hook contract on @p hook and reports how it ended. The handle ends disengaged.
272 * @param deferred Receives a restored hook whose trampoline or mid route waits for the proof of
273 * HookStack::retire, while its reserved capacity lasts. Null proves every hook at once.
274 */
275 [[nodiscard]] static TeardownOutcome teardown(
276 Hook &hook,
277 CountedDrainProof proof,
278 std::vector<DeferredRestore> *deferred = nullptr
279 ) noexcept;
280
281 /// Completes the teardown of @p restore after the batch reclaim and reports how it ended.
282 [[nodiscard]] static TeardownOutcome finish_deferred_restore(DeferredRestore &restore) noexcept;
283
284 /**
285 * @brief Installs a disabled inline hook on @p reserved whose relay jumps to @p destination.
286 * @details @p counted carries the thunk of a counted hook. Every failure releases the reservation.
287 */
288 [[nodiscard]] static Result<Hook> install_inline(
289 InlineRequest request,
290 const ReservedTarget &reserved,
291 void *destination,
292 std::optional<CountedThunk> counted
293 );
294
295 /// The warning line and lifecycle event of an @ref arm_installed call, for the install to emit later.
296 struct DeferredArm
297 {
298 /// The warning text, or null when the arm logs nothing.
299 const char *warning{nullptr};
300 std::string_view reason;
301 std::optional<diagnostics::HookTransition> transition;
302 };
303
304 /**
305 * @brief Runs the Hook::enable arm on a hook that HookStack::install created under its hold.
306 * @details The arm writes through the kept target slot of the install and acquires the coordinator
307 * again with no wait. The call gate of @p hook is unpublished, so its lock cannot wait. The
308 * target still holds the bytes that the create saved, so the arm never recaptures and never
309 * waits for a busy trampoline (HookStackTest.InstallOverAnArmedLayerRunsNoIdlePass). The arm
310 * emits nothing: @p deferred receives its warning line and its lifecycle event.
311 */
312 [[nodiscard]] static Result<void> arm_installed(Hook &hook, DeferredArm &deferred) noexcept;
313
314 /// Logs the warning line of @p deferred for the hook @p name at @p target.
315 static void
316 log_deferred_arm(const DeferredArm &deferred, const std::string &name, std::uintptr_t target) noexcept;
317
318 /// Stores @p original into @p slot with release order.
319 4234 static void publish_original(const OriginalSlot &slot, void *original) noexcept
320 {
321 4234 slot.publish(original);
322 4234 }
323
324 /**
325 * @brief Runs the HookStack::install transaction on @p reserved (docs/design/hooking.md `[B-83]`).
326 * @param create_backend Creates the disabled backend hook from the hook name, under the hold.
327 * @param hand_off Moves the module reference and the state of the hook kind into the Impl. No step
328 * after it can fail.
329 * @return The stored hook, or the Error of the create or of the arm. Either Error leaves @p hooks
330 * unchanged. A create Error releases the reservation.
331 * @details The stack slot, the handle, and the Impl allocate before the hold. Under the hold, the
332 * create, the original store, the stack store, and the arm run in that order. The hold
333 * releases first, then the kept slot, and then the log lines and lifecycle events run. A
334 * failed arm then destroys the hook through its teardown and stores null into @p original,
335 * unless the teardown pins a reachable detour.
336 * src/hook.cpp defines it.
337 */
338 template <class Backend, class CreateBackend, class HandOff>
339 [[nodiscard]] static Result<std::reference_wrapper<Hook>> install_into_stack(
340 std::vector<std::unique_ptr<Hook>> &hooks,
341 std::string name,
342 const ReservedTarget &reserved,
343 const Options &options,
344 const OriginalSlot &original,
345 CreateBackend &&create_backend,
346 HandOff &&hand_off
347 ) noexcept;
348 };
349
350 /// The thunk geometry that the install decodes. The positions serve the region-boundary proofs.
351 struct CountedThunkShape
352 {
353 CountedThunk thunk;
354 /**
355 * @brief The offset of the locked increment that counts an entry.
356 * @details A stack word equal to a helper site keeps the drain Busy, so the shape holds offsets.
357 */
358 std::size_t enter_count_offset{0};
359 /// The offset of the first instruction after the locked increment that counts an exit.
360 std::size_t after_exit_count_offset{0};
361 /// The calls outside the counted window. Each one adds a helper site.
362 std::size_t helper_calls{0};
363 /// The calls inside the counted window: the detour call, plus any argument copy helper.
364 std::size_t counted_calls{0};
365 /// The push and pop instructions before the return, so a proof can check a balanced frame.
366 std::size_t pushes{0};
367 std::size_t pops{0};
368 };
369
370 /**
371 * @brief Decodes the counted thunk at @p entry, through any incremental-link jump, for the drain proof.
372 * @return The shape, or a sentence that names why the proof cannot cover the thunk. The proof needs one
373 * unchained RUNTIME_FUNCTION of straight-line code with exactly two locked increments and at
374 * least one call between them.
375 */
376 [[nodiscard]] std::expected<CountedThunkShape, const char *> inspect_counted_thunk(void *entry) noexcept;
377
378 /// The verdict of @ref drain_counted.
379 struct CountedDrain
380 {
381 bool idle{false};
382 /// The calls between enter and exit at the last counter read.
383 std::uint64_t in_flight{0};
384 };
385
386 /**
387 * @brief Proves that no thread runs the thunks behind @p blocks or their detours (`[B-106]`).
388 * @details The relays must already send every caller past the thunks. The proof runs pass 1 over
389 * @p ranges and @p sites, reads every exit count and then every enter count, and runs pass 2. It
390 * accepts only a clean pass 1, equal sums, and a clean pass 2. Otherwise it retries until
391 * @p deadline and holds no coordinator or ledger slot between passes.
392 */
393 [[nodiscard]] CountedDrain drain_counted(
394 std::span<const safetyhook::AddressRange> ranges,
395 std::span<const safetyhook::ReturnSites *const> sites,
396 std::span<CountedBlock *const> blocks,
397 std::chrono::steady_clock::time_point deadline
398 ) noexcept;
399
400 /**
401 * @brief Reports whether a return address on the current thread's stack lies inside one of @p ranges.
402 * @details An unwind of the current thread through its function tables finds the frames, so a stale stack
403 * word never matches. A walk deeper than its frame bound misses the frames beyond it.
404 */
405 [[nodiscard]] bool current_thread_returns_into(std::span<const safetyhook::AddressRange> ranges) noexcept;
406 } // namespace detail
407
408 /**
409 * @brief Returns the SafetyHook allocator shared by every hook this linked instance installs.
410 * @details The hold keeps the trampoline arena mapped and is never released (`[B-47]`). A hook owned by a
411 * namespace-scope object runs ~Impl during static destruction and must not free its trampoline into
412 * a destroyed arena. An empty result means the backend provided no global allocator, which the
413 * create paths report as ErrorCode::AllocatorNotAvailable.
414 */
415 [[nodiscard]] const std::shared_ptr<safetyhook::Allocator> &backend_allocator() noexcept;
416 } // namespace hook
417
418 #if defined(DMK_ENABLE_TEST_SEAMS)
419 namespace detail
420 {
421 /// Holds or releases a backend trap transaction immediately before its first protection change.
422 void set_backend_trap_transaction_hold_for_test(bool hold) noexcept;
423
424 /// Reports whether the held backend trap transaction reached its deterministic park.
425 [[nodiscard]] bool backend_trap_transaction_reached_for_test() noexcept;
426
427 /// Returns the number of page-protection calls made by backend trap transactions.
428 [[nodiscard]] std::size_t backend_trap_protect_calls_for_test() noexcept;
429
430 /// Returns the number of backend idle passes, so a proof can count the passes of one operation.
431 [[nodiscard]] std::size_t backend_idle_pass_calls_for_test() noexcept;
432
433 /// Parks every backend idle pass before its thread suspension while @p hold is true.
434 void set_backend_idle_scan_hold_for_test(bool hold) noexcept;
435
436 /// Reports whether an idle pass reached the park since the hold was set.
437 [[nodiscard]] bool backend_idle_scan_reached_for_test() noexcept;
438
439 /// Returns the number of backend idle passes that walked a thread snapshot in place of NtGetNextThread.
440 [[nodiscard]] std::size_t backend_idle_snapshot_walks_for_test() noexcept;
441
442 /// Makes the backend lookup of NtGetNextThread fail while @p fail is true.
443 void fail_backend_next_thread_lookup_for_test(bool fail) noexcept;
444
445 /// Runs one backend idle pass over [@p begin, @p end) with no return site and reports whether it was clean.
446 [[nodiscard]] bool backend_idle_pass_for_test(void *begin, void *end) noexcept;
447
448 /// Parks every thread that enters a backend route gateway, before the gateway counts its entry, until false.
449 void set_backend_gateway_park_for_test(bool park) noexcept;
450
451 /// Reports whether a thread reached the armed gateway park.
452 [[nodiscard]] bool backend_gateway_park_reached_for_test() noexcept;
453
454 /// One backend idle pass and the sentence that a teardown warning appends for its refusal.
455 struct BackendIdleRefusal
456 {
457 bool idle{false};
458 std::uint32_t thread_id{0};
459 std::array<char, 96> sentence{};
460 std::size_t sentence_size{0};
461 };
462
463 /**
464 * @brief Runs one backend idle pass over [@p begin, @p end) and reports its refusal record.
465 * @details A non-null @p site_base adds the return site at @p site_base plus @p site_offset. The two stay
466 * apart, so no frame of the caller holds the site.
467 */
468 [[nodiscard]] BackendIdleRefusal
469 backend_idle_refusal_for_test(void *begin, void *end, const void *site_base, std::uint8_t site_offset) noexcept;
470
471 /// Closes backend trap-transaction admission and retires its vectored handler.
472 void retire_backend_trap_runtime_for_test() noexcept;
473
474 /// Describes the result of a directly driven backend protection transaction.
475 enum class TrapTransactionOutcome : std::uint8_t
476 {
477 Restored,
478 ReportedFailure,
479 Threw,
480 };
481
482 /**
483 * @brief Runs a backend protection transaction over caller-owned spans.
484 * @param from Source span start.
485 * @param to Destination span start.
486 * @param len Span length in bytes.
487 * @param run_fn Function that runs inside the transaction.
488 * @return The transaction outcome.
489 */
490 [[nodiscard]] TrapTransactionOutcome drive_backend_trap_transaction_for_test(
491 void *from,
492 void *to,
493 std::size_t len,
494 const std::function<void()> &run_fn
495 ) noexcept;
496
497 /// Arms one forward segment-protection change to report failure, or disarms it with nullptr.
498 void set_backend_trap_change_failure_target_for_test(void *segment_address) noexcept;
499
500 /// Arms one segment restore to report failure, or disarms it with nullptr.
501 void set_backend_trap_segment_restore_failure_target_for_test(void *segment_address) noexcept;
502
503 /// Returns the number of segment restore attempts in the latest directly driven transaction.
504 [[nodiscard]] std::size_t backend_trap_restore_trace_size_for_test() noexcept;
505
506 /// Returns one segment address from the latest ordered restore trace.
507 [[nodiscard]] void *backend_trap_restore_trace_address_for_test(std::size_t index) noexcept;
508
509 /// Describes one backend instruction-cache flush and its transaction position.
510 struct BackendInstructionFlushObservation
511 {
512 void *address{nullptr};
513 std::size_t size{0};
514 std::size_t protect_calls_before{0};
515 bool succeeded{false};
516 };
517
518 /** @brief Clears the backend flush trace and makes the current thread its only recorder. */
519 void reset_backend_instruction_flush_trace_for_test() noexcept;
520
521 /// Forces one upcoming backend instruction-cache flush call to fail, or disables failure with zero.
522 void set_backend_instruction_flush_failure_call_for_test(std::size_t call) noexcept;
523
524 /** @brief Returns the trace size for the recorder, or zero for any other thread. */
525 [[nodiscard]] std::size_t backend_instruction_flush_trace_size_for_test() noexcept;
526
527 /// Returns one backend instruction-cache flush trace record.
528 [[nodiscard]] BackendInstructionFlushObservation
529 backend_instruction_flush_trace_for_test(std::size_t index) noexcept;
530
531 /// Forces the next backend inline setup to use its FF path.
532 void force_backend_ff_hook_for_test(bool force) noexcept;
533
534 /// Returns the latest backend E9 instruction-boundary count for the current thread.
535 [[nodiscard]] std::size_t backend_instruction_boundary_trace_size_for_test() noexcept;
536
537 /// Returns one original-to-trampoline instruction boundary.
538 [[nodiscard]] std::array<std::size_t, 2>
539 backend_instruction_boundary_trace_for_test(std::size_t index) noexcept;
540
541 /// Returns the latest backend inline error type for the current thread.
542 [[nodiscard]] std::uint8_t backend_last_inline_error_type_for_test() noexcept;
543
544 /// Returns an address whose page cannot enter a non-executable backend transaction.
545 [[nodiscard]] void *backend_non_executable_transaction_marker_for_test() noexcept;
546
547 /// Withholds the position claim from inline_at and mid_at. See hook.cpp.
548 extern std::atomic<bool> g_hook_position_claims_refused_for_test;
549
550 /// Makes every later backend setup emit the trampoline shape of a 4.3.0 copy, with no relay cell.
551 void omit_backend_relay_cell_for_test(bool omit) noexcept;
552
553 /// Selects 0 for no refusal, or a safetyhook::g_coordinator_refusal_for_test value.
554 void set_backend_coordinator_refusal_for_test(unsigned refusal) noexcept;
555
556 /// Counts the process route records by state.
557 struct BackendCoordinatorCounts
558 {
559 std::size_t live{0};
560 std::size_t retained{0};
561 std::size_t vacant{0};
562 std::size_t free{0};
563 };
564
565 /// Reads the process route records under the coordinator.
566 [[nodiscard]] BackendCoordinatorCounts backend_coordinator_counts_for_test() noexcept;
567
568 /// Selects the kind of a raw backend probe.
569 enum class BackendProbeKind : std::uint8_t
570 {
571 Inline,
572 Mid,
573 Routed,
574 };
575
576 /// Names one operation that a proof drives on a raw backend probe.
577 enum class BackendProbeOperation : std::uint8_t
578 {
579 Enable,
580 Disable,
581 Bypass,
582 Resume,
583 Vacate,
584 Reset,
585 MoveAssign,
586 RetainRoute,
587 /// Reads the route entries through observe_route_entries(), and reports a zero count.
588 ObserveRouteEntries,
589 };
590
591 /// Reads one raw backend probe. A null address means absent.
592 struct BackendProbeState
593 {
594 bool valid{false};
595 bool enabled{false};
596 bool claimed{false};
597 bool bypassed{false};
598 void *trampoline{nullptr};
599 void *relay_slot{nullptr};
600 void *relay_cell{nullptr};
601 /// The verdict of the latest reset: the retention, its reason, and, for a mid probe, the stub release.
602 bool route_retained{false};
603 bool stub_freed{false};
604 const char *retention_reason{nullptr};
605 };
606
607 /// One raw backend hook that a proof drives without a Hook handle.
608 struct BackendProbe;
609
610 /**
611 * @brief Creates a disabled raw backend hook on @p target with the claim of @p position_name.
612 * @param destination The detour, or a `void(safetyhook::Context &)` callback for a mid probe.
613 * @return The probe, or nullptr when the backend refused.
614 */
615 [[nodiscard]] BackendProbe *create_backend_probe_for_test(
616 BackendProbeKind kind,
617 void *target,
618 void *destination,
619 std::string_view position_name
620 ) noexcept;
621
622 /// Drives @p operation on @p probe and reports the backend result.
623 [[nodiscard]] bool drive_backend_probe_for_test(BackendProbe *probe, BackendProbeOperation operation) noexcept;
624
625 /// Reads @p probe.
626 [[nodiscard]] BackendProbeState backend_probe_state_for_test(const BackendProbe *probe) noexcept;
627
628 /// Destroys @p probe through the backend destructor.
629 void destroy_backend_probe_for_test(BackendProbe *probe) noexcept;
630
631 /// Adds up to @p count one-byte filler records to the process route registry and returns the number added.
632 [[nodiscard]] std::size_t fill_backend_coordinator_for_test(std::size_t count) noexcept;
633
634 /// Removes every filler record that fill_backend_coordinator_for_test added.
635 void clear_backend_coordinator_fill_for_test() noexcept;
636
637 /// Names the point of a counted drain proof at which @ref g_counted_drain_probe runs.
638 enum class CountedDrainStage : std::uint8_t
639 {
640 AfterFirstPass,
641 BetweenCounterReads,
642 AfterCounterReads,
643 };
644
645 /// Runs at each stage of every counted drain proof while it is not null.
646 extern void (*g_counted_drain_probe)(CountedDrainStage) noexcept;
647
648 /// The decoded thunk of a counted hook. Every address is null for a raw or disengaged hook.
649 struct CountedThunkProbe
650 {
651 void *entry{nullptr};
652 void *range_end{nullptr};
653 void *enter_count{nullptr};
654 void *after_exit_count{nullptr};
655 std::size_t helper_calls{0};
656 std::size_t counted_calls{0};
657 std::size_t pushes{0};
658 std::size_t pops{0};
659 };
660
661 /// Decodes the thunk of the counted hook behind @p hook.
662 [[nodiscard]] CountedThunkProbe counted_thunk_for_test(const hook::Hook &hook) noexcept;
663 } // namespace detail
664 #endif
665 } // namespace DetourModKit
666
667 #endif // DETOURMODKIT_INTERNAL_HOOK_BACKEND_HPP
668