./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
155 / 172
90.1%
Branches:
117 / 150
78.0%
Functions:
10 / 10
100.0%

src/scan_export.cpp
Line Branch Exec Source
1 /**
2 * @file scan_export.cpp
3 * @brief The named-export resolver: maps a module + export name to an address by walking the PE Export Address Table.
4 * @details The walk parses the mapped image's own IMAGE_EXPORT_DIRECTORY. It never calls GetProcAddress, enters the
5 * loader, or triggers DllMain. It bound-checks every RVA and reads through the guarded path. scan.hpp's
6 * resolve_export owns the public contract. A forwarder is classified by the loader's own range check: a
7 * function RVA inside the export directory's [VirtualAddress, VirtualAddress + Size) window. The walk lives
8 * in `detail::resolve_export_with_provenance`. The public entry point is a thin wrapper because only the walk
9 * knows the slot and RVA that resolved a name.
10 */
11
12 #include "DetourModKit/scan.hpp"
13
14 #include "internal/export_resolution.hpp"
15 #include "internal/image_identity.hpp"
16 #include "internal/memory_guarded.hpp"
17 #include "internal/memory_representation_win32.hpp"
18
19 #include <windows.h>
20
21 #include <cstddef>
22 #include <cstdint>
23 #include <limits>
24 #include <optional>
25 #include <string_view>
26
27 namespace DetourModKit
28 {
29 namespace detail
30 {
31 namespace
32 {
33 // Upper bound on the export name / function counts the walk will iterate. No real module approaches this;
34 // the cap exists only so a corrupt or hostile IMAGE_EXPORT_DIRECTORY (whose count fields are entirely
35 // attacker-controlled) cannot steer the scan into an unbounded loop. It is far below any value that can
36 // overflow the byte-size arithmetic on the parallel arrays below.
37 constexpr std::uint32_t MAX_EXPORT_ENTRIES = 1U << 20;
38
39 // Reports whether the half-open byte range [start, start + bytes) lies wholly inside the module image, with
40 // an explicit wrap guard so a hostile RVA/size that overflows the address space is rejected rather than
41 // aliasing a low address. A zero-length range is vacuously contained.
42 [[nodiscard]] bool
43 784 region_in_span(const ModuleSpan &span, std::uintptr_t start, std::uintptr_t bytes) noexcept
44 {
45
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 784 times.
784 if (bytes == 0)
46 {
47 ✗ return true;
48 }
49
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 784 times.
784 if (start > std::numeric_limits<std::uintptr_t>::max() - (bytes - 1))
50 {
51 // Address-space wrap: a corrupt RVA or size pushed the range past the top of memory.
52 ✗ return false;
53 }
54 784 const std::uintptr_t last = start + (bytes - 1);
55
4/4
✓ Branch 8 → 9 taken 781 times.
✓ Branch 8 → 12 taken 3 times.
✓ Branch 10 → 11 taken 777 times.
✓ Branch 10 → 12 taken 4 times.
784 return span.contains(start) && span.contains(last);
56 }
57
58 // Converts an image-relative address to an absolute range only when the addition cannot wrap and the
59 // entire range lies inside the mapped image. Keeping this arithmetic in one helper prevents a hostile RVA
60 // from wrapping below the module base before a later span check sees it.
61 [[nodiscard]] std::optional<std::uintptr_t>
62 659 checked_rva(const ModuleSpan &span, std::uint32_t rva, std::uintptr_t bytes) noexcept
63 {
64
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 659 times.
659 if (static_cast<std::uintptr_t>(rva) > std::numeric_limits<std::uintptr_t>::max() - span.base)
65 {
66 ✗ return std::nullopt;
67 }
68 659 const std::uintptr_t address = span.base + static_cast<std::uintptr_t>(rva);
69
2/2
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 653 times.
659 if (!region_in_span(span, address, bytes))
70 {
71 6 return std::nullopt;
72 }
73 653 return address;
74 }
75
76 // Byte-exact, case-sensitive compare of the NUL-terminated export name at name_addr against target, with
77 // every byte bound-checked and fault-guarded so a truncated name table fails closed. PE export names are
78 // case-sensitive (GetProcAddress matches them exactly), so the anchor must be too. Comparing target.size()
79 // bytes AND the following terminator rejects both a shorter name (its NUL lands early) and a longer one
80 // (its byte at target.size() is not the terminator), so "malloc" never matches "malloc_base".
81 [[nodiscard]] std::optional<bool>
82 312 export_name_matches(const ModuleSpan &span, std::uintptr_t name_addr, std::string_view target) noexcept
83 {
84
2/2
✓ Branch 28 → 3 taken 2552 times.
✓ Branch 28 → 29 taken 56 times.
2608 for (std::size_t index = 0; index < target.size(); ++index)
85 {
86
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 8 taken 2552 times.
2552 if (index > std::numeric_limits<std::uintptr_t>::max() - name_addr)
87 {
88 ✗ return std::nullopt;
89 }
90 2552 const std::uintptr_t byte_addr = name_addr + index;
91
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 2552 times.
2552 if (!span.contains(byte_addr))
92 {
93 ✗ return std::nullopt;
94 }
95 2552 const std::optional<char> byte = guarded_read<char>(byte_addr);
96
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2552 times.
2552 if (!byte)
97 {
98 ✗ return std::nullopt;
99 }
100
2/2
✓ Branch 21 → 22 taken 256 times.
✓ Branch 21 → 25 taken 2296 times.
2552 if (*byte != target[index])
101 {
102 256 return false;
103 }
104 }
105
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 56 times.
56 if (target.size() > std::numeric_limits<std::uintptr_t>::max() - name_addr)
106 {
107 ✗ return std::nullopt;
108 }
109 56 const std::uintptr_t terminator_addr = name_addr + target.size();
110
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 41 taken 56 times.
56 if (!span.contains(terminator_addr))
111 {
112 ✗ return std::nullopt;
113 }
114 56 const std::optional<char> terminator = guarded_read<char>(terminator_addr);
115
2/2
✓ Branch 43 → 44 taken 1 time.
✓ Branch 43 → 47 taken 55 times.
56 if (!terminator)
116 {
117 1 return std::nullopt;
118 }
119 55 return *terminator == '\0';
120 }
121
122 /**
123 * @brief Reads and checks the DOS and NT headers inside @p supplied_span.
124 * @details The checks cover both signatures, the PE32+ magic, and the export data-directory entry. An
125 * invalid span or a bad header returns InvalidRange, and a missing export data-directory entry
126 * returns ExportNotFound. On success the helper writes the NT header address to @p nt_address.
127 */
128 [[nodiscard]] Result<IMAGE_NT_HEADERS64>
129 66 read_nt_headers(const ModuleSpan &supplied_span, std::uintptr_t &nt_address) noexcept
130 {
131
2/2
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 7 taken 64 times.
66 if (!supplied_span.valid())
132 {
133 2 return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
134 }
135 64 const std::uintptr_t base = supplied_span.base;
136
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 12 taken 63 times.
64 if (!region_in_span(supplied_span, base, sizeof(IMAGE_DOS_HEADER)))
137 {
138 1 return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
139 }
140
141 // The library is x64-only, and an #error arch gate enforces it. The explicit PE32+ magic check on
142 // IMAGE_NT_HEADERS64 rejects a wrong-bitness image and is not a portability branch.
143 //
144 // This parse stays separate from the RTTI image walk because the error models differ. Here a bad header
145 // fails closed with an ErrorCode. The RTTI image walk returns a range count, and its caller falls back
146 // to the whole module. A shared helper couples the two subsystems and removes no real duplication.
147 63 const std::optional<IMAGE_DOS_HEADER> dos = guarded_read<IMAGE_DOS_HEADER>(base);
148
3/6
✓ Branch 14 → 15 taken 63 times.
✗ Branch 14 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 63 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 23 taken 63 times.
63 if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
149 {
150 ✗ return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
151 }
152
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 28 taken 62 times.
63 if (dos->e_lfanew < 0)
153 {
154 1 return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
155 }
156 const std::optional<std::uintptr_t> nt_addr =
157 62 checked_rva(supplied_span, static_cast<std::uint32_t>(dos->e_lfanew), sizeof(IMAGE_NT_HEADERS64));
158
1/2
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 62 times.
62 if (!nt_addr)
159 {
160 ✗ return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
161 }
162 62 const std::optional<IMAGE_NT_HEADERS64> nt = guarded_read<IMAGE_NT_HEADERS64>(*nt_addr);
163
3/6
✓ Branch 38 → 39 taken 62 times.
✗ Branch 38 → 43 not taken.
✓ Branch 40 → 41 taken 62 times.
✗ Branch 40 → 43 not taken.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 49 taken 62 times.
124 if (!nt || nt->Signature != IMAGE_NT_SIGNATURE ||
164
1/2
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 62 times.
62 nt->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
165 {
166 ✗ return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
167 }
168
169 62 constexpr std::size_t export_directory_end =
170 offsetof(IMAGE_OPTIONAL_HEADER64, DataDirectory) +
171 (IMAGE_DIRECTORY_ENTRY_EXPORT + 1) * sizeof(IMAGE_DATA_DIRECTORY);
172
4/4
✓ Branch 50 → 51 taken 61 times.
✓ Branch 50 → 53 taken 1 time.
✓ Branch 55 → 56 taken 1 time.
✓ Branch 55 → 59 taken 61 times.
123 if (nt->FileHeader.SizeOfOptionalHeader < export_directory_end ||
173
1/2
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 61 times.
61 nt->OptionalHeader.NumberOfRvaAndSizes <= IMAGE_DIRECTORY_ENTRY_EXPORT)
174 {
175 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
176 }
177 61 nt_address = *nt_addr;
178 61 return *nt;
179 }
180
181 /**
182 * @brief Returns @p supplied_span clipped to the image size that the NT headers declare.
183 * @details The helper returns InvalidRange when SizeOfImage is zero, when the declared image end wraps, or
184 * when the NT headers fall outside the clipped span. @p supplied_span is only an outer safety
185 * boundary. The SizeOfImage that @p nt declares is the authoritative inner boundary for every EAT
186 * read. Stale or oversized mapped backing therefore cannot make a declared out-of-image RVA appear
187 * valid.
188 */
189 61 [[nodiscard]] Result<ModuleSpan> clip_span_to_declared_image(
190 const ModuleSpan &supplied_span,
191 const IMAGE_NT_HEADERS64 &nt,
192 std::uintptr_t nt_address
193 ) noexcept
194 {
195 61 const std::uintptr_t base = supplied_span.base;
196 61 const std::uintptr_t image_size = nt.OptionalHeader.SizeOfImage;
197
3/6
✓ Branch 2 → 3 taken 61 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 61 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 11 taken 61 times.
61 if (image_size == 0 || image_size > std::numeric_limits<std::uintptr_t>::max() - base)
198 {
199 ✗ return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
200 }
201 61 const std::uintptr_t declared_end = base + image_size;
202
2/2
✓ Branch 11 → 12 taken 59 times.
✓ Branch 11 → 13 taken 2 times.
61 const ModuleSpan span{base, declared_end < supplied_span.end ? declared_end : supplied_span.end};
203
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 61 times.
61 if (!region_in_span(span, nt_address, sizeof(IMAGE_NT_HEADERS64)))
204 {
205 ✗ return std::unexpected(Error{ErrorCode::InvalidRange, "scan::resolve_export"});
206 }
207 61 return span;
208 }
209
210 /**
211 * @brief Reads the IMAGE_EXPORT_DIRECTORY that @p dir locates inside @p span.
212 * @details The helper returns ExportNotFound when @p dir is empty, too small, outside @p span, or
213 * unreadable.
214 */
215 [[nodiscard]] Result<IMAGE_EXPORT_DIRECTORY>
216 61 read_export_directory(const ModuleSpan &span, const IMAGE_DATA_DIRECTORY &dir) noexcept
217 {
218 // A module with no exports leaves dir zeroed. That is not a fault: the module has no name for this
219 // backend to resolve.
220
2/4
✓ Branch 2 → 3 taken 61 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 61 times.
61 if (dir.VirtualAddress == 0 || dir.Size == 0)
221 {
222 ✗ return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
223 }
224 61 const std::optional<std::uintptr_t> export_va = checked_rva(span, dir.VirtualAddress, dir.Size);
225
6/6
✓ Branch 9 → 10 taken 60 times.
✓ Branch 9 → 11 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 59 times.
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 17 taken 59 times.
61 if (!export_va || dir.Size < sizeof(IMAGE_EXPORT_DIRECTORY))
226 {
227 2 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
228 }
229 59 const std::optional<IMAGE_EXPORT_DIRECTORY> exports = guarded_read<IMAGE_EXPORT_DIRECTORY>(*export_va);
230
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 59 times.
59 if (!exports)
231 {
232 ✗ return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
233 }
234 59 return *exports;
235 }
236
237 /**
238 * @brief Maps the matched name @p index through the ordinal and function arrays to its physical slot.
239 * @details A forwarder returns ExportForwarded. A bad ordinal, an unreadable or empty slot, or a target
240 * outside @p span returns ExportNotFound.
241 */
242 52 [[nodiscard]] Result<ExportResolution> resolve_export_slot(
243 const ModuleSpan &span,
244 const IMAGE_DATA_DIRECTORY &dir,
245 std::uintptr_t ordinals_va,
246 std::uintptr_t funcs_va,
247 std::uint32_t func_count,
248 std::uint32_t index
249 ) noexcept
250 {
251 // The name index maps to the function index AddressOfNameOrdinals[index]. That WORD is a 0-based index
252 // into AddressOfFunctions. The directory's Base biases only the ordinal exposed to callers, not this
253 // array index. The helper uses the WORD as-is after the bounds check.
254 52 const std::optional<std::uint16_t> ordinal = guarded_read<std::uint16_t>(
255 52 ordinals_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint16_t)
256 );
257
5/6
✓ Branch 4 → 5 taken 52 times.
✗ Branch 4 → 7 not taken.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 51 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 13 taken 51 times.
52 if (!ordinal || *ordinal >= func_count)
258 {
259 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
260 }
261 51 const std::optional<std::uint32_t> func_rva = guarded_read<std::uint32_t>(
262 51 funcs_va + static_cast<std::uintptr_t>(*ordinal) * sizeof(std::uint32_t)
263 );
264
5/6
✓ Branch 16 → 17 taken 51 times.
✗ Branch 16 → 19 not taken.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 50 times.
✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 25 taken 50 times.
51 if (!func_rva || *func_rva == 0)
265 {
266 // A zero RVA in the functions array marks an unused or absent slot, not a resolvable address.
267 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
268 }
269
270 // A function RVA inside the export directory is a forwarder. The DWORD addresses an ASCII
271 // "TargetDll.TargetFunc" string, not code in this image. Only the loader can resolve it. The helper
272 // fails closed so that a declared forwarder never becomes a code anchor to hook or read through.
273 50 const std::uint64_t forwarder_begin = dir.VirtualAddress;
274 50 const std::uint64_t forwarder_end = forwarder_begin + dir.Size;
275
4/4
✓ Branch 26 → 27 taken 14 times.
✓ Branch 26 → 30 taken 36 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 35 taken 49 times.
64 if (static_cast<std::uint64_t>(*func_rva) >= forwarder_begin &&
276
2/2
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 13 times.
14 static_cast<std::uint64_t>(*func_rva) < forwarder_end)
277 {
278 1 return std::unexpected(Error{ErrorCode::ExportForwarded, "scan::resolve_export"});
279 }
280
281 49 const std::optional<std::uintptr_t> target = checked_rva(span, *func_rva, 1);
282
2/2
✓ Branch 38 → 39 taken 2 times.
✓ Branch 38 → 42 taken 47 times.
49 if (!target)
283 {
284 // An RVA outside the mapped image marks a corrupt entry, not a usable code address.
285 2 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
286 }
287 94 return ExportResolution{
288 47 .module_base = span.base,
289 47 .function_index = *ordinal,
290 47 .function_rva = *func_rva,
291 47 .target = Address{*target},
292 47 };
293 }
294 } // namespace
295
296 Result<Address>
297 68 resolve_export_with_provenance(std::string_view export_name, Region module, ExportResolution &out) noexcept
298 {
299 68 out = ExportResolution{};
300
301 // PE names are non-empty NUL-terminated byte strings. The check rejects an embedded terminator up front so
302 // that a query cannot match zero padding after the real name's terminator.
303
6/6
✓ Branch 4 → 5 taken 67 times.
✓ Branch 4 → 7 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 66 times.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 66 times.
68 if (export_name.empty() || export_name.find('\0') != std::string_view::npos)
304 {
305 2 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
306 }
307
308 66 const ModuleSpan supplied_span = module_span(module);
309 66 std::uintptr_t nt_address = 0;
310 66 const Result<IMAGE_NT_HEADERS64> nt = read_nt_headers(supplied_span, nt_address);
311
2/2
✓ Branch 16 → 17 taken 5 times.
✓ Branch 16 → 21 taken 61 times.
66 if (!nt)
312 {
313 5 return std::unexpected(nt.error());
314 }
315 61 const Result<ModuleSpan> declared_span = clip_span_to_declared_image(supplied_span, *nt, nt_address);
316
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 29 taken 61 times.
61 if (!declared_span)
317 {
318 ✗ return std::unexpected(declared_span.error());
319 }
320 61 const ModuleSpan &span = *declared_span;
321
322 61 const IMAGE_DATA_DIRECTORY dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
323 61 const Result<IMAGE_EXPORT_DIRECTORY> exports = read_export_directory(span, dir);
324
2/2
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 38 taken 59 times.
61 if (!exports)
325 {
326 2 return std::unexpected(exports.error());
327 }
328
329 59 const std::uint32_t name_count = exports->NumberOfNames;
330 59 const std::uint32_t func_count = exports->NumberOfFunctions;
331
5/8
✓ Branch 40 → 41 taken 58 times.
✓ Branch 40 → 44 taken 1 time.
✓ Branch 41 → 42 taken 58 times.
✗ Branch 41 → 44 not taken.
✓ Branch 42 → 43 taken 58 times.
✗ Branch 42 → 44 not taken.
✗ Branch 43 → 44 not taken.
✓ Branch 43 → 47 taken 58 times.
59 if (name_count == 0 || func_count == 0 || name_count > MAX_EXPORT_ENTRIES ||
332 func_count > MAX_EXPORT_ENTRIES)
333 {
334 // No name table to match against, or an implausibly large count from a corrupt/hostile directory.
335 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
336 }
337
338 116 const std::optional<std::uintptr_t> names_va = checked_rva(
339 span,
340 58 exports->AddressOfNames,
341 58 static_cast<std::uintptr_t>(name_count) * sizeof(std::uint32_t)
342 );
343 116 const std::optional<std::uintptr_t> ordinals_va = checked_rva(
344 span,
345 58 exports->AddressOfNameOrdinals,
346 58 static_cast<std::uintptr_t>(name_count) * sizeof(std::uint16_t)
347 );
348 116 const std::optional<std::uintptr_t> funcs_va = checked_rva(
349 span,
350 58 exports->AddressOfFunctions,
351 58 static_cast<std::uintptr_t>(func_count) * sizeof(std::uint32_t)
352 );
353
7/8
✓ Branch 54 → 55 taken 57 times.
✓ Branch 54 → 59 taken 1 time.
✓ Branch 56 → 57 taken 57 times.
✗ Branch 56 → 59 not taken.
✓ Branch 58 → 59 taken 1 time.
✓ Branch 58 → 60 taken 56 times.
✓ Branch 61 → 62 taken 2 times.
✓ Branch 61 → 65 taken 56 times.
58 if (!names_va || !ordinals_va || !funcs_va)
354 {
355 2 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
356 }
357
358 // Linear scan the parallel name / ordinal arrays. The name table is spec-sorted for GetProcAddress's binary
359 // search, but a linear scan is chosen deliberately: it is O(exports) at setup time (never a hot path) and
360 // stays correct on the unsorted tables some packers emit. A duplicate matching name is rejected as an
361 // ambiguous, malformed table rather than letting array order choose a target.
362 56 std::optional<Address> match;
363 56 ExportResolution resolution;
364
2/2
✓ Branch 110 → 66 taken 313 times.
✓ Branch 110 → 111 taken 48 times.
361 for (std::uint32_t index = 0; index < name_count; ++index)
365 {
366 const std::optional<std::uint32_t> name_rva =
367 313 guarded_read<std::uint32_t>(*names_va + static_cast<std::uintptr_t>(index) * sizeof(std::uint32_t));
368
1/2
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 73 taken 313 times.
313 if (!name_rva)
369 {
370 ✗ return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
371 }
372 313 const std::optional<std::uintptr_t> name_va = checked_rva(span, *name_rva, 1);
373
2/2
✓ Branch 76 → 77 taken 1 time.
✓ Branch 76 → 80 taken 312 times.
313 if (!name_va)
374 {
375 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
376 }
377 312 const std::optional<bool> name_matches = export_name_matches(span, *name_va, export_name);
378
2/2
✓ Branch 83 → 84 taken 1 time.
✓ Branch 83 → 87 taken 311 times.
312 if (!name_matches)
379 {
380 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
381 }
382
2/2
✓ Branch 88 → 89 taken 258 times.
✓ Branch 88 → 90 taken 53 times.
311 if (!*name_matches)
383 {
384 258 continue;
385 }
386
2/2
✓ Branch 91 → 92 taken 1 time.
✓ Branch 91 → 95 taken 52 times.
53 if (match)
387 {
388 1 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
389 }
390
391 const Result<ExportResolution> slot =
392 52 resolve_export_slot(span, dir, *ordinals_va, *funcs_va, func_count, index);
393
2/2
✓ Branch 99 → 100 taken 5 times.
✓ Branch 99 → 104 taken 47 times.
52 if (!slot)
394 {
395 5 return std::unexpected(slot.error());
396 }
397 47 match = slot->target;
398 // The slot this name mapped to and the value read out of it, so a caller weighing two names can tell
399 // one physical entry point from two. A duplicate name aborts above, so this is written at most once.
400 47 resolution = *slot;
401 }
402
403
2/2
✓ Branch 112 → 113 taken 44 times.
✓ Branch 112 → 115 taken 4 times.
48 if (match)
404 {
405 44 out = resolution;
406 44 return *match;
407 }
408 4 return std::unexpected(Error{ErrorCode::ExportNotFound, "scan::resolve_export"});
409 }
410
411 } // namespace detail
412
413 namespace scan
414 {
415 22 Result<Address> resolve_export(std::string_view export_name, Region module) noexcept
416 {
417 22 detail::ExportResolution ignored;
418 22 return detail::resolve_export_with_provenance(export_name, module, ignored);
419 }
420
421 568 ImageIdentity image_identity(Region range) noexcept
422 {
423
5/6
✓ Branch 3 → 4 taken 567 times.
✓ Branch 3 → 5 taken 1 time.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 567 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 567 times.
568 if (range.base.raw() == 0 || range.size == 0)
424 {
425 1 return ImageIdentity{};
426 }
427
428 // The supplied range may be a narrow scan scope or a stale cached extent after a same-base remap, so the
429 // authoritative extent is re-read from the PE at the base. The base itself is never folded in, which keeps
430 // a persisted baseline ASLR-insensitive; the RTTI generation token adds the base separately because it
431 // must also separate two modules.
432 567 const detail::ImageIdentityFields fields = detail::image_identity_at(range.base.raw());
433
2/2
✓ Branch 11 → 12 taken 11 times.
✓ Branch 11 → 13 taken 556 times.
567 if (!fields.valid)
434 {
435 11 return ImageIdentity{};
436 }
437 return ImageIdentity{
438 556 .timestamp = fields.timestamp,
439 556 .size_of_image = fields.size_of_image,
440 556 .section_digest = fields.section_digest,
441 556 };
442 }
443 } // namespace scan
444 } // namespace DetourModKit
445