GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.3% 137 / 0 / 150
Functions: 100.0% 15 / 0 / 15
Branches: 77.5% 79 / 0 / 102

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 16866 HookLedger &HookLedger::instance() noexcept
16 {
17 // Constructed once into static storage and never destroyed. Default construction is noexcept and leaves
18 // the containers empty, so first use requires no bookkeeping allocation.
19 alignas(HookLedger) static unsigned char storage[sizeof(HookLedger)];
20
4/6
✓ Branch 2 → 3 taken 378 times.
✓ Branch 2 → 10 taken 16488 times.
✓ Branch 4 → 5 taken 378 times.
✗ Branch 4 → 10 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 378 times.
16866 static HookLedger *const ledger = ::new (static_cast<void *>(storage)) HookLedger();
21 16866 return *ledger;
22 }
23
24 24656 std::unique_lock<std::mutex> HookLedger::lock_state() const noexcept
25 {
26 try
27 {
28 #if defined(DMK_ENABLE_TEST_SEAMS)
29
2/2
✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 4 taken 24638 times.
24656 if (auto *probe = g_hook_ledger_lock_probe)
30 {
31
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 16 times.
18 probe();
32 }
33 #endif
34
1/2
✓ Branch 4 → 5 taken 24642 times.
✗ Branch 4 → 7 not taken.
24640 return std::unique_lock<std::mutex>(m_mutex);
35 }
36 16 catch (...)
37 {
38 16 return std::unique_lock<std::mutex>{};
39 16 }
40 }
41
42 2980 HookLedger::Reservation HookLedger::try_reserve_hook(std::uintptr_t target, bool refuse_if_hooked) noexcept
43 {
44 2980 const std::uint64_t id = m_next_id.fetch_add(1, std::memory_order_relaxed);
45 try
46 {
47 2980 std::unique_lock<std::mutex> guard = lock_state();
48
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 2978 times.
2980 if (!guard.owns_lock())
49 {
50 2 return Reservation{ReserveStatus::OutOfMemory, 0, false};
51 }
52
1/2
✓ Branch 8 → 9 taken 2978 times.
✗ Branch 8 → 36 not taken.
2978 const auto it = m_by_target.find(target);
53 2978 const bool preexisting = (it != m_by_target.end());
54
4/4
✓ Branch 11 → 12 taken 41 times.
✓ Branch 11 → 14 taken 2937 times.
✓ Branch 12 → 13 taken 10 times.
✓ Branch 12 → 14 taken 31 times.
2978 if (preexisting && refuse_if_hooked)
55 {
56 // Exact same-kit duplicate: refuse without reserving. The id is skipped (ids are a monotonic
57 // counter with no reuse requirement).
58 10 return Reservation{ReserveStatus::AlreadyHooked, 0, true};
59 }
60 // No fixed ceiling is needed: `order` is bounded by live backend hooks, and `pending` by concurrent
61 // installer threads. Capping either would refuse legitimate layering without limiting an external
62 // event-rate backlog.
63
2/2
✓ Branch 14 → 15 taken 31 times.
✓ Branch 14 → 19 taken 2937 times.
2968 if (preexisting)
64 {
65 // push_back gives the strong guarantee, so a failed growth leaves the existing order unchanged.
66
1/2
✓ Branch 16 → 17 taken 31 times.
✗ Branch 16 → 36 not taken.
31 it->second.order.push_back(id);
67
1/2
✓ Branch 18 → 26 taken 31 times.
✗ Branch 18 → 36 not taken.
31 it->second.pending.push_back(id);
68 }
69 else
70 {
71 // Build the entry fully before publishing it, so a throwing allocation never leaves an empty entry
72 // in the map (which is_target_hooked would misread as a live hook).
73 2937 TargetEntry entry;
74
2/2
✓ Branch 19 → 20 taken 2935 times.
✓ Branch 19 → 33 taken 2 times.
2937 entry.order.push_back(id);
75
2/2
✓ Branch 20 → 21 taken 2933 times.
✓ Branch 20 → 33 taken 2 times.
2935 entry.pending.push_back(id);
76
2/2
✓ Branch 23 → 24 taken 2931 times.
✓ Branch 23 → 32 taken 2 times.
5866 m_by_target.emplace(target, std::move(entry));
77 2937 }
78
79
1/2
✓ Branch 26 → 27 taken 2962 times.
✗ Branch 26 → 36 not taken.
2962 m_install_cv.wait(
80 guard,
81 2974 [this, target, id]
82 {
83
1/2
✓ Branch 2 → 3 taken 2974 times.
✗ Branch 2 → 16 not taken.
2974 const auto current = m_by_target.find(target);
84
2/4
✓ Branch 5 → 6 taken 2974 times.
✗ Branch 5 → 13 not taken.
✓ Branch 8 → 9 taken 2974 times.
✗ Branch 8 → 13 not taken.
5948 return current != m_by_target.end() && !current->second.pending.empty() &&
85
2/2
✓ Branch 11 → 12 taken 2962 times.
✓ Branch 11 → 13 taken 12 times.
5948 current->second.pending.front() == id;
86 }
87 );
88 2962 return Reservation{ReserveStatus::Reserved, id, preexisting};
89 2980 }
90 6 catch (...)
91 {
92 6 (void)release_hook(target, id);
93 6 return Reservation{ReserveStatus::OutOfMemory, 0, false};
94 6 }
95 }
96
97 2930 bool HookLedger::commit_hook(std::uintptr_t target, std::uint64_t id) noexcept
98 {
99 2930 std::unique_lock<std::mutex> guard = lock_state();
100
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 2928 times.
2930 if (!guard.owns_lock())
101 {
102 2 return false;
103 }
104 2928 auto it = m_by_target.find(target);
105
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2928 times.
2928 if (it == m_by_target.end())
106 {
107 return false;
108 }
109 2928 std::vector<std::uint64_t> &pending = it->second.pending;
110 2928 const auto found = std::find(pending.begin(), pending.end(), id);
111
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2928 times.
5856 if (found == pending.end())
112 {
113 return false;
114 }
115 2928 pending.erase(found);
116 2928 m_install_cv.notify_all();
117 2928 return true;
118 2930 }
119
120 2937 std::size_t HookLedger::release_hook(std::uintptr_t target, std::uint64_t id) noexcept
121 {
122 2937 std::unique_lock<std::mutex> guard = lock_state();
123
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 2937 times.
2938 if (!guard.owns_lock())
124 {
125 // Without the lock the newer-live count is unknowable. Fail closed to a positive count so a teardown
126 // caller leaks its backend rather than restoring bytes a newer layer may still depend on.
127 1 return 1;
128 }
129 2937 auto it = m_by_target.find(target);
130
2/2
✓ Branch 9 → 10 taken 6 times.
✓ Branch 9 → 11 taken 2931 times.
2937 if (it == m_by_target.end())
131 {
132 6 return 0;
133 }
134 2931 std::vector<std::uint64_t> &pending = it->second.pending;
135 2931 const auto pending_found = std::find(pending.begin(), pending.end(), id);
136
2/2
✓ Branch 22 → 23 taken 510 times.
✓ Branch 22 → 28 taken 2421 times.
5862 if (pending_found != pending.end())
137 {
138 510 pending.erase(pending_found);
139 }
140
141 2931 std::vector<std::uint64_t> &order = it->second.order;
142 2931 const auto found = std::find(order.begin(), order.end(), id);
143
1/2
✗ Branch 39 → 40 not taken.
✓ Branch 39 → 42 taken 2931 times.
5862 if (found == order.end())
144 {
145 m_install_cv.notify_all();
146 return 0;
147 }
148 // Entries after this id (toward the back) were created later: they are the newer layers still live.
149 8793 const std::size_t newer = static_cast<std::size_t>(std::distance(std::next(found), order.end()));
150 2931 order.erase(found);
151
2/2
✓ Branch 66 → 67 taken 2903 times.
✓ Branch 66 → 68 taken 28 times.
2931 if (order.empty())
152 {
153 2903 m_by_target.erase(it);
154 }
155 2931 m_install_cv.notify_all();
156 2931 return newer;
157 2938 }
158
159 7899 std::size_t HookLedger::acquire_target_slot(std::uintptr_t target, std::uint64_t id) noexcept
160 {
161 7899 std::unique_lock<std::mutex> guard = lock_state();
162
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 7896 times.
7899 if (!guard.owns_lock())
163 {
164 3 return 1;
165 }
166 7896 auto it = m_by_target.find(target);
167
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 11 taken 7895 times.
7896 if (it == m_by_target.end())
168 {
169 // Without the ledger entry there is no serialization guarantee. Fail closed.
170 1 return 1;
171 }
172 7895 const auto order_found = std::find(it->second.order.begin(), it->second.order.end(), id);
173
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 7894 times.
15790 if (order_found == it->second.order.end())
174 {
175 // An unknown id cannot safely claim this target's write right. Fail closed.
176 1 return 1;
177 }
178 try
179 {
180
1/2
✓ Branch 27 → 28 taken 7894 times.
✗ Branch 27 → 70 not taken.
7894 it->second.pending.push_back(id);
181 }
182 catch (...)
183 {
184 // Could not claim the slot. Fail closed: report a positive count so the caller refuses or leaks rather
185 // than writing bytes without the guarantee. The id was never queued, so nothing leaks.
186 return 1;
187 }
188 // Wait our turn behind any installer already mid-patch on this target (front of pending), exactly as an
189 // installer does. Once this id is front, no install is touching the target's prologue.
190 7894 m_install_cv.wait(
191 guard,
192 7894 [this, target, id]
193 {
194
1/2
✓ Branch 2 → 3 taken 7894 times.
✗ Branch 2 → 16 not taken.
7894 const auto current = m_by_target.find(target);
195
2/4
✓ Branch 5 → 6 taken 7894 times.
✗ Branch 5 → 13 not taken.
✓ Branch 8 → 9 taken 7894 times.
✗ Branch 8 → 13 not taken.
15788 return current != m_by_target.end() && !current->second.pending.empty() &&
196
1/2
✓ Branch 11 → 12 taken 7894 times.
✗ Branch 11 → 13 not taken.
15788 current->second.pending.front() == id;
197 }
198 );
199 // Re-find under the still-held lock (a concurrent emplace/erase may have rehashed the map while we waited)
200 // and measure the newer-live count at the instant the slot is owned.
201 7894 const auto current = m_by_target.find(target);
202
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 7894 times.
7894 if (current == m_by_target.end())
203 {
204 return 1;
205 }
206 7894 const std::vector<std::uint64_t> &order = current->second.order;
207 7894 const auto found = std::find(order.begin(), order.end(), id);
208
1/2
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 7894 times.
15788 if (found == order.end())
209 {
210 return 1;
211 }
212 23682 return static_cast<std::size_t>(std::distance(std::next(found), order.end()));
213 7899 }
214
215 7422 void HookLedger::release_target_slot(std::uintptr_t target, std::uint64_t id) noexcept
216 {
217 7422 std::unique_lock<std::mutex> guard = lock_state();
218
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 7418 times.
7422 if (!guard.owns_lock())
219 {
220 4 return;
221 }
222 7418 auto it = m_by_target.find(target);
223
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 7418 times.
7418 if (it == m_by_target.end())
224 {
225 return;
226 }
227 7418 std::vector<std::uint64_t> &pending = it->second.pending;
228 7418 const auto found = std::find(pending.begin(), pending.end(), id);
229
1/2
✓ Branch 22 → 23 taken 7418 times.
✗ Branch 22 → 28 not taken.
14836 if (found != pending.end())
230 {
231 7418 pending.erase(found);
232 }
233 7418 m_install_cv.notify_all();
234
2/2
✓ Branch 31 → 32 taken 7418 times.
✓ Branch 31 → 34 taken 4 times.
7422 }
235
236 102 bool HookLedger::is_target_hooked(std::uintptr_t target) const noexcept
237 {
238 102 std::unique_lock<std::mutex> guard = lock_state();
239
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 101 times.
102 if (!guard.owns_lock())
240 {
241 // Fail closed: report the target as hooked so a fail_if_already_hooked install refuses rather than
242 // patching over a layer this instance cannot currently see.
243 1 return true;
244 }
245 101 return m_by_target.find(target) != m_by_target.end();
246 102 }
247
248 108 std::optional<std::uint64_t> HookLedger::try_record_vmt(std::uintptr_t cloned_base) noexcept
249 {
250 108 const std::uint64_t id = m_next_id.fetch_add(1, std::memory_order_relaxed);
251 try
252 {
253 108 std::unique_lock<std::mutex> guard = lock_state();
254
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 9 taken 107 times.
108 if (!guard.owns_lock())
255 {
256 1 return std::nullopt;
257 }
258 // push_back gives the strong guarantee, so a failed growth leaves m_vmt unchanged.
259
1/2
✓ Branch 9 → 10 taken 107 times.
✗ Branch 9 → 18 not taken.
107 m_vmt.push_back(VmtEntry{id, cloned_base});
260
2/2
✓ Branch 12 → 13 taken 107 times.
✓ Branch 12 → 15 taken 1 time.
108 }
261 catch (...)
262 {
263 return std::nullopt;
264 }
265 107 return id;
266 }
267
268 101 void HookLedger::release_vmt(std::uint64_t id) noexcept
269 {
270 101 std::unique_lock<std::mutex> guard = lock_state();
271
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 100 times.
101 if (!guard.owns_lock())
272 {
273 1 return;
274 }
275 239 std::erase_if(m_vmt, [id](const VmtEntry &entry) { return entry.id == id; });
276
2/2
✓ Branch 9 → 10 taken 100 times.
✓ Branch 9 → 12 taken 1 time.
101 }
277
278 179 bool HookLedger::is_vmt_clone_base(std::uintptr_t vptr) const noexcept
279 {
280
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 178 times.
179 if (vptr == 0)
281 {
282 1 return false;
283 }
284 178 std::unique_lock<std::mutex> guard = lock_state();
285
2/2
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 177 times.
178 if (!guard.owns_lock())
286 {
287 // Fail closed: report the vptr as one of this kit's clone bases so the caller refuses or warns rather
288 // than silently baking another handle's hooked slots into its own pristine snapshot.
289 1 return true;
290 }
291 177 return std::any_of(
292 m_vmt.begin(),
293 m_vmt.end(),
294 99 [vptr](const VmtEntry &entry) { return entry.base == vptr; }
295 177 );
296 178 }
297 } // namespace detail
298 } // namespace DetourModKit
299