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 |