GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 88.6% 248 / 0 / 280
Functions: 100.0% 30 / 0 / 30
Branches: 74.2% 132 / 0 / 178

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