./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
69 / 72
95.8%
Branches:
27 / 32
84.4%
Functions:
15 / 15
100.0%

src/internal/mid_hook_adapter.cpp
Line Branch Exec Source
1 /**
2 * @file internal/mid_hook_adapter.cpp
3 * @brief This TU owns the mid-hook dispatch pool state, its rundown drains, and the mid route test seams.
4 */
5
6 #include "internal/mid_hook_adapter.hpp"
7
8 #include "internal/drain_backoff.hpp"
9 #include "internal/shared_tls_index.hpp"
10
11 #include "DetourModKit/logger.hpp"
12
13 #include <windows.h>
14
15 #include <atomic>
16 #include <chrono>
17 #include <cstddef>
18 #include <cstdint>
19
20 namespace DetourModKit::detail
21 {
22 #if defined(DMK_ENABLE_TEST_SEAMS)
23 // This probe fires inside the mid-hook adapter between the fast-path live check and callback commit. See its
24 // declaration in internal/mid_hook_adapter.hpp for the race it exists to make reachable.
25 void (*g_mid_adapter_precommit_probe)() noexcept = nullptr;
26 // Selects one thread whose adapter entry-chain store reports failure. See its declaration in
27 // internal/mid_hook_adapter.hpp for the platform condition it stands in for.
28 std::atomic<std::uint32_t> g_mid_entry_store_failure_thread{0};
29 std::atomic<std::uint64_t> g_mid_entry_store_failure_hits{0};
30
31 24 void set_mid_route_park_for_test(MidRouteParkStage stage) noexcept
32 {
33 24 safetyhook::RouteParkStage backend_stage = safetyhook::RouteParkStage::NONE;
34
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 21 times.
24 if (stage == MidRouteParkStage::BeforeAdapter)
35 {
36 3 backend_stage = safetyhook::RouteParkStage::BEFORE_DESTINATION;
37 }
38
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 19 times.
21 else if (stage == MidRouteParkStage::AfterAdapter)
39 {
40 2 backend_stage = safetyhook::RouteParkStage::BEFORE_EXIT;
41 }
42 24 safetyhook::set_route_park_for_test(backend_stage);
43 24 }
44
45 9285 bool mid_route_park_reached_for_test() noexcept
46 {
47 9285 return safetyhook::route_park_reached_for_test();
48 }
49 #endif
50
51 // The mid-hook dispatch pool uses constant initialization and trivial destruction. It registers no destructor, so
52 // it outlives an adapter that remains active during static destruction.
53 namespace
54 {
55 MidAdapterSlot s_mid_slots[MID_ADAPTER_CAPACITY];
56 constinit SharedTlsIndex s_mid_entry_tls;
57
58 /// Defines the committed callback wait bound. Expiry pins the backend instead of a hang (MidHookDrainTest).
59 constexpr auto MID_CALLBACK_DRAIN_TIMEOUT = std::chrono::seconds{5};
60
61 /// Defines the post-restore adapter-body drain bound. Expiry retains the slot and stub (MidHookDrainTest).
62 constexpr auto MID_ADAPTER_ENTRY_DRAIN_TIMEOUT = std::chrono::seconds{1};
63
64 #if defined(DMK_ENABLE_TEST_SEAMS)
65 std::atomic<std::size_t> s_last_claimed_mid_slot{MID_ADAPTER_CAPACITY};
66 #endif
67 } // namespace
68
69 29502095 MidAdapterSlot *mid_adapter_slots() noexcept
70 {
71 29502095 return s_mid_slots;
72 }
73
74 29480074 DWORD mid_entry_tls_index() noexcept
75 {
76 29480074 return s_mid_entry_tls.index();
77 }
78
79 5261 bool thread_is_inside_mid_adapter(const MidAdapterSlot &slot) noexcept
80 {
81
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 5260 times.
10522 if (slot.untracked_entries.load(std::memory_order_seq_cst) != 0)
82 {
83 // An unrecorded entrant is in flight, so self-entry cannot be disproven. Claim it: a wrong "no"
84 // deadlocks while a wrong "yes" only pins.
85 1 return true;
86 }
87 5260 const DWORD tls = s_mid_entry_tls.index();
88
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 5260 times.
5260 if (tls == TLS_OUT_OF_INDEXES)
89 {
90 ✗ return false;
91 }
92
2/2
✓ Branch 19 → 16 taken 3 times.
✓ Branch 19 → 20 taken 5257 times.
5260 for (const auto *frame = static_cast<const MidEntryFrame *>(::TlsGetValue(tls)); frame != nullptr;
93 ✗ frame = frame->prev)
94 {
95
1/2
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 18 not taken.
3 if (frame->slot == &slot)
96 {
97 3 return true;
98 }
99 }
100 5257 return false;
101 }
102
103 17 void note_contained_mid_exception(MidAdapterSlot &slot) noexcept
104 {
105 // The counter is exact. The log fires only on a slot's first escape, so a per-frame exception cannot flood the
106 // host's hot path.
107 17 const std::uint64_t previous = slot.contained_exceptions.fetch_add(1, std::memory_order_relaxed);
108
2/2
✓ Branch 4 → 5 taken 15 times.
✓ Branch 4 → 6 taken 2 times.
17 if (previous != 0)
109 {
110 15 return;
111 }
112 2 (void)log().try_log(
113 LogLevel::Error,
114 "hook: a mid-hook callback at 0x{:0{}X} threw; the exception was contained at the DMK "
115 "adapter "
116 "boundary and the callback treated as complete. A mid-hook callback must not throw: the "
117 "backend stub it returns into adjusts the stack pointer dynamically and carries no unwind "
118 "data. Further escapes at this site are counted but not logged.",
119 2 slot.target.load(std::memory_order_relaxed),
120 4 sizeof(std::uintptr_t) * 2
121 );
122 }
123
124 5270 MidSlotClaim claim_mid_adapter_slot() noexcept
125 {
126
2/2
✓ Branch 23 → 3 taken 9370 times.
✓ Branch 23 → 24 taken 1 time.
9371 for (std::size_t index = 0; index < MID_ADAPTER_CAPACITY; ++index)
127 {
128 9370 bool expected = false;
129
2/2
✓ Branch 4 → 5 taken 5269 times.
✓ Branch 4 → 22 taken 4101 times.
9370 if (s_mid_slots[index].claimed.compare_exchange_strong(
130 expected,
131 true,
132 std::memory_order_acq_rel,
133 std::memory_order_relaxed
134 ))
135 {
136 // Reserved on the thread that installs the hook, so no callback reserves an index on a host thread.
137 5269 DWORD system_error = ERROR_SUCCESS;
138
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 10 taken 5268 times.
5269 if (s_mid_entry_tls.acquire(&system_error) == TLS_OUT_OF_INDEXES)
139 {
140 1 s_mid_entry_tls.release();
141 1 s_mid_slots[index].claimed.store(false, std::memory_order_release);
142 return MidSlotClaim{
143 .index = MID_ADAPTER_CAPACITY,
144 .status = MidSlotClaimStatus::EntryIndexUnavailable,
145 .system_error = system_error,
146 1 };
147 }
148 5268 s_mid_slots[index].detour.store(nullptr, std::memory_order_relaxed);
149 5268 s_mid_slots[index].live.store(false, std::memory_order_relaxed);
150 #if defined(DMK_ENABLE_TEST_SEAMS)
151 s_last_claimed_mid_slot.store(index, std::memory_order_release);
152 #endif
153 return MidSlotClaim{
154 .index = index,
155 .status = MidSlotClaimStatus::Claimed,
156 5268 };
157 }
158 }
159 1 return MidSlotClaim{};
160 }
161
162 10491 void release_mid_adapter_slot(std::size_t index) noexcept
163 {
164
2/2
✓ Branch 2 → 3 taken 5254 times.
✓ Branch 2 → 4 taken 5237 times.
10491 if (index >= MID_ADAPTER_CAPACITY)
165 {
166 5254 return;
167 }
168 // This path runs only after the slot is tombstoned and drained. No thread is inside its adapter, so the
169 // contents can be recycled. The slot storage itself is never reclaimed.
170 5237 s_mid_slots[index].detour.store(nullptr, std::memory_order_relaxed);
171 5237 s_mid_slots[index].claimed.store(false, std::memory_order_release);
172 5237 s_mid_entry_tls.release();
173 }
174
175 5252 MidRundown run_down_mid_slot(MidAdapterSlot &slot) noexcept
176 {
177
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 5248 times.
5252 if (thread_is_inside_mid_adapter(slot))
178 {
179 // This thread can itself be the in-flight entrant. The drain below then never observes zero. The caller
180 // pins instead of a wait.
181 4 return MidRundown::Unwaitable;
182 }
183 5248 return drain_until_zero(
184 16996 [&slot]() noexcept { return slot.callbacks_in_flight.load(std::memory_order_seq_cst); },
185 10496 std::chrono::steady_clock::now() + MID_CALLBACK_DRAIN_TIMEOUT
186 )
187
2/2
✓ Branch 8 → 9 taken 5247 times.
✓ Branch 8 → 10 taken 1 time.
5248 ? MidRundown::Drained
188 5248 : MidRundown::Expired;
189 }
190
191 5233 bool drain_mid_adapter_entries(MidAdapterSlot &slot) noexcept
192 {
193 5233 return drain_until_zero(
194 16265 [&slot]() noexcept { return slot.adapter_entries.load(std::memory_order_seq_cst); },
195 10466 std::chrono::steady_clock::now() + MID_ADAPTER_ENTRY_DRAIN_TIMEOUT
196 5233 );
197 }
198
199 #if defined(DMK_ENABLE_TEST_SEAMS)
200 39 std::size_t last_claimed_mid_slot_for_test() noexcept
201 {
202 39 return s_last_claimed_mid_slot.load(std::memory_order_acquire);
203 }
204
205 5 bool mid_slot_claimed_for_test(std::size_t index) noexcept
206 {
207
2/4
✓ Branch 2 → 3 taken 5 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 6 not taken.
5 return index < MID_ADAPTER_CAPACITY && s_mid_slots[index].claimed.load(std::memory_order_acquire);
208 }
209
210 4 void adjust_mid_adapter_entries_for_test(std::size_t index, std::int32_t delta) noexcept
211 {
212
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 4 times.
4 if (index >= MID_ADAPTER_CAPACITY)
213 {
214 ✗ return;
215 }
216 4 s_mid_slots[index].adapter_entries.fetch_add(static_cast<std::uint32_t>(delta), std::memory_order_seq_cst);
217 }
218 #endif
219
220 // The adapters instantiate here, in the object that defines their pool accessors, so each accessor call
221 // resolves in this TU and inlines in Release (EmitPathHasNoEmulatedTls pins this ownership).
222 constinit const std::array<safetyhook::MidHookFn, MID_ADAPTER_CAPACITY> MID_ADAPTER_TABLE =
223 make_mid_adapter_table(std::make_index_sequence<MID_ADAPTER_CAPACITY>{});
224 } // namespace DetourModKit::detail
225