src/internal/memory_protect_ledger.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file memory_protect_ledger.cpp | ||
| 3 | * @brief This TU owns the page-protection transaction ledger and the patch path that changes protection. | ||
| 4 | * | ||
| 5 | * The fault-containment engine (SEH/VEH guarded byte access) stays in memory_guarded.cpp. patch_bytes reaches its | ||
| 6 | * guarded copy through the detail:: seam in internal/memory_guarded.hpp. The ledger serializes every protection | ||
| 7 | * transaction. Guards that overlap restore the true original protection. | ||
| 8 | */ | ||
| 9 | |||
| 10 | #include "internal/memory_guarded.hpp" | ||
| 11 | |||
| 12 | #include "internal/hook_coordinator.hpp" | ||
| 13 | |||
| 14 | #include "DetourModKit/memory.hpp" | ||
| 15 | |||
| 16 | #include <windows.h> | ||
| 17 | |||
| 18 | #include <algorithm> | ||
| 19 | #include <cstddef> | ||
| 20 | #include <cstdint> | ||
| 21 | #include <new> | ||
| 22 | #include <unordered_map> | ||
| 23 | #include <vector> | ||
| 24 | |||
| 25 | namespace DetourModKit | ||
| 26 | { | ||
| 27 | namespace | ||
| 28 | { | ||
| 29 | // These page protections carry execute. A writable protection derived for a patch must preserve execute for a | ||
| 30 | // code region and must NOT add it to a data region. | ||
| 31 | constexpr DWORD EXECUTE_PERMISSION_FLAGS = | ||
| 32 | PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; | ||
| 33 | |||
| 34 | // Derive writable protection from the current value. An executable region keeps execute, so DEP permits its | ||
| 35 | // next execution. A data page never gains execute. | ||
| 36 | 37 | [[nodiscard]] DWORD writable_protection_for(DWORD current) noexcept | |
| 37 | { | ||
| 38 | 37 | return (current & EXECUTE_PERMISSION_FLAGS) != 0 ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; | |
| 39 | } | ||
| 40 | |||
| 41 | 6574 | [[nodiscard]] std::uintptr_t system_page_size() noexcept | |
| 42 | { | ||
| 43 | 38 | static const std::uintptr_t size = []() noexcept | |
| 44 | { | ||
| 45 | 38 | SYSTEM_INFO info{}; | |
| 46 | 38 | GetSystemInfo(&info); | |
| 47 | 38 | return static_cast<std::uintptr_t>(info.dwPageSize); | |
| 48 | 6574 | }(); | |
| 49 | 6574 | return size; | |
| 50 | } | ||
| 51 | |||
| 52 | 2651 | [[nodiscard]] std::uintptr_t page_floor(std::uintptr_t address) noexcept | |
| 53 | { | ||
| 54 | 2651 | return address & ~(system_page_size() - 1); | |
| 55 | } | ||
| 56 | |||
| 57 | 1330 | [[nodiscard]] std::uintptr_t page_ceiling(std::uintptr_t address) noexcept | |
| 58 | { | ||
| 59 | 1330 | const std::uintptr_t mask = system_page_size() - 1; | |
| 60 | 1330 | return (address + mask) & ~mask; | |
| 61 | } | ||
| 62 | |||
| 63 | struct PageProtectionHolder | ||
| 64 | { | ||
| 65 | std::uint64_t transaction_id = 0; | ||
| 66 | DWORD target_protection = 0; | ||
| 67 | }; | ||
| 68 | |||
| 69 | // The first holder stays inline because disjoint guards are common. Additional holders preserve acquisition | ||
| 70 | // order so removal of an inner transaction restores the newest live target. | ||
| 71 | struct PageProtectionState | ||
| 72 | { | ||
| 73 | DWORD original_protection = 0; | ||
| 74 | PageProtectionHolder first_holder{}; | ||
| 75 | std::vector<PageProtectionHolder> newer_holders; | ||
| 76 | |||
| 77 | 3685 | [[nodiscard]] bool empty() const noexcept { return first_holder.transaction_id == 0; } | |
| 78 | |||
| 79 | 1035 | void add_holder(PageProtectionHolder holder) | |
| 80 | { | ||
| 81 | 1035 | if (empty()) | |
| 82 | { | ||
| 83 | ✗ | first_holder = holder; | |
| 84 | ✗ | return; | |
| 85 | } | ||
| 86 | 1035 | newer_holders.push_back(holder); | |
| 87 | } | ||
| 88 | |||
| 89 | [[nodiscard]] bool | ||
| 90 | 1330 | remove_holder(std::uint64_t transaction_id, DWORD *removed_target, DWORD *desired_protection) noexcept | |
| 91 | { | ||
| 92 | 1330 | DWORD target = 0; | |
| 93 | 1330 | if (first_holder.transaction_id == transaction_id) | |
| 94 | { | ||
| 95 | 1324 | target = first_holder.target_protection; | |
| 96 | 1324 | if (newer_holders.empty()) | |
| 97 | { | ||
| 98 | 295 | first_holder = {}; | |
| 99 | } | ||
| 100 | else | ||
| 101 | { | ||
| 102 | 1029 | first_holder = newer_holders.front(); | |
| 103 | 2058 | newer_holders.erase(newer_holders.begin()); | |
| 104 | } | ||
| 105 | } | ||
| 106 | else | ||
| 107 | { | ||
| 108 | 6 | const auto holder = std::find_if( | |
| 109 | newer_holders.begin(), | ||
| 110 | newer_holders.end(), | ||
| 111 | 7 | [transaction_id](const PageProtectionHolder &candidate) noexcept | |
| 112 | 7 | { return candidate.transaction_id == transaction_id; } | |
| 113 | ); | ||
| 114 | 12 | if (holder == newer_holders.end()) | |
| 115 | { | ||
| 116 | ✗ | return false; | |
| 117 | } | ||
| 118 | 6 | target = holder->target_protection; | |
| 119 | 12 | newer_holders.erase(holder); | |
| 120 | } | ||
| 121 | |||
| 122 | 1330 | if (removed_target != nullptr) | |
| 123 | { | ||
| 124 | 2 | *removed_target = target; | |
| 125 | } | ||
| 126 | 1330 | if (desired_protection != nullptr) | |
| 127 | { | ||
| 128 | 2355 | *desired_protection = empty() ? original_protection | |
| 129 | 1035 | : (newer_holders.empty() ? first_holder.target_protection | |
| 130 | 598 | : newer_holders.back().target_protection); | |
| 131 | } | ||
| 132 | 1330 | return true; | |
| 133 | } | ||
| 134 | }; | ||
| 135 | |||
| 136 | SRWLOCK s_protection_ledger_lock = SRWLOCK_INIT; | ||
| 137 | std::uint64_t s_next_transaction_id = 1; | ||
| 138 | |||
| 139 | class ProtectionLedgerLock | ||
| 140 | { | ||
| 141 | public: | ||
| 142 | 2500 | ProtectionLedgerLock() noexcept { AcquireSRWLockExclusive(&s_protection_ledger_lock); } | |
| 143 | |||
| 144 | 2500 | ~ProtectionLedgerLock() noexcept { ReleaseSRWLockExclusive(&s_protection_ledger_lock); } | |
| 145 | |||
| 146 | ProtectionLedgerLock(const ProtectionLedgerLock &) = delete; | ||
| 147 | ProtectionLedgerLock &operator=(const ProtectionLedgerLock &) = delete; | ||
| 148 | ProtectionLedgerLock(ProtectionLedgerLock &&) = delete; | ||
| 149 | ProtectionLedgerLock &operator=(ProtectionLedgerLock &&) = delete; | ||
| 150 | }; | ||
| 151 | |||
| 152 | // The ledger intentionally has process lifetime so late teardown cannot observe a destroyed registry. Its | ||
| 153 | // constructor allocates on the MSVC STL, so a first-use failure latches a null ledger and every transaction | ||
| 154 | // fails closed (`[B-91]`). Lifecycle.ProtectionLedgerFirstUseOomFailsClosed pins the latch. | ||
| 155 | 3849 | [[nodiscard]] std::unordered_map<std::uintptr_t, PageProtectionState> *protection_ledger() noexcept | |
| 156 | { | ||
| 157 | static std::unordered_map<std::uintptr_t, PageProtectionState> *const ledger = | ||
| 158 | 39 | []() noexcept -> std::unordered_map<std::uintptr_t, PageProtectionState> * | |
| 159 | { | ||
| 160 | try | ||
| 161 | { | ||
| 162 | 39 | return new std::unordered_map<std::uintptr_t, PageProtectionState>(); | |
| 163 | } | ||
| 164 | 1 | catch (...) | |
| 165 | { | ||
| 166 | 1 | return nullptr; | |
| 167 | 1 | } | |
| 168 | 3849 | }(); | |
| 169 | 3849 | return ledger; | |
| 170 | } | ||
| 171 | |||
| 172 | 1254 | [[nodiscard]] std::uint64_t next_transaction_id() noexcept | |
| 173 | { | ||
| 174 | 1254 | const std::uint64_t id = s_next_transaction_id++; | |
| 175 | 1254 | if (s_next_transaction_id == 0) | |
| 176 | { | ||
| 177 | ✗ | s_next_transaction_id = 1; | |
| 178 | } | ||
| 179 | 1254 | return id; | |
| 180 | } | ||
| 181 | |||
| 182 | 9 | void ledger_cancel_pages(std::uintptr_t page_lo, std::uintptr_t page_hi, std::uint64_t transaction_id) noexcept | |
| 183 | { | ||
| 184 | 9 | auto *const ledger = protection_ledger(); | |
| 185 | 9 | if (ledger == nullptr) | |
| 186 | { | ||
| 187 | ✗ | return; | |
| 188 | } | ||
| 189 | 9 | const std::uintptr_t step = system_page_size(); | |
| 190 | 17 | for (std::uintptr_t page = page_lo; page < page_hi; page += step) | |
| 191 | { | ||
| 192 | 8 | const auto entry = ledger->find(page); | |
| 193 | 8 | if (entry == ledger->end()) | |
| 194 | { | ||
| 195 | ✗ | continue; | |
| 196 | } | ||
| 197 | 8 | if (entry->second.remove_holder(transaction_id, nullptr, nullptr) && entry->second.empty()) | |
| 198 | { | ||
| 199 | 8 | ledger->erase(entry); | |
| 200 | } | ||
| 201 | } | ||
| 202 | } | ||
| 203 | |||
| 204 | 1330 | [[nodiscard]] bool ledger_acquire_pages( | |
| 205 | std::uintptr_t page_lo, | ||
| 206 | std::uintptr_t page_hi, | ||
| 207 | std::uint64_t transaction_id, | ||
| 208 | DWORD target_protection, | ||
| 209 | DWORD current_original | ||
| 210 | ) noexcept | ||
| 211 | { | ||
| 212 | 1330 | auto *const ledger = protection_ledger(); | |
| 213 | 1330 | if (ledger == nullptr) | |
| 214 | { | ||
| 215 | ✗ | return false; | |
| 216 | } | ||
| 217 | 1330 | const std::uintptr_t step = system_page_size(); | |
| 218 | 1330 | std::uintptr_t page = page_lo; | |
| 219 | try | ||
| 220 | { | ||
| 221 | 2660 | for (; page < page_hi; page += step) | |
| 222 | { | ||
| 223 | 1331 | auto entry = ledger->find(page); | |
| 224 | 1331 | if (entry == ledger->end()) | |
| 225 | { | ||
| 226 | 296 | PageProtectionState state{}; | |
| 227 | 296 | state.original_protection = current_original; | |
| 228 | 296 | state.first_holder = PageProtectionHolder{transaction_id, target_protection}; | |
| 229 | 592 | entry = ledger->try_emplace(page, std::move(state)).first; | |
| 230 | 296 | } | |
| 231 | else | ||
| 232 | { | ||
| 233 | 1035 | entry->second.add_holder(PageProtectionHolder{transaction_id, target_protection}); | |
| 234 | } | ||
| 235 | } | ||
| 236 | } | ||
| 237 | 1 | catch (...) | |
| 238 | { | ||
| 239 | 1 | ledger_cancel_pages(page_lo, page, transaction_id); | |
| 240 | 1 | return false; | |
| 241 | 1 | } | |
| 242 | 1329 | return true; | |
| 243 | } | ||
| 244 | |||
| 245 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 246 | // Thread-local seams isolate one test's injection from another thread's guarded operation. | ||
| 247 | thread_local bool s_seam_flush_fails = false; | ||
| 248 | thread_local detail::InstructionFlushObservation s_seam_flush_observation{}; | ||
| 249 | thread_local bool s_seam_patch_write_not_written = false; | ||
| 250 | thread_local std::uint64_t s_seam_virtual_protect_failures = 0; | ||
| 251 | thread_local std::size_t s_seam_virtual_protect_call = 0; | ||
| 252 | #endif | ||
| 253 | |||
| 254 | [[nodiscard]] bool | ||
| 255 | 2648 | change_page_protection(LPVOID address, SIZE_T bytes, DWORD protection, DWORD *previous) noexcept | |
| 256 | { | ||
| 257 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 258 | 2648 | detail::note_protection_call_for_test(); | |
| 259 | 2648 | const std::size_t call = s_seam_virtual_protect_call++; | |
| 260 | 2648 | if (call < 64 && (s_seam_virtual_protect_failures & (std::uint64_t{1} << call)) != 0) | |
| 261 | { | ||
| 262 | 11 | SetLastError(ERROR_ACCESS_DENIED); | |
| 263 | 11 | return false; | |
| 264 | } | ||
| 265 | #endif | ||
| 266 | 2637 | return VirtualProtect(address, bytes, protection, previous) != 0; | |
| 267 | } | ||
| 268 | |||
| 269 | // Release each transaction holder and restore the newest live target, or the original protection when no | ||
| 270 | // holder remains. Attempt every page so one failure cannot strand unrelated pages. Erase each holder-free entry | ||
| 271 | // after its final restore attempt. A retained entry gives an obsolete baseline to the next guard over that | ||
| 272 | // page. Proof: MemoryTest.MemoryProtectGuardProof_FailedRestoreDoesNotPoisonDifferentLaterBaseline. | ||
| 273 | 1252 | [[nodiscard]] bool restore_segments_locked( | |
| 274 | const detail::ProtectionSegment *segments, | ||
| 275 | std::size_t count, | ||
| 276 | std::uint32_t &os_error | ||
| 277 | ) noexcept | ||
| 278 | { | ||
| 279 | 1252 | auto *const ledger = protection_ledger(); | |
| 280 | 1252 | if (ledger == nullptr) | |
| 281 | { | ||
| 282 | ✗ | os_error = ERROR_NOT_ENOUGH_MEMORY; | |
| 283 | ✗ | return false; | |
| 284 | } | ||
| 285 | |||
| 286 | 1252 | bool all_restored = true; | |
| 287 | 1252 | const std::uintptr_t step = system_page_size(); | |
| 288 | 2571 | for (std::size_t i = 0; i < count; ++i) | |
| 289 | { | ||
| 290 | 1319 | std::uintptr_t run_begin = 0; | |
| 291 | 1319 | std::uintptr_t run_end = 0; | |
| 292 | 1319 | DWORD run_protection = 0; | |
| 293 | 1319 | bool in_run = false; | |
| 294 | 1319 | const auto restore_run = [&]() noexcept | |
| 295 | { | ||
| 296 | 1319 | if (!in_run) | |
| 297 | { | ||
| 298 | ✗ | return; | |
| 299 | } | ||
| 300 | 1319 | DWORD previous = 0; | |
| 301 | 1319 | if (!change_page_protection( | |
| 302 | 1319 | reinterpret_cast<LPVOID>(run_begin), | |
| 303 | 1319 | run_end - run_begin, | |
| 304 | run_protection, | ||
| 305 | &previous | ||
| 306 | )) | ||
| 307 | { | ||
| 308 | 7 | if (all_restored) | |
| 309 | { | ||
| 310 | 7 | os_error = static_cast<std::uint32_t>(GetLastError()); | |
| 311 | } | ||
| 312 | 7 | all_restored = false; | |
| 313 | } | ||
| 314 | 2639 | for (std::uintptr_t page = run_begin; page < run_end; page += step) | |
| 315 | { | ||
| 316 | 1320 | const auto entry = ledger->find(page); | |
| 317 | 1320 | if (entry != ledger->end() && entry->second.empty()) | |
| 318 | { | ||
| 319 | 285 | ledger->erase(entry); | |
| 320 | } | ||
| 321 | } | ||
| 322 | 1319 | in_run = false; | |
| 323 | 1319 | }; | |
| 324 | |||
| 325 | 1319 | const std::uintptr_t seg_end = segments[i].base + segments[i].size; | |
| 326 | 2639 | for (std::uintptr_t page = page_floor(segments[i].base); page < seg_end; page += step) | |
| 327 | { | ||
| 328 | 1320 | const auto entry = ledger->find(page); | |
| 329 | 1320 | DWORD desired = 0; | |
| 330 | 2640 | if (entry == ledger->end() || | |
| 331 | 1320 | !entry->second.remove_holder(segments[i].transaction_id, nullptr, &desired)) | |
| 332 | { | ||
| 333 | ✗ | restore_run(); | |
| 334 | ✗ | if (all_restored) | |
| 335 | { | ||
| 336 | ✗ | os_error = ERROR_INVALID_DATA; | |
| 337 | } | ||
| 338 | ✗ | all_restored = false; | |
| 339 | ✗ | continue; | |
| 340 | } | ||
| 341 | |||
| 342 | 1320 | if (in_run && desired != run_protection) | |
| 343 | { | ||
| 344 | ✗ | restore_run(); | |
| 345 | } | ||
| 346 | 1320 | if (!in_run) | |
| 347 | { | ||
| 348 | 1319 | run_begin = page; | |
| 349 | 1319 | run_protection = desired; | |
| 350 | 1319 | in_run = true; | |
| 351 | } | ||
| 352 | 1320 | run_end = page + step; | |
| 353 | } | ||
| 354 | 1319 | restore_run(); | |
| 355 | } | ||
| 356 | 1252 | return all_restored; | |
| 357 | } | ||
| 358 | } // anonymous namespace | ||
| 359 | |||
| 360 | 1242 | bool detail::restore_across_regions( | |
| 361 | const ProtectionSegment *segments, | ||
| 362 | std::size_t count, | ||
| 363 | std::uint32_t &os_error | ||
| 364 | ) noexcept | ||
| 365 | { | ||
| 366 | // The coordinator excludes a backend trap window, whose transient protection this restore must not interleave | ||
| 367 | // with (`[B-18]`). A refused coordinator still restores, because a page left writable is the worse outcome. | ||
| 368 | 1242 | const detail::BackendCoordinatorHold coordinator; | |
| 369 | 1242 | ProtectionLedgerLock lock; | |
| 370 | 1242 | return restore_segments_locked(segments, count, os_error); | |
| 371 | 1242 | } | |
| 372 | |||
| 373 | 2 | void detail::abandon_protection_tracking(const ProtectionSegment *segments, std::size_t count) noexcept | |
| 374 | { | ||
| 375 | 2 | ProtectionLedgerLock lock; | |
| 376 | 2 | auto *const ledger = protection_ledger(); | |
| 377 | 2 | if (ledger == nullptr) | |
| 378 | { | ||
| 379 | ✗ | return; | |
| 380 | } | ||
| 381 | 2 | const std::uintptr_t step = system_page_size(); | |
| 382 | 4 | for (std::size_t i = 0; i < count; ++i) | |
| 383 | { | ||
| 384 | 2 | const std::uintptr_t seg_end = segments[i].base + segments[i].size; | |
| 385 | 4 | for (std::uintptr_t page = page_floor(segments[i].base); page < seg_end; page += step) | |
| 386 | { | ||
| 387 | 2 | const auto entry = ledger->find(page); | |
| 388 | 2 | if (entry == ledger->end()) | |
| 389 | { | ||
| 390 | ✗ | continue; | |
| 391 | } | ||
| 392 | 2 | DWORD released_target = 0; | |
| 393 | 2 | if (!entry->second.remove_holder(segments[i].transaction_id, &released_target, nullptr)) | |
| 394 | { | ||
| 395 | ✗ | continue; | |
| 396 | } | ||
| 397 | // The released target becomes the baseline after all live guards over the same range leave. | ||
| 398 | 2 | entry->second.original_protection = released_target; | |
| 399 | 2 | if (entry->second.empty()) | |
| 400 | { | ||
| 401 | 2 | ledger->erase(entry); | |
| 402 | } | ||
| 403 | } | ||
| 404 | } | ||
| 405 | 2 | } | |
| 406 | |||
| 407 | 1259 | detail::ProtectionChangeOutcome detail::protect_across_regions( | |
| 408 | std::uintptr_t address, | ||
| 409 | std::size_t bytes, | ||
| 410 | std::uint32_t new_protection, | ||
| 411 | ProtectionSegment *out, | ||
| 412 | std::size_t out_cap, | ||
| 413 | bool derive_writable_preserving_execute | ||
| 414 | ) noexcept | ||
| 415 | { | ||
| 416 | 1259 | if (bytes == 0 || out == nullptr || out_cap == 0) | |
| 417 | { | ||
| 418 | ✗ | return {0, ProtectionChangeStatus::ChangeFailed, ERROR_INVALID_PARAMETER}; | |
| 419 | } | ||
| 420 | |||
| 421 | 1259 | const std::uintptr_t span_end = address + bytes; | |
| 422 | 1259 | if (span_end < address || address < memory::USERSPACE_PTR_MIN || span_end > memory::USERSPACE_PTR_MAX) | |
| 423 | { | ||
| 424 | 3 | return {0, ProtectionChangeStatus::ChangeFailed, ERROR_INVALID_ADDRESS}; | |
| 425 | } | ||
| 426 | |||
| 427 | // A backend trap window holds a code page at PAGE_READWRITE under the process coordinator. The capture below | ||
| 428 | // must not record that transient value as the original (`[B-18]`), so a refused coordinator fails closed. | ||
| 429 | // TrapProtect.ProtectGuardWaitsForTheBackendTrapWindow pins the exclusion. | ||
| 430 | 1256 | const detail::BackendCoordinatorHold coordinator; | |
| 431 | 1256 | if (!coordinator) | |
| 432 | { | ||
| 433 | ✗ | return {0, ProtectionChangeStatus::ChangeFailed, ERROR_BUSY}; | |
| 434 | } | ||
| 435 | |||
| 436 | 1256 | ProtectionLedgerLock lock; | |
| 437 | 1256 | if (protection_ledger() == nullptr) | |
| 438 | { | ||
| 439 | 2 | return {0, ProtectionChangeStatus::ChangeFailed, ERROR_NOT_ENOUGH_MEMORY}; | |
| 440 | } | ||
| 441 | |||
| 442 | 1254 | std::size_t count = 0; | |
| 443 | 1254 | const std::uint64_t transaction_id = next_transaction_id(); | |
| 444 | 10 | const auto fail = [&](std::uint32_t change_error) noexcept -> ProtectionChangeOutcome | |
| 445 | { | ||
| 446 | 10 | std::uint32_t rollback_error = 0; | |
| 447 | 10 | if (!restore_segments_locked(out, count, rollback_error)) | |
| 448 | { | ||
| 449 | 2 | return {0, ProtectionChangeStatus::RestoreFailed, rollback_error}; | |
| 450 | } | ||
| 451 | 8 | return {0, ProtectionChangeStatus::ChangeFailed, change_error}; | |
| 452 | 1254 | }; | |
| 453 | |||
| 454 | 1254 | std::uintptr_t cur = address; | |
| 455 | 2575 | while (cur < span_end) | |
| 456 | { | ||
| 457 | 1331 | MEMORY_BASIC_INFORMATION mbi{}; | |
| 458 | 1331 | if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0) | |
| 459 | { | ||
| 460 | 10 | return fail(static_cast<std::uint32_t>(GetLastError())); | |
| 461 | } | ||
| 462 | |||
| 463 | // Clip this region to the span. Page-aligned regions meet exactly on a page boundary, so VirtualProtect | ||
| 464 | // cannot include a neighbor. Treat region-size overflow as the span end. | ||
| 465 | 1331 | const std::uintptr_t region_base = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress); | |
| 466 | 1331 | const std::uintptr_t region_end = region_base + mbi.RegionSize; | |
| 467 | 1331 | const std::uintptr_t effective_region_end = (region_end < region_base) ? span_end : region_end; | |
| 468 | 1331 | const std::uintptr_t seg_end = (effective_region_end < span_end) ? effective_region_end : span_end; | |
| 469 | 1331 | if (seg_end <= cur) | |
| 470 | { | ||
| 471 | ✗ | return fail(ERROR_INVALID_ADDRESS); | |
| 472 | } | ||
| 473 | |||
| 474 | 1331 | if (count >= out_cap) | |
| 475 | { | ||
| 476 | 1 | return fail(ERROR_INSUFFICIENT_BUFFER); | |
| 477 | } | ||
| 478 | |||
| 479 | // Use either the caller's fixed ProtectGuard value or a patch value derived from this region's protection. | ||
| 480 | // A data page never becomes executable. | ||
| 481 | 1330 | const DWORD target = derive_writable_preserving_execute ? writable_protection_for(mbi.Protect) | |
| 482 | 1293 | : static_cast<DWORD>(new_protection); | |
| 483 | |||
| 484 | 1330 | const bool had_execute = (mbi.Protect & EXECUTE_PERMISSION_FLAGS) != 0; | |
| 485 | 1330 | const std::uintptr_t page_lo = page_floor(cur); | |
| 486 | 1330 | const std::uintptr_t page_hi = page_ceiling(seg_end); | |
| 487 | 1330 | if (!ledger_acquire_pages(page_lo, page_hi, transaction_id, target, mbi.Protect)) | |
| 488 | { | ||
| 489 | 1 | return fail(ERROR_NOT_ENOUGH_MEMORY); | |
| 490 | } | ||
| 491 | |||
| 492 | 1329 | const std::size_t seg_size = static_cast<std::size_t>(seg_end - cur); | |
| 493 | 1329 | DWORD old_protection = 0; | |
| 494 | 1329 | if (!change_page_protection(reinterpret_cast<LPVOID>(cur), seg_size, target, &old_protection)) | |
| 495 | { | ||
| 496 | 8 | const std::uint32_t change_error = static_cast<std::uint32_t>(GetLastError()); | |
| 497 | 8 | ledger_cancel_pages(page_lo, page_hi, transaction_id); | |
| 498 | 8 | return fail(change_error); | |
| 499 | } | ||
| 500 | |||
| 501 | 1321 | out[count].base = cur; | |
| 502 | 1321 | out[count].size = seg_size; | |
| 503 | 1321 | out[count].originally_executable = had_execute; | |
| 504 | 1321 | out[count].transaction_id = transaction_id; | |
| 505 | 1321 | ++count; | |
| 506 | 1321 | cur = seg_end; | |
| 507 | } | ||
| 508 | |||
| 509 | 1244 | return {count, ProtectionChangeStatus::Ok, 0}; | |
| 510 | 1256 | } | |
| 511 | |||
| 512 | 31 | detail::PatchStatus detail::patch_bytes( | |
| 513 | std::uintptr_t address, | ||
| 514 | const void *source, | ||
| 515 | std::size_t bytes, | ||
| 516 | std::uint32_t &os_error, | ||
| 517 | bool flush_all_regions | ||
| 518 | ) noexcept | ||
| 519 | { | ||
| 520 | 31 | os_error = 0; | |
| 521 | |||
| 522 | // Make the target writable one protection region at a time. A data page never gains execute. A cross-region | ||
| 523 | // write never restores one flat protection. The transaction ledger lets a guard over the same page restore | ||
| 524 | // the true original protection. | ||
| 525 | 31 | ProtectionSegment segments[MAX_PROTECTION_SEGMENTS]; | |
| 526 | const ProtectionChangeOutcome protection = | ||
| 527 | 31 | protect_across_regions(address, bytes, 0, segments, MAX_PROTECTION_SEGMENTS, true); | |
| 528 | 31 | if (protection.status != ProtectionChangeStatus::Ok) | |
| 529 | { | ||
| 530 | 13 | os_error = protection.os_error; | |
| 531 | 13 | return protection.status == ProtectionChangeStatus::RestoreFailed ? PatchStatus::ProtectionRestoreFailed | |
| 532 | 13 | : PatchStatus::ProtectionChangeFailed; | |
| 533 | } | ||
| 534 | 18 | const std::size_t segment_count = protection.segment_count; | |
| 535 | |||
| 536 | // Route the store through the fault-guarded writer instead of bare memcpy. If a page loses access during the | ||
| 537 | // store, the writer contains and reports the fault, possibly as WriteMayBePartial. The host stays alive. | ||
| 538 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 539 | const GuardedWriteStatus write_status = s_seam_patch_write_not_written | ||
| 540 | 18 | ? GuardedWriteStatus::NotWritten | |
| 541 | 18 | : guarded_write_bytes(address, source, bytes); | |
| 542 | #else | ||
| 543 | const GuardedWriteStatus write_status = guarded_write_bytes(address, source, bytes); | ||
| 544 | #endif | ||
| 545 | |||
| 546 | // An explicit code patch flushes every touched region. An ordinary data write flushes only a region that was | ||
| 547 | // executable when this transaction began, so a read-only data write issues no flush. | ||
| 548 | 18 | bool flush_ok = true; | |
| 549 | 39 | for (std::size_t i = 0; i < segment_count; ++i) | |
| 550 | { | ||
| 551 | 21 | if (flush_all_regions || segments[i].originally_executable) | |
| 552 | { | ||
| 553 | 11 | if (!flush_instruction_cache(segments[i].base, segments[i].size)) | |
| 554 | { | ||
| 555 | 2 | flush_ok = false; | |
| 556 | } | ||
| 557 | } | ||
| 558 | } | ||
| 559 | |||
| 560 | // Restore protection per region. The transaction ledger preserves any live transaction over the same range. | ||
| 561 | 18 | std::uint32_t restore_error = 0; | |
| 562 | 18 | const bool restore_succeeded = restore_across_regions(segments, segment_count, restore_error); | |
| 563 | |||
| 564 | // Report the most severe outcome. A failed restore can leave a page writable-executable and outranks all | ||
| 565 | // other failures. A partial copy ranks next, followed by a stale instruction cache. | ||
| 566 | 18 | if (!restore_succeeded) | |
| 567 | { | ||
| 568 | ✗ | os_error = restore_error; | |
| 569 | ✗ | return PatchStatus::ProtectionRestoreFailed; | |
| 570 | } | ||
| 571 | 18 | if (write_status == GuardedWriteStatus::NotWritten) | |
| 572 | { | ||
| 573 | 2 | return PatchStatus::WriteFaulted; | |
| 574 | } | ||
| 575 | 16 | if (write_status == GuardedWriteStatus::MayBePartial) | |
| 576 | { | ||
| 577 | ✗ | return PatchStatus::WriteMayBePartial; | |
| 578 | } | ||
| 579 | 16 | if (!flush_ok) | |
| 580 | { | ||
| 581 | 2 | return PatchStatus::InstructionFlushFailed; | |
| 582 | } | ||
| 583 | 14 | return PatchStatus::Ok; | |
| 584 | } | ||
| 585 | |||
| 586 | 20 | bool detail::flush_instruction_cache(std::uintptr_t address, std::size_t bytes) noexcept | |
| 587 | { | ||
| 588 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 589 | 20 | s_seam_flush_observation.address = address; | |
| 590 | 20 | s_seam_flush_observation.bytes = bytes; | |
| 591 | 20 | ++s_seam_flush_observation.call_count; | |
| 592 | 20 | if (s_seam_flush_fails) | |
| 593 | { | ||
| 594 | 5 | s_seam_flush_observation.succeeded = false; | |
| 595 | 5 | return false; | |
| 596 | } | ||
| 597 | #endif | ||
| 598 | const bool succeeded = | ||
| 599 | 15 | FlushInstructionCache(GetCurrentProcess(), reinterpret_cast<LPCVOID>(address), bytes) != 0; | |
| 600 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 601 | 15 | s_seam_flush_observation.succeeded = succeeded; | |
| 602 | #endif | ||
| 603 | 15 | return succeeded; | |
| 604 | } | ||
| 605 | |||
| 606 | 6 | void detail::flush_if_executable(std::uintptr_t address, std::size_t bytes) noexcept | |
| 607 | { | ||
| 608 | // A request can start in data and cross an executable region. Walk every covered region and flush after the | ||
| 609 | // first executable region appears. An all-data request issues nothing. | ||
| 610 | 6 | const std::uintptr_t span_end = address + bytes; | |
| 611 | 6 | if (span_end <= address) | |
| 612 | { | ||
| 613 | ✗ | return; | |
| 614 | } | ||
| 615 | 15 | for (std::uintptr_t cur = address; cur < span_end;) | |
| 616 | { | ||
| 617 | 11 | MEMORY_BASIC_INFORMATION mbi{}; | |
| 618 | 11 | if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0) | |
| 619 | { | ||
| 620 | 2 | return; | |
| 621 | } | ||
| 622 | 11 | if ((mbi.Protect & EXECUTE_PERMISSION_FLAGS) != 0) | |
| 623 | { | ||
| 624 | 2 | (void)flush_instruction_cache(address, bytes); | |
| 625 | 2 | return; | |
| 626 | } | ||
| 627 | // Treat region-size overflow as the span end. A region that does not advance past the cursor ends the walk | ||
| 628 | // instead of an endless loop. | ||
| 629 | 9 | const std::uintptr_t region_end = reinterpret_cast<std::uintptr_t>(mbi.BaseAddress) + mbi.RegionSize; | |
| 630 | 9 | if (region_end <= cur) | |
| 631 | { | ||
| 632 | ✗ | return; | |
| 633 | } | ||
| 634 | 9 | cur = region_end; | |
| 635 | } | ||
| 636 | } | ||
| 637 | |||
| 638 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 639 | 10 | void detail::set_flush_failure_seam(bool fail) noexcept | |
| 640 | { | ||
| 641 | 10 | s_seam_flush_fails = fail; | |
| 642 | 10 | } | |
| 643 | |||
| 644 | 20 | void detail::reset_instruction_flush_observation_for_test() noexcept | |
| 645 | { | ||
| 646 | 20 | s_seam_flush_observation = {}; | |
| 647 | 20 | } | |
| 648 | |||
| 649 | 20 | detail::InstructionFlushObservation detail::instruction_flush_observation_for_test() noexcept | |
| 650 | { | ||
| 651 | 20 | return s_seam_flush_observation; | |
| 652 | } | ||
| 653 | |||
| 654 | 4 | void detail::set_patch_write_not_written_for_test(bool fail) noexcept | |
| 655 | { | ||
| 656 | 4 | s_seam_patch_write_not_written = fail; | |
| 657 | 4 | } | |
| 658 | |||
| 659 | 18 | void detail::set_virtual_protect_failure_mask(std::uint64_t call_mask) noexcept | |
| 660 | { | ||
| 661 | 18 | s_seam_virtual_protect_failures = call_mask; | |
| 662 | 18 | s_seam_virtual_protect_call = 0; | |
| 663 | 18 | } | |
| 664 | #endif | ||
| 665 | } // namespace DetourModKit | ||
| 666 |