./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
370 / 408
90.7%
Branches:
182 / 255
71.4%
Functions:
42 / 43
97.7%

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, unless the guard-page re-arm in [B-20] fails. This
8 * boundary keeps memory.hpp free of <windows.h> and structured-exception constructs. The page-protection transaction
9 * ledger and the patch path that changes protection live in memory_protect_ledger.cpp.
10 */
11
12 #include "internal/memory_guarded.hpp"
13 #include "internal/memory_fault.hpp"
14 #include "internal/lifecycle_context.hpp"
15
16 #include "DetourModKit/memory.hpp"
17
18 #include <windows.h>
19 #if defined(_MSC_VER)
20 #include <intrin.h> // __movsb provides an ASan-safe forward copy from foreign memory.
21 #endif
22
23 #include <array>
24 #include <atomic>
25 #include <chrono>
26 #include <cstddef>
27 #include <cstdint>
28 #include <cstring>
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 10386 [[nodiscard]] bool rearm_guard_page_if_consumed(const EXCEPTION_RECORD *record) noexcept
57 {
58
1/2
✓ Branch 2 → 3 taken 10386 times.
✗ Branch 2 → 4 not taken.
10386 if (record->ExceptionCode != GUARD_PAGE_FAULT_CODE)
59 {
60 10386 return true;
61 }
62
0/2
✗ Branch 4 → 5 not taken.
✗ Branch 4 → 6 not taken.
✗ 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
3/6
✓ Branch 7 → 8 taken 12 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 12 times.
✗ 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 8782 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 8338 times.
✓ Branch 2 → 4 taken 3 times.
✓ Branch 2 → 5 taken 36 times.
✓ Branch 2 → 6 taken 10 times.
✓ Branch 2 → 7 taken 395 times.
8782 switch (bytes)
98 {
99 8338 case 1:
100 8338 std::memcpy(destination, source, 1);
101 8338 return;
102 3 case 2:
103 3 std::memcpy(destination, source, 2);
104 3 return;
105 36 case 4:
106 36 std::memcpy(destination, source, 4);
107 36 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 // unless the guard-page re-arm in [B-20] fails.
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 18 bool virtualquery_validated_copy(std::uintptr_t addr, void *out, std::size_t bytes) noexcept
219 {
220 18 std::size_t copied = 0;
221
2/2
✓ Branch 29 → 3 taken 18 times.
✓ Branch 29 → 30 taken 16 times.
34 while (copied < bytes)
222 {
223 18 const std::uintptr_t cur = addr + copied;
224 18 MEMORY_BASIC_INFORMATION mbi{};
225
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 18 times.
18 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)))
226 2 return false;
227
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 18 times.
18 if (mbi.State != MEM_COMMIT)
228 ✗ return false;
229
3/4
✓ Branch 8 → 9 taken 16 times.
✓ Branch 8 → 10 taken 2 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 16 times.
18 if ((mbi.Protect & READ_PERMISSION_FLAGS) == 0 || (mbi.Protect & NOACCESS_GUARD_FLAGS) != 0)
230 2 return false;
231
232 16 const std::uintptr_t region_start = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
233 16 const std::uintptr_t region_end = region_start + mbi.RegionSize;
234
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 16 times.
16 if (region_end < region_start)
235 ✗ return false;
236
2/4
✓ Branch 13 → 14 taken 16 times.
✗ Branch 13 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 16 times.
16 if (cur < region_start || cur >= region_end)
237 ✗ return false;
238
239 16 const std::size_t available = static_cast<std::size_t>(region_end - cur);
240 16 const std::size_t remaining = bytes - copied;
241
1/2
✓ Branch 16 → 17 taken 16 times.
✗ Branch 16 → 18 not taken.
16 const std::size_t to_copy = (remaining < available) ? remaining : available;
242 16 SIZE_T copied_now = 0;
243 16 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 16 times.
✗ Branch 21 → 23 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 16 times.
32 ) ||
250
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 16 times.
16 copied_now != to_copy)
251 ✗ return false;
252 16 copied += to_copy;
253 }
254 16 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 2 virtualquery_validated_write(std::uintptr_t addr, const void *source, std::size_t bytes) noexcept
261 {
262 2 std::size_t copied = 0;
263
2/2
✓ Branch 35 → 3 taken 2 times.
✓ Branch 35 → 36 taken 1 time.
3 while (copied < bytes)
264 {
265 2 const std::uintptr_t cur = addr + copied;
266 2 MEMORY_BASIC_INFORMATION mbi{};
267
1/2
✓ Branch 5 → 6 taken 2 times.
✗ Branch 5 → 8 not taken.
4 if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT ||
268
6/8
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 15 taken 1 time.
4 (mbi.Protect & WRITE_PERMISSION_FLAGS) == 0 || (mbi.Protect & NOACCESS_GUARD_FLAGS) != 0)
269
1/2
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 13 not taken.
1 return copied == 0 ? detail::GuardedWriteStatus::NotWritten
270 1 : detail::GuardedWriteStatus::MayBePartial;
271
272 1 const std::uintptr_t region_start = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress);
273 1 const std::uintptr_t region_end = region_start + mbi.RegionSize;
274
3/6
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 22 taken 1 time.
1 if (region_end < region_start || cur < region_start || cur >= region_end)
275
0/2
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ return copied == 0 ? detail::GuardedWriteStatus::NotWritten
276 ✗ : detail::GuardedWriteStatus::MayBePartial;
277
278 1 const std::size_t available = static_cast<std::size_t>(region_end - cur);
279 1 const std::size_t remaining = bytes - copied;
280
1/2
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 24 not taken.
1 const std::size_t to_copy = (remaining < available) ? remaining : available;
281 1 SIZE_T copied_now = 0;
282 1 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 1 ) != 0;
289 1 copied += copied_now;
290
2/4
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 29 not taken.
✗ Branch 28 → 29 not taken.
✓ Branch 28 → 33 taken 1 time.
1 if (!ok || copied_now != to_copy)
291
0/2
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
✗ return copied == 0 ? detail::GuardedWriteStatus::NotWritten
292 ✗ : detail::GuardedWriteStatus::MayBePartial;
293 }
294 1 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 // Constant-initialized and trivially destructible, so install and removal stay usable during static
310 // destruction (`[B-47]`).
311 SRWLOCK s_veh_lock = SRWLOCK_INIT;
312
313 /// Holds s_veh_lock exclusively for one scope.
314 struct VehLockGuard
315 {
316 28445 VehLockGuard() noexcept { AcquireSRWLockExclusive(&s_veh_lock); }
317 28445 ~VehLockGuard() noexcept { ReleaseSRWLockExclusive(&s_veh_lock); }
318
319 VehLockGuard(const VehLockGuard &) = delete;
320 VehLockGuard &operator=(const VehLockGuard &) = delete;
321 VehLockGuard(VehLockGuard &&) = delete;
322 VehLockGuard &operator=(VehLockGuard &&) = delete;
323 };
324
325 std::atomic<void *> s_veh_handle{nullptr};
326 std::atomic<bool> s_veh_session_retired{false};
327 // remove_veh_handler returns the index after the drain and the unlink, when no armed access can hold it. A
328 // failed install keeps the index until that return (Lifecycle.GuardedReadTlsIndexReturnsOnRelease).
329 std::atomic<DWORD> s_veh_tls_index{TLS_OUT_OF_INDEXES};
330
331 #if defined(DMK_ENABLE_TEST_SEAMS)
332 std::atomic<std::uint32_t> s_seam_veh_arm_failure_thread{0};
333 #endif
334
335 // Arms @p guard in this thread's slot. The first store of an index past the TEB's inline slots can fail. An
336 // unarmed access must not touch the foreign range.
337 3336033 [[nodiscard]] bool arm_veh_guard(DWORD slot, VehAccessGuard *guard) noexcept
338 {
339 #if defined(DMK_ENABLE_TEST_SEAMS)
340
2/2
✓ Branch 10 → 11 taken 5 times.
✓ Branch 10 → 12 taken 3336504 times.
3336572 if (s_seam_veh_arm_failure_thread.load(std::memory_order_relaxed) == GetCurrentThreadId())
341 5 return false;
342 #endif
343 3336504 return TlsSetValue(slot, guard) != FALSE;
344 }
345
346 // Cache-line-padded counters stripe current guarded-path accesses and avoid contention on one global line.
347 // The release_guarded_engine function drains the sum to zero before handler removal. The handle-null store,
348 // stripe increment, and drain loads use seq_cst under Dekker. An access that observes a live handle enters the
349 // count before the drain observes zero.
350 constexpr std::size_t VEH_IN_FLIGHT_STRIPE_COUNT = 64;
351
352 // alignas(64) needs no MSVC C4324 suppression here. The #ifndef _MSC_VER region hides this padded struct from
353 // every MSVC build.
354 struct alignas(64) VehInFlightStripe
355 {
356 std::atomic<int> count{0};
357 };
358
359 std::array<VehInFlightStripe, VEH_IN_FLIGHT_STRIPE_COUNT> s_veh_in_flight_stripes{};
360
361 // A stable Win32 thread ID selects this thread's in-flight stripe. The same stripe receives entry and exit,
362 // so its count stays nonnegative. GetCurrentThreadId allocates nothing and takes no lock, so loader lock
363 // permits it. A stripe collision adds contention but cannot cause a miscount.
364 3336891 [[nodiscard]] inline std::size_t veh_in_flight_stripe_index() noexcept
365 {
366 3336891 const std::uint64_t mixed = static_cast<std::uint64_t>(GetCurrentThreadId()) * 0x9E3779B97F4A7C15ULL;
367 3337147 return static_cast<std::size_t>(mixed >> 48) % VEH_IN_FLIGHT_STRIPE_COUNT;
368 }
369
370 // The sum of all in-flight stripes equals the guarded accesses on the handler path. After publication of
371 // s_veh_handle = nullptr, remove_veh_handler waits for this sum to reach zero under seq_cst.
372 172020 [[nodiscard]] inline int veh_in_flight_total() noexcept
373 {
374 172020 int total = 0;
375
2/2
✓ Branch 11 → 3 taken 11009280 times.
✓ Branch 11 → 12 taken 172020 times.
11181300 for (const VehInFlightStripe &stripe : s_veh_in_flight_stripes)
376 {
377 22018560 total += stripe.count.load(std::memory_order_seq_cst);
378 }
379 172020 return total;
380 }
381
382 // Return true when this thread already executes a guarded access. A nested access must not call
383 // ensure_veh_installed. A wait on s_veh_lock deadlocks with remove_veh_handler, which holds that lock until
384 // this thread exits. The omitted install loses nothing. After teardown, the seq_cst handle load routes a
385 // nested access to the fallback.
386 3337661 [[nodiscard]] inline bool inside_guarded_access() noexcept
387 {
388 3337770 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
389
2/2
✓ Branch 9 → 10 taken 10668 times.
✓ Branch 9 → 11 taken 3327102 times.
3337770 if (slot == TLS_OUT_OF_INDEXES)
390 10668 return false;
391 3327102 return TlsGetValue(slot) != nullptr;
392 }
393
394 // The handler redirects a thread with a fault into this recovery stub. __builtin_longjmp restores the paired
395 // __builtin_setjmp snapshot without an SEH stack unwind. That unwind can abort from a vectored-handler resume
396 // context. noinline gives the handler a stable target address.
397 10486 [[noreturn]] __attribute__((noinline)) void veh_perform_longjmp(void *env) noexcept
398 {
399 // __builtin_longjmp has type void(void **, int). env points to the VehAccessGuard::env[5] buffer. The
400 // explicit cast matches that signature. GCC accepts bare void *, but the Clang front end rejects it.
401 10486 __builtin_longjmp(static_cast<void **>(env), 1);
402 }
403
404 // The vectored exception handler claims only faults from a guarded access. The code must belong to the same
405 // set as the MSVC filters, and the record must contain an address within the armed foreign range. Every other
406 // fault passes through. A claimed fault redirects the thread to veh_perform_longjmp, which reports access
407 // failure.
408 12182 LONG NTAPI dmk_veh_read_handler(PEXCEPTION_POINTERS info) noexcept
409 {
410 12164 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
411
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12164 times.
12164 if (slot == TLS_OUT_OF_INDEXES)
412 ✗ return EXCEPTION_CONTINUE_SEARCH;
413
414 12164 auto *const guard = static_cast<VehAccessGuard *>(TlsGetValue(slot));
415
2/2
✓ Branch 12 → 13 taken 1696 times.
✓ Branch 12 → 14 taken 10467 times.
12163 if (guard == nullptr)
416 1696 return EXCEPTION_CONTINUE_SEARCH;
417
418 10467 const EXCEPTION_RECORD *const record = info->ExceptionRecord;
419
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 10396 times.
10467 if (!detail::is_guarded_read_fault(record->ExceptionCode))
420 ✗ return EXCEPTION_CONTINUE_SEARCH;
421
422 // Refuse a record without a fault address. A host RaiseException call that reuses one of these NTSTATUS
423 // codes must stay in host control flow.
424
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 10396 times.
10396 if (record->NumberParameters < 2)
425 ✗ return EXCEPTION_CONTINUE_SEARCH;
426
427 // Confine the claim to the armed foreign range. A defect outside it reaches the host handlers.
428 10396 const std::uintptr_t fault_address = static_cast<std::uintptr_t>(record->ExceptionInformation[1]);
429
2/4
✓ Branch 19 → 20 taken 10402 times.
✗ Branch 19 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 10404 times.
10396 if (fault_address < guard->guard_lo || fault_address >= guard->guard_hi)
430 ✗ return EXCEPTION_CONTINUE_SEARCH;
431 // Re-arm the host fence before the read returns a closed failure. See rearm_guard_page_if_consumed.
432
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 10396 times.
10404 if (!rearm_guard_page_if_consumed(record))
433 ✗ return EXCEPTION_CONTINUE_SEARCH;
434 10396 guard->fault_address = fault_address;
435
436 // Disarm before resume so a fault inside the longjmp stub passes through instead of a recursive claim.
437 10396 TlsSetValue(slot, nullptr);
438
439 // Resume the thread in veh_perform_longjmp(env). Set RIP to the stub and place the setjmp buffer in RCX.
440 // Entry uses an injected RIP change instead of CALL. Pre-align the fault-point RSP for the stub prologue.
441 // The stub reloads RSP from the snapshot.
442 10403 CONTEXT *const ctx = info->ContextRecord;
443 10403 ctx->Rsp = (ctx->Rsp & ~static_cast<DWORD64>(15)) - 8;
444 10403 ctx->Rcx = reinterpret_cast<DWORD64>(&guard->env);
445 10403 ctx->Rip = reinterpret_cast<DWORD64>(&veh_perform_longjmp);
446 10403 return EXCEPTION_CONTINUE_EXECUTION;
447 }
448
449 /** @brief Installs the handler with a reserved index. The caller holds s_veh_lock. */
450 11391 void install_veh_handler_locked() noexcept
451 {
452
1/2
✓ Branch 9 → 10 taken 11391 times.
✗ Branch 9 → 22 not taken.
11391 if (s_veh_tls_index.load(std::memory_order_relaxed) == TLS_OUT_OF_INDEXES)
453 {
454 11391 const DWORD slot = TlsAlloc();
455
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 11391 times.
11391 if (slot == TLS_OUT_OF_INDEXES)
456 ✗ return; // Guard setup failed. Access paths use their fail-closed fallback.
457 s_veh_tls_index.store(slot, std::memory_order_release);
458 }
459 // This handler is first, so a guarded access resolves through it before any consumer VEH or SEH.
460 // Every other fault passes through, so first position never starves the host handlers.
461 11391 void *const handle = AddVectoredExceptionHandler(1, dmk_veh_read_handler);
462 11391 s_veh_handle.store(handle, std::memory_order_release);
463 }
464
465 3337829 void ensure_veh_installed() noexcept
466 {
467
2/2
✓ Branch 3 → 4 taken 3326537 times.
✓ Branch 3 → 5 taken 11339 times.
3337829 if (s_veh_handle.load(std::memory_order_acquire) != nullptr)
468 3326575 return;
469
2/2
✓ Branch 6 → 7 taken 14 times.
✓ Branch 6 → 8 taken 11413 times.
11339 if (s_veh_session_retired.load(std::memory_order_acquire))
470 14 return;
471
472 11413 VehLockGuard lock;
473
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 11414 times.
11414 if (s_veh_session_retired.load(std::memory_order_relaxed))
474 ✗ return;
475
2/2
✓ Branch 13 → 14 taken 24 times.
✓ Branch 13 → 15 taken 11390 times.
11414 if (s_veh_handle.load(std::memory_order_relaxed) != nullptr)
476 24 return;
477 11390 install_veh_handler_locked();
478
2/2
✓ Branch 18 → 19 taken 11390 times.
✓ Branch 18 → 21 taken 24 times.
11414 }
479
480 14580 void remove_veh_handler(bool retire_session = false) noexcept
481 {
482 // A terminated thread can retain the control lock or an in-flight stripe.
483
2/2
✓ Branch 3 → 4 taken 4 times.
✓ Branch 3 → 5 taken 14576 times.
14580 if (detail::process_is_exiting())
484 4 return;
485 14576 VehLockGuard lock;
486
2/2
✓ Branch 6 → 7 taken 1637 times.
✓ Branch 6 → 8 taken 12939 times.
14576 if (retire_session)
487 1637 s_veh_session_retired.store(true, std::memory_order_release);
488 14576 void *const handle = s_veh_handle.load(std::memory_order_relaxed);
489
2/2
✓ Branch 9 → 10 taken 10443 times.
✓ Branch 9 → 21 taken 4133 times.
14576 if (handle != nullptr)
490 {
491 // Stop new guarded accesses, then wait for each access already committed to the handler path. No fault
492 // can arrive after handler removal. The seq_cst store pairs with the helpers' seq_cst stripe fetch_add
493 // and handle load under Dekker. The sum below cannot read zero while such an access is live.
494 10443 s_veh_handle.store(nullptr, std::memory_order_seq_cst);
495 10443 int spins = 0;
496
2/2
✓ Branch 19 → 12 taken 161577 times.
✓ Branch 19 → 20 taken 10443 times.
172020 while (veh_in_flight_total() > 0)
497 {
498
2/2
✓ Branch 12 → 13 taken 4127 times.
✓ Branch 12 → 14 taken 157450 times.
161577 if (spins < 4096)
499 4127 std::this_thread::yield();
500 else
501 157450 std::this_thread::sleep_for(std::chrono::microseconds(100));
502 161577 ++spins;
503 }
504 10443 RemoveVectoredExceptionHandler(handle);
505 }
506 // A failed AddVectoredExceptionHandler leaves an index with no handle, so the return runs on both paths.
507 // The reset precedes TlsFree, so a later install reserves a fresh index instead of one another module owns.
508 14576 const DWORD slot = s_veh_tls_index.load(std::memory_order_relaxed);
509
2/2
✓ Branch 28 → 29 taken 10443 times.
✓ Branch 28 → 38 taken 4133 times.
14576 if (slot != TLS_OUT_OF_INDEXES)
510 {
511 s_veh_tls_index.store(TLS_OUT_OF_INDEXES, std::memory_order_release);
512 10443 (void)TlsFree(slot);
513 }
514 14576 }
515
516 // Copy [src, src + len) into out under the vectored handler. Raw inline asm hides the single rep movsb from
517 // ASan, so this deliberate cross-region read cannot cause a false positive. The MSVC probe uses __movsb for
518 // the same reason. __builtin_setjmp records the recovery point. The handler uses longjmp so the setjmp
519 // expression returns nonzero. noinline keeps the read and its anchor in one frame.
520 __attribute__((noinline)) bool
521 3297822 veh_guarded_copy(void *out, const void *src, std::size_t len, volatile std::uintptr_t *fault_out) noexcept
522 {
523 3297391 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
524 // Read before the setjmp so it survives the longjmp return.
525 3297391 void *const enclosing = TlsGetValue(slot);
526 3297760 VehAccessGuard guard{};
527 3297760 guard.guard_lo = reinterpret_cast<std::uintptr_t>(src);
528 3297760 guard.guard_hi = guard.guard_lo + len;
529
530
2/2
✓ Branch 14 → 15 taken 4961 times.
✓ Branch 14 → 19 taken 3297428 times.
3302721 if (__builtin_setjmp(guard.env) != 0)
531 {
532 // This path runs only after the handler uses longjmp to contain a read fault.
533 4961 TlsSetValue(slot, enclosing);
534
2/2
✓ Branch 16 → 17 taken 46 times.
✓ Branch 16 → 18 taken 4901 times.
4947 if (fault_out != nullptr)
535 {
536 46 *fault_out = guard.fault_address;
537 }
538 4947 return false;
539 }
540
541 // Arm after the setjmp captures env and before the read. An unarmed copy takes the fallback, which never
542 // faults.
543
2/2
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 23 taken 3297777 times.
3297428 if (!arm_veh_guard(slot, &guard)) [[unlikely]]
544 2 return virtualquery_validated_copy(guard.guard_lo, out, len);
545
546 3297777 void *dst = out;
547 3297777 const void *cur = src;
548 3297777 std::size_t n = len;
549 3297777 __asm__ __volatile__("rep movsb" : "+D"(dst), "+S"(cur), "+c"(n) : : "memory");
550
551 3292968 TlsSetValue(slot, enclosing);
552 3293472 return true;
553 }
554
555 __attribute__((noinline)) detail::GuardedWriteStatus
556 8784 veh_guarded_write(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
557 {
558 8784 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
559 8784 void *const enclosing = TlsGetValue(slot);
560 8784 VehAccessGuard guard{};
561 8784 guard.guard_lo = address;
562 8784 guard.guard_hi = address + bytes;
563
564
2/2
✓ Branch 14 → 15 taken 417 times.
✓ Branch 14 → 20 taken 8784 times.
9201 if (__builtin_setjmp(guard.env) != 0)
565 {
566 417 TlsSetValue(slot, enclosing);
567
2/2
✓ Branch 16 → 17 taken 366 times.
✓ Branch 16 → 18 taken 51 times.
417 return guard.fault_address == address ? detail::GuardedWriteStatus::NotWritten
568 417 : detail::GuardedWriteStatus::MayBePartial;
569 }
570
571
2/2
✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 24 taken 8782 times.
8784 if (!arm_veh_guard(slot, &guard)) [[unlikely]]
572 2 return virtualquery_validated_write(address, source, bytes);
573 8782 copy_with_fault_progress(reinterpret_cast<void *>(address), source, bytes);
574 8365 TlsSetValue(slot, enclosing);
575 8365 return detail::GuardedWriteStatus::Ok;
576 }
577
578 // Run fn(ctx) with the vectored handler armed over [lo, hi) for an in-place access. fn must touch only that
579 // range because the handler does not claim other faults. A claimed fault abandons fn without destructor calls.
580 // fn must hold no resource whose release depends on stack unwind. It must not block indefinitely because
581 // teardown waits for its in-flight stripe count. fn can call a nested guarded access outside [lo, hi), and the
582 // nested wrapper restores this guard after that call returns.
583 __attribute__((noinline)) bool
584 29903 veh_guarded_region(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept
585 {
586 29903 const DWORD slot = s_veh_tls_index.load(std::memory_order_acquire);
587 29903 void *const enclosing = TlsGetValue(slot);
588 29903 VehAccessGuard guard{};
589 29903 guard.guard_lo = lo;
590 29903 guard.guard_hi = hi;
591
592
2/2
✓ Branch 14 → 15 taken 5100 times.
✓ Branch 14 → 17 taken 29903 times.
35003 if (__builtin_setjmp(guard.env) != 0)
593 {
594 5100 TlsSetValue(slot, enclosing);
595 5100 return false;
596 }
597
598 // An unarmed region fails closed before fn touches the range.
599
2/2
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 29902 times.
29903 if (!arm_veh_guard(slot, &guard)) [[unlikely]]
600 1 return false;
601 29902 fn(ctx);
602 24801 TlsSetValue(slot, enclosing);
603 24801 return true;
604 }
605
606 // This entry point serves all MinGW read paths. Reject a source range below the floor or across address-space
607 // wrap. A wrapped range inverts the handler guard check and lets a real fault escape. Count the read in the
608 // drain epoch around the path choice. Use the VirtualQuery copy when the handler is unavailable.
609 bool
610 3298858 veh_read_bytes(std::uintptr_t addr, void *out, std::size_t bytes, volatile std::uintptr_t *fault_out) noexcept
611 {
612
2/4
✓ Branch 2 → 3 taken 3299059 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3299137 times.
3298858 if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr)
613 ✗ return false;
614
615
1/2
✓ Branch 6 → 7 taken 3298563 times.
✗ Branch 6 → 8 not taken.
3299137 if (!inside_guarded_access())
616 {
617 3298563 ensure_veh_installed();
618 }
619
620 3298313 const std::size_t stripe = veh_in_flight_stripe_index();
621 3298308 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
622 3299553 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
623 // The VirtualQuery fallback never faults and has no fault address to report into fault_out.
624
1/2
✓ Branch 13 → 14 taken 3298076 times.
✗ Branch 13 → 16 not taken.
3297966 const bool ok = armed ? veh_guarded_copy(out, reinterpret_cast<const void *>(addr), bytes, fault_out)
625 3298171 : virtualquery_validated_copy(addr, out, bytes);
626 3298297 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
627 3298204 return ok;
628 }
629
630 8784 detail::GuardedWriteStatus veh_write_bytes(std::uintptr_t addr, const void *source, std::size_t bytes) noexcept
631 {
632
2/4
✓ Branch 2 → 3 taken 8784 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8784 times.
8784 if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr)
633 ✗ return detail::GuardedWriteStatus::NotWritten;
634
635
1/2
✓ Branch 6 → 7 taken 8784 times.
✗ Branch 6 → 8 not taken.
8784 if (!inside_guarded_access())
636 {
637 8784 ensure_veh_installed();
638 }
639
640 8784 const std::size_t stripe = veh_in_flight_stripe_index();
641 8784 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
642 8784 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
643 const detail::GuardedWriteStatus status =
644
1/2
✓ Branch 13 → 14 taken 8784 times.
✗ Branch 13 → 15 not taken.
8784 armed ? veh_guarded_write(addr, source, bytes) : virtualquery_validated_write(addr, source, bytes);
645 8784 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
646 8784 return status;
647 }
648 #endif // _WIN64
649 } // namespace
650 #endif // !_MSC_VER
651
652 #if !defined(_MSC_VER) && defined(_WIN64)
653 737 void detail::ensure_guarded_engine_installed() noexcept
654 {
655 737 ensure_veh_installed();
656 737 }
657
658 12942 void detail::release_guarded_engine() noexcept
659 {
660 12942 remove_veh_handler();
661 12942 }
662
663 1637 void detail::retire_session_guarded_engine() noexcept
664 {
665 1637 remove_veh_handler(true);
666 1637 }
667
668 2454 void detail::reopen_guarded_engine() noexcept
669 {
670 2454 VehLockGuard lock;
671 2454 s_veh_session_retired.store(false, std::memory_order_release);
672 2454 }
673
674 bool
675 29906 detail::run_guarded_region(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept
676 {
677 // An empty range or one with address-space wrap has nothing to guard. A wrapped [lo, hi) inverts the handler
678 // check.
679
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 29906 times.
29906 if (hi <= lo)
680 {
681 ✗ fn(ctx);
682 ✗ return true;
683 }
684
685
1/2
✓ Branch 6 → 7 taken 29906 times.
✗ Branch 6 → 8 not taken.
29906 if (!inside_guarded_access())
686 {
687 29906 ensure_veh_installed();
688 }
689
690 // Count the call in the drain epoch around the path choice, as veh_read_bytes does.
691 29906 const std::size_t stripe = veh_in_flight_stripe_index();
692 29906 s_veh_in_flight_stripes[stripe].count.fetch_add(1, std::memory_order_seq_cst);
693 29906 const bool armed = s_veh_handle.load(std::memory_order_seq_cst) != nullptr;
694 29906 bool completed = true;
695
2/2
✓ Branch 13 → 14 taken 29903 times.
✓ Branch 13 → 15 taken 3 times.
29906 if (armed)
696 {
697 29903 completed = veh_guarded_region(lo, hi, fn, ctx);
698 }
699 else
700 {
701 // The handler is unavailable. Do not run an in-place scan without a guard. The caller treats false as a
702 // skipped or faulted region and closes uniqueness-sensitive work.
703 3 completed = false;
704 }
705 29905 s_veh_in_flight_stripes[stripe].count.fetch_sub(1, std::memory_order_release);
706 29905 return completed;
707 }
708
709 namespace
710 {
711 /**
712 * @brief Runs an in-place update, with a call-scoped handler after Session retirement.
713 * @details The retired epoch refuses lazy installation. An update that installs the handler removes it before
714 * it returns, so a VmtHook that outlives its Session restores objects and leaves no handler. A failed
715 * installation leaves the region closed. A removal after a concurrent reopen is an ordinary release,
716 * and the next guarded access installs the handler again.
717 */
718 [[nodiscard]] bool
719 226 run_guarded_update(std::uintptr_t lo, std::uintptr_t hi, void (*fn)(void *) noexcept, void *ctx) noexcept
720 {
721 226 bool call_scoped = false;
722 // A nested access must not wait on the lock. See inside_guarded_access.
723
5/6
✓ Branch 3 → 4 taken 226 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 225 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 21 taken 225 times.
226 if (!inside_guarded_access() && s_veh_session_retired.load(std::memory_order_acquire))
724 {
725 1 VehLockGuard lock;
726
3/6
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 15 not taken.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 19 not taken.
2 if (s_veh_session_retired.load(std::memory_order_relaxed) &&
727 1 s_veh_handle.load(std::memory_order_relaxed) == nullptr)
728 {
729 1 install_veh_handler_locked();
730 1 call_scoped = true;
731 }
732 1 }
733 226 const bool completed = detail::run_guarded_region(lo, hi, fn, ctx);
734
2/2
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 24 taken 225 times.
226 if (call_scoped)
735 1 remove_veh_handler();
736 226 return completed;
737 }
738 } // namespace
739 #endif // !_MSC_VER && _WIN64
740
741 3299395 bool detail::guarded_read_bytes(
742 std::uintptr_t address,
743 void *out,
744 std::size_t bytes,
745 volatile std::uintptr_t *fault_address_out
746 ) noexcept
747 {
748 #if defined(DMK_ENABLE_TEST_SEAMS)
749
2/2
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 7 taken 3299090 times.
3299395 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
750 {
751 s_seam_guarded_read_calls.fetch_add(1, std::memory_order_relaxed);
752 }
753 #endif
754
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3299100 times.
3299100 if (bytes == 0)
755 ✗ return true;
756
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 3299100 times.
3299100 if (!out)
757 ✗ return false;
758
759 // Validate the complete half-open span [address, address + bytes) against the user-mode window before any
760 // read. A low-endpoint and wrap check alone admits a range that reaches the upper ceiling and causes a
761 // first-chance exception.
762
2/4
✓ Branch 11 → 12 taken 3299145 times.
✗ Branch 11 → 14 not taken.
✓ Branch 12 → 13 taken 3299172 times.
✗ Branch 12 → 14 not taken.
3299100 if (address < memory::USERSPACE_PTR_MIN || address + bytes < address ||
763
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 3299205 times.
3299172 address + bytes > memory::USERSPACE_PTR_MAX)
764 ✗ return false;
765
766 #ifdef _MSC_VER
767 __try
768 {
769 #if defined(__SANITIZE_ADDRESS__)
770 // Under ASan, MSVC routes std::memcpy through an interceptor that reports this valid foreign-memory probe
771 // as a false positive. Release keeps std::memcpy.
772 __movsb(static_cast<unsigned char *>(out), reinterpret_cast<const unsigned char *>(address), bytes);
773 #else
774 std::memcpy(out, reinterpret_cast<const void *>(address), bytes);
775 #endif
776 return true;
777 }
778 // Swallow only a fault whose address lies in the foreign source span. A caller-buffer fault or any address
779 // outside [address, address + bytes) identifies a caller or DMK defect and propagates.
780 __except (guarded_range_fault_filter(GetExceptionInformation(), address, address + bytes, fault_address_out))
781 {
782 return false;
783 }
784 #else
785 // On MinGW, read through the vectored fault guard. The success path uses one rep movsb and no system call.
786 3299205 return veh_read_bytes(address, out, bytes, fault_address_out);
787 #endif
788 }
789
790 namespace
791 {
792 // The raw fault-guarded store performs only the contained copy, with no argument validation or test-seam work.
793 // The guarded_write_bytes entry point and its forward-copy seam share this store and use the same fault path.
794 [[nodiscard]] detail::GuardedWriteStatus
795 8784 guarded_store_bytes(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
796 {
797 #ifdef _MSC_VER
798 volatile std::uintptr_t fault_address = 0;
799 __try
800 {
801 copy_with_fault_progress(reinterpret_cast<void *>(address), source, bytes);
802 return detail::GuardedWriteStatus::Ok;
803 }
804 // Do not contain a fault on the caller-owned source buffer. Qualify detail:: because unqualified lookup
805 // from this anonymous namespace does not reach it.
806 __except (
807 detail::guarded_range_fault_filter(GetExceptionInformation(), address, address + bytes, &fault_address)
808 )
809 {
810 return fault_address == address ? detail::GuardedWriteStatus::NotWritten
811 : detail::GuardedWriteStatus::MayBePartial;
812 }
813 #else
814 // On MinGW, write through the same guard and fallback split as guarded_read_bytes.
815 8784 return veh_write_bytes(address, source, bytes);
816 #endif
817 }
818 } // namespace
819
820 detail::GuardedWriteStatus
821 469 detail::guarded_write_bytes(std::uintptr_t address, const void *source, std::size_t bytes) noexcept
822 {
823 #if defined(DMK_ENABLE_TEST_SEAMS)
824
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 469 times.
469 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
825 {
826 s_seam_guarded_write_calls.fetch_add(1, std::memory_order_relaxed);
827 }
828 #endif
829
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 469 times.
469 if (bytes == 0)
830 ✗ return GuardedWriteStatus::Ok;
831
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 469 times.
469 if (!source)
832 ✗ return GuardedWriteStatus::NotWritten;
833
834 // Validate the complete half-open destination span against the user-mode window before any store. As in
835 // guarded_read_bytes, a low-endpoint and wrap check alone admits a range that reaches the ceiling.
836
4/4
✓ Branch 11 → 12 taken 468 times.
✓ Branch 11 → 14 taken 1 time.
✓ Branch 12 → 13 taken 465 times.
✓ Branch 12 → 14 taken 3 times.
469 if (address < memory::USERSPACE_PTR_MIN || address + bytes < address ||
837
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 464 times.
465 address + bytes > memory::USERSPACE_PTR_MAX)
838 5 return GuardedWriteStatus::NotWritten;
839
840 464 const auto *const in = static_cast<const std::byte *>(source);
841
842 #if defined(DMK_ENABLE_TEST_SEAMS)
843
2/2
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 28 taken 455 times.
464 if (s_seam_forward_copy)
844 {
845 9 std::size_t written = 0;
846
1/2
✓ Branch 21 → 17 taken 8329 times.
✗ Branch 21 → 22 not taken.
8329 for (; written < bytes; ++written)
847 {
848
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)
849 9 break;
850 }
851 9 s_seam_last_prefix = written;
852
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 9 times.
9 if (written == bytes)
853 ✗ return GuardedWriteStatus::Ok;
854
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 26 taken 8 times.
9 return written == 0 ? GuardedWriteStatus::NotWritten : GuardedWriteStatus::MayBePartial;
855 }
856 #endif
857
858 455 const GuardedWriteStatus status = guarded_store_bytes(address, in, bytes);
859 #if defined(DMK_ENABLE_TEST_SEAMS)
860
2/2
✓ Branch 29 → 30 taken 46 times.
✓ Branch 29 → 31 taken 409 times.
455 s_seam_last_prefix = status == GuardedWriteStatus::Ok ? bytes : 0;
861 #endif
862 455 return status;
863 }
864
865 namespace
866 {
867 struct CompareExchangeWordContext
868 {
869 std::uintptr_t address{0};
870 std::uintptr_t expected{0};
871 std::uintptr_t replacement{0};
872 std::uintptr_t observed{0};
873 };
874
875 226 void compare_exchange_word(void *raw_context) noexcept
876 {
877 226 auto *const context = static_cast<CompareExchangeWordContext *>(raw_context);
878 static_assert(sizeof(LONG64) == sizeof(std::uintptr_t));
879 896 const LONG64 observed = ::InterlockedCompareExchange64(
880 222 reinterpret_cast<volatile LONG64 *>(context->address),
881 226 std::bit_cast<LONG64>(context->replacement),
882 226 std::bit_cast<LONG64>(context->expected)
883 222 );
884 222 context->observed = std::bit_cast<std::uintptr_t>(observed);
885 222 }
886 } // namespace
887
888 228 bool detail::guarded_compare_exchange_word(
889 std::uintptr_t address,
890 std::uintptr_t expected,
891 std::uintptr_t replacement
892 ) noexcept
893 {
894 228 constexpr std::size_t word_bytes = sizeof(std::uintptr_t);
895
4/6
✓ Branch 2 → 3 taken 226 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 3 → 4 taken 226 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 226 times.
✗ Branch 4 → 6 not taken.
228 if (address % alignof(std::uintptr_t) != 0 || address < memory::USERSPACE_PTR_MIN ||
896
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 226 times.
226 address + word_bytes < address || address + word_bytes > memory::USERSPACE_PTR_MAX)
897 {
898 2 return false;
899 }
900
901 226 CompareExchangeWordContext context{address, expected, replacement, 0};
902 #ifdef _MSC_VER
903 __try
904 {
905 compare_exchange_word(&context);
906 }
907 __except (guarded_range_fault_filter(GetExceptionInformation(), address, address + word_bytes))
908 {
909 return false;
910 }
911 #else
912
2/2
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 222 times.
226 if (!run_guarded_update(address, address + word_bytes, &compare_exchange_word, &context))
913 {
914 4 return false;
915 }
916 #endif
917 222 return context.observed == expected;
918 }
919
920 35 detail::ChainWalkOutcome detail::guarded_resolve_chain(
921 Address base,
922 const memory::ChainStep *steps,
923 std::size_t count,
924 Address *trace,
925 std::size_t trace_cap
926 ) noexcept
927 {
928 35 ChainWalkOutcome outcome;
929
930 // When count == 0, the identity walk returns base itself and performs no dereference or screen.
931
2/2
✓ Branch 2 → 3 taken 7 times.
✓ Branch 2 → 4 taken 28 times.
35 if (count == 0)
932 {
933 7 outcome.address = base;
934 7 outcome.ok = true;
935 7 return outcome;
936 }
937
938 // Both toolchains use one walk. The range-aware guarded byte copy reads each link. checked_offset rejects a
939 // hop when signed offset addition wraps the address space.
940 28 std::uintptr_t cur = base.raw();
941
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)
942 {
943 63 std::uintptr_t link_address = 0;
944
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 63 times.
63 if (!checked_offset(cur, steps[i].offset, link_address))
945 {
946 ✗ outcome.fail_index = i;
947 7 return outcome;
948 }
949 63 std::uintptr_t next = 0;
950
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 62 times.
63 if (!guarded_read_bytes(link_address, &next, sizeof(next)))
951 {
952 1 outcome.fail_index = i;
953 1 return outcome;
954 }
955 // Screen the dereferenced link against this hop's floor and the user-mode ceiling before use as the next
956 // dereference base.
957
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)
958 {
959 6 outcome.fail_index = i;
960 6 return outcome;
961 }
962
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)
963 3 trace[i] = Address{next};
964 56 cur = next;
965 }
966 21 std::uintptr_t leaf = 0;
967
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 ||
968
2/2
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 31 taken 18 times.
19 leaf >= memory::USERSPACE_PTR_MAX)
969 {
970 3 outcome.fail_index = count - 1;
971 3 return outcome;
972 }
973
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)
974 1 trace[count - 1] = Address{leaf};
975 18 outcome.address = Address{leaf};
976 18 outcome.ok = true;
977 18 return outcome;
978 }
979
980 #if defined(DMK_ENABLE_TEST_SEAMS)
981 2648 void detail::note_protection_call_for_test() noexcept
982 {
983
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 2648 times.
2648 if (s_seam_observe_guarded_access.load(std::memory_order_relaxed))
984 {
985 s_seam_protection_calls.fetch_add(1, std::memory_order_relaxed);
986 }
987 2648 }
988
989 18 void detail::reset_guarded_access_observation_for_test() noexcept
990 {
991 s_seam_guarded_read_calls.store(0, std::memory_order_relaxed);
992 s_seam_guarded_write_calls.store(0, std::memory_order_relaxed);
993 s_seam_protection_calls.store(0, std::memory_order_relaxed);
994 18 s_seam_observe_guarded_access.store(true, std::memory_order_release);
995 18 }
996
997 18 detail::GuardedAccessObservation detail::guarded_access_observation_for_test() noexcept
998 {
999 return GuardedAccessObservation{
1000 18 s_seam_guarded_read_calls.load(std::memory_order_relaxed),
1001 18 s_seam_guarded_write_calls.load(std::memory_order_relaxed),
1002 18 s_seam_protection_calls.load(std::memory_order_relaxed)
1003 54 };
1004 }
1005
1006 18 void detail::stop_guarded_access_observation_for_test() noexcept
1007 {
1008 18 s_seam_observe_guarded_access.store(false, std::memory_order_release);
1009 18 }
1010
1011 18 void detail::set_forward_copy_seam(bool enable) noexcept
1012 {
1013 18 s_seam_forward_copy = enable;
1014 18 }
1015
1016 9 std::size_t detail::last_forward_copy_prefix() noexcept
1017 {
1018 9 return s_seam_last_prefix;
1019 }
1020
1021 ✗ void detail::set_guard_rearm_failure_seam(bool fail) noexcept
1022 {
1023 ✗ s_seam_guard_rearm_fails.store(fail, std::memory_order_relaxed);
1024 ✗ }
1025
1026 #if !defined(_MSC_VER) && defined(_WIN64)
1027 17 std::uint32_t detail::guarded_engine_tls_index_for_test() noexcept
1028 {
1029 17 return s_veh_tls_index.load(std::memory_order_acquire);
1030 }
1031
1032 1 void detail::with_guarded_engine_lock_for_test(void (*callback)(void *) noexcept, void *context) noexcept
1033 {
1034 1 VehLockGuard lock;
1035 1 callback(context);
1036 ✗ }
1037
1038 2 void detail::set_guard_arm_failure_for_test(bool fail) noexcept
1039 {
1040
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 1 time.
2 s_seam_veh_arm_failure_thread.store(fail ? GetCurrentThreadId() : 0, std::memory_order_relaxed);
1041 2 }
1042 #endif
1043 #endif
1044 } // namespace DetourModKit
1045