include/DetourModKit/memory.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_MEMORY_HPP | ||
| 2 | #define DETOURMODKIT_MEMORY_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file memory.hpp | ||
| 6 | * @brief The guarded-memory surface: fault-tolerant reads, writes, pointer-chain walks, and a protection guard. | ||
| 7 | * @details Each guarded entry point reports a fault as an `Error`, unless the guard-page re-arm in `[B-20]` fails. | ||
| 8 | * @warning `[B-100]` Under the loader lock, call only the Callback-safe entry points in this header. Cache startup and | ||
| 9 | * the exact-case module lookup fail closed. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/address.hpp" | ||
| 13 | #include "DetourModKit/defines.hpp" | ||
| 14 | #include "DetourModKit/error.hpp" | ||
| 15 | #include "DetourModKit/region.hpp" | ||
| 16 | |||
| 17 | #include <array> | ||
| 18 | #include <bit> | ||
| 19 | #include <cassert> | ||
| 20 | #include <climits> | ||
| 21 | #include <cstddef> | ||
| 22 | #include <cstdint> | ||
| 23 | #include <cstring> | ||
| 24 | #include <initializer_list> | ||
| 25 | #include <limits> | ||
| 26 | #include <memory> | ||
| 27 | #include <ranges> | ||
| 28 | #include <span> | ||
| 29 | #include <string> | ||
| 30 | #include <string_view> | ||
| 31 | #include <type_traits> | ||
| 32 | |||
| 33 | namespace DetourModKit | ||
| 34 | { | ||
| 35 | namespace detail | ||
| 36 | { | ||
| 37 | /// True for a `std::initializer_list` specialization. | ||
| 38 | template <class T> inline constexpr bool is_initializer_list_v = false; | ||
| 39 | template <class U> inline constexpr bool is_initializer_list_v<std::initializer_list<U>> = true; | ||
| 40 | |||
| 41 | /** | ||
| 42 | * @brief True for every `std::ranges::view` and every `std::initializer_list`. | ||
| 43 | * @details `[B-21]` Typed `write<T>` rejects every view, even a byte span. `write_bytes` and `write_in_place` | ||
| 44 | * write the viewed bytes of a contiguous byte view. | ||
| 45 | */ | ||
| 46 | template <class T> | ||
| 47 | inline constexpr bool is_non_owning_view_v = std::ranges::view<T> || is_initializer_list_v<T>; | ||
| 48 | |||
| 49 | /** | ||
| 50 | * @brief Opt-in trait for an aggregate whose every object representation is valid to read from foreign bytes. | ||
| 51 | * @details Before you specialize it, verify the whole transitive object representation. Also verify that no | ||
| 52 | * padding byte holds a value that you interpret. `std::array` opts in when its element type qualifies. | ||
| 53 | */ | ||
| 54 | template <class T> struct enable_representation_safe_aggregate : std::false_type | ||
| 55 | { | ||
| 56 | }; | ||
| 57 | |||
| 58 | /// True when @p T explicitly opts into representation-safe aggregate reads. | ||
| 59 | template <class T> | ||
| 60 | inline constexpr bool enable_representation_safe_aggregate_v = | ||
| 61 | enable_representation_safe_aggregate<std::remove_cv_t<T>>::value; | ||
| 62 | |||
| 63 | /** @brief True when @p E has a fixed underlying type, so [dcl.enum]/8 admits every underlying value. */ | ||
| 64 | template <class E> | ||
| 65 | concept fixed_underlying_enum = std::is_enum_v<E> && requires { E{std::underlying_type_t<E>{}}; }; | ||
| 66 | |||
| 67 | /** @brief True for a binary floating-point type with no padding bits. MinGW's x87 `long double` fails it. */ | ||
| 68 | template <class F> [[nodiscard]] constexpr bool padding_free_binary_float() noexcept | ||
| 69 | { | ||
| 70 | using Limits = std::numeric_limits<F>; | ||
| 71 | if constexpr (!Limits::is_iec559 || Limits::radix != 2 || Limits::max_exponent <= 0 || Limits::digits <= 0) | ||
| 72 | { | ||
| 73 | return false; | ||
| 74 | } | ||
| 75 | else | ||
| 76 | { | ||
| 77 | const int exponent_bits = | ||
| 78 | static_cast<int>(std::bit_width(static_cast<unsigned long long>(Limits::max_exponent))); | ||
| 79 | return 1 + exponent_bits + (Limits::digits - 1) == static_cast<int>(sizeof(F) * CHAR_BIT); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | /// @cond | ||
| 84 | template <class T> struct representation_read_value | ||
| 85 | { | ||
| 86 | using type = T; | ||
| 87 | }; | ||
| 88 | |||
| 89 | template <class T, std::size_t Size> struct representation_read_value<T[Size]> | ||
| 90 | { | ||
| 91 | using type = std::array<typename representation_read_value<T>::type, Size>; | ||
| 92 | }; | ||
| 93 | |||
| 94 | template <class T> using representation_read_value_t = typename representation_read_value<T>::type; | ||
| 95 | |||
| 96 | template <class T> | ||
| 97 | [[nodiscard]] representation_read_value_t<T> | ||
| 98 | 3083104 | decode_foreign_representation(const std::array<std::byte, sizeof(T)> &storage) noexcept | |
| 99 | { | ||
| 100 | static_assert( | ||
| 101 | sizeof(representation_read_value_t<T>) == sizeof(T), | ||
| 102 | "a built-in array read requires the equivalent std::array to have identical size" | ||
| 103 | ); | ||
| 104 | 3083104 | return std::bit_cast<representation_read_value_t<T>>(storage); | |
| 105 | } | ||
| 106 | |||
| 107 | template <class T> [[nodiscard]] constexpr bool representation_safe() noexcept | ||
| 108 | { | ||
| 109 | using U = std::remove_cv_t<T>; | ||
| 110 | if constexpr (std::is_same_v<U, bool>) | ||
| 111 | return false; | ||
| 112 | else if constexpr (std::is_bounded_array_v<U>) | ||
| 113 | return representation_safe<std::remove_extent_t<U>>(); | ||
| 114 | else if constexpr (std::is_unbounded_array_v<U>) | ||
| 115 | return false; | ||
| 116 | else if constexpr (std::is_integral_v<U>) | ||
| 117 | return true; | ||
| 118 | else if constexpr (std::is_floating_point_v<U>) | ||
| 119 | return padding_free_binary_float<U>(); | ||
| 120 | else if constexpr (std::is_enum_v<U>) | ||
| 121 | // [dcl.enum]/8 gives `enum class E : bool` only the two bool values, so the base must qualify too. | ||
| 122 | return fixed_underlying_enum<U> && representation_safe<std::underlying_type_t<U>>(); | ||
| 123 | else if constexpr (std::is_pointer_v<U>) | ||
| 124 | return true; | ||
| 125 | else if constexpr (std::is_member_pointer_v<U> || std::is_null_pointer_v<U>) | ||
| 126 | return false; | ||
| 127 | else | ||
| 128 | return (std::is_class_v<U> || std::is_union_v<U>) && std::is_trivially_copyable_v<U> && | ||
| 129 | enable_representation_safe_aggregate_v<U>; | ||
| 130 | } | ||
| 131 | |||
| 132 | template <class T, std::size_t Size> | ||
| 133 | struct enable_representation_safe_aggregate<std::array<T, Size>> : std::bool_constant<representation_safe<T>()> | ||
| 134 | { | ||
| 135 | }; | ||
| 136 | |||
| 137 | template <> struct enable_representation_safe_aggregate<Address> : std::true_type | ||
| 138 | { | ||
| 139 | }; | ||
| 140 | /// @endcond | ||
| 141 | |||
| 142 | static_assert( | ||
| 143 | std::is_trivially_copyable_v<Address> && std::is_standard_layout_v<Address> && | ||
| 144 | sizeof(Address) == sizeof(std::uintptr_t) && alignof(Address) == alignof(std::uintptr_t), | ||
| 145 | "Address participates in representation-safe reads only while it is exactly one padding-free " | ||
| 146 | "std::uintptr_t; a stored flag or a wider member would make read<Address> unsound" | ||
| 147 | ); | ||
| 148 | |||
| 149 | /** | ||
| 150 | * @brief True when every bit pattern of the object representation of @p T is a valid value of @p T. | ||
| 151 | * @details The domain is an allowlist. Copy any other type as raw bytes with @ref memory::read_into. | ||
| 152 | * - Every integral type except `bool`. Decode `bool` with @ref memory::read_bool. | ||
| 153 | * - A padding-free binary float: `float`, `double`, and `long double` only on MSVC. | ||
| 154 | * - An enumeration whose fixed underlying type is in the domain. The value can match no enumerator. | ||
| 155 | * - An object or function pointer, under the Windows x64 ABI. The value has no pointer provenance. | ||
| 156 | * Screen it with @ref memory::is_plausible_ptr. Read through it only by a guarded route. | ||
| 157 | * - A bounded built-in array or `std::array` of an element type in the domain. | ||
| 158 | * - @ref Address, and a class or union opted in through @ref enable_representation_safe_aggregate. | ||
| 159 | */ | ||
| 160 | template <class T> inline constexpr bool is_representation_safe_v = representation_safe<T>(); | ||
| 161 | } // namespace detail | ||
| 162 | |||
| 163 | namespace memory | ||
| 164 | { | ||
| 165 | /// Inclusive lower bound of the x64 user-mode pointer window. The reserved low 64 KiB holds no object. | ||
| 166 | inline constexpr std::uintptr_t USERSPACE_PTR_MIN = 0x10000; | ||
| 167 | |||
| 168 | /// Exclusive upper bound of the x64 user-mode pointer window, at the 47-bit canonical split. | ||
| 169 | inline constexpr std::uintptr_t USERSPACE_PTR_MAX = 0x0000800000000000ULL; | ||
| 170 | |||
| 171 | /// Largest span that each write entry point accepts. A larger span fails with `ErrorCode::SizeTooLarge`. | ||
| 172 | inline constexpr std::size_t MAX_WRITE_SIZE = 64ULL * 1024 * 1024; | ||
| 173 | |||
| 174 | /// Default number of region entries across the protection cache. | ||
| 175 | inline constexpr std::size_t DEFAULT_CACHE_SIZE = 256; | ||
| 176 | /// Default expiry window of a cache entry, in milliseconds, before a re-query. | ||
| 177 | inline constexpr unsigned int DEFAULT_CACHE_EXPIRY_MS = 50; | ||
| 178 | /// Minimum permitted cache size. | ||
| 179 | inline constexpr std::size_t MIN_CACHE_SIZE = 1; | ||
| 180 | /// Default number of cache shards. | ||
| 181 | inline constexpr std::size_t DEFAULT_CACHE_SHARD_COUNT = 16; | ||
| 182 | /// Default ratio of the hard entry maximum of a shard to its configured capacity. | ||
| 183 | inline constexpr std::size_t DEFAULT_MAX_CACHE_SIZE_MULTIPLIER = 2; | ||
| 184 | |||
| 185 | /** | ||
| 186 | * @brief True when @p address lies in [@ref USERSPACE_PTR_MIN, @ref USERSPACE_PTR_MAX). It proves no mapping. | ||
| 187 | * @note Callback-safe. | ||
| 188 | */ | ||
| 189 | 1750 | [[nodiscard]] inline constexpr bool is_plausible_ptr(Address address) noexcept | |
| 190 | { | ||
| 191 | 1750 | const std::uintptr_t value = address.raw(); | |
| 192 | 1750 | return value >= USERSPACE_PTR_MIN && value < USERSPACE_PTR_MAX; | |
| 193 | } | ||
| 194 | |||
| 195 | /** | ||
| 196 | * @brief Guarded copy of @p out.size() bytes from @p address into @p out. An empty @p out is a no-op success. | ||
| 197 | * @return An empty `Result`, or `ErrorCode::OverlappingRanges`, with nothing read, if @p out intersects the | ||
| 198 | * source. Otherwise `ErrorCode::ReadFaulted` for a source span that wraps, leaves | ||
| 199 | * [@ref USERSPACE_PTR_MIN, @ref USERSPACE_PTR_MAX), or holds an unreadable byte, unless the guard-page | ||
| 200 | * re-arm in `[B-20]` fails. On failure the contents of @p out are unspecified. | ||
| 201 | * @details `Error::detail` holds an unreadable source address. A scalar typed @ref read names its first | ||
| 202 | * unreadable byte. A larger span can name a later byte of the same region. A span rejected before any | ||
| 203 | * access, and the MinGW `VirtualQuery` fallback, report @p address. | ||
| 204 | * @note Callback-safe: on the established fast path it allocates nothing, takes no lock, and issues no syscall. | ||
| 205 | */ | ||
| 206 | [[nodiscard]] Result<void> read_into(Address address, std::span<std::byte> out) noexcept; | ||
| 207 | |||
| 208 | /** | ||
| 209 | * @brief Guarded typed read of a representation-safe @p T, which need not be default constructible. | ||
| 210 | * @return The value, or the @ref read_into error. A top-level bounded built-in array returns as the equivalent | ||
| 211 | * nested `std::array`. | ||
| 212 | * @note Callback-safe (see @ref read_into). | ||
| 213 | */ | ||
| 214 | template <class T> | ||
| 215 | requires(std::is_trivially_copyable_v<T> && detail::is_representation_safe_v<T>) | ||
| 216 | 1540 | [[nodiscard]] Result<detail::representation_read_value_t<T>> read(Address address) noexcept | |
| 217 | { | ||
| 218 | 1540 | std::array<std::byte, sizeof(T)> storage{}; | |
| 219 | 1540 | if (auto outcome = read_into(address, storage); !outcome) | |
| 220 | { | ||
| 221 | 20 | return std::unexpected(outcome.error()); | |
| 222 | } | ||
| 223 | 1520 | return detail::decode_foreign_representation<T>(storage); | |
| 224 | } | ||
| 225 | |||
| 226 | /** | ||
| 227 | * @brief Guarded checked decode of one foreign byte into a `bool`. | ||
| 228 | * @return `false` for a byte of `0` and `true` for `1`. A read fault returns the @ref read_into error. Any | ||
| 229 | * other byte returns `ErrorCode::InvalidRepresentation` with the source address in `Error::detail`. | ||
| 230 | * @note Callback-safe (see @ref read_into). | ||
| 231 | */ | ||
| 232 | [[nodiscard]] Result<bool> read_bool(Address address) noexcept; | ||
| 233 | |||
| 234 | /** | ||
| 235 | * @brief Guarded data write. It changes page protection only if a first attempt without a change faults. | ||
| 236 | * @param source An empty span is a no-op success, but a null @p address still fails with `NullTargetAddress`. | ||
| 237 | * @return An empty `Result`, or `ErrorCode::NullTargetAddress`, `NullSourceBytes`, `SizeTooLarge` (over | ||
| 238 | * @ref MAX_WRITE_SIZE), `OverlappingRanges` (nothing written), `ProtectionChangeFailed`, `WriteFaulted` | ||
| 239 | * (nothing written), @ref ErrorCode::WriteMayBePartial, `InstructionFlushFailed`, or | ||
| 240 | * `ProtectionRestoreFailed`. | ||
| 241 | * @details A successful first attempt issues no flush. A first attempt that faults past the first byte flushes | ||
| 242 | * the whole request if any region of it is executable. Use @ref patch_code for bytes that execute. | ||
| 243 | * | ||
| 244 | * The slow path makes each `VirtualQuery` region writable, with no execute added to a data page. It | ||
| 245 | * then copies, flushes executable regions, restores each prior protection, and invalidates the cached | ||
| 246 | * range. A failed change, for example over too many regions, returns `WriteMayBePartial` if the first | ||
| 247 | * attempt faulted past the first byte, and otherwise `ProtectionChangeFailed`. A slow-path copy fault | ||
| 248 | * returns `WriteFaulted` after the restore and flush if both attempts faulted on the first byte, and | ||
| 249 | * otherwise `WriteMayBePartial`. `ProtectionRestoreFailed` outranks every other slow-path error. | ||
| 250 | * @note Callback-safe on the fast path. The slow path changes protection and is setup/control-plane work. | ||
| 251 | */ | ||
| 252 | [[nodiscard]] Result<void> write_bytes(Address address, std::span<const std::byte> source) noexcept; | ||
| 253 | |||
| 254 | /** | ||
| 255 | * @brief Guarded write of the object representation of @p value through @ref write_bytes, which it returns. | ||
| 256 | * @note Callback-safe on the fast path (see @ref write_bytes). | ||
| 257 | */ | ||
| 258 | template <class T> | ||
| 259 | requires std::is_trivially_copyable_v<T> && (!detail::is_non_owning_view_v<std::remove_cvref_t<T>>) | ||
| 260 | 8 | [[nodiscard]] Result<void> write(Address address, const T &value) noexcept | |
| 261 | { | ||
| 262 | 8 | const auto storage = std::bit_cast<std::array<std::byte, sizeof(T)>>(value); | |
| 263 | 8 | return write_bytes(address, std::span<const std::byte>{storage}); | |
| 264 | } | ||
| 265 | |||
| 266 | /** | ||
| 267 | * @brief Guarded code patch: writes @p source at @p address and flushes the instruction cache for the target. | ||
| 268 | * @return An empty `Result`, or an error of @ref write_bytes. `InstructionFlushFailed` means that the bytes | ||
| 269 | * landed but the flush failed. | ||
| 270 | * @details The argument rules and the slow path match @ref write_bytes, except that every path that can modify | ||
| 271 | * the target flushes, even on already-writable code. | ||
| 272 | * @warning The write is not atomic. `ProtectionRestoreFailed` outranks `WriteMayBePartial`, which outranks | ||
| 273 | * `InstructionFlushFailed`. | ||
| 274 | * @note Callback-safe on the fast path. The slow path changes protection and is setup/control-plane work. | ||
| 275 | */ | ||
| 276 | [[nodiscard]] Result<void> patch_code(Address address, std::span<const std::byte> source) noexcept; | ||
| 277 | |||
| 278 | /** | ||
| 279 | * @brief Strict guarded write of a byte span that never changes page protection. | ||
| 280 | * @return An empty `Result`, or an argument error of @ref write_bytes under its rules. `WriteFaulted`, with no | ||
| 281 | * byte changed, when the first target byte is not writable, as on a read-only or execute-read page. | ||
| 282 | * `WriteMayBePartial` when a later byte faulted after the copy reached a writable page. | ||
| 283 | * @warning The write is not atomic across a writability seam, and the changed prefix of `WriteMayBePartial` is | ||
| 284 | * indeterminate and can be empty. Size a per-frame store so that it cannot cross a seam. | ||
| 285 | * @note Callback-safe (see @ref read_into). | ||
| 286 | */ | ||
| 287 | [[nodiscard]] Result<void> write_in_place(Address address, std::span<const std::byte> source) noexcept; | ||
| 288 | |||
| 289 | /** | ||
| 290 | * @brief Strict guarded write of the object representation of @p value through @ref write_in_place. | ||
| 291 | * @note A `std::span<std::byte>` or other contiguous byte view writes its viewed bytes, not the view object. | ||
| 292 | * @note Callback-safe (see @ref write_in_place). | ||
| 293 | */ | ||
| 294 | template <class T> | ||
| 295 | requires std::is_trivially_copyable_v<T> && (!detail::is_non_owning_view_v<std::remove_cvref_t<T>>) | ||
| 296 | 4 | [[nodiscard]] Result<void> write_in_place(Address address, const T &value) noexcept | |
| 297 | { | ||
| 298 | 4 | const auto storage = std::bit_cast<std::array<std::byte, sizeof(T)>>(value); | |
| 299 | 4 | return write_in_place(address, std::span<const std::byte>{storage}); | |
| 300 | } | ||
| 301 | |||
| 302 | /** | ||
| 303 | * @brief One hop of a pointer-chain @ref walk. The walk adds @ref offset to the current address, then | ||
| 304 | * dereferences the result on every hop except the last. | ||
| 305 | */ | ||
| 306 | struct ChainStep | ||
| 307 | { | ||
| 308 | /// Byte offset of this hop. It can be negative. | ||
| 309 | std::ptrdiff_t offset; | ||
| 310 | /// Floor of the dereferenced link. A link outside [min_valid, @ref USERSPACE_PTR_MAX) stops the walk. | ||
| 311 | Address min_valid = Address{USERSPACE_PTR_MIN}; | ||
| 312 | }; | ||
| 313 | |||
| 314 | /** | ||
| 315 | * @brief Resolves a pointer chain under the fault guard and reports each intermediate hop. | ||
| 316 | * @param trace `trace[i]` receives the dereferenced link or the final leaf of hop `i`, for as many hops as fit. | ||
| 317 | * The walk fills `trace` for each completed hop, even when a later hop fails. | ||
| 318 | * @return The leaf address, which the walk does not read, or @p base for an empty chain. `ErrorCode::NullChain` | ||
| 319 | * for a null @p base with a non-empty chain. `ErrorCode::ReadFaulted` for a faulted dereference, a link | ||
| 320 | * outside its @ref ChainStep range, or a leaf that wraps or leaves | ||
| 321 | * [@ref USERSPACE_PTR_MIN, @ref USERSPACE_PTR_MAX). `Error::detail` holds the failed hop index. | ||
| 322 | * @note Callback-safe (see @ref read_into). | ||
| 323 | */ | ||
| 324 | [[nodiscard]] Result<Address> | ||
| 325 | walk(Address base, std::span<const ChainStep> steps, std::span<Address> trace = {}) noexcept; | ||
| 326 | |||
| 327 | /** | ||
| 328 | * @brief Pointer-chain @ref walk over bare offsets, with every hop floored at @ref USERSPACE_PTR_MIN. | ||
| 329 | * @return The @ref ChainStep overload result, or `ErrorCode::SizeTooLarge` for more than 32 offsets. | ||
| 330 | * @note Callback-safe (see @ref read_into). Route a longer chain through the @ref ChainStep overload. | ||
| 331 | */ | ||
| 332 | [[nodiscard]] Result<Address> | ||
| 333 | walk(Address base, std::span<const std::ptrdiff_t> offsets, std::span<Address> trace = {}) noexcept; | ||
| 334 | |||
| 335 | /** | ||
| 336 | * @brief Move-only guard that changes a @ref Region to a @ref Prot until scope exit. | ||
| 337 | * @details The guard captures and restores each `VirtualQuery` region of the span separately. Destructor | ||
| 338 | * restoration is best-effort. To observe the result, call @ref restore. | ||
| 339 | * @note @ref make, @ref restore, the destructor, and move-assignment each invalidate the cached range. | ||
| 340 | * Move-assignment first restores the region of the replaced guard. | ||
| 341 | */ | ||
| 342 | class ProtectGuard | ||
| 343 | { | ||
| 344 | public: | ||
| 345 | /** | ||
| 346 | * @brief Changes @p region to @p protection and returns a guard that restores the prior protection. | ||
| 347 | * @return An armed guard, or `ErrorCode::OutOfMemory` with no protection changed. | ||
| 348 | * `ErrorCode::ProtectionChangeFailed` for a null or empty region, with `Error::extra` zero. The | ||
| 349 | * same code, with the OS error in `Error::extra`, for a failed change or too many protection | ||
| 350 | * regions. The call first rolls back each changed region. `ErrorCode::ProtectionRestoreFailed` if | ||
| 351 | * that rollback failed. | ||
| 352 | * @note Setup/control-plane only: the guard allocates and issues `VirtualProtect` calls. | ||
| 353 | */ | ||
| 354 | [[nodiscard]] static Result<ProtectGuard> make(Region region, Prot protection) noexcept; | ||
| 355 | |||
| 356 | ProtectGuard(ProtectGuard &&other) noexcept; | ||
| 357 | ProtectGuard &operator=(ProtectGuard &&other) noexcept; | ||
| 358 | ProtectGuard(const ProtectGuard &) = delete; | ||
| 359 | ProtectGuard &operator=(const ProtectGuard &) = delete; | ||
| 360 | |||
| 361 | /// Restores the original page protection if the guard is still armed. | ||
| 362 | ~ProtectGuard() noexcept; | ||
| 363 | |||
| 364 | /// True while the guard is armed. False after a move, @ref release, or @ref restore. | ||
| 365 | [[nodiscard]] explicit operator bool() const noexcept; | ||
| 366 | |||
| 367 | /** | ||
| 368 | * @brief Disarms the guard, so its destructor leaves the changed protection in place. | ||
| 369 | * @details If no other guard holds a page, the next guard captures its current protection. If another | ||
| 370 | * guard still holds the page, the applied protection becomes that guard's restore baseline. | ||
| 371 | * @note Setup/control-plane only: the call takes the protection ledger lock. | ||
| 372 | */ | ||
| 373 | void release() noexcept; | ||
| 374 | |||
| 375 | /** | ||
| 376 | * @brief Restores the original protection now, reports the result, and disarms the guard. | ||
| 377 | * @return An empty `Result`, also for a disarmed guard, or `ErrorCode::ProtectionRestoreFailed` with the OS | ||
| 378 | * error in `Error::extra`. The guard disarms on both outcomes. | ||
| 379 | * @note Setup/control-plane only: the restore issues `VirtualProtect` calls. | ||
| 380 | */ | ||
| 381 | [[nodiscard]] Result<void> restore() noexcept; | ||
| 382 | |||
| 383 | private: | ||
| 384 | // Only make() constructs a guard. | ||
| 385 | ProtectGuard() noexcept; | ||
| 386 | |||
| 387 | // The captured regions live in the implementation file, so this header holds no Win32 type. | ||
| 388 | struct Impl; | ||
| 389 | std::unique_ptr<Impl> m_impl; | ||
| 390 | }; | ||
| 391 | |||
| 392 | /** | ||
| 393 | * @brief Resolves the mapped image span of the module that owns @p address, from its current PE headers. | ||
| 394 | * @return The module's @ref Region, or an empty Region when @p address is null, lies in no loaded module, or | ||
| 395 | * the module's PE headers do not validate. | ||
| 396 | * @note Setup/control-plane only: the call issues a loader lookup and a guarded PE-header read. | ||
| 397 | * @warning The Region does not pin the module. After an unload, the span references freed address space. | ||
| 398 | */ | ||
| 399 | [[nodiscard]] Region module_of(Address address) noexcept; | ||
| 400 | |||
| 401 | /** | ||
| 402 | * @brief Reports whether a loaded module has the base name @p basename, for example "kernel32.dll", not a path. | ||
| 403 | * @details A module path longer than `MAX_PATH` does not change the result in either case mode. | ||
| 404 | * @note Setup/control-plane only: the query reaches the loader. An exact-case request fails closed under the | ||
| 405 | * loader lock. | ||
| 406 | */ | ||
| 407 | [[nodiscard]] bool is_module_loaded(std::string_view basename, bool case_insensitive = true) noexcept; | ||
| 408 | |||
| 409 | /** | ||
| 410 | * @brief Allocation-free snapshot of the cache configuration and counters, not atomic as a whole. | ||
| 411 | * @details @ref clear_cache on a running cache and a clean @ref shutdown_cache reset every counter except | ||
| 412 | * `lifecycle_violations`. | ||
| 413 | */ | ||
| 414 | struct MemoryStats | ||
| 415 | { | ||
| 416 | /// Configured number of cache shards. | ||
| 417 | std::size_t shard_count = 0; | ||
| 418 | /// Configured soft entry capacity per shard. | ||
| 419 | std::size_t max_entries_per_shard = 0; | ||
| 420 | /// Hard maximum entries per shard (capacity * multiplier), averaged across shards. | ||
| 421 | std::size_t hard_max_per_shard = 0; | ||
| 422 | /// Cache-entry expiry window in milliseconds. | ||
| 423 | unsigned int expiry_ms = 0; | ||
| 424 | /// Cache hits. | ||
| 425 | std::uint64_t hits = 0; | ||
| 426 | /// Cache misses. | ||
| 427 | std::uint64_t misses = 0; | ||
| 428 | /// Range invalidations. | ||
| 429 | std::uint64_t invalidations = 0; | ||
| 430 | /// In-flight query coalesces. | ||
| 431 | std::uint64_t coalesced_queries = 0; | ||
| 432 | /// On-demand cleanup passes. | ||
| 433 | std::uint64_t on_demand_cleanups = 0; | ||
| 434 | /// Live entry count summed across all shards at snapshot time. | ||
| 435 | std::size_t total_entries = 0; | ||
| 436 | /// hits / (hits + misses) * 100, or -1.0 when hits + misses is zero. | ||
| 437 | double hit_rate_percent = -1.0; | ||
| 438 | /// Recovered cleanup-thread lifecycle violations, normally zero. No call resets it. | ||
| 439 | std::uint64_t lifecycle_violations = 0; | ||
| 440 | }; | ||
| 441 | |||
| 442 | /** | ||
| 443 | * @brief Initializes the protection cache that @ref is_readable and @ref is_writable consult. | ||
| 444 | * @param cache_size Soft entry capacity across all shards. Each shard rounds its share up. | ||
| 445 | * @return True when the cache runs. False when the lifecycle state blocks a start or setup fails, and readers | ||
| 446 | * then use the uncached `VirtualQuery` route. | ||
| 447 | * @details A call while the cache runs returns true and keeps its configuration, with no loader-lock check. A | ||
| 448 | * call after @ref shutdown_cache uses its own arguments. A start fails if readers of a prior session | ||
| 449 | * outlive the drain deadline (see @ref shutdown_cache). A start creates the cleanup thread if the | ||
| 450 | * platform permits it, and otherwise cleanup runs on demand. On MinGW, a start also installs the | ||
| 451 | * guarded-read fault handler. | ||
| 452 | * @note Setup/control-plane only. | ||
| 453 | */ | ||
| 454 | [[nodiscard]] bool init_cache( | ||
| 455 | std::size_t cache_size = DEFAULT_CACHE_SIZE, | ||
| 456 | unsigned int expiry_ms = DEFAULT_CACHE_EXPIRY_MS, | ||
| 457 | std::size_t shard_count = DEFAULT_CACHE_SHARD_COUNT | ||
| 458 | ); | ||
| 459 | |||
| 460 | /** | ||
| 461 | * @brief Clears every protection-cache entry. The cache and its cleanup thread continue to run. | ||
| 462 | * @note Setup/control-plane only: the clear takes every shard's exclusive lock. | ||
| 463 | */ | ||
| 464 | void clear_cache() noexcept; | ||
| 465 | |||
| 466 | /** | ||
| 467 | * @brief Shuts the cache down and joins the cleanup thread, or detaches it under the loader lock. | ||
| 468 | * @details The cache then needs @ref init_cache before reuse, and a permission query takes the uncached | ||
| 469 | * `VirtualQuery` route. The wait for admitted readers has a fixed deadline. A clean shutdown releases | ||
| 470 | * the cache storage and module reference. On deadline expiry, the cache retains both and records one | ||
| 471 | * @ref diagnostics::LeakSubsystem::MemoryCache event. A later @ref init_cache or @ref shutdown_cache | ||
| 472 | * can reclaim the storage after the stalled reader exits. | ||
| 473 | * | ||
| 474 | * On MinGW, the call releases the guarded-read fault handler, except at process exit. The first | ||
| 475 | * guarded read can install that handler without @ref init_cache. Without a @ref Session, call this | ||
| 476 | * after the last guarded read of the module and before unload. Hook destruction off the loader lock | ||
| 477 | * takes a guarded read. | ||
| 478 | * | ||
| 479 | * Session teardown blocks lazy handler installation until Session setup or @ref init_cache succeeds. | ||
| 480 | * In that interval, MinGW byte access uses the validated fallback, and guarded region scans fail | ||
| 481 | * closed. A VMT hook object update installs the handler only for that call. Without an active | ||
| 482 | * @ref Session, the call also returns the emit-chain TLS index of each idle diagnostics dispatcher, | ||
| 483 | * as @ref diagnostics::hook_lifecycle documents. | ||
| 484 | * @note Setup/control-plane only. | ||
| 485 | */ | ||
| 486 | void shutdown_cache() noexcept; | ||
| 487 | |||
| 488 | /** @brief Returns an allocation-free snapshot of the cache statistics. */ | ||
| 489 | [[nodiscard]] MemoryStats get_memory_stats() noexcept; | ||
| 490 | |||
| 491 | /** @brief Formats @ref get_memory_stats as a human-readable string. */ | ||
| 492 | [[nodiscard]] std::string get_cache_stats(); | ||
| 493 | |||
| 494 | /** | ||
| 495 | * @brief Drops the cached protection of every entry that overlaps @p range. An empty range is a no-op. | ||
| 496 | * @details Call it after other code changes protection. Paths in this header that change protection call it. | ||
| 497 | * @note Setup/control-plane only: the invalidation mutates the cache shards. | ||
| 498 | */ | ||
| 499 | void invalidate_range(Region range) noexcept; | ||
| 500 | |||
| 501 | /** @brief Tri-state result of @ref is_readable_nonblocking. */ | ||
| 502 | enum class ReadableStatus : std::uint8_t | ||
| 503 | { | ||
| 504 | /// The region is committed and readable. | ||
| 505 | Readable, | ||
| 506 | /// The region is not committed, not readable, or the call rejected the arguments. | ||
| 507 | NotReadable, | ||
| 508 | /// The check needs a wait. @ref is_readable_nonblocking lists the cases. | ||
| 509 | Unknown | ||
| 510 | }; | ||
| 511 | |||
| 512 | /** | ||
| 513 | * @brief Reports whether @p range is committed and readable. An empty range returns false. | ||
| 514 | * @warning Do not call it on a per-dereference hot path. A hit takes a shard reader lock, and a miss can call | ||
| 515 | * `VirtualQuery` once per region. The answer is a time-of-check/time-of-use snapshot. | ||
| 516 | * @note Setup/control-plane only. A hot path uses a guarded @ref read or @ref is_readable_nonblocking. | ||
| 517 | */ | ||
| 518 | [[nodiscard]] bool is_readable(Region range) noexcept; | ||
| 519 | |||
| 520 | /** | ||
| 521 | * @brief Reports whether @p range is committed and writable. An empty range returns false. | ||
| 522 | * @warning The @ref is_readable hot-path warning applies. To write, attempt a guarded @ref write_bytes instead. | ||
| 523 | * @note Setup/control-plane only (see @ref is_readable). | ||
| 524 | */ | ||
| 525 | [[nodiscard]] bool is_writable(Region range) noexcept; | ||
| 526 | |||
| 527 | /** | ||
| 528 | * @brief Readability check that returns @ref ReadableStatus::Unknown instead of a wait. | ||
| 529 | * @return `NotReadable` for an empty range. `Unknown` only while the cache runs, after shard lock contention, a | ||
| 530 | * cache miss, or a concurrent shutdown that unpublished the shards. | ||
| 531 | * @note Callback-safe while the cache runs: a try-lock probe with no allocation. If the cache does not run, the | ||
| 532 | * call blocks on a `VirtualQuery` walk of each region. | ||
| 533 | */ | ||
| 534 | [[nodiscard]] ReadableStatus is_readable_nonblocking(Region range) noexcept; | ||
| 535 | |||
| 536 | /** @brief Raw reads with no validation. Each entry point here faults the host on an unreadable byte. */ | ||
| 537 | namespace unchecked | ||
| 538 | { | ||
| 539 | /** | ||
| 540 | * @brief Unguarded typed read of a representation-safe @p T. | ||
| 541 | * @param address Every byte of `[address, address + sizeof(T))` must be committed, readable, and live for | ||
| 542 | * the current frame. Use the guarded @ref read for an address that can be stale. | ||
| 543 | * @return The value. A top-level bounded built-in array returns as the equivalent nested `std::array`. | ||
| 544 | * @note Callback-safe under `NDEBUG`. A Debug build can block or call `VirtualQuery` in its `is_readable` | ||
| 545 | * assertion. | ||
| 546 | * @warning Under `NDEBUG`, an invalid address faults the host. A Debug build stops at the assertion. | ||
| 547 | */ | ||
| 548 | template <class T> | ||
| 549 | requires(std::is_trivially_copyable_v<T> && detail::is_representation_safe_v<T>) | ||
| 550 | 16 | [[nodiscard]] detail::representation_read_value_t<T> read(Address address) noexcept | |
| 551 | { | ||
| 552 | // assert() discards the is_readable() probe under NDEBUG. | ||
| 553 | 16 | assert( | |
| 554 | is_readable(Region{address, sizeof(T)}) && | ||
| 555 | "unchecked::read<T>: address is not fully readable; the caller's safety precondition is violated" | ||
| 556 | ); | ||
| 557 | 16 | std::array<std::byte, sizeof(T)> storage{}; | |
| 558 | 16 | std::memcpy(storage.data(), address.as<const void *>(), sizeof(T)); | |
| 559 | 16 | return detail::decode_foreign_representation<T>(storage); | |
| 560 | } | ||
| 561 | } // namespace unchecked | ||
| 562 | } // namespace memory | ||
| 563 | } // namespace DetourModKit | ||
| 564 | |||
| 565 | #endif // DETOURMODKIT_MEMORY_HPP | ||
| 566 |