src/internal/scan_exclusions.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_INTERNAL_SCAN_EXCLUSIONS_HPP | ||
| 2 | #define DETOURMODKIT_INTERNAL_SCAN_EXCLUSIONS_HPP | ||
| 3 | |||
| 4 | /** | ||
| 5 | * @file internal/scan_exclusions.hpp | ||
| 6 | * @brief The scan-time set of address spans a match may not come from: DMK's own query storage plus caller-declared | ||
| 7 | * copies of it. | ||
| 8 | * @details Never installed. A page-gated readable sweep reads every committed page of its scope, including the heap and | ||
| 9 | * stack the caller's own query material lives on, so without this set a scan can find its needle in its own | ||
| 10 | * haystack and report the query's storage as the target. The set is built per call from live objects (the | ||
| 11 | * value Pattern, the compiled EnginePattern, ladder storage, string-query buffers) and consulted once per | ||
| 12 | * candidate match, so it costs nothing per scanned byte. | ||
| 13 | */ | ||
| 14 | |||
| 15 | #include "internal/scan_engine.hpp" | ||
| 16 | |||
| 17 | #include "DetourModKit/region.hpp" | ||
| 18 | #include "DetourModKit/scan.hpp" | ||
| 19 | |||
| 20 | #include <array> | ||
| 21 | #include <cstddef> | ||
| 22 | #include <cstdint> | ||
| 23 | #include <span> | ||
| 24 | #include <string_view> | ||
| 25 | |||
| 26 | namespace DetourModKit | ||
| 27 | { | ||
| 28 | namespace detail | ||
| 29 | { | ||
| 30 | /** | ||
| 31 | * @class ScanExclusions | ||
| 32 | * @brief A bounded, self-merging set of half-open address spans a scan must not match inside. | ||
| 33 | * @details Fixed capacity and allocation-free, so it is usable from the noexcept scan paths. Adding a span that | ||
| 34 | * touches or overlaps a stored one widens that entry instead of consuming a slot, which keeps the | ||
| 35 | * common case (a pattern's bytes and mask from the same allocation run) at one or two entries. When | ||
| 36 | * capacity is genuinely exhausted the set latches @ref overflowed rather than dropping a span | ||
| 37 | * silently: a caller whose uniqueness verdict depends on complete exclusion must then fail closed. | ||
| 38 | */ | ||
| 39 | class ScanExclusions | ||
| 40 | { | ||
| 41 | public: | ||
| 42 | /// Slot count. A ladder contributes one span for its array plus one per owned string, so this is generous. | ||
| 43 | static constexpr std::size_t MAX_SPANS = 32; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * @brief Ignores every span that lies wholly outside [@p lo, @p hi) from here on. | ||
| 47 | * @details Set to the range the scan will actually read. A caller's heap or stack copy of the query cannot | ||
| 48 | * be matched by a sweep that never reads it, so storing it would only consume a slot and push a | ||
| 49 | * span that IS in range toward overflow. Applies to subsequent additions, not retroactively. | ||
| 50 | */ | ||
| 51 | 1125 | constexpr void restrict_to(std::uintptr_t lo, std::uintptr_t hi) noexcept | |
| 52 | { | ||
| 53 | 1125 | m_window_lo = lo; | |
| 54 | 1125 | m_window_hi = hi; | |
| 55 | 1125 | } | |
| 56 | |||
| 57 | /** | ||
| 58 | * @brief Adds the half-open span [@p address, @p address + @p size). | ||
| 59 | * @details A zero size or a null address is ignored. An end that would wrap latches overflow because the | ||
| 60 | * requested exclusion cannot be represented safely. Touching and overlapping spans are fully | ||
| 61 | * coalesced, including a new span that bridges multiple existing entries. | ||
| 62 | */ | ||
| 63 | 14069 | constexpr void add(std::uintptr_t address, std::size_t size) noexcept | |
| 64 | { | ||
| 65 |
2/4✓ Branch 2 → 3 taken 14069 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 14069 times.
|
14069 | if (address == 0 || size == 0) |
| 66 | { | ||
| 67 | ✗ | return; | |
| 68 | } | ||
| 69 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 14068 times.
|
14069 | if (size > UINTPTR_MAX - address) |
| 70 | { | ||
| 71 | 1 | m_overflow = true; | |
| 72 | 1 | return; | |
| 73 | } | ||
| 74 | 14068 | insert(address, address + size); | |
| 75 | } | ||
| 76 | |||
| 77 | /// Adds the storage an object span occupies. | ||
| 78 | 392 | template <typename T> void add_object_span(std::span<const T> objects) noexcept | |
| 79 | { | ||
| 80 | 392 | add(reinterpret_cast<std::uintptr_t>(objects.data()), objects.size_bytes()); | |
| 81 | 393 | } | |
| 82 | |||
| 83 | /// Adds the storage a borrowed text view occupies. | ||
| 84 | 733 | void add_text(std::string_view text) noexcept | |
| 85 | { | ||
| 86 | 733 | add(reinterpret_cast<std::uintptr_t>(text.data()), text.size()); | |
| 87 | 733 | } | |
| 88 | |||
| 89 | /** | ||
| 90 | * @brief True when [@p lo, @p hi) intersects any stored span. | ||
| 91 | * @details The scan calls this once per candidate match with the match's true start and end, so a | ||
| 92 | * variable-length bounded-jump match is tested against the bytes it actually occupies. | ||
| 93 | */ | ||
| 94 | 2308 | [[nodiscard]] constexpr bool overlaps(std::uintptr_t lo, std::uintptr_t hi) const noexcept | |
| 95 | { | ||
| 96 |
2/2✓ Branch 12 → 3 taken 2917 times.
✓ Branch 12 → 13 taken 2288 times.
|
5205 | for (std::size_t i = 0; i < m_count; ++i) |
| 97 | { | ||
| 98 |
6/6✓ Branch 4 → 5 taken 1113 times.
✓ Branch 4 → 8 taken 1804 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 8 taken 1093 times.
✓ Branch 9 → 10 taken 20 times.
✓ Branch 9 → 11 taken 2897 times.
|
2917 | if (lo < m_spans[i].hi && hi > m_spans[i].lo) |
| 99 | { | ||
| 100 | 20 | return true; | |
| 101 | } | ||
| 102 | } | ||
| 103 | 2288 | return false; | |
| 104 | } | ||
| 105 | |||
| 106 | /// True when a span could not be stored, so exclusion is incomplete and a uniqueness verdict is unsafe. | ||
| 107 | 1140 | [[nodiscard]] constexpr bool overflowed() const noexcept { return m_overflow; } | |
| 108 | |||
| 109 | private: | ||
| 110 | struct Span | ||
| 111 | { | ||
| 112 | std::uintptr_t lo = 0; | ||
| 113 | std::uintptr_t hi = 0; | ||
| 114 | }; | ||
| 115 | |||
| 116 | 14067 | constexpr void insert(std::uintptr_t lo, std::uintptr_t hi) noexcept | |
| 117 | { | ||
| 118 |
4/4✓ Branch 2 → 3 taken 13747 times.
✓ Branch 2 → 4 taken 320 times.
✓ Branch 3 → 4 taken 809 times.
✓ Branch 3 → 5 taken 12938 times.
|
14067 | if (hi <= m_window_lo || lo >= m_window_hi) |
| 119 | { | ||
| 120 | 1129 | return; | |
| 121 | } | ||
| 122 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12938 times.
|
12938 | lo = (lo < m_window_lo) ? m_window_lo : lo; |
| 123 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12938 times.
|
12938 | hi = (hi > m_window_hi) ? m_window_hi : hi; |
| 124 | |||
| 125 | 12938 | std::size_t i = 0; | |
| 126 |
2/2✓ Branch 34 → 12 taken 8702 times.
✓ Branch 34 → 35 taken 12936 times.
|
21638 | while (i < m_count) |
| 127 | { | ||
| 128 |
6/6✓ Branch 13 → 14 taken 1343 times.
✓ Branch 13 → 17 taken 7358 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 17 taken 1339 times.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 32 taken 8697 times.
|
8702 | if (lo <= m_spans[i].hi && hi >= m_spans[i].lo) |
| 129 | { | ||
| 130 |
2/2✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 23 taken 1 time.
|
3 | lo = (lo < m_spans[i].lo) ? lo : m_spans[i].lo; |
| 131 |
2/2✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 28 taken 2 times.
|
3 | hi = (hi > m_spans[i].hi) ? hi : m_spans[i].hi; |
| 132 | 3 | --m_count; | |
| 133 | 3 | m_spans[i] = m_spans[m_count]; | |
| 134 | 3 | continue; | |
| 135 | } | ||
| 136 | 8697 | ++i; | |
| 137 | } | ||
| 138 |
2/2✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 37 taken 12931 times.
|
12936 | if (m_count == MAX_SPANS) |
| 139 | { | ||
| 140 | 5 | m_overflow = true; | |
| 141 | 5 | return; | |
| 142 | } | ||
| 143 | 12931 | m_spans[m_count] = Span{lo, hi}; | |
| 144 | 12931 | ++m_count; | |
| 145 | } | ||
| 146 | |||
| 147 | std::array<Span, MAX_SPANS> m_spans{}; | ||
| 148 | std::size_t m_count = 0; | ||
| 149 | std::uintptr_t m_window_lo = 0; | ||
| 150 | std::uintptr_t m_window_hi = UINTPTR_MAX; | ||
| 151 | bool m_overflow = false; | ||
| 152 | }; | ||
| 153 | |||
| 154 | /** | ||
| 155 | * @brief Adds the value Pattern's own storage. | ||
| 156 | * @details The Pattern object holds the query bytes inline, so its storage IS query material wherever it lives, | ||
| 157 | * including a Pattern the compiler placed in read-only data. This covers only the live object: a | ||
| 158 | * separate initializer image the compiler materialized for a `consteval` construction sits at another | ||
| 159 | * address, is not enumerable, and stays visible to the sweep. | ||
| 160 | */ | ||
| 161 | 13 | inline void add_pattern_storage(ScanExclusions &exclusions, const scan::Pattern &pattern) noexcept | |
| 162 | { | ||
| 163 | 13 | exclusions.add(reinterpret_cast<std::uintptr_t>(&pattern), sizeof(scan::Pattern)); | |
| 164 | 13 | } | |
| 165 | |||
| 166 | /** | ||
| 167 | * @brief Adds caller-declared copies of the query. | ||
| 168 | * @details DMK can enumerate only the query representations it owns. A caller that keeps its own copy of the | ||
| 169 | * pattern bytes alive during the scan declares them here; that declaration is what lets an otherwise | ||
| 170 | * unprovable readable sweep return an authoritative result. | ||
| 171 | */ | ||
| 172 | 407 | inline void add_regions(ScanExclusions &exclusions, std::span<const Region> regions) noexcept | |
| 173 | { | ||
| 174 |
2/2✓ Branch 17 → 4 taken 71 times.
✓ Branch 17 → 18 taken 407 times.
|
885 | for (const Region ®ion : regions) |
| 175 | { | ||
| 176 | 71 | exclusions.add(region.base.raw(), region.size); | |
| 177 | } | ||
| 178 | 407 | } | |
| 179 | |||
| 180 | /// Adds a compiled pattern's heap-backed bytes and mask buffers. | ||
| 181 | 6207 | inline void add_engine_pattern_storage(ScanExclusions &exclusions, const EnginePattern &pattern) noexcept | |
| 182 | { | ||
| 183 | 6207 | exclusions.add(reinterpret_cast<std::uintptr_t>(pattern.bytes.data()), pattern.bytes.size()); | |
| 184 | 6206 | exclusions.add(reinterpret_cast<std::uintptr_t>(pattern.mask.data()), pattern.mask.size()); | |
| 185 | 6206 | } | |
| 186 | } // namespace detail | ||
| 187 | } // namespace DetourModKit | ||
| 188 | |||
| 189 | #endif // DETOURMODKIT_INTERNAL_SCAN_EXCLUSIONS_HPP | ||
| 190 |