GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.7% 59 / 0 / 63
Functions: 100.0% 3 / 0 / 3
Branches: 86.2% 50 / 0 / 58

src/scan_rip_relative.cpp
Line Branch Exec Source
1 /**
2 * @file scan_rip_relative.cpp
3 * @brief Standalone x86-64 RIP-relative resolvers and candidate semantic verification.
4 * @details Resolves an absolute address from a RIP-relative instruction whose displacement is read under a fault guard,
5 * then screened against the plausible-userspace floor. find_and_resolve_rip_relative scans a Region for an
6 * opcode prefix and returns the first occurrence whose disp32 resolves plausibly to a readable target,
7 * skipping decoy prefixes whose displacement cannot be trusted. A failure on either path surfaces as a typed
8 * ErrorCode in the Scan block rather than undefined behaviour.
9 */
10
11 #include "DetourModKit/scan.hpp"
12
13 #include "DetourModKit/memory.hpp"
14
15 #include "internal/memory_guarded.hpp"
16 #include "internal/scan_shared.hpp"
17
18 #include <Zydis/Zydis.h>
19
20 #include <cstddef>
21 #include <cstdint>
22 #include <cstring>
23 #include <optional>
24 #include <span>
25
26 namespace DetourModKit
27 {
28 namespace scan
29 {
30 34 Result<Address> resolve_rip_relative(
31 Address instruction,
32 std::size_t displacement_offset,
33 std::size_t instruction_length
34 ) noexcept
35 {
36
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 31 times.
34 if (!instruction)
37 {
38 3 return std::unexpected(Error{ErrorCode::NullInput, "scan::resolve_rip_relative"});
39 }
40
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 30 times.
31 if (!is_valid_rip_relative_layout(displacement_offset, instruction_length))
41 {
42 1 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::resolve_rip_relative"});
43 }
44
45 30 const std::uintptr_t base = instruction.raw();
46 30 const std::uintptr_t disp_addr = base + static_cast<std::uintptr_t>(displacement_offset);
47 // Read the displacement under one fault guard instead of is_readable + raw memcpy. is_readable is
48 // a time-of-check/time-of-use illusion (the page can change protection or unmap between the check and the
49 // copy), so an unguarded memcpy can fault the host.
50 30 const auto displacement = detail::guarded_read<std::int32_t>(disp_addr);
51
2/2
✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 19 taken 29 times.
30 if (!displacement)
52 {
53 1 return std::unexpected(Error{ErrorCode::UnreadableDisplacement, "scan::resolve_rip_relative"});
54 }
55
56 // Compute the target in unsigned modular arithmetic so the math stays well-defined on every input,
57 // including kernel-range instruction addresses (where intptr_t would be negative and signed overflow is
58 // UB). The displacement is sign-extended first so negative disp32 values wrap to the correct 64-bit offset.
59 29 const std::uintptr_t target = detail::add_rip_displacement(base, instruction_length, *displacement);
60
61 // Fail closed on a target that cannot be a real in-process address. A corrupt or hostile displacement can
62 // resolve to 0, a low guard-page address, or a kernel-range value; returning that as "success" would hand
63 // the caller a pointer that faults on first use. is_plausible_ptr is pure arithmetic, so this guard
64 // adds no syscall and no memory access.
65
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 26 taken 27 times.
29 if (!detail::is_plausible_ptr(target))
66 {
67 2 return std::unexpected(Error{ErrorCode::ImplausibleTarget, "scan::resolve_rip_relative"});
68 }
69 27 return Address{target};
70 }
71
72 24 Result<Address> find_and_resolve_rip_relative(
73 Region search,
74 std::span<const std::byte> opcode_prefix,
75 std::size_t instruction_length
76 ) noexcept
77 {
78 24 const std::byte *search_start = search.base.ptr<const std::byte>();
79
6/6
✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 21 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 12 taken 21 times.
24 if (!search_start || opcode_prefix.empty())
80 {
81 3 return std::unexpected(Error{ErrorCode::NullInput, "scan::find_and_resolve_rip_relative"});
82 }
83
84 21 const std::size_t prefix_len = opcode_prefix.size();
85
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 20 times.
21 if (!is_valid_rip_relative_layout(prefix_len, instruction_length))
86 {
87 1 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::find_and_resolve_rip_relative"});
88 }
89 20 const std::size_t min_bytes = prefix_len + sizeof(std::int32_t);
90
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 22 taken 18 times.
20 if (search.size < min_bytes)
91 {
92 2 return std::unexpected(Error{ErrorCode::RegionTooSmall, "scan::find_and_resolve_rip_relative"});
93 }
94
95 18 const std::size_t scan_limit = search.size - min_bytes;
96 18 const std::byte first = opcode_prefix[0];
97
98 // A byte sequence that matches the opcode prefix but whose disp32 resolves to an
99 // implausible or unreadable target is a decoy, not a hard stop (B-60): the same prefix
100 // can legitimately recur, so the genuine instruction may be a later occurrence. Keep
101 // scanning past each decoy and return the first occurrence whose displacement resolves
102 // plausibly to a readable target. The last resolve failure is retained so an all-decoy
103 // region reports WHY resolution never succeeded (e.g. ImplausibleTarget or
104 // UnreadableTarget) rather than a bare PrefixNotFound.
105 18 std::optional<Error> last_resolve_error;
106
107
2/2
✓ Branch 49 → 24 taken 203 times.
✓ Branch 49 → 50 taken 3 times.
206 for (std::size_t i = 0; i <= scan_limit; ++i)
108 {
109
2/2
✓ Branch 24 → 25 taken 184 times.
✓ Branch 24 → 26 taken 19 times.
203 if (search_start[i] != first)
110 {
111 188 continue;
112 }
113
114
6/6
✓ Branch 26 → 27 taken 9 times.
✓ Branch 26 → 30 taken 10 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 8 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 18 times.
19 if (prefix_len > 1 && std::memcmp(&search_start[i + 1], opcode_prefix.data() + 1, prefix_len - 1) != 0)
115 {
116 1 continue;
117 }
118
119 18 auto resolved = resolve_rip_relative(Address{&search_start[i]}, prefix_len, instruction_length);
120
2/2
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 17 times.
18 if (!resolved)
121 {
122 1 last_resolve_error = resolved.error();
123 1 continue;
124 }
125 // Plausibility is pure arithmetic, so a coincidental disp32 can still name a committed-looking
126 // address on a PAGE_NOACCESS page. One target byte is checked here rather than in
127 // resolve_rip_relative, which stays the guarded arithmetic primitive.
128
2/2
✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 47 taken 15 times.
17 if (!memory::is_readable(Region{*resolved, 1}))
129 {
130 last_resolve_error =
131 2 Error{ErrorCode::UnreadableTarget, "scan::find_and_resolve_rip_relative", resolved->raw()};
132 2 continue;
133 }
134 15 return resolved;
135 }
136
137 // A prefix was found but no occurrence resolved: surface the concrete decode failure.
138
2/2
✓ Branch 51 → 52 taken 1 time.
✓ Branch 51 → 56 taken 2 times.
3 if (last_resolve_error)
139 {
140 1 return std::unexpected(*last_resolve_error);
141 }
142 2 return std::unexpected(Error{ErrorCode::PrefixNotFound, "scan::find_and_resolve_rip_relative"});
143 }
144 } // namespace scan
145
146 namespace detail
147 {
148 24 std::optional<std::int32_t> decode_rip_displacement(
149 std::span<const std::byte> instruction_bytes,
150 std::size_t displacement_offset,
151 std::size_t instruction_length
152 ) noexcept
153 {
154
5/6
✓ Branch 3 → 4 taken 24 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 23 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 23 times.
48 if (!scan::is_valid_rip_relative_layout(displacement_offset, instruction_length) ||
155 24 instruction_bytes.size() != instruction_length)
156 {
157 1 return std::nullopt;
158 }
159
160 ZydisDecoder decoder;
161
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 17 taken 23 times.
23 if (!ZYAN_SUCCESS(ZydisDecoderInit(&decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_STACK_WIDTH_64)))
162 {
163 return std::nullopt;
164 }
165 ZydisDecodedInstruction instruction;
166 ZydisDecodedOperand operands[ZYDIS_MAX_OPERAND_COUNT];
167
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 23 times.
23 if (!ZYAN_SUCCESS(ZydisDecoderDecodeFull(
168 &decoder,
169 instruction_bytes.data(),
170 instruction_bytes.size(),
171 &instruction,
172 operands
173 )))
174 {
175 return std::nullopt;
176 }
177
178 // The resolution computes (match + instruction_length + disp), so the declared length must be the true
179 // encoded length or the next-instruction anchor drifts, and the declared field must be a disp32 at exactly
180 // the declared offset.
181
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 28 taken 22 times.
23 if (instruction.length != instruction_length)
182 {
183 1 return std::nullopt;
184 }
185
2/4
✓ Branch 28 → 29 taken 22 times.
✗ Branch 28 → 30 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 33 taken 22 times.
22 if (instruction.raw.disp.size != 32 || instruction.raw.disp.offset != displacement_offset)
186 {
187 return std::nullopt;
188 }
189 // The disp32 must belong to a RIP-relative memory operand: a same-offset disp32 on an absolute or
190 // SIB-indexed operand is not the reference the metadata resolves.
191
1/2
✓ Branch 42 → 34 taken 41 times.
✗ Branch 42 → 43 not taken.
41 for (std::size_t i = 0; i < instruction.operand_count_visible; ++i)
192 {
193 41 const ZydisDecodedOperand &operand = operands[i];
194
3/4
✓ Branch 34 → 35 taken 22 times.
✓ Branch 34 → 41 taken 19 times.
✓ Branch 35 → 36 taken 22 times.
✗ Branch 35 → 41 not taken.
41 if (operand.type == ZYDIS_OPERAND_TYPE_MEMORY && operand.mem.base == ZYDIS_REGISTER_RIP &&
195
1/2
✓ Branch 36 → 37 taken 22 times.
✗ Branch 36 → 41 not taken.
22 operand.mem.disp.has_displacement)
196 {
197 22 std::int32_t displacement = 0;
198 22 std::memcpy(&displacement, instruction_bytes.data() + displacement_offset, sizeof(displacement));
199 22 return displacement;
200 }
201 }
202 return std::nullopt;
203 }
204 } // namespace detail
205 } // namespace DetourModKit
206