GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 80.8% 42 / 0 / 52
Functions: 100.0% 5 / 0 / 5
Branches: 70.0% 35 / 0 / 50

src/scan_matching.cpp
Line Branch Exec Source
1 /**
2 * @file scan_matching.cpp
3 * @brief Public single-pattern matching: scan() (page-gated, occurrence + Pages), unchecked::find_pattern() (raw Nth),
4 * active_simd_level(), and is_likely_function_prologue().
5 * @details Expresses the public matching surface in the Address / Region / Result vocabulary over the private engine.
6 * scan() walks the OS page map for the requested Pages class and reads only committed pages under a fault
7 * guard; the unchecked twin performs a raw, page-unfiltered scan the caller guarantees readable. The
8 * haystack-frequency anchor override accelerates the page-gated scan; the unchecked primitive uses the
9 * Pattern's compile-time anchor directly.
10 */
11
12 #include "DetourModKit/scan.hpp"
13
14 #include "internal/memory_guarded.hpp"
15 #include "internal/scan_engine.hpp"
16 #include "internal/scan_exclusions.hpp"
17 #include "internal/scan_pages.hpp"
18 #include "internal/scan_shared.hpp"
19
20 #include <cstddef>
21 #include <cstdint>
22 #include <new>
23 #include <span>
24
25 namespace DetourModKit
26 {
27 namespace scan
28 {
29 4 SimdLevel active_simd_level() noexcept
30 {
31 4 return detail::active_simd_level();
32 }
33
34 14 Result<Address> scan(const Pattern &pattern, Region scope, std::size_t occurrence, Pages pages) noexcept
35 {
36 14 return scan(pattern, scope, std::span<const Region>{}, occurrence, pages);
37 }
38
39 17 Result<Address> scan(
40 const Pattern &pattern,
41 Region scope,
42 std::span<const Region> exclusions,
43 std::size_t occurrence,
44 Pages pages
45 ) noexcept
46 {
47
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 17 times.
17 if (occurrence == 0)
48 {
49 return std::unexpected(Error{ErrorCode::NoMatch, "scan::scan"});
50 }
51
4/4
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 11 taken 15 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 11 taken 1 time.
17 if (pages != Pages::Readable && pages != Pages::Executable)
52 {
53 1 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::scan"});
54 }
55 16 const detail::ModuleSpan range = detail::module_span(scope);
56
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 17 taken 15 times.
16 if (!range.valid())
57 {
58 1 return std::unexpected(Error{ErrorCode::InvalidRange, "scan::scan"});
59 }
60
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 22 taken 13 times.
15 if (!detail::readable_scan_is_authoritative(range, pages, exclusions))
61 {
62 2 return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::scan"});
63 }
64 try
65 {
66 13 detail::ScanExclusions excluded;
67 // Only spans the sweep will actually read can affect its result, so dropping the rest keeps a caller
68 // that declares many copies from exhausting the set over spans that were never in play.
69 13 excluded.restrict_to(range.base, range.end);
70 13 detail::add_pattern_storage(excluded, pattern);
71 13 detail::add_regions(excluded, exclusions);
72
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 13 times.
13 if (excluded.overflowed())
73 {
74 // More declared spans than the set can hold: some query storage would go unexcluded, so no result
75 // from this scan is trustworthy. Fail closed instead of silently narrowing the exclusion.
76 return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::scan"});
77 }
78
79 13 const detail::HaystackHistogram histogram = detail::sample_haystack(scope);
80
1/2
✓ Branch 31 → 32 taken 13 times.
✗ Branch 31 → 52 not taken.
13 const detail::EnginePattern compiled = detail::to_engine_pattern(pattern, histogram);
81 13 const detail::MatchResult result = detail::scan_module_pages(
82 compiled,
83 range,
84 pages,
85 13 detail::ScanQuery{
86 .occurrence = occurrence,
87 .count_beyond = false,
88 .exclusions = &excluded,
89 }
90 );
91 // A truncated sweep never visited part of the scope, so an earlier occurrence may hide there: the match
92 // it did find is not provably the Nth one. That makes truncation fatal to the result whether or not a
93 // match was found. The two causes are distinct caller problems and stay distinct codes: a concurrent
94 // unmap of the scanned range versus a pattern whose bounded jumps are too broad to search exhaustively.
95
2/2
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 37 taken 11 times.
13 if (result.budget_exhausted)
96 {
97 2 return std::unexpected(Error{ErrorCode::BudgetExceeded, "scan::scan"});
98 }
99
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 41 taken 11 times.
11 if (result.incomplete)
100 {
101 return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::scan"});
102 }
103
2/2
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 45 taken 7 times.
11 if (result.match == nullptr)
104 {
105 4 return std::unexpected(Error{ErrorCode::NoMatch, "scan::scan"});
106 }
107 7 return Address{reinterpret_cast<std::uintptr_t>(result.match)};
108 13 }
109 catch (const std::bad_alloc &)
110 {
111 return std::unexpected(Error{ErrorCode::OutOfMemory, "scan::scan"});
112 }
113 }
114
115 17 bool is_likely_function_prologue(Address addr) noexcept
116 {
117
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 16 times.
17 if (!addr)
118 {
119 1 return false;
120 }
121
122 // Read the first opcode byte under a fault guard rather than is_readable + a raw dereference. is_readable
123 // is a TOCTOU illusion (the page can change or unmap between the check and the read), and the bare
124 // dereference would then fault the host. guarded_read returns nullopt on any fault.
125 16 const auto b0 = detail::guarded_read<std::uint8_t>(addr.raw());
126
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 15 times.
16 if (!b0)
127 {
128 1 return false;
129 }
130
131 // Reject bytes that never begin a real function prologue, so an AOB match that landed in inter-function
132 // padding or past a function's end is filtered out instead of accepted as a target:
133 // 0x00 - zero fill / uninitialized page (decodes as `add [rax], al`)
134 // 0xCC - INT3, the alignment padding linkers insert between functions
135 // 0xC3 - RET (near return): a function epilogue, not a prologue
136 // 0xC2 - RET imm16: likewise a return, not a prologue
137
8/8
✓ Branch 11 → 12 taken 14 times.
✓ Branch 11 → 19 taken 1 time.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 19 taken 1 time.
✓ Branch 15 → 16 taken 12 times.
✓ Branch 15 → 19 taken 1 time.
✓ Branch 17 → 18 taken 11 times.
✓ Branch 17 → 19 taken 1 time.
15 return *b0 != 0x00 && *b0 != 0xCC && *b0 != 0xC2 && *b0 != 0xC3;
138 }
139
140 namespace unchecked
141 {
142 3 const std::byte *find_pattern(Region region, const Pattern &pattern, std::size_t occurrence) noexcept
143 {
144
4/8
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 3 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
3 if (region.base.raw() == 0 || region.size == 0 || occurrence == 0)
145 {
146 return nullptr;
147 }
148 try
149 {
150 // The raw primitive does no page filtering, so the caller owns readability; it also does not
151 // consult the haystack histogram (that override accelerates the page-gated scan), using the
152 // Pattern's compile-time anchor directly.
153
1/2
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 13 not taken.
3 const std::size_t anchor = pattern.has_anchor() ? pattern.anchor_index() : pattern.size();
154
1/2
✓ Branch 14 → 15 taken 3 times.
✗ Branch 14 → 23 not taken.
3 const detail::EnginePattern compiled = detail::engine_pattern_from(pattern, anchor);
155
1/2
✓ Branch 16 → 17 taken 3 times.
✗ Branch 16 → 21 not taken.
3 return detail::find_pattern(region.base.ptr<const std::byte>(), region.size, compiled, occurrence);
156 3 }
157 catch (const std::bad_alloc &)
158 {
159 return nullptr;
160 }
161 }
162 } // namespace unchecked
163 } // namespace scan
164 } // namespace DetourModKit
165