The Pedigree Project 0.1
usb-transfer-lifecycle-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/LockGuard.h"
10#include "pedigree/kernel/Log.h"
11#include "pedigree/kernel/process/Mutex.h"
12#include "pedigree/kernel/process/OperationBarrier.h"
13#include "pedigree/kernel/process/Scheduler.h"
14#include "pedigree/kernel/process/Semaphore.h"
15#include "pedigree/kernel/process/Thread.h"
16#include "pedigree/kernel/utilities/List.h"
17#include "pedigree/kernel/utilities/utility.h"
18
19#include "modules/drivers/common/usb-hcd/CallbackDelivery.h"
20#include "modules/drivers/common/usb-hcd/TransferCompletion.h"
21#include "modules/system/usb/Usb.h"
22
23namespace {
25using DeliveryQueue = UsbHcd::CallbackDeliveryQueue;
26
27bool check(bool condition, const char* test, const char* detail) {
28 if (condition)
29 return true;
30
31 ERROR("HOSTED-WAIT-TEST: FAIL " << test << ": " << detail);
32 return false;
33}
34
35class TransferLifecycleModel {
36 public:
37 static constexpr size_t SlotCount = 2;
38
39 struct SlotCleanup {
40 SlotCleanup() : controller(nullptr), slot(0) {}
41
42 TransferLifecycleModel* controller;
43 size_t slot;
44 };
45
46 struct Slot {
47 Slot()
48 : completion(), accepted(), linked(false), dmaOwned(false), reclaimed(false), cleanup() {}
49
50 Completion completion;
52 bool linked;
53 bool dmaOwned;
54 bool reclaimed;
55 SlotCleanup cleanup;
56 };
57
58 TransferLifecycleModel()
59 : m_Lock(),
60 m_Submissions(),
61 m_Cancellations(),
62 m_Accepted(),
63 m_Deliveries(),
64 m_Slots(),
65 m_Closing(false),
66 m_Shutdown(false),
67 m_DmaViolations(0) {
68 for (size_t i = 0; i < SlotCount; ++i) {
69 m_Slots[i].cleanup.controller = this;
70 m_Slots[i].cleanup.slot = i;
71 }
72 }
73
74 ~TransferLifecycleModel() {
75 assert(m_Shutdown);
76 }
77
78 bool submit(size_t slotIndex, Completion::Callback callback, uintptr_t parameter,
79 Semaphore* commitEntered = nullptr, Semaphore* allowCommit = nullptr) {
80 OperationBarrier::Lease submission;
81 if (!m_Submissions.tryAcquire(submission))
82 return false;
83
84 LockGuard<Mutex> guard(m_Lock);
85 if (commitEntered)
86 commitEntered->release();
87 if (allowCommit) {
88 const bool allowed = allowCommit->acquireForCompletion();
89 (void)allowed;
90 }
91
92 if (m_Closing || slotIndex >= SlotCount)
93 return false;
94
95 Slot& slot = m_Slots[slotIndex];
96 if (slot.completion.state() != Completion::State::Idle)
97 return false;
98
100 if (!m_Accepted.tryAcquire(accepted))
101 return false;
102
103 // Active is published last, while cancellation is excluded by this
104 // same lock. It can therefore never describe an unlinked DMA object.
105 slot.accepted = pedigree_std::move(accepted);
106 slot.linked = true;
107 slot.dmaOwned = true;
108 slot.reclaimed = false;
109 slot.completion.arm(callback, parameter, m_Deliveries.nextGeneration());
110 return true;
111 }
112
113 bool captureNatural(size_t slotIndex, ssize_t result) {
114 LockGuard<Mutex> guard(m_Lock);
115 if (slotIndex >= SlotCount)
116 return false;
117 return m_Slots[slotIndex].completion.captureNatural(result);
118 }
119
120 bool cancel(size_t slotIndex, Completion::Callback callback, uintptr_t parameter) {
121 OperationBarrier::Lease cancellation;
122 if (!m_Cancellations.tryAcquire(cancellation))
123 return false;
124
125 DeliveryQueue::Record* record = nullptr;
126 DeliveryQueue::Key drainKey = {0, 0};
127 bool drain = false;
128 {
129 LockGuard<Mutex> guard(m_Lock);
130 if (slotIndex >= SlotCount)
131 return false;
132
133 Slot& slot = m_Slots[slotIndex];
134 Completion::Claim claim;
135 const Completion::CancellationDisposition disposition =
136 slot.completion.claimCancellation(callback, parameter, -TransactionError, claim);
137 if (disposition == Completion::CancellationDisposition::NoMatch)
138 return false;
139 if (disposition == Completion::CancellationDisposition::DrainPublished) {
140 drainKey = {slotIndex, claim.generation};
141 drain = true;
142 } else {
143 releaseDmaLocked(slot);
144 record = createRecordLocked(slotIndex, claim);
146 batch.pushBack(record);
147 m_Deliveries.publish(batch);
148 }
149 }
150
151 if (record)
152 m_Deliveries.deliver(record);
153 else if (drain)
154 (void)m_Deliveries.drain(drainKey);
155 return true;
156 }
157
158 bool shutdown() {
159 m_Submissions.closeAndWait();
160 m_Accepted.close();
161
163 {
164 LockGuard<Mutex> guard(m_Lock);
165 m_Closing = true;
166 for (size_t i = 0; i < SlotCount; ++i) {
167 Slot& slot = m_Slots[i];
168 Completion::Claim claim;
169 if (!slot.completion.claimForTeardown(-TransactionError, claim)) {
170 continue;
171 }
172
173 releaseDmaLocked(slot);
174 batch.pushBack(createRecordLocked(i, claim));
175 }
176 if (batch.count())
177 m_Deliveries.publish(batch);
178 }
179
180 while (batch.count())
181 m_Deliveries.deliver(batch.popFront());
182
183 m_Accepted.wait();
184 (void)m_Deliveries.drainAll();
185 m_Cancellations.closeAndWait();
186 const bool passed = m_Deliveries.empty() && m_Accepted.isClosedAndDrained() && !m_DmaViolations;
187 m_Shutdown = true;
188 return passed;
189 }
190
191 bool linked(size_t slotIndex) {
192 LockGuard<Mutex> guard(m_Lock);
193 return slotIndex < SlotCount && m_Slots[slotIndex].linked;
194 }
195
196 bool reclaimed(size_t slotIndex) {
197 LockGuard<Mutex> guard(m_Lock);
198 return slotIndex < SlotCount && m_Slots[slotIndex].reclaimed;
199 }
200
201 private:
202 static void reclaim(void* parameter) {
203 auto* cleanup = reinterpret_cast<SlotCleanup*>(parameter);
204 TransferLifecycleModel* controller = cleanup->controller;
205 LockGuard<Mutex> guard(controller->m_Lock);
206 Slot& slot = controller->m_Slots[cleanup->slot];
207 if (slot.dmaOwned)
208 controller->m_DmaViolations += 1;
209 slot.linked = false;
210 slot.reclaimed = true;
211 slot.accepted = OperationBarrier::Lease();
212 }
213
214 void releaseDmaLocked(Slot& slot) {
215 slot.linked = false;
216 slot.dmaOwned = false;
217 }
218
219 DeliveryQueue::Record* createRecordLocked(size_t slotIndex, const Completion::Claim& claim) {
220 Slot& slot = m_Slots[slotIndex];
221 assert(!slot.dmaOwned);
222 return m_Deliveries.create({slotIndex, claim.generation}, claim.callback, claim.parameter,
223 claim.result, reclaim, &slot.cleanup);
224 }
225
226 Mutex m_Lock;
227 OperationBarrier m_Submissions;
228 OperationBarrier m_Cancellations;
229 OperationBarrier m_Accepted;
230 DeliveryQueue m_Deliveries;
231 Slot m_Slots[SlotCount];
232 bool m_Closing;
233 bool m_Shutdown;
234 Atomic<size_t> m_DmaViolations;
235};
236
237struct ResultContext {
238 ResultContext() : calls(0), result(0) {}
239
240 Atomic<size_t> calls;
241 Atomic<ssize_t> result;
242};
243
244void recordResult(uintptr_t parameter, ssize_t result) {
245 auto* context = reinterpret_cast<ResultContext*>(parameter);
246 context->result = result;
247 context->calls += 1;
248}
249
250struct SubmissionRaceContext {
251 SubmissionRaceContext(TransferLifecycleModel* controller)
252 : controller(controller),
253 commitEntered(0),
254 allowCommit(0),
255 callback(),
256 submitted(0),
257 cancelled(0) {}
258
259 TransferLifecycleModel* controller;
260 Semaphore commitEntered;
261 Semaphore allowCommit;
262 ResultContext callback;
263 Atomic<size_t> submitted;
264 Atomic<size_t> cancelled;
265};
266
267int submitPausedTransfer(void* parameter) {
268 auto* context = reinterpret_cast<SubmissionRaceContext*>(parameter);
269 if (context->controller->submit(0, recordResult, reinterpret_cast<uintptr_t>(&context->callback),
270 &context->commitEntered, &context->allowCommit)) {
271 context->submitted += 1;
272 }
273 return 0;
274}
275
276int cancelPausedTransfer(void* parameter) {
277 auto* context = reinterpret_cast<SubmissionRaceContext*>(parameter);
278 if (context->controller->cancel(0, recordResult,
279 reinterpret_cast<uintptr_t>(&context->callback))) {
280 context->cancelled += 1;
281 }
282 return 0;
283}
284
285bool waitUntilSleeping(Thread* thread) {
286 for (size_t attempt = 0; attempt < 10000; ++attempt) {
287 if (thread->getStatus() == Thread::Sleeping)
288 return true;
290 }
291 return false;
292}
293
294bool cancellationCannotObserveHalfPublishedTransfer() {
295 TransferLifecycleModel controller;
296 SubmissionRaceContext context(&controller);
297 Process* kernelProcess = Scheduler::instance().getKernelProcess();
298 Thread* submission =
299 new Thread(kernelProcess, submitPausedTransfer, &context, nullptr, false, true);
300 const bool commitEntered = context.commitEntered.acquireForCompletion();
301 Thread* cancellation =
302 new Thread(kernelProcess, cancelPausedTransfer, &context, nullptr, false, true);
303 const bool cancellationBlocked = waitUntilSleeping(cancellation);
304 const bool callbackRanEarly = static_cast<size_t>(context.callback.calls) != 0;
305
306 context.allowCommit.release();
307 const bool submissionJoined = submission->joinForCompletion();
308 const bool cancellationJoined = cancellation->joinForCompletion();
309 const bool shutdown = controller.shutdown();
310
311 const bool passed =
312 check(commitEntered && cancellationBlocked && !callbackRanEarly && submissionJoined &&
313 cancellationJoined && context.submitted == 1 && context.cancelled == 1 &&
314 context.callback.calls == 1 && context.callback.result == -TransactionError &&
315 controller.reclaimed(0) && !controller.linked(0) && shutdown,
316 "usb-transfer-atomic-publication",
317 "cancellation observed Active before the transfer was fully linked");
318 if (passed)
319 NOTICE("HOSTED-WAIT-TEST: PASS usb-transfer-atomic-publication");
320 return passed;
321}
322
323bool teardownPreservesNaturalAndFailsActive() {
324 TransferLifecycleModel controller;
325 ResultContext natural;
326 ResultContext active;
327 const bool naturalSubmitted =
328 controller.submit(0, recordResult, reinterpret_cast<uintptr_t>(&natural));
329 const bool activeSubmitted =
330 controller.submit(1, recordResult, reinterpret_cast<uintptr_t>(&active));
331 const bool captured = controller.captureNatural(0, 2718);
332 const bool shutdown = controller.shutdown();
333
334 const bool passed =
335 check(naturalSubmitted && activeSubmitted && captured && shutdown && natural.calls == 1 &&
336 natural.result == 2718 && active.calls == 1 && active.result == -TransactionError &&
337 controller.reclaimed(0) && controller.reclaimed(1),
338 "usb-transfer-teardown-results",
339 "teardown lost a natural result or stranded an active obligation");
340 if (passed)
341 NOTICE("HOSTED-WAIT-TEST: PASS usb-transfer-teardown-results");
342 return passed;
343}
344
345struct NestedCancelContext {
346 NestedCancelContext(TransferLifecycleModel* controller, ResultContext* target)
347 : controller(controller), target(target), calls(0), cancelReturned(0) {}
348
349 TransferLifecycleModel* controller;
350 ResultContext* target;
351 Atomic<size_t> calls;
352 Atomic<size_t> cancelReturned;
353};
354
355void cancelSecondFromFirst(uintptr_t parameter, ssize_t result) {
356 auto* context = reinterpret_cast<NestedCancelContext*>(parameter);
357 if (result != -TransactionError)
358 return;
359 context->calls += 1;
360 if (context->controller->cancel(1, recordResult, reinterpret_cast<uintptr_t>(context->target))) {
361 context->cancelReturned += 1;
362 }
363}
364
365bool teardownBatchSupportsNestedCancellation() {
366 TransferLifecycleModel controller;
367 ResultContext second;
368 NestedCancelContext first(&controller, &second);
369 const bool firstSubmitted =
370 controller.submit(0, cancelSecondFromFirst, reinterpret_cast<uintptr_t>(&first));
371 const bool secondSubmitted =
372 controller.submit(1, recordResult, reinterpret_cast<uintptr_t>(&second));
373 const bool shutdown = controller.shutdown();
374
375 const bool passed = check(firstSubmitted && secondSubmitted && shutdown && first.calls == 1 &&
376 first.cancelReturned == 1 && second.calls == 1 &&
377 second.result == -TransactionError && controller.reclaimed(0) &&
378 controller.reclaimed(1),
379 "usb-transfer-teardown-nested-cancel",
380 "a teardown callback could not synchronously drain a later callback");
381 if (passed)
382 NOTICE("HOSTED-WAIT-TEST: PASS usb-transfer-teardown-nested-cancel");
383 return passed;
384}
385} // namespace
386
387bool runHostedUsbTransferLifecycleRegressions() {
388 return cancellationCannotObserveHalfPublishedTransfer() &&
389 teardownPreservesNaturalAndFailsActive() && teardownBatchSupportsNestedCancellation();
390}
Definition List.h:61
Definition Mutex.h:56
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
bool joinForCompletion()
Definition Thread.cc:2771
Status getStatus() const
Definition Thread.h:431
T popFront()
Definition List.h:330
size_t count() const
Definition List.h:212
void pushBack(const T &value)
Definition List.h:216