GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.0% 61 / 0 / 67
Functions: 100.0% 16 / 0 / 16
Branches: 76.5% 26 / 0 / 34

src/internal/memory_guarded.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_MEMORY_GUARDED_HPP
2 #define DETOURMODKIT_INTERNAL_MEMORY_GUARDED_HPP
3
4 /**
5 * @file memory_guarded.hpp
6 * @brief Private engine interface for the guarded byte primitives that back the public memory::read / write / walk.
7 *
8 * MSVC uses frame-based __try / __except filters in memory_guarded.cpp. Scanner TUs route their __try filters through
9 * detail::guarded_range_fault_filter. MinGW uses its process-wide vectored handler in memory_guarded.cpp. An armed
10 * foreign-range fault returns a clean failure through __builtin_longjmp. The page-protection ledger and patch path live
11 * in memory_protect_ledger.cpp. Public memory TUs call only this private seam. This keeps installed headers free of
12 * Win32 and structured-exception constructs. The chain resolver accepts public memory::ChainStep values by pointer.
13 * This passes each offset and plausibility floor without a parallel-array copy.
14 */
15
16 #include "DetourModKit/address.hpp"
17 #include "DetourModKit/memory.hpp"
18 #include "DetourModKit/region.hpp"
19
20 #include <array>
21 #include <bit>
22 #include <cstddef>
23 #include <cstdint>
24 #include <optional>
25 #include <type_traits>
26
27 namespace DetourModKit
28 {
29 namespace detail
30 {
31 /**
32 * @struct ModuleSpan
33 * @brief The engine's raw half-open module range [base, end), in plain integers for hot arithmetic.
34 * @details The scan engine, the RTTI walk, and the hooked-prologue recovery all work in raw addresses inside
35 * their inner loops, so they carry a module's extent as two uintptr_t rather than the public Region.
36 * A Region (the public scope vocabulary) converts to a ModuleSpan at each public boundary via
37 * module_span(); an empty Region yields an invalid span that contains() rejects.
38 */
39 struct ModuleSpan
40 {
41 std::uintptr_t base = 0;
42 std::uintptr_t end = 0;
43
44 /// True iff this span is populated (base != 0 && end > base).
45
3/4
✓ Branch 2 → 3 taken 13736737 times.
✓ Branch 2 → 5 taken 988 times.
✓ Branch 3 → 4 taken 13736738 times.
✗ Branch 3 → 5 not taken.
13737725 [[nodiscard]] constexpr bool valid() const noexcept { return base != 0 && end > base; }
46
47 /// True iff the span is valid and @p address lies in [base, end).
48 11230663 [[nodiscard]] constexpr bool contains(std::uintptr_t address) const noexcept
49 {
50
5/6
✓ Branch 3 → 4 taken 11230663 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 5733620 times.
✓ Branch 4 → 7 taken 5497043 times.
✓ Branch 5 → 6 taken 3752153 times.
✓ Branch 5 → 7 taken 1981467 times.
11230663 return valid() && address >= base && address < end;
51 }
52 };
53
54 /// Converts a public Region scope into the engine's raw ModuleSpan; an empty Region yields an invalid span.
55 2631 [[nodiscard]] inline ModuleSpan module_span(Region scope) noexcept
56 {
57 2631 return ModuleSpan{scope.base.raw(), scope.end().raw()};
58 }
59
60 /// Raw-integer form of memory::is_plausible_ptr for engine code that works in uintptr_t inside its hot loops.
61 1716 [[nodiscard]] inline constexpr bool is_plausible_ptr(std::uintptr_t address) noexcept
62 {
63 1716 return memory::is_plausible_ptr(Address{address});
64 }
65
66 /**
67 * @brief Resolves a loaded module's base address to the Region spanning its full mapped image.
68 * @param module_base The module's base address (its HMODULE value); null yields an empty Region.
69 * @return The module image span, or an empty Region when @p module_base is null or its PE headers do not
70 * validate.
71 * @details The single canonical "module base -> Region" resolver, shared by region.cpp's Region factories
72 * (host/module_named/own) and memory::module_of so the PE-header walk (DOS magic, a bounded
73 * e_lfanew, the NT signature, and OptionalHeader.SizeOfImage) lives in one place rather than a
74 * raw-deref copy in each. The headers are read through the guarded engine, so a partially-mapped or
75 * corrupt image fails closed to an empty Region instead of faulting the host. Each call re-reads the
76 * live headers: an HMODULE is its image base and the loader may hand the same base to a replacement
77 * image, so a memoized span would be a claim about an identity that can change underneath it.
78 */
79 [[nodiscard]] Region module_image_region(Address module_base) noexcept;
80
81 /**
82 * @brief Resolves the current loader owner of @p address and reads its image span.
83 * @return The live module span, or an empty Region when the loader lookup or PE-header read fails.
84 * @note Setup/control-plane only: performs a loader query and guarded PE-header reads. The returned Region is
85 * non-owning: it does not pin the module against an unload after the call returns.
86 */
87 [[nodiscard]] Region live_module_region(Address address) noexcept;
88
89 /**
90 * @brief Guarded copy of @p bytes bytes from @p address into @p out.
91 * @param address Source address. Below memory::USERSPACE_PTR_MIN, or an end that wraps the address space, is
92 * rejected without a read.
93 * @param out Destination buffer; null is rejected.
94 * @param bytes Byte count; zero is a successful no-op.
95 * @param fault_address_out When non-null and the read faults, receives the faulting address, letting a caller
96 * report which byte of the span was unreadable rather than only that some byte was.
97 * Left untouched when the span is rejected without a read, and on the MinGW fallback
98 * path that validates through VirtualQuery instead of faulting.
99 * @return true on full success; false on any fault or rejected argument (then @p out is unspecified).
100 */
101 [[nodiscard]] bool guarded_read_bytes(
102 std::uintptr_t address,
103 void *out,
104 std::size_t bytes,
105 volatile std::uintptr_t *fault_address_out = nullptr
106 ) noexcept;
107
108 /**
109 * @brief Guarded typed read for engine code: a representation-safe @p T at @p address, or nullopt on fault.
110 * @tparam T A trivially copyable type for which every bit pattern is a valid object representation
111 * (@ref is_representation_safe_v). Read through untyped storage + bit_cast, so it need not be default
112 * constructible, but a representation-sensitive type such as bool is excluded: forming it from an
113 * arbitrary foreign byte would be undefined behaviour before the optional could report failure.
114 * Decode such a type from raw bytes instead (memory::read_bool for bool).
115 * @details The engine-side counterpart of public memory::read<T>, returning std::optional instead of Result so
116 * the scan / RTTI inner loops keep the lightweight optional checks they already used. A top-level
117 * bounded built-in array is returned as the equivalent nested `std::array`. Forwards to
118 * guarded_read_bytes, so the __try frame stays in the engine TU.
119 */
120 template <class T>
121 requires(std::is_trivially_copyable_v<T> && is_representation_safe_v<T>)
122 2508180 [[nodiscard]] std::optional<representation_read_value_t<T>> guarded_read(std::uintptr_t address) noexcept
123 {
124 2508180 std::array<std::byte, sizeof(T)> storage{};
125
18/24
std::optional<DetourModKit::detail::representation_read_value<_IMAGE_DOS_HEADER>::type> DetourModKit::detail::guarded_read<_IMAGE_DOS_HEADER>(unsigned long long):
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 7 taken 735 times.
std::optional<DetourModKit::detail::representation_read_value<_IMAGE_NT_HEADERS64>::type> DetourModKit::detail::guarded_read<_IMAGE_NT_HEADERS64>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 688 times.
std::optional<DetourModKit::detail::representation_read_value<_IMAGE_SECTION_HEADER>::type> DetourModKit::detail::guarded_read<_IMAGE_SECTION_HEADER>(unsigned long long):
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 311 times.
std::optional<DetourModKit::detail::representation_read_value<_IMAGE_EXPORT_DIRECTORY>::type> DetourModKit::detail::guarded_read<_IMAGE_EXPORT_DIRECTORY>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 59 times.
std::optional<DetourModKit::detail::representation_read_value<int [2]>::type> DetourModKit::detail::guarded_read<int [2]>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1 time.
std::optional<DetourModKit::detail::representation_read_value<DetourModKit::rtti::detail::ColHead>::type> DetourModKit::detail::guarded_read<DetourModKit::rtti::detail::ColHead>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1249131 times.
std::optional<DetourModKit::detail::representation_read_value<char>::type> DetourModKit::detail::guarded_read<char>(unsigned long long):
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 2435 times.
std::optional<DetourModKit::detail::representation_read_value<unsigned char>::type> DetourModKit::detail::guarded_read<unsigned char>(unsigned long long):
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 15 times.
std::optional<DetourModKit::detail::representation_read_value<int>::type> DetourModKit::detail::guarded_read<int>(unsigned long long):
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 29 times.
std::optional<DetourModKit::detail::representation_read_value<unsigned int>::type> DetourModKit::detail::guarded_read<unsigned int>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 312 times.
std::optional<DetourModKit::detail::representation_read_value<unsigned short>::type> DetourModKit::detail::guarded_read<unsigned short>(unsigned long long):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 52 times.
std::optional<DetourModKit::detail::representation_read_value<unsigned long long>::type> DetourModKit::detail::guarded_read<unsigned long long>(unsigned long long):
✓ Branch 5 → 6 taken 13 times.
✓ Branch 5 → 7 taken 1254390 times.
2508180 if (!guarded_read_bytes(address, storage.data(), sizeof(T)))
126 {
127 22 return std::nullopt;
128 }
129 2508158 return decode_foreign_representation<T>(storage);
130 }
131
132 /** @brief Outcome of a guarded byte write that never changes protection. */
133 enum class GuardedWriteStatus
134 {
135 Ok,
136 NotWritten,
137 MayBePartial
138 };
139
140 /**
141 * @brief Guarded copy of @p bytes bytes from @p source into @p address, changing no page protection.
142 * @param address Destination address. Below memory::USERSPACE_PTR_MIN, or a wrapping end, is rejected.
143 * @param source Source buffer; null is rejected.
144 * @param bytes Byte count; zero is a successful no-op.
145 * @return Whether all bytes landed, the first byte faulted (`NotWritten`), or the fault address lay past the
146 * first byte (`MayBePartial`).
147 * @details The primitive exposes whether a fault occurred at byte zero or past it.
148 * This keeps `NotWritten` truthful without a racy permission query. `MayBePartial` bounds the damage
149 * to the requested span, but it does not prove that any byte changed. A fixed-width store faults as
150 * one instruction. This is memory::write_bytes' no-protect path.
151 */
152 [[nodiscard]] GuardedWriteStatus
153 guarded_write_bytes(std::uintptr_t address, const void *source, std::size_t bytes) noexcept;
154
155 /**
156 * @brief Atomically replaces one aligned pointer word when it still equals @p expected, under the fault guard.
157 * @param address Address of the pointer-sized word; rejected unless naturally aligned and inside user space.
158 * @param expected Value that must still be present.
159 * @param replacement Value stored when the comparison succeeds.
160 * @return True only when the comparison and replacement both complete; false on mismatch, fault, or rejection.
161 */
162 [[nodiscard]] bool guarded_compare_exchange_word(
163 std::uintptr_t address,
164 std::uintptr_t expected,
165 std::uintptr_t replacement
166 ) noexcept;
167
168 /**
169 * @struct ProtectionSegment
170 * @brief One VirtualQuery region within a protection-changed span, plus the protection to restore it to.
171 * @details A write or a ProtectGuard may cover a span that crosses a `.rdata`/`.text` (or any) protection seam,
172 * so it is not one uniform-protection block. VirtualProtect over the whole span reports only the
173 * first page's prior protection, so restoring the whole span to that single value flattens an
174 * executable page adjacent to a read-only seam down to PAGE_READONLY (its next execution then AVs
175 * under DEP). The span is therefore changed and restored one VirtualQuery region at a time; each
176 * region's own prior protection is captured here so the restore is exact per region.
177 */
178 struct ProtectionSegment
179 {
180 // First byte of the touched sub-span within this region.
181 std::uintptr_t base = 0;
182 // Length of the touched sub-span (VirtualProtect operates on whole pages).
183 std::size_t size = 0;
184 // Whether the segment was executable when this transaction began.
185 bool originally_executable = false;
186 // Identifies this transaction in the page ledger.
187 std::uint64_t transaction_id = 0;
188 };
189
190 /** @brief Outcome of a multi-region protection change. */
191 enum class ProtectionChangeStatus
192 {
193 Ok,
194 ChangeFailed,
195 RestoreFailed
196 };
197
198 /** @brief Result of changing a span's protection. */
199 struct ProtectionChangeOutcome
200 {
201 std::size_t segment_count = 0;
202 ProtectionChangeStatus status = ProtectionChangeStatus::ChangeFailed;
203 std::uint32_t os_error = 0;
204 };
205
206 /**
207 * @brief Upper bound on distinct protection regions a single span may cross before @ref protect_across_regions
208 * fails closed.
209 * @details A real write or guard site spans one to a few regions; a span crossing this many alternating
210 * protection blocks is not a legitimate patch, so the cap is a defensive ceiling, not a functional
211 * limit.
212 */
213 inline constexpr std::size_t MAX_PROTECTION_SEGMENTS = 64;
214
215 /**
216 * @brief Changes every VirtualQuery region overlapping the requested span and records it for restoration.
217 * @param address First byte of the validated span.
218 * @param bytes Validated non-zero span length.
219 * @param new_protection Fixed Win32 PAGE_* value. Ignored when writable protection is derived.
220 * @param out Caller-owned buffer receiving one @ref ProtectionSegment per region changed.
221 * @param out_cap Capacity of @p out in elements.
222 * @param derive_writable_preserving_execute When true, derive writable protection per region while
223 * preserving execute; otherwise use @p new_protection.
224 * @return The captured segment count and whether the change, or its rollback, failed.
225 * @details A process-wide ledger serializes protection transactions. Each page records its original
226 * protection and live holders in acquisition order. Removing an inner guard restores the newest
227 * surviving holder; removing the last restores the original. The span is walked by VirtualQuery
228 * region so protection seams restore exactly. Query, protection, capacity, and allocation failures
229 * fail closed. A rollback failure is reported separately because a temporary protection may remain.
230 */
231 [[nodiscard]] ProtectionChangeOutcome protect_across_regions(
232 std::uintptr_t address,
233 std::size_t bytes,
234 std::uint32_t new_protection,
235 ProtectionSegment *out,
236 std::size_t out_cap,
237 bool derive_writable_preserving_execute = false
238 ) noexcept;
239
240 /**
241 * @brief Restores every segment captured by @ref protect_across_regions to its recorded prior protection.
242 * @param segments The segments to restore (as filled by @ref protect_across_regions).
243 * @param count Number of valid entries in @p segments.
244 * @param os_error Receives the OS error from the first failing VirtualProtect (captured before any later call
245 * or FlushInstructionCache can overwrite GetLastError).
246 * @return true if every segment restored; false if any VirtualProtect failed (best-effort: it still attempts
247 * the remaining segments so a single failure does not strand the rest in the changed protection).
248 */
249 [[nodiscard]] bool
250 restore_across_regions(const ProtectionSegment *segments, std::size_t count, std::uint32_t &os_error) noexcept;
251
252 /**
253 * @brief Drops @p segments from the protection ledger WITHOUT restoring their protection.
254 * @param segments The segments to stop tracking (as filled by @ref protect_across_regions).
255 * @param count Number of valid entries in @p segments.
256 * @details The counterpart to @ref restore_across_regions for a guard that is intentionally abandoned
257 * (ProtectGuard::release keeps the changed protection permanently): the page stays at its changed
258 * protection, but its ledger depth must be released so the ledger does not carry a phantom transaction
259 * that would block a later overlapping guard from ever restoring, or make a reused page address
260 * resolve to a stale original. A page whose depth reaches zero is simply forgotten; the changed
261 * protection it is left at becomes the baseline a future guard captures.
262 */
263 void abandon_protection_tracking(const ProtectionSegment *segments, std::size_t count) noexcept;
264
265 /**
266 * @enum PatchStatus
267 * @brief Outcome of patch_bytes (the protection-changing slow path of memory::write_bytes).
268 */
269 enum class PatchStatus
270 {
271 /// The bytes were written, the instruction cache flushed for executable regions, and protection restored.
272 Ok,
273 /// The page could not be made writable; nothing was written.
274 ProtectionChangeFailed,
275 /// The first target byte faulted after protection changed, so nothing was written.
276 WriteFaulted,
277 /**
278 * @brief The guarded copy faulted past the first target byte.
279 * @details Required cache maintenance was attempted and protection was restored. The changed prefix has an
280 * unknown length and can be empty. See @ref ErrorCode::WriteMayBePartial for the caller contract.
281 */
282 WriteMayBePartial,
283 /// Bytes written and protection restored, but an executable region's instruction-cache flush failed.
284 InstructionFlushFailed,
285 /// The bytes were written but the original protection could not be restored.
286 ProtectionRestoreFailed
287 };
288
289 /**
290 * @brief Makes a span writable, performs a guarded copy and required cache flushes, then restores protection.
291 * @param address Validated destination address.
292 * @param source Source buffer.
293 * @param bytes Validated non-zero byte count.
294 * @param os_error Receives a failing VirtualProtect error for change or restoration failures.
295 * @param flush_all_regions Flush every touched region when true; otherwise flush executable regions.
296 * @return The outcome of the protect, copy, flush, and restore transaction.
297 * @details Writable protection is derived per region, preserving execute without granting it to data.
298 * With @p flush_all_regions false, read-only data writes issue no cache flush. The guarded copy
299 * contains a concurrent reprotect or unmap fault, after which flush and restoration are still tried.
300 * Restoration failure outranks partial copy, which outranks cache-flush failure. The caller owns
301 * protection-cache invalidation.
302 */
303 [[nodiscard]] PatchStatus patch_bytes(
304 std::uintptr_t address,
305 const void *source,
306 std::size_t bytes,
307 std::uint32_t &os_error,
308 bool flush_all_regions = false
309 ) noexcept;
310
311 /**
312 * @brief Flushes the instruction cache over [@p address, @p address + @p bytes), returning whether it
313 * succeeded.
314 * @param address First byte to flush.
315 * @param bytes Byte count.
316 * @return true on success; false if FlushInstructionCache failed (or a test seam forced a failure).
317 * @details Code-patch paths call this after every possibly modifying write. Ordinary data writes call it only
318 * on a protection-changing slow path that touched an executable region; their already-writable fast
319 * path and read-only non-executable slow path remain flush-free.
320 */
321 [[nodiscard]] bool flush_instruction_cache(std::uintptr_t address, std::size_t bytes) noexcept;
322
323 /**
324 * @brief Flushes [@p address, @p address + @p bytes) when any region the request covers is executable.
325 * @param address First byte of the possibly changed span.
326 * @param bytes Byte count of the validated request.
327 * @details The data-route counterpart of @ref flush_instruction_cache for a guarded attempt that may have
328 * changed a prefix. @ref patch_bytes flushes the executable regions it made writable, but a protection
329 * setup failure produces no segments, so a caller whose no-reprotect attempt already changed
330 * executable bytes must cover the request itself. The decision is taken over every region the request
331 * covers, because a forward copy that begins in data can reach code before it faults. Best-effort: the
332 * flush result is not reportable because a partial write outranks a flush-only failure, and a request
333 * with no executable region keeps the flush-free data contract. Portable copy order cannot reveal the
334 * exact prefix, so the whole validated request is flushed.
335 */
336 void flush_if_executable(std::uintptr_t address, std::size_t bytes) noexcept;
337
338 /**
339 * @struct ChainWalkOutcome
340 * @brief Result of guarded_resolve_chain: the resolved leaf, or the hop index at which the walk failed.
341 */
342 struct ChainWalkOutcome
343 {
344 /// The resolved leaf address; meaningful only when @ref ok is true.
345 Address address{};
346 /// Index of the hop that faulted or yielded an implausible link; meaningful only when @ref ok is false.
347 std::size_t fail_index{0};
348 /// True when the whole chain resolved.
349 bool ok{false};
350 };
351
352 /**
353 * @brief Resolves a Cheat-Engine-style pointer chain under a single fault guard, capturing intermediates.
354 * @param base Root address of the chain.
355 * @param steps One memory::ChainStep per hop (offset + per-hop plausibility floor).
356 * @param count Number of hops.
357 * @param trace Optional out-buffer; when non-null, trace[i] receives the value resolved at hop i for the first
358 * @p trace_cap hops, populated for the successfully-walked prefix even on failure.
359 * @param trace_cap Capacity of @p trace in elements (0 when @p trace is null).
360 * @return A ChainWalkOutcome: ok + leaf on success, or !ok + the failing hop index.
361 * @details Every offset except the last is added and dereferenced to obtain the next link.
362 * The last is added but not dereferenced. Each intermediate link is screened against its hop's floor
363 * and the user-mode ceiling. A torn or sentinel pointer stops the walk before the next dereference.
364 */
365 [[nodiscard]] ChainWalkOutcome guarded_resolve_chain(
366 Address base,
367 const memory::ChainStep *steps,
368 std::size_t count,
369 Address *trace,
370 std::size_t trace_cap
371 ) noexcept;
372
373 #if defined(DMK_ENABLE_TEST_SEAMS)
374 /**
375 * @struct InstructionFlushObservation
376 * @brief Test-only record of the most recent instruction-cache flush and the number of observed calls.
377 */
378 struct InstructionFlushObservation
379 {
380 std::uintptr_t address{};
381 std::size_t bytes{};
382 std::size_t call_count{};
383 bool succeeded{};
384 };
385
386 /**
387 * @struct GuardedAccessObservation
388 * @brief Test-only call counts for guarded reads, guarded writes, and page-protection changes.
389 */
390 struct GuardedAccessObservation
391 {
392 std::size_t read_calls{};
393 std::size_t write_calls{};
394 std::size_t protection_calls{};
395 };
396
397 /**
398 * @brief Counts one page-protection change into the guarded-access observation.
399 * @details memory_guarded.cpp owns the counters. memory_protect_ledger.cpp reports its VirtualProtect calls
400 * through this hook.
401 */
402 void note_protection_call_for_test() noexcept;
403
404 /// Clears and enables the process-wide guarded-access call observation.
405 void reset_guarded_access_observation_for_test() noexcept;
406
407 /// Returns the process-wide guarded-access call counts.
408 [[nodiscard]] GuardedAccessObservation guarded_access_observation_for_test() noexcept;
409
410 /// Disarms guarded-access observation after the observed call completes.
411 void stop_guarded_access_observation_for_test() noexcept;
412
413 /**
414 * @brief Test seam: forces instruction-cache flushes to report failure.
415 * @details Set on the calling thread only, disabled by default, and compiled out of shipping archives.
416 */
417 void set_flush_failure_seam(bool fail) noexcept;
418
419 /// Clears the current thread's instruction-cache flush observation.
420 void reset_instruction_flush_observation_for_test() noexcept;
421
422 /// Returns the current thread's instruction-cache flush observation.
423 [[nodiscard]] InstructionFlushObservation instruction_flush_observation_for_test() noexcept;
424
425 /// Forces @ref patch_bytes to report that its post-protection copy wrote no bytes.
426 void set_patch_write_not_written_for_test(bool fail) noexcept;
427
428 /**
429 * @brief Test seam: makes @ref guarded_write_bytes copy one byte at a time and record the prefix it wrote.
430 * @details Enabling it turns the guarded copy into a forward byte-at-a-time loop, making a faulting prefix
431 * deterministic. Portable copy ordering is otherwise unspecified. The seam is thread-local and
432 * compiled out of shipping archives.
433 */
434 void set_forward_copy_seam(bool enable) noexcept;
435
436 /// Test seam: bytes the most recent @ref guarded_write_bytes committed before a fault (forward-copy seam).
437 [[nodiscard]] std::size_t last_forward_copy_prefix() noexcept;
438
439 /** @brief Fails selected subsequent VirtualProtect calls by zero-based call-index bits. */
440 void set_virtual_protect_failure_mask(std::uint64_t call_mask) noexcept;
441
442 /// Test seam: forces PAGE_GUARD restoration to report failure.
443 void set_guard_rearm_failure_seam(bool fail) noexcept;
444
445 /// Resets the current thread's best-effort restoration diagnostic count.
446 void reset_restore_diagnostic_count() noexcept;
447
448 /// Returns the current thread's best-effort restoration diagnostic count.
449 [[nodiscard]] std::size_t restore_diagnostic_count() noexcept;
450 #endif
451
452 #if !defined(_MSC_VER) && defined(_WIN64)
453 /**
454 * @brief Eagerly installs the MinGW process-wide vectored fault handler the guarded reads rely on.
455 * @details Lazy install also happens on the first guarded access, so this is purely an optimization: the cache
456 * setup path calls it so the handler is present before a hook callback can be the first guarded read,
457 * sparing that first read the VirtualQuery fallback. A no-op on MSVC (frame-based __try needs no
458 * handler), hence the MinGW-x64 guard. Best-effort: a failed install only costs guarded reads their
459 * fallback.
460 */
461 void ensure_guarded_engine_installed() noexcept;
462
463 /**
464 * @brief Drains in-flight guarded accesses, then removes the MinGW vectored fault handler.
465 * @details Called on memory-subsystem teardown so the handler cannot dangle into freed code if the DMK module
466 * is unloaded. It waits for every guarded access already committed to the handler path to finish
467 * before unregistering, so a fault can never arrive after the handler is gone. Idempotent and
468 * re-installable: a later guarded access re-installs a fresh handler. A no-op on MSVC.
469 */
470 void release_guarded_engine() noexcept;
471 #endif
472 } // namespace detail
473 } // namespace DetourModKit
474
475 #endif // DETOURMODKIT_INTERNAL_MEMORY_GUARDED_HPP
476