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 |