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