GCC Code Coverage Report


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

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 &region : 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