./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
115 / 126
91.3%
Branches:
21 / 34
61.8%
Functions:
38 / 38
100.0%

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
4/4
✓ Branch 3 → 4 taken 1742 times.
✓ Branch 3 → 6 taken 8 times.
✓ Branch 4 → 5 taken 1736 times.
✓ Branch 4 → 6 taken 6 times.
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
15/26
std::expected<DetourModKit::detail::representation_read_value<_IMAGE_DOS_HEADER>::type, DetourModKit::Error> DetourModKit::memory::read<_IMAGE_DOS_HEADER>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 745 times.
std::expected<DetourModKit::detail::representation_read_value<_IMAGE_NT_HEADERS64>::type, DetourModKit::Error> DetourModKit::memory::read<_IMAGE_NT_HEADERS64>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 742 times.
std::expected<DetourModKit::detail::representation_read_value<int [2][3]>::type, DetourModKit::Error> DetourModKit::memory::read<int [2][3]>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<int [2]>::type, DetourModKit::Error> DetourModKit::memory::read<int [2]>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<(anonymous namespace)::OptedAggregate>::type, DetourModKit::Error> DetourModKit::memory::read<(anonymous namespace)::OptedAggregate>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<representation_read::Sample>::type, DetourModKit::Error> DetourModKit::memory::read<representation_read::Sample>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<representation_read::NoDefault>::type, DetourModKit::Error> DetourModKit::memory::read<representation_read::NoDefault>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<DetourModKit::Address>::type, DetourModKit::Error> DetourModKit::memory::read<DetourModKit::Address>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<std::array<std::byte, 4ull> >::type, DetourModKit::Error> DetourModKit::memory::read<std::array<std::byte, 4ull> >(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 1 time.
std::expected<DetourModKit::detail::representation_read_value<unsigned char>::type, DetourModKit::Error> DetourModKit::memory::read<unsigned char>(DetourModKit::Address):
✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 10 not taken.
std::expected<DetourModKit::detail::representation_read_value<int>::type, DetourModKit::Error> DetourModKit::memory::read<int>(DetourModKit::Address):
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 10 taken 2 times.
std::expected<DetourModKit::detail::representation_read_value<unsigned int>::type, DetourModKit::Error> DetourModKit::memory::read<unsigned int>(DetourModKit::Address):
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 10 taken 2 times.
std::expected<DetourModKit::detail::representation_read_value<unsigned long long>::type, DetourModKit::Error> DetourModKit::memory::read<unsigned long long>(DetourModKit::Address):
✓ Branch 5 → 6 taken 17 times.
✓ Branch 5 → 10 taken 22 times.
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
2/4
DetourModKit::detail::representation_read_value<int [2]>::type DetourModKit::memory::unchecked::read<int [2]>(DetourModKit::Address):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
DetourModKit::detail::representation_read_value<unsigned long long>::type DetourModKit::memory::unchecked::read<unsigned long long>(DetourModKit::Address):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 15 times.
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