GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 82.7% 296 / 0 / 358
Functions: 93.9% 31 / 0 / 33
Branches: 61.4% 137 / 0 / 223

src/internal/memory_guarded.cpp
Line Branch Exec Source
1 /**
2 * @file memory_guarded.cpp
3 * @brief This TU implements the shared fault-containment engine for guarded byte access.
4 *
5 * MSVC uses frame-based __try / __except filters here. Scanner TUs also use __try and route their filters through
6 * guarded_range_fault_filter. MinGW/GCC uses a process-wide vectored exception handler here. A fault within an armed
7 * foreign range returns a clean failure through __builtin_longjmp. This boundary keeps memory.hpp free of <windows.h>
8 * and structured-exception constructs. The page-protection transaction ledger and the patch path that changes
9 * protection live in memory_protect_ledger.cpp.
10 */
11
12 #include "internal/memory_guarded.hpp"
13 #include "internal/memory_fault.hpp"
14
15 #include "DetourModKit/memory.hpp"
16
17 #include <windows.h>
18 #if defined(_MSC_VER)
19 #include <intrin.h> // __movsb provides an ASan-safe forward copy from foreign memory.
20 #endif
21
22 #include <array>
23 #include <atomic>
24 #include <chrono>
25 #include <cstddef>
26 #include <cstdint>
27 #include <cstring>
28 #include <mutex>
29 #include <thread>
30
31 namespace DetourModKit
32 {
33 namespace
34 {
35 // Page-protection flag groups support the VirtualQuery-validated fallbacks. The cache TU keeps a separate copy
36 // so this engine TU stays independent of the cache subsystem.
37 constexpr DWORD READ_PERMISSION_FLAGS = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
38 PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
39 constexpr DWORD WRITE_PERMISSION_FLAGS =
40 PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
41 constexpr DWORD NOACCESS_GUARD_FLAGS = PAGE_NOACCESS | PAGE_GUARD;
42
43 // The STATUS_GUARD_PAGE_VIOLATION literal matches <winnt.h>. It needs no ntstatus.h include and cannot collide
44 // with a platform macro of the same name.
45 constexpr unsigned long GUARD_PAGE_FAULT_CODE = 0x80000001ul;
46
47 #if defined(DMK_ENABLE_TEST_SEAMS)
48 std::atomic<bool> s_seam_guard_rearm_fails{false};
49 #endif
50
51 // Re-arm a PAGE_GUARD page after the OS consumes the bit during fault dispatch. Otherwise the foreign guard
52 // page loses its host fence and fails open. The read still fails closed, and the host's next access faults.
53 // A restore failure is reported so the caller continues exception search instead of a fault claim.
54 // Both the MinGW vectored handler and MSVC __except filters call this helper. VirtualQuery and VirtualProtect
55 // neither allocate nor take a lock forbidden within exception dispatch.
56 214829 [[nodiscard]] bool rearm_guard_page_if_consumed(const EXCEPTION_RECORD *record) noexcept
57 {
58
1/2
✓ Branch 2 → 3 taken 214849 times.
✗ Branch 2 → 4 not taken.
214829 if (record->ExceptionCode != GUARD_PAGE_FAULT_CODE)
59 {
60 214849 return true;
61 }
62 if (record->NumberParameters < 2)
63 {
64 return false;
65 }
66 const auto fault_address = reinterpret_cast<LPVOID>(record->ExceptionInformation[1]);
67 MEMORY_BASIC_INFORMATION mbi{};
68 if (VirtualQuery(fault_address, &mbi, sizeof(mbi)) == 0 || mbi.State != MEM_COMMIT)
69 {
70 return false;
71 }
72 // The OS already cleared PAGE_GUARD, so mbi.Protect omits it. Add it back to restore the fence over the
73 // page that contains the fault address.
74 #if defined(DMK_ENABLE_TEST_SEAMS)
75
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 12 times.
12 if (s_seam_guard_rearm_fails.load(std::memory_order_relaxed))
76 {
77 return false;
78 }
79 #endif
80 12 DWORD previous = 0;
81 12 return VirtualProtect(fault_address, 1, mbi.Protect | PAGE_GUARD, &previous) != 0;
82 }
83 } // namespace
84
85 namespace
86 {
87 // Use an explicit forward copy so a fault at the first target byte proves that no later target byte was
88 // written. The intrinsic/inline instruction also bypasses ASan's memcpy interceptor for deliberate
89 // foreign-memory access.
90 8771 inline void copy_with_fault_progress(void *destination, const void *source, std::size_t bytes) noexcept
91 {
92 #if defined(_MSC_VER) && defined(__SANITIZE_ADDRESS__)
93 __movsb(static_cast<unsigned char *>(destination), static_cast<const unsigned char *>(source), bytes);
94 #else
95 // Fixed-width copies compile to one destination store on both supported x64 toolchains. They preserve the
96 // first-byte classification and avoid REP setup for the common scalar-write sizes.
97
5/5
✓ Branch 2 → 3 taken 8329 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 34 times.
✓ Branch 2 → 6 taken 10 times.
✓ Branch 2 → 7 taken 395 times.
8771 switch (bytes)
98 {
99 8329 case 1:
100 8329 std::memcpy(destination, source, 1);
101 8329 return;
102 3 case 2:
103 3 std::memcpy(destination, source, 2);
104 3 return;
105 34 case 4:
106 34 std::memcpy(destination, source, 4);
107 34 return;
108 10 case 8:
109 10 std::memcpy(destination, source, 8);
110 10 return;
111 395 default:
112 395 break;
113 }
114 #if defined(_MSC_VER)
115 __movsb(static_cast<unsigned char *>(destination), static_cast<const unsigned char *>(source), bytes);
116 #elif defined(__x86_64__)
117 395 void *current_destination = destination;
118 395 const void *current_source = source;
119 395 std::size_t remaining = bytes;
120 395 __asm__ __volatile__("rep movsb"
121 : "+D"(current_destination), "+S"(current_source), "+c"(remaining)
122 :
123 : "memory");
124 #else
125 std::memcpy(destination, source, bytes);
126 #endif
127 #endif
128 }
129
130 // Add a signed byte offset to an address and reject address-space wrap. A pointer-chain hop near either end
131 // must not produce a wrapped link that modulo-2^64 addition reports as plausible.
132 84 [[nodiscard]] bool checked_offset(std::uintptr_t base, std::ptrdiff_t offset, std::uintptr_t &out) noexcept
133 {
134
2/2
✓ Branch 2 → 3 taken 81 times.
✓ Branch 2 → 6 taken 3 times.
84 if (offset >= 0)
135 {
136 81 const std::uintptr_t delta = static_cast<std::uintptr_t>(offset);
137
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 80 times.
81 if (base > UINTPTR_MAX - delta)
138 {
139 1 return false;
140 }
141 80 out = base + delta;
142 80 return true;
143 }
144 // For offset < 0, unsigned negation behavior is defined. Reject a magnitude that underflows past zero.
145 3 const std::uintptr_t magnitude = static_cast<std::uintptr_t>(-(offset + 1)) + 1U;
146
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 3 times.
3 if (magnitude > base)
147 {
148 return false;
149 }
150 3 out = base - magnitude;
151 3 return true;
152 }
153
154 #if defined(DMK_ENABLE_TEST_SEAMS)
155 // Thread-local seams isolate one test's injection from another thread's guarded operation.
156 thread_local bool s_seam_forward_copy = false;
157 thread_local std::size_t s_seam_last_prefix = 0;
158 // Process-wide lock-free counters avoid a first-touch emulated-TLS allocation inside loader-lock-safe guarded
159 // paths. The overlap proof is single-threaded and resets them immediately before each observed call.
160 std::atomic<std::size_t> s_seam_guarded_read_calls{0};
161 std::atomic<std::size_t> s_seam_guarded_write_calls{0};
162 std::atomic<std::size_t> s_seam_protection_calls{0};
163 std::atomic<bool> s_seam_observe_guarded_access{false};
164 static_assert(std::atomic<std::size_t>::is_always_lock_free);
165 static_assert(std::atomic<bool>::is_always_lock_free);
166 #endif
167 } // namespace
168
169 #ifdef _MSC_VER
170 // Every MSVC guarded foreign access routes its __except through this shared frame-based SEH filter. Each route
171 // uses the same fault set, address screen, and guard-page re-arm.
172 // GetExceptionInformation() is valid only inside a filter expression, so call sites pass EXCEPTION_POINTERS in.
173 long detail::guarded_range_fault_filter(
174 EXCEPTION_POINTERS *info,
175 std::uintptr_t lo,
176 std::uintptr_t hi,
177 volatile std::uintptr_t *fault_address_out
178 ) noexcept
179 {
180 const EXCEPTION_RECORD *const record = info->ExceptionRecord;
181 if (!detail::is_guarded_read_fault(record->ExceptionCode))
182 {
183 return EXCEPTION_CONTINUE_SEARCH;
184 }
185 // A guarded foreign access claims only a code with the data fault address in ExceptionInformation[1]. A host
186 // RaiseException record without that address passes through, even if it reuses one of these NTSTATUS codes.
187 if (record->NumberParameters < 2)
188 {
189 return EXCEPTION_CONTINUE_SEARCH;
190 }
191 // Claim only a fault inside the declared foreign span. An unrelated defect or a caller-buffer fault reaches
192 // the host handlers. The MinGW vectored handler uses the same rule.
193 const std::uintptr_t fault_address = static_cast<std::uintptr_t>(record->ExceptionInformation[1]);
194 if (fault_address < lo || fault_address >= hi)
195 {
196 return EXCEPTION_CONTINUE_SEARCH;
197 }
198 if (!rearm_guard_page_if_consumed(record))
199 {
200 return EXCEPTION_CONTINUE_SEARCH;
201 }
202 if (fault_address_out != nullptr)
203 {
204 *fault_address_out = fault_address;
205 }
206 return EXCEPTION_EXECUTE_HANDLER;
207 }
208 #endif
209
210 #ifndef _MSC_VER
211 // MinGW/GCC has no __try / __except. One process-wide vectored exception handler provides the equivalent fault
212 // guard. Each guarded access records its foreign range in a thread slot. A fault in that range returns failure
213 // instead of host termination.
214 namespace
215 {
216 // This fallback applies whenever s_veh_handle is unavailable. ReadProcessMemory turns a page change after the
217 // query into API failure rather than a user-mode fault.
218 6 bool virtualquery_validated_copy(std::uintptr_t addr, void *out, std::size_t bytes) noexcept
219 {
220 6 std::size_t copied = 0;
221
2/2
✓ Branch 29 → 3 taken 6 times.
✓ Branch 29 → 30 taken 6 times.
12 while (copied < bytes)
222 {
223 6 const std::uintptr_t cur = addr + copied;
224 6 MEMORY_BASIC_INFORMATION mbi{};
225
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 6 times.
6 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)))
226 return false;
227
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
6 if (mbi.State != MEM_COMMIT)
228 return false;
229
2/4
✓ Branch 8 → 9 taken 6 times.
✗ Branch 8 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6 times.
6 if ((mbi.Protect & READ_PERMISSION_FLAGS) == 0 || (mbi.Protect & NOACCESS_GUARD_FLAGS) != 0)
230 return false;
231
232 6 const std::uintptr_t region_start = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
233 6 const std::uintptr_t region_end = region_start + mbi.RegionSize;
234
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 6 times.
6 if (region_end < region_start)
235 return false;
236
2/4
✓ Branch 13 → 14 taken 6 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 6 times.
6 if (cur < region_start || cur >= region_end)
237 return false;
238
239 6 const std::size_t available = static_cast<std::size_t>(region_end - cur);
240 6 const std::size_t remaining = bytes - copied;
241
1/2
✓ Branch 16 → 17 taken 6 times.
✗ Branch 16 → 18 not taken.
6 const std::size_t to_copy = (remaining < available) ? remaining : available;
242 6 SIZE_T copied_now = 0;
243 6 if (!ReadProcessMemory(
244 GetCurrentProcess(),
245 reinterpret_cast<const void *>(cur),
246 static_cast<std::byte *>(out) + copied,
247 to_copy,
248 &copied_now
249
2/4
✓ Branch 21 → 22 taken 6 times.
✗ Branch 21 → 23 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 6 times.
12 ) ||
250
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 6 times.
6 copied_now != to_copy)
251 return false;
252 6 copied += to_copy;
253 }
254 6 return true;
255 }
256
257 // This fallback writes when no fault guard is available. It never changes page protection (a non-writable
258 // protection fails closed) and copies through WriteProcessMemory.
259 detail::GuardedWriteStatus
260 virtualquery_validated_write(std::uintptr_t addr, const void *source, std::size_t bytes) noexcept
261 {
262 std::size_t copied = 0;
263 while (copied < bytes)
264 {
265 const std::uintptr_t cur = addr + copied;
266 MEMORY_BASIC_INFORMATION mbi{};
267 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT ||
268 (mbi.Protect & WRITE_PERMISSION_FLAGS) == 0 || (mbi.Protect & NOACCESS_GUARD_FLAGS) != 0)
269 return copied == 0 ? detail::GuardedWriteStatus::NotWritten
270 : detail::GuardedWriteStatus::MayBePartial;
271
272 const std::uintptr_t region_start = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
273 const std::uintptr_t region_end = region_start + mbi.RegionSize;
274 if (region_end < region_start || cur < region_start || cur >= region_end)
275 return copied == 0 ? detail::GuardedWriteStatus::NotWritten
276 : detail::GuardedWriteStatus::MayBePartial;
277
278 const std::size_t available = static_cast<std::size_t>(region_end - cur);
279 const std::size_t remaining = bytes - copied;
280 const std::size_t to_copy = (remaining < available) ? remaining : available;
281 SIZE_T copied_now = 0;
282 const bool ok = WriteProcessMemory(
283 GetCurrentProcess(),
284 reinterpret_cast<void *>(cur),
285 static_cast<const std::byte *>(source) + copied,
286 to_copy,
287 &copied_now
288 ) != 0;
289 copied += copied_now;
290 if (!ok || copied_now != to_copy)
291 return copied == 0 ? detail::GuardedWriteStatus::NotWritten
292 : detail::GuardedWriteStatus::MayBePartial;
293 }
294 return detail::GuardedWriteStatus::Ok;
295 }
296
297 #if defined(_WIN64)
298 // Each guarded access publishes its stack record to the thread's Win32 TLS slot. Nested accesses preserve and
299 // restore the prior slot value. MinGW lowers thread_local to __emutls_get_address, which allocates and locks
300 // on first access. Exception dispatch forbids those operations. TlsGetValue is valid in that context.
301 struct VehAccessGuard
302 {
303 void *env[5]; // Stores the __builtin_setjmp buffer for longjmp under the five-word GCC ABI.
304 std::uintptr_t guard_lo; // Marks the first byte of the foreign range.
305 std::uintptr_t guard_hi; // Marks one byte past the foreign range.
306 volatile std::uintptr_t fault_address;
307 };
308
309 std::mutex s_veh_mutex;
310 std::atomic<void *> s_veh_handle{nullptr};
311 // The process-lifetime TLS index never becomes free. Handler removal cannot invalidate an index held by a
312 // concurrent access.
313 std::atomic<DWORD> s_veh_tls_index{TLS_OUT_OF_INDEXES};
314
315 // Cache-line-padded counters stripe current guarded-path accesses and avoid contention on one global line.
316 // The release_guarded_engine function drains the sum to zero before handler removal. The handle-null store,
317 // stripe increment, and drain loads use seq_cst under Dekker. An access that observes a live handle enters the
318 // count before the drain observes zero.
319 constexpr std::size_t VEH_IN_FLIGHT_STRIPE_COUNT = 64;
320
321 // alignas(64) needs no MSVC C4324 suppression here. The #ifndef _MSC_VER region hides this padded struct from
322 // every MSVC build.
323 struct alignas(64) VehInFlightStripe
324 {
325 std::atomic<int> count{0};
326 };
327
328 std::array<VehInFlightStripe, VEH_IN_FLIGHT_STRIPE_COUNT> s_veh_in_flight_stripes{};
329
330 // A stable Win32 thread ID selects this thread's in-flight stripe. The same stripe receives entry and exit,
331 // so its count stays nonnegative. GetCurrentThreadId allocates nothing and takes no lock, so loader lock
332 // permits it. A stripe collision adds contention but cannot cause a miscount.
333 6915010 [[nodiscard]] inline std::size_t veh_in_flight_stripe_index() noexcept
334 {
335 6915010 const std::uint64_t mixed = static_cast<std::uint64_t>(GetCurrentThreadId()) * 0x9E3779B97F4A7C15ULL;
336 6949622 return static_cast<std::size_t>(mixed >> 48) % VEH_IN_FLIGHT_STRIPE_COUNT;
337 }
338
339 // The sum of all in-flight stripes equals the guarded accesses on the handler path. After publication of
340 // s_veh_handle = nullptr, remove_veh_handler waits for this sum to reach zero under seq_cst.
341 759 [[nodiscard]] inline int veh_in_flight_total() noexcept
342 {
343 759 int total = 0;
344
2/2
✓ Branch 11 → 3 taken 48576 times.
✓ Branch 11 → 12 taken 759 times.
49335 for (const VehInFlightStripe &stripe : s_veh_in_flight_stripes)
345 {
346 97152 total += stripe.count.load(std::memory_order_seq_cst);
347 }
348 759 return total;
349 }
350
351 // Return true when this thread already executes a guarded access. A nested access must not call
352 // ensure_veh_installed. A wait on s_veh_mutex deadlocks with remove_veh_handler, which holds that mutex until
353 // this thread exits. The omitted install loses nothing. After teardown, the seq_cst handle load routes a
354 // nested access to the fallback.
355 6964348 [[nodiscard]] inline bool inside_guarded_access() noexcept
356 {
357 6964991 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
358
2/2
✓ Branch 9 → 10 taken 851 times.
✓ Branch 9 → 11 taken 6964140 times.
6964991 if (slot == TLS_OUT_OF_INDEXES)
359 851 return false;
360 6964140 return TlsGetValue(slot) != nullptr;
361 }
362
363 // The handler redirects a thread with a fault into this recovery stub. __builtin_longjmp restores the paired
364 // __builtin_setjmp snapshot without an SEH stack unwind. That unwind can abort from a vectored-handler resume
365 // context. noinline gives the handler a stable target address.
366 215007 [[noreturn]] __attribute__((noinline)) void veh_perform_longjmp(void *env) noexcept
367 {
368 // __builtin_longjmp has type void(void **, int). env points to the VehAccessGuard::env[5] buffer. The
369 // explicit cast matches that signature. GCC accepts bare void *, but the Clang front end rejects it.
370 215007 __builtin_longjmp(static_cast<void **>(env), 1);
371 }
372
373 // The vectored exception handler claims only faults from a guarded access. The code must belong to the same
374 // set as the MSVC filters, and the record must contain an address within the armed foreign range. Every other
375 // fault passes through. A claimed fault redirects the thread to veh_perform_longjmp, which reports access
376 // failure.
377 215397 LONG NTAPI dmk_veh_read_handler(PEXCEPTION_POINTERS info) noexcept
378 {
379 215383 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
380
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 215383 times.
215383 if (slot == TLS_OUT_OF_INDEXES)
381 return EXCEPTION_CONTINUE_SEARCH;
382
383 215383 auto *const guard = static_cast<VehAccessGuard *>(TlsGetValue(slot));
384
2/2
✓ Branch 12 → 13 taken 390 times.
✓ Branch 12 → 14 taken 214993 times.
215383 if (guard == nullptr)
385 390 return EXCEPTION_CONTINUE_SEARCH;
386
387 214993 const EXCEPTION_RECORD *const record = info->ExceptionRecord;
388
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 214844 times.
214993 if (!detail::is_guarded_read_fault(record->ExceptionCode))
389 return EXCEPTION_CONTINUE_SEARCH;
390
391 // Refuse a record without a fault address. A host RaiseException call that reuses one of these NTSTATUS
392 // codes must stay in host control flow.
393
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 214844 times.
214844 if (record->NumberParameters < 2)
394 return EXCEPTION_CONTINUE_SEARCH;
395
396 // Confine the claim to the armed foreign range. A defect outside it reaches the host handlers.
397 214844 const std::uintptr_t fault_address = static_cast<std::uintptr_t>(record->ExceptionInformation[1]);
398
2/4
✓ Branch 19 → 20 taken 214844 times.
✗ Branch 19 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 214849 times.
214844 if (fault_address < guard->guard_lo || fault_address >= guard->guard_hi)
399 return EXCEPTION_CONTINUE_SEARCH;
400 // Re-arm the host fence before the read returns a closed failure. See rearm_guard_page_if_consumed.
401
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 214865 times.
214849 if (!rearm_guard_page_if_consumed(record))
402 return EXCEPTION_CONTINUE_SEARCH;
403 214865 guard->fault_address = fault_address;
404
405 // Disarm before resume so a fault inside the longjmp stub passes through instead of a recursive claim.
406 214865 TlsSetValue(slot, nullptr);
407
408 // Resume the thread in veh_perform_longjmp(env). Set RIP to the stub and place the setjmp buffer in RCX.
409 // Entry uses an injected RIP change instead of CALL. Pre-align the fault-point RSP for the stub prologue.
410 // The stub reloads RSP from the snapshot.
411 214879 CONTEXT *const ctx = info->ContextRecord;
412 214879 ctx->Rsp = (ctx->Rsp & ~static_cast<DWORD64>(15)) - 8;
413 214879 ctx->Rcx = reinterpret_cast<DWORD64>(&guard->env);
414 214879 ctx->Rip = reinterpret_cast<DWORD64>(&veh_perform_longjmp);
415 214879 return EXCEPTION_CONTINUE_EXECUTION;
416 }
417
418 // Install the handler on first demand and permit installation after teardown. On failure, the null handle
419 // routes byte-copy guards to the VirtualQuery fallback. In-place region guards fail closed before access to
420 // the foreign range.
421 6935851 void ensure_veh_installed() noexcept
422 {
423
1/2
✓ Branch 3 → 4 taken 6971447 times.
✗ Branch 3 → 5 not taken.
6935851 if (s_veh_handle.load(std::memory_order_acquire) != nullptr)
424 6971472 return;
425
426 std::lock_guard<std::mutex> lock(s_veh_mutex);
427
2/2
✓ Branch 7 → 8 taken 25 times.
✓ Branch 7 → 9 taken 1595 times.
1620 if (s_veh_handle.load(std::memory_order_relaxed) != nullptr)
428 25 return;
429
2/2
✓ Branch 16 → 17 taken 1296 times.
✓ Branch 16 → 29 taken 299 times.
1595 if (s_veh_tls_index.load(std::memory_order_relaxed) == TLS_OUT_OF_INDEXES)
430 {
431 1296 const DWORD slot = TlsAlloc();
432
1/2
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1296 times.
1296 if (slot == TLS_OUT_OF_INDEXES)
433 return; // Guard setup failed. Access paths use their fail-closed fallback.
434 s_veh_tls_index.store(slot, std::memory_order_release);
435 }
436 // This handler is first, so a guarded access resolves through it before any consumer VEH or SEH.
437 // Every other fault passes through, so first position never starves the host handlers.
438 1595 void *const handle = AddVectoredExceptionHandler(1, dmk_veh_read_handler);
439 1595 s_veh_handle.store(handle, std::memory_order_release);
440
2/2
✓ Branch 33 → 34 taken 1595 times.
✓ Branch 33 → 36 taken 25 times.
1620 }
441
442 746 void remove_veh_handler() noexcept
443 {
444 746 std::lock_guard<std::mutex> lock(s_veh_mutex);
445 746 void *const handle = s_veh_handle.load(std::memory_order_relaxed);
446
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 744 times.
746 if (handle == nullptr)
447 2 return;
448 // Stop new guarded accesses, then wait for each access already committed to the handler path. No fault can
449 // arrive after handler removal. The seq_cst store pairs with the helpers' seq_cst stripe fetch_add and
450 // handle load under Dekker. The sum below cannot read zero while such an access is live.
451 744 s_veh_handle.store(nullptr, std::memory_order_seq_cst);
452 744 int spins = 0;
453
2/2
✓ Branch 15 → 8 taken 15 times.
✓ Branch 15 → 16 taken 744 times.
759 while (veh_in_flight_total() > 0)
454 {
455
1/2
✓ Branch 8 → 9 taken 15 times.
✗ Branch 8 → 10 not taken.
15 if (spins < 4096)
456 15 std::this_thread::yield();
457 else
458 std::this_thread::sleep_for(std::chrono::microseconds(100));
459 15 ++spins;
460 }
461 744 RemoveVectoredExceptionHandler(handle);
462
2/2
✓ Branch 19 → 20 taken 744 times.
✓ Branch 19 → 22 taken 2 times.
746 }
463
464 // Copy [src, src + len) into out under the vectored handler. Raw inline asm hides the single rep movsb from
465 // ASan, so this deliberate cross-region read cannot cause a false positive. The MSVC probe uses __movsb for
466 // the same reason. __builtin_setjmp records the recovery point. The handler uses longjmp so the setjmp
467 // expression returns nonzero. noinline keeps the read and its anchor in one frame.
468 __attribute__((noinline)) bool
469 6808417 veh_guarded_copy(void *out, const void *src, std::size_t len, volatile std::uintptr_t *fault_out) noexcept
470 {
471 6914476 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
472 // Read before the setjmp so it survives the longjmp return.
473 6914476 void *const enclosing = TlsGetValue(slot);
474 6866105 VehAccessGuard guard{};
475 6866105 guard.guard_lo = reinterpret_cast<std::uintptr_t>(src);
476 6866105 guard.guard_hi = guard.guard_lo + len;
477
478
2/2
✓ Branch 14 → 15 taken 210774 times.
✓ Branch 14 → 19 taken 6855669 times.
7076886 if (__builtin_setjmp(guard.env) != 0)
479 {
480 // This path runs only after the handler uses longjmp to contain a read fault.
481 210774 TlsSetValue(slot, enclosing);
482
2/2
✓ Branch 16 → 17 taken 46 times.
✓ Branch 16 → 18 taken 210675 times.
210721 if (fault_out != nullptr)
483 {
484 46 *fault_out = guard.fault_address;
485 }
486 210721 return false;
487 }
488
489 // Arm after the setjmp captures env and before the read.
490 6855669 TlsSetValue(slot, &guard);
491
492 6897144 void *dst = out;
493 6897144 const void *cur = src;
494 6897144 std::size_t n = len;
495 6897144 __asm__ __volatile__("rep movsb" : "+D"(dst), "+S"(cur), "+c"(n) : : "memory");
496
497 6705335 TlsSetValue(slot, enclosing);
498 6777900 return true;
499 }
500
501 __attribute__((noinline)) detail::GuardedWriteStatus
502 8771 veh_guarded_write(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
503 {
504 8771 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
505 8771 void *const enclosing = TlsGetValue(slot);
506 8771 VehAccessGuard guard{};
507 8771 guard.guard_lo = address;
508 8771 guard.guard_hi = address + bytes;
509
510
2/2
✓ Branch 14 → 15 taken 411 times.
✓ Branch 14 → 20 taken 8771 times.
9182 if (__builtin_setjmp(guard.env) != 0)
511 {
512 411 TlsSetValue(slot, enclosing);
513
2/2
✓ Branch 16 → 17 taken 360 times.
✓ Branch 16 → 18 taken 51 times.
411 return guard.fault_address == address ? detail::GuardedWriteStatus::NotWritten
514 411 : detail::GuardedWriteStatus::MayBePartial;
515 }
516
517 8771 TlsSetValue(slot, &guard);
518 8771 copy_with_fault_progress(reinterpret_cast<void *>(address), source, bytes);
519 8360 TlsSetValue(slot, enclosing);
520 8360 return detail::GuardedWriteStatus::Ok;
521 }
522
523 // Run fn(ctx) with the vectored handler armed over [lo, hi) for an in-place access. fn must touch only that
524 // range because the handler does not claim other faults. A claimed fault abandons fn without destructor calls.
525 // fn must hold no resource whose release depends on stack unwind. It must not block indefinitely because
526 // teardown waits for its in-flight stripe count. fn can call a nested guarded access outside [lo, hi), and the
527 // nested wrapper restores this guard after that call returns.
528 __attribute__((noinline)) bool
529 26924 veh_guarded_region(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept
530 {
531 26925 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
532 26925 void *const enclosing = TlsGetValue(slot);
533 26925 VehAccessGuard guard{};
534 26925 guard.guard_lo = lo;
535 26925 guard.guard_hi = hi;
536
537
2/2
✓ Branch 14 → 15 taken 3803 times.
✓ Branch 14 → 17 taken 26924 times.
30728 if (__builtin_setjmp(guard.env) != 0)
538 {
539 3803 TlsSetValue(slot, enclosing);
540 3803 return false;
541 }
542
543 26924 TlsSetValue(slot, &guard);
544 26925 fn(ctx);
545 23122 TlsSetValue(slot, enclosing);
546 23122 return true;
547 }
548
549 // This entry point serves all MinGW read paths. Reject a source range below the floor or across address-space
550 // wrap. A wrapped range inverts the handler guard check and lets a real fault escape. Count the read in the
551 // drain epoch around the path choice. Use the VirtualQuery copy when the handler is unavailable.
552 bool
553 6922283 veh_read_bytes(std::uintptr_t addr, void *out, std::size_t bytes, volatile std::uintptr_t *fault_out) noexcept
554 {
555
2/4
✓ Branch 2 → 3 taken 6971176 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 6983450 times.
6922283 if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr)
556 return false;
557
558
1/2
✓ Branch 6 → 7 taken 6926923 times.
✗ Branch 6 → 8 not taken.
6983450 if (!inside_guarded_access())
559 {
560 6926923 ensure_veh_installed();
561 }
562
563 6901398 const std::size_t stripe = veh_in_flight_stripe_index();
564 6874173 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
565 6989658 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
566 // The VirtualQuery fallback never faults and has no fault address to report into fault_out.
567
1/2
✓ Branch 13 → 14 taken 6865326 times.
✗ Branch 13 → 16 not taken.
6836971 const bool ok = armed ? veh_guarded_copy(out, reinterpret_cast<const void *>(addr), bytes, fault_out)
568 6955299 : virtualquery_validated_copy(addr, out, bytes);
569 6983660 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
570 6982810 return ok;
571 }
572
573 8771 detail::GuardedWriteStatus veh_write_bytes(std::uintptr_t addr, const void *source, std::size_t bytes) noexcept
574 {
575
2/4
✓ Branch 2 → 3 taken 8771 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8771 times.
8771 if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr)
576 return detail::GuardedWriteStatus::NotWritten;
577
578
1/2
✓ Branch 6 → 7 taken 8771 times.
✗ Branch 6 → 8 not taken.
8771 if (!inside_guarded_access())
579 {
580 8771 ensure_veh_installed();
581 }
582
583 8771 const std::size_t stripe = veh_in_flight_stripe_index();
584 8771 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
585 8771 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
586 const detail::GuardedWriteStatus status =
587
1/2
✓ Branch 13 → 14 taken 8771 times.
✗ Branch 13 → 15 not taken.
8771 armed ? veh_guarded_write(addr, source, bytes) : virtualquery_validated_write(addr, source, bytes);
588 8771 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
589 8771 return status;
590 }
591 #endif // _WIN64
592 } // namespace
593 #endif // !_MSC_VER
594
595 #if !defined(_MSC_VER) && defined(_WIN64)
596 744 void detail::ensure_guarded_engine_installed() noexcept
597 {
598 744 ensure_veh_installed();
599 744 }
600
601 746 void detail::release_guarded_engine() noexcept
602 {
603 746 remove_veh_handler();
604 746 }
605
606 bool
607 26924 detail::run_guarded_region(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept
608 {
609 // An empty range or one with address-space wrap has nothing to guard. A wrapped [lo, hi) inverts the handler
610 // check.
611
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 26924 times.
26924 if (hi <= lo)
612 {
613 fn(ctx);
614 return true;
615 }
616
617
1/2
✓ Branch 6 → 7 taken 26925 times.
✗ Branch 6 → 8 not taken.
26924 if (!inside_guarded_access())
618 {
619 26925 ensure_veh_installed();
620 }
621
622 // Count the call in the drain epoch around the path choice, as veh_read_bytes does.
623 26924 const std::size_t stripe = veh_in_flight_stripe_index();
624 26924 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
625 26925 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
626 26924 bool completed = true;
627
1/2
✓ Branch 13 → 14 taken 26924 times.
✗ Branch 13 → 15 not taken.
26924 if (armed)
628 {
629 26924 completed = veh_guarded_region(lo, hi, fn, ctx);
630 }
631 else
632 {
633 // The handler is unavailable. Do not run an in-place scan without a guard. The caller treats false as a
634 // skipped or faulted region and closes uniqueness-sensitive work.
635 completed = false;
636 }
637 26925 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
638 26925 return completed;
639 }
640 #endif // !_MSC_VER && _WIN64
641
642 6902452 bool detail::guarded_read_bytes(
643 std::uintptr_t address,
644 void *out,
645 std::size_t bytes,
646 volatile std::uintptr_t *fault_address_out
647 ) noexcept
648 {
649 #if defined(DMK_ENABLE_TEST_SEAMS)
650
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 7 taken 6965857 times.
6902452 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
651 {
652 s_seam_guarded_read_calls.fetch_add(1, std::memory_order_relaxed);
653 }
654 #endif
655
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 6965867 times.
6965867 if (bytes == 0)
656 return true;
657
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 6965867 times.
6965867 if (!out)
658 return false;
659
660 // Validate the complete half-open span [address, address + bytes) against the user-mode window before any
661 // read. A low-endpoint and wrap check alone admits a range that reaches the upper ceiling and causes a
662 // first-chance exception.
663
3/4
✓ Branch 11 → 12 taken 6948180 times.
✓ Branch 11 → 14 taken 17687 times.
✓ Branch 12 → 13 taken 6963645 times.
✗ Branch 12 → 14 not taken.
6965867 if (address < memory::USERSPACE_PTR_MIN || address + bytes < address ||
664
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 6968745 times.
6963645 address + bytes > memory::USERSPACE_PTR_MAX)
665 return false;
666
667 #ifdef _MSC_VER
668 __try
669 {
670 #if defined(__SANITIZE_ADDRESS__)
671 // Under ASan, MSVC routes std::memcpy through an interceptor that reports this valid foreign-memory probe
672 // as a false positive. Release keeps std::memcpy.
673 __movsb(static_cast<unsigned char *>(out), reinterpret_cast<const unsigned char *>(address), bytes);
674 #else
675 std::memcpy(out, reinterpret_cast<const void *>(address), bytes);
676 #endif
677 return true;
678 }
679 // Swallow only a fault whose address lies in the foreign source span. A caller-buffer fault or any address
680 // outside [address, address + bytes) identifies a caller or DMK defect and propagates.
681 __except (guarded_range_fault_filter(GetExceptionInformation(), address, address + bytes, fault_address_out))
682 {
683 return false;
684 }
685 #else
686 // On MinGW, read through the vectored fault guard. The success path uses one rep movsb and no system call.
687 6968745 return veh_read_bytes(address, out, bytes, fault_address_out);
688 #endif
689 }
690
691 namespace
692 {
693 // The raw fault-guarded store performs only the contained copy, with no argument validation or test-seam work.
694 // The guarded_write_bytes entry point and its forward-copy seam share this store and use the same fault path.
695 [[nodiscard]] detail::GuardedWriteStatus
696 8771 guarded_store_bytes(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
697 {
698 #ifdef _MSC_VER
699 volatile std::uintptr_t fault_address = 0;
700 __try
701 {
702 copy_with_fault_progress(reinterpret_cast<void *>(address), source, bytes);
703 return detail::GuardedWriteStatus::Ok;
704 }
705 // Do not contain a fault on the caller-owned source buffer. Qualify detail:: because unqualified lookup
706 // from this anonymous namespace does not reach it.
707 __except (
708 detail::guarded_range_fault_filter(GetExceptionInformation(), address, address + bytes, &fault_address)
709 )
710 {
711 return fault_address == address ? detail::GuardedWriteStatus::NotWritten
712 : detail::GuardedWriteStatus::MayBePartial;
713 }
714 #else
715 // On MinGW, write through the same guard and fallback split as guarded_read_bytes.
716 8771 return veh_write_bytes(address, source, bytes);
717 #endif
718 }
719 } // namespace
720
721 detail::GuardedWriteStatus
722 456 detail::guarded_write_bytes(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
723 {
724 #if defined(DMK_ENABLE_TEST_SEAMS)
725
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 456 times.
456 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
726 {
727 s_seam_guarded_write_calls.fetch_add(1, std::memory_order_relaxed);
728 }
729 #endif
730
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 456 times.
456 if (bytes == 0)
731 return GuardedWriteStatus::Ok;
732
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 456 times.
456 if (!source)
733 return GuardedWriteStatus::NotWritten;
734
735 // Validate the complete half-open destination span against the user-mode window before any store. As in
736 // guarded_read_bytes, a low-endpoint and wrap check alone admits a range that reaches the ceiling.
737
4/4
✓ Branch 11 → 12 taken 455 times.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 12 → 13 taken 452 times.
✓ Branch 12 → 14 taken 3 times.
456 if (address < memory::USERSPACE_PTR_MIN || address + bytes < address ||
738
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 451 times.
452 address + bytes > memory::USERSPACE_PTR_MAX)
739 5 return GuardedWriteStatus::NotWritten;
740
741 451 const auto *const in = static_cast<const std::byte *>(source);
742
743 #if defined(DMK_ENABLE_TEST_SEAMS)
744
2/2
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 28 taken 442 times.
451 if (s_seam_forward_copy)
745 {
746 9 std::size_t written = 0;
747
1/2
✓ Branch 21 → 17 taken 8329 times.
✗ Branch 21 → 22 not taken.
8329 for (; written < bytes; ++written)
748 {
749
2/2
✓ Branch 18 → 19 taken 9 times.
✓ Branch 18 → 20 taken 8320 times.
8329 if (guarded_store_bytes(address + written, in + written, 1) != GuardedWriteStatus::Ok)
750 9 break;
751 }
752 9 s_seam_last_prefix = written;
753
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 9 times.
9 if (written == bytes)
754 return GuardedWriteStatus::Ok;
755
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 8 times.
9 return written == 0 ? GuardedWriteStatus::NotWritten : GuardedWriteStatus::MayBePartial;
756 }
757 #endif
758
759 442 const GuardedWriteStatus status = guarded_store_bytes(address, in, bytes);
760 #if defined(DMK_ENABLE_TEST_SEAMS)
761
2/2
✓ Branch 29 → 30 taken 40 times.
✓ Branch 29 → 31 taken 402 times.
442 s_seam_last_prefix = status == GuardedWriteStatus::Ok ? bytes : 0;
762 #endif
763 442 return status;
764 }
765
766 namespace
767 {
768 struct CompareExchangeWordContext
769 {
770 std::uintptr_t address{0};
771 std::uintptr_t expected{0};
772 std::uintptr_t replacement{0};
773 std::uintptr_t observed{0};
774 };
775
776 220 void compare_exchange_word(void *raw_context) noexcept
777 {
778 220 auto *const context = static_cast<CompareExchangeWordContext *>(raw_context);
779 static_assert(sizeof(LONG64) == sizeof(std::uintptr_t));
780 872 const LONG64 observed = ::InterlockedCompareExchange64(
781 216 reinterpret_cast<volatile LONG64 *>(context->address),
782 220 std::bit_cast<LONG64>(context->replacement),
783 220 std::bit_cast<LONG64>(context->expected)
784 216 );
785 216 context->observed = std::bit_cast<std::uintptr_t>(observed);
786 216 }
787 } // namespace
788
789 222 bool detail::guarded_compare_exchange_word(
790 std::uintptr_t address,
791 std::uintptr_t expected,
792 std::uintptr_t replacement
793 ) noexcept
794 {
795 222 constexpr std::size_t word_bytes = sizeof(std::uintptr_t);
796
4/6
✓ Branch 2 → 3 taken 220 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 3 → 4 taken 220 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 220 times.
✗ Branch 4 → 6 not taken.
222 if (address % alignof(std::uintptr_t) != 0 || address < memory::USERSPACE_PTR_MIN ||
797
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 220 times.
220 address + word_bytes < address || address + word_bytes > memory::USERSPACE_PTR_MAX)
798 {
799 2 return false;
800 }
801
802 220 CompareExchangeWordContext context{address, expected, replacement, 0};
803 #ifdef _MSC_VER
804 __try
805 {
806 compare_exchange_word(&context);
807 }
808 __except (guarded_range_fault_filter(GetExceptionInformation(), address, address + word_bytes))
809 {
810 return false;
811 }
812 #else
813
2/2
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 216 times.
220 if (!run_guarded_region(address, address + word_bytes, &compare_exchange_word, &context))
814 {
815 4 return false;
816 }
817 #endif
818 216 return context.observed == expected;
819 }
820
821 35 detail::ChainWalkOutcome detail::guarded_resolve_chain(
822 Address base,
823 const memory::ChainStep *steps,
824 std::size_t count,
825 Address *trace,
826 std::size_t trace_cap
827 ) noexcept
828 {
829 35 ChainWalkOutcome outcome;
830
831 // When count == 0, the identity walk returns base itself and performs no dereference or screen.
832
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 28 times.
35 if (count == 0)
833 {
834 7 outcome.address = base;
835 7 outcome.ok = true;
836 7 return outcome;
837 }
838
839 // Both toolchains use one walk. The range-aware guarded byte copy reads each link. checked_offset rejects a
840 // hop when signed offset addition wraps the address space.
841 28 std::uintptr_t cur = base.raw();
842
2/2
✓ Branch 25 → 6 taken 63 times.
✓ Branch 25 → 26 taken 21 times.
84 for (std::size_t i = 0; i + 1 < count; ++i)
843 {
844 63 std::uintptr_t link_address = 0;
845
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 63 times.
63 if (!checked_offset(cur, steps[i].offset, link_address))
846 {
847 outcome.fail_index = i;
848 7 return outcome;
849 }
850 63 std::uintptr_t next = 0;
851
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 62 times.
63 if (!guarded_read_bytes(link_address, &next, sizeof(next)))
852 {
853 1 outcome.fail_index = i;
854 1 return outcome;
855 }
856 // Screen the dereferenced link against this hop's floor and the user-mode ceiling before use as the next
857 // dereference base.
858
5/6
✓ Branch 13 → 14 taken 56 times.
✓ Branch 13 → 15 taken 6 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 56 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 17 → 19 taken 56 times.
62 if (next < steps[i].min_valid.raw() || next >= memory::USERSPACE_PTR_MAX)
859 {
860 6 outcome.fail_index = i;
861 6 return outcome;
862 }
863
3/4
✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 23 taken 53 times.
✓ Branch 20 → 21 taken 3 times.
✗ Branch 20 → 23 not taken.
56 if (trace != nullptr && i < trace_cap)
864 3 trace[i] = Address{next};
865 56 cur = next;
866 }
867 21 std::uintptr_t leaf = 0;
868
6/6
✓ Branch 27 → 28 taken 20 times.
✓ Branch 27 → 30 taken 1 time.
✓ Branch 28 → 29 taken 19 times.
✓ Branch 28 → 30 taken 1 time.
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 18 times.
40 if (!checked_offset(cur, steps[count - 1].offset, leaf) || leaf < memory::USERSPACE_PTR_MIN ||
869
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 31 taken 18 times.
19 leaf >= memory::USERSPACE_PTR_MAX)
870 {
871 3 outcome.fail_index = count - 1;
872 3 return outcome;
873 }
874
3/4
✓ Branch 34 → 35 taken 1 time.
✓ Branch 34 → 38 taken 17 times.
✓ Branch 35 → 36 taken 1 time.
✗ Branch 35 → 38 not taken.
18 if (trace != nullptr && (count - 1) < trace_cap)
875 1 trace[count - 1] = Address{leaf};
876 18 outcome.address = Address{leaf};
877 18 outcome.ok = true;
878 18 return outcome;
879 }
880
881 #if defined(DMK_ENABLE_TEST_SEAMS)
882 2640 void detail::note_protection_call_for_test() noexcept
883 {
884
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 2640 times.
2640 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
885 {
886 s_seam_protection_calls.fetch_add(1, std::memory_order_relaxed);
887 }
888 2640 }
889
890 18 void detail::reset_guarded_access_observation_for_test() noexcept
891 {
892 s_seam_guarded_read_calls.store(0, std::memory_order_relaxed);
893 s_seam_guarded_write_calls.store(0, std::memory_order_relaxed);
894 s_seam_protection_calls.store(0, std::memory_order_relaxed);
895 18 s_seam_observe_guarded_access.store(true, std::memory_order_release);
896 18 }
897
898 18 detail::GuardedAccessObservation detail::guarded_access_observation_for_test() noexcept
899 {
900 return GuardedAccessObservation{
901 18 s_seam_guarded_read_calls.load(std::memory_order_relaxed),
902 18 s_seam_guarded_write_calls.load(std::memory_order_relaxed),
903 18 s_seam_protection_calls.load(std::memory_order_relaxed)
904 54 };
905 }
906
907 18 void detail::stop_guarded_access_observation_for_test() noexcept
908 {
909 18 s_seam_observe_guarded_access.store(false, std::memory_order_release);
910 18 }
911
912 18 void detail::set_forward_copy_seam(bool enable) noexcept
913 {
914 18 s_seam_forward_copy = enable;
915 18 }
916
917 9 std::size_t detail::last_forward_copy_prefix() noexcept
918 {
919 9 return s_seam_last_prefix;
920 }
921
922 void detail::set_guard_rearm_failure_seam(bool fail) noexcept
923 {
924 s_seam_guard_rearm_fails.store(fail, std::memory_order_relaxed);
925 }
926 #endif
927 } // namespace DetourModKit
928