The Pedigree Project 0.1
usb-endpoint-halt-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/Log.h"
9#include "pedigree/kernel/compiler.h"
10#include "pedigree/kernel/utilities/utility.h"
11
12#include "modules/system/usb/UsbConstants.h"
13#include "modules/system/usb/UsbDescriptors.h"
14#include "modules/system/usb/UsbDevice.h"
15#include "modules/system/usb/UsbHub.h"
16
17namespace {
18constexpr size_t MaxTransactions = 2;
19constexpr size_t MaxTransfers = 2;
20constexpr size_t MaxTraceEvents = 5;
21
22enum class TraceEvent { ControlSetup, ControlStatus, ControlComplete, BulkData, BulkComplete };
23
24struct RecordedTransfer {
25 RecordedTransfer() : toggle(false), pid(UsbPidOut), buffer(0), bytes(0) {}
26
27 bool toggle;
28 UsbPid pid;
29 uintptr_t buffer;
30 size_t bytes;
31};
32
33struct RecordedTransaction {
34 RecordedTransaction() : endpoint(), transfers(), transferCount(0), completed(false) {}
35
36 UsbEndpoint endpoint;
37 RecordedTransfer transfers[MaxTransfers];
38 size_t transferCount;
39 bool completed;
40};
41
42class ScriptedEndpointHaltHub final : public UsbHub {
43 public:
44 explicit ScriptedEndpointHaltHub(ssize_t controlResult)
45 : UsbHub(),
46 m_Transactions(),
47 m_TransactionCount(0),
48 m_Trace(),
49 m_TraceCount(0),
50 m_Setup(0, 0, 0, 0, 0),
51 m_ControlResult(controlResult),
52 m_Callbacks(0),
53 m_Cancellations(0),
54 m_Valid(true) {}
55
56 void addTransferToTransaction(uintptr_t transaction, bool toggle, UsbPid pid, uintptr_t buffer,
57 size_t bytes) override {
58 RecordedTransaction* recorded = transactionFor(transaction);
59 if (!recorded || recorded->transferCount >= MaxTransfers) {
60 m_Valid = false;
61 return;
62 }
63
64 RecordedTransfer& transfer = recorded->transfers[recorded->transferCount++];
65 transfer.toggle = toggle;
66 transfer.pid = pid;
67 transfer.buffer = buffer;
68 transfer.bytes = bytes;
69
70 const size_t transactionIndex = transaction - 1;
71 if (transactionIndex == 0 && recorded->transferCount == 1) {
72 appendTrace(TraceEvent::ControlSetup);
73 } else if (transactionIndex == 0 && recorded->transferCount == 2) {
74 appendTrace(TraceEvent::ControlStatus);
75 } else if (transactionIndex == 1 && recorded->transferCount == 1) {
76 appendTrace(TraceEvent::BulkData);
77 } else {
78 m_Valid = false;
79 }
80 }
81
82 uintptr_t createTransaction(UsbEndpoint endpoint) override {
83 if (m_TransactionCount >= MaxTransactions) {
84 m_Valid = false;
85 return static_cast<uintptr_t>(-1);
86 }
87
88 RecordedTransaction& transaction = m_Transactions[m_TransactionCount];
89 transaction.endpoint = endpoint;
90 return ++m_TransactionCount;
91 }
92
93 bool doAsync(uintptr_t transaction, void (*callback)(uintptr_t, ssize_t),
94 uintptr_t parameter) override {
95 RecordedTransaction* recorded = transactionFor(transaction);
96 if (!recorded || recorded->completed || !callback) {
97 m_Valid = false;
98 return false;
99 }
100
101 const size_t transactionIndex = transaction - 1;
102 ssize_t result = 0;
103 if (transactionIndex == 0) {
104 captureControlTransaction(*recorded);
105 result = m_ControlResult;
106 appendTrace(TraceEvent::ControlComplete);
107 } else if (transactionIndex == 1) {
108 if (recorded->transferCount != 1) {
109 m_Valid = false;
110 } else {
111 result = static_cast<ssize_t>(recorded->transfers[0].bytes);
112 }
113 appendTrace(TraceEvent::BulkComplete);
114 } else {
115 m_Valid = false;
116 }
117
118 recorded->completed = true;
119 for (size_t i = 0; i < recorded->transferCount; ++i) {
120 recorded->transfers[i].buffer = 0;
121 }
122
123 ++m_Callbacks;
124 callback(parameter, result);
125 return true;
126 }
127
128 void cancelAsyncAndDrain(uintptr_t, void (*)(uintptr_t, ssize_t), uintptr_t) override {
129 ++m_Cancellations;
130 m_Valid = false;
131 }
132
133 bool addInterruptInHandler(UsbEndpoint, uintptr_t, uint16_t, void (*)(uintptr_t, ssize_t),
134 UsbInterruptInHandle&, uintptr_t) override {
135 m_Valid = false;
136 return false;
137 }
138
139 bool portReset(uint8_t, bool) override {
140 m_Valid = false;
141 return false;
142 }
143
144 bool matches(uint16_t endpointIndex, uint8_t bulkEndpoint, UsbPid bulkPid,
145 bool bulkToggle) const {
146 if (!m_Valid || m_TransactionCount != MaxTransactions || m_Callbacks != MaxTransactions ||
147 m_Cancellations || m_TraceCount != MaxTraceEvents) {
148 return false;
149 }
150
151 const TraceEvent expectedTrace[MaxTraceEvents] = {
152 TraceEvent::ControlSetup, TraceEvent::ControlStatus, TraceEvent::ControlComplete,
153 TraceEvent::BulkData, TraceEvent::BulkComplete};
154 for (size_t i = 0; i < MaxTraceEvents; ++i) {
155 if (m_Trace[i] != expectedTrace[i]) {
156 return false;
157 }
158 }
159
160 const RecordedTransaction& control = m_Transactions[0];
161 const RecordedTransaction& bulk = m_Transactions[1];
162 if (!control.completed || control.endpoint.nEndpoint != 0 || control.transferCount != 2 ||
163 control.transfers[0].toggle || control.transfers[0].pid != UsbPidSetup ||
164 control.transfers[0].bytes != sizeof(UsbDevice::Setup) || !control.transfers[1].toggle ||
165 control.transfers[1].pid != UsbPidIn || control.transfers[1].bytes != 0) {
166 return false;
167 }
168
169 if (m_Setup.nRequestType != UsbRequestRecipient::Endpoint ||
170 m_Setup.nRequest != UsbRequest::ClearFeature || m_Setup.nValue != 0 ||
171 m_Setup.nIndex != endpointIndex || m_Setup.nLength != 0) {
172 return false;
173 }
174
175 return bulk.completed && bulk.endpoint.nEndpoint == bulkEndpoint && bulk.transferCount == 1 &&
176 bulk.transfers[0].toggle == bulkToggle && bulk.transfers[0].pid == bulkPid &&
177 bulk.transfers[0].bytes == 1 && buffersRetired();
178 }
179
180 protected:
181 bool cancelInterruptInAndDrain(const UsbInterruptInToken&, void (*)(uintptr_t, ssize_t),
182 uintptr_t, bool) override {
183 m_Valid = false;
184 return false;
185 }
186
187 private:
188 RecordedTransaction* transactionFor(uintptr_t transaction) {
189 if (!transaction || transaction > m_TransactionCount) {
190 return nullptr;
191 }
192 return &m_Transactions[transaction - 1];
193 }
194
195 void appendTrace(TraceEvent event) {
196 if (m_TraceCount >= MaxTraceEvents) {
197 m_Valid = false;
198 return;
199 }
200 m_Trace[m_TraceCount++] = event;
201 }
202
203 void captureControlTransaction(RecordedTransaction& transaction) {
204 if (transaction.transferCount != 2 || !transaction.transfers[0].buffer ||
205 transaction.transfers[0].bytes != sizeof(UsbDevice::Setup)) {
206 m_Valid = false;
207 return;
208 }
209 MemoryCopy(&m_Setup, reinterpret_cast<void*>(transaction.transfers[0].buffer), sizeof(m_Setup));
210 }
211
212 bool buffersRetired() const {
213 for (size_t i = 0; i < m_TransactionCount; ++i) {
214 for (size_t j = 0; j < m_Transactions[i].transferCount; ++j) {
215 if (m_Transactions[i].transfers[j].buffer) {
216 return false;
217 }
218 }
219 }
220 return true;
221 }
222
223 RecordedTransaction m_Transactions[MaxTransactions];
224 size_t m_TransactionCount;
225 TraceEvent m_Trace[MaxTraceEvents];
226 size_t m_TraceCount;
227 UsbDevice::Setup m_Setup;
228 ssize_t m_ControlResult;
229 size_t m_Callbacks;
230 size_t m_Cancellations;
231 bool m_Valid;
232};
233
234class EndpointHaltTestDevice final : public UsbDevice {
235 public:
236 explicit EndpointHaltTestDevice(UsbHub* hub) : UsbDevice(hub, 1, HighSpeed) {}
237
238 bool clearHalt(Endpoint* endpoint) {
239 return clearEndpointHalt(endpoint);
240 }
241
242 ssize_t bulkIn(Endpoint* endpoint, uintptr_t buffer, size_t bytes) {
243 return syncIn(endpoint, buffer, bytes);
244 }
245
246 ssize_t bulkOut(Endpoint* endpoint, uintptr_t buffer, size_t bytes) {
247 return syncOut(endpoint, buffer, bytes);
248 }
249};
250
251UsbEndpointDescriptor endpointDescriptor(uint8_t endpoint, bool in) {
252 UsbEndpointDescriptor descriptor;
253 ByteSet(&descriptor, 0, sizeof(descriptor));
254 descriptor.nLength = sizeof(descriptor);
255 descriptor.nType = UsbDescriptor::Endpoint;
256 descriptor.nEndpoint = endpoint;
257 descriptor.bDirection = in;
258 descriptor.nTransferType = UsbDevice::Endpoint::Bulk;
259 descriptor.nMaxPacketSize = 64;
260 return descriptor;
261}
262
263bool outEndpointSuccess() {
264 ScriptedEndpointHaltHub hub(8);
265 EndpointHaltTestDevice device(&hub);
266 UsbEndpointDescriptor descriptor = endpointDescriptor(2, false);
267 UsbDevice::Endpoint endpoint(&descriptor, HighSpeed);
268 endpoint.bDataToggle = true;
269 alignas(16) uint8_t buffer[16] = {};
270
271 const bool cleared = device.clearHalt(&endpoint);
272 const ssize_t transferred = device.bulkOut(&endpoint, reinterpret_cast<uintptr_t>(buffer), 1);
273 const bool passed = cleared && transferred == 1 && hub.matches(0x02, 2, UsbPidOut, false);
274 if (passed) {
275 NOTICE("HOSTED-WAIT-TEST: PASS usb-clear-halt-out-endpoint-data0");
276 } else {
277 ERROR(
278 "HOSTED-WAIT-TEST: FAIL usb-clear-halt-out-endpoint-data0: OUT clear did not use "
279 "endpoint address 0x02 and restart bulk traffic at DATA0");
280 }
281 return passed;
282}
283
284bool inEndpointSuccess() {
285 ScriptedEndpointHaltHub hub(8);
286 EndpointHaltTestDevice device(&hub);
287 UsbEndpointDescriptor descriptor = endpointDescriptor(3, true);
288 UsbDevice::Endpoint endpoint(&descriptor, HighSpeed);
289 endpoint.bDataToggle = true;
290 alignas(16) uint8_t buffer[16] = {};
291
292 const bool cleared = device.clearHalt(&endpoint);
293 const ssize_t transferred = device.bulkIn(&endpoint, reinterpret_cast<uintptr_t>(buffer), 1);
294 const bool passed = cleared && transferred == 1 && hub.matches(0x83, 3, UsbPidIn, false);
295 if (passed) {
296 NOTICE("HOSTED-WAIT-TEST: PASS usb-clear-halt-in-endpoint-data0");
297 } else {
298 ERROR(
299 "HOSTED-WAIT-TEST: FAIL usb-clear-halt-in-endpoint-data0: IN clear did not use endpoint "
300 "address 0x83 and restart bulk traffic at DATA0");
301 }
302 return passed;
303}
304
305bool failedClearPreservesToggle() {
306 ScriptedEndpointHaltHub hub(-TransactionError);
307 EndpointHaltTestDevice device(&hub);
308 UsbEndpointDescriptor descriptor = endpointDescriptor(2, false);
309 UsbDevice::Endpoint endpoint(&descriptor, HighSpeed);
310 endpoint.bDataToggle = true;
311 alignas(16) uint8_t buffer[16] = {};
312
313 const bool cleared = device.clearHalt(&endpoint);
314 const bool preserved = endpoint.bDataToggle;
315 const ssize_t transferred = device.bulkOut(&endpoint, reinterpret_cast<uintptr_t>(buffer), 1);
316 const bool passed =
317 !cleared && preserved && transferred == 1 && hub.matches(0x02, 2, UsbPidOut, true);
318 if (passed) {
319 NOTICE("HOSTED-WAIT-TEST: PASS usb-clear-halt-failure-preserves-data1");
320 } else {
321 ERROR(
322 "HOSTED-WAIT-TEST: FAIL usb-clear-halt-failure-preserves-data1: failed ClearFeature "
323 "changed the endpoint toggle or reordered the next transfer");
324 }
325 return passed;
326}
327} // namespace
328
329EXPORTED_PUBLIC bool runHostedUsbEndpointHaltRegressions() {
330 const bool outPassed = outEndpointSuccess();
331 const bool inPassed = inEndpointSuccess();
332 const bool failurePassed = failedClearPreservesToggle();
333 return outPassed && inPassed && failurePassed;
334}
bool clearEndpointHalt(Endpoint *pEndpoint)
Clears a halt on the given endpoint.
Definition UsbDevice.cc:593
virtual void addTransferToTransaction(uintptr_t pTransaction, bool bToggle, UsbPid pid, uintptr_t pBuffer, size_t nBytes)=0
Adds a new transfer to an existent transaction.
virtual uintptr_t createTransaction(UsbEndpoint endpointInfo)=0
Creates a new transaction with the given endpoint data.
virtual bool portReset(uint8_t nPort, bool bErrorResponse=false)=0
Gets a UsbDevice from a given vendor:product pair.
virtual void cancelAsyncAndDrain(uintptr_t pTransaction, void(*pCallback)(uintptr_t, ssize_t), uintptr_t pParam)=0
virtual MUST_USE_RESULT bool addInterruptInHandler(UsbEndpoint endpointInfo, uintptr_t pBuffer, uint16_t nBytes, void(*pCallback)(uintptr_t, ssize_t), UsbInterruptInHandle &handle, uintptr_t pParam=0)=0
Adds an owned recurring interrupt-IN transaction.
virtual MUST_USE_RESULT bool doAsync(uintptr_t pTransaction, void(*pCallback)(uintptr_t, ssize_t)=0, uintptr_t pParam=0)=0
virtual MUST_USE_RESULT bool cancelInterruptInAndDrain(const UsbInterruptInToken &token, void(*callback)(uintptr_t, ssize_t), uintptr_t parameter, bool producerAlreadyStopped)=0