GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.2% 118 / 0 / 128
Functions: 100.0% 6 / 0 / 6
Branches: 69.9% 95 / 0 / 136

src/scan_code_constant.cpp
Line Branch Exec Source
1 /**
2 * @file scan_code_constant.cpp
3 * @brief Zydis-backed extraction of a constant encoded in engine machine code: read_code_constant().
4 * @details The code-side twin of the RTTI self-heal: the CodeConstant's candidate ladder resolves to an instruction
5 * site (via scan::resolve), the live instruction is decoded, and the requested operand's immediate or memory
6 * displacement is returned as the CURRENT value. The caller's nominal is never a short-circuit, so a
7 * same-shape / different-value drift is reported as the new value. The CodeConstant's Candidate ladder
8 * resolves the site; Zydis is confined to this TU.
9 */
10
11 #include "DetourModKit/scan.hpp"
12
13 #include "internal/scan_engine.hpp"
14 #include "internal/scan_pages.hpp"
15 #include "internal/scan_shared.hpp"
16
17 #include <Zydis/Zydis.h>
18
19 #include <algorithm>
20 #include <cstddef>
21 #include <cstdint>
22 #include <cstring>
23 #include <span>
24
25 namespace DetourModKit
26 {
27 namespace scan
28 {
29 namespace
30 {
31 // Narrows an already-64-bit-sign-extended value to @p byte_width bytes and re-sign-extends from that width,
32 // so a deliberately narrowed negative value (for example a disp8 of -1) stays negative instead of becoming
33 // a large positive number. @p byte_width 0 returns the value verbatim, since Zydis has already
34 // sign-extended immediates and displacements to 64 bits.
35 121 std::int64_t narrow_signed(std::int64_t value, std::uint8_t byte_width) noexcept
36 {
37
4/4
✓ Branch 2 → 3 taken 24 times.
✓ Branch 2 → 4 taken 97 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 22 times.
121 if (byte_width == 0 || byte_width >= sizeof(std::int64_t))
38 {
39 99 return value;
40 }
41 22 const unsigned bits = static_cast<unsigned>(byte_width) * 8u;
42 22 const std::uint64_t mask = (std::uint64_t{1} << bits) - 1u;
43 22 const std::uint64_t masked = static_cast<std::uint64_t>(value) & mask;
44 22 const std::uint64_t sign_bit = std::uint64_t{1} << (bits - 1u);
45 // Two's-complement sign-extension from the top bit of the chosen width.
46 22 const std::uint64_t extended = (masked ^ sign_bit) - sign_bit;
47 22 return static_cast<std::int64_t>(extended);
48 }
49 } // namespace
50
51 namespace
52 {
53 141 void overlay_snapshot(
54 std::span<std::byte> destination,
55 std::uintptr_t destination_base,
56 std::span<const std::byte> source,
57 std::uintptr_t source_base
58 ) noexcept
59 {
60 141 std::size_t destination_offset = 0;
61 141 std::size_t source_offset = 0;
62
2/2
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 7 taken 136 times.
141 if (destination_base < source_base)
63 {
64 5 const std::uintptr_t delta = source_base - destination_base;
65
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 1 time.
5 if (delta >= destination.size())
66 {
67 4 return;
68 }
69 1 destination_offset = static_cast<std::size_t>(delta);
70 }
71 else
72 {
73 136 const std::uintptr_t delta = destination_base - source_base;
74
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 136 times.
136 if (delta >= source.size())
75 {
76 return;
77 }
78 136 source_offset = static_cast<std::size_t>(delta);
79 }
80
81 const std::size_t overlap =
82 137 std::min(destination.size() - destination_offset, source.size() - source_offset);
83 137 std::memcpy(destination.data() + destination_offset, source.data() + source_offset, overlap);
84 }
85
86 141 [[nodiscard]] bool selector_still_resolves_site(
87 const Candidate &candidate,
88 Region match_span,
89 Region physical_source,
90 std::uintptr_t decoded_site,
91 std::uintptr_t window_base,
92 std::span<const std::byte> window
93 )
94 {
95 141 constexpr std::size_t MAX_EVIDENCE_SPAN =
96 detail::MAX_PATTERN_BYTES + detail::MAX_PATTERN_JUMPS * detail::MAX_JUMP_SPAN;
97 141 constexpr std::size_t MAX_SOURCE_SPAN = MAX_EVIDENCE_SPAN + MAX_X86_INSTRUCTION_LENGTH;
98 141 const Pattern *pattern = detail::byte_pattern_of(candidate);
99 141 const std::uintptr_t span_base = match_span.base.raw();
100 141 const std::uintptr_t source_base = physical_source.base.raw();
101
4/8
✓ Branch 5 → 6 taken 141 times.
✗ Branch 5 → 9 not taken.
✓ Branch 6 → 7 taken 141 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 141 times.
✗ Branch 7 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 141 times.
141 if (pattern == nullptr || span_base == 0 || match_span.size == 0 || match_span.size > MAX_EVIDENCE_SPAN)
102 {
103 return false;
104 }
105
2/4
✓ Branch 10 → 11 taken 141 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 141 times.
✗ Branch 11 → 13 not taken.
141 if (source_base != span_base || physical_source.size < match_span.size ||
106
1/2
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 141 times.
141 physical_source.size > MAX_SOURCE_SPAN)
107 {
108 return false;
109 }
110 std::byte fresh[MAX_SOURCE_SPAN];
111
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 141 times.
141 if (!detail::guarded_read_bytes(source_base, fresh, physical_source.size))
112 {
113 return false;
114 }
115 141 overlay_snapshot(std::span<std::byte>{fresh, physical_source.size}, source_base, window, window_base);
116 const detail::EnginePattern engine = detail::engine_pattern_from(
117 *pattern,
118 282 pattern->has_anchor() ? pattern->anchor_index() : pattern->size()
119
2/4
✓ Branch 20 → 21 taken 141 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 141 times.
✗ Branch 23 → 55 not taken.
282 );
120 141 const detail::RawMatch fresh_match = detail::find_pattern_raw(fresh, match_span.size, engine);
121
3/4
✓ Branch 25 → 26 taken 141 times.
✗ Branch 25 → 31 not taken.
✓ Branch 26 → 27 taken 137 times.
✓ Branch 26 → 31 taken 4 times.
141 if (fresh_match.budget_exhausted || fresh_match.start != fresh ||
122
2/4
✓ Branch 27 → 28 taken 137 times.
✗ Branch 27 → 31 not taken.
✓ Branch 28 → 29 taken 137 times.
✗ Branch 28 → 31 not taken.
137 fresh_match.end != fresh + match_span.size || fresh_match.point == nullptr ||
123
2/4
✓ Branch 29 → 30 taken 137 times.
✗ Branch 29 → 31 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 137 times.
137 fresh_match.point < fresh || fresh_match.point > fresh + match_span.size)
124 {
125 4 return false;
126 }
127
128 137 const std::size_t point_offset = static_cast<std::size_t>(fresh_match.point - fresh);
129
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 137 times.
137 if (point_offset > static_cast<std::size_t>(UINTPTR_MAX - source_base))
130 {
131 return false;
132 }
133 137 const std::uintptr_t live_point = source_base + point_offset;
134 137 std::optional<std::uintptr_t> fresh_site;
135
2/2
✓ Branch 35 → 36 taken 136 times.
✓ Branch 35 → 37 taken 1 time.
137 if (const DirectPattern *direct = candidate.as_direct())
136 {
137 136 fresh_site = detail::resolve_direct(live_point, *direct);
138 }
139
1/2
✓ Branch 38 → 39 taken 1 time.
✗ Branch 38 → 45 not taken.
1 else if (const RipRelativePattern *rip = candidate.as_rip_relative())
140 {
141
1/2
✓ Branch 39 → 40 taken 1 time.
✗ Branch 39 → 41 not taken.
1 if (point_offset > physical_source.size ||
142
1/2
✗ Branch 40 → 41 not taken.
✓ Branch 40 → 42 taken 1 time.
1 rip->instruction_length > physical_source.size - point_offset)
143 {
144 return false;
145 }
146 const std::span<const std::byte> instruction_snapshot{
147 fresh + point_offset,
148 1 rip->instruction_length
149 1 };
150 1 fresh_site = detail::resolve_rip_relative_candidate(live_point, *rip, instruction_snapshot);
151 }
152
3/4
✓ Branch 46 → 47 taken 137 times.
✗ Branch 46 → 50 not taken.
✓ Branch 48 → 49 taken 135 times.
✓ Branch 48 → 50 taken 2 times.
137 return fresh_site && *fresh_site == decoded_site;
153 141 }
154
155 154 Result<std::int64_t> read_code_constant_impl(
156 const CodeConstant &code_constant,
157 Region scope,
158 Region *instruction_span,
159 Region *physical_source
160 )
161 {
162
2/2
✓ Branch 2 → 3 taken 108 times.
✓ Branch 2 → 5 taken 46 times.
154 if (instruction_span != nullptr)
163 {
164 108 *instruction_span = Region{};
165 }
166
2/2
✓ Branch 5 → 6 taken 108 times.
✓ Branch 5 → 8 taken 46 times.
154 if (physical_source != nullptr)
167 {
168 108 *physical_source = Region{};
169 }
170 351 if ((code_constant.kind != OperandKind::Immediate &&
171
6/6
✓ Branch 8 → 9 taken 43 times.
✓ Branch 8 → 10 taken 111 times.
✓ Branch 9 → 10 taken 41 times.
✓ Branch 9 → 12 taken 2 times.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 14 → 18 taken 149 times.
306 code_constant.kind != OperandKind::MemoryDisplacement) ||
172
2/2
✓ Branch 11 → 12 taken 3 times.
✓ Branch 11 → 13 taken 149 times.
152 !detail::valid_code_constant_byte_width(code_constant.byte_width))
173 {
174 5 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::read_code_constant"});
175 }
176
177 // Resolve the instruction site through the candidate ladder and propagate its typed failure verbatim
178 // (EmptyCandidates, NoMatch, InvalidRange, ...). The resolved address must name an executable
179 // instruction site; require_executable_result rejects an unsuitable rung and lets resolve() try a later
180 // ladder fallback.
181 //
182 // A code constant is encoded in machine code. Restrict byte tiers to execute-readable pages so an
183 // identical run in .rdata / .data cannot win or make the code match ambiguous. A RipRelative or
184 // walked-back candidate can still transform an executable match into a data address, so enforce the
185 // final-page policy in resolve() and recheck below before the fault-safe read and decode. The recheck
186 // narrows, but cannot eliminate, a concurrent protection change; guarded_read_bytes preserves the
187 // fail-closed host-safety guarantee.
188 149 const ScanRequest request{
189 .ladder = code_constant.site,
190 .label = "read_code_constant",
191 .scope = scope,
192 .pages = Pages::Executable,
193 .require_executable_result = true,
194 149 };
195
1/2
✓ Branch 20 → 21 taken 149 times.
✗ Branch 20 → 132 not taken.
149 const Result<detail::ResolvedScanHit> resolved = detail::resolve_scan_with_provenance(request);
196
2/2
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 27 taken 143 times.
149 if (!resolved)
197 {
198 6 return std::unexpected(resolved.error());
199 }
200 143 const std::uintptr_t site = resolved->hit.address.raw();
201
2/2
✓ Branch 29 → 30 taken 107 times.
✓ Branch 29 → 32 taken 36 times.
143 if (physical_source != nullptr)
202 {
203 107 *physical_source = resolved->physical_source;
204 }
205
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 37 taken 143 times.
143 if (!detail::is_executable_address(site))
206 {
207 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
208 }
209
210 143 const detail::ModuleSpan range = detail::module_span(scope);
211
212 // Read a full maximum-length instruction window, clamped to the module so the read never runs past the
213 // end of the image, behind a fault guard. A truncated window that fails to decode is reported as
214 // DecodeFailed below.
215 std::byte buf[ZYDIS_MAX_INSTRUCTION_LENGTH];
216 143 std::size_t avail = sizeof(buf);
217
3/6
✓ Branch 39 → 40 taken 143 times.
✗ Branch 39 → 42 not taken.
✓ Branch 40 → 41 taken 143 times.
✗ Branch 40 → 42 not taken.
✓ Branch 43 → 44 taken 143 times.
✗ Branch 43 → 46 not taken.
143 if (range.valid() && site < range.end)
218 {
219 143 const std::uintptr_t to_end = range.end - site;
220
2/2
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 142 times.
143 if (to_end < avail)
221 {
222 1 avail = static_cast<std::size_t>(to_end);
223 }
224 }
225
3/6
✓ Branch 46 → 47 taken 143 times.
✗ Branch 46 → 49 not taken.
✗ Branch 48 → 49 not taken.
✓ Branch 48 → 50 taken 143 times.
✗ Branch 51 → 52 not taken.
✓ Branch 51 → 55 taken 143 times.
143 if (avail == 0 || !detail::guarded_read_bytes(site, buf, avail))
226 {
227 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
228 }
229
230 ZydisDecoder decoder;
231
2/4
✓ Branch 55 → 56 taken 143 times.
✗ Branch 55 → 130 not taken.
✗ Branch 56 → 57 not taken.
✓ Branch 56 → 60 taken 143 times.
143 if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)))
232 {
233 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
234 }
235
236 ZydisDecodedInstruction insn;
237 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
238
3/4
✓ Branch 60 → 61 taken 143 times.
✗ Branch 60 → 130 not taken.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 65 taken 142 times.
143 if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, buf, avail, &insn, operands)))
239 {
240 1 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
241 }
242
2/2
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 70 taken 141 times.
142 if (!detail::is_executable_range(site, insn.length))
243 {
244 1 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
245 }
246
247
3/6
✓ Branch 72 → 73 taken 141 times.
✗ Branch 72 → 76 not taken.
✓ Branch 74 → 75 taken 141 times.
✗ Branch 74 → 76 not taken.
✓ Branch 77 → 78 taken 141 times.
✗ Branch 77 → 93 not taken.
141 if (resolved->winning_index < code_constant.site.size() && resolved->match_span.size != 0)
248 {
249 141 const Candidate &winning_candidate = code_constant.site[resolved->winning_index];
250
1/2
✓ Branch 81 → 82 taken 141 times.
✗ Branch 81 → 88 not taken.
282 if (detail::byte_pattern_of(winning_candidate) != nullptr &&
251
5/6
✓ Branch 85 → 86 taken 141 times.
✗ Branch 85 → 128 not taken.
✓ Branch 86 → 87 taken 6 times.
✓ Branch 86 → 88 taken 135 times.
✓ Branch 89 → 90 taken 6 times.
✓ Branch 89 → 93 taken 135 times.
282 !selector_still_resolves_site(
252 winning_candidate,
253 141 resolved->match_span,
254 141 resolved->physical_source,
255 site,
256 site,
257 141 std::span<const std::byte>{buf, avail}
258 ))
259 {
260 6 return std::unexpected(Error{ErrorCode::EvidenceMismatch, "scan::read_code_constant"});
261 }
262 }
263
264 // Published only once the decode has proven the length, so the provenance names the instruction's real
265 // extent rather than a one-byte point a co-voting selector could straddle without overlapping.
266
2/2
✓ Branch 93 → 94 taken 106 times.
✓ Branch 93 → 96 taken 29 times.
135 if (instruction_span != nullptr)
267 {
268 106 *instruction_span = Region{Address{site}, static_cast<std::size_t>(insn.length)};
269 }
270
271 // Index the VISIBLE operands - the ones a human counts in a disassembler. operand_count includes
272 // implicit/hidden operands (flags, implicit registers, stack writes), which would make a fixed
273 // operand_index drift between mnemonics.
274
2/2
✓ Branch 96 → 97 taken 1 time.
✓ Branch 96 → 100 taken 134 times.
135 if (code_constant.operand_index >= insn.operand_count_visible)
275 {
276 1 return std::unexpected(Error{ErrorCode::OperandOutOfRange, "scan::read_code_constant"});
277 }
278 134 const ZydisDecodedOperand &operand = operands[code_constant.operand_index];
279
280
2/2
✓ Branch 100 → 101 taken 96 times.
✓ Branch 100 → 108 taken 38 times.
134 if (code_constant.kind == OperandKind::Immediate)
281 {
282
2/2
✓ Branch 101 → 102 taken 1 time.
✓ Branch 101 → 105 taken 95 times.
96 if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE)
283 {
284 1 return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"});
285 }
286 // imm.value.s is already 64-bit sign-extended by Zydis.
287 95 return narrow_signed(static_cast<std::int64_t>(operand.imm.value.s), code_constant.byte_width);
288 }
289
290 // MemoryDisplacement. A register-indirect operand with no displacement (for example plain `[rcx]`)
291 // carries no constant to read.
292
3/4
✓ Branch 108 → 109 taken 38 times.
✗ Branch 108 → 110 not taken.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 113 taken 37 times.
38 if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || !operand.mem.disp.has_displacement)
293 {
294 1 return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"});
295 }
296
297
2/2
✓ Branch 113 → 114 taken 11 times.
✓ Branch 113 → 122 taken 26 times.
37 if (operand.mem.base == ZYDIS_REGISTER_RIP)
298 {
299 // RIP-relative: the raw displacement is measured from the next instruction, not the absolute
300 // constant the caller wants. Resolve it to the absolute target so the return value is meaningful
301 // rather than a misleading relative offset.
302 11 ZyanU64 absolute = 0;
303
2/4
✓ Branch 114 → 115 taken 11 times.
✗ Branch 114 → 129 not taken.
✗ Branch 115 → 116 not taken.
✓ Branch 115 → 119 taken 11 times.
11 if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(site), &absolute)))
304 {
305 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
306 }
307 11 return static_cast<std::int64_t>(absolute);
308 }
309
310 // disp.value is already 64-bit sign-extended.
311 26 return narrow_signed(static_cast<std::int64_t>(operand.mem.disp.value), code_constant.byte_width);
312 149 }
313 } // namespace
314
315 46 Result<std::int64_t> read_code_constant(const CodeConstant &code_constant, Region scope)
316 {
317 46 return read_code_constant_impl(code_constant, scope, nullptr, nullptr);
318 }
319 } // namespace scan
320
321 Result<detail::ResolvedCodeConstant>
322 108 detail::read_code_constant_with_provenance(const scan::CodeConstant &code_constant, Region scope)
323 {
324 108 Region instruction_span;
325 108 Region physical_source;
326 Result<std::int64_t> value =
327
1/2
✓ Branch 2 → 3 taken 108 times.
✗ Branch 2 → 14 not taken.
108 scan::read_code_constant_impl(code_constant, scope, &instruction_span, &physical_source);
328
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 9 taken 106 times.
108 if (!value)
329 {
330 2 return std::unexpected(value.error());
331 }
332 106 return ResolvedCodeConstant{*value, instruction_span, physical_source};
333 }
334 } // namespace DetourModKit
335