./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
133 / 137
97.1%
Branches:
87 / 104
83.7%
Functions:
17 / 17
100.0%

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
4/4
✓ Branch 2 → 3 taken 2626 times.
✓ Branch 2 → 4 taken 2630 times.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 5252 times.
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
3/4
✓ Branch 17 → 18 taken 2631 times.
✓ Branch 17 → 19 taken 2 times.
✓ Branch 18 → 10 taken 2631 times.
✗ Branch 18 → 19 not taken.
2633 while ((gate & CLOSED_BIT) == 0 && (gate & COUNT_MASK) != COUNT_MASK)
67 {
68
1/2
✓ Branch 15 → 16 taken 2631 times.
✗ Branch 15 → 17 not taken.
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
1/2
✓ Branch 17 → 10 taken 2631 times.
✗ Branch 17 → 18 not taken.
2631 while (nonce <= MAX_NONCE)
85 {
86
1/2
✓ Branch 15 → 16 taken 2631 times.
✗ Branch 15 → 17 not taken.
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
4/4
✓ Branch 3 → 4 taken 2636 times.
✓ Branch 3 → 9 taken 1 time.
✓ Branch 4 → 5 taken 2635 times.
✓ Branch 4 → 9 taken 1 time.
2637 return identity.owner != 0 && identity.hook_epoch != 0 && identity.state_address != 0 &&
102
4/4
✓ Branch 2 → 3 taken 2637 times.
✓ Branch 2 → 9 taken 1 time.
✓ Branch 5 → 6 taken 2634 times.
✓ Branch 5 → 9 taken 1 time.
7909 identity.user_index < 4 &&
103
4/4
✓ Branch 6 → 7 taken 1274 times.
✓ Branch 6 → 8 taken 1360 times.
✓ Branch 7 → 8 taken 1273 times.
✓ Branch 7 → 9 taken 1 time.
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
2/2
✓ Branch 10 → 11 taken 231 times.
✓ Branch 10 → 27 taken 1415 times.
1646 if (index == TLS_OUT_OF_INDEXES)
112 {
113 #if defined(DMK_ENABLE_TEST_SEAMS)
114
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 229 times.
231 if (s_fail_reservation.load(std::memory_order_relaxed))
115 {
116 2 return false;
117 }
118 #endif
119 229 index = ::TlsAlloc();
120
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 229 times.
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
2/2
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 229 times.
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
3/4
✓ Branch 14 → 15 taken 213 times.
✓ Branch 14 → 17 taken 16 times.
✓ Branch 16 → 17 taken 213 times.
✗ Branch 16 → 18 not taken.
229 const bool released = index == TLS_OUT_OF_INDEXES || ::TlsFree(index) != FALSE;
152
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 29 taken 229 times.
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
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 16760 times.
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
5/8
✓ Branch 14 → 15 taken 2423 times.
✓ Branch 14 → 18 taken 14392 times.
✓ Branch 15 → 16 taken 2423 times.
✗ Branch 15 → 18 not taken.
✓ Branch 16 → 17 taken 2423 times.
✗ Branch 16 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 2423 times.
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
2/2
✓ Branch 35 → 36 taken 2418 times.
✓ Branch 35 → 79 taken 3 times.
4842 slot.owner.load(std::memory_order_relaxed) != identity.owner ||
178
2/2
✓ Branch 43 → 44 taken 2416 times.
✓ Branch 43 → 79 taken 2 times.
4836 slot.hook_epoch.load(std::memory_order_relaxed) != identity.hook_epoch ||
179
2/2
✓ Branch 45 → 46 taken 2414 times.
✓ Branch 45 → 79 taken 2 times.
2416 slot.member.load(std::memory_order_relaxed) != identity.member ||
180
2/2
✓ Branch 53 → 54 taken 2413 times.
✓ Branch 53 → 79 taken 1 time.
4828 slot.user_index.load(std::memory_order_relaxed) != identity.user_index ||
181
2/2
✓ Branch 61 → 62 taken 2412 times.
✓ Branch 61 → 79 taken 1 time.
4826 slot.state_address.load(std::memory_order_relaxed) != identity.state_address ||
182
6/6
✓ Branch 27 → 28 taken 2421 times.
✓ Branch 27 → 79 taken 2 times.
✓ Branch 70 → 71 taken 2411 times.
✓ Branch 70 → 79 taken 1 time.
✓ Branch 81 → 82 taken 12 times.
✓ Branch 81 → 83 taken 2411 times.
9667 slot.thread_id.load(std::memory_order_relaxed) != ::GetCurrentThreadId() ||
183
1/2
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 2411 times.
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
6/6
✓ Branch 3 → 4 taken 2633 times.
✓ Branch 3 → 6 taken 5 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 2631 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 10 taken 2631 times.
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
1/2
✓ Branch 18 → 19 taken 2631 times.
✗ Branch 18 → 104 not taken.
2631 if (m_index != TLS_OUT_OF_INDEXES)
200 {
201 2631 const std::uintptr_t nonce = next_nonce();
202
1/2
✓ Branch 20 → 21 taken 2631 times.
✗ Branch 20 → 104 not taken.
2631 if (nonce != 0)
203 {
204
2/2
✓ Branch 103 → 22 taken 5993 times.
✓ Branch 103 → 104 taken 1 time.
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
2/2
✓ Branch 28 → 29 taken 3363 times.
✓ Branch 28 → 30 taken 2630 times.
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
2/2
✓ Branch 90 → 91 taken 3 times.
✓ Branch 90 → 100 taken 2627 times.
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
2/2
✓ Branch 105 → 106 taken 4 times.
✓ Branch 105 → 109 taken 2627 times.
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
2/2
✓ Branch 2 → 3 taken 11 times.
✓ Branch 2 → 4 taken 2627 times.
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
2/2
✓ Branch 2 → 3 taken 2564 times.
✓ Branch 2 → 4 taken 2629 times.
5193 if (m_finished)
259 {
260 2564 return m_receipt;
261 }
262 2629 m_finished = true;
263
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 2626 times.
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
6/6
✓ Branch 10 → 11 taken 2625 times.
✓ Branch 10 → 22 taken 1 time.
✓ Branch 11 → 12 taken 2624 times.
✓ Branch 11 → 22 taken 1 time.
✓ Branch 20 → 21 taken 2408 times.
✓ Branch 20 → 22 taken 216 times.
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
1/2
✓ Branch 10 → 11 taken 11 times.
✗ Branch 10 → 12 not taken.
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
2/4
✓ Branch 10 → 11 taken 21 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 21 times.
✗ Branch 12 → 14 not taken.
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