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 | 10386 | if (record->ExceptionCode != GUARD_PAGE_FAULT_CODE) | |
| 59 | { | ||
| 60 | 10386 | 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 | 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 | 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 | 84 | if (offset >= 0) | |
| 135 | { | ||
| 136 | 81 | const std::uintptr_t delta = static_cast<std::uintptr_t>(offset); | |
| 137 | 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 | 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 | 34 | while (copied < bytes) | |
| 222 | { | ||
| 223 | 18 | const std::uintptr_t cur = addr + copied; | |
| 224 | 18 | MEMORY_BASIC_INFORMATION mbi{}; | |
| 225 | 18 | if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi))) | |
| 226 | 2 | return false; | |
| 227 | 18 | if (mbi.State != MEM_COMMIT) | |
| 228 | ✗ | return false; | |
| 229 | 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 | 16 | if (region_end < region_start) | |
| 235 | ✗ | return false; | |
| 236 | 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 | 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 | 32 | ) || | |
| 250 | 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 | 3 | while (copied < bytes) | |
| 264 | { | ||
| 265 | 2 | const std::uintptr_t cur = addr + copied; | |
| 266 | 2 | MEMORY_BASIC_INFORMATION mbi{}; | |
| 267 | 4 | if (!VirtualQuery(reinterpret_cast<const void *>(cur), &mbi, sizeof(mbi)) || mbi.State != MEM_COMMIT || | |
| 268 | 4 | (mbi.Protect & WRITE_PERMISSION_FLAGS) == 0 || (mbi.Protect & NOACCESS_GUARD_FLAGS) != 0) | |
| 269 | 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 | 1 | if (region_end < region_start || cur < region_start || cur >= region_end) | |
| 275 | ✗ | 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 | 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 | 1 | if (!ok || copied_now != to_copy) | |
| 291 | ✗ | 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 | 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 | 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 | 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 | 12164 | if (slot == TLS_OUT_OF_INDEXES) | |
| 412 | ✗ | return EXCEPTION_CONTINUE_SEARCH; | |
| 413 | |||
| 414 | 12164 | auto *const guard = static_cast<VehAccessGuard *>(TlsGetValue(slot)); | |
| 415 | 12163 | if (guard == nullptr) | |
| 416 | 1696 | return EXCEPTION_CONTINUE_SEARCH; | |
| 417 | |||
| 418 | 10467 | const EXCEPTION_RECORD *const record = info->ExceptionRecord; | |
| 419 | 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 | 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 | 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 | 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 | 11391 | if (s_veh_tls_index.load(std::memory_order_relaxed) == TLS_OUT_OF_INDEXES) | |
| 453 | { | ||
| 454 | 11391 | const DWORD slot = TlsAlloc(); | |
| 455 | 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 | 3337829 | if (s_veh_handle.load(std::memory_order_acquire) != nullptr) | |
| 468 | 3326575 | return; | |
| 469 | 11339 | if (s_veh_session_retired.load(std::memory_order_acquire)) | |
| 470 | 14 | return; | |
| 471 | |||
| 472 | 11413 | VehLockGuard lock; | |
| 473 | 11414 | if (s_veh_session_retired.load(std::memory_order_relaxed)) | |
| 474 | ✗ | return; | |
| 475 | 11414 | if (s_veh_handle.load(std::memory_order_relaxed) != nullptr) | |
| 476 | 24 | return; | |
| 477 | 11390 | install_veh_handler_locked(); | |
| 478 | 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 | 14580 | if (detail::process_is_exiting()) | |
| 484 | 4 | return; | |
| 485 | 14576 | VehLockGuard lock; | |
| 486 | 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 | 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 | 172020 | while (veh_in_flight_total() > 0) | |
| 497 | { | ||
| 498 | 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 | 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 | 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 | 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 | 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 | 9201 | if (__builtin_setjmp(guard.env) != 0) | |
| 565 | { | ||
| 566 | 417 | TlsSetValue(slot, enclosing); | |
| 567 | 417 | return guard.fault_address == address ? detail::GuardedWriteStatus::NotWritten | |
| 568 | 417 | : detail::GuardedWriteStatus::MayBePartial; | |
| 569 | } | ||
| 570 | |||
| 571 | 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 | 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 | 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 | 3298858 | if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr) | |
| 613 | ✗ | return false; | |
| 614 | |||
| 615 | 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 | 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 | 8784 | if (addr < memory::USERSPACE_PTR_MIN || addr + bytes < addr) | |
| 633 | ✗ | return detail::GuardedWriteStatus::NotWritten; | |
| 634 | |||
| 635 | 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 | 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 | 29906 | if (hi <= lo) | |
| 680 | { | ||
| 681 | ✗ | fn(ctx); | |
| 682 | ✗ | return true; | |
| 683 | } | ||
| 684 | |||
| 685 | 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 | 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 | 226 | if (!inside_guarded_access() && s_veh_session_retired.load(std::memory_order_acquire)) | |
| 724 | { | ||
| 725 | 1 | VehLockGuard lock; | |
| 726 | 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 | 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 | 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 | 3299100 | if (bytes == 0) | |
| 755 | ✗ | return true; | |
| 756 | 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 | 3299100 | if (address < memory::USERSPACE_PTR_MIN || address + bytes < address || | |
| 763 | 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 | 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 | 469 | if (bytes == 0) | |
| 830 | ✗ | return GuardedWriteStatus::Ok; | |
| 831 | 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 | 469 | if (address < memory::USERSPACE_PTR_MIN || address + bytes < address || | |
| 837 | 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 | 464 | if (s_seam_forward_copy) | |
| 844 | { | ||
| 845 | 9 | std::size_t written = 0; | |
| 846 | 8329 | for (; written < bytes; ++written) | |
| 847 | { | ||
| 848 | 8329 | if (guarded_store_bytes(address + written, in + written, 1) != GuardedWriteStatus::Ok) | |
| 849 | 9 | break; | |
| 850 | } | ||
| 851 | 9 | s_seam_last_prefix = written; | |
| 852 | 9 | if (written == bytes) | |
| 853 | ✗ | return GuardedWriteStatus::Ok; | |
| 854 | 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 | 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 | 228 | if (address % alignof(std::uintptr_t) != 0 || address < memory::USERSPACE_PTR_MIN || | |
| 896 | 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 | 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 | 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 | 84 | for (std::size_t i = 0; i + 1 < count; ++i) | |
| 942 | { | ||
| 943 | 63 | std::uintptr_t link_address = 0; | |
| 944 | 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 | 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 | 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 | 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 | 40 | if (!checked_offset(cur, steps[count - 1].offset, leaf) || leaf < memory::USERSPACE_PTR_MIN || | |
| 968 | 19 | leaf >= memory::USERSPACE_PTR_MAX) | |
| 969 | { | ||
| 970 | 3 | outcome.fail_index = count - 1; | |
| 971 | 3 | return outcome; | |
| 972 | } | ||
| 973 | 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 | 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 | s_seam_veh_arm_failure_thread.store(fail ? GetCurrentThreadId() : 0, std::memory_order_relaxed); | |
| 1041 | 2 | } | |
| 1042 | #endif | ||
| 1043 | #endif | ||
| 1044 | } // namespace DetourModKit | ||
| 1045 |