GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 98.2% 109 / 0 / 111
Functions: 100.0% 13 / 0 / 13
Branches: 95.2% 80 / 0 / 84

src/internal/scan_shared.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP
2 #define DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP
3
4 /**
5 * @file internal/scan_shared.hpp
6 * @brief Small shared helpers for scan-module TUs: anchor selection, resolution arithmetic, and strict UTF-8 decoding.
7 * @details Never installed. The scan_matching, scan_resolution, and scan_prologue_recovery TUs all turn a value Pattern
8 * into an EnginePattern with a haystack-chosen anchor and screen byte-tier resolutions through the same
9 * plausible-userspace floor, so those helpers live here in one place rather than being duplicated. The anchor
10 * override is correctness-neutral: it only changes which single byte the memchr prefilter sweeps for; the full
11 * masked compare still decides every accepted position.
12 */
13
14 #include "internal/memory_guarded.hpp"
15 #include "internal/scan_engine.hpp"
16 #include "internal/scan_exclusions.hpp"
17
18 #include "DetourModKit/region.hpp"
19 #include "DetourModKit/scan.hpp"
20
21 #include <array>
22 #include <cstddef>
23 #include <cstdint>
24 #include <optional>
25 #include <span>
26 #include <string_view>
27
28 namespace DetourModKit
29 {
30 namespace detail
31 {
32 // Bounded haystack sampling budget. A byte-frequency ranking needs only a representative sample, not the whole
33 // image: 64 page reads (256 KiB) strided across the scope are ample to rank 256 byte values, and the fixed cap
34 // keeps anchor selection a constant cost regardless of scope size so it never approaches the cost of the scan
35 // it accelerates. Scopes larger than the budget ceiling (a whole-process window, terabytes of mostly-unmapped
36 // address space) are not sampled at all: the sample would be unrepresentative and would pay for dozens of
37 // guaranteed page faults, so those fall back to the compile-time anchor.
38 inline constexpr std::size_t SAMPLE_PAGE = 0x1000;
39 inline constexpr std::size_t SAMPLE_MAX_PAGES = 64;
40 inline constexpr std::size_t SAMPLE_MIN_BYTES = SAMPLE_PAGE;
41 inline constexpr std::size_t SAMPLE_MAX_SCOPE = std::size_t{512} * 1024 * 1024;
42
43 struct HaystackHistogram
44 {
45 std::array<std::uint32_t, 256> counts{};
46 std::size_t sampled = 0;
47 };
48
49 // Sample a bounded, strided set of pages from the scope and tally a 256-bin byte-frequency histogram. Reads go
50 // through guarded_read_bytes one page at a time, so an unmapped / guard page inside the scope is skipped rather
51 // than faulting the host; the stride spreads the sample across .text and .rdata / .data so the frequencies
52 // reflect the whole image, not just its first pages. Entirely stack-based (the histogram and the page buffer
53 // are locals), so it never allocates and is safe to call from the noexcept scan paths.
54 390 [[nodiscard]] inline HaystackHistogram sample_haystack(Region scope) noexcept
55 {
56 390 HaystackHistogram histogram;
57 390 const std::uintptr_t base = scope.base.raw();
58
4/6
✓ Branch 3 → 4 taken 390 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 390 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 387 times.
390 if (base == 0 || scope.size == 0 || scope.size > SAMPLE_MAX_SCOPE)
59 {
60 3 return histogram;
61 }
62 387 const std::size_t total_pages = (scope.size + SAMPLE_PAGE - 1) / SAMPLE_PAGE;
63
2/2
✓ Branch 7 → 8 taken 29 times.
✓ Branch 7 → 9 taken 358 times.
387 const std::size_t stride = (total_pages > SAMPLE_MAX_PAGES) ? (total_pages / SAMPLE_MAX_PAGES) : 1;
64 387 std::array<std::uint8_t, SAMPLE_PAGE> buffer{};
65 387 std::size_t pages_read = 0;
66
4/4
✓ Branch 23 → 24 taken 3532 times.
✓ Branch 23 → 25 taken 359 times.
✓ Branch 24 → 11 taken 3503 times.
✓ Branch 24 → 25 taken 29 times.
3891 for (std::size_t page = 0; page < total_pages && pages_read < SAMPLE_MAX_PAGES; page += stride)
67 {
68 3503 const std::uintptr_t page_address = base + static_cast<std::uintptr_t>(page) * SAMPLE_PAGE;
69 3503 const std::size_t remaining = scope.size - page * SAMPLE_PAGE;
70 3503 const std::size_t want = (remaining < SAMPLE_PAGE) ? remaining : SAMPLE_PAGE;
71
2/2
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 3499 times.
3503 if (!guarded_read_bytes(page_address, buffer.data(), want))
72 {
73 // A page that faults mid-read is skipped; the sample stays a valid (smaller) lower bound.
74 4 continue;
75 }
76
2/2
✓ Branch 20 → 17 taken 12338859 times.
✓ Branch 20 → 21 taken 3500 times.
12342359 for (std::size_t i = 0; i < want; ++i)
77 {
78 12338859 ++histogram.counts[buffer[i]];
79 }
80 3500 histogram.sampled += want;
81 3500 ++pages_read;
82 }
83 388 return histogram;
84 }
85
86 // When a sufficient haystack sample exists, pick the pattern's fully-known byte whose value is rarest in this
87 // image (the most selective prefilter for this haystack), overriding the compile-time rarest-byte anchor the
88 // Pattern carries; otherwise fall back to that compile-time anchor. Returns the engine "no fully-known byte"
89 // sentinel (size()) when segment 0 has no full byte. Correctness-neutral: the anchor only selects which single
90 // byte the memchr prefilter sweeps for; the full masked compare still decides every accepted position.
91 //
92 // The override is confined to segment 0 (the fixed run before the first bounded jump), exactly like the
93 // compile-time anchor: the segmented matcher locates that first run and then walks the variable gaps, so a byte
94 // in a later segment sits at a gap-dependent address the memchr prefilter cannot sweep for. Choosing an anchor
95 // outside segment 0 would compute a wrong candidate start and silently miss real matches.
96 [[nodiscard]] inline std::size_t
97 397 choose_scan_anchor(const scan::Pattern &pattern, const HaystackHistogram &histogram) noexcept
98 {
99 397 const std::size_t size = pattern.size();
100
2/2
✓ Branch 3 → 4 taken 55 times.
✓ Branch 3 → 9 taken 341 times.
396 if (histogram.sampled < SAMPLE_MIN_BYTES)
101 {
102
2/2
✓ Branch 5 → 6 taken 55 times.
✓ Branch 5 → 7 taken 1 time.
55 return pattern.has_anchor() ? pattern.anchor_index() : size;
103 }
104 341 const std::span<const std::byte> bytes = pattern.bytes();
105 341 const std::span<const std::byte> mask = pattern.mask();
106 341 const detail::PatternBuffer &data = detail::pattern_buffer(pattern);
107
2/2
✓ Branch 12 → 13 taken 15 times.
✓ Branch 12 → 15 taken 326 times.
341 const std::size_t segment0_end = (data.jump_count == 0) ? size : data.jumps[0].position;
108 341 std::size_t best_index = size;
109 341 std::uint32_t best_count = 0;
110
2/2
✓ Branch 28 → 17 taken 3845 times.
✓ Branch 28 → 29 taken 341 times.
4186 for (std::size_t i = 0; i < segment0_end; ++i)
111 {
112
2/2
✓ Branch 18 → 19 taken 116 times.
✓ Branch 18 → 20 taken 3729 times.
3845 if (mask[i] != std::byte{0xFF})
113 {
114 // Only a fully-known byte gives the prefilter one exact value to memchr for; nibble / wildcard
115 // positions cannot anchor.
116 116 continue;
117 }
118 7458 const std::uint32_t count = histogram.counts[std::to_integer<std::uint8_t>(bytes[i])];
119
4/4
✓ Branch 24 → 25 taken 3388 times.
✓ Branch 24 → 26 taken 341 times.
✓ Branch 25 → 26 taken 332 times.
✓ Branch 25 → 27 taken 3056 times.
3729 if (best_index == size || count < best_count)
120 {
121 673 best_index = i;
122 673 best_count = count;
123 }
124 }
125 341 return best_index;
126 }
127
128 // Builds the engine pattern for a value Pattern with the haystack-chosen anchor. Allocates two small vectors,
129 // so every caller on a noexcept path guards this against std::bad_alloc.
130 [[nodiscard]] inline EnginePattern
131 397 to_engine_pattern(const scan::Pattern &pattern, const HaystackHistogram &histogram)
132 {
133 397 const std::size_t anchor = choose_scan_anchor(pattern, histogram);
134 397 return engine_pattern_from(pattern, anchor);
135 }
136
137 /**
138 * @brief Decodes one UTF-8 code point and advances @p pos.
139 * @details Runtime and persistence share this strict decoder. A replacement character changes the requested
140 * literal. The caller must pass pos less than text.size().
141 * @param text Input bytes.
142 * @param pos Input and output byte position.
143 * @param out Decoded scalar value on success.
144 * @return True on success. False for an invalid leader, tail, scalar value, overlong form, or truncated
145 * sequence.
146 */
147 259 [[nodiscard]] inline bool decode_utf8(std::string_view text, std::size_t &pos, char32_t &out) noexcept
148 {
149 340 const auto byte_at = [&text](std::size_t index) noexcept { return static_cast<std::uint8_t>(text[index]); };
150
151 259 const std::uint8_t lead = byte_at(pos);
152 259 std::size_t extra = 0;
153 259 char32_t value = 0;
154
2/2
✓ Branch 3 → 4 taken 199 times.
✓ Branch 3 → 5 taken 60 times.
259 if (lead < 0x80)
155 {
156 199 out = lead;
157 199 ++pos;
158 199 return true;
159 }
160
4/4
✓ Branch 5 → 6 taken 54 times.
✓ Branch 5 → 8 taken 6 times.
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 8 taken 35 times.
60 if (lead >= 0xC2 && lead <= 0xDF)
161 {
162 19 extra = 1;
163 19 value = lead & 0x1FU;
164 }
165
4/4
✓ Branch 8 → 9 taken 35 times.
✓ Branch 8 → 11 taken 6 times.
✓ Branch 9 → 10 taken 19 times.
✓ Branch 9 → 11 taken 16 times.
41 else if (lead >= 0xE0 && lead <= 0xEF)
166 {
167 19 extra = 2;
168 19 value = lead & 0x0FU;
169 }
170
4/4
✓ Branch 11 → 12 taken 16 times.
✓ Branch 11 → 14 taken 6 times.
✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 14 taken 2 times.
22 else if (lead >= 0xF0 && lead <= 0xF4)
171 {
172 14 extra = 3;
173 14 value = lead & 0x07U;
174 }
175 else
176 {
177 8 return false;
178 }
179
180 // The caller guarantees pos < size. The subtraction keeps this bounds check free from pointer overflow.
181
2/2
✓ Branch 16 → 17 taken 11 times.
✓ Branch 16 → 18 taken 41 times.
52 if (extra >= text.size() - pos)
182 {
183 11 return false;
184 }
185
2/2
✓ Branch 23 → 19 taken 81 times.
✓ Branch 23 → 24 taken 30 times.
111 for (std::size_t i = 1; i <= extra; ++i)
186 {
187 81 const std::uint8_t continuation = byte_at(pos + i);
188
2/2
✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 22 taken 70 times.
81 if ((continuation & 0xC0U) != 0x80U)
189 {
190 11 return false;
191 }
192 70 value = (value << 6) | (continuation & 0x3FU);
193 }
194
195 // The leader ranges above exclude two-byte overlongs. These checks reject the remaining invalid values.
196
10/10
✓ Branch 24 → 25 taken 14 times.
✓ Branch 24 → 26 taken 16 times.
✓ Branch 25 → 26 taken 9 times.
✓ Branch 25 → 31 taken 5 times.
✓ Branch 26 → 27 taken 13 times.
✓ Branch 26 → 28 taken 12 times.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 31 taken 5 times.
✓ Branch 28 → 29 taken 13 times.
✓ Branch 28 → 30 taken 7 times.
30 if ((extra == 2 && value < 0x800) || (extra == 3 && value < 0x10000) ||
197
4/4
✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 31 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
✓ Branch 30 → 32 taken 10 times.
20 (value >= 0xD800 && value <= 0xDFFF) || value > 0x10FFFF)
198 {
199 20 return false;
200 }
201 10 pos += extra + 1;
202 10 out = value;
203 10 return true;
204 }
205
206 /// Reports whether every byte of @p text belongs to a well-formed UTF-8 sequence (decode_utf8's rule).
207 48 [[nodiscard]] inline bool utf8_is_well_formed(std::string_view text) noexcept
208 {
209 48 std::size_t pos = 0;
210
2/2
✓ Branch 8 → 3 taken 226 times.
✓ Branch 8 → 9 taken 9 times.
235 while (pos < text.size())
211 {
212 226 char32_t code_point = 0;
213
2/2
✓ Branch 4 → 5 taken 39 times.
✓ Branch 4 → 6 taken 187 times.
226 if (!decode_utf8(text, pos, code_point))
214 {
215 39 return false;
216 }
217 }
218 9 return true;
219 }
220
221 // Exact-membership validity checks for the string-xref facet enums, shared by the direct find_string_xref
222 // boundary and resolve()'s whole-ladder prepass so both fail closed on the same vocabulary.
223 748 [[nodiscard]] inline constexpr bool valid_string_encoding(scan::StringEncoding encoding) noexcept
224 {
225
4/4
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 727 times.
✓ Branch 3 → 4 taken 18 times.
✓ Branch 3 → 5 taken 3 times.
748 return encoding == scan::StringEncoding::Utf8 || encoding == scan::StringEncoding::Utf16le;
226 }
227
228 745 [[nodiscard]] inline constexpr bool valid_xref_return(scan::XrefReturn mode) noexcept
229 {
230
2/2
✓ Branch 2 → 3 taken 741 times.
✓ Branch 2 → 4 taken 4 times.
745 switch (mode)
231 {
232 741 case scan::XrefReturn::ReferencingInstruction:
233 case scan::XrefReturn::EnclosingFunction:
234 case scan::XrefReturn::StringPointerSlot:
235 741 return true;
236 }
237 4 return false;
238 }
239
240 // Zero keeps the decoder's natural width; a positive width may narrow at most the 64-bit result type.
241 1166 [[nodiscard]] inline constexpr bool valid_code_constant_byte_width(std::uint8_t byte_width) noexcept
242 {
243 1166 return byte_width <= sizeof(std::int64_t);
244 }
245
246 /**
247 * @struct ResolvedScanHit
248 * @brief A public scan hit plus the physical matched span that supplied its byte evidence.
249 * @details @ref winning_index and @ref match_span identify the selected rung for fresh epoch validation.
250 * @ref match_span is the raw matched extent. @ref physical_source can also cover a RIP instruction
251 * suffix that extends past that match (T-CODE-EPOCH).
252 */
253 struct ResolvedScanHit
254 {
255 scan::Hit hit;
256 Region physical_source;
257 /// Ladder index of the selected candidate. SIZE_MAX denotes a prologue-recovery hit.
258 std::size_t winning_index = static_cast<std::size_t>(-1);
259 /// The selected byte rung's raw matched span. Text tiers and recovery leave it empty.
260 Region match_span;
261 };
262
263 /// Resolves a request and retains private physical-source provenance for anchor quorum checks.
264 [[nodiscard]] Result<ResolvedScanHit> resolve_scan_with_provenance(const scan::ScanRequest &request);
265
266 /// Returns the compiled byte Pattern of a Direct or RipRelative candidate, or nullptr for a text tier.
267 666 [[nodiscard]] inline const scan::Pattern *byte_pattern_of(const scan::Candidate &candidate) noexcept
268 {
269
2/2
✓ Branch 3 → 4 taken 638 times.
✓ Branch 3 → 5 taken 28 times.
666 if (const scan::DirectPattern *direct = candidate.as_direct())
270 {
271 638 return &direct->pattern;
272 }
273
1/2
✓ Branch 6 → 7 taken 28 times.
✗ Branch 6 → 8 not taken.
28 if (const scan::RipRelativePattern *rip = candidate.as_rip_relative())
274 {
275 28 return &rip->pattern;
276 }
277 return nullptr;
278 }
279
280 /**
281 * @struct ResolvedCodeConstant
282 * @brief A decoded constant plus the instruction and matched byte span that supplied it.
283 * @details @ref instruction_span covers the whole decoded instruction, not just its first byte: every byte of
284 * it is evidence this constant depends on, so a co-voting selector that witnesses any of those bytes
285 * shares the failure domain and must not corroborate.
286 */
287 struct ResolvedCodeConstant
288 {
289 std::int64_t value = 0;
290 Region instruction_span;
291 Region physical_source;
292 };
293
294 /// Reads a code constant while retaining private physical-source provenance for anchor quorum checks.
295 [[nodiscard]] Result<ResolvedCodeConstant>
296 read_code_constant_with_provenance(const scan::CodeConstant &code_constant, Region scope);
297
298 /**
299 * @brief Resolves a string xref while retaining the referencing instruction's span for quorum checks.
300 * @param physical_source Receives the half-open span of the bytes the result rides on; empty on failure.
301 * @details The span covers the referencing instruction whole, and for @ref scan::XrefReturn::StringPointerSlot
302 * it reaches through the store the slot address was decoded from, since that store's own disp32 is
303 * what the mode returns. A co-voting selector that witnesses any of those bytes shares the failure
304 * domain and must not corroborate.
305 */
306 [[nodiscard]] Result<Address>
307 find_string_xref_with_provenance(const scan::StringRefQuery &query, Region scope, Region &physical_source);
308
309 /**
310 * @brief Resolves a string xref while honouring an exclusion set the caller already assembled.
311 * @param query String literal and reference-selection facets.
312 * @param scope Address range to search.
313 * @param exclusions Combined DMK-owned and caller-declared query storage; null builds the set from @p query
314 * alone.
315 * @param declared_exclusions Caller-declared spans, used only to establish readable-scan authority.
316 * @param physical_source When non-null, receives the half-open span of the bytes the result rides on; empty on
317 * failure. See @ref find_string_xref_with_provenance for what that span covers.
318 * @return The resolved address, or a typed scan error.
319 * @details Exists so the ladder resolver can carry its own query storage into phase 1 instead of re-deriving
320 * it. Internal on purpose, so no consumer depends on DMK's exclusion representation.
321 */
322 [[nodiscard]] Result<Address> find_string_xref_with_exclusions(
323 const scan::StringRefQuery &query,
324 Region scope,
325 const ScanExclusions *exclusions,
326 std::span<const Region> declared_exclusions,
327 Region *physical_source = nullptr
328 );
329
330 // Direct-tier resolution: the resolved address is the match plus the signed walk-back. Screened through the
331 // plausible-userspace floor so a pathological walk-back that underflows to a near-null / kernel-range address
332 // is a miss, never a hit.
333 [[nodiscard]] inline std::optional<std::uintptr_t>
334 464 resolve_direct(std::uintptr_t match, const scan::DirectPattern &direct) noexcept
335 {
336 464 const std::uintptr_t resolved = match + static_cast<std::uintptr_t>(direct.walk_back);
337
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 462 times.
464 if (!is_plausible_ptr(resolved))
338 {
339 2 return std::nullopt;
340 }
341 462 return resolved;
342 }
343
344 // Adds a sign-extended disp32 to the next-instruction address with defined modular arithmetic.
345 51 [[nodiscard]] inline constexpr std::uintptr_t add_rip_displacement(
346 std::uintptr_t instruction,
347 std::size_t instruction_length,
348 std::int32_t displacement
349 ) noexcept
350 {
351 51 const std::uintptr_t displacement_offset =
352 static_cast<std::uintptr_t>(static_cast<std::int64_t>(displacement));
353 51 return instruction + static_cast<std::uintptr_t>(instruction_length) + displacement_offset;
354 }
355
356 /**
357 * @brief Decodes one guarded instruction snapshot and returns its declared RIP-relative disp32.
358 * @param instruction Immutable bytes captured at the matched instruction during the page sweep.
359 * @param displacement_offset Declared byte offset of the disp32 field.
360 * @param instruction_length Declared total instruction length.
361 * @return The displacement when the decoded instruction has the declared length and disp32 location on a
362 * RIP-relative memory operand; otherwise std::nullopt.
363 * @details Defined in scan_rip_relative.cpp so Zydis stays confined to that TU. The displacement comes from the
364 * same guarded byte snapshot that is decoded, so a second live-memory read cannot diverge from it.
365 */
366 [[nodiscard]] std::optional<std::int32_t> decode_rip_displacement(
367 std::span<const std::byte> instruction,
368 std::size_t displacement_offset,
369 std::size_t instruction_length
370 ) noexcept;
371
372 // RipRelative-tier resolution: decode-verify the immutable sweep snapshot, then compute next-IP plus its
373 // sign-extended disp32. A drifted layout, an absent snapshot, or an implausible target is a miss.
374 24 [[nodiscard]] inline std::optional<std::uintptr_t> resolve_rip_relative_candidate(
375 std::uintptr_t match,
376 const scan::RipRelativePattern &rip,
377 std::span<const std::byte> instruction
378 ) noexcept
379 {
380 // A wildcarded pattern can byte-match an instruction whose opcode, addressing form, or length drifted from
381 // the declared layout; applying the declared displacement_at to that instruction would read a
382 // plausible-but-wrong disp32 and resolve a wrong target. Decode-verify gates the resolution so a drift is a
383 // miss, not a silently wrong hit.
384 const std::optional<std::int32_t> displacement =
385 24 decode_rip_displacement(instruction, rip.displacement_at, rip.instruction_length);
386
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 22 times.
24 if (!displacement)
387 {
388 2 return std::nullopt;
389 }
390 22 const std::uintptr_t resolved = add_rip_displacement(match, rip.instruction_length, *displacement);
391
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 22 times.
22 if (!is_plausible_ptr(resolved))
392 {
393 return std::nullopt;
394 }
395 22 return resolved;
396 }
397 } // namespace detail
398 } // namespace DetourModKit
399
400 #endif // DETOURMODKIT_INTERNAL_SCAN_SHARED_HPP
401