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 forward-declares the nested Impl of every backend-owning handle (Hook, VmtHook) and holds it behind | ||
| 8 | * a std::unique_ptr; those Impl bodies are completed here, where safetyhook.hpp is visible. This, | ||
| 9 | * internal/hook_backend_visit.hpp, and internal/mid_hook_adapter.hpp are the only DetourModKit headers that | ||
| 10 | * name the SafetyHook backend; none is installed and only the hook sibling TUs (src/hook.cpp, | ||
| 11 | * src/hook_toggle.cpp, src/hook_mid_context.cpp, src/internal/mid_hook_adapter.cpp) include them, so the | ||
| 12 | * backend (and the Zydis headers it drags in) stays confined to that island. A public consumer that includes | ||
| 13 | * hook.hpp pulls in none of it. | ||
| 14 | * | ||
| 15 | * The opaque hook::MidContext bridge is deliberately NOT defined here. MidContext must stay an incomplete | ||
| 16 | * type in every translation unit so that the backend-context <-> MidContext reinterpret_cast remains a pure | ||
| 17 | * pass-through; the casts therefore live in the accessor function bodies in src/hook_mid_context.cpp, never | ||
| 18 | * as a type definition. | ||
| 19 | */ | ||
| 20 | |||
| 21 | #include "DetourModKit/hook.hpp" | ||
| 22 | |||
| 23 | #include "internal/srw_shared_mutex.hpp" | ||
| 24 | |||
| 25 | #include <safetyhook.hpp> | ||
| 26 | |||
| 27 | #include <array> | ||
| 28 | #include <atomic> | ||
| 29 | #include <cstddef> | ||
| 30 | #include <cstdint> | ||
| 31 | #include <functional> | ||
| 32 | #include <memory> | ||
| 33 | #include <mutex> | ||
| 34 | #include <string> | ||
| 35 | #include <unordered_map> | ||
| 36 | #include <utility> | ||
| 37 | #include <variant> | ||
| 38 | #include <vector> | ||
| 39 | |||
| 40 | namespace DetourModKit | ||
| 41 | { | ||
| 42 | namespace hook | ||
| 43 | { | ||
| 44 | /** | ||
| 45 | * @enum HookState | ||
| 46 | * @brief Internal enable/disable state machine for a managed Hook (never public). | ||
| 47 | * @details The intermediate Enabling / Disabling states let an atomic compare-exchange publish a terminal | ||
| 48 | * state only after the backend toggle returns, so a concurrent reader never observes a speculative | ||
| 49 | * Active/Disabled while the backend call is mid-flight. | ||
| 50 | */ | ||
| 51 | enum class HookState : std::uint8_t | ||
| 52 | { | ||
| 53 | Active, | ||
| 54 | Disabled, | ||
| 55 | Enabling, | ||
| 56 | Disabling | ||
| 57 | }; | ||
| 58 | |||
| 59 | /** | ||
| 60 | * @brief The refcounted call gate that outlives the backend it fronts, so a late @ref Hook::call is safe. | ||
| 61 | * @details A caller pins the gate before locking, and teardown nulls @ref callable under the same mutex before | ||
| 62 | * freeing the trampoline. The recursive mutex permits a detour to re-enter @ref Hook::call. During | ||
| 63 | * loader-lock teardown the backend and its module reference are intentionally leaked, so callable | ||
| 64 | * remains valid. Every access to callable holds the mutex. | ||
| 65 | */ | ||
| 66 | struct Hook::CallGate | ||
| 67 | { | ||
| 68 | std::recursive_mutex mutex; | ||
| 69 | void *callable{nullptr}; | ||
| 70 | }; | ||
| 71 | |||
| 72 | /** | ||
| 73 | * @brief The complete backend state behind a @ref Hook handle. | ||
| 74 | * @details Holds one backend hook, its state, identity, target, and ledger token. The call mutex lives in the | ||
| 75 | * separate refcounted @ref Hook::CallGate. Atomic state makes this type non-movable. | ||
| 76 | */ | ||
| 77 | struct Hook::Impl | ||
| 78 | { | ||
| 79 | std::variant<safetyhook::InlineHook, safetyhook::MidHook> backend; | ||
| 80 | std::atomic<HookState> status; | ||
| 81 | std::string name; | ||
| 82 | std::uintptr_t target{0}; | ||
| 83 | std::uint64_t ledger_id{0}; | ||
| 84 | bool is_inline{false}; | ||
| 85 | // Index into the mid-hook adapter pool, or SIZE_MAX for an inline hook. Kept as a bare index (rather than a | ||
| 86 | // slot pointer) so this header does not have to name the adapter pool. Teardown runs the slot down through | ||
| 87 | // it; a pinned Impl carries it away and the slot is never reclaimed, which is what keeps a still-reachable | ||
| 88 | // stub's adapter pointing at storage that outlives it. | ||
| 89 | std::size_t mid_slot{static_cast<std::size_t>(-1)}; | ||
| 90 | // Counted reference on the module this hook's trampoline/detour code lives in, taken before the backend is | ||
| 91 | // published while the module is mapped. ~Hook releases it on the clean off-loader-lock teardown; on a leak | ||
| 92 | // branch it rides along with the leaked Impl (never released), keeping the trampoline mapped for a late | ||
| 93 | // foreign caller. Holds an HMODULE (kept as void* so the acquire/release stay in hook.cpp). See | ||
| 94 | // detail::acquire_module_ref. | ||
| 95 | void *self_ref{nullptr}; | ||
| 96 | |||
| 97 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 98 | ~Impl() noexcept; | ||
| 99 | #endif | ||
| 100 | |||
| 101 | 288 | Impl( | |
| 102 | safetyhook::InlineHook hook, | ||
| 103 | std::string hook_name, | ||
| 104 | std::uintptr_t hook_target, | ||
| 105 | std::uint64_t ledger, | ||
| 106 | HookState initial_state | ||
| 107 | ) | ||
| 108 | 864 | : backend(std::move(hook)), status(initial_state), name(std::move(hook_name)), target(hook_target), | |
| 109 | 288 | ledger_id(ledger), is_inline(true) | |
| 110 | { | ||
| 111 | 288 | } | |
| 112 | |||
| 113 | 224 | Impl( | |
| 114 | safetyhook::MidHook hook, | ||
| 115 | std::string hook_name, | ||
| 116 | std::uintptr_t hook_target, | ||
| 117 | std::uint64_t ledger, | ||
| 118 | HookState initial_state | ||
| 119 | ) | ||
| 120 | 672 | : backend(std::move(hook)), status(initial_state), name(std::move(hook_name)), target(hook_target), | |
| 121 | 224 | ledger_id(ledger), is_inline(false) | |
| 122 | { | ||
| 123 | 224 | } | |
| 124 | }; | ||
| 125 | |||
| 126 | /** | ||
| 127 | * @brief The complete backend state behind a @ref VmtHook handle. | ||
| 128 | * @details Owns the detached backend clone, its identity, bindings, and per-index method hooks. SafetyHook is | ||
| 129 | * created on a private snapshot surrogate whose counted run is normalized to a stable executable | ||
| 130 | * marker, then populated with the captured targets. It retains no host object pointer; DMK publishes | ||
| 131 | * and restores every binding through guarded stores. method_count exactly matches the backend-owned | ||
| 132 | * allocation and bounds every unchecked VmHook slot access. | ||
| 133 | * | ||
| 134 | * Per-method hooks are backend VmHooks keyed by vtable index. Each VmHook, on destruction, rewrites | ||
| 135 | * its cloned-vtable slot back to the original function pointer, so erasing an entry (@ref | ||
| 136 | * VmtHook::remove_method) or destroying the map (handle teardown) restores that method. The map dies | ||
| 137 | * before `backend`, keeping the clone allocation alive until every VmHook has released it. | ||
| 138 | * | ||
| 139 | * method_mutex is the reader/writer guard for per-method state: @ref VmtHook::original snapshots a | ||
| 140 | * slot's original pointer under a shared read, while @ref VmtHook::hook_method, | ||
| 141 | * @ref VmtHook::remove_method, @ref VmtHook::apply_to, and @ref VmtHook::remove_from mutate under the | ||
| 142 | * exclusive write, so a snapshot reader never traverses the map (or observes an apply) | ||
| 143 | * mid-mutation. It is an SRWLOCK wrapper rather than std::shared_mutex because winpthreads' rwlock | ||
| 144 | * corrupts under reader contention; all of its operations are noexcept, which is what lets | ||
| 145 | * @ref VmtHook::original stay noexcept. | ||
| 146 | */ | ||
| 147 | struct VmtHook::Impl | ||
| 148 | { | ||
| 149 | struct ObjectBinding | ||
| 150 | { | ||
| 151 | void *object{nullptr}; | ||
| 152 | std::uintptr_t original_vptr{0}; | ||
| 153 | }; | ||
| 154 | |||
| 155 | safetyhook::VmtHook backend; | ||
| 156 | std::string name; | ||
| 157 | std::uintptr_t cloned_vptr_base{0}; | ||
| 158 | std::size_t method_count{0}; | ||
| 159 | std::uint64_t ledger_id{0}; | ||
| 160 | // Counted reference on the module this clone's code lives in, taken before the clone is published; | ||
| 161 | // released on clean teardown, left outstanding with the leaked Impl on a leak branch. Holds an HMODULE; | ||
| 162 | // acquire/release live in hook.cpp. | ||
| 163 | void *self_ref{nullptr}; | ||
| 164 | // Complete restoration state for objects whose dependency on this clone has not been safely released. | ||
| 165 | // Guarded by the process-wide VMT object gate. | ||
| 166 | std::vector<ObjectBinding> object_bindings; | ||
| 167 | mutable DetourModKit::detail::SrwSharedMutex method_mutex; | ||
| 168 | std::unordered_map<std::size_t, safetyhook::VmHook> method_hooks; | ||
| 169 | |||
| 170 | 108 | Impl( | |
| 171 | safetyhook::VmtHook hook, | ||
| 172 | std::string hook_name, | ||
| 173 | std::uintptr_t base, | ||
| 174 | std::size_t methods, | ||
| 175 | std::uint64_t ledger | ||
| 176 | ) | ||
| 177 | 324 | : backend(std::move(hook)), name(std::move(hook_name)), cloned_vptr_base(base), method_count(methods), | |
| 178 | 108 | ledger_id(ledger) | |
| 179 | { | ||
| 180 | 108 | } | |
| 181 | }; | ||
| 182 | |||
| 183 | /** | ||
| 184 | * @brief Returns the SafetyHook allocator shared by every hook this linked instance installs. | ||
| 185 | * @details The reference is a refcount hold on the backend's allocator that keeps the trampoline arena mapped, | ||
| 186 | * and it is deliberately never released: a hook owned by a namespace-scope object runs ~Impl during | ||
| 187 | * static destruction and would otherwise free its trampoline into an already-destroyed arena. The | ||
| 188 | * returned shared_ptr is empty only if the backend could not provide a global allocator, which the | ||
| 189 | * create paths report as ErrorCode::AllocatorNotAvailable. | ||
| 190 | */ | ||
| 191 | [[nodiscard]] const std::shared_ptr<safetyhook::Allocator> &backend_allocator() noexcept; | ||
| 192 | } // namespace hook | ||
| 193 | |||
| 194 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 195 | namespace detail | ||
| 196 | { | ||
| 197 | /// Holds or releases a backend trap transaction immediately before its first protection change. | ||
| 198 | void set_backend_trap_transaction_hold_for_test(bool hold) noexcept; | ||
| 199 | |||
| 200 | /// Reports whether the held backend trap transaction reached its deterministic park. | ||
| 201 | [[nodiscard]] bool backend_trap_transaction_reached_for_test() noexcept; | ||
| 202 | |||
| 203 | /// Returns the number of page-protection calls made by backend trap transactions. | ||
| 204 | [[nodiscard]] std::size_t backend_trap_protect_calls_for_test() noexcept; | ||
| 205 | |||
| 206 | /// Closes backend trap-transaction admission and retires its vectored handler. | ||
| 207 | void retire_backend_trap_runtime_for_test() noexcept; | ||
| 208 | |||
| 209 | /// Describes the result of a directly driven backend protection transaction. | ||
| 210 | enum class TrapTransactionOutcome : std::uint8_t | ||
| 211 | { | ||
| 212 | Restored, | ||
| 213 | ReportedFailure, | ||
| 214 | Threw, | ||
| 215 | }; | ||
| 216 | |||
| 217 | /** | ||
| 218 | * @brief Runs a backend protection transaction over caller-owned spans. | ||
| 219 | * @param from Source span start. | ||
| 220 | * @param to Destination span start. | ||
| 221 | * @param len Span length in bytes. | ||
| 222 | * @param run_fn Function that runs inside the transaction. | ||
| 223 | * @return The transaction outcome. | ||
| 224 | */ | ||
| 225 | [[nodiscard]] TrapTransactionOutcome drive_backend_trap_transaction_for_test( | ||
| 226 | void *from, | ||
| 227 | void *to, | ||
| 228 | std::size_t len, | ||
| 229 | const std::function<void()> &run_fn | ||
| 230 | ) noexcept; | ||
| 231 | |||
| 232 | /// Arms one forward segment-protection change to report failure, or disarms it with nullptr. | ||
| 233 | void set_backend_trap_change_failure_target_for_test(void *segment_address) noexcept; | ||
| 234 | |||
| 235 | /// Arms one segment restore to report failure, or disarms it with nullptr. | ||
| 236 | void set_backend_trap_segment_restore_failure_target_for_test(void *segment_address) noexcept; | ||
| 237 | |||
| 238 | /// Returns the number of segment restore attempts in the latest directly driven transaction. | ||
| 239 | [[nodiscard]] std::size_t backend_trap_restore_trace_size_for_test() noexcept; | ||
| 240 | |||
| 241 | /// Returns one segment address from the latest ordered restore trace. | ||
| 242 | [[nodiscard]] void *backend_trap_restore_trace_address_for_test(std::size_t index) noexcept; | ||
| 243 | |||
| 244 | /// Describes one backend instruction-cache flush and its transaction position. | ||
| 245 | struct BackendInstructionFlushObservation | ||
| 246 | { | ||
| 247 | void *address{nullptr}; | ||
| 248 | std::size_t size{0}; | ||
| 249 | std::size_t protect_calls_before{0}; | ||
| 250 | bool succeeded{false}; | ||
| 251 | }; | ||
| 252 | |||
| 253 | /// Clears the backend instruction-cache flush trace for the current thread. | ||
| 254 | void reset_backend_instruction_flush_trace_for_test() noexcept; | ||
| 255 | |||
| 256 | /// Forces one upcoming backend instruction-cache flush call to fail, or disables failure with zero. | ||
| 257 | void set_backend_instruction_flush_failure_call_for_test(std::size_t call) noexcept; | ||
| 258 | |||
| 259 | /// Returns the backend instruction-cache flush trace size for the current thread. | ||
| 260 | [[nodiscard]] std::size_t backend_instruction_flush_trace_size_for_test() noexcept; | ||
| 261 | |||
| 262 | /// Returns one backend instruction-cache flush trace record. | ||
| 263 | [[nodiscard]] BackendInstructionFlushObservation | ||
| 264 | backend_instruction_flush_trace_for_test(std::size_t index) noexcept; | ||
| 265 | |||
| 266 | /// Forces the next backend inline setup to use its FF path. | ||
| 267 | void force_backend_ff_hook_for_test(bool force) noexcept; | ||
| 268 | |||
| 269 | /// Returns the latest backend E9 instruction-boundary count for the current thread. | ||
| 270 | [[nodiscard]] std::size_t backend_instruction_boundary_trace_size_for_test() noexcept; | ||
| 271 | |||
| 272 | /// Returns one original-to-trampoline instruction boundary. | ||
| 273 | [[nodiscard]] std::array<std::size_t, 2> | ||
| 274 | backend_instruction_boundary_trace_for_test(std::size_t index) noexcept; | ||
| 275 | |||
| 276 | /// Returns the latest backend inline error type for the current thread. | ||
| 277 | [[nodiscard]] std::uint8_t backend_last_inline_error_type_for_test() noexcept; | ||
| 278 | |||
| 279 | /// Returns an address whose page cannot enter a non-executable backend transaction. | ||
| 280 | [[nodiscard]] void *backend_non_executable_transaction_marker_for_test() noexcept; | ||
| 281 | } // namespace detail | ||
| 282 | #endif | ||
| 283 | } // namespace DetourModKit | ||
| 284 | |||
| 285 | #endif // DETOURMODKIT_INTERNAL_HOOK_BACKEND_HPP | ||
| 286 |