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 |