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