./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
16 / 16
100.0%
Branches:
5 / 6
83.3%
Functions:
9 / 9
100.0%

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 10700793 [[nodiscard]] Module module() const noexcept { return m_module.load(std::memory_order_acquire); }
54 /// Publishes the module identity.
55 1649 void publish_module(Module module) noexcept { m_module.store(module, std::memory_order_release); }
56 /// Clears the module identity.
57 1697 void clear_module() noexcept { m_module.store(nullptr, std::memory_order_release); }
58
59 /// Returns the current session phase.
60 29149 [[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 75830 [[nodiscard]] LoaderContext loader_context() const noexcept
79 {
80 75830 return m_loader_context.load(std::memory_order_acquire);
81 }
82 1867 void set_loader_context(LoaderContext context) noexcept
83 {
84 1867 m_loader_context.store(context, std::memory_order_release);
85 1867 }
86
87 /// Publishes the calling thread as the bootstrap worker. Called by the worker before consumer code can run.
88 void publish_worker_thread() noexcept;
89 /**
90 * @brief Retires the published worker identity.
91 * @details Required before the worker's id can be recycled by the OS, or a later unrelated thread would inherit
92 * both its blocking authorization and its refused self-drain.
93 */
94 void clear_worker_thread() noexcept;
95 /// The published bootstrap worker's OS thread id, or 0 when no worker is published.
96 87 [[nodiscard]] std::uint32_t worker_thread_id() const noexcept
97 {
98 174 return m_worker_thread_id.load(std::memory_order_acquire);
99 }
100 /// Reports whether the calling thread is the published bootstrap worker.
101 [[nodiscard]] bool is_worker_thread() const noexcept;
102
103 private:
104 std::atomic<Module> m_module{nullptr};
105 std::atomic<LifecycleState> m_state{LifecycleState::Stopped};
106 std::atomic<std::uint64_t> m_generation{0};
107 std::atomic<LoaderContext> m_loader_context{LoaderContext::Normal};
108 std::atomic<std::uint32_t> m_worker_thread_id{0};
109 };
110
111 // module_handle() is callback-safe and therefore cannot use an atomic implementation backed by a hidden lock.
112 static_assert(
113 std::atomic<LifecycleContext::Module>::is_always_lock_free,
114 "the module identity must be a lock-free atomic pointer."
115 );
116
117 /// The one process-global session control block.
118 [[nodiscard]] LifecycleContext &lifecycle() noexcept;
119
120 /** @brief Reports process termination from the published context or the native loader state. */
121 [[nodiscard]] bool process_is_exiting() noexcept;
122
123 /**
124 * @brief Reports whether the caller is authorized to block, before the loader-lock veto is applied.
125 * @details Either the published phase is @ref LoaderContext::Normal or @ref LoaderContext::ExplicitDrain, which
126 * authorizes every thread, or the caller is the bootstrap worker. The worker needs its own clause because
127 * the loader context is one process-global word that describes the DllMain thread's phase. A bare
128 * FreeLibrary publishes @ref LoaderContext::LoaderDetach and returns. The worker then runs the ordered
129 * teardown it was created to run. That thread is in no loader callback and still holds a counted module
130 * reference. A widened published phase instead authorizes every other thread for that whole window.
131 */
132 [[nodiscard]] bool teardown_caller_authorized() noexcept;
133
134 /**
135 * @brief The single gate every teardown must pass before it joins a thread, waits, closes a sink, or destroys
136 * callable state that may run consumer code.
137 * @details Combines both halves of the rule in one call so no site can apply only one: the caller must be
138 * authorized (@ref teardown_caller_authorized), AND the fail-closed loader-lock diagnostic must not veto
139 * it. A heuristic false alone never authorizes blocking, which is why this is a function rather than a
140 * bare `!is_loader_lock_held()` at each call site.
141 * @return true only when blocking teardown is safe; false means take the abandon/retain path.
142 */
143 [[nodiscard]] bool blocking_teardown_permitted() noexcept;
144
145 /**
146 * @brief @ref blocking_teardown_permitted with the loader-lock probe replaced by a test-installed override.
147 * @param probe_override The subsystem's override hook, or nullptr to use the real probe.
148 * @details Subsystems that expose their own probe seam route through this rather than composing the two halves
149 * locally, so the authorization rule has exactly one definition. A forced "held" still vetoes; a forced
150 * "not held" only withdraws the veto, because a heuristic false never authorizes blocking on its own.
151 */
152 2073 [[nodiscard]] inline bool blocking_teardown_permitted(bool (*probe_override)() noexcept) noexcept
153 {
154
2/2
✓ Branch 2 → 3 taken 36 times.
✓ Branch 2 → 10 taken 2037 times.
2073 if (probe_override != nullptr)
155 {
156
3/4
✓ Branch 4 → 5 taken 36 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 8 taken 33 times.
36 return teardown_caller_authorized() && !probe_override();
157 }
158 2037 return blocking_teardown_permitted();
159 }
160 } // namespace DetourModKit::detail
161
162 #endif // DETOURMODKIT_INTERNAL_LIFECYCLE_CONTEXT_HPP
163