src/internal/xinput_route_probe.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "internal/xinput_route_probe.hpp" | ||
| 2 | |||
| 3 | #include <windows.h> | ||
| 4 | |||
| 5 | #include <array> | ||
| 6 | #include <atomic> | ||
| 7 | #include <cstddef> | ||
| 8 | #include <limits> | ||
| 9 | |||
| 10 | namespace DetourModKit | ||
| 11 | { | ||
| 12 | namespace detail | ||
| 13 | { | ||
| 14 | namespace | ||
| 15 | { | ||
| 16 | constexpr std::size_t MAX_PROBE_SLOTS = 64; | ||
| 17 | constexpr std::uintptr_t SLOT_BITS = 7; | ||
| 18 | constexpr std::uintptr_t SLOT_MASK = (std::uintptr_t{1} << SLOT_BITS) - 1; | ||
| 19 | constexpr std::uintptr_t CLAIMED_TOKEN = std::numeric_limits<std::uintptr_t>::max(); | ||
| 20 | constexpr std::uintptr_t MAX_NONCE = (CLAIMED_TOKEN >> SLOT_BITS) - 1; | ||
| 21 | constexpr std::uint64_t CLOSED_BIT = std::uint64_t{1} << 63; | ||
| 22 | constexpr std::uint64_t COUNT_MASK = CLOSED_BIT - 1; | ||
| 23 | |||
| 24 | struct ProbeSlot | ||
| 25 | { | ||
| 26 | std::atomic<std::uintptr_t> token{0}; | ||
| 27 | std::atomic<std::uintptr_t> observed_token{0}; | ||
| 28 | std::atomic<std::uint64_t> owner{0}; | ||
| 29 | std::atomic<std::uint64_t> hook_epoch{0}; | ||
| 30 | std::atomic<std::uintptr_t> state_address{0}; | ||
| 31 | std::atomic<std::uint32_t> user_index{0}; | ||
| 32 | std::atomic<std::uint32_t> thread_id{0}; | ||
| 33 | std::atomic<XInputRouteMember> member{XInputRouteMember::Primary}; | ||
| 34 | }; | ||
| 35 | |||
| 36 | std::array<ProbeSlot, MAX_PROBE_SLOTS> s_probe_slots{}; | ||
| 37 | std::atomic<DWORD> s_probe_tls{TLS_OUT_OF_INDEXES}; | ||
| 38 | std::atomic<std::uint64_t> s_lease_gate{CLOSED_BIT}; | ||
| 39 | std::atomic<std::uintptr_t> s_next_nonce{1}; | ||
| 40 | |||
| 41 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 42 | std::atomic<bool> s_fail_reservation{false}; | ||
| 43 | std::atomic<bool> s_fail_entry{false}; | ||
| 44 | std::atomic<bool> s_fail_restore{false}; | ||
| 45 | #endif | ||
| 46 | |||
| 47 | 21994 | [[nodiscard]] std::uintptr_t read_token(DWORD index) noexcept | |
| 48 | { | ||
| 49 | 21994 | return reinterpret_cast<std::uintptr_t>(::TlsGetValue(index)); | |
| 50 | } | ||
| 51 | |||
| 52 | 5256 | [[nodiscard]] bool write_token(DWORD index, std::uintptr_t token, [[maybe_unused]] bool restore) noexcept | |
| 53 | { | ||
| 54 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 55 | 5256 | if ((restore ? s_fail_restore : s_fail_entry).load(std::memory_order_relaxed)) | |
| 56 | { | ||
| 57 | 4 | return false; | |
| 58 | } | ||
| 59 | #endif | ||
| 60 | 5252 | return ::TlsSetValue(index, reinterpret_cast<void *>(token)) != FALSE; | |
| 61 | } | ||
| 62 | |||
| 63 | 2633 | [[nodiscard]] bool acquire_lease() noexcept | |
| 64 | { | ||
| 65 | 2633 | std::uint64_t gate = s_lease_gate.load(std::memory_order_acquire); | |
| 66 | 2633 | while ((gate & CLOSED_BIT) == 0 && (gate & COUNT_MASK) != COUNT_MASK) | |
| 67 | { | ||
| 68 | 5262 | if (s_lease_gate.compare_exchange_weak( | |
| 69 | gate, | ||
| 70 | gate + 1, | ||
| 71 | std::memory_order_acq_rel, | ||
| 72 | std::memory_order_acquire | ||
| 73 | )) | ||
| 74 | { | ||
| 75 | 2631 | return true; | |
| 76 | } | ||
| 77 | } | ||
| 78 | 2 | return false; | |
| 79 | } | ||
| 80 | |||
| 81 | 2631 | [[nodiscard]] std::uintptr_t next_nonce() noexcept | |
| 82 | { | ||
| 83 | 2631 | std::uintptr_t nonce = s_next_nonce.load(std::memory_order_relaxed); | |
| 84 | 2631 | while (nonce <= MAX_NONCE) | |
| 85 | { | ||
| 86 | 5262 | if (s_next_nonce.compare_exchange_weak( | |
| 87 | nonce, | ||
| 88 | nonce + 1, | ||
| 89 | std::memory_order_relaxed, | ||
| 90 | std::memory_order_relaxed | ||
| 91 | )) | ||
| 92 | { | ||
| 93 | 2631 | return nonce; | |
| 94 | } | ||
| 95 | } | ||
| 96 | ✗ | return 0; | |
| 97 | } | ||
| 98 | |||
| 99 | 2638 | [[nodiscard]] bool valid_identity(const XInputRouteProbeIdentity &identity) noexcept | |
| 100 | { | ||
| 101 | 2637 | return identity.owner != 0 && identity.hook_epoch != 0 && identity.state_address != 0 && | |
| 102 | 7909 | identity.user_index < 4 && | |
| 103 | 5272 | (identity.member == XInputRouteMember::Primary || identity.member == XInputRouteMember::Ex); | |
| 104 | } | ||
| 105 | } // namespace | ||
| 106 | |||
| 107 | 1646 | bool reserve_xinput_route_probe() noexcept | |
| 108 | { | ||
| 109 | 1646 | const DWORD last_error = ::GetLastError(); | |
| 110 | 1646 | DWORD index = s_probe_tls.load(std::memory_order_acquire); | |
| 111 | 1646 | if (index == TLS_OUT_OF_INDEXES) | |
| 112 | { | ||
| 113 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 114 | 231 | if (s_fail_reservation.load(std::memory_order_relaxed)) | |
| 115 | { | ||
| 116 | 2 | return false; | |
| 117 | } | ||
| 118 | #endif | ||
| 119 | 229 | index = ::TlsAlloc(); | |
| 120 | 229 | if (index == TLS_OUT_OF_INDEXES) | |
| 121 | { | ||
| 122 | ✗ | ::SetLastError(last_error); | |
| 123 | ✗ | return false; | |
| 124 | } | ||
| 125 | s_probe_tls.store(index, std::memory_order_release); | ||
| 126 | } | ||
| 127 | // Reopen preserves leases from the revoked epoch. They still pin their index and stable slots. | ||
| 128 | s_lease_gate.fetch_and(COUNT_MASK, std::memory_order_acq_rel); | ||
| 129 | 1644 | ::SetLastError(last_error); | |
| 130 | 1644 | return true; | |
| 131 | } | ||
| 132 | |||
| 133 | 555 | void close_xinput_route_probes() noexcept | |
| 134 | { | ||
| 135 | s_lease_gate.fetch_or(CLOSED_BIT, std::memory_order_acq_rel); | ||
| 136 | 555 | } | |
| 137 | |||
| 138 | 112955 | std::uint64_t xinput_route_probe_inflight() noexcept | |
| 139 | { | ||
| 140 | 112955 | return s_lease_gate.load(std::memory_order_acquire) & COUNT_MASK; | |
| 141 | } | ||
| 142 | |||
| 143 | 231 | bool release_xinput_route_probe_if_idle() noexcept | |
| 144 | { | ||
| 145 | 231 | if (s_lease_gate.load(std::memory_order_acquire) != CLOSED_BIT) | |
| 146 | { | ||
| 147 | 2 | return false; | |
| 148 | } | ||
| 149 | 229 | const DWORD last_error = ::GetLastError(); | |
| 150 | 229 | const DWORD index = s_probe_tls.exchange(TLS_OUT_OF_INDEXES, std::memory_order_acq_rel); | |
| 151 | 229 | const bool released = index == TLS_OUT_OF_INDEXES || ::TlsFree(index) != FALSE; | |
| 152 | 229 | if (!released) | |
| 153 | { | ||
| 154 | s_probe_tls.store(index, std::memory_order_release); | ||
| 155 | } | ||
| 156 | 229 | ::SetLastError(last_error); | |
| 157 | 229 | return released; | |
| 158 | } | ||
| 159 | |||
| 160 | 16751 | void xinput_route_probe_record(const XInputRouteProbeIdentity &identity) noexcept | |
| 161 | { | ||
| 162 | 16760 | const DWORD index = s_probe_tls.load(std::memory_order_acquire); | |
| 163 | 16760 | if (index == TLS_OUT_OF_INDEXES) | |
| 164 | { | ||
| 165 | ✗ | return; | |
| 166 | } | ||
| 167 | 16760 | const DWORD last_error = ::GetLastError(); | |
| 168 | 16796 | const std::uintptr_t token = read_token(index); | |
| 169 | 16799 | ::SetLastError(last_error); | |
| 170 | 16815 | const std::uintptr_t encoded_slot = token & SLOT_MASK; | |
| 171 | 16815 | if (token == 0 || token == CLAIMED_TOKEN || encoded_slot == 0 || encoded_slot > MAX_PROBE_SLOTS) | |
| 172 | { | ||
| 173 | 14392 | return; | |
| 174 | } | ||
| 175 | 2423 | ProbeSlot &slot = s_probe_slots[encoded_slot - 1]; | |
| 176 | 2423 | if (slot.token.load(std::memory_order_acquire) != token || | |
| 177 | 4842 | slot.owner.load(std::memory_order_relaxed) != identity.owner || | |
| 178 | 4836 | slot.hook_epoch.load(std::memory_order_relaxed) != identity.hook_epoch || | |
| 179 | 2416 | slot.member.load(std::memory_order_relaxed) != identity.member || | |
| 180 | 4828 | slot.user_index.load(std::memory_order_relaxed) != identity.user_index || | |
| 181 | 4826 | slot.state_address.load(std::memory_order_relaxed) != identity.state_address || | |
| 182 | 9667 | slot.thread_id.load(std::memory_order_relaxed) != ::GetCurrentThreadId() || | |
| 183 | 4822 | slot.token.load(std::memory_order_acquire) != token) | |
| 184 | { | ||
| 185 | 12 | return; | |
| 186 | } | ||
| 187 | // A delayed stale write carries its own nonce and cannot authenticate a reused slot. | ||
| 188 | 2411 | slot.observed_token.store(token, std::memory_order_release); | |
| 189 | } | ||
| 190 | |||
| 191 | 2638 | XInputRouteProbe::XInputRouteProbe(const XInputRouteProbeIdentity &identity) noexcept | |
| 192 | { | ||
| 193 | 2638 | if (!valid_identity(identity) || !acquire_lease()) | |
| 194 | { | ||
| 195 | 7 | return; | |
| 196 | } | ||
| 197 | 2631 | const DWORD last_error = ::GetLastError(); | |
| 198 | 2631 | m_index = s_probe_tls.load(std::memory_order_acquire); | |
| 199 | 2631 | if (m_index != TLS_OUT_OF_INDEXES) | |
| 200 | { | ||
| 201 | 2631 | const std::uintptr_t nonce = next_nonce(); | |
| 202 | 2631 | if (nonce != 0) | |
| 203 | { | ||
| 204 | 8625 | for (std::size_t i = 0; i < s_probe_slots.size(); ++i) | |
| 205 | { | ||
| 206 | 5993 | ProbeSlot &slot = s_probe_slots[i]; | |
| 207 | 5993 | std::uintptr_t expected = 0; | |
| 208 | 11986 | if (!slot.token.compare_exchange_strong( | |
| 209 | expected, | ||
| 210 | CLAIMED_TOKEN, | ||
| 211 | std::memory_order_acquire, | ||
| 212 | std::memory_order_relaxed | ||
| 213 | )) | ||
| 214 | { | ||
| 215 | 3363 | continue; | |
| 216 | } | ||
| 217 | 2630 | m_slot = static_cast<std::uint32_t>(i); | |
| 218 | 2630 | m_token = (nonce << SLOT_BITS) | (i + 1); | |
| 219 | 2630 | slot.observed_token.store(0, std::memory_order_relaxed); | |
| 220 | 2630 | slot.owner.store(identity.owner, std::memory_order_relaxed); | |
| 221 | 2630 | slot.hook_epoch.store(identity.hook_epoch, std::memory_order_relaxed); | |
| 222 | 2630 | slot.member.store(identity.member, std::memory_order_relaxed); | |
| 223 | 2630 | slot.user_index.store(identity.user_index, std::memory_order_relaxed); | |
| 224 | 2630 | slot.state_address.store(identity.state_address, std::memory_order_relaxed); | |
| 225 | 2630 | slot.thread_id.store(::GetCurrentThreadId(), std::memory_order_relaxed); | |
| 226 | 2630 | slot.token.store(m_token, std::memory_order_release); | |
| 227 | 2630 | m_previous_token = read_token(m_index); | |
| 228 | 2630 | m_admitted = write_token(m_index, m_token, false); | |
| 229 | 2630 | if (!m_admitted) | |
| 230 | { | ||
| 231 | 3 | slot.token.store(0, std::memory_order_release); | |
| 232 | 3 | m_token = 0; | |
| 233 | } | ||
| 234 | 2630 | break; | |
| 235 | } | ||
| 236 | } | ||
| 237 | } | ||
| 238 | 2631 | ::SetLastError(last_error); | |
| 239 | 2631 | if (!m_admitted) | |
| 240 | { | ||
| 241 | s_lease_gate.fetch_sub(1, std::memory_order_release); | ||
| 242 | } | ||
| 243 | } | ||
| 244 | |||
| 245 | 5265 | XInputRouteProbe::~XInputRouteProbe() noexcept | |
| 246 | { | ||
| 247 | 2638 | if (!m_admitted) | |
| 248 | { | ||
| 249 | 11 | return; | |
| 250 | } | ||
| 251 | 2627 | (void)finish(); | |
| 252 | 2627 | s_probe_slots[m_slot].token.store(0, std::memory_order_release); | |
| 253 | s_lease_gate.fetch_sub(1, std::memory_order_release); | ||
| 254 | 2638 | } | |
| 255 | |||
| 256 | 5193 | bool XInputRouteProbe::finish() noexcept | |
| 257 | { | ||
| 258 | 5193 | if (m_finished) | |
| 259 | { | ||
| 260 | 2564 | return m_receipt; | |
| 261 | } | ||
| 262 | 2629 | m_finished = true; | |
| 263 | 2629 | if (!m_admitted) | |
| 264 | { | ||
| 265 | 3 | return false; | |
| 266 | } | ||
| 267 | 2626 | const DWORD last_error = ::GetLastError(); | |
| 268 | 2626 | const bool current = read_token(m_index) == m_token; | |
| 269 | 2626 | const bool restored = write_token(m_index, m_previous_token, true); | |
| 270 | 2626 | ::SetLastError(last_error); | |
| 271 | 2626 | m_receipt = | |
| 272 | 5250 | current && restored && s_probe_slots[m_slot].observed_token.load(std::memory_order_acquire) == m_token; | |
| 273 | 2626 | return m_receipt; | |
| 274 | } | ||
| 275 | |||
| 276 | #if defined(DMK_ENABLE_TEST_SEAMS) | ||
| 277 | 40 | void set_xinput_route_probe_failures_for_test(bool reservation, bool entry, bool restore) noexcept | |
| 278 | { | ||
| 279 | 40 | s_fail_reservation.store(reservation, std::memory_order_relaxed); | |
| 280 | 40 | s_fail_entry.store(entry, std::memory_order_relaxed); | |
| 281 | 40 | s_fail_restore.store(restore, std::memory_order_relaxed); | |
| 282 | 40 | } | |
| 283 | |||
| 284 | 4 | std::uint32_t xinput_route_probe_tls_index_for_test() noexcept | |
| 285 | { | ||
| 286 | 4 | return s_probe_tls.load(std::memory_order_acquire); | |
| 287 | } | ||
| 288 | |||
| 289 | 11 | std::uintptr_t xinput_route_probe_token_for_test() noexcept | |
| 290 | { | ||
| 291 | 11 | const DWORD last_error = ::GetLastError(); | |
| 292 | 11 | const DWORD index = s_probe_tls.load(std::memory_order_acquire); | |
| 293 | 11 | const std::uintptr_t token = index == TLS_OUT_OF_INDEXES ? 0 : read_token(index); | |
| 294 | 11 | ::SetLastError(last_error); | |
| 295 | 11 | return token; | |
| 296 | } | ||
| 297 | |||
| 298 | 21 | bool set_xinput_route_probe_token_for_test(std::uintptr_t token) noexcept | |
| 299 | { | ||
| 300 | 21 | const DWORD last_error = ::GetLastError(); | |
| 301 | 21 | const DWORD index = s_probe_tls.load(std::memory_order_acquire); | |
| 302 | const bool stored = | ||
| 303 | 21 | index != TLS_OUT_OF_INDEXES && ::TlsSetValue(index, reinterpret_cast<void *>(token)) != FALSE; | |
| 304 | 21 | ::SetLastError(last_error); | |
| 305 | 21 | return stored; | |
| 306 | } | ||
| 307 | #endif | ||
| 308 | } // namespace detail | ||
| 309 | } // namespace DetourModKit | ||
| 310 |