GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 2 / 0 / 2
Functions: 100.0% 2 / 0 / 2
Branches: -% 0 / 0 / 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 * The depth is backed by a reserved Win32 TLS slot rather than thread_local because MinGW lowers thread_local
25 * to __emutls_get_address, which allocates on first use per thread and abort()s uncatchably under OOM (see
26 * mid_hook_adapter.hpp and event_dispatcher.cpp for the same reservation). Not installed.
27 */
28
29 #include <cstdint>
30
31 namespace DetourModKit::detail
32 {
33 /// Returns the allocation-free Win32 identity of the calling thread.
34 [[nodiscard]] std::uint32_t current_native_thread_id() noexcept;
35
36 #if defined(DMK_ENABLE_TEST_SEAMS)
37 /// Seam signature; see set_delivery_scope_reservation_seam_for_test.
38 using DeliveryScopeReservationSeam = void (*)() noexcept;
39
40 /// Runs a probe after the first reservation check and before its serialized recheck.
41 void set_delivery_scope_reservation_seam_for_test(DeliveryScopeReservationSeam seam) noexcept;
42
43 /**
44 * @brief Makes the reserved slot's depth store report failure for the calling thread.
45 * @details A store into a reserved index past the TEB's inline slots is backed by a lazily heap-allocated
46 * expansion array, so it can fail on a thread that has never used a high index while the reservation
47 * itself stays valid. No host can provoke that heap state on demand, and refusing the frame is the only
48 * branch a caller's correctness depends on, so it is driven here instead of guessed at. Registration is
49 * per calling thread and bounded, so two threads can refuse simultaneously; a caller that exceeds the
50 * bound gets false and must not treat its store as refused.
51 * @return false when @p fail was requested and no registration slot was free.
52 */
53 [[nodiscard]] bool set_delivery_scope_store_failure_for_test(bool fail) noexcept;
54 #endif
55
56 /**
57 * @brief Reserves the delivery marker's Win32 TLS slot before callbacks can run.
58 * @return false when the process has no slot available, after which every ordinary delivery is refused.
59 * @note Setup/control-plane only.
60 */
61 [[nodiscard]] bool reserve_delivery_scope_tls() noexcept;
62
63 /**
64 * @brief Reports whether the calling thread is currently executing input-gate consumer code.
65 * @details Exact for the calling thread and silent about every other one. False therefore means "this thread is
66 * not inside gate consumer code", which is what entitles a control-plane caller to run its blocking
67 * rundown. True for a recorded ordinary delivery and for a mandatory teardown span on this thread.
68 */
69 [[nodiscard]] bool current_thread_in_delivery() noexcept;
70
71 /**
72 * @struct TeardownRegistration
73 * @brief Intrusive registry node owned by one @ref MandatoryDeliveryScope frame.
74 * @details Plain data manipulated only by the registry under its lock; the node itself lives on the registering
75 * thread's stack, which is what makes registration allocation-free.
76 */
77 struct TeardownRegistration
78 {
79 TeardownRegistration *next{nullptr};
80 std::uint32_t thread{0};
81 };
82
83 /**
84 * @class DeliveryScope
85 * @brief RAII marker bracketing one user-callback invocation inside a gate; nesting-safe and noexcept.
86 * @details Construct it immediately before invoking the user callback and let it destruct immediately after, with
87 * no gate mutex held. Nested deliveries on one thread increment the depth.
88 * @note Construction can fail (see @ref admitted). An ordinary delivery path must treat a refused scope as "do not
89 * run the callback" and undo whatever it had already committed for this edge.
90 */
91 class DeliveryScope
92 {
93 public:
94 DeliveryScope() noexcept;
95 ~DeliveryScope() noexcept;
96
97 DeliveryScope(const DeliveryScope &) = delete;
98 DeliveryScope &operator=(const DeliveryScope &) = delete;
99 DeliveryScope(DeliveryScope &&) = delete;
100 DeliveryScope &operator=(DeliveryScope &&) = delete;
101
102 /**
103 * @brief Whether this frame is recorded, so the thread now reads as in-delivery.
104 * @return false when the reserved slot is unavailable or the per-thread store failed under host OOM.
105 */
106 22269 [[nodiscard]] bool admitted() const noexcept { return m_admitted; }
107
108 private:
109 bool m_admitted;
110 };
111
112 /**
113 * @class MandatoryDeliveryScope
114 * @brief RAII marker for teardown consumer code, which must run whether or not the depth store can record it.
115 * @details Brackets the whole span a teardown path spends in consumer code: a hold's balancing edge, retirement's
116 * balancing edge, and the destruction of the retired callable's captures. Unlike @ref DeliveryScope this
117 * cannot be refused. It takes the ordinary depth when it can and always records the calling thread in a
118 * process-wide intrusive registry of stack-local nodes, so a teardown whose depth store failed under host
119 * OOM is still exactly identifiable. Without that, two threads each running a teardown callback that
120 * releases the other's gate would both read as depth-zero control plane and each wait on the other's
121 * claim.
122 *
123 * Allocates nothing, throws nothing, nests, and holds the registry lock only across pointer surgery, so
124 * no consumer code ever runs under it. The registry is keyed by native thread id, so an unrelated
125 * control-plane thread still reads false and still waits for real quiescence.
126 */
127 class MandatoryDeliveryScope
128 {
129 public:
130 MandatoryDeliveryScope() noexcept;
131 ~MandatoryDeliveryScope() noexcept;
132
133 MandatoryDeliveryScope(const MandatoryDeliveryScope &) = delete;
134 MandatoryDeliveryScope &operator=(const MandatoryDeliveryScope &) = delete;
135 MandatoryDeliveryScope(MandatoryDeliveryScope &&) = delete;
136 MandatoryDeliveryScope &operator=(MandatoryDeliveryScope &&) = delete;
137
138 /// Whether the ordinary TLS depth also recorded this frame; the registry records it either way.
139 1 [[nodiscard]] bool depth_recorded() const noexcept { return m_scope.admitted(); }
140
141 private:
142 DeliveryScope m_scope;
143 TeardownRegistration m_registration;
144 };
145 } // namespace DetourModKit::detail
146
147 #endif // DETOURMODKIT_INTERNAL_INPUT_DELIVERY_SCOPE_HPP
148