src/memory_access.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /** | ||
| 2 | * @file memory_access.cpp | ||
| 3 | * @brief Public faces of the guarded access surface: read_into, write_bytes, and the pointer-chain walk. | ||
| 4 | * | ||
| 5 | * These translation units hold no Structured Exception Handling and touch no Win32 directly. They validate arguments in | ||
| 6 | * the v4 value vocabulary (Address / Region / Result / Error) and call the fault-guard engine in memory_guarded.cpp. | ||
| 7 | * They map the engine's plain bool / status results onto ErrorCode. The header-side read<T> / write<T> templates | ||
| 8 | * forward into read_into / write_bytes defined here, so the only typed-read machinery in the installed header is a | ||
| 9 | * bit_cast. | ||
| 10 | */ | ||
| 11 | |||
| 12 | #include "DetourModKit/memory.hpp" | ||
| 13 | |||
| 14 | #include "internal/memory_guarded.hpp" | ||
| 15 | |||
| 16 | #include <array> | ||
| 17 | #include <cstddef> | ||
| 18 | #include <limits> | ||
| 19 | #include <span> | ||
| 20 | |||
| 21 | namespace DetourModKit | ||
| 22 | { | ||
| 23 | namespace memory | ||
| 24 | { | ||
| 25 | namespace | ||
| 26 | { | ||
| 27 | /** | ||
| 28 | * @brief Reports whether the half-open ranges [a, a+a_size) and [b, b+b_size) intersect. | ||
| 29 | * @details Wrap-safe by construction. It subtracts the smaller base from the larger and compares against | ||
| 30 | * the lower range's size, so no sum can overflow. The engine separately rejects a range whose | ||
| 31 | * own end crosses the address-space boundary. | ||
| 32 | */ | ||
| 33 | [[nodiscard]] constexpr bool | ||
| 34 | 1645 | ranges_overlap(std::uintptr_t a, std::size_t a_size, std::uintptr_t b, std::size_t b_size) noexcept | |
| 35 | { | ||
| 36 | 1645 | if (a_size == 0 || b_size == 0) | |
| 37 | { | ||
| 38 | ✗ | return false; | |
| 39 | } | ||
| 40 | 1645 | return a <= b ? (b - a) < a_size : (a - b) < b_size; | |
| 41 | } | ||
| 42 | |||
| 43 | constexpr std::uintptr_t ADDRESS_MAX = std::numeric_limits<std::uintptr_t>::max(); | ||
| 44 | static_assert(ranges_overlap(ADDRESS_MAX - 7, 8, ADDRESS_MAX - 3, 1)); | ||
| 45 | static_assert(ranges_overlap(ADDRESS_MAX, 1, ADDRESS_MAX, 1)); | ||
| 46 | static_assert(!ranges_overlap(ADDRESS_MAX - 7, 0, ADDRESS_MAX - 3, 1)); | ||
| 47 | |||
| 48 | /// Refuses a caller span that intersects the target range, in either direction (see T-OVERLAP). | ||
| 49 | [[nodiscard]] bool | ||
| 50 | 1645 | span_overlaps_target(Address address, const void *span_data, std::size_t span_size) noexcept | |
| 51 | { | ||
| 52 | 1645 | return ranges_overlap(address.raw(), span_size, reinterpret_cast<std::uintptr_t>(span_data), span_size); | |
| 53 | } | ||
| 54 | |||
| 55 | // Maps a protection-changing patch outcome onto the public Result, invalidating the cached protection for | ||
| 56 | // the touched range on every exit (every slow-path exit changed protection, and a failed change still | ||
| 57 | // rolled back regions it had already flipped, so a snapshot a concurrent reader cached from the transient | ||
| 58 | // protection must not survive). Shared by write_bytes and patch_code. | ||
| 59 | 31 | [[nodiscard]] Result<void> finish_patch( | |
| 60 | detail::PatchStatus status, | ||
| 61 | std::uint32_t os_error, | ||
| 62 | const char *where, | ||
| 63 | Address address, | ||
| 64 | std::size_t size, | ||
| 65 | detail::GuardedWriteStatus fast_status | ||
| 66 | ) noexcept | ||
| 67 | { | ||
| 68 | 31 | invalidate_range(Region{address, size}); | |
| 69 | 31 | switch (status) | |
| 70 | { | ||
| 71 | 14 | case detail::PatchStatus::Ok: | |
| 72 | 14 | return {}; | |
| 73 | ✗ | case detail::PatchStatus::WriteMayBePartial: | |
| 74 | ✗ | return std::unexpected(Error{ErrorCode::WriteMayBePartial, where, address.raw(), 0}); | |
| 75 | 2 | case detail::PatchStatus::WriteFaulted: | |
| 76 | 2 | if (fast_status == detail::GuardedWriteStatus::MayBePartial) | |
| 77 | { | ||
| 78 | 1 | return std::unexpected(Error{ErrorCode::WriteMayBePartial, where, address.raw(), 0}); | |
| 79 | } | ||
| 80 | 1 | return std::unexpected(Error{ErrorCode::WriteFaulted, where, address.raw(), 0}); | |
| 81 | 2 | case detail::PatchStatus::InstructionFlushFailed: | |
| 82 | 2 | return std::unexpected(Error{ErrorCode::InstructionFlushFailed, where, address.raw(), 0}); | |
| 83 | 1 | case detail::PatchStatus::ProtectionRestoreFailed: | |
| 84 | 1 | return std::unexpected(Error{ErrorCode::ProtectionRestoreFailed, where, address.raw(), os_error}); | |
| 85 | 12 | case detail::PatchStatus::ProtectionChangeFailed: | |
| 86 | default: | ||
| 87 | // The escalation could not make the whole span writable (e.g. an unmapped tail). If the | ||
| 88 | // no-reprotect fast path already modified a writable-head prefix, the target is partially written; | ||
| 89 | // otherwise nothing was written and the protection change simply failed. | ||
| 90 | 12 | if (fast_status == detail::GuardedWriteStatus::MayBePartial) | |
| 91 | { | ||
| 92 | 6 | return std::unexpected(Error{ErrorCode::WriteMayBePartial, where, address.raw(), 0}); | |
| 93 | } | ||
| 94 | 6 | return std::unexpected(Error{ErrorCode::ProtectionChangeFailed, where, address.raw(), os_error}); | |
| 95 | } | ||
| 96 | } | ||
| 97 | } // namespace | ||
| 98 | |||
| 99 | 1573 | Result<void> read_into(Address address, std::span<std::byte> out) noexcept | |
| 100 | { | ||
| 101 | 1573 | if (out.empty()) | |
| 102 | { | ||
| 103 | 4 | return {}; | |
| 104 | } | ||
| 105 | 1569 | if (span_overlaps_target(address, out.data(), out.size())) | |
| 106 | { | ||
| 107 | 3 | return std::unexpected(Error{ErrorCode::OverlappingRanges, "memory::read_into", address.raw(), 0}); | |
| 108 | } | ||
| 109 | // Seed with the requested address so the argument-rejection paths (below USERSPACE_PTR_MIN, wrapping or | ||
| 110 | // over-ceiling span, VirtualQuery fallback) still name an address: those refuse before any access, so there | ||
| 111 | // is no faulting address to report and the guard leaves the slot untouched. | ||
| 112 | 1566 | volatile std::uintptr_t fault_address = address.raw(); | |
| 113 | 1566 | if (!detail::guarded_read_bytes(address.raw(), out.data(), out.size(), &fault_address)) | |
| 114 | { | ||
| 115 | 38 | return std::unexpected(Error{ErrorCode::ReadFaulted, "memory::read_into", fault_address, 0}); | |
| 116 | } | ||
| 117 | 1528 | return {}; | |
| 118 | } | ||
| 119 | |||
| 120 | 5 | Result<bool> read_bool(Address address) noexcept | |
| 121 | { | ||
| 122 | 5 | std::byte raw{}; | |
| 123 | 5 | if (auto outcome = read_into(address, std::span<std::byte>{&raw, 1}); !outcome) | |
| 124 | { | ||
| 125 | 1 | return std::unexpected(outcome.error()); | |
| 126 | } | ||
| 127 | // Validate the byte BEFORE forming the bool: only 0 and 1 are valid bool object representations, so an | ||
| 128 | // arbitrary foreign byte can never be bit-cast into an invalid bool (undefined behaviour the raw read | ||
| 129 | // excludes at compile time; this checked route reports it as InvalidRepresentation instead). | ||
| 130 | 8 | switch (std::to_integer<unsigned char>(raw)) | |
| 131 | { | ||
| 132 | 1 | case 0: | |
| 133 | 1 | return false; | |
| 134 | 1 | case 1: | |
| 135 | 1 | return true; | |
| 136 | 2 | default: | |
| 137 | 2 | return std::unexpected(Error{ErrorCode::InvalidRepresentation, "memory::read_bool", address.raw(), 0}); | |
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | 48 | Result<void> write_bytes(Address address, std::span<const std::byte> source) noexcept | |
| 142 | { | ||
| 143 | // Validation order: a null target outranks a null source, and a zero-length write is a success no-op that | ||
| 144 | // never inspects the source pointer or the size cap. | ||
| 145 | 48 | if (!address) | |
| 146 | { | ||
| 147 | 4 | return std::unexpected(Error{ErrorCode::NullTargetAddress, "memory::write_bytes", address.raw(), 0}); | |
| 148 | } | ||
| 149 | 44 | if (source.data() == nullptr && !source.empty()) | |
| 150 | { | ||
| 151 | 3 | return std::unexpected(Error{ErrorCode::NullSourceBytes, "memory::write_bytes", address.raw(), 0}); | |
| 152 | } | ||
| 153 | 41 | if (source.empty()) | |
| 154 | { | ||
| 155 | 4 | return {}; | |
| 156 | } | ||
| 157 | 37 | if (source.size() > MAX_WRITE_SIZE) | |
| 158 | { | ||
| 159 | 1 | return std::unexpected(Error{ErrorCode::SizeTooLarge, "memory::write_bytes", address.raw(), 0}); | |
| 160 | } | ||
| 161 | 36 | if (span_overlaps_target(address, source.data(), source.size())) | |
| 162 | { | ||
| 163 | 3 | return std::unexpected(Error{ErrorCode::OverlappingRanges, "memory::write_bytes", address.raw(), 0}); | |
| 164 | } | ||
| 165 | |||
| 166 | // Fast path: a guarded write that changes no protection. It succeeds for an already-writable target (a | ||
| 167 | // live game field, or any page held writable by a ProtectGuard) with no VirtualProtect and no flush, so a | ||
| 168 | // per-frame writer stays off the syscall path. | ||
| 169 | const detail::GuardedWriteStatus fast_status = | ||
| 170 | 33 | detail::guarded_write_bytes(address.raw(), source.data(), source.size()); | |
| 171 | 33 | if (fast_status == detail::GuardedWriteStatus::Ok) | |
| 172 | { | ||
| 173 | 16 | return {}; | |
| 174 | } | ||
| 175 | |||
| 176 | 17 | if (fast_status == detail::GuardedWriteStatus::MayBePartial) | |
| 177 | { | ||
| 178 | // The fallback flushes the executable regions it makes writable, but a setup failure leaves no segment | ||
| 179 | // to flush, so an executable prefix this attempt may already have changed is covered here. A | ||
| 180 | // non-executable target owes nothing and stays on the flush-free data route. | ||
| 181 | 6 | detail::flush_if_executable(address.raw(), source.size()); | |
| 182 | } | ||
| 183 | |||
| 184 | // Slow path: the target was read-only or executable (or straddles into one), so the engine changes | ||
| 185 | // protection (writable derived per region from its own execute semantics), writes, flushes executable | ||
| 186 | // regions, and restores. If the span cannot be made fully writable and a prefix was already written, | ||
| 187 | // finish_patch reports WriteMayBePartial rather than a clean ProtectionChangeFailed. | ||
| 188 | 17 | std::uint32_t os_error = 0; | |
| 189 | const detail::PatchStatus status = | ||
| 190 | 17 | detail::patch_bytes(address.raw(), source.data(), source.size(), os_error); | |
| 191 | 17 | return finish_patch(status, os_error, "memory::write_bytes", address, source.size(), fast_status); | |
| 192 | } | ||
| 193 | |||
| 194 | 23 | Result<void> patch_code(Address address, std::span<const std::byte> source) noexcept | |
| 195 | { | ||
| 196 | // Same validation order as write_bytes. | ||
| 197 | 23 | if (!address) | |
| 198 | { | ||
| 199 | 1 | return std::unexpected(Error{ErrorCode::NullTargetAddress, "memory::patch_code", address.raw(), 0}); | |
| 200 | } | ||
| 201 | 22 | if (source.data() == nullptr && !source.empty()) | |
| 202 | { | ||
| 203 | ✗ | return std::unexpected(Error{ErrorCode::NullSourceBytes, "memory::patch_code", address.raw(), 0}); | |
| 204 | } | ||
| 205 | 22 | if (source.empty()) | |
| 206 | { | ||
| 207 | 1 | return {}; | |
| 208 | } | ||
| 209 | 21 | if (source.size() > MAX_WRITE_SIZE) | |
| 210 | { | ||
| 211 | ✗ | return std::unexpected(Error{ErrorCode::SizeTooLarge, "memory::patch_code", address.raw(), 0}); | |
| 212 | } | ||
| 213 | 21 | if (span_overlaps_target(address, source.data(), source.size())) | |
| 214 | { | ||
| 215 | 3 | return std::unexpected(Error{ErrorCode::OverlappingRanges, "memory::patch_code", address.raw(), 0}); | |
| 216 | } | ||
| 217 | |||
| 218 | // Fast path: the target is already writable, so the store changes no protection. Unlike write_bytes, | ||
| 219 | // patch_code then flushes the instruction cache so an already-writable code patch is visible to execution. | ||
| 220 | // No protection changed, so nothing is invalidated in the protection cache. | ||
| 221 | const detail::GuardedWriteStatus fast_status = | ||
| 222 | 18 | detail::guarded_write_bytes(address.raw(), source.data(), source.size()); | |
| 223 | 18 | if (fast_status == detail::GuardedWriteStatus::Ok) | |
| 224 | { | ||
| 225 | 4 | if (!detail::flush_instruction_cache(address.raw(), source.size())) | |
| 226 | { | ||
| 227 | 1 | return std::unexpected( | |
| 228 | 2 | Error{ErrorCode::InstructionFlushFailed, "memory::patch_code", address.raw(), 0} | |
| 229 | 1 | ); | |
| 230 | } | ||
| 231 | 3 | return {}; | |
| 232 | } | ||
| 233 | |||
| 234 | 14 | if (fast_status == detail::GuardedWriteStatus::MayBePartial) | |
| 235 | { | ||
| 236 | // The copy order is unspecified outside the deterministic test seam, so flush the full request before | ||
| 237 | // fallback setup can fail. A partial-write or restoration error outranks this best-effort flush. | ||
| 238 | 3 | (void)detail::flush_instruction_cache(address.raw(), source.size()); | |
| 239 | } | ||
| 240 | |||
| 241 | // Slow path: unprotect (execute preserved for a code region), write, flush executable regions, restore. | ||
| 242 | 14 | std::uint32_t os_error = 0; | |
| 243 | const detail::PatchStatus status = | ||
| 244 | 14 | detail::patch_bytes(address.raw(), source.data(), source.size(), os_error, true); | |
| 245 | 14 | return finish_patch(status, os_error, "memory::patch_code", address, source.size(), fast_status); | |
| 246 | } | ||
| 247 | |||
| 248 | 24 | Result<void> write_in_place(Address address, std::span<const std::byte> source) noexcept | |
| 249 | { | ||
| 250 | // Same validation order as write_bytes: a null target outranks a null source, and a zero-length write is a | ||
| 251 | // success no-op that inspects neither the source pointer nor the target's protection. | ||
| 252 | 24 | if (!address) | |
| 253 | { | ||
| 254 | 2 | return std::unexpected(Error{ErrorCode::NullTargetAddress, "memory::write_in_place", address.raw(), 0}); | |
| 255 | } | ||
| 256 | 22 | if (source.data() == nullptr && !source.empty()) | |
| 257 | { | ||
| 258 | 1 | return std::unexpected(Error{ErrorCode::NullSourceBytes, "memory::write_in_place", address.raw(), 0}); | |
| 259 | } | ||
| 260 | 21 | if (source.empty()) | |
| 261 | { | ||
| 262 | 1 | return {}; | |
| 263 | } | ||
| 264 | // Cap oversized spans for parity with write_bytes: the guarded copy already fails closed at the first | ||
| 265 | // unwritable page, so this is an API-symmetry guard that rejects an obviously-wrong length up front with | ||
| 266 | // the same ErrorCode::SizeTooLarge rather than attempting a multi-gigabyte guarded copy. | ||
| 267 | 20 | if (source.size() > MAX_WRITE_SIZE) | |
| 268 | { | ||
| 269 | 1 | return std::unexpected(Error{ErrorCode::SizeTooLarge, "memory::write_in_place", address.raw(), 0}); | |
| 270 | } | ||
| 271 | 19 | if (span_overlaps_target(address, source.data(), source.size())) | |
| 272 | { | ||
| 273 | 3 | return std::unexpected(Error{ErrorCode::OverlappingRanges, "memory::write_in_place", address.raw(), 0}); | |
| 274 | } | ||
| 275 | |||
| 276 | // The strict path: a guarded write that changes NO protection. A read-only, executable, or no-access target | ||
| 277 | // faults the guarded copy and fails closed. This entry point exists precisely to reject a write the | ||
| 278 | // caller did not intend to escalate, so it never reaches the VirtualProtect dance write_bytes takes on a | ||
| 279 | // fault. No cache invalidation either: changing nothing leaves the cached protection state valid. | ||
| 280 | const detail::GuardedWriteStatus status = | ||
| 281 | 16 | detail::guarded_write_bytes(address.raw(), source.data(), source.size()); | |
| 282 | 16 | if (status == detail::GuardedWriteStatus::Ok) | |
| 283 | { | ||
| 284 | 9 | return {}; | |
| 285 | } | ||
| 286 | |||
| 287 | 7 | if (status == detail::GuardedWriteStatus::MayBePartial) | |
| 288 | { | ||
| 289 | 2 | return std::unexpected(Error{ErrorCode::WriteMayBePartial, "memory::write_in_place", address.raw(), 0}); | |
| 290 | } | ||
| 291 | 5 | return std::unexpected(Error{ErrorCode::WriteFaulted, "memory::write_in_place", address.raw(), 0}); | |
| 292 | } | ||
| 293 | |||
| 294 | 35 | Result<Address> walk(Address base, std::span<const ChainStep> steps, std::span<Address> trace) noexcept | |
| 295 | { | ||
| 296 | // A null root cannot be dereferenced. An empty chain is the identity walk (engine returns base), so the | ||
| 297 | // null root is only an error when there is at least one hop to take. | ||
| 298 | 35 | if (!base && !steps.empty()) | |
| 299 | { | ||
| 300 | ✗ | return std::unexpected(Error{ErrorCode::NullChain, "memory::walk", 0, 0}); | |
| 301 | } | ||
| 302 | |||
| 303 | const detail::ChainWalkOutcome outcome = | ||
| 304 | 35 | detail::guarded_resolve_chain(base, steps.data(), steps.size(), trace.data(), trace.size()); | |
| 305 | 35 | if (!outcome.ok) | |
| 306 | { | ||
| 307 | // ReadFaulted carries the failing hop index in Error::detail: the hop whose dereference faulted, or | ||
| 308 | // whose dereferenced link fell below that hop's plausibility floor. | ||
| 309 | 10 | return std::unexpected(Error{ErrorCode::ReadFaulted, "memory::walk", outcome.fail_index, 0}); | |
| 310 | } | ||
| 311 | 25 | return outcome.address; | |
| 312 | } | ||
| 313 | |||
| 314 | 34 | Result<Address> walk(Address base, std::span<const std::ptrdiff_t> offsets, std::span<Address> trace) noexcept | |
| 315 | { | ||
| 316 | // The bare-offset chain applies the default plausibility floor to every hop, so it is the ChainStep walk | ||
| 317 | // with every min_valid defaulted. This overload is documented callback-safe (allocation-free), so it must | ||
| 318 | // build the ChainStep view on a fixed stack buffer and never touch the heap: a chain longer than the inline | ||
| 319 | // bound fails closed with SizeTooLarge rather than allocating a std::vector (which would contradict the | ||
| 320 | // allocation-free label and, on OOM, force a bad_alloc catch on a hot path). A caller with a genuinely | ||
| 321 | // long chain is steered to the ChainStep-taking overload above, where the caller owns the step storage. | ||
| 322 | 34 | constexpr std::size_t inline_capacity = 32; | |
| 323 | 34 | if (offsets.size() > inline_capacity) | |
| 324 | { | ||
| 325 | 1 | return std::unexpected(Error{ErrorCode::SizeTooLarge, "memory::walk", offsets.size(), inline_capacity}); | |
| 326 | } | ||
| 327 | 1056 | std::array<ChainStep, inline_capacity> steps{}; | |
| 328 | 120 | for (std::size_t i = 0; i < offsets.size(); ++i) | |
| 329 | { | ||
| 330 | 87 | steps[i] = ChainStep{offsets[i]}; | |
| 331 | } | ||
| 332 | 33 | return walk(base, std::span<const ChainStep>{steps.data(), offsets.size()}, trace); | |
| 333 | } | ||
| 334 | } // namespace memory | ||
| 335 | } // namespace DetourModKit | ||
| 336 |