./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
187 / 209
89.5%
Branches:
115 / 152
75.7%
Functions:
20 / 20
100.0%

src/internal/hook_ledger.cpp
Line Branch Exec Source
1 #include "internal/hook_ledger.hpp"
2
3 #include <algorithm>
4 #include <iterator>
5 #include <new>
6
7 namespace DetourModKit
8 {
9 namespace detail
10 {
11 #if defined(DMK_ENABLE_TEST_SEAMS)
12 void (*g_hook_ledger_lock_probe)() = nullptr;
13 #endif
14
15
2/2
✓ Branch 4 → 5 taken 2034 times.
✓ Branch 4 → 8 taken 1 time.
2036 HookLedger::HookLedger() : m_by_target(std::make_unique<std::unordered_map<std::uintptr_t, TargetEntry>>()) {}
16
17 1 HookLedger::HookLedger(InertTag) noexcept {}
18
19 104596 HookLedger &HookLedger::instance() noexcept
20 {
21 // Constructed once into static storage and never destroyed. The target map allocates, so a first-use
22 // failure publishes the inert ledger instead of a throw across this noexcept accessor.
23 alignas(HookLedger) static unsigned char storage[sizeof(HookLedger)];
24 2035 static HookLedger *const ledger = []() noexcept -> HookLedger *
25 {
26 try
27 {
28
4/6
✓ Branch 3 → 4 taken 2034 times.
✓ Branch 3 → 9 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2034 times.
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 11 not taken.
2035 return ::new (static_cast<void *>(storage)) HookLedger();
29 }
30 1 catch (...)
31 {
32
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 1 time.
1 return ::new (static_cast<void *>(storage)) HookLedger(InertTag{});
33 1 }
34
3/4
✓ Branch 2 → 3 taken 2035 times.
✓ Branch 2 → 8 taken 102561 times.
✓ Branch 4 → 5 taken 2035 times.
✗ Branch 4 → 8 not taken.
104596 }();
35 104596 return *ledger;
36 }
37
38 128769 std::unique_lock<std::mutex> HookLedger::lock_state() const noexcept
39 {
40 try
41 {
42 #if defined(DMK_ENABLE_TEST_SEAMS)
43
2/2
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 128748 times.
128769 if (auto *probe = g_hook_ledger_lock_probe)
44 {
45
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 7 taken 17 times.
21 probe();
46 }
47 #endif
48
1/2
✓ Branch 4 → 5 taken 128758 times.
✗ Branch 4 → 7 not taken.
128752 return std::unique_lock<std::mutex>(m_mutex);
49 }
50 17 catch (...)
51 {
52 17 return std::unique_lock<std::mutex>{};
53 17 }
54 }
55
56 19417 HookLedger::Reservation HookLedger::try_reserve_hook(std::uintptr_t target, bool refuse_if_hooked) noexcept
57 {
58
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 19413 times.
19417 if (inert())
59 {
60 1 return Reservation{ReserveStatus::OutOfMemory, 0, false};
61 }
62 19413 const std::uint64_t id = m_next_id.fetch_add(1, std::memory_order_relaxed);
63 try
64 {
65 19413 std::unique_lock<std::mutex> guard = lock_state();
66
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 19415 times.
19417 if (!guard.owns_lock())
67 {
68 2 return Reservation{ReserveStatus::OutOfMemory, 0, false};
69 }
70
1/2
✓ Branch 12 → 13 taken 19415 times.
✗ Branch 12 → 45 not taken.
19415 const auto it = m_by_target->find(target);
71 19415 const bool preexisting = (it != m_by_target->end());
72
4/4
✓ Branch 16 → 17 taken 4303 times.
✓ Branch 16 → 19 taken 15112 times.
✓ Branch 17 → 18 taken 9 times.
✓ Branch 17 → 19 taken 4294 times.
19415 if (preexisting && refuse_if_hooked)
73 {
74 // Exact same-kit duplicate: refuse without reserving. The id is skipped (ids are a monotonic
75 // counter with no reuse requirement).
76 9 return Reservation{ReserveStatus::AlreadyHooked, 0, true};
77 }
78 // No fixed ceiling is needed: `order` is bounded by live backend hooks, and `pending` by concurrent
79 // installer threads. Capping either would refuse legitimate layering without limiting an external
80 // event-rate backlog.
81
2/2
✓ Branch 19 → 20 taken 4294 times.
✓ Branch 19 → 26 taken 15112 times.
19406 if (preexisting)
82 {
83 // Each push_back gives the strong guarantee, and the catch below drops the id from every list.
84
1/2
✓ Branch 21 → 22 taken 4294 times.
✗ Branch 21 → 45 not taken.
4294 it->second.order.push_back(id);
85
1/2
✓ Branch 23 → 24 taken 4294 times.
✗ Branch 23 → 45 not taken.
4294 it->second.pending.push_back(id);
86
1/2
✓ Branch 25 → 35 taken 4294 times.
✗ Branch 25 → 45 not taken.
4294 it->second.uncommitted.push_back(id);
87 }
88 else
89 {
90 // Build the entry fully before publishing it, so a throwing allocation never leaves an empty entry
91 // in the map (which is_target_hooked would misread as a live hook).
92 15112 TargetEntry entry;
93
2/2
✓ Branch 26 → 27 taken 15107 times.
✓ Branch 26 → 42 taken 5 times.
15112 entry.order.push_back(id);
94
2/2
✓ Branch 27 → 28 taken 15102 times.
✓ Branch 27 → 42 taken 5 times.
15107 entry.pending.push_back(id);
95
2/2
✓ Branch 28 → 29 taken 15097 times.
✓ Branch 28 → 42 taken 5 times.
15102 entry.uncommitted.push_back(id);
96
2/2
✓ Branch 32 → 33 taken 15092 times.
✓ Branch 32 → 41 taken 5 times.
30194 m_by_target->emplace(target, std::move(entry));
97 15112 }
98
99
1/2
✓ Branch 35 → 36 taken 19386 times.
✗ Branch 35 → 45 not taken.
19386 m_install_cv.wait(
100 guard,
101 20854 [this, target, id]
102 {
103
1/2
✓ Branch 3 → 4 taken 20854 times.
✗ Branch 3 → 18 not taken.
20854 const auto current = m_by_target->find(target);
104
2/4
✓ Branch 7 → 8 taken 20854 times.
✗ Branch 7 → 15 not taken.
✓ Branch 10 → 11 taken 20854 times.
✗ Branch 10 → 15 not taken.
41708 return current != m_by_target->end() && !current->second.pending.empty() &&
105
2/2
✓ Branch 13 → 14 taken 19386 times.
✓ Branch 13 → 15 taken 1468 times.
41708 current->second.pending.front() == id;
106 }
107 );
108 19386 return Reservation{ReserveStatus::Reserved, id, preexisting};
109 19417 }
110 20 catch (...)
111 {
112 20 (void)release_hook(target, id);
113 20 return Reservation{ReserveStatus::OutOfMemory, 0, false};
114 20 }
115 }
116
117 15032 bool HookLedger::commit_hook(std::uintptr_t target, std::uint64_t id) noexcept
118 {
119
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 15032 times.
15032 if (inert())
120 {
121 ✗ return false;
122 }
123 15032 std::unique_lock<std::mutex> guard = lock_state();
124
2/2
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 15030 times.
15032 if (!guard.owns_lock())
125 {
126 2 return false;
127 }
128 15030 auto it = m_by_target->find(target);
129
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 15030 times.
15030 if (it == m_by_target->end())
130 {
131 ✗ return false;
132 }
133 15030 std::vector<std::uint64_t> &pending = it->second.pending;
134 15030 const auto found = std::find(pending.begin(), pending.end(), id);
135
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 15030 times.
30060 if (found == pending.end())
136 {
137 ✗ return false;
138 }
139 15030 pending.erase(found);
140 15030 std::erase(it->second.uncommitted, id);
141 15030 m_install_cv.notify_all();
142 15030 return true;
143 15032 }
144
145 4236 bool HookLedger::commit_hook_keep_slot(std::uintptr_t target, std::uint64_t id) noexcept
146 {
147
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 4236 times.
4236 if (inert())
148 {
149 ✗ return false;
150 }
151 4236 std::unique_lock<std::mutex> guard = lock_state();
152
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 4235 times.
4236 if (!guard.owns_lock())
153 {
154 1 return false;
155 }
156 4235 const auto it = m_by_target->find(target);
157
7/8
✓ Branch 14 → 15 taken 4234 times.
✓ Branch 14 → 21 taken 1 time.
✓ Branch 17 → 18 taken 4234 times.
✗ Branch 17 → 21 not taken.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 4233 times.
✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 25 taken 4233 times.
4235 if (it == m_by_target->end() || it->second.pending.empty() || it->second.pending.front() != id)
158 {
159 2 return false;
160 }
161 4233 std::erase(it->second.uncommitted, id);
162 4233 return true;
163 4236 }
164
165 18589 std::size_t HookLedger::release_hook(std::uintptr_t target, std::uint64_t id) noexcept
166 {
167
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 18589 times.
18589 if (inert())
168 {
169 ✗ return SLOT_UNAVAILABLE;
170 }
171 18589 std::unique_lock<std::mutex> guard = lock_state();
172
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 18588 times.
18589 if (!guard.owns_lock())
173 {
174 // Without the lock the newer-live count is unknowable. The positive sentinel makes a teardown caller
175 // leak its backend rather than restore bytes that a newer layer can still depend on.
176 1 return SLOT_UNAVAILABLE;
177 }
178 18588 auto it = m_by_target->find(target);
179
2/2
✓ Branch 14 → 15 taken 20 times.
✓ Branch 14 → 16 taken 18568 times.
18588 if (it == m_by_target->end())
180 {
181 20 return 0;
182 }
183 18568 std::vector<std::uint64_t> &pending = it->second.pending;
184 18568 const auto pending_found = std::find(pending.begin(), pending.end(), id);
185
2/2
✓ Branch 27 → 28 taken 16138 times.
✓ Branch 27 → 33 taken 2430 times.
37136 if (pending_found != pending.end())
186 {
187 16138 pending.erase(pending_found);
188 }
189 18568 std::erase(it->second.uncommitted, id);
190
191 18568 std::vector<std::uint64_t> &order = it->second.order;
192 18568 const auto found = std::find(order.begin(), order.end(), id);
193
1/2
✗ Branch 46 → 47 not taken.
✓ Branch 46 → 49 taken 18568 times.
37136 if (found == order.end())
194 {
195 ✗ m_install_cv.notify_all();
196 ✗ return 0;
197 }
198 // Entries after this id (toward the back) were created later: they are the newer layers still live.
199 55704 const std::size_t newer = static_cast<std::size_t>(std::distance(std::next(found), order.end()));
200 18568 order.erase(found);
201
2/2
✓ Branch 73 → 74 taken 14391 times.
✓ Branch 73 → 76 taken 4177 times.
18568 if (order.empty())
202 {
203 14391 m_by_target->erase(it);
204 }
205 18568 m_install_cv.notify_all();
206 18568 return newer;
207 18589 }
208
209 33661 std::size_t HookLedger::acquire_target_slot(std::uintptr_t target, std::uint64_t id) noexcept
210 {
211
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 33660 times.
33661 if (inert())
212 {
213 ✗ return SLOT_UNAVAILABLE;
214 }
215 33660 std::unique_lock<std::mutex> guard = lock_state();
216
2/2
✓ Branch 7 → 8 taken 3 times.
✓ Branch 7 → 9 taken 33659 times.
33662 if (!guard.owns_lock())
217 {
218 3 return SLOT_UNAVAILABLE;
219 }
220 33659 auto it = m_by_target->find(target);
221
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 33658 times.
33659 if (it == m_by_target->end())
222 {
223 // Without the ledger entry there is no serialization guarantee. Fail closed.
224 1 return SLOT_UNAVAILABLE;
225 }
226 33658 const auto order_found = std::find(it->second.order.begin(), it->second.order.end(), id);
227
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 31 taken 33657 times.
67316 if (order_found == it->second.order.end())
228 {
229 // An unknown id cannot safely claim this target's write right. Fail closed.
230 1 return SLOT_UNAVAILABLE;
231 }
232 try
233 {
234
1/2
✓ Branch 32 → 33 taken 33657 times.
✗ Branch 32 → 77 not taken.
33657 it->second.pending.push_back(id);
235 }
236 ✗ catch (...)
237 {
238 // The slot claim failed. The sentinel makes the caller refuse or leak rather than write bytes
239 // without the guarantee. The id was never queued, so nothing leaks.
240 ✗ return SLOT_UNAVAILABLE;
241 ✗ }
242 // Wait our turn behind any installer already mid-patch on this target (front of pending), exactly as an
243 // installer does. Once this id is front, no install is touching the target's prologue.
244 33657 m_install_cv.wait(
245 guard,
246 37348 [this, target, id]
247 {
248
1/2
✓ Branch 3 → 4 taken 37348 times.
✗ Branch 3 → 18 not taken.
37348 const auto current = m_by_target->find(target);
249
2/4
✓ Branch 7 → 8 taken 37348 times.
✗ Branch 7 → 15 not taken.
✓ Branch 10 → 11 taken 37348 times.
✗ Branch 10 → 15 not taken.
74696 return current != m_by_target->end() && !current->second.pending.empty() &&
250
2/2
✓ Branch 13 → 14 taken 33657 times.
✓ Branch 13 → 15 taken 3691 times.
74696 current->second.pending.front() == id;
251 }
252 );
253 // Re-find under the still-held lock (a concurrent emplace/erase may have rehashed the map while we waited)
254 // and measure the newer-live count at the instant the slot is owned.
255 33657 const auto current = m_by_target->find(target);
256
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 41 taken 33657 times.
33657 if (current == m_by_target->end())
257 {
258 ✗ return SLOT_UNAVAILABLE;
259 }
260 33657 const std::vector<std::uint64_t> &order = current->second.order;
261 33657 const auto found = std::find(order.begin(), order.end(), id);
262
1/2
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 33657 times.
67314 if (found == order.end())
263 {
264 ✗ return SLOT_UNAVAILABLE;
265 }
266 100971 return static_cast<std::size_t>(std::distance(std::next(found), order.end()));
267 33662 }
268
269 21879 void HookLedger::release_target_slot(std::uintptr_t target, std::uint64_t id) noexcept
270 {
271
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 21879 times.
21879 if (inert())
272 {
273 4 return;
274 }
275 21879 std::unique_lock<std::mutex> guard = lock_state();
276
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 9 taken 21875 times.
21879 if (!guard.owns_lock())
277 {
278 4 return;
279 }
280 21875 auto it = m_by_target->find(target);
281
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 21875 times.
21875 if (it == m_by_target->end())
282 {
283 ✗ return;
284 }
285 21875 std::vector<std::uint64_t> &pending = it->second.pending;
286 21875 const auto found = std::find(pending.begin(), pending.end(), id);
287
1/2
✓ Branch 27 → 28 taken 21875 times.
✗ Branch 27 → 33 not taken.
43750 if (found != pending.end())
288 {
289 21875 pending.erase(found);
290 }
291 21875 m_install_cv.notify_all();
292
2/2
✓ Branch 36 → 37 taken 21875 times.
✓ Branch 36 → 39 taken 4 times.
21879 }
293
294 15336 std::uint64_t HookLedger::promote_to_newest(std::uintptr_t target, std::uint64_t id) noexcept
295 {
296
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 15336 times.
15336 if (inert())
297 {
298 ✗ return 0;
299 }
300 15336 std::unique_lock<std::mutex> guard = lock_state();
301
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 15336 times.
15336 if (!guard.owns_lock())
302 {
303 ✗ return 0;
304 }
305 15336 auto it = m_by_target->find(target);
306
2/2
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 15334 times.
15336 if (it == m_by_target->end())
307 {
308 2 return 0;
309 }
310 15334 std::vector<std::uint64_t> &order = it->second.order;
311 15334 const auto found = std::find(order.begin(), order.end(), id);
312
1/2
✗ Branch 27 → 28 not taken.
✓ Branch 27 → 29 taken 15334 times.
30668 if (found == order.end())
313 {
314 ✗ return 0;
315 }
316 15334 const std::vector<std::uint64_t> &uncommitted = it->second.uncommitted;
317 const auto first_uncommitted =
318 30668 std::find_first_of(std::next(found), order.end(), uncommitted.begin(), uncommitted.end());
319 15334 std::rotate(found, std::next(found), first_uncommitted);
320 30668 return m_next_id.fetch_add(1, std::memory_order_relaxed);
321 15336 }
322
323 230 bool HookLedger::is_target_hooked(std::uintptr_t target) const noexcept
324 {
325
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 228 times.
230 if (inert())
326 {
327 2 return true;
328 }
329 228 std::unique_lock<std::mutex> guard = lock_state();
330
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 227 times.
228 if (!guard.owns_lock())
331 {
332 // Fail closed: report the target as hooked so a fail_if_already_hooked install refuses rather than
333 // patching over a layer this instance cannot currently see.
334 1 return true;
335 }
336 227 return m_by_target->find(target) != m_by_target->end();
337 228 }
338
339 111 std::optional<std::uint64_t> HookLedger::try_record_vmt(std::uintptr_t cloned_base) noexcept
340 {
341
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 111 times.
111 if (inert())
342 {
343 ✗ return std::nullopt;
344 }
345 111 const std::uint64_t id = m_next_id.fetch_add(1, std::memory_order_relaxed);
346 try
347 {
348 111 std::unique_lock<std::mutex> guard = lock_state();
349
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 12 taken 110 times.
111 if (!guard.owns_lock())
350 {
351 1 return std::nullopt;
352 }
353 // push_back gives the strong guarantee, so a failed growth leaves m_vmt unchanged.
354
1/2
✓ Branch 12 → 13 taken 110 times.
✗ Branch 12 → 21 not taken.
110 m_vmt.push_back(VmtEntry{id, cloned_base});
355
2/2
✓ Branch 15 → 16 taken 110 times.
✓ Branch 15 → 18 taken 1 time.
111 }
356 ✗ catch (...)
357 {
358 ✗ return std::nullopt;
359 ✗ }
360 110 return id;
361 }
362
363 104 void HookLedger::release_vmt(std::uint64_t id) noexcept
364 {
365
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 104 times.
104 if (inert())
366 {
367 1 return;
368 }
369 104 std::unique_lock<std::mutex> guard = lock_state();
370
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 103 times.
104 if (!guard.owns_lock())
371 {
372 1 return;
373 }
374 245 std::erase_if(m_vmt, [id](const VmtEntry &entry) { return entry.id == id; });
375
2/2
✓ Branch 12 → 13 taken 103 times.
✓ Branch 12 → 15 taken 1 time.
104 }
376
377 182 bool HookLedger::is_vmt_clone_base(std::uintptr_t vptr) const noexcept
378 {
379
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 181 times.
182 if (vptr == 0)
380 {
381 1 return false;
382 }
383
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 181 times.
181 if (inert())
384 {
385 ✗ return true;
386 }
387 181 std::unique_lock<std::mutex> guard = lock_state();
388
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 180 times.
181 if (!guard.owns_lock())
389 {
390 // Fail closed: report the vptr as one of this kit's clone bases so the caller refuses or warns rather
391 // than silently baking another handle's hooked slots into its own pristine snapshot.
392 1 return true;
393 }
394 180 return std::any_of(
395 m_vmt.begin(),
396 m_vmt.end(),
397 99 [vptr](const VmtEntry &entry) { return entry.base == vptr; }
398 180 );
399 181 }
400 } // namespace detail
401 } // namespace DetourModKit
402