./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
93 / 114
81.6%
Branches:
68 / 102
66.7%
Functions:
6 / 6
100.0%

src/internal/hook_fault_boundary.cpp
Line Branch Exec Source
1 #include "internal/hook_fault_boundary.hpp"
2
3 #include "internal/memory_guarded.hpp"
4 #include "internal/scan_pages.hpp"
5
6 #include <windows.h>
7
8 #include <array>
9 #include <cstddef>
10 #include <cstdint>
11 #include <optional>
12 #include <string_view>
13
14 namespace DetourModKit
15 {
16 21340 std::optional<detail::FunctionBound> detail::unwind_bound(std::uintptr_t target) noexcept
17 {
18 21340 DWORD64 image_base = 0;
19 21340 const PRUNTIME_FUNCTION entry = RtlLookupFunctionEntry(target, &image_base, nullptr);
20
3/4
✓ Branch 3 → 4 taken 12032 times.
✓ Branch 3 → 5 taken 9308 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 12032 times.
21340 if (entry == nullptr || image_base == 0)
21 {
22 9308 return std::nullopt;
23 }
24
25 12032 RUNTIME_FUNCTION record{};
26
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 12032 times.
12032 if (!guarded_read_bytes(reinterpret_cast<std::uintptr_t>(entry), &record, sizeof(record)))
27 {
28 ✗ return std::nullopt;
29 }
30
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12032 times.
12032 if (record.EndAddress <= record.BeginAddress)
31 {
32 ✗ return std::nullopt;
33 }
34 12032 const auto base = static_cast<std::uintptr_t>(image_base);
35
2/4
✓ Branch 11 → 12 taken 12032 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 12032 times.
12032 if (record.BeginAddress > UINTPTR_MAX - base || record.EndAddress > UINTPTR_MAX - base)
36 {
37 ✗ return std::nullopt;
38 }
39 12032 const FunctionBound bound{base + record.BeginAddress, base + record.EndAddress};
40
2/4
✓ Branch 14 → 15 taken 12032 times.
✗ Branch 14 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 12032 times.
12032 if (target < bound.lo || target >= bound.hi)
41 {
42 ✗ return std::nullopt;
43 }
44 12032 return bound;
45 }
46
47 namespace
48 {
49 /// The protection granularity of a mapped section on x86-64 Windows.
50 constexpr std::uintptr_t SECTION_PAGE_BYTES = 0x1000;
51
52 /**
53 * @brief Reports whether every page of [address, address + size) is committed image memory that is readable
54 * and either executable or writable.
55 * @details A patch window writes its page, so it leaves the page writable. A guard, no-access, read-only, or
56 * execute-only page keeps that protection until its owner changes it, so it is never a patch window.
57 */
58 318 [[nodiscard]] bool image_pages_patchable(std::uintptr_t address, std::size_t size) noexcept
59 {
60 318 constexpr DWORD PATCHABLE_PAGE_FLAGS =
61 PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY | PAGE_READWRITE | PAGE_WRITECOPY;
62
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 318 times.
318 if (size > UINTPTR_MAX - address)
63 {
64 ✗ return false;
65 }
66 318 const std::uintptr_t end = address + size;
67
2/2
✓ Branch 19 → 5 taken 318 times.
✓ Branch 19 → 20 taken 305 times.
623 for (std::uintptr_t cursor = address; cursor < end;)
68 {
69 318 MEMORY_BASIC_INFORMATION region{};
70 318 if (VirtualQuery(reinterpret_cast<LPCVOID>(cursor), &region, sizeof(region)) != sizeof(region) ||
71
4/4
✓ Branch 7 → 8 taken 317 times.
✓ Branch 7 → 11 taken 1 time.
✓ Branch 8 → 9 taken 309 times.
✓ Branch 8 → 11 taken 8 times.
318 region.State != MEM_COMMIT || region.Type != MEM_IMAGE ||
72
5/6
✓ Branch 6 → 7 taken 318 times.
✗ Branch 6 → 11 not taken.
✓ Branch 9 → 10 taken 307 times.
✓ Branch 9 → 11 taken 2 times.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 15 taken 305 times.
943 (region.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0 ||
73
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 305 times.
307 (region.Protect & PATCHABLE_PAGE_FLAGS) == 0)
74 {
75 13 return false;
76 }
77 305 const std::uintptr_t region_end =
78 305 reinterpret_cast<std::uintptr_t>(region.BaseAddress) + region.RegionSize;
79
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 305 times.
305 if (region_end <= cursor)
80 {
81 ✗ return false;
82 }
83 305 cursor = region_end;
84 }
85 305 return true;
86 }
87
88 /// Reports whether every byte of [address, address + size) lies in an executable section of one mapped image.
89 305 [[nodiscard]] bool image_sections_execute(std::uintptr_t address, std::size_t size) noexcept
90 {
91 305 MEMORY_BASIC_INFORMATION region{};
92 305 if (VirtualQuery(reinterpret_cast<LPCVOID>(address), &region, sizeof(region)) != sizeof(region) ||
93
4/8
✓ Branch 3 → 4 taken 305 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 305 times.
✗ Branch 4 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 305 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 305 times.
305 region.State != MEM_COMMIT || region.Type != MEM_IMAGE)
94 {
95 ✗ return false;
96 }
97 305 const auto base = reinterpret_cast<std::uintptr_t>(region.AllocationBase);
98 305 IMAGE_DOS_HEADER dos{};
99
3/6
✓ Branch 11 → 12 taken 305 times.
✗ Branch 11 → 14 not taken.
✓ Branch 12 → 13 taken 305 times.
✗ Branch 12 → 14 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 305 times.
610 if (!detail::guarded_read_bytes(base, &dos, sizeof(dos)) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
100
1/2
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 305 times.
305 dos.e_lfanew <= 0)
101 {
102 ✗ return false;
103 }
104 305 const std::uintptr_t nt = base + static_cast<std::uintptr_t>(dos.e_lfanew);
105 305 DWORD signature = 0;
106 305 IMAGE_FILE_HEADER file{};
107
3/6
✓ Branch 19 → 20 taken 305 times.
✗ Branch 19 → 23 not taken.
✓ Branch 20 → 21 taken 305 times.
✗ Branch 20 → 23 not taken.
✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 305 times.
610 if (!detail::guarded_read_bytes(nt, &signature, sizeof(signature)) || signature != IMAGE_NT_SIGNATURE ||
108
1/2
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 305 times.
305 !detail::guarded_read_bytes(nt + sizeof(signature), &file, sizeof(file)))
109 {
110 ✗ return false;
111 }
112 305 const std::uintptr_t table = nt + offsetof(IMAGE_NT_HEADERS64, OptionalHeader) + file.SizeOfOptionalHeader;
113 // Section headers ascend by address, so one pass covers a window that spans two adjacent sections.
114 305 std::uintptr_t cursor = address - base;
115 305 const std::uintptr_t end = cursor + size;
116
3/4
✓ Branch 39 → 40 taken 915 times.
✗ Branch 39 → 41 not taken.
✓ Branch 40 → 28 taken 618 times.
✓ Branch 40 → 41 taken 297 times.
915 for (WORD index = 0; index < file.NumberOfSections && cursor < end; ++index)
117 {
118 618 IMAGE_SECTION_HEADER section{};
119
1/2
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 618 times.
618 if (!detail::guarded_read_bytes(table + index * sizeof(section), &section, sizeof(section)))
120 {
121 8 return false;
122 }
123 618 const std::uintptr_t begin = section.VirtualAddress;
124 618 const std::uintptr_t extent =
125 618 (static_cast<std::uintptr_t>(section.Misc.VirtualSize) + SECTION_PAGE_BYTES - 1) &
126 ~(SECTION_PAGE_BYTES - 1);
127
3/4
✓ Branch 31 → 32 taken 618 times.
✗ Branch 31 → 33 not taken.
✓ Branch 32 → 33 taken 313 times.
✓ Branch 32 → 34 taken 305 times.
618 if (cursor < begin || cursor - begin >= extent)
128 {
129 313 continue;
130 }
131
2/2
✓ Branch 34 → 35 taken 8 times.
✓ Branch 34 → 36 taken 297 times.
305 if ((section.Characteristics & IMAGE_SCN_MEM_EXECUTE) == 0)
132 {
133 8 return false;
134 }
135 297 cursor = begin + extent;
136 }
137 297 return cursor >= end;
138 }
139 } // namespace
140
141 17236 detail::TargetWindowResult detail::validate_backend_steal_window(std::uintptr_t target) noexcept
142 {
143 // The backend decodes forward with no region bound, so the whole window must be executable and committed.
144
2/2
✓ Branch 3 → 4 taken 318 times.
✓ Branch 3 → 11 taken 16918 times.
17236 if (!is_executable_range(target, BACKEND_MAX_STEAL_WINDOW))
145 {
146 // Code runs only from executable pages, so a patcher restores execute access to an image section that
147 // carries it before that code runs again.
148
2/2
✓ Branch 7 → 8 taken 297 times.
✓ Branch 7 → 9 taken 8 times.
623 const TargetWindowVerdict verdict = image_pages_patchable(target, BACKEND_MAX_STEAL_WINDOW) &&
149 305 image_sections_execute(target, BACKEND_MAX_STEAL_WINDOW)
150
2/2
✓ Branch 5 → 6 taken 305 times.
✓ Branch 5 → 9 taken 13 times.
318 ? TargetWindowVerdict::PatchInFlight
151 318 : TargetWindowVerdict::NotExecutable;
152 318 return TargetWindowResult{verdict, target};
153 }
154
155 // Only a guarded touch finds a guard page or a backing store that fails to fault in.
156 16918 std::array<std::uint8_t, BACKEND_MAX_STEAL_WINDOW> window{};
157 16918 volatile std::uintptr_t fault_address = target;
158
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 16918 times.
33836 if (!guarded_read_bytes(target, window.data(), window.size(), &fault_address))
159 {
160 ✗ return TargetWindowResult{TargetWindowVerdict::Unreadable, fault_address};
161 }
162
163 // The fallback can write more bytes than the near jump, and the backend picks the form after this check.
164 16918 const std::optional<FunctionBound> bound = unwind_bound(target);
165
6/6
✓ Branch 20 → 21 taken 7610 times.
✓ Branch 20 → 24 taken 9308 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 7608 times.
✓ Branch 25 → 26 taken 2 times.
✓ Branch 25 → 28 taken 16916 times.
16918 if (bound && target + BACKEND_FALLBACK_MIN_PATCH > bound->hi)
166 {
167 2 return TargetWindowResult{TargetWindowVerdict::BoundOverrun, bound->hi};
168 }
169
170 16916 return TargetWindowResult{TargetWindowVerdict::Ok, target};
171 }
172
173 22 std::string_view detail::target_window_description(TargetWindowVerdict verdict) noexcept
174 {
175
2/6
✓ Branch 2 → 3 taken 21 times.
✗ Branch 2 → 4 not taken.
✓ Branch 2 → 5 taken 1 time.
✗ Branch 2 → 6 not taken.
✗ Branch 2 → 7 not taken.
✗ Branch 2 → 8 not taken.
22 switch (verdict)
176 {
177 21 case TargetWindowVerdict::NotExecutable:
178 21 return "the bytes the backend decodes are not all executable committed memory";
179 ✗ case TargetWindowVerdict::Unreadable:
180 ✗ return "the bytes the backend decodes are not all readable";
181 1 case TargetWindowVerdict::BoundOverrun:
182 1 return "the target function is shorter than the largest patch minimum the backend may select";
183 ✗ case TargetWindowVerdict::PatchInFlight:
184 ✗ return "a patch window outside the process coordinator holds the target page without execute access";
185 ✗ case TargetWindowVerdict::Ok:
186 ✗ return "the target is hookable";
187 }
188 ✗ return "the target is hookable";
189 }
190
191 295 detail::ObjectWordResult detail::validate_vmt_object_word(std::uintptr_t object) noexcept
192 {
193 // Readability first, and by touching rather than querying: VirtualQuery cannot report that a guard page will
194 // trap the first access.
195 295 volatile std::uintptr_t fault_address = object;
196 295 std::uintptr_t vptr = 0;
197
2/2
✓ Branch 3 → 4 taken 24 times.
✓ Branch 3 → 5 taken 271 times.
295 if (!guarded_read_bytes(object, &vptr, sizeof(vptr), &fault_address))
198 {
199 24 return ObjectWordResult{ObjectWordVerdict::Unreadable, fault_address, 0};
200 }
201
202 // Readable does not imply writable. There is no non-mutating write probe, so ask the OS and let the later
203 // fault-contained publication compare-exchange close a displacement/protection/unmap race.
204 271 MEMORY_BASIC_INFORMATION info{};
205
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 271 times.
271 if (VirtualQuery(reinterpret_cast<LPCVOID>(object), &info, sizeof(info)) != sizeof(info))
206 {
207 ✗ return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr};
208 }
209
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 271 times.
271 if (info.State != MEM_COMMIT)
210 {
211 ✗ return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr};
212 }
213 // A guard armed before the read already failed it closed above. This catches one armed since: PAGE_GUARD traps
214 // the first access to a page whose protection otherwise reads as writable, so the word is not publishable even
215 // though its protection bits say it is.
216
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 271 times.
271 if ((info.Protect & PAGE_GUARD) != 0)
217 {
218 ✗ return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr};
219 }
220 271 constexpr DWORD WRITABLE_PROTECTIONS =
221 PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
222
2/2
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 14 taken 259 times.
271 if ((info.Protect & WRITABLE_PROTECTIONS) == 0)
223 {
224 12 return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr};
225 }
226
227 // The word must not straddle two regions with different protections: VirtualQuery reports the region containing
228 // the first byte, so a pointer-sized word ending in a read-only neighbour would pass on its first byte alone.
229 // VirtualQuery places @p object inside the region it reports, so measuring the remainder as an offset from the
230 // region base keeps this arithmetic wrap-free without trusting the reported bounds not to overflow.
231 259 const std::uintptr_t offset_in_region = object - reinterpret_cast<std::uintptr_t>(info.BaseAddress);
232
1/2
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 259 times.
259 if (static_cast<std::uintptr_t>(info.RegionSize) - offset_in_region < sizeof(std::uintptr_t))
233 {
234 ✗ return ObjectWordResult{ObjectWordVerdict::NotWritable, object, vptr};
235 }
236
237 259 return ObjectWordResult{ObjectWordVerdict::Ok, object, vptr};
238 }
239 } // namespace DetourModKit
240