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 |