GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 78.4% 29 / 0 / 37
Functions: 100.0% 5 / 0 / 5
Branches: 59.1% 13 / 0 / 22

src/platform.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_PLATFORM_HPP
2 #define DETOURMODKIT_PLATFORM_HPP
3
4 #include "internal/diagnostics_population.hpp"
5
6 #include <windows.h>
7
8 namespace DetourModKit::detail
9 {
10 /**
11 * @brief Checks if the current thread holds the Windows loader lock.
12 * @details Reads the loader-lock CRITICAL_SECTION pointer from the PEB at the architecture-specific offset (0x110
13 * on x64, 0xA0 on x86), then compares its OwningThread to the current thread id. Thread joins are unsafe
14 * while the loader lock is held (DllMain context), so this probe supplies the veto half of
15 * detail::blocking_teardown_permitted(), which every subsystem teardown reads.
16 * @note Supported Windows x86/x64 layouts keep the PEB and its loader-lock field readable for the process lifetime.
17 * The probe's PEB reads do not fault on those layouts. The probe validates only the loader-lock pointer target
18 * before it reads OwningThread. A null PEB, null pointer, or pointer outside committed readable memory returns
19 * true. A spurious true retains a detached thread and its module reference. A spurious false joins under the
20 * loader lock and deadlocks host unload. Therefore, uncertainty must produce true.
21 * @note This is a fail-closed diagnostic only. A true or indeterminate result can veto a join. A false result never
22 * authorizes a teardown wait. The explicit loader context or bootstrap worker grants that authority. See
23 * detail::teardown_caller_authorized.
24 * @return true if the current thread holds the loader lock, or if ownership cannot be determined.
25 */
26 16616 [[nodiscard]] inline bool is_loader_lock_held_impl() noexcept
27 {
28 #ifdef _WIN64
29 // GCC 14 models mingw-w64's __readgsqword as an array access at address zero and flags -Warray-bounds
30 // under -O2. The read is a segment-register load, so the diagnostic is a false positive.
31 #if defined(__GNUC__) && !defined(__clang__)
32 #pragma GCC diagnostic push
33 #pragma GCC diagnostic ignored "-Warray-bounds"
34 #endif
35 16616 auto *peb = reinterpret_cast<char *>(__readgsqword(0x60));
36 #if defined(__GNUC__) && !defined(__clang__)
37 #pragma GCC diagnostic pop
38 #endif
39 16616 constexpr size_t LOADER_LOCK_OFFSET = 0x110;
40 #else
41 auto *peb = reinterpret_cast<char *>(__readfsdword(0x30));
42 constexpr size_t LOADER_LOCK_OFFSET = 0xA0;
43 #endif
44
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 16616 times.
16616 if (!peb)
45 return true;
46
47 16616 auto *cs = *reinterpret_cast<PCRITICAL_SECTION *>(peb + LOADER_LOCK_OFFSET);
48
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 16616 times.
16616 if (!cs)
49 return true;
50
51 // Check that the critical section uses committed, readable memory before the OwningThread read. A stale or
52 // corrupt slot value can fault that read.
53 16616 constexpr DWORD READABLE_PROTECT = PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | PAGE_EXECUTE_READ |
54 PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
55 16616 MEMORY_BASIC_INFORMATION mbi{};
56
1/2
✓ Branch 10 → 11 taken 16617 times.
✗ Branch 10 → 13 not taken.
33233 if (VirtualQuery(cs, &mbi, sizeof(mbi)) != sizeof(mbi) || mbi.State != MEM_COMMIT ||
57
4/8
✓ Branch 9 → 10 taken 16617 times.
✗ Branch 9 → 13 not taken.
✓ Branch 11 → 12 taken 16617 times.
✗ Branch 11 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 16617 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 16617 times.
33234 (mbi.Protect & READABLE_PROTECT) == 0 || (mbi.Protect & (PAGE_GUARD | PAGE_NOACCESS)) != 0)
58 {
59 return true;
60 }
61
62 16617 return cs->OwningThread == reinterpret_cast<HANDLE>(static_cast<uintptr_t>(GetCurrentThreadId()));
63 }
64
65 #if defined(DMK_ENABLE_TEST_SEAMS)
66 // Forces the probe's verdict so a proof can drive the "heuristic disagrees with the explicit context" case in both
67 // directions, which is the only way to show a false result never authorizes blocking teardown on its own. Defined
68 // in src/internal/lifecycle_context.cpp; set before the participating threads start and cleared after they join.
69 extern bool (*g_loader_lock_override)() noexcept;
70 #endif
71
72 /// Returns @ref is_loader_lock_held_impl, or the forced verdict when a test seam has overridden the probe.
73 16640 [[nodiscard]] inline bool is_loader_lock_held() noexcept
74 {
75 #if defined(DMK_ENABLE_TEST_SEAMS)
76
2/2
✓ Branch 2 → 3 taken 23 times.
✓ Branch 2 → 4 taken 16617 times.
16640 if (auto *const override_fn = g_loader_lock_override)
77 {
78 23 return override_fn();
79 }
80 #endif
81 16617 return is_loader_lock_held_impl();
82 }
83
84 /**
85 * @brief Tries to take a counted reference on the module DetourModKit is linked into.
86 * @details Bumps the module's loader reference count by one, the "bonus LoadLibrary on yourself" a DLL performs so
87 * it cannot be unmapped while it still has code running. The reference is counted, not pinned: a matching
88 * @ref release_module_ref (or a thread's FreeLibraryAndExitThread) balances it, so the module can still
89 * unload once every holder releases, unlike a GET_MODULE_HANDLE_EX_FLAG_PIN reference, which pins the
90 * module for the process lifetime and can never be released. A pin is also useless from a detach path (the
91 * loader refuses to pin a module that is already unloading), which is why this counted reference, taken
92 * while the module is live, is the correct primitive.
93 *
94 * The reference MUST be taken while the module is still fully loaded (before a background thread is
95 * created, or before a hook/callback is published) and NEVER from a DLL_PROCESS_DETACH/unload path. Once
96 * an explicit FreeLibrary has driven the count to zero the loader has already committed to unmapping the
97 * module, and a reference requested at that point does not abort the in-progress unload (GetModuleHandleEx
98 * cannot find a module that is unloading). A thread that needs its code to survive a caller's FreeLibrary
99 * therefore acquires this reference before the thread can run, not when it is told to stop.
100 *
101 * GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS identifies the module from a code address; omitting
102 * GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT is what makes the call take a reference (its default,
103 * count-incrementing behavior) rather than a bare identity lookup.
104 * @param reason Books the outstanding reference under diagnostics::module_pin_count until its release. The
105 * matching release must pass the same reason.
106 * @return The module handle to pass to a matching release, or nullptr without diagnostics when the reference could
107 * not be taken.
108 */
109 3247 [[nodiscard]] inline HMODULE try_acquire_module_ref(diagnostics::ModulePinReason reason) noexcept
110 {
111 3247 HMODULE module = nullptr;
112
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3247 times.
3247 if (!GetModuleHandleExW(
113 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
114 reinterpret_cast<LPCWSTR>(&try_acquire_module_ref),
115 &module
116 ))
117 {
118 return nullptr;
119 }
120 3247 module_pin_observability::note_acquired(reason);
121 3247 return module;
122 }
123
124 /**
125 * @brief @ref try_acquire_module_ref with a debugger diagnostic on failure.
126 * @details Same acquisition contract, including the requirement to take the reference while the module is still
127 * fully loaded. Use this wherever a failure is worth reporting; use the plain try form on a path that must
128 * stay silent, such as bootstrap under the loader lock.
129 * @param reason Books the outstanding reference under diagnostics::module_pin_count until its release.
130 * @return The module handle, which the holder must balance with @ref release_module_ref (or, for a thread
131 * releasing its own reference as its final act, FreeLibraryAndExitThread); nullptr on failure, with
132 * GetLastError() preserved across the diagnostic.
133 */
134 3194 [[nodiscard]] inline HMODULE acquire_module_ref(diagnostics::ModulePinReason reason) noexcept
135 {
136 3194 const HMODULE module = try_acquire_module_ref(reason);
137
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 3194 times.
3194 if (module == nullptr)
138 {
139 // Preserve the failure code across the debug print: OutputDebugStringA may overwrite the thread's
140 // last-error value, and callers report GetLastError() after this returns nullptr.
141 const DWORD last_error = GetLastError();
142 OutputDebugStringA(
143 "DetourModKit: acquire_module_ref failed; a background thread's code may be unmapped by "
144 "a caller's FreeLibrary.\n"
145 );
146 SetLastError(last_error);
147 return nullptr;
148 }
149 3194 return module;
150 }
151
152 /**
153 * @brief Releases a counted reference taken by @ref acquire_module_ref, from a thread OTHER than the one whose code
154 * the release might unmap.
155 * @details Call this after a background thread has been JOINED (its code is done running) to balance the
156 * start-of-thread acquire, so the module is no longer held mapped on this account. It is safe only as long
157 * as another reference on the module still exists (the host's own load reference, or another live
158 * worker's), so it can never be the terminal release that unmaps the module out from under the caller,
159 * which is still executing this module's code. A background thread must NOT release its OWN reference this
160 * way as its final act: it uses FreeLibraryAndExitThread so the FreeLibrary's return address is never in
161 * code the release just unmapped. A FreeLibraryAndExitThread caller first invokes
162 * module_pin_observability::note_released for its reason.
163 * @param module A handle returned by @ref acquire_module_ref. A null value changes no count.
164 * @param reason The reason passed to the matching acquire.
165 * The count decrements before FreeLibrary.
166 */
167 3900 inline void release_module_ref(HMODULE module, diagnostics::ModulePinReason reason) noexcept
168 {
169
2/2
✓ Branch 2 → 3 taken 3207 times.
✓ Branch 2 → 5 taken 693 times.
3900 if (module != nullptr)
170 {
171 3207 module_pin_observability::note_released(reason);
172 3207 FreeLibrary(module);
173 }
174 3900 }
175
176 } // namespace DetourModKit::detail
177
178 #endif // DETOURMODKIT_PLATFORM_HPP
179