src/internal/hook_fault_boundary.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "internal/hook_fault_boundary.hpp" | ||
| 2 | |||
| 3 | #include "internal/memory_guarded.hpp" | ||
| 4 | #include "internal/scan_pages.hpp" | ||
| 5 | |||
| 6 | #include <windows.h> | ||
| 7 | |||
| 8 | #include <array> | ||
| 9 | #include <cstdint> | ||
| 10 | #include <optional> | ||
| 11 | #include <string_view> | ||
| 12 | |||
| 13 | namespace DetourModKit | ||
| 14 | { | ||
| 15 | namespace | ||
| 16 | { | ||
| 17 | /// A function's half-open extent as declared by its unwind metadata. | ||
| 18 | struct FunctionBound | ||
| 19 | { | ||
| 20 | std::uintptr_t lo{0}; | ||
| 21 | std::uintptr_t hi{0}; | ||
| 22 | }; | ||
| 23 | |||
| 24 | // Resolves the unwind-declared extent of the function containing target, or nullopt when the target declares | ||
| 25 | // none. Absence is the normal case for a leaf function or JIT-emitted code and is never treated as a refusal. | ||
| 26 | // | ||
| 27 | // The RUNTIME_FUNCTION the loader hands back points into the image's exception directory, which is ordinary | ||
| 28 | // process memory that a concurrent unload can withdraw; it is copied under the fault guard rather than | ||
| 29 | // dereferenced. A record whose extent is inverted or empty is discarded as untrustworthy. | ||
| 30 | // | ||
| 31 | // Only the directly registered entry is honoured. A chained entry names the primary fragment of a | ||
| 32 | // hot/cold-split function, which is a different, non-contiguous span that does not contain target and is | ||
| 33 | // therefore not a containment bound for it. | ||
| 34 | 1078 | std::optional<FunctionBound> unwind_bound(std::uintptr_t target) noexcept | |
| 35 | { | ||
| 36 | 1078 | DWORD64 image_base = 0; | |
| 37 | 1078 | const PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(target, &image_base, nullptr); | |
| 38 |
3/4✓ Branch 3 → 4 taken 938 times.
✓ Branch 3 → 5 taken 140 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 938 times.
|
1078 | if (entry == nullptr || image_base == 0) |
| 39 | { | ||
| 40 | 140 | return std::nullopt; | |
| 41 | } | ||
| 42 | |||
| 43 | 938 | RUNTIME_FUNCTION record{}; | |
| 44 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 938 times.
|
938 | if (!detail::guarded_read_bytes(reinterpret_cast<std::uintptr_t>(entry), &record, sizeof(record))) |
| 45 | { | ||
| 46 | ✗ | return std::nullopt; | |
| 47 | } | ||
| 48 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 938 times.
|
938 | if (record.EndAddress <= record.BeginAddress) |
| 49 | { | ||
| 50 | ✗ | return std::nullopt; | |
| 51 | } | ||
| 52 | 938 | const auto base = static_cast<std::uintptr_t>(image_base); | |
| 53 |
2/4✓ Branch 11 → 12 taken 938 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 938 times.
|
938 | if (record.BeginAddress > UINTPTR_MAX - base || record.EndAddress > UINTPTR_MAX - base) |
| 54 | { | ||
| 55 | ✗ | return std::nullopt; | |
| 56 | } | ||
| 57 | 938 | const FunctionBound bound{base + record.BeginAddress, base + record.EndAddress}; | |
| 58 |
2/4✓ Branch 14 → 15 taken 938 times.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 938 times.
|
938 | if (target < bound.lo || target >= bound.hi) |
| 59 | { | ||
| 60 | ✗ | return std::nullopt; | |
| 61 | } | ||
| 62 | 938 | return bound; | |
| 63 | } | ||
| 64 | } // namespace | ||
| 65 | |||
| 66 | 1089 | detail::TargetWindowResult detail::validate_backend_steal_window(std::uintptr_t target) noexcept | |
| 67 | { | ||
| 68 | // Executable and committed across the whole window. Stricter than testing the first byte: the backend decodes | ||
| 69 | // forward without a region bound, so a prologue whose tail runs off the end of its executable region would let | ||
| 70 | // the decoder consume an adjacent data page (or unmapped space) as instruction bytes. | ||
| 71 |
2/2✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 1078 times.
|
1089 | if (!is_executable_range(target, BACKEND_MAX_STEAL_WINDOW)) |
| 72 | { | ||
| 73 | 11 | return TargetWindowResult{TargetWindowVerdict::NotExecutable, target}; | |
| 74 | } | ||
| 75 | |||
| 76 | // Executable-and-committed is not readable. VirtualQuery reports the protection the OS recorded; it cannot | ||
| 77 | // report that a guard page will trap the first touch, or that a section's backing store will fail to fault in. | ||
| 78 | // Touching the bytes under the guard is the only way to learn that, and it consults no protection cache, so it | ||
| 79 | // cannot answer from a stale snapshot. | ||
| 80 | 1078 | std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> window{}; | |
| 81 | 1078 | volatile std::uintptr_t fault_address = target; | |
| 82 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1078 times.
|
2156 | if (!guarded_read_bytes(target, window.data(), window.size(), &fault_address)) |
| 83 | { | ||
| 84 | ✗ | return TargetWindowResult{TargetWindowVerdict::Unreadable, fault_address}; | |
| 85 | } | ||
| 86 | |||
| 87 | // Which patch form runs is decided after this check. The fallback can overwrite more bytes than the near jump, | ||
| 88 | // so an unwind-bounded target must accommodate the fallback minimum even when the near form might be selected. | ||
| 89 | 1078 | const std::optional<FunctionBound> bound = unwind_bound(target); | |
| 90 |
6/6✓ Branch 14 → 15 taken 938 times.
✓ Branch 14 → 18 taken 140 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 936 times.
✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 22 taken 1076 times.
|
1078 | if (bound && target + BACKEND_FALLBACK_MIN_PATCH > bound->hi) |
| 91 | { | ||
| 92 | 2 | return TargetWindowResult{TargetWindowVerdict::BoundOverrun, bound->hi}; | |
| 93 | } | ||
| 94 | |||
| 95 | 1076 | return TargetWindowResult{TargetWindowVerdict::Ok, target}; | |
| 96 | } | ||
| 97 | |||
| 98 | 12 | std::string_view detail::target_window_description(TargetWindowVerdict verdict) noexcept | |
| 99 | { | ||
| 100 |
2/5✓ Branch 2 → 3 taken 11 times.
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 2 → 6 not taken.
✗ Branch 2 → 7 not taken.
|
12 | switch (verdict) |
| 101 | { | ||
| 102 | 11 | case TargetWindowVerdict::NotExecutable: | |
| 103 | 11 | return "the bytes the backend decodes are not all executable committed memory"; | |
| 104 | ✗ | case TargetWindowVerdict::Unreadable: | |
| 105 | ✗ | return "the bytes the backend decodes are not all readable"; | |
| 106 | 1 | case TargetWindowVerdict::BoundOverrun: | |
| 107 | 1 | return "the target function is shorter than the largest patch minimum the backend may select"; | |
| 108 | ✗ | case TargetWindowVerdict::Ok: | |
| 109 | ✗ | return "the target is hookable"; | |
| 110 | } | ||
| 111 | ✗ | return "the target is hookable"; | |
| 112 | } | ||
| 113 | |||
| 114 | 289 | detail::ObjectWordResult detail::validate_vmt_object_word(std::uintptr_t object) noexcept | |
| 115 | { | ||
| 116 | // Readability first, and by touching rather than querying: VirtualQuery cannot report that a guard page will | ||
| 117 | // trap the first access. | ||
| 118 | 289 | volatile std::uintptr_t fault_address = object; | |
| 119 | 289 | std::uintptr_t vptr = 0; | |
| 120 |
2/2✓ Branch 3 → 4 taken 24 times.
✓ Branch 3 → 5 taken 265 times.
|
289 | if (!guarded_read_bytes(object, &vptr, sizeof(vptr), &fault_address)) |
| 121 | { | ||
| 122 | 24 | return ObjectWordResult{ObjectWordVerdict::Unreadable, fault_address, 0}; | |
| 123 | } | ||
| 124 | |||
| 125 | // Readable does not imply writable. There is no non-mutating write probe, so ask the OS and let the later | ||
| 126 | // fault-contained publication compare-exchange close a displacement/protection/unmap race. | ||
| 127 | 265 | MEMORY_BASIC_INFORMATION info{}; | |
| 128 |
1/2✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 265 times.
|
265 | if (VirtualQuery(reinterpret_cast<LPCVOID>(object), &info, sizeof(info)) != sizeof(info)) |
| 129 | { | ||
| 130 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 131 | } | ||
| 132 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 265 times.
|
265 | if (info.State != MEM_COMMIT) |
| 133 | { | ||
| 134 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 135 | } | ||
| 136 | // A guard armed before the read already failed it closed above. This catches one armed since: PAGE_GUARD traps | ||
| 137 | // the first access to a page whose protection otherwise reads as writable, so the word is not publishable even | ||
| 138 | // though its protection bits say it is. | ||
| 139 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 265 times.
|
265 | if ((info.Protect & PAGE_GUARD) != 0) |
| 140 | { | ||
| 141 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 142 | } | ||
| 143 | 265 | constexpr DWORD WRITABLE_PROTECTIONS = | |
| 144 | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; | ||
| 145 |
2/2✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 14 taken 253 times.
|
265 | if ((info.Protect & WRITABLE_PROTECTIONS) == 0) |
| 146 | { | ||
| 147 | 12 | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 148 | } | ||
| 149 | |||
| 150 | // The word must not straddle two regions with different protections: VirtualQuery reports the region containing | ||
| 151 | // the first byte, so a pointer-sized word ending in a read-only neighbour would pass on its first byte alone. | ||
| 152 | // VirtualQuery places @p object inside the region it reports, so measuring the remainder as an offset from the | ||
| 153 | // region base keeps this arithmetic wrap-free without trusting the reported bounds not to overflow. | ||
| 154 | 253 | const std::uintptr_t offset_in_region = object - reinterpret_cast<std::uintptr_t>(info.BaseAddress); | |
| 155 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 253 times.
|
253 | if (static_cast<std::uintptr_t>(info.RegionSize) - offset_in_region < sizeof(std::uintptr_t)) |
| 156 | { | ||
| 157 | ✗ | return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr}; | |
| 158 | } | ||
| 159 | |||
| 160 | 253 | return ObjectWordResult{ObjectWordVerdict::Ok, object, vptr}; | |
| 161 | } | ||
| 162 | } // namespace DetourModKit | ||
| 163 |