GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 92.0% 104 / 0 / 113
Functions: 100.0% 12 / 0 / 12
Branches: 72.2% 65 / 0 / 90

src/memory_module.cpp
Line Branch Exec Source
1 /**
2 * @file memory_module.cpp
3 * @brief Provides module presence and address ownership queries.
4 */
5
6 #include "DetourModKit/memory.hpp"
7 #include "internal/memory_representation_win32.hpp"
8 #include "internal/module_name.hpp"
9 #include "platform.hpp"
10
11 #include <windows.h>
12 #include <tlhelp32.h>
13
14 #include <climits>
15 #include <cstddef>
16 #include <cstdint>
17 #include <cwchar>
18 #include <string>
19 #include <string_view>
20
21 namespace DetourModKit
22 {
23 namespace detail
24 {
25 #if defined(DMK_ENABLE_TEST_SEAMS)
26 void (*g_module_loaded_after_reference_test_hook)() noexcept = nullptr;
27 HMODULE g_module_loaded_reference_candidate_test_override = nullptr;
28 #endif
29
30 // The canonical module-base -> Region resolver (declared in internal/memory_guarded.hpp). Both region.cpp's
31 // Region factories (host/module_named/own) and memory::module_of route through this one definition, so the
32 // PE-header walk (DOS magic, a bounded e_lfanew, the NT signature, and OptionalHeader.SizeOfImage) has a
33 // single source of truth. Reads go through the guarded engine (memory::read), so a partially-mapped or corrupt
34 // image fails closed to an empty Region rather than faulting the host. The walk is repeated per call rather
35 // than memoized per handle: an HMODULE IS its image base and Windows reuses it after an unload, so any cached
36 // span is a claim about an identity the loader can reassign, and a completed same-base replacement carrying a
37 // different SizeOfImage would keep serving the previous image's extent from this public query.
38 748 Region module_image_region(Address module_base) noexcept
39 {
40
2/2
✓ Branch 3 → 4 taken 9 times.
✓ Branch 3 → 6 taken 739 times.
748 if (!module_base)
41 {
42 9 return Region{};
43 }
44
45 739 const auto dos = memory::read<IMAGE_DOS_HEADER>(module_base);
46
5/6
✓ Branch 8 → 9 taken 739 times.
✗ Branch 8 → 11 not taken.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 12 taken 736 times.
✓ Branch 13 → 14 taken 3 times.
✓ Branch 13 → 16 taken 736 times.
739 if (!dos || dos->e_magic != IMAGE_DOS_SIGNATURE)
47 {
48 3 return Region{};
49 }
50
51 // Bound e_lfanew. A genuine PE places its NT headers within the first few KiB; anything beyond a generous
52 // 1 MiB cap is corrupt or hostile.
53
3/6
✓ Branch 17 → 18 taken 736 times.
✗ Branch 17 → 20 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 736 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 25 taken 736 times.
736 if (dos->e_lfanew <= 0 || static_cast<std::uint32_t>(dos->e_lfanew) > 0x100000U)
54 {
55 return Region{};
56 }
57
58 736 const auto nt = memory::read<IMAGE_NT_HEADERS>(module_base.offset(dos->e_lfanew));
59
3/6
✓ Branch 29 → 30 taken 736 times.
✗ Branch 29 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 736 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 736 times.
736 if (!nt || nt->Signature != IMAGE_NT_SIGNATURE)
60 {
61 return Region{};
62 }
63
64 736 const std::size_t size_of_image = static_cast<std::size_t>(nt->OptionalHeader.SizeOfImage);
65
1/2
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 41 taken 736 times.
736 if (size_of_image == 0)
66 {
67 return Region{};
68 }
69
70 736 return Region{module_base, size_of_image};
71 }
72
73 1347 Region live_module_region(Address address) noexcept
74 {
75
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 6 taken 1346 times.
1347 if (!address)
76 1 return Region{};
77
78 1346 HMODULE owning_module = nullptr;
79 1346 if (!::GetModuleHandleExW(
80 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
81 address.as<LPCWSTR>(),
82 &owning_module
83
4/4
✓ Branch 8 → 9 taken 415 times.
✓ Branch 8 → 10 taken 930 times.
✓ Branch 12 → 13 taken 930 times.
✓ Branch 12 → 15 taken 415 times.
1760 ) ||
84
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 415 times.
415 owning_module == nullptr)
85 930 return Region{};
86 415 return module_image_region(Address{owning_module});
87 }
88 } // namespace detail
89
90 namespace
91 {
92 inline constexpr std::size_t MAX_MODULE_PATH_CHARS = 32768;
93
94 class ScopedModuleReference
95 {
96 public:
97 10 explicit ScopedModuleReference(HMODULE module) noexcept : m_module{module} {}
98 10 ~ScopedModuleReference() noexcept
99 {
100
1/2
✓ Branch 2 → 3 taken 10 times.
✗ Branch 2 → 4 not taken.
10 if (m_module != nullptr)
101 {
102 10 (void)::FreeLibrary(m_module);
103 }
104 10 }
105
106 ScopedModuleReference(const ScopedModuleReference &) = delete;
107 ScopedModuleReference &operator=(const ScopedModuleReference &) = delete;
108 ScopedModuleReference(ScopedModuleReference &&) = delete;
109 ScopedModuleReference &operator=(ScopedModuleReference &&) = delete;
110
111 private:
112 HMODULE m_module;
113 };
114
115 class ScopedSnapshot
116 {
117 public:
118 3 explicit ScopedSnapshot(HANDLE snapshot) noexcept : m_snapshot{snapshot} {}
119 3 ~ScopedSnapshot() noexcept { (void)::CloseHandle(m_snapshot); }
120
121 ScopedSnapshot(const ScopedSnapshot &) = delete;
122 ScopedSnapshot &operator=(const ScopedSnapshot &) = delete;
123 ScopedSnapshot(ScopedSnapshot &&) = delete;
124 ScopedSnapshot &operator=(ScopedSnapshot &&) = delete;
125
126 private:
127 HANDLE m_snapshot;
128 };
129
130 10 [[nodiscard]] bool module_basename_matches(HMODULE module, std::wstring_view expected) noexcept
131 {
132 try
133 {
134 10 std::wstring module_path;
135 10 DWORD length = 0;
136 10 std::size_t capacity = MAX_PATH;
137 while (true)
138 {
139
2/2
✓ Branch 4 → 5 taken 10 times.
✓ Branch 4 → 27 taken 2 times.
12 module_path.resize(capacity);
140
1/2
✓ Branch 6 → 7 taken 10 times.
✗ Branch 6 → 27 not taken.
10 length = ::GetModuleFileNameW(module, module_path.data(), static_cast<DWORD>(capacity));
141
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 10 times.
10 if (length == 0)
142 {
143 return false;
144 }
145
2/2
✓ Branch 9 → 10 taken 8 times.
✓ Branch 9 → 11 taken 2 times.
10 if (length < capacity)
146 {
147 8 break;
148 }
149
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
2 if (capacity >= MAX_MODULE_PATH_CHARS)
150 {
151 return false;
152 }
153
1/2
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 15 not taken.
2 capacity = (capacity <= MAX_MODULE_PATH_CHARS / 2) ? capacity * 2 : MAX_MODULE_PATH_CHARS;
154 }
155
156 8 const std::wstring_view path_view{module_path.data(), length};
157 8 const std::size_t separator = path_view.find_last_of(L"\\/");
158 const std::wstring_view actual =
159
2/4
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 8 times.
✓ Branch 22 → 23 taken 8 times.
✗ Branch 22 → 27 not taken.
8 (separator == std::wstring_view::npos) ? path_view : path_view.substr(separator + 1);
160 8 return actual == expected;
161 10 }
162 2 catch (...)
163 {
164 2 return false;
165 2 }
166 }
167
168 77 [[nodiscard]] bool module_name_matches_insensitive(const wchar_t *actual, std::wstring_view expected) noexcept
169 {
170
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 77 times.
77 if (expected.size() > static_cast<std::size_t>(INT_MAX))
171 {
172 return false;
173 }
174 77 return ::CompareStringOrdinal(actual, -1, expected.data(), static_cast<int>(expected.size()), TRUE) ==
175 77 CSTR_EQUAL;
176 }
177
178 4 [[nodiscard]] HANDLE create_module_snapshot() noexcept
179 {
180 4 constexpr int max_attempts = 3;
181
1/2
✓ Branch 11 → 3 taken 4 times.
✗ Branch 11 → 12 not taken.
4 for (int attempt = 0; attempt < max_attempts; ++attempt)
182 {
183 const HANDLE snapshot =
184 4 ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, ::GetCurrentProcessId());
185
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 7 taken 1 time.
4 if (snapshot != INVALID_HANDLE_VALUE)
186 {
187 3 return snapshot;
188 }
189
1/2
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
1 if (::GetLastError() != ERROR_BAD_LENGTH)
190 {
191 1 break;
192 }
193 }
194 1 return INVALID_HANDLE_VALUE;
195 }
196
197 4 [[nodiscard]] bool any_module_basename_matches(std::wstring_view expected) noexcept
198 {
199 4 const HANDLE raw_snapshot = create_module_snapshot();
200
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 3 times.
4 if (raw_snapshot == INVALID_HANDLE_VALUE)
201 {
202 1 return false;
203 }
204 3 const ScopedSnapshot snapshot{raw_snapshot};
205
206 3 MODULEENTRY32W entry{};
207 3 entry.dwSize = sizeof(entry);
208
1/2
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
3 if (::Module32FirstW(raw_snapshot, &entry) == FALSE)
209 {
210 return false;
211 }
212
213 do
214 {
215
2/2
✓ Branch 11 → 12 taken 73 times.
✓ Branch 11 → 13 taken 4 times.
77 if (!module_name_matches_insensitive(entry.szModule, expected))
216 {
217 73 continue;
218 }
219
220 4 HMODULE module = nullptr;
221 12 if (::GetModuleHandleExW(
222 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
223 4 reinterpret_cast<LPCWSTR>(entry.modBaseAddr),
224 &module
225
2/4
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 16 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 4 times.
8 ) == FALSE ||
226
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 4 times.
4 module == nullptr)
227 {
228 continue;
229 }
230 4 const ScopedModuleReference reference{module};
231
2/2
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 2 times.
4 if (module_basename_matches(module, expected))
232 {
233 2 return true;
234 }
235
4/4
✓ Branch 26 → 27 taken 2 times.
✓ Branch 26 → 30 taken 2 times.
✓ Branch 32 → 33 taken 74 times.
✓ Branch 32 → 34 taken 1 time.
79 } while (::Module32NextW(raw_snapshot, &entry) != FALSE);
236
237 1 return false;
238 3 }
239 } // namespace
240
241 namespace memory
242 {
243 1037 Region module_of(Address address) noexcept
244 {
245 1037 return detail::live_module_region(address);
246 }
247
248 17 bool is_module_loaded(std::string_view basename, bool case_insensitive) noexcept
249 {
250 17 const std::wstring wide_name = detail::widen_module_name(basename);
251
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 6 taken 15 times.
17 if (wide_name.empty())
252 {
253 2 return false;
254 }
255
256
2/2
✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 10 taken 7 times.
15 if (case_insensitive)
257 {
258 8 return ::GetModuleHandleW(wide_name.c_str()) != nullptr;
259 }
260
2/2
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 6 times.
7 if (detail::is_loader_lock_held())
261 {
262 1 return false;
263 }
264
265 6 HMODULE module = nullptr;
266 6 BOOL reference_acquired = FALSE;
267 #if defined(DMK_ENABLE_TEST_SEAMS)
268
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 16 taken 4 times.
6 if (DetourModKit::detail::g_module_loaded_reference_candidate_test_override != nullptr)
269 {
270 2 reference_acquired = ::GetModuleHandleExW(
271 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
272 reinterpret_cast<LPCWSTR>(DetourModKit::detail::g_module_loaded_reference_candidate_test_override),
273 &module
274 );
275 }
276 else
277 #endif
278 {
279 4 reference_acquired = ::GetModuleHandleExW(0, wide_name.c_str(), &module);
280 }
281
2/4
✓ Branch 19 → 20 taken 6 times.
✗ Branch 19 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 6 times.
6 if (reference_acquired == FALSE || module == nullptr)
282 {
283 return false;
284 }
285 6 const ScopedModuleReference reference{module};
286
287 #if defined(DMK_ENABLE_TEST_SEAMS)
288
2/2
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 5 times.
6 if (auto *const hook = DetourModKit::detail::g_module_loaded_after_reference_test_hook)
289 {
290 1 hook();
291 }
292 #endif
293
294
2/2
✓ Branch 27 → 28 taken 2 times.
✓ Branch 27 → 29 taken 4 times.
6 if (module_basename_matches(module, wide_name))
295 {
296 2 return true;
297 }
298 4 return any_module_basename_matches(wide_name);
299 17 }
300 } // namespace memory
301 } // namespace DetourModKit
302