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