GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 16.2% 36 / 0 / 222
Functions: 6.1% 4 / 0 / 66
Branches: 88.9% 16 / 0 / 18

src/internal/mid_hook_adapter.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_MID_HOOK_ADAPTER_HPP
2 #define DETOURMODKIT_INTERNAL_MID_HOOK_ADAPTER_HPP
3
4 /**
5 * @file internal/mid_hook_adapter.hpp
6 * @brief The backend-typed mid-hook dispatch pool: per-hook adapters, exception containment, and rundown.
7 * @details A mid hook's callback is reached from a hand-emitted assembly stub that the backend calls directly. This
8 * pool places a DMK frame between the stub and the user callback, where exceptions can be contained and
9 * callback entries counted.
10 *
11 * The backend's destination type is `void(*)(safetyhook::Context&)` and carries no user-data parameter, so the
12 * only way to reach a per-hook callback from an exactly typed function is to give each hook its own function.
13 * `mid_adapter<I>` is that function, and slot @p I supplies the identity the signature has no room to pass.
14 *
15 * The adapter's exact backend type requires no function-pointer conversion or related warning suppression.
16 *
17 * The opaque hook::MidContext is recovered by a REFERENCE cast inside the adapter, which is the same
18 * pass-through the public accessors in src/hook_mid_context.cpp perform and is sound because MidContext is
19 * forever incomplete and is only ever the Context the backend passed.
20 *
21 * Only the hook sibling TUs (src/hook.cpp and src/internal/mid_hook_adapter.cpp) include this header, so the
22 * backend stays confined to that island.
23 */
24
25 #include "DetourModKit/hook.hpp"
26
27 #include "platform.hpp"
28
29 #include <safetyhook.hpp>
30
31 #include <array>
32 #include <atomic>
33 #include <cstddef>
34 #include <cstdint>
35 #include <utility>
36
37 namespace DetourModKit::detail
38 {
39 /**
40 * @brief The number of DMK-managed mid hooks that may exist at one time.
41 * @details Each slot costs one generated adapter function, so the pool is a compile-time set. Exhaustion is
42 * reported as ErrorCode::MidHookCapacityExhausted rather than degraded into an untyped failure.
43 */
44 inline constexpr std::size_t MID_ADAPTER_CAPACITY = 64;
45
46 /**
47 * @brief Process-lifetime dispatch state for one mid hook.
48 * @details Never destroyed. A thread that has already loaded a slot pointer may still be inside its adapter when
49 * teardown releases the slot, so the storage must outlive every hook that ever used it; only the slot's
50 * CONTENTS are recycled, and `claimed` is released only once `adapter_entries` reaches zero.
51 */
52 struct MidAdapterSlot
53 {
54 /// Owns the slot for one hook's lifetime. Released only after a witnessed drain.
55 std::atomic<bool> claimed{false};
56 /// The rundown tombstone: false means no further user callback may begin through this slot.
57 std::atomic<bool> live{false};
58 /// Threads currently inside this slot's adapter body, including ones that back out at the tombstone.
59 std::atomic<std::uint32_t> adapter_entries{0};
60 /// User callbacks that passed the tombstone recheck and have not returned.
61 std::atomic<std::uint32_t> callbacks_in_flight{0};
62 /// Callbacks whose thread could not be recorded in the entry chain, so self-entry cannot be disproven.
63 std::atomic<std::uint32_t> untracked_entries{0};
64 /// The user callback. Only ever read by a thread that has entered and observed `live`.
65 std::atomic<hook::MidHookFn> detour{nullptr};
66 /// Counts user exceptions contained at this boundary, for diagnostics.
67 std::atomic<std::uint64_t> contained_exceptions{0};
68 /// The hooked address, carried here so a containment report can name the site without touching Impl storage.
69 std::atomic<std::uintptr_t> target{0};
70 };
71
72 /// The pool. Namespace-scope storage with no destructor, by the same never-destroyed discipline the ledger uses.
73 [[nodiscard]] MidAdapterSlot *mid_adapter_slots() noexcept;
74
75 /**
76 * @brief One frame of the current thread's mid-adapter entry chain.
77 * @details Lives on the adapter's own stack, so maintaining the chain allocates nothing. It exists so teardown can
78 * answer "is THIS thread inside THIS slot" exactly, which is what keeps a hook destroyed from inside its
79 * own callback from waiting on itself forever.
80 */
81 struct MidEntryFrame
82 {
83 const MidAdapterSlot *slot{nullptr};
84 MidEntryFrame *prev{nullptr};
85 };
86
87 /**
88 * @brief The Win32 TLS index holding this thread's MidEntryFrame chain head.
89 * @details Win32 TLS rather than thread_local: MinGW lowers thread_local to __emutls_get_address, which allocates
90 * on every thread's first touch and takes a process-wide lock to do it, inside a callback reached from a
91 * hooked function on an arbitrary host thread. A reserved Win32 index costs a TEB slot read instead.
92 * Reserved by hook::mid_at before any adapter of its slot can run, so a live slot always has a valid
93 * index. Recording a frame under it can still fail (an index past the TEB's inline slots is backed by a
94 * lazily heap-allocated expansion array), which is why an entry that cannot be recorded is counted rather
95 * than assumed absent.
96 */
97 [[nodiscard]] std::atomic<DWORD> &mid_entry_tls_index() noexcept;
98
99 /// Reserves the TLS index once. Returns false if the process has no index to give.
100 [[nodiscard]] bool ensure_mid_entry_tls() noexcept;
101
102 /// True when the calling thread is currently inside @p slot's adapter body.
103 [[nodiscard]] bool thread_is_inside_mid_adapter(const MidAdapterSlot &slot) noexcept;
104
105 /// Reports a contained user exception. Defined in src/internal/mid_hook_adapter.cpp, where the logger is visible.
106 void note_contained_mid_exception(MidAdapterSlot &slot) noexcept;
107
108 #if defined(DMK_ENABLE_TEST_SEAMS)
109 /**
110 * @brief Stable executable interval exposed by the backend route park.
111 * @note Test hosts that cannot include this header redeclare this enum, because including it would pull the
112 * backend into targets that deliberately carry no safetyhook include path. These values are therefore part
113 * of that cross-target contract: reordering an enumerator fails here rather than silently changing which
114 * interval a redeclaring host selects.
115 */
116 enum class MidRouteParkStage : std::uint8_t
117 {
118 None,
119 BeforeAdapter,
120 AfterAdapter
121 };
122 static_assert(static_cast<std::uint8_t>(MidRouteParkStage::None) == 0);
123 static_assert(static_cast<std::uint8_t>(MidRouteParkStage::BeforeAdapter) == 1);
124 static_assert(static_cast<std::uint8_t>(MidRouteParkStage::AfterAdapter) == 2);
125
126 /// Arms one backend-route park, or releases an existing park with None.
127 void set_mid_route_park_for_test(MidRouteParkStage stage) noexcept;
128
129 /// Reports whether the armed backend-route park was reached.
130 [[nodiscard]] bool mid_route_park_reached_for_test() noexcept;
131
132 /**
133 * @brief Fired inside the adapter after the fast-path live check and before the callback commit.
134 * @details The window between those two points is the only one the tombstone recheck below exists to close, and it
135 * is a pure thread race: an entrant that has already passed the fast-path check must still not run a
136 * callback if a rundown completes before it commits. No stress schedule reaches that instant reliably, so
137 * a test parks a thread here and runs the teardown to completion underneath it.
138 */
139 extern void (*g_mid_adapter_precommit_probe)() noexcept;
140
141 /**
142 * @brief Native id of the thread whose entry-chain store must report failure, or 0 for none.
143 * @details A store into a reserved index past the TEB's inline slots is backed by a lazily heap-allocated
144 * expansion array, so it can fail on a thread that has never used a high index while the reservation
145 * itself stays valid. The untracked accounting that failure selects is what keeps a teardown from waiting
146 * on the very thread running it, and no host can provoke the heap state on demand. Keyed by thread id
147 * rather than held in thread_local storage because MinGW lowers thread_local to emutls, which would
148 * allocate on this exact callback path.
149 */
150 extern std::atomic<std::uint32_t> g_mid_entry_store_failure_thread;
151
152 /// Counts stores the exact failure seam refused, so a proof cannot pass through the ordinary tracked path.
153 extern std::atomic<std::uint64_t> g_mid_entry_store_failure_hits;
154
155 /// Returns the pool index from the most recent successful claim, or MID_ADAPTER_CAPACITY before any claim.
156 [[nodiscard]] std::size_t last_claimed_mid_slot_for_test() noexcept;
157
158 /// Reports whether pool slot @p index is currently claimed. Out-of-range indices report false.
159 [[nodiscard]] bool mid_slot_claimed_for_test(std::size_t index) noexcept;
160
161 /**
162 * @brief Adjusts slot @p index's adapter-body entry count by @p delta.
163 * @details Stands in for an indefinitely parked adapter entrant. The drains read only this counter, so an
164 * injected entry is indistinguishable from a preempted thread inside the body. The caller must balance
165 * the adjustment.
166 */
167 void adjust_mid_adapter_entries_for_test(std::size_t index, std::int32_t delta) noexcept;
168 #endif
169
170 /// The one store whose failure decides tracked versus untracked accounting for this entry.
171 60 [[nodiscard]] inline bool store_mid_entry(DWORD tls, MidEntryFrame *frame) noexcept
172 {
173 #if defined(DMK_ENABLE_TEST_SEAMS)
174
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 14 taken 58 times.
60 if (g_mid_entry_store_failure_thread.load(std::memory_order_relaxed) == ::GetCurrentThreadId())
175 {
176 g_mid_entry_store_failure_hits.fetch_add(1, std::memory_order_relaxed);
177 2 return false;
178 }
179 #endif
180 58 return ::TlsSetValue(tls, frame) != FALSE;
181 }
182
183 /// Takes ownership of a free slot, or returns @ref MID_ADAPTER_CAPACITY when the pool is full.
184 [[nodiscard]] std::size_t claim_mid_adapter_slot() noexcept;
185
186 /// Returns a drained slot to the pool. Never call on a slot that has not been run down.
187 void release_mid_adapter_slot(std::size_t index) noexcept;
188
189 /// The outcome of @ref run_down_mid_slot.
190 enum class MidRundown : std::uint8_t
191 {
192 /// No user callback is executing; teardown may proceed.
193 Drained,
194 /**
195 * @brief Waiting cannot be proven to terminate, so the caller must pin instead.
196 * @details Either the calling thread is itself inside the adapter (much the likelier cause), or an entrant
197 * lacks a record and cannot be ruled out as the caller. The two cases are not distinguished
198 * because the required action is the same.
199 */
200 Unwaitable,
201 /// The bounded wait ended with a callback still in flight. The caller must pin.
202 Expired
203 };
204
205 /**
206 * @brief Waits for every user callback committed before @p slot was tombstoned.
207 * @details New adapter entries can still arrive through a pinned backend. The live recheck prevents a callback
208 * commit. The function returns Unwaitable to avoid a self-wait. It returns Expired when the bounded wait
209 * ends with a callback still counted.
210 */
211 [[nodiscard]] MidRundown run_down_mid_slot(MidAdapterSlot &slot) noexcept;
212
213 /**
214 * @brief Waits for every adapter body to leave after entry through the backend has stopped.
215 * @param slot The tombstoned slot to drain.
216 * @return The result is true when no entrant remains. It is false when the bounded wait expires with an occupied
217 * body. A false result never permits reclamation of the slot or stub.
218 * @note Backend route rundown separately spans the generated stub from its stable gateway through its stable exit
219 * thunk. This counter remains the slot-reuse authority for the DMK adapter body itself.
220 */
221 [[nodiscard]] bool drain_mid_adapter_entries(MidAdapterSlot &slot) noexcept;
222
223 /**
224 * @brief The body every generated adapter shares.
225 * @details Enter, recheck, invoke, leave. The callback-counter/recheck pair and the tombstone/drain pair are a
226 * Dekker seam: both sides are seq_cst so at least one of them observes the other, which is what makes "no
227 * callback begins after rundown returns" hold without a lock on the callback path.
228 *
229 * The reference count is held across the WHOLE body, including the back-out, so a drained slot has no
230 * thread anywhere in this function and its contents may be recycled.
231 */
232 1066 inline void dispatch_mid_adapter(MidAdapterSlot &slot, safetyhook::Context &ctx) noexcept
233 {
234 1066 slot.adapter_entries.fetch_add(1, std::memory_order_seq_cst);
235
2/2
✓ Branch 5 → 6 taken 61 times.
✓ Branch 5 → 39 taken 1005 times.
1066 if (slot.live.load(std::memory_order_seq_cst))
236 {
237 #if defined(DMK_ENABLE_TEST_SEAMS)
238
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 60 times.
61 if (auto *probe = g_mid_adapter_precommit_probe)
239 {
240 1 probe();
241 }
242 #endif
243 61 slot.callbacks_in_flight.fetch_add(1, std::memory_order_seq_cst);
244
2/2
✓ Branch 11 → 12 taken 60 times.
✓ Branch 11 → 36 taken 1 time.
61 if (slot.live.load(std::memory_order_seq_cst))
245 {
246
1/2
✓ Branch 13 → 14 taken 60 times.
✗ Branch 13 → 36 not taken.
60 if (const hook::MidHookFn detour = slot.detour.load(std::memory_order_acquire))
247 {
248 60 const DWORD tls = mid_entry_tls_index().load(std::memory_order_acquire);
249 60 MidEntryFrame frame{&slot, nullptr};
250 60 bool tracked = false;
251
1/2
✓ Branch 22 → 23 taken 60 times.
✗ Branch 22 → 25 not taken.
60 if (tls != TLS_OUT_OF_INDEXES)
252 {
253 60 frame.prev = static_cast<MidEntryFrame *>(::TlsGetValue(tls));
254 60 tracked = store_mid_entry(tls, &frame);
255 }
256
2/2
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 29 taken 58 times.
60 if (!tracked)
257 {
258 // The chain walk cannot see this thread, so a teardown must not conclude it is absent: a wrong
259 // "no" makes the rundown wait on the very thread running it, while a wrong "yes" only pins.
260 // Counted rather than made sticky so the pool recovers once the entry leaves.
261 2 slot.untracked_entries.fetch_add(1, std::memory_order_seq_cst);
262 }
263 try
264 {
265 // The one boundary that must never let a throw reach the backend's generated mid stub. The
266 // routed gateway, wrapper, and exit thunk are all described by registered unwind data, but the
267 // stub between them adjusts RSP dynamically and is not, so an escaping exception unwinds
268 // through a frame the platform cannot describe: a host crash rather than an error.
269
2/2
✓ Branch 29 → 30 taken 43 times.
✓ Branch 29 → 42 taken 17 times.
60 detour(reinterpret_cast<hook::MidContext &>(ctx));
270 }
271 17 catch (...)
272 {
273 17 note_contained_mid_exception(slot);
274 17 }
275
2/2
✓ Branch 30 → 31 taken 58 times.
✓ Branch 30 → 32 taken 2 times.
60 if (tracked)
276 {
277 58 (void)::TlsSetValue(tls, frame.prev);
278 }
279 else
280 {
281 2 slot.untracked_entries.fetch_sub(1, std::memory_order_seq_cst);
282 }
283 }
284 }
285 61 slot.callbacks_in_flight.fetch_sub(1, std::memory_order_seq_cst);
286 }
287 1066 slot.adapter_entries.fetch_sub(1, std::memory_order_seq_cst);
288 1066 }
289
290 /**
291 * @brief The generated per-hook adapter.
292 * @details This IS the backend's destination: a genuine `void(safetyhook::Context&)`, so the backend's call through
293 * it is an ordinary call of a function of its own type.
294 */
295 1066 template <std::size_t Index> void mid_adapter(safetyhook::Context &ctx) noexcept
296 {
297 1066 dispatch_mid_adapter(mid_adapter_slots()[Index], ctx);
298 1066 }
299
300 template <std::size_t... Indices>
301 [[nodiscard]] constexpr std::array<safetyhook::MidHookFn, sizeof...(Indices)>
302 make_mid_adapter_table(std::index_sequence<Indices...>) noexcept
303 {
304 // Dropping noexcept from each adapter pointer's type is a standard implicit conversion.
305 return {&mid_adapter<Indices>...};
306 }
307
308 /// Adapter addresses in the exact type the backend consumes.
309 extern constinit const std::array<safetyhook::MidHookFn, MID_ADAPTER_CAPACITY> MID_ADAPTER_TABLE;
310 } // namespace DetourModKit::detail
311
312 #endif // DETOURMODKIT_INTERNAL_MID_HOOK_ADAPTER_HPP
313