GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 91.7% 111 / 0 / 121
Functions: 100.0% 37 / 0 / 37
Branches: 62.5% 20 / 0 / 32

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 A guarded access turns a fault into a `Result` error instead of a host termination. The fault guard (MSVC
8 * `__try`, MinGW vectored handler) lives entirely in the engine translation unit, so this header pulls in no
9 * `<windows.h>` and no SEH.
10 *
11 * The surface is layered by safety:
12 * - `read`, `read_into`, `write`, `write_bytes`, and `walk` are GUARDED. They validate, fault-protect, and
13 * report failure as an `Error`. Use them whenever the address can be stale.
14 * - `is_plausible_ptr` is a pure arithmetic pre-screen with no syscall and no access.
15 * - The cache and the `is_readable` and `is_writable` predicates answer protection questions for one-shot
16 * setup validation and diagnostics, not for per-frame hot paths. Each consults a lock and, on a miss, can
17 * walk the range one `VirtualQuery` per region.
18 * - `unchecked::read` performs NO validation and FAULTS THE HOST on an unreadable byte.
19 * @warning `[B-100]` Under the loader lock, call only the Callback-safe entry points in this header. Cache startup and
20 * the exact-case module lookup fail closed.
21 */
22
23 #include "DetourModKit/address.hpp"
24 #include "DetourModKit/defines.hpp"
25 #include "DetourModKit/error.hpp"
26 #include "DetourModKit/region.hpp"
27
28 #include <array>
29 #include <bit>
30 #include <cassert>
31 #include <climits>
32 #include <cstddef>
33 #include <cstdint>
34 #include <cstring>
35 #include <limits>
36 #include <memory>
37 #include <span>
38 #include <string>
39 #include <string_view>
40 #include <type_traits>
41
42 namespace DetourModKit
43 {
44 namespace detail
45 {
46 /**
47 * @brief Trait that is true for any non-owning view type: a `std::span<U, Extent>` of any element type, or a
48 * `std::basic_string_view`.
49 * @details `[B-21]` A view is trivially copyable, but its bit-copy stores the view's pointer and length. Typed
50 * `write<T>` rejects every view, even byte spans. Only `write_in_place` routes a byte span to its
51 * byte-span overload. Use `write_bytes` for other views. Constraint sites inspect
52 * `std::remove_cvref_t<T>` so a cv/ref qualification cannot slip one past.
53 */
54 template <class T> inline constexpr bool is_non_owning_view_v = false;
55 template <class U, std::size_t Extent> inline constexpr bool is_non_owning_view_v<std::span<U, Extent>> = true;
56 template <class CharT, class Traits>
57 inline constexpr bool is_non_owning_view_v<std::basic_string_view<CharT, Traits>> = true;
58
59 /**
60 * @brief Opt-in trait for aggregate types whose every object representation may be read from foreign bytes.
61 * @details The default is false because C++23 cannot inspect aggregate members: a trivially copyable class may
62 * still contain `bool` or another representation-sensitive member. Specialize this trait to
63 * `std::true_type` only after verifying the complete transitive object representation, including that
64 * the type has no padding bytes whose value the caller intends to interpret. Built-in arrays are
65 * checked recursively; `std::array` opts in when its element type is representation-safe.
66 * @tparam T Aggregate type to classify.
67 */
68 template <class T> struct enable_representation_safe_aggregate : std::false_type
69 {
70 };
71
72 /// True when @p T explicitly opts into representation-safe aggregate reads.
73 template <class T>
74 inline constexpr bool enable_representation_safe_aggregate_v =
75 enable_representation_safe_aggregate<std::remove_cv_t<T>>::value;
76
77 /**
78 * @brief True when @p E is an enumeration with a fixed underlying type.
79 * @details [dcl.enum]/8 gives such an enumeration the value range of its underlying type, so every bit pattern
80 * of that type is a valid enumerator value. Direct-list-initialization from the underlying type is
81 * well-formed only for the fixed case, which is the detection this concept uses.
82 */
83 template <class E>
84 concept fixed_underlying_enum = std::is_enum_v<E> && requires { E{std::underlying_type_t<E>{}}; };
85
86 /**
87 * @brief True when @p F is a binary floating-point type whose object representation carries no padding bits.
88 * @details Padding bits have no defined value, so a foreign byte pattern read into such a type is not
89 * necessarily a valid object representation. The bit-count test is required because `is_iec559` alone
90 * is not enough: MinGW's 16-byte x87 `long double` reports `is_iec559` for an 80-bit format.
91 */
92 template <class F> [[nodiscard]] constexpr bool padding_free_binary_float() noexcept
93 {
94 using Limits = std::numeric_limits<F>;
95 if constexpr (!Limits::is_iec559 || Limits::radix != 2 || Limits::max_exponent <= 0 || Limits::digits <= 0)
96 {
97 return false;
98 }
99 else
100 {
101 const int exponent_bits =
102 static_cast<int>(std::bit_width(static_cast<unsigned long long>(Limits::max_exponent)));
103 return 1 + exponent_bits + (Limits::digits - 1) == static_cast<int>(sizeof(F) * CHAR_BIT);
104 }
105 }
106
107 /// @cond
108 template <class T> struct representation_read_value
109 {
110 using type = T;
111 };
112
113 template <class T, std::size_t Size> struct representation_read_value<T[Size]>
114 {
115 using type = std::array<typename representation_read_value<T>::type, Size>;
116 };
117
118 template <class T> using representation_read_value_t = typename representation_read_value<T>::type;
119
120 template <class T>
121 [[nodiscard]] representation_read_value_t<T>
122 2509674 decode_foreign_representation(const std::array<std::byte, sizeof(T)> &storage) noexcept
123 {
124 static_assert(
125 sizeof(representation_read_value_t<T>) == sizeof(T),
126 "a built-in array read requires the equivalent std::array to have identical size"
127 );
128 2509674 return std::bit_cast<representation_read_value_t<T>>(storage);
129 }
130
131 template <class T> [[nodiscard]] constexpr bool representation_safe() noexcept
132 {
133 using U = std::remove_cv_t<T>;
134 if constexpr (std::is_same_v<U, bool>)
135 return false;
136 else if constexpr (std::is_bounded_array_v<U>)
137 return representation_safe<std::remove_extent_t<U>>();
138 else if constexpr (std::is_unbounded_array_v<U>)
139 return false;
140 else if constexpr (std::is_integral_v<U>)
141 return true;
142 else if constexpr (std::is_floating_point_v<U>)
143 return padding_free_binary_float<U>();
144 else if constexpr (std::is_enum_v<U>)
145 // The underlying type must itself qualify: `enum class E : bool` has a fixed base, yet [dcl.enum]/8
146 // gives it only bool's two values, so a foreign 0x02 is no more valid as an E than as a bool.
147 return fixed_underlying_enum<U> && representation_safe<std::underlying_type_t<U>>();
148 else if constexpr (std::is_pointer_v<U>)
149 return true;
150 else if constexpr (std::is_member_pointer_v<U> || std::is_null_pointer_v<U>)
151 return false;
152 else
153 return (std::is_class_v<U> || std::is_union_v<U>) && std::is_trivially_copyable_v<U> &&
154 enable_representation_safe_aggregate_v<U>;
155 }
156
157 template <class T, std::size_t Size>
158 struct enable_representation_safe_aggregate<std::array<T, Size>> : std::bool_constant<representation_safe<T>()>
159 {
160 };
161
162 template <> struct enable_representation_safe_aggregate<Address> : std::true_type
163 {
164 };
165 /// @endcond
166
167 static_assert(
168 std::is_trivially_copyable_v<Address> && std::is_standard_layout_v<Address> &&
169 sizeof(Address) == sizeof(std::uintptr_t) && alignof(Address) == alignof(std::uintptr_t),
170 "Address participates in representation-safe reads only while it is exactly one padding-free "
171 "std::uintptr_t; a stored flag or a wider member would make read<Address> unsound"
172 );
173
174 /**
175 * @brief True when every bit pattern of @p T's object representation is a valid value, so forming @p T from
176 * arbitrary foreign bytes with `std::bit_cast` is well defined.
177 * @details The participation gate for the raw typed reads (@ref memory::read, @ref memory::unchecked::read, and
178 * the engine's `detail::guarded_read`). The domain is an explicit allowlist, not "every scalar":
179 * - every integral type except `bool`;
180 * - a binary floating-point type with no padding bits (@ref padding_free_binary_float), which admits
181 * `float` and `double` on both toolchains and `long double` only on MSVC, where it is `double`;
182 * - an enumeration with a fixed underlying type (@ref fixed_underlying_enum) that is itself in the
183 * domain, which admits every scoped enumeration over an integer and `std::byte`;
184 * - an object or function pointer, as a Windows x64 ABI concession, not a portable C++ theorem. The
185 * result does NOT recover pointer provenance, so treat it as an address to screen with
186 * @ref memory::is_plausible_ptr and read through a guarded route, never as a pointer to
187 * dereference.
188 * - a bounded built-in array or `std::array` whose element type qualifies, recursively;
189 * - @ref Address, and any other class or union explicitly opted in through
190 * @ref enable_representation_safe_aggregate.
191 *
192 * Rejected: `bool`, because a foreign byte such as `0x02` is not a valid `bool` object
193 * representation and the bit-cast is undefined behavior before a `Result` can report it. Decode it
194 * with @ref memory::read_bool. Also rejected: `std::nullptr_t`; member-object and member-function
195 * pointers, whose representations are implementation-defined multi-field structures; an unscoped
196 * enumeration with no fixed base; an enumeration over `bool`; an unbounded array; and a
197 * floating-point format with padding bits, such as MinGW's 16-byte x87 `long double`. Use
198 * @ref memory::read_into to copy any of these as raw bytes and decode them yourself.
199 *
200 * Enum DOMAIN validity is a separate concern: a fixed-underlying enumeration's bit patterns are all
201 * valid representations even when a specific value is semantically invalid for an API.
202 */
203 template <class T> inline constexpr bool is_representation_safe_v = representation_safe<T>();
204 } // namespace detail
205
206 namespace memory
207 {
208 /**
209 * @brief Inclusive lower bound of the canonical x64 user-mode address window.
210 * @details The low 64 KiB is the reserved null-dereference region, so any value below this bound cannot be a
211 * valid object pointer.
212 */
213 inline constexpr std::uintptr_t USERSPACE_PTR_MIN = 0x10000;
214
215 /**
216 * @brief Exclusive upper bound of the canonical x64 user-mode address window.
217 * @details Mapped user addresses sit below the 47-bit canonical split, so a value at or above this bound is a
218 * kernel-range or non-canonical address.
219 */
220 inline constexpr std::uintptr_t USERSPACE_PTR_MAX = 0x0000800000000000ULL;
221
222 /// Maximum byte count a single @ref write_bytes call accepts before failing with ErrorCode::SizeTooLarge.
223 inline constexpr std::size_t MAX_WRITE_SIZE = 64ULL * 1024 * 1024;
224
225 /// Default number of region entries the protection cache holds.
226 inline constexpr std::size_t DEFAULT_CACHE_SIZE = 256;
227 /// Default cache entry lifetime, in milliseconds, before a re-query.
228 inline constexpr unsigned int DEFAULT_CACHE_EXPIRY_MS = 50;
229 /// Minimum permitted cache size.
230 inline constexpr std::size_t MIN_CACHE_SIZE = 1;
231 /// Default number of cache shards, striped to reduce reader contention.
232 inline constexpr std::size_t DEFAULT_CACHE_SHARD_COUNT = 16;
233 /// Default multiplier bounding the cache's hard maximum size relative to its configured size.
234 inline constexpr std::size_t DEFAULT_MAX_CACHE_SIZE_MULTIPLIER = 2;
235
236 /**
237 * @brief Structural plausibility test for an x64 user-mode pointer.
238 * @param address The address to test.
239 * @return True only when @p address lies in [@ref USERSPACE_PTR_MIN, @ref USERSPACE_PTR_MAX).
240 * @details Rejects obviously bad values (null, small enum-shaped integers, non-canonical addresses) before a
241 * guarded read pays for a fault. It does NOT prove the pointer is mapped or that the target object is
242 * the expected type. Pair it with @ref module_of and a guarded @ref read for full validation.
243 * @note Callback-safe: pure `constexpr` arithmetic with no memory access, lock, or syscall.
244 */
245 1726 [[nodiscard]] inline constexpr bool is_plausible_ptr(Address address) noexcept
246 {
247 1726 const std::uintptr_t value = address.raw();
248
4/4
✓ Branch 3 → 4 taken 1718 times.
✓ Branch 3 → 6 taken 8 times.
✓ Branch 4 → 5 taken 1712 times.
✓ Branch 4 → 6 taken 6 times.
1726 return value >= USERSPACE_PTR_MIN && value < USERSPACE_PTR_MAX;
249 }
250
251 /**
252 * @brief Guarded copy of @p out.size() bytes from @p address into @p out.
253 * @param address Source address.
254 * @param out Destination byte span. An empty span is a successful no-op.
255 * @return An empty `Result` on full success; `ErrorCode::OverlappingRanges` when @p out intersects the source
256 * range (see @ref ErrorCode::OverlappingRanges; nothing is read); otherwise `ErrorCode::ReadFaulted` on
257 * any fault or rejected argument, with the faulting byte's address in `Error::detail` - a byte inside
258 * the requested source span `[address, address + out.size())`, not inside the destination @p out. It is
259 * the first unreadable byte for the small spans a typed @ref read issues; for a span wide enough that
260 * the platform's `memcpy` touches bytes out of order it can be a later byte of the same unreadable
261 * region. A span rejected before any access, and the MinGW fallback that validates through
262 * `VirtualQuery` instead of faulting, have no faulting byte to name and report @p address instead.
263 * @details The byte-level read primitive every typed @ref read forwards to. The copy runs under the engine's
264 * fault guard, so it reports a fault anywhere in the span without host termination. The pre-screen
265 * rejects only out-of-range spans: addresses below @ref USERSPACE_PTR_MIN, wrapped ends, or ends above
266 * @ref USERSPACE_PTR_MAX. It does not prove pointer validity. An in-range pointer can remain unmapped
267 * or stale, and the fault guard reports that access failure. On failure the contents of @p out are
268 * unspecified.
269 * @note Callback-safe: allocates nothing, takes no lock, and on the established hot path issues no syscall.
270 */
271 [[nodiscard]] Result<void> read_into(Address address, std::span<std::byte> out) noexcept;
272
273 /**
274 * @brief Guarded typed read of a representation-safe @p T at @p address.
275 * @tparam T A trivially copyable type in the representation-safe domain
276 * (@ref detail::is_representation_safe_v, which enumerates what participates and what does not). It
277 * need not be default constructible: the bytes are read into untyped storage and reinterpreted with
278 * `std::bit_cast`, so no @p T object is constructed on the failure path. A type outside the domain is
279 * a compile error, not a runtime risk; decode `bool` through @ref read_bool and anything else through
280 * raw bytes with @ref read_into.
281 * @param address Source address.
282 * @return The value on success, or the propagated @ref read_into error on a read fault. A top-level bounded
283 * built-in array is returned as the equivalent nested `std::array`, because C++ functions cannot return
284 * a built-in array by value.
285 * @details Forwards to @ref read_into so the `__try` frame stays in the engine TU. On success, the read
286 * collapses to a single guarded copy of `sizeof(T)` bytes followed by a no-op bit_cast.
287 * @note Callback-safe (see @ref read_into).
288 */
289 template <class T>
290 requires(std::is_trivially_copyable_v<T> && detail::is_representation_safe_v<T>)
291 1520 [[nodiscard]] Result<detail::representation_read_value_t<T>> read(Address address) noexcept
292 {
293 1520 std::array<std::byte, sizeof(T)> storage{};
294
14/24
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 739 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 736 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<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 16 times.
1520 if (auto outcome = read_into(address, storage); !outcome)
295 {
296 20 return std::unexpected(outcome.error());
297 }
298 1500 return detail::decode_foreign_representation<T>(storage);
299 }
300
301 /**
302 * @brief Guarded checked decode of a single foreign byte into a `bool`.
303 * @param address Source address of the byte.
304 * @return `false`/`true` for a byte of `0`/`1`; `ErrorCode::ReadFaulted` (faulting address in `Error::detail`)
305 * on a read fault, or `ErrorCode::InvalidRepresentation` (source address in `Error::detail`) for any
306 * other byte value.
307 * @details The representation-safe route for `bool`, which the raw typed @ref read excludes: it reads one byte
308 * through the fault guard and validates it before forming the `bool`, so an arbitrary foreign byte can
309 * never be bit-cast into an invalid `bool`. Extend this checked-decoder pattern for any other
310 * representation-sensitive type a caller needs.
311 * @note Callback-safe (see @ref read_into).
312 */
313 [[nodiscard]] Result<bool> read_bool(Address address) noexcept;
314
315 /**
316 * @brief Guarded write of a byte span to @p address, changing page protection only if it must.
317 * @param address Destination address.
318 * @param source Source byte span. An empty span is a successful no-op, but the null-target check runs
319 * first: a null @p address fails with `NullTargetAddress` even for an empty span.
320 * @return An empty `Result` on success; one of `ErrorCode::NullTargetAddress`, `NullSourceBytes`,
321 * `SizeTooLarge` (over @ref MAX_WRITE_SIZE), `OverlappingRanges` (@p source intersects the target
322 * range; nothing is written), `ProtectionChangeFailed`, `WriteFaulted` (nothing was written),
323 * `WriteMayBePartial` (the changed prefix is indeterminate, as @ref ErrorCode::WriteMayBePartial
324 * defines),
325 * `InstructionFlushFailed`, or `ProtectionRestoreFailed`.
326 * @details The escalating DATA write. @ref patch_code is the route for bytes that are executed. It first
327 * attempts a guarded write that changes NO page protection, so a target that is already writable
328 * costs no `VirtualProtect` and no instruction-cache flush. Only a fault on that attempt takes the
329 * slow path: change protection to writable per region, so a data page never gains execute, copy,
330 * flush the instruction cache for an executable region, restore the original protection, and
331 * invalidate the affected cache range. A @ref ProtectGuard held over a hot region therefore keeps
332 * the writes inside it on the cheap path. The slow-path copy also runs under the fault guard. A page
333 * reprotected or unmapped mid-copy returns `WriteMayBePartial`, and the restore and flush still run,
334 * with `ProtectionRestoreFailed` taking priority. A successful already-writable fast path issues no
335 * flush. A fast path that faults after it changed a prefix of an EXECUTABLE target is flushed before
336 * the fallback runs, so a protection-setup failure cannot leave modified code unflushed.
337 * @note A slow-path write that straddles a protection seam is handled per region: each VirtualQuery region the
338 * span covers is unprotected and restored to its own prior protection, so patching across a .rdata/.text
339 * boundary never flattens the executable region to PAGE_READONLY. A span crossing an unrealistically
340 * large number of distinct protection regions fails closed with `ProtectionChangeFailed`.
341 * @note Callback-safe on the fast path; the slow (protection-changing) path is setup/control-plane work.
342 */
343 [[nodiscard]] Result<void> write_bytes(Address address, std::span<const std::byte> source) noexcept;
344
345 /**
346 * @brief Guarded write of a trivially copyable @p T to @p address.
347 * @tparam T A trivially copyable type. Its object representation is copied byte-for-byte; no @p T object is
348 * constructed at @p address.
349 * @param address Destination address.
350 * @param value Value whose object representation is written.
351 * @return The propagated @ref write_bytes result.
352 * @details Forwards to @ref write_bytes, so the same fast-path-then-unprotect policy and fault guard apply.
353 * @note Constrained against any non-owning view. A view argument is a compile error instead of a silent
354 * bit-copy of the view object. @ref detail::is_non_owning_view_v owns the rationale.
355 * @note Callback-safe on the fast path (see @ref write_bytes).
356 */
357 template <class T>
358 requires std::is_trivially_copyable_v<T> && (!detail::is_non_owning_view_v<std::remove_cvref_t<T>>)
359 8 [[nodiscard]] Result<void> write(Address address, const T &value) noexcept
360 {
361 8 const auto storage = std::bit_cast<std::array<std::byte, sizeof(T)>>(value);
362 8 return write_bytes(address, std::span<const std::byte>{storage});
363 }
364
365 /**
366 * @brief Guarded code patch: writes @p source at @p address and flushes the instruction cache for the target.
367 * @param address Destination code address.
368 * @param source Bytes to write. Empty-span and null-target rules match @ref write_bytes.
369 * @return An empty `Result` on success; `NullTargetAddress` / `NullSourceBytes` / `SizeTooLarge` /
370 * `OverlappingRanges` (@p source intersects the target range) for a rejected argument,
371 * `ProtectionChangeFailed`, `WriteFaulted` (nothing was written), `WriteMayBePartial` (the changed
372 * prefix is indeterminate, as @ref ErrorCode::WriteMayBePartial defines), `ProtectionRestoreFailed`, or
373 * `InstructionFlushFailed` (the bytes landed but the flush failed).
374 * @details Use this route whenever the target bytes are executed as code. Every path that may modify the target
375 * checks an instruction-cache flush, including already-writable code and a partial guarded prefix. A
376 * covering flush for a partial prefix uses the full requested range before protection-changing
377 * fallback setup. Read-only targets are made writable without adding execute to data pages, then
378 * written, flushed, restored, and invalidated in the protection cache. Use @ref write_bytes or
379 * @ref write_in_place for data.
380 * @warning The write is not atomic.
381 * A copy that can have changed a prefix receives a full-range flush. A later retry that writes nothing
382 * cannot downgrade `WriteMayBePartial` to `WriteFaulted`.
383 * Restoration failure outranks partial-write status, which outranks a flush-only failure.
384 * @note Callback-safe on the fast path; the protection-changing slow path is setup/control-plane work.
385 */
386 [[nodiscard]] Result<void> patch_code(Address address, std::span<const std::byte> source) noexcept;
387
388 /**
389 * @brief Strict guarded write of a byte span that NEVER changes page protection.
390 * @param address Destination address.
391 * @param source Source byte span. Empty-span and null-target rules match @ref write_bytes.
392 * @return An empty `Result` on success; `ErrorCode::NullTargetAddress` / `NullSourceBytes` / `SizeTooLarge`
393 * (over @ref MAX_WRITE_SIZE) / `OverlappingRanges` (@p source intersects the target range) for a
394 * rejected argument; `ErrorCode::WriteFaulted` when the target's first byte was not writable and
395 * nothing was written; or `ErrorCode::WriteMayBePartial` when a byte further in the span faulted after
396 * the copy reached a writable page.
397 * @warning Not atomic across a writability seam. When @p source straddles a writable page and an adjacent
398 * unwritable one, the copy faults and returns `ErrorCode::WriteMayBePartial`, whose changed prefix is
399 * indeterminate and can be empty. Size a per-frame store so it cannot straddle a protection boundary,
400 * or treat a `WriteMayBePartial` target as indeterminate; a `WriteFaulted` return, by contrast,
401 * guarantees that no byte changed.
402 * @details The counterpart to @ref write_bytes for memory the target already keeps writable. It does NOT
403 * escalate: a read-only, executable, or no-access target fails closed with `WriteFaulted` instead of
404 * an unprotect and a write. Use it to keep a per-frame store off the `VirtualProtect` path, or to
405 * make a stale pointer that lands in read-only memory surface as an error. For a one-shot code patch
406 * use @ref patch_code.
407 * @note Callback-safe: allocates nothing, takes no lock, changes no protection, and issues no syscall on the
408 * fast path.
409 */
410 [[nodiscard]] Result<void> write_in_place(Address address, std::span<const std::byte> source) noexcept;
411
412 /**
413 * @brief Strict guarded write of a trivially copyable @p T that NEVER changes page protection.
414 * @tparam T A trivially copyable type; its object representation is copied byte-for-byte.
415 * @param address Destination address.
416 * @param value Value whose object representation is written.
417 * @return The propagated @ref write_in_place result.
418 * @details Forwards to @ref write_in_place, so the same no-reprotect, fail-closed-if-not-writable contract and
419 * seam warning apply. This is the typed per-frame store.
420 * @note Constrained against any non-owning view. A mutable `std::span<std::byte>` routes to the byte-span
421 * overload above. Any other view is a compile error instead of a silent bit-copy of the view object.
422 * @ref detail::is_non_owning_view_v owns the rationale.
423 * @note Callback-safe (see @ref write_in_place).
424 */
425 template <class T>
426 requires std::is_trivially_copyable_v<T> && (!detail::is_non_owning_view_v<std::remove_cvref_t<T>>)
427 4 [[nodiscard]] Result<void> write_in_place(Address address, const T &value) noexcept
428 {
429 4 const auto storage = std::bit_cast<std::array<std::byte, sizeof(T)>>(value);
430 4 return write_in_place(address, std::span<const std::byte>{storage});
431 }
432
433 /**
434 * @struct ChainStep
435 * @brief One hop of a pointer-chain @ref walk: a byte offset plus the per-hop plausibility floor.
436 * @details A walk applies each step's @ref offset to the running address; every step except the last is then
437 * dereferenced to obtain the next link, and that link must be at or above @ref min_valid (and below
438 * @ref USERSPACE_PTR_MAX) or the walk stops. @ref min_valid is the per-hop equivalent of
439 * @ref is_plausible_ptr's floor, defaulting to the canonical user-mode minimum; raise it for a hop
440 * whose link must live above a known module base.
441 */
442 struct ChainStep
443 {
444 /// Byte offset added to the running address at this hop (may be negative).
445 std::ptrdiff_t offset;
446 /// Lowest address the dereferenced link at this hop may hold; a link below it stops the walk.
447 Address min_valid = Address{USERSPACE_PTR_MIN};
448 };
449
450 /**
451 * @brief Resolves a multi-level pointer chain under the engine's fault guard, exposing every intermediate hop.
452 * @param base Root address of the chain.
453 * @param steps One @ref ChainStep per hop. Every offset except the last is added and dereferenced to obtain the
454 * next link; the final offset is added but not dereferenced, yielding the target field address. An
455 * empty span returns @p base unchanged.
456 * @param trace Optional out-buffer. When non-empty, `trace[i]` receives the value resolved at hop `i` (the
457 * dereferenced link for an intermediate hop, the leaf address for the final hop), for as many hops
458 * as fit, and is populated for the successfully-walked prefix EVEN ON PARTIAL FAILURE so a caller
459 * can inspect how far the chain got.
460 * @return The resolved leaf address on success; on failure, `ErrorCode::NullChain` for a null @p base with a
461 * non-empty chain, or `ErrorCode::ReadFaulted` with the FAILING HOP INDEX in `Error::detail` when an
462 * intermediate dereference faults or yields a link below that hop's @ref ChainStep::min_valid, or when
463 * the final leaf's signed-offset arithmetic wraps or lands outside [@ref USERSPACE_PTR_MIN,
464 * @ref USERSPACE_PTR_MAX).
465 * @details The walk gates each hop, captures each intermediate link, and exits at the first bad hop. It does
466 * not dereference the returned leaf, but it screens the leaf into
467 * [@ref USERSPACE_PTR_MIN, @ref USERSPACE_PTR_MAX) like every intermediate link, so a wrapped or
468 * non-canonical result reports a failure instead of a plausible success. The caller reads the leaf,
469 * usually through @ref read.
470 * @note Callback-safe (see @ref read_into).
471 */
472 [[nodiscard]] Result<Address>
473 walk(Address base, std::span<const ChainStep> steps, std::span<Address> trace = {}) noexcept;
474
475 /**
476 * @brief Convenience @ref walk taking bare offsets, flooring every hop at @ref USERSPACE_PTR_MIN.
477 * @param base Root address of the chain.
478 * @param offsets Byte offsets applied left to right (see the @ref ChainStep overload for the hop semantics).
479 * Capped at 32 hops. Past the cap the call fails with @ref ErrorCode::SizeTooLarge (see the @note).
480 * @param trace Optional intermediate-capture buffer (see the @ref ChainStep overload).
481 * @return The resolved leaf address, or the same errors as the @ref ChainStep overload, plus
482 * `ErrorCode::SizeTooLarge` when @p offsets exceeds the 32-hop inline bound.
483 * @details The common chain shape carries no per-hop floor, so this overload accepts a plain `{0x18, 0x40}`
484 * offset list and applies the default plausibility floor to each dereferenced link. It is exactly the
485 * @ref ChainStep overload with every `min_valid` defaulted.
486 * @note Callback-safe (see @ref read_into): it builds the step list on a fixed 32-entry stack buffer and never
487 * allocates. A chain longer than 32 hops therefore fails closed with `ErrorCode::SizeTooLarge`. Route
488 * such a chain through the @ref ChainStep overload, whose caller owns the step storage.
489 */
490 [[nodiscard]] Result<Address>
491 walk(Address base, std::span<const std::ptrdiff_t> offsets, std::span<Address> trace = {}) noexcept;
492
493 /**
494 * @class ProtectGuard
495 * @brief Move-only RAII page-protection change: applies a @ref Prot to a @ref Region and restores it on scope
496 * exit.
497 * @details Built only through @ref make, so a guard cannot exist without a successful protection change to
498 * unwind. Hold one over a region that is patched or written repeatedly. If the applied @ref Prot
499 * includes @ref Prot::W, every @ref write_bytes inside the guarded window uses the cheap no-reprotect
500 * fast path. Destructor restoration is best-effort. To observe the restore result, call @ref restore
501 * before the guard dies.
502 * @note The guard captures each VirtualQuery region's own prior protection across the span and restores every
503 * region to its own value, so a guard laid over a .rdata/.text seam does not flatten the executable
504 * region to PAGE_READONLY on restore. A span that crosses an unrealistically large number of distinct
505 * protection regions fails closed at @ref make instead of a partially-changed span.
506 * @note Every protection-restoring path invalidates the cached span: @ref make, the destructor, and
507 * move-assignment (which restores the replaced guard's own region before adopting the source) each call
508 * @ref invalidate_range, so the protection cache never answers a later @ref is_readable /
509 * @ref is_writable from a snapshot taken before the guard changed (or restored) the protection.
510 */
511 class ProtectGuard
512 {
513 public:
514 /**
515 * @brief Changes @p region to @p protection and returns a guard that restores the prior protection.
516 * @param region The span whose protection is changed; an empty region fails closed. It may cross protection
517 * seams: each region within it is captured and restored separately (see the class notes).
518 * @param protection The protection to apply for the guard's lifetime.
519 * @return An armed guard on success; `ErrorCode::OutOfMemory` if the guard's capture state could not be
520 * allocated (no protection change is attempted, so nothing leaks);
521 * `ErrorCode::ProtectionChangeFailed` (with the OS error in `Error::extra`) if the protection could
522 * not be changed for a region, or the span crosses more distinct protection regions than the guard
523 * can track, in which case any region already changed is rolled back before returning; or
524 * `ErrorCode::ProtectionRestoreFailed` if that rollback itself failed, leaving a region in a
525 * transient protection.
526 * @details The capture state is allocated before any protection is changed, so a failed allocation cannot
527 * strand the region in the new protection with no guard to restore it. On success the changed
528 * range is dropped from the protection cache (@ref invalidate_range).
529 * @note Setup/control-plane only: the guard allocates and issues VirtualProtect syscalls.
530 */
531 [[nodiscard]] static Result<ProtectGuard> make(Region region, Prot protection) noexcept;
532
533 ProtectGuard(ProtectGuard &&other) noexcept;
534 ProtectGuard &operator=(ProtectGuard &&other) noexcept;
535 ProtectGuard(const ProtectGuard &) = delete;
536 ProtectGuard &operator=(const ProtectGuard &) = delete;
537
538 /// Restores the original page protection unless the guard was moved-from or @ref release was called.
539 ~ProtectGuard() noexcept;
540
541 /// True while the guard is armed (it will restore on destruction); false after a move or @ref release.
542 [[nodiscard]] explicit operator bool() const noexcept;
543
544 /**
545 * @brief Disarms the guard. Its destructor then leaves the changed protection in place.
546 * @details The page entry leaves the ledger once no other guard holds that page, so the next guard over
547 * it captures the current protection. While another guard still holds the page, this guard's
548 * applied protection becomes that guard's restore baseline.
549 * @note Setup/control-plane only: ledger removal takes the protection ledger lock.
550 */
551 void release() noexcept;
552
553 /**
554 * @brief Restores the original protection now, reports the result, and disarms the guard.
555 * @return An empty `Result` on success; `ErrorCode::ProtectionRestoreFailed` (OS error in `Error::extra`)
556 * when a region could not be restored. A moved-from, released, or already-restored guard returns
557 * success. There is nothing left to restore.
558 * @details The observable counterpart to the best-effort destructor. Idempotent: it disarms the guard, so
559 * the destructor then does nothing. On failure the guard is still disarmed, and the range is
560 * dropped from the protection cache exactly as the destructor does.
561 * @note Setup/control-plane only: the restore issues VirtualProtect syscalls.
562 */
563 [[nodiscard]] Result<void> restore() noexcept;
564
565 private:
566 // Private so the only way to obtain a guard is make(), which guarantees the protection change succeeded.
567 ProtectGuard() noexcept;
568
569 // The captured base/size/old-protection live in the engine TU so this header carries no Win32 type.
570 struct Impl;
571 std::unique_ptr<Impl> m_impl;
572 };
573
574 /**
575 * @brief Resolves the mapped image span of the module that owns @p address.
576 * @param address Any address inside the target module.
577 * @return The owning module's @ref Region, or an empty Region when @p address is null, falls inside no loaded
578 * module, or the module's PE headers do not validate.
579 * @details Every call reports the extent the image currently publishes, so a module replaced at the same
580 * base is never answered from the previous image's headers.
581 * @note Setup/control-plane only: the call issues a loader lookup and a guarded PE-header read.
582 * @warning The returned Region is a non-owning scope. It does not pin the module, so a module unloaded after
583 * this returns leaves a span that references freed address space.
584 */
585 [[nodiscard]] Region module_of(Address address) noexcept;
586
587 /**
588 * @brief Reports whether a module with the given base name is currently loaded in the process.
589 * @param basename The module's file name as the loader knows it (e.g. "kernel32.dll"); a bare name, not a path.
590 * @param case_insensitive When true (the default, matching Windows module-name semantics) the comparison
591 * ignores case.
592 * @return True when a loaded module's base name matches @p basename.
593 * A path longer than `MAX_PATH` does not change either answer.
594 * @note Setup/control-plane only: the query reaches the loader. An exact-case request fails closed under the
595 * loader lock, because it requires a counted module reference.
596 */
597 [[nodiscard]] bool is_module_loaded(std::string_view basename, bool case_insensitive = true) noexcept;
598
599 /**
600 * @struct MemoryStats
601 * @brief Allocation-free snapshot of protection-cache configuration and counters.
602 * @details Every field mirrors a value reported by @ref get_cache_stats. Counters are loaded with relaxed
603 * atomics and the live-entry totals are summed under the shard reader guard, so the struct is a
604 * consistent-per-field but not globally-atomic view. @ref hit_rate_percent is -1.0 when no queries
605 * have been tracked (hits + misses == 0).
606 */
607 struct MemoryStats
608 {
609 /// Configured number of cache shards.
610 std::size_t shard_count = 0;
611 /// Configured soft entry capacity per shard.
612 std::size_t max_entries_per_shard = 0;
613 /// Hard maximum entries per shard (capacity * multiplier), averaged across shards.
614 std::size_t hard_max_per_shard = 0;
615 /// Cache-entry expiry window in milliseconds.
616 unsigned int expiry_ms = 0;
617 /// Cumulative cache hits.
618 std::uint64_t hits = 0;
619 /// Cumulative cache misses.
620 std::uint64_t misses = 0;
621 /// Cumulative range invalidations.
622 std::uint64_t invalidations = 0;
623 /// Cumulative in-flight query coalesces.
624 std::uint64_t coalesced_queries = 0;
625 /// Cumulative on-demand cleanup passes.
626 std::uint64_t on_demand_cleanups = 0;
627 /// Live entry count summed across all shards at snapshot time.
628 std::size_t total_entries = 0;
629 /// hits / (hits + misses) * 100, or -1.0 when no queries have been tracked.
630 double hit_rate_percent = -1.0;
631 /**
632 * @brief Sticky count of lifecycle-invariant violations recovered without terminating.
633 * @details Includes an unexpected joinable handle before start and any contained join/detach failure.
634 * Monotonic; never reset by clear or shutdown, and expected to remain zero in normal operation.
635 */
636 std::uint64_t lifecycle_violations = 0;
637 };
638
639 /**
640 * @brief Initializes the protection-region cache used by @ref is_readable / @ref is_writable.
641 * @param cache_size Desired number of entries across the cache.
642 * @param expiry_ms Cache entry expiry time in milliseconds.
643 * @param shard_count Number of cache shards for concurrent access.
644 * @return True if the cache is ready for use.
645 * False if lifecycle state blocks a start or cache setup fails.
646 * A false return leaves the cache stopped, so readers use the uncached `VirtualQuery` route.
647 * @details A call while the cache is running returns true and keeps the running configuration, with no
648 * reconfiguration and no loader-lock check.
649 * A call after @ref shutdown_cache starts a fresh cache with the arguments of that call. A start fails
650 * if readers from a prior session do not exit before the drain deadline. It retains that session's
651 * storage and precommitted module reference.
652 * A successful start creates the cleanup thread when the platform permits it.
653 * Otherwise, the cache uses on-demand cleanup.
654 * MinGW also installs the process fault handler for guarded reads.
655 * @note Setup/control-plane only. Every cache setup failure appears in the return value.
656 */
657 [[nodiscard]] bool init_cache(
658 std::size_t cache_size = DEFAULT_CACHE_SIZE,
659 unsigned int expiry_ms = DEFAULT_CACHE_EXPIRY_MS,
660 std::size_t shard_count = DEFAULT_CACHE_SHARD_COUNT
661 );
662
663 /**
664 * @brief Clears all entries from the protection cache, leaving it initialized.
665 * @details Invalidates all cached region information; the background cleanup thread keeps running.
666 * @note Setup/control-plane only: the clear takes every shard's exclusive lock.
667 */
668 void clear_cache() noexcept;
669
670 /**
671 * @brief Shuts the cache down and joins the background cleanup thread.
672 * @details Call before module unload to terminate the cleanup thread cleanly. After shutdown, the cache cannot
673 * be reused without re-initialization. Under loader lock the thread is detached rather than joined to
674 * avoid deadlock, and on MinGW the vectored fault handler is drained and removed.
675 * Teardown closes reader admission first. A later permission query takes the uncached `VirtualQuery`
676 * route. The wait for admitted readers has a fixed deadline. The cache precommits a module reference
677 * before admission opens. On expiry it retains that reference and the cache storage. It also records
678 * one @ref diagnostics::LeakSubsystem::MemoryCache event. A later @ref init_cache or
679 * @ref shutdown_cache call can reclaim the storage after the stalled reader exits. A clean shutdown
680 * releases the cache reference.
681 * @note Setup/control-plane only.
682 */
683 void shutdown_cache() noexcept;
684
685 /**
686 * @brief Returns an allocation-free snapshot of cache statistics.
687 * @return A @ref MemoryStats snapshot.
688 */
689 [[nodiscard]] MemoryStats get_memory_stats() noexcept;
690
691 /**
692 * @brief Returns a human-readable string of cache statistics, built over @ref get_memory_stats.
693 * @return A formatted statistics string. Prefer @ref get_memory_stats for telemetry consumers.
694 */
695 [[nodiscard]] std::string get_cache_stats();
696
697 /**
698 * @brief Invalidates cache entries overlapping @p range, forcing a re-query on the next probe.
699 * @param range The span whose cached protection state is dropped. An empty range is a no-op.
700 * @details Used after external protection changes (a VirtualProtect by other code) so a later @ref is_readable
701 * does not answer from stale protection. @ref write_bytes performs this automatically on its
702 * protection-changing slow path.
703 * @note Setup/control-plane only: the invalidation mutates the cache shards.
704 */
705 void invalidate_range(Region range) noexcept;
706
707 /**
708 * @enum ReadableStatus
709 * @brief Tri-state result for the non-blocking readability check.
710 */
711 enum class ReadableStatus : std::uint8_t
712 {
713 /// The region is committed and readable.
714 Readable,
715 /// The region is not committed, not readable, or the arguments were rejected.
716 NotReadable,
717 /**
718 * @brief Reports that a wait is required before the check can produce a result.
719 * @details This value arises only while the cache runs, in these cases:
720 * - The shard lock is contended.
721 * - The cache misses.
722 * - A concurrent shutdown unpublished the shards.
723 */
724 Unknown
725 };
726
727 /**
728 * @brief Reports whether @p range is committed and readable.
729 * @param range The span to check. An empty range returns false.
730 * @return True when the entire range is readable and committed.
731 * @warning On a per-dereference hot path, do not use this function. A hit takes a shard reader lock. A miss can
732 * walk the range's regions with one VirtualQuery per region. The answer is a time-of-check/time-of-use
733 * snapshot. For hot game-owned reads, a guarded @ref read provides a checked `Result`. An optional
734 * @ref is_plausible_ptr call can pre-screen the address.
735 * @note Setup/control-plane only: see the hot-path warning above; a latency-sensitive caller uses
736 * @ref is_readable_nonblocking.
737 */
738 [[nodiscard]] bool is_readable(Region range) noexcept;
739
740 /**
741 * @brief Reports whether @p range is committed and writable.
742 * @param range The span to check. An empty range returns false.
743 * @return True when the entire range is writable and committed.
744 * @warning Carries the same hot-path cost and time-of-check/time-of-use caveat as @ref is_readable; reserve it
745 * for one-shot setup validation. To write, prefer attempting a guarded @ref write_bytes which fails
746 * closed.
747 * @note Setup/control-plane only (see @ref is_readable).
748 */
749 [[nodiscard]] bool is_writable(Region range) noexcept;
750
751 /**
752 * @brief Non-blocking readability check that returns @ref ReadableStatus::Unknown rather than stalling.
753 * @param range The span to check. An empty range returns @ref ReadableStatus::NotReadable.
754 * @return @ref ReadableStatus::Readable / NotReadable for a definite answer, or @ref ReadableStatus::Unknown
755 * when answering would require blocking (a contended shard try-lock or a cache miss, while the cache
756 * runs), so a latency-sensitive caller can fall back to a guarded @ref read instead of stalling.
757 * @details While the cache is not in its running state (before @ref init_cache, during initialization or
758 * shutdown, or after @ref shutdown_cache), there is no cache to consult. The check then falls back
759 * to a blocking range walk with one VirtualQuery per region and returns a definite answer, never
760 * Unknown.
761 * @note Callback-safe while the cache runs: a try-lock probe with no allocation. Outside the running state it
762 * takes the blocking fallback above.
763 */
764 [[nodiscard]] ReadableStatus is_readable_nonblocking(Region range) noexcept;
765
766 /**
767 * @namespace DetourModKit::memory::unchecked
768 * @brief The raw, validation-free fast path. Every entry point here FAULTS THE HOST on an unreadable byte.
769 * @details Quarantined in its own namespace so the danger is visible at the call site: nothing here guards,
770 * gates, or reports an error, because the contract is "the caller has already proven this access is
771 * safe".
772 */
773 namespace unchecked
774 {
775 /**
776 * @brief Unguarded typed read of a representation-safe @p T at @p address.
777 * @tparam T A trivially copyable type in the representation-safe domain
778 * (@ref detail::is_representation_safe_v), the same gate the guarded @ref read applies. This
779 * route has no error channel at all, so the domain is enforced purely at compile time; decode
780 * `bool` through the guarded @ref read_bool.
781 * @param address Source address. EVERY byte of `[address, address + sizeof(T))` MUST be committed and
782 * readable; this performs NO validation and a violation faults the host process.
783 * @return The value at @p address. A top-level bounded built-in array is returned as the equivalent nested
784 * `std::array`, because C++ functions cannot return a built-in array by value.
785 * @details Under `NDEBUG`, this is a single inlined copy with no SEH, `VirtualQuery`, or cache lookup. A
786 * Debug build first evaluates `assert(is_readable(...))`, which can take a shard lock or call
787 * `VirtualQuery`. Use it only for pointers that the caller proves are live for the current frame.
788 * For anything that can be stale, use the guarded @ref read.
789 * @note Callback-safe under `NDEBUG`: it does nothing but copy. A Debug build can block or call
790 * `VirtualQuery` during the assertion.
791 * @warning Under `NDEBUG`, an invalid address faults the host. A Debug build stops at the assertion.
792 */
793 template <class T>
794 requires(std::is_trivially_copyable_v<T> && detail::is_representation_safe_v<T>)
795 16 [[nodiscard]] detail::representation_read_value_t<T> read(Address address) noexcept
796 {
797 // The is_readable() probe must not survive into Release. assert() discards it under NDEBUG.
798
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(
799 is_readable(Region{address, sizeof(T)}) &&
800 "unchecked::read<T>: address is not fully readable; the caller's safety precondition is violated"
801 );
802 16 std::array<std::byte, sizeof(T)> storage{};
803 16 std::memcpy(storage.data(), address.as<const void *>(), sizeof(T));
804 16 return detail::decode_foreign_representation<T>(storage);
805 }
806 } // namespace unchecked
807 } // namespace memory
808 } // namespace DetourModKit
809
810 #endif // DETOURMODKIT_MEMORY_HPP
811