./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
3 / 3
100.0%
Branches:
-
-%
Functions:
3 / 3
100.0%

src/internal/input_delivery_scope.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_INTERNAL_INPUT_DELIVERY_SCOPE_HPP
2 #define DETOURMODKIT_INTERNAL_INPUT_DELIVERY_SCOPE_HPP
3
4 /**
5 * @file input_delivery_scope.hpp
6 * @brief Per-thread marker for "this thread is inside an input-gate callback", used to break cross-binding teardown.
7 * @details A binding gate runs the user callback outside its own mutex and, for a control-plane release, blocks until
8 * any in-flight delivery has drained so the caller can safely destroy captured state. That blocking rundown
9 * must NOT run when the release is reached from inside a callback: a self-release, or a callback that releases
10 * a second binding's guard, would otherwise wait on a delivery that is (transitively) waiting on it, which is
11 * the cross-binding ABBA. This marker lets a gate distinguish the two: a release at depth zero blocks; a
12 * release from a marked thread defers its rundown to the in-flight delivery's unwind.
13 *
14 * The answer is exact per thread. It is never widened to "some thread somewhere might be in a callback",
15 * because a control-plane release on an unrelated thread would then skip the rundown its public contract
16 * promises and let its caller destroy state a live callback is still reading.
17 *
18 * Two recording mechanisms back it, because the two callers differ in what they may do when recording fails.
19 * An ordinary delivery is optional and uses @ref DeliveryScope, which is refused rather than admitted
20 * untracked. Teardown consumer code is mandatory (a balancing edge and the destruction of a consumer's
21 * captures have to run), so it uses @ref MandatoryDeliveryScope, which additionally records the thread in an
22 * allocation-free stack-local registry that cannot fail.
23 *
24 * A Win32 TLS slot holds the depth instead of `thread_local` (`[B-86]`). @ref DeliveryTlsOwner owns the slot.
25 * Not installed.
26 */
27
28 #include <windows.h>
29
30 #include <cstdint>
31
32 namespace DetourModKit::detail
33 {
34 /// Returns the allocation-free Win32 identity of the calling thread.
35 [[nodiscard]] std::uint32_t current_native_thread_id() noexcept;
36
37 #if defined(DMK_ENABLE_TEST_SEAMS)
38 /// Seam signature; see set_delivery_scope_reservation_seam_for_test.
39 using DeliveryScopeReservationSeam = void (*)() noexcept;
40
41 /// Runs a probe in owner registration while no index is published, before the registration reserves one.
42 void set_delivery_scope_reservation_seam_for_test(DeliveryScopeReservationSeam seam) noexcept;
43
44 /**
45 * @brief Makes the reserved slot's depth store report failure for the calling thread.
46 * @details A store into a reserved index past the TEB's inline slots is backed by a lazily heap-allocated
47 * expansion array, so it can fail on a thread that has never used a high index while the reservation
48 * itself stays valid. No host can provoke that heap state on demand, and refusing the frame is the only
49 * branch a caller's correctness depends on, so it is driven here instead of guessed at. Registration is
50 * per calling thread and bounded, so two threads can refuse simultaneously; a caller that exceeds the
51 * bound gets false and must not treat its store as refused.
52 * @return false when @p fail was requested and no registration slot was free.
53 */
54 [[nodiscard]] bool set_delivery_scope_store_failure_for_test(bool fail) noexcept;
55 #endif
56
57 /**
58 * @class DeliveryTlsOwner
59 * @brief One owner registration on the delivery marker's Win32 TLS index.
60 * @details Every binding gate holds one, so the index outlives each frame that a gate can open. The last owner
61 * returns the index. A registration that finds no index leaves every ordinary delivery refused until a
62 * later registration reserves one (proofs: `Lifecycle.DeliveryTlsIndexReturnsWithTheLastGate`,
63 * `Lifecycle.DeliveryTlsIndexStaysWithALiveGate`).
64 * @note Setup/control-plane only.
65 */
66 class DeliveryTlsOwner
67 {
68 public:
69 DeliveryTlsOwner() noexcept;
70 ~DeliveryTlsOwner() noexcept;
71
72 DeliveryTlsOwner(const DeliveryTlsOwner &) = delete;
73 DeliveryTlsOwner &operator=(const DeliveryTlsOwner &) = delete;
74 DeliveryTlsOwner(DeliveryTlsOwner &&) = delete;
75 DeliveryTlsOwner &operator=(DeliveryTlsOwner &&) = delete;
76
77 /// Whether an index was published when this owner registered.
78 1891 [[nodiscard]] bool reserved() const noexcept { return m_reserved; }
79
80 private:
81 bool m_reserved;
82 };
83
84 #if defined(DMK_ENABLE_TEST_SEAMS)
85 /// Returns the published delivery marker index, or TLS_OUT_OF_INDEXES.
86 [[nodiscard]] DWORD delivery_scope_tls_index_for_test() noexcept;
87
88 /// Returns the registered owner count of the delivery marker index.
89 [[nodiscard]] std::uint32_t delivery_scope_tls_owners_for_test() noexcept;
90 #endif
91
92 /**
93 * @brief Reports whether the calling thread is currently executing input-gate consumer code.
94 * @details Exact for the calling thread and silent about every other one. False therefore means "this thread is
95 * not inside gate consumer code", which is what entitles a control-plane caller to run its blocking
96 * rundown. True for a recorded ordinary delivery and for a mandatory teardown span on this thread.
97 */
98 [[nodiscard]] bool current_thread_in_delivery() noexcept;
99
100 /**
101 * @struct TeardownRegistration
102 * @brief Intrusive registry node owned by one @ref MandatoryDeliveryScope frame.
103 * @details Plain data manipulated only by the registry under its lock; the node itself lives on the registering
104 * thread's stack, which is what makes registration allocation-free.
105 */
106 struct TeardownRegistration
107 {
108 TeardownRegistration *next{nullptr};
109 std::uint32_t thread{0};
110 };
111
112 /**
113 * @class DeliveryScope
114 * @brief RAII marker bracketing one user-callback invocation inside a gate; nesting-safe and noexcept.
115 * @details Construct it immediately before invoking the user callback and let it destruct immediately after, with
116 * no gate mutex held. Nested deliveries on one thread increment the depth.
117 * @note Construction can fail (see @ref admitted). An ordinary delivery path must treat a refused scope as "do not
118 * run the callback" and undo whatever it had already committed for this edge.
119 */
120 class DeliveryScope
121 {
122 public:
123 DeliveryScope() noexcept;
124 ~DeliveryScope() noexcept;
125
126 DeliveryScope(const DeliveryScope &) = delete;
127 DeliveryScope &operator=(const DeliveryScope &) = delete;
128 DeliveryScope(DeliveryScope &&) = delete;
129 DeliveryScope &operator=(DeliveryScope &&) = delete;
130
131 /**
132 * @brief Whether this frame is recorded, so the thread now reads as in-delivery.
133 * @return false when no owner published an index or the per-thread store failed under host OOM.
134 */
135 2735 [[nodiscard]] bool admitted() const noexcept { return m_admitted; }
136
137 private:
138 /// The index this frame incremented, so the destructor decrements that index and no other.
139 DWORD m_index;
140 bool m_admitted;
141 };
142
143 /**
144 * @class MandatoryDeliveryScope
145 * @brief RAII marker for teardown consumer code, which must run whether or not the depth store can record it.
146 * @details Brackets the whole span a teardown path spends in consumer code: a hold's balancing edge, retirement's
147 * balancing edge, and the destruction of the retired callable's captures. Unlike @ref DeliveryScope this
148 * cannot be refused. It takes the ordinary depth when it can and always records the calling thread in a
149 * process-wide intrusive registry of stack-local nodes, so a teardown whose depth store failed under host
150 * OOM is still exactly identifiable. Without that, two threads each running a teardown callback that
151 * releases the other's gate would both read as depth-zero control plane and each wait on the other's
152 * claim.
153 *
154 * Allocates nothing, throws nothing, nests, and holds the registry lock only across pointer surgery, so
155 * no consumer code ever runs under it. The registry is keyed by native thread id, so an unrelated
156 * control-plane thread still reads false and still waits for real quiescence.
157 */
158 class MandatoryDeliveryScope
159 {
160 public:
161 MandatoryDeliveryScope() noexcept;
162 ~MandatoryDeliveryScope() noexcept;
163
164 MandatoryDeliveryScope(const MandatoryDeliveryScope &) = delete;
165 MandatoryDeliveryScope &operator=(const MandatoryDeliveryScope &) = delete;
166 MandatoryDeliveryScope(MandatoryDeliveryScope &&) = delete;
167 MandatoryDeliveryScope &operator=(MandatoryDeliveryScope &&) = delete;
168
169 /// Whether the ordinary TLS depth also recorded this frame; the registry records it either way.
170 1 [[nodiscard]] bool depth_recorded() const noexcept { return m_scope.admitted(); }
171
172 private:
173 DeliveryScope m_scope;
174 TeardownRegistration m_registration;
175 };
176 } // namespace DetourModKit::detail
177
178 #endif // DETOURMODKIT_INTERNAL_INPUT_DELIVERY_SCOPE_HPP
179