GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 94.7% 71 / 0 / 75
Functions: 100.0% 16 / 0 / 16
Branches: 82.4% 28 / 0 / 34

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
10 #include "DetourModKit/logger.hpp"
11
12 #include <windows.h>
13
14 #include <atomic>
15 #include <chrono>
16 #include <cstddef>
17 #include <cstdint>
18
19 namespace DetourModKit::detail
20 {
21 #if defined(DMK_ENABLE_TEST_SEAMS)
22 // This probe fires inside the mid-hook adapter between the fast-path live check and callback commit. See its
23 // declaration in internal/mid_hook_adapter.hpp for the race it exists to make reachable.
24 void (*g_mid_adapter_precommit_probe)() noexcept = nullptr;
25 // Selects one thread whose adapter entry-chain store reports failure. See its declaration in
26 // internal/mid_hook_adapter.hpp for the platform condition it stands in for.
27 std::atomic<std::uint32_t> g_mid_entry_store_failure_thread{0};
28 std::atomic<std::uint64_t> g_mid_entry_store_failure_hits{0};
29
30 24 void set_mid_route_park_for_test(MidRouteParkStage stage) noexcept
31 {
32 24 safetyhook::RouteParkStage backend_stage = safetyhook::RouteParkStage::NONE;
33
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 21 times.
24 if (stage == MidRouteParkStage::BeforeAdapter)
34 {
35 3 backend_stage = safetyhook::RouteParkStage::BEFORE_DESTINATION;
36 }
37
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 19 times.
21 else if (stage == MidRouteParkStage::AfterAdapter)
38 {
39 2 backend_stage = safetyhook::RouteParkStage::BEFORE_EXIT;
40 }
41 24 safetyhook::set_route_park_for_test(backend_stage);
42 24 }
43
44 9642 bool mid_route_park_reached_for_test() noexcept
45 {
46 9642 return safetyhook::route_park_reached_for_test();
47 }
48 #endif
49
50 // The mid-hook dispatch pool uses constant initialization and trivial destruction. It registers no destructor, so
51 // it outlives an adapter that remains active during static destruction.
52 namespace
53 {
54 MidAdapterSlot s_mid_slots[MID_ADAPTER_CAPACITY];
55 std::atomic<DWORD> s_mid_entry_tls{TLS_OUT_OF_INDEXES};
56
57 /// Defines the committed callback wait bound. Expiry pins the backend instead of a hang (MidHookDrainTest).
58 constexpr auto MID_CALLBACK_DRAIN_TIMEOUT = std::chrono::seconds{5};
59
60 /// Defines the post-restore adapter-body drain bound. Expiry retains the slot and stub (MidHookDrainTest).
61 constexpr auto MID_ADAPTER_ENTRY_DRAIN_TIMEOUT = std::chrono::seconds{1};
62
63 #if defined(DMK_ENABLE_TEST_SEAMS)
64 std::atomic<std::size_t> s_last_claimed_mid_slot{MID_ADAPTER_CAPACITY};
65 #endif
66 } // namespace
67
68 1944 MidAdapterSlot *mid_adapter_slots() noexcept
69 {
70 1944 return s_mid_slots;
71 }
72
73 60 std::atomic<DWORD> &mid_entry_tls_index() noexcept
74 {
75 60 return s_mid_entry_tls;
76 }
77
78 229 bool ensure_mid_entry_tls() noexcept
79 {
80
2/2
✓ Branch 9 → 10 taken 170 times.
✓ Branch 9 → 11 taken 59 times.
229 if (s_mid_entry_tls.load(std::memory_order_acquire) != TLS_OUT_OF_INDEXES)
81 {
82 170 return true;
83 }
84 59 const DWORD fresh = ::TlsAlloc();
85
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 59 times.
59 if (fresh == TLS_OUT_OF_INDEXES)
86 {
87 return false;
88 }
89 59 DWORD expected = TLS_OUT_OF_INDEXES;
90 59 if (!s_mid_entry_tls
91
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 59 times.
59 .compare_exchange_strong(expected, fresh, std::memory_order_acq_rel, std::memory_order_acquire))
92 {
93 // Another installer won. Its index is already public, and this slot contains no value.
94 ::TlsFree(fresh);
95 }
96 59 return true;
97 }
98
99 220 bool thread_is_inside_mid_adapter(const MidAdapterSlot &slot) noexcept
100 {
101
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 219 times.
440 if (slot.untracked_entries.load(std::memory_order_seq_cst) != 0)
102 {
103 // An unrecorded entrant is in flight, so self-entry cannot be disproven. Claim it: a wrong "no"
104 // deadlocks while a wrong "yes" only pins.
105 1 return true;
106 }
107 219 const DWORD tls = s_mid_entry_tls.load(std::memory_order_acquire);
108
2/2
✓ Branch 23 → 20 taken 1 time.
✓ Branch 23 → 24 taken 218 times.
219 for (const auto *frame = static_cast<const MidEntryFrame *>(::TlsGetValue(tls)); frame != nullptr;
109 frame = frame->prev)
110 {
111
1/2
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 22 not taken.
1 if (frame->slot == &slot)
112 {
113 1 return true;
114 }
115 }
116 218 return false;
117 }
118
119 17 void note_contained_mid_exception(MidAdapterSlot &slot) noexcept
120 {
121 // The counter is exact. The log fires only on a slot's first escape, so a per-frame exception cannot flood the
122 // host's hot path.
123 17 const std::uint64_t previous = slot.contained_exceptions.fetch_add(1, std::memory_order_relaxed);
124
2/2
✓ Branch 4 → 5 taken 15 times.
✓ Branch 4 → 6 taken 2 times.
17 if (previous != 0)
125 {
126 15 return;
127 }
128 2 (void)log().try_log(
129 LogLevel::Error,
130 "hook: a mid-hook callback at 0x{:0{}X} threw; the exception was contained at the DMK "
131 "adapter "
132 "boundary and the callback treated as complete. A mid-hook callback must not throw: the "
133 "backend stub it returns into adjusts the stack pointer dynamically and carries no unwind "
134 "data. Further escapes at this site are counted but not logged.",
135 2 slot.target.load(std::memory_order_relaxed),
136 4 sizeof(std::uintptr_t) * 2
137 );
138 }
139
140 228 std::size_t claim_mid_adapter_slot() noexcept
141 {
142
2/2
✓ Branch 17 → 3 taken 2309 times.
✓ Branch 17 → 18 taken 1 time.
2310 for (std::size_t index = 0; index < MID_ADAPTER_CAPACITY; ++index)
143 {
144 2309 bool expected = false;
145
2/2
✓ Branch 4 → 5 taken 227 times.
✓ Branch 4 → 16 taken 2082 times.
2309 if (s_mid_slots[index].claimed.compare_exchange_strong(
146 expected,
147 true,
148 std::memory_order_acq_rel,
149 std::memory_order_relaxed
150 ))
151 {
152 227 s_mid_slots[index].detour.store(nullptr, std::memory_order_relaxed);
153 227 s_mid_slots[index].live.store(false, std::memory_order_relaxed);
154 #if defined(DMK_ENABLE_TEST_SEAMS)
155 s_last_claimed_mid_slot.store(index, std::memory_order_release);
156 #endif
157 227 return index;
158 }
159 }
160 1 return MID_ADAPTER_CAPACITY;
161 }
162
163 437 void release_mid_adapter_slot(std::size_t index) noexcept
164 {
165
2/2
✓ Branch 2 → 3 taken 221 times.
✓ Branch 2 → 4 taken 216 times.
437 if (index >= MID_ADAPTER_CAPACITY)
166 {
167 221 return;
168 }
169 // This path runs only after the slot is tombstoned and drained. No thread is inside its adapter, so the
170 // contents can be recycled. The slot storage itself is never reclaimed.
171 216 s_mid_slots[index].detour.store(nullptr, std::memory_order_relaxed);
172 216 s_mid_slots[index].claimed.store(false, std::memory_order_release);
173 }
174
175 220 MidRundown run_down_mid_slot(MidAdapterSlot &slot) noexcept
176 {
177
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 218 times.
220 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 2 return MidRundown::Unwaitable;
182 }
183 218 return drain_until_zero(
184 1908 [&slot]() noexcept { return slot.callbacks_in_flight.load(std::memory_order_seq_cst); },
185 436 std::chrono::steady_clock::now() + MID_CALLBACK_DRAIN_TIMEOUT
186 )
187
2/2
✓ Branch 8 → 9 taken 217 times.
✓ Branch 8 → 10 taken 1 time.
218 ? MidRundown::Drained
188 218 : MidRundown::Expired;
189 }
190
191 211 bool drain_mid_adapter_entries(MidAdapterSlot &slot) noexcept
192 {
193 211 return drain_until_zero(
194 889 [&slot]() noexcept { return slot.adapter_entries.load(std::memory_order_seq_cst); },
195 422 std::chrono::steady_clock::now() + MID_ADAPTER_ENTRY_DRAIN_TIMEOUT
196 211 );
197 }
198
199 #if defined(DMK_ENABLE_TEST_SEAMS)
200 3 std::size_t last_claimed_mid_slot_for_test() noexcept
201 {
202 3 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 2 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 2 times.
2 if (index >= MID_ADAPTER_CAPACITY)
213 {
214 return;
215 }
216 2 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