./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
120 / 130
92.3%
Branches:
94 / 134
70.1%
Functions:
7 / 7
100.0%

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 /// Extracts the requested visible operand constant from the instruction decoded at @p site.
156 135 [[nodiscard]] Result<std::int64_t> extract_operand_constant(
157 const CodeConstant &code_constant,
158 const ZydisDecodedInstruction &insn,
159 const ZydisDecodedOperand (&operands)[ZYDIS_MAX_OPERAND_COUNT],
160 std::uintptr_t site
161 ) noexcept
162 {
163 // Index the VISIBLE operands, the ones that a human counts in a disassembler. operand_count also counts
164 // hidden operands (flags, implicit registers, stack writes), so a bound on operand_count admits a
165 // hidden operand whose position differs between mnemonics.
166
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 134 times.
135 if (code_constant.operand_index >= insn.operand_count_visible)
167 {
168 1 return std::unexpected(Error{ErrorCode::OperandOutOfRange, "scan::read_code_constant"});
169 }
170 134 const ZydisDecodedOperand &operand = operands[code_constant.operand_index];
171
172
2/2
✓ Branch 6 → 7 taken 96 times.
✓ Branch 6 → 14 taken 38 times.
134 if (code_constant.kind == OperandKind::Immediate)
173 {
174
2/2
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 11 taken 95 times.
96 if (operand.type != ZYDIS_OPERAND_TYPE_IMMEDIATE)
175 {
176 1 return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"});
177 }
178 95 return narrow_signed(static_cast<std::int64_t>(operand.imm.value.s), code_constant.byte_width);
179 }
180
181 // For MemoryDisplacement, a register-indirect operand without a displacement (for example `[rcx]`)
182 // carries no constant to read.
183
3/4
✓ Branch 14 → 15 taken 38 times.
✗ Branch 14 → 16 not taken.
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 19 taken 37 times.
38 if (operand.type != ZYDIS_OPERAND_TYPE_MEMORY || !operand.mem.disp.has_displacement)
184 {
185 1 return std::unexpected(Error{ErrorCode::UnexpectedShape, "scan::read_code_constant"});
186 }
187
188
2/2
✓ Branch 19 → 20 taken 11 times.
✓ Branch 19 → 28 taken 26 times.
37 if (operand.mem.base == ZYDIS_REGISTER_RIP)
189 {
190 // A RIP-relative displacement is relative to the next instruction. Return the absolute target that
191 // the caller wants, not the raw relative offset.
192 11 ZyanU64 absolute = 0;
193
1/2
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 25 taken 11 times.
11 if (!ZYAN_SUCCESS(ZydisCalcAbsoluteAddress(&insn, &operand, static_cast<ZyanU64>(site), &absolute)))
194 {
195 ✗ return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
196 }
197 11 return static_cast<std::int64_t>(absolute);
198 }
199
200 26 return narrow_signed(static_cast<std::int64_t>(operand.mem.disp.value), code_constant.byte_width);
201 }
202
203 154 Result<std::int64_t> read_code_constant_impl(
204 const CodeConstant &code_constant,
205 Region scope,
206 Region *instruction_span,
207 Region *physical_source
208 )
209 {
210
2/2
✓ Branch 2 → 3 taken 108 times.
✓ Branch 2 → 5 taken 46 times.
154 if (instruction_span != nullptr)
211 {
212 108 *instruction_span = Region{};
213 }
214
2/2
✓ Branch 5 → 6 taken 108 times.
✓ Branch 5 → 8 taken 46 times.
154 if (physical_source != nullptr)
215 {
216 108 *physical_source = Region{};
217 }
218 351 if ((code_constant.kind != OperandKind::Immediate &&
219
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) ||
220
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))
221 {
222 5 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::read_code_constant"});
223 }
224
225 // Resolve the instruction site through the candidate ladder and propagate its typed failure verbatim
226 // (EmptyCandidates, NoMatch, InvalidRange, ...). The resolved address must name an executable
227 // instruction site; require_executable_result rejects an unsuitable rung and lets resolve() try a later
228 // ladder fallback.
229 //
230 // A code constant is encoded in machine code. Restrict byte tiers to execute-readable pages so an
231 // identical run in .rdata / .data cannot win or make the code match ambiguous. A RipRelative or
232 // walked-back candidate can still transform an executable match into a data address, so enforce the
233 // final-page policy in resolve() and recheck below before the fault-safe read and decode. The recheck
234 // narrows, but cannot eliminate, a concurrent protection change; guarded_read_bytes preserves the
235 // fail-closed host-safety guarantee.
236 149 const ScanRequest request{
237 .ladder = code_constant.site,
238 .label = "read_code_constant",
239 .scope = scope,
240 .pages = Pages::Executable,
241 .require_executable_result = true,
242 149 };
243
1/2
✓ Branch 20 → 21 taken 149 times.
✗ Branch 20 → 103 not taken.
149 const Result<detail::ResolvedScanHit> resolved = detail::resolve_scan_with_provenance(request);
244
2/2
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 27 taken 143 times.
149 if (!resolved)
245 {
246 6 return std::unexpected(resolved.error());
247 }
248 143 const std::uintptr_t site = resolved->hit.address.raw();
249
2/2
✓ Branch 29 → 30 taken 107 times.
✓ Branch 29 → 32 taken 36 times.
143 if (physical_source != nullptr)
250 {
251 107 *physical_source = resolved->physical_source;
252 }
253
1/2
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 37 taken 143 times.
143 if (!detail::is_executable_address(site))
254 {
255 ✗ return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
256 }
257
258 143 const detail::ModuleSpan range = detail::module_span(scope);
259
260 // Read a full maximum-length instruction window, clamped to the module so the read never runs past the
261 // end of the image, behind a fault guard. A truncated window that fails to decode is reported as
262 // DecodeFailed below.
263 std::byte buf[ZYDIS_MAX_INSTRUCTION_LENGTH];
264 143 std::size_t avail = sizeof(buf);
265
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)
266 {
267 143 const std::uintptr_t to_end = range.end - site;
268
2/2
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 142 times.
143 if (to_end < avail)
269 {
270 1 avail = static_cast<std::size_t>(to_end);
271 }
272 }
273
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))
274 {
275 ✗ return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
276 }
277
278 ZydisDecoder decoder;
279
2/4
✓ Branch 55 → 56 taken 143 times.
✗ Branch 55 → 101 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)))
280 {
281 ✗ return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
282 }
283
284 ZydisDecodedInstruction insn;
285 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
286
3/4
✓ Branch 60 → 61 taken 143 times.
✗ Branch 60 → 101 not taken.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 61 → 65 taken 142 times.
143 if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(&decoder, buf, avail, &insn, operands)))
287 {
288 1 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
289 }
290
2/2
✓ Branch 66 → 67 taken 1 time.
✓ Branch 66 → 70 taken 141 times.
142 if (!detail::is_executable_range(site, insn.length))
291 {
292 1 return std::unexpected(Error{ErrorCode::DecodeFailed, "scan::read_code_constant"});
293 }
294
295
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)
296 {
297 141 const Candidate &winning_candidate = code_constant.site[resolved->winning_index];
298
1/2
✓ Branch 81 → 82 taken 141 times.
✗ Branch 81 → 88 not taken.
282 if (detail::byte_pattern_of(winning_candidate) != nullptr &&
299
5/6
✓ Branch 85 → 86 taken 141 times.
✗ Branch 85 → 100 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(
300 winning_candidate,
301 141 resolved->match_span,
302 141 resolved->physical_source,
303 site,
304 site,
305 141 std::span<const std::byte>{buf, avail}
306 ))
307 {
308 6 return std::unexpected(Error{ErrorCode::EvidenceMismatch, "scan::read_code_constant"});
309 }
310 }
311
312 // Published only once the decode has proven the length, so the provenance names the instruction's real
313 // extent rather than a one-byte point a co-voting selector could straddle without overlapping.
314
2/2
✓ Branch 93 → 94 taken 106 times.
✓ Branch 93 → 96 taken 29 times.
135 if (instruction_span != nullptr)
315 {
316 106 *instruction_span = Region{Address{site}, static_cast<std::size_t>(insn.length)};
317 }
318
319 135 return extract_operand_constant(code_constant, insn, operands, site);
320 149 }
321 } // namespace
322
323 46 Result<std::int64_t> read_code_constant(const CodeConstant &code_constant, Region scope)
324 {
325 46 return read_code_constant_impl(code_constant, scope, nullptr, nullptr);
326 }
327 } // namespace scan
328
329 Result<detail::ResolvedCodeConstant>
330 108 detail::read_code_constant_with_provenance(const scan::CodeConstant &code_constant, Region scope)
331 {
332 108 Region instruction_span;
333 108 Region physical_source;
334 Result<std::int64_t> value =
335
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);
336
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 9 taken 106 times.
108 if (!value)
337 {
338 2 return std::unexpected(value.error());
339 }
340 106 return ResolvedCodeConstant{*value, instruction_span, physical_source};
341 }
342 } // namespace DetourModKit
343