The Pedigree Project 0.1
modules/system/usb-smoke/main.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2#include "pedigree/kernel/LockGuard.h"
3#include "pedigree/kernel/Log.h"
4#include "pedigree/kernel/machine/InputManager.h"
5#include "pedigree/kernel/process/Mutex.h"
6#include "pedigree/kernel/process/Semaphore.h"
7#include "pedigree/kernel/time/Time.h"
8
9#include "modules/Module.h"
10#include "modules/drivers/common/scsi/ScsiDisk.h"
11#include "modules/drivers/common/usb-mass-storage/UsbMassStorageDevice.h"
12#include "modules/system/usb/UsbDevice.h"
13#include "modules/system/vfs/Filesystem.h"
14#include "modules/system/vfs/VFS.h"
15
16extern bool runHostedUsbBotRegressions();
17extern bool runHostedHidReportRegressions();
18
19namespace {
20constexpr uint64_t DiskBytes = 32 * 1024 * 1024;
21constexpr char Magic[] = "PEDIGREE-USB-SMOKE-v1";
22constexpr uint64_t WriteOffsets[] = {8 * 1024 * 1024, 8 * 1024 * 1024 + 4096, DiskBytes - 4096};
23ScsiDisk* scratch = nullptr;
24UsbSpeed storageSpeed = LowSpeed;
25bool duplicate = false;
26Mutex eventLock;
27Semaphore events(0, false);
28uint8_t observed = 0;
29size_t hidDevices = 0;
30uint8_t pattern(uint64_t offset, uint8_t seed = 0x5a) {
31 return ((offset * 37) ^ (offset >> 8) ^ (offset >> 16) ^ seed) & 255;
32}
33bool fail(const char* reason) {
34 ERROR("USB-SMOKE: FAIL " << reason);
35 return false;
36}
37Device* findScratch(Device* device) {
38 if (device->getType() == Device::UsbContainer) {
39 auto* usb = static_cast<UsbDeviceContainer*>(device)->getUsbDevice();
40 if (usb && usb->getUsbState() == UsbDevice::HasDriver && usb->getInterface() &&
41 usb->getInterface()->nClass == 3)
42 ++hidDevices;
43 }
44 if (device->getSpecificType() == String("usb-msd-controller")) {
45 for (size_t i = 0; i < device->getNumChildren(); ++i) {
46 auto* disk = static_cast<ScsiDisk*>(device->getChild(i));
47 if (disk->getSize() == DiskBytes) {
48 duplicate |= scratch != nullptr;
49 scratch = disk;
50 storageSpeed = static_cast<UsbMassStorageDevice*>(device)->getSpeed();
51 }
52 }
53 }
54 return device;
55}
56bool storage() {
57 const auto deadline = Time::getTicks() + 30 * Time::Multiplier::Second;
58 do {
59 scratch = nullptr;
60 duplicate = false;
61 hidDevices = 0;
62 Device::foreach (findScratch);
63 if ((scratch && hidDevices >= 2) || duplicate)
64 break;
65 Time::delay(10 * Time::Multiplier::Millisecond);
66 } while (Time::getTicks() < deadline);
67 auto* filesystem = VFS::instance().getRootFilesystem();
68 Disk* root = filesystem ? filesystem->getDisk() : nullptr;
69 Disk* physicalRoot = root ? root->physicalDisk() : nullptr;
70 if (physicalRoot && physicalRoot->getSpecificType() == String("nvme-disk") &&
71 physicalRoot->getNativeBlockSize() == 4096)
72 NOTICE("USB-SMOKE: root=NVMe-4Kn");
73 if (!scratch || duplicate || hidDevices < 2 || (root && scratch == root->physicalDisk()) ||
74 (scratch->getNativeBlockSize() != 512 && scratch->getNativeBlockSize() != 4096))
75 return fail("unique disposable USB namespace");
76 DiskUse use;
77 if (!scratch->acquireUse(use))
78 return fail("scratch admission");
79 const BufferView header = scratch->read(0);
80 bool valid = bool(header) && header.size() == 4096;
81 for (size_t i = 0; valid && i < header.size(); ++i) {
82 uint8_t expected = 0;
83 if (i < sizeof(Magic) - 1)
84 expected = Magic[i];
85 else if (i == 32)
86 expected = 1;
87 else if (i >= 40 && i < 48)
88 expected = DiskBytes >> ((i - 40) * 8);
89 else if (i >= 48 && i < 52)
90 expected = scratch->getNativeBlockSize() >> ((i - 48) * 8);
91 valid = header[i] == expected;
92 }
93 if (header)
94 scratch->unpin(0);
95 if (!valid)
96 return fail("exact disposable fixture header");
97 NOTICE("USB-SMOKE: PASS fixture-identification");
98 NOTICE("USB-SMOKE: storage-link=" << (storageSpeed >= SuperSpeed ? "SuperSpeed" : "USB1/2"));
99 if (storageSpeed >= SuperSpeed) {
100 // Exercise persistent endpoint and event rings beyond their first cycle.
101 for (size_t page = 0; page < 320; ++page) {
102 const uint64_t offset = 1024 * 1024 + page * 4096;
103 const BufferView view = scratch->read(offset);
104 valid = bool(view) && view.size() == 4096;
105 for (size_t i = 0; valid && i < view.size(); ++i)
106 valid = view[i] == pattern(offset + i);
107 if (view)
108 scratch->unpin(offset);
109 if (!valid || !scratch->retireCachePage(offset))
110 return fail("uncached transfer-ring reads");
111 }
112 NOTICE("USB-SMOKE: PASS sustained-reads");
113 }
114 for (uint64_t offset : WriteOffsets) {
115 const BufferView view = scratch->read(offset);
116 valid = bool(view) && view.size() == 4096;
117 for (size_t i = 0; valid && i < view.size(); ++i)
118 valid = view[i] == pattern(offset + i);
119 if (view) {
120 if (valid) {
121 for (size_t i = 0; i < view.size(); ++i)
122 view[i] = pattern(offset + i, 0xa5);
123 valid = scratch->sync(offset, false);
124 }
125 scratch->unpin(offset);
126 }
127 if (!valid || !scratch->syncAll() || !scratch->retireCachePage(offset))
128 return fail("patterned write, flush or cache retirement");
129 const BufferView reread = scratch->read(offset);
130 valid = bool(reread) && reread.size() == 4096;
131 for (size_t i = 0; valid && i < reread.size(); ++i)
132 valid = reread[i] == pattern(offset + i, 0xa5);
133 if (reread)
134 scratch->unpin(offset);
135 if (!valid)
136 return fail("uncached reread");
137 }
138 if (!scratch->hasNoCacheLoans())
139 return fail("cache pins retained");
140 NOTICE("USB-SMOKE: PASS msd-write-flush-reread");
141 return true;
142}
143void input(InputManager::InputNotification& event) {
144 LockGuard<Mutex> lock(eventLock);
145 uint8_t bits = 0;
146 if (event.type == InputManager::RawKey && event.data.rawkey.scancode == 4)
147 bits |= event.data.rawkey.keyUp ? 2 : 1;
148 if (event.type == InputManager::Mouse) {
149 if (event.data.pointy.relx == 17)
150 bits |= 4;
151 if (event.data.pointy.rely == -9)
152 bits |= 8;
153 if (event.data.pointy.buttons[0])
154 bits |= 16;
155 else if (observed & 16)
156 bits |= 32;
157 }
158 bits &= ~observed;
159 observed |= bits;
160 if (bits) {
161 NOTICE("USB-SMOKE: PASS input-bits=" << Hex << static_cast<size_t>(bits)
162 << " observed=" << static_cast<size_t>(observed));
163 events.release();
164 }
165}
166bool entry() {
167#if CRIPPLE_HDD
168 return fail("CRIPPLE_HDD must be disabled");
169#else
170 if (!runHostedUsbBotRegressions() || !runHostedHidReportRegressions())
171 return fail("BOT or HID contract regressions");
172 NOTICE("USB-SMOKE: PASS bot-hid-contracts");
173 if (!storage())
174 return false;
175 InputManager::instance().installCallback(InputManager::RawKey | InputManager::Mouse, input);
176 NOTICE("USB-SMOKE: READY input");
177 bool complete = false;
178 while (events.acquireForCompletion(1, 30)) {
179 LockGuard<Mutex> lock(eventLock);
180 if (observed == 63) {
181 complete = true;
182 break;
183 }
184 }
186 if (!complete)
187 return fail("keyboard or mouse events missing");
188 NOTICE("USB-SMOKE: PASS complete");
189 return true;
190#endif
191}
192void exit() {}
193} // namespace
194MODULE_INFO("usb-smoke", &entry, &exit, "usb-hid", "usb-mass-storage", "mountroot", "scsi");
static void foreach(Callback callback, Device *root=0)
Definition Device.cc:107
@ UsbContainer
Definition Device.h:89
virtual const String & getSpecificType()
Definition Device.h:183
Definition Disk.h:35
virtual size_t getNativeBlockSize() const
Definition Disk.cc:383
static InputManager & instance()
Singleton design.
void installCallback(CallbackType filter, callback_t callback, void *meta=0)
Installs a callback.
void removeCallback(callback_t callback, void *meta=0)
Removes a callback.
Definition Mutex.h:56
Filesystem * getRootFilesystem() const
Definition VFS.cc:742
static VFS & instance()
Definition VFS.cc:311
@ Hex
Definition Log.h:124
uint8_t scancode
Machine-specific scancode for the key.