GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 12 / 0 / 12
Functions: 100.0% 3 / 0 / 3
Branches: -% 0 / 0 / 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 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