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/24std::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 |