./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
61 / 67
91.0%
Branches:
27 / 34
79.4%
Functions:
16 / 16
100.0%

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