./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
52 / 53
98.1%
Branches:
34 / 38
89.5%
Functions:
10 / 10
100.0%

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
2/4
✓ Branch 2 → 3 taken 14227 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 14227 times.
14227 if (address == 0 || size == 0)
64 {
65 ✗ return;
66 }
67
2/2
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 14226 times.
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
2/2
✓ Branch 12 → 3 taken 2908 times.
✓ Branch 12 → 13 taken 2303 times.
5211 for (std::size_t i = 0; i < m_count; ++i)
95 {
96
6/6
✓ Branch 4 → 5 taken 2348 times.
✓ Branch 4 → 8 taken 561 times.
✓ Branch 6 → 7 taken 22 times.
✓ Branch 6 → 8 taken 2324 times.
✓ Branch 9 → 10 taken 22 times.
✓ Branch 9 → 11 taken 2885 times.
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
4/4
✓ Branch 2 → 3 taken 13868 times.
✓ Branch 2 → 4 taken 358 times.
✓ Branch 3 → 4 taken 880 times.
✓ Branch 3 → 5 taken 12988 times.
14226 if (hi <= m_window_lo || lo >= m_window_hi)
117 {
118 1238 return;
119 }
120
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 12988 times.
12988 lo = (lo < m_window_lo) ? m_window_lo : lo;
121
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 12988 times.
12988 hi = (hi > m_window_hi) ? m_window_hi : hi;
122
123 12988 std::size_t i = 0;
124
2/2
✓ Branch 34 → 12 taken 8746 times.
✓ Branch 34 → 35 taken 12988 times.
21734 while (i < m_count)
125 {
126
6/6
✓ Branch 13 → 14 taken 6310 times.
✓ Branch 13 → 17 taken 2436 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 15 → 17 taken 6307 times.
✓ Branch 18 → 19 taken 3 times.
✓ Branch 18 → 32 taken 8743 times.
8746 if (lo <= m_spans[i].hi && hi >= m_spans[i].lo)
127 {
128
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;
129
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;
130 3 --m_count;
131 3 m_spans[i] = m_spans[m_count];
132 3 continue;
133 }
134 8743 ++i;
135 }
136
2/2
✓ Branch 35 → 36 taken 5 times.
✓ Branch 35 → 37 taken 12983 times.
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
2/2
✓ Branch 17 → 4 taken 71 times.
✓ Branch 17 → 18 taken 455 times.
981 for (const Region &region : 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