GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 19 / 0 / 19
Functions: 100.0% 10 / 0 / 10
Branches: 90.0% 9 / 0 / 10

src/internal/lifecycle_context.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_LIFECYCLE_CONTEXT_HPP
2 #define DETOURMODKIT_INTERNAL_LIFECYCLE_CONTEXT_HPP
3
4 #include <atomic>
5 #include <cstdint>
6
7 // Keep the lifecycle control block independent of the Win32 headers while retaining HMODULE's exact pointer type.
8 struct HINSTANCE__;
9
10 namespace DetourModKit::detail
11 {
12 /// Represents the serialized session phase.
13 enum class LifecycleState : std::uint8_t
14 {
15 Stopped,
16 Starting,
17 Running,
18 Stopping
19 };
20
21 /**
22 * @brief Identifies the loader phase published by the controlled bootstrap path.
23 * @details This context authorizes blocking teardown for threads the process cannot identify individually;
24 * loader-lock detection may only veto it. @ref ExplicitDrain and @ref LoaderDetach are deliberately
25 * distinct: both follow a consumer's decision to unload, but only the first runs on a thread that is
26 * outside a loader callback and may therefore join.
27 */
28 enum class LoaderContext : std::uint8_t
29 {
30 /// No loader callback is in progress; a normally-hosted session.
31 Normal,
32 /// Inside DllMain DLL_PROCESS_ATTACH.
33 Attach,
34 /// An off-loader-lock shutdown handshake. The only unload phase that may block.
35 ExplicitDrain,
36 /// Inside DllMain DLL_PROCESS_DETACH for an explicit FreeLibrary.
37 LoaderDetach,
38 /// Inside DllMain DLL_PROCESS_DETACH for process termination.
39 ProcessExit
40 };
41
42 /**
43 * @class LifecycleContext
44 * @brief The session lifecycle control block. All fields are lock-free atomics so any getter is race-free against a
45 * concurrent detach-path write.
46 */
47 class LifecycleContext
48 {
49 public:
50 using Module = ::HINSTANCE__ *;
51
52 /// Returns the atomically published module identity, or null.
53 2875149 [[nodiscard]] Module module() const noexcept { return m_module.load(std::memory_order_acquire); }
54 /// Publishes the module identity.
55 58 void publish_module(Module module) noexcept { m_module.store(module, std::memory_order_release); }
56 /// Clears the module identity.
57 57 void clear_module() noexcept { m_module.store(nullptr, std::memory_order_release); }
58
59 /// Returns the current session phase.
60 13 [[nodiscard]] LifecycleState state() const noexcept { return m_state.load(std::memory_order_acquire); }
61 /// Returns the monotonically increasing session generation.
62 24 [[nodiscard]] std::uint64_t generation() const noexcept { return m_generation.load(std::memory_order_acquire); }
63
64 /**
65 * @brief Claims the single-session slot: Stopped -> Starting, bumping the generation and resetting the loader
66 * context to Normal for the new epoch.
67 * @return true if the slot was free; false if a session is already Starting, Running, or Stopping (the caller
68 * reports SessionAlreadyActive).
69 */
70 [[nodiscard]] bool begin_start() noexcept;
71 /// Starting -> Running once setup has succeeded.
72 void mark_running() noexcept;
73 /// Running -> Stopping at the start of teardown; a concurrent start stays rejected through the whole teardown.
74 void begin_stop() noexcept;
75 /// -> Stopped from any state (teardown complete, or a start-setup failure rollback).
76 void mark_stopped() noexcept;
77
78 9987 [[nodiscard]] LoaderContext loader_context() const noexcept
79 {
80 9987 return m_loader_context.load(std::memory_order_acquire);
81 }
82 397 void set_loader_context(LoaderContext context) noexcept
83 {
84 397 m_loader_context.store(context, std::memory_order_release);
85 397 }
86
87 /**
88 * @brief Reports whether the published context permits a teardown to block (join, wait, run user destruction).
89 * @details Only @ref LoaderContext::Normal and @ref LoaderContext::ExplicitDrain qualify. This is one of the
90 * two authorizing halves of the decision; callers must use @ref blocking_teardown_permitted, which
91 * also admits the bootstrap worker and applies the fail-closed loader-lock veto.
92 */
93 8400 [[nodiscard]] bool context_permits_blocking() const noexcept
94 {
95 8400 const LoaderContext context = loader_context();
96
4/4
✓ Branch 3 → 4 taken 135 times.
✓ Branch 3 → 5 taken 8265 times.
✓ Branch 4 → 5 taken 110 times.
✓ Branch 4 → 6 taken 25 times.
8400 return context == LoaderContext::Normal || context == LoaderContext::ExplicitDrain;
97 }
98
99 /// Publishes the calling thread as the bootstrap worker. Called by the worker before consumer code can run.
100 void publish_worker_thread() noexcept;
101 /**
102 * @brief Retires the published worker identity.
103 * @details Required before the worker's id can be recycled by the OS, or a later unrelated thread would inherit
104 * both its blocking authorization and its refused self-drain.
105 */
106 void clear_worker_thread() noexcept;
107 /// The published bootstrap worker's OS thread id, or 0 when no worker is published.
108 77 [[nodiscard]] std::uint32_t worker_thread_id() const noexcept
109 {
110 154 return m_worker_thread_id.load(std::memory_order_acquire);
111 }
112 /// Reports whether the calling thread is the published bootstrap worker.
113 [[nodiscard]] bool is_worker_thread() const noexcept;
114
115 private:
116 std::atomic<Module> m_module{nullptr};
117 std::atomic<LifecycleState> m_state{LifecycleState::Stopped};
118 std::atomic<std::uint64_t> m_generation{0};
119 std::atomic<LoaderContext> m_loader_context{LoaderContext::Normal};
120 std::atomic<std::uint32_t> m_worker_thread_id{0};
121 };
122
123 // module_handle() is callback-safe and therefore cannot use an atomic implementation backed by a hidden lock.
124 static_assert(
125 std::atomic<LifecycleContext::Module>::is_always_lock_free,
126 "the module identity must be a lock-free atomic pointer."
127 );
128
129 /// The one process-global session control block.
130 [[nodiscard]] LifecycleContext &lifecycle() noexcept;
131
132 /**
133 * @brief Reports whether the caller is authorized to block, before the loader-lock veto is applied.
134 * @details Either the published phase authorizes every thread (@ref LifecycleContext::context_permits_blocking),
135 * or the caller is the bootstrap worker. The worker needs its own clause because the loader context is one
136 * process-global word describing the DllMain thread's phase: a bare FreeLibrary publishes
137 * @ref LoaderContext::LoaderDetach and returns, and the worker then runs the ordered teardown it was
138 * created to run on a thread that is in no loader callback and still holds a counted module reference.
139 * Widening the published phase instead would authorize every other thread for that whole window.
140 */
141 [[nodiscard]] bool teardown_caller_authorized() noexcept;
142
143 /**
144 * @brief The single gate every teardown must pass before it joins a thread, waits, closes a sink, or destroys
145 * callable state that may run consumer code.
146 * @details Combines both halves of the rule in one call so no site can apply only one: the caller must be
147 * authorized (@ref teardown_caller_authorized), AND the fail-closed loader-lock diagnostic must not veto
148 * it. A heuristic false alone never authorizes blocking, which is why this is a function rather than a
149 * bare `!is_loader_lock_held()` at each call site.
150 * @return true only when blocking teardown is safe; false means take the abandon/retain path.
151 */
152 [[nodiscard]] bool blocking_teardown_permitted() noexcept;
153
154 /**
155 * @brief @ref blocking_teardown_permitted with the loader-lock probe replaced by a test-installed override.
156 * @param probe_override The subsystem's override hook, or nullptr to use the real probe.
157 * @details Subsystems that expose their own probe seam route through this rather than composing the two halves
158 * locally, so the authorization rule has exactly one definition. A forced "held" still vetoes; a forced
159 * "not held" only withdraws the veto, because a heuristic false never authorizes blocking on its own.
160 */
161 594 [[nodiscard]] inline bool blocking_teardown_permitted(bool (*probe_override)() noexcept) noexcept
162 {
163
2/2
✓ Branch 2 → 3 taken 35 times.
✓ Branch 2 → 10 taken 559 times.
594 if (probe_override != nullptr)
164 {
165
3/4
✓ Branch 4 → 5 taken 35 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 33 times.
35 return teardown_caller_authorized() && !probe_override();
166 }
167 559 return blocking_teardown_permitted();
168 }
169 } // namespace DetourModKit::detail
170
171 #endif // DETOURMODKIT_INTERNAL_LIFECYCLE_CONTEXT_HPP
172