The Pedigree Project 0.1
ps2mouse-regressions.cc
1/*
2 * Copyright (c) 2026, Pedigree Developers
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted.
6 */
7
8#include "pedigree/kernel/Atomic.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/process/Scheduler.h"
11#include "pedigree/kernel/process/Semaphore.h"
12#include "pedigree/kernel/process/Thread.h"
13#include "pedigree/kernel/time/Time.h"
14
15#include "modules/drivers/x86/ps2mouse/Ps2MouseCallbackRegistry.h"
16
17namespace {
18bool check(bool condition, const char* detail) {
19 if (condition) {
20 return true;
21 }
22
23 ERROR("HOSTED-WAIT-TEST: FAIL ps2mouse-callback-lifetime: " << detail);
24 return false;
25}
26
27struct CallbackLifetimeContext {
28 CallbackLifetimeContext()
29 : registration(),
30 remover(nullptr),
31 phase(0),
32 hookCalls(0),
33 hookObservedDrain(0),
34 callbackCalls(0),
35 callbacksAfterReturn(0),
36 unregisterReturned(0),
37 failures(0) {}
38
40 Thread* remover;
41 Atomic<size_t> phase;
42 Atomic<size_t> hookCalls;
43 Atomic<size_t> hookObservedDrain;
44 Atomic<size_t> callbackCalls;
45 Atomic<size_t> callbacksAfterReturn;
46 Atomic<size_t> unregisterReturned;
47 Atomic<size_t> failures;
48};
49
50CallbackLifetimeContext* g_CallbackLifetimeContext = nullptr;
51
52void lifetimeCallback(void* parameter, const void*, size_t) {
53 CallbackLifetimeContext* context = reinterpret_cast<CallbackLifetimeContext*>(parameter);
54 context->callbackCalls += 1;
55 if (context->unregisterReturned) {
56 context->callbacksAfterReturn += 1;
57 }
58}
59
60void callbackPinHook(Ps2MouseCallbackRegistry::Handler handler, void* parameter) {
61 CallbackLifetimeContext* context = g_CallbackLifetimeContext;
62 if (!context || handler != lifetimeCallback || parameter != context ||
63 !context->phase.compareAndSwap(0, 1)) {
64 return;
65 }
66
67 context->hookCalls += 1;
68 for (size_t attempt = 0; attempt < 10000; ++attempt) {
69 Thread::WaitDebugInfo info = {};
70 uintptr_t debugAddress = 0;
71 if (context->phase == static_cast<size_t>(2) && context->remover->getWaitDebugInfo(info) &&
72 info.queue && info.channelOwner && info.queued &&
73 context->remover->getDebugState(debugAddress) == Thread::CallbackDrain &&
74 debugAddress == reinterpret_cast<uintptr_t>(context)) {
75 break;
76 }
78 }
79
80 Thread::WaitDebugInfo info = {};
81 uintptr_t debugAddress = 0;
82 if (context->phase == static_cast<size_t>(2) && !context->unregisterReturned &&
83 context->remover->getWaitDebugInfo(info) && info.queue && info.channelOwner && info.queued &&
84 context->remover->getDebugState(debugAddress) == Thread::CallbackDrain &&
85 debugAddress == reinterpret_cast<uintptr_t>(context)) {
86 context->hookObservedDrain += 1;
87 } else {
88 context->failures += 1;
89 }
90 context->phase = 3;
91}
92
93int unregisterPinnedCallback(void* parameter) {
94 CallbackLifetimeContext* context = reinterpret_cast<CallbackLifetimeContext*>(parameter);
95 const Time::Timestamp deadline = Time::getTicks() + (500 * Time::Multiplier::Millisecond);
96 while (context->phase != static_cast<size_t>(1) && Time::getTicks() < deadline) {
98 }
99
100 if (context->phase != static_cast<size_t>(1)) {
101 context->failures += 1;
102 return 1;
103 }
104
105 context->phase = 2;
106 if (!context->registration.reset()) {
107 context->failures += 1;
108 }
109 context->unregisterReturned += 1;
110 context->phase = 4;
111 return 0;
112}
113
114struct SelfRemovalContext {
115 SelfRemovalContext() : registration(), calls(0) {}
116
118 Atomic<size_t> calls;
119};
120
121void selfRemovingCallback(void* parameter, const void*, size_t) {
122 SelfRemovalContext* context = reinterpret_cast<SelfRemovalContext*>(parameter);
123 context->calls += 1;
124 context->registration.reset();
125}
126
127bool callbackLifetimeBarrier() {
129 CallbackLifetimeContext context;
130 context.remover = new Thread(Scheduler::instance().getKernelProcess(), unregisterPinnedCallback,
131 &context, nullptr, false, true);
132 context.remover->setName("hosted PS/2 callback remover");
133
134 g_CallbackLifetimeContext = &context;
135 registry.setCallbackPinHook(callbackPinHook);
136 const bool registered = registry.subscribe(lifetimeCallback, &context, context.registration);
137 const bool duplicateRejected =
138 !registry.subscribe(lifetimeCallback, &context, context.registration);
139 const uint8_t byte = 0x7F;
140 registry.dispatch(&byte, 1);
141 const bool joined = context.remover->join();
142 registry.setCallbackPinHook(nullptr);
143 g_CallbackLifetimeContext = nullptr;
144
145 const size_t callsAtUnregisterReturn = context.callbackCalls;
146 registry.dispatch(&byte, 1);
147 const bool lateDispatchRejected =
148 context.callbackCalls == callsAtUnregisterReturn && context.callbacksAfterReturn == 0;
149
150 const bool reregistered = registry.subscribe(lifetimeCallback, &context, context.registration);
151 if (reregistered) {
152 registry.dispatch(&byte, 1);
153 context.registration.reset();
154 }
155
156 SelfRemovalContext selfRemoval;
157 const bool selfRegistered =
158 registry.subscribe(selfRemovingCallback, &selfRemoval, selfRemoval.registration);
159 if (selfRegistered) {
160 registry.dispatch(&byte, 1);
161 registry.dispatch(&byte, 1);
162 }
163 const bool selfReregistered =
164 registry.subscribe(selfRemovingCallback, &selfRemoval, selfRemoval.registration);
165 selfRemoval.registration.reset();
166
167 bool passed = true;
168 passed &= check(registered && duplicateRejected, "registration or duplicate rejection failed");
169 passed &= check(joined && context.failures == 0,
170 "the concurrent callback remover did not finish cleanly");
171 passed &= check(
172 context.hookCalls == 1 && context.hookObservedDrain == 1 && context.unregisterReturned == 1,
173 "unregister returned instead of waiting for the admitted callback");
174 passed &= check(context.callbackCalls >= 1 && lateDispatchRejected,
175 "a callback began after its registration was reset");
176 passed &= check(reregistered && context.callbackCalls == callsAtUnregisterReturn + 1,
177 "the drained callback slot could not be reused");
178 passed &= check(
179 selfRegistered && selfRemoval.calls == 1 && !selfRemoval.registration && selfReregistered,
180 "self-removal did not retire the callback after it returned");
181
182 if (passed) {
183 NOTICE("HOSTED-WAIT-TEST: PASS ps2mouse-callback-lifetime");
184 }
185 return passed;
186}
187
188struct ReciprocalRemovalContext {
189 ReciprocalRemovalContext()
190 : registry(nullptr),
191 first(),
192 second(),
193 beginReset(0),
194 firstParticipant(0),
195 callbacksEntered(0),
196 firstCalls(0),
197 secondCalls(0),
198 resetRejections(0),
199 resetsFinished(0),
200 invocationsFinished(0),
201 failures(0) {}
202
203 Ps2MouseCallbackRegistry* registry;
206 Semaphore beginReset;
207 Atomic<size_t> firstParticipant;
208 Atomic<size_t> callbacksEntered;
209 Atomic<size_t> firstCalls;
210 Atomic<size_t> secondCalls;
211 Atomic<size_t> resetRejections;
212 Atomic<size_t> resetsFinished;
213 Atomic<size_t> invocationsFinished;
214 Atomic<size_t> failures;
215};
216
217bool waitForValue(Atomic<size_t>& value, size_t expected) {
218 const Time::Timestamp deadline = Time::getTicks() + (500 * Time::Multiplier::Millisecond);
219 while (value != expected && Time::getTicks() < deadline) {
221 }
222 return value == expected;
223}
224
225void firstReciprocalCallback(void* parameter, const void*, size_t) {
226 ReciprocalRemovalContext* context = reinterpret_cast<ReciprocalRemovalContext*>(parameter);
227 context->firstCalls += 1;
228 if (!context->firstParticipant.compareAndSwap(0, 1)) {
229 return;
230 }
231
232 context->callbacksEntered += 1;
233 if (!context->beginReset.acquireForCompletion()) {
234 context->failures += 1;
235 return;
236 }
237 if (!context->second.reset() && context->second) {
238 context->resetRejections += 1;
239 } else {
240 context->failures += 1;
241 }
242 context->resetsFinished += 1;
243 if (!waitForValue(context->resetsFinished, 2)) {
244 context->failures += 1;
245 }
246}
247
248void secondReciprocalCallback(void* parameter, const void*, size_t) {
249 ReciprocalRemovalContext* context = reinterpret_cast<ReciprocalRemovalContext*>(parameter);
250 context->secondCalls += 1;
251 context->callbacksEntered += 1;
252 if (!context->beginReset.acquireForCompletion()) {
253 context->failures += 1;
254 return;
255 }
256 if (!context->first.reset() && context->first) {
257 context->resetRejections += 1;
258 } else {
259 context->failures += 1;
260 }
261 context->resetsFinished += 1;
262 if (!waitForValue(context->resetsFinished, 2)) {
263 context->failures += 1;
264 }
265}
266
267int dispatchReciprocalCallbacks(void* parameter) {
268 ReciprocalRemovalContext* context = reinterpret_cast<ReciprocalRemovalContext*>(parameter);
269 const uint8_t byte = 0x5A;
270 context->registry->dispatch(&byte, 1);
271 context->invocationsFinished += 1;
272 return 0;
273}
274
275bool reciprocalRemovalIsRetryable() {
277 ReciprocalRemovalContext context;
278 context.registry = &registry;
279
280 const bool firstRegistered = registry.subscribe(firstReciprocalCallback, &context, context.first);
281 const bool secondRegistered =
282 registry.subscribe(secondReciprocalCallback, &context, context.second);
283
284 Process* process = Scheduler::instance().getKernelProcess();
285 Thread* firstInvoker = nullptr;
286 Thread* secondInvoker = nullptr;
287 bool firstEntered = false;
288 bool bothEntered = false;
289 if (firstRegistered && secondRegistered) {
290 firstInvoker = new Thread(process, dispatchReciprocalCallbacks, &context, nullptr, false, true);
291 firstInvoker->setName("hosted PS/2 reciprocal callback A");
292 firstEntered = waitForValue(context.callbacksEntered, 1);
293
294 if (firstEntered) {
295 secondInvoker =
296 new Thread(process, dispatchReciprocalCallbacks, &context, nullptr, false, true);
297 secondInvoker->setName("hosted PS/2 reciprocal callback B");
298 bothEntered = waitForValue(context.callbacksEntered, 2);
299 }
300 }
301
302 context.beginReset.release(2);
303 const bool firstJoined = !firstInvoker || firstInvoker->joinForCompletion();
304 const bool secondJoined = !secondInvoker || secondInvoker->joinForCompletion();
305 const bool tokensPreserved = context.first && context.second;
306
307 const size_t firstCalls = context.firstCalls;
308 const size_t secondCalls = context.secondCalls;
309 const uint8_t byte = 0xA5;
310 registry.dispatch(&byte, 1);
311 const bool admissionClosed =
312 context.firstCalls == firstCalls && context.secondCalls == secondCalls;
313
314 const bool firstRetired = context.first && context.first.reset();
315 const bool secondRetired = context.second && context.second.reset();
316 const bool firstReused = registry.subscribe(firstReciprocalCallback, &context, context.first);
317 const bool secondReused = registry.subscribe(secondReciprocalCallback, &context, context.second);
318 const bool firstReuseRetired = context.first.reset();
319 const bool secondReuseRetired = context.second.reset();
320
321 const bool passed = check(
322 firstRegistered && secondRegistered && firstEntered && bothEntered && firstJoined &&
323 secondJoined && tokensPreserved && admissionClosed && firstRetired && secondRetired &&
324 firstReused && secondReused && firstReuseRetired && secondReuseRetired &&
325 !context.first && !context.second && context.firstCalls == static_cast<size_t>(2) &&
326 context.secondCalls == static_cast<size_t>(1) &&
327 context.resetRejections == static_cast<size_t>(2) &&
328 context.resetsFinished == static_cast<size_t>(2) &&
329 context.invocationsFinished == static_cast<size_t>(2) && !context.failures,
330 "reciprocal callbacks did not preserve retryable registration ownership");
331 if (passed) {
332 NOTICE("HOSTED-WAIT-TEST: PASS ps2mouse-reciprocal-removal");
333 }
334 return passed;
335}
336} // namespace
337
338bool runHostedPs2MouseRegressions() {
339 return callbackLifetimeBarrier() && reciprocalRemovalIsRetryable();
340}
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
bool joinForCompletion()
Definition Thread.cc:2771