./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
262 / 289
90.7%
Branches:
136 / 178
76.4%
Functions:
31 / 31
100.0%

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
2/2
✓ Branch 2 → 3 taken 14 times.
✓ Branch 2 → 4 taken 23 times.
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
3/4
✓ Branch 2 → 3 taken 38 times.
✓ Branch 2 → 8 taken 6536 times.
✓ Branch 4 → 5 taken 38 times.
✗ Branch 4 → 8 not taken.
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1035 times.
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
2/2
✓ Branch 2 → 3 taken 1324 times.
✓ Branch 2 → 13 taken 6 times.
1330 if (first_holder.transaction_id == transaction_id)
94 {
95 1324 target = first_holder.target_protection;
96
2/2
✓ Branch 4 → 5 taken 295 times.
✓ Branch 4 → 6 taken 1029 times.
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
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 6 times.
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
2/2
✓ Branch 32 → 33 taken 2 times.
✓ Branch 32 → 34 taken 1328 times.
1330 if (removed_target != nullptr)
123 {
124 2 *removed_target = target;
125 }
126
2/2
✓ Branch 34 → 35 taken 1320 times.
✓ Branch 34 → 44 taken 10 times.
1330 if (desired_protection != nullptr)
127 {
128
2/2
✓ Branch 36 → 37 taken 285 times.
✓ Branch 36 → 38 taken 1035 times.
2355 *desired_protection = empty() ? original_protection
129
2/2
✓ Branch 39 → 40 taken 437 times.
✓ Branch 39 → 41 taken 598 times.
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
3/4
✓ Branch 2 → 3 taken 38 times.
✓ Branch 2 → 9 taken 1 time.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 38 times.
39 return new std::unordered_map<std::uintptr_t, PageProtectionState>();
163 }
164 1 catch (...)
165 {
166 1 return nullptr;
167 1 }
168
3/4
✓ Branch 2 → 3 taken 39 times.
✓ Branch 2 → 8 taken 3810 times.
✓ Branch 4 → 5 taken 39 times.
✗ Branch 4 → 8 not taken.
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
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1254 times.
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 9 times.
9 if (ledger == nullptr)
186 {
187 ✗ return;
188 }
189 9 const std::uintptr_t step = system_page_size();
190
2/2
✓ Branch 24 → 7 taken 8 times.
✓ Branch 24 → 25 taken 9 times.
17 for (std::uintptr_t page = page_lo; page < page_hi; page += step)
191 {
192 8 const auto entry = ledger->find(page);
193
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 8 times.
8 if (entry == ledger->end())
194 {
195 ✗ continue;
196 }
197
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())
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1330 times.
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
2/2
✓ Branch 19 → 7 taken 1331 times.
✓ Branch 19 → 20 taken 1329 times.
2660 for (; page < page_hi; page += step)
222 {
223
1/2
✓ Branch 7 → 8 taken 1331 times.
✗ Branch 7 → 27 not taken.
1331 auto entry = ledger->find(page);
224
2/2
✓ Branch 10 → 11 taken 296 times.
✓ Branch 10 → 16 taken 1035 times.
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
2/2
✓ Branch 13 → 14 taken 295 times.
✓ Branch 13 → 23 taken 1 time.
592 entry = ledger->try_emplace(page, std::move(state)).first;
230 296 }
231 else
232 {
233
1/2
✓ Branch 17 → 18 taken 1035 times.
✗ Branch 17 → 27 not taken.
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
4/4
✓ Branch 3 → 4 taken 440 times.
✓ Branch 3 → 7 taken 2208 times.
✓ Branch 4 → 5 taken 11 times.
✓ Branch 4 → 7 taken 429 times.
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
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1252 times.
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
2/2
✓ Branch 33 → 7 taken 1319 times.
✓ Branch 33 → 34 taken 1252 times.
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
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 1319 times.
1319 if (!in_run)
297 {
298 ✗ return;
299 }
300 1319 DWORD previous = 0;
301
2/2
✓ Branch 5 → 6 taken 7 times.
✓ Branch 5 → 10 taken 1312 times.
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
1/2
✓ Branch 6 → 7 taken 7 times.
✗ Branch 6 → 9 not taken.
7 if (all_restored)
309 {
310 7 os_error = static_cast<std::uint32_t>(GetLastError());
311 }
312 7 all_restored = false;
313 }
314
2/2
✓ Branch 23 → 11 taken 1320 times.
✓ Branch 23 → 24 taken 1319 times.
2639 for (std::uintptr_t page = run_begin; page < run_end; page += step)
315 {
316 1320 const auto entry = ledger->find(page);
317
5/6
✓ Branch 14 → 15 taken 1320 times.
✗ Branch 14 → 19 not taken.
✓ Branch 17 → 18 taken 285 times.
✓ Branch 17 → 19 taken 1035 times.
✓ Branch 20 → 21 taken 285 times.
✓ Branch 20 → 22 taken 1035 times.
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
2/2
✓ Branch 30 → 9 taken 1320 times.
✓ Branch 30 → 31 taken 1319 times.
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
2/4
✓ Branch 12 → 13 taken 1320 times.
✗ Branch 12 → 16 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 23 taken 1320 times.
2640 if (entry == ledger->end() ||
331
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1320 times.
1320 !entry->second.remove_holder(segments[i].transaction_id, nullptr, &desired))
332 {
333 ✗ restore_run();
334
0/2
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
✗ if (all_restored)
335 {
336 ✗ os_error = ERROR_INVALID_DATA;
337 }
338 ✗ all_restored = false;
339 ✗ continue;
340 }
341
342
3/4
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 26 taken 1319 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1 time.
1320 if (in_run && desired != run_protection)
343 {
344 ✗ restore_run();
345 }
346
2/2
✓ Branch 26 → 27 taken 1319 times.
✓ Branch 26 → 28 taken 1 time.
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
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
2 if (ledger == nullptr)
378 {
379 ✗ return;
380 }
381 2 const std::uintptr_t step = system_page_size();
382
2/2
✓ Branch 29 → 8 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
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
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)
386 {
387 2 const auto entry = ledger->find(page);
388
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 2 times.
2 if (entry == ledger->end())
389 {
390 ✗ continue;
391 }
392 2 DWORD released_target = 0;
393
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))
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
1/2
✓ Branch 22 → 23 taken 2 times.
✗ Branch 22 → 24 not taken.
2 if (entry->second.empty())
400 {
401 2 ledger->erase(entry);
402 }
403 }
404 }
405
1/2
✓ Branch 32 → 33 taken 2 times.
✗ Branch 32 → 35 not taken.
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
3/6
✓ Branch 2 → 3 taken 1259 times.
✗ Branch 2 → 5 not taken.
✓ Branch 3 → 4 taken 1259 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1259 times.
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
5/6
✓ Branch 6 → 7 taken 1257 times.
✓ Branch 6 → 9 taken 2 times.
✓ Branch 7 → 8 taken 1256 times.
✓ Branch 7 → 9 taken 1 time.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1256 times.
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
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1256 times.
1256 if (!coordinator)
432 {
433 ✗ return {0, ProtectionChangeStatus::ChangeFailed, ERROR_BUSY};
434 }
435
436 1256 ProtectionLedgerLock lock;
437
2/2
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 1254 times.
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
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 8 times.
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
2/2
✓ Branch 49 → 20 taken 1331 times.
✓ Branch 49 → 50 taken 1244 times.
2575 while (cur < span_end)
456 {
457 1331 MEMORY_BASIC_INFORMATION mbi{};
458
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 1331 times.
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
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 1331 times.
1331 const std::uintptr_t effective_region_end = (region_end < region_base) ? span_end : region_end;
468
2/2
✓ Branch 27 → 28 taken 78 times.
✓ Branch 27 → 29 taken 1253 times.
1331 const std::uintptr_t seg_end = (effective_region_end < span_end) ? effective_region_end : span_end;
469
1/2
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 1331 times.
1331 if (seg_end <= cur)
470 {
471 ✗ return fail(ERROR_INVALID_ADDRESS);
472 }
473
474
2/2
✓ Branch 32 → 33 taken 1 time.
✓ Branch 32 → 34 taken 1330 times.
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
2/2
✓ Branch 34 → 35 taken 37 times.
✓ Branch 34 → 36 taken 1293 times.
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
2/2
✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 42 taken 1329 times.
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
2/2
✓ Branch 43 → 44 taken 8 times.
✓ Branch 43 → 47 taken 1321 times.
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
2/2
✓ Branch 3 → 4 taken 13 times.
✓ Branch 3 → 8 taken 18 times.
31 if (protection.status != ProtectionChangeStatus::Ok)
529 {
530 13 os_error = protection.os_error;
531
2/2
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 12 times.
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
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 16 times.
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
2/2
✓ Branch 18 → 12 taken 21 times.
✓ Branch 18 → 19 taken 18 times.
39 for (std::size_t i = 0; i < segment_count; ++i)
550 {
551
4/4
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 14 taken 9 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 17 taken 10 times.
21 if (flush_all_regions || segments[i].originally_executable)
552 {
553
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 9 times.
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
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 18 times.
18 if (!restore_succeeded)
567 {
568 ✗ os_error = restore_error;
569 ✗ return PatchStatus::ProtectionRestoreFailed;
570 }
571
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 16 times.
18 if (write_status == GuardedWriteStatus::NotWritten)
572 {
573 2 return PatchStatus::WriteFaulted;
574 }
575
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 16 times.
16 if (write_status == GuardedWriteStatus::MayBePartial)
576 {
577 ✗ return PatchStatus::WriteMayBePartial;
578 }
579
2/2
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 28 taken 14 times.
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
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 4 taken 15 times.
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
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 6 times.
6 if (span_end <= address)
612 {
613 ✗ return;
614 }
615
2/2
✓ Branch 15 → 5 taken 11 times.
✓ Branch 15 → 16 taken 4 times.
15 for (std::uintptr_t cur = address; cur < span_end;)
616 {
617 11 MEMORY_BASIC_INFORMATION mbi{};
618
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 11 times.
11 if (VirtualQuery(reinterpret_cast<LPCVOID>(cur), &mbi, sizeof(mbi)) == 0)
619 {
620 2 return;
621 }
622
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 11 taken 9 times.
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
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 9 times.
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