The Pedigree Project 0.1
modules/system/nvme-smoke/main.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2#include "pedigree/kernel/Log.h"
3#include "pedigree/kernel/machine/PciExpress.h"
4#include "pedigree/kernel/machine/PciVirtualFunctions.h"
5#include "pedigree/kernel/process/Scheduler.h"
6#include "pedigree/kernel/process/Semaphore.h"
7#include "pedigree/kernel/process/Thread.h"
8#include "pedigree/kernel/time/Time.h"
9#include "pedigree/kernel/utilities/new"
10
11#include "modules/Module.h"
12#include "modules/drivers/common/nvme/NvmeController.h"
13#include "modules/drivers/common/nvme/NvmeDisk.h"
14#include "modules/system/vfs/Filesystem.h"
15#include "modules/system/vfs/VFS.h"
16
17namespace {
18constexpr uint64_t DiskBytes = 32 * 1024 * 1024;
19constexpr char Magic[] = "PEDIGREE-NVME-SMOKE-v1";
20constexpr size_t Workers = 20;
21Disk* rootDisk;
22bool success = true;
23size_t tested = 0;
24NvmeController* virtualizationFixture = nullptr;
25bool hotplugFixture = false;
26Semaphore start(0, false);
27uint8_t pattern(uint64_t offset, uint8_t seed = 0x5a) {
28 return ((offset * 37) ^ (offset >> 8) ^ (offset >> 16) ^ seed) & 255;
29}
30bool fail(const char* reason) {
31 ERROR("NVME-SMOKE: FAIL " << reason);
32 return false;
33}
34bool checkPattern(const uint8_t* data, uint64_t offset, size_t bytes, uint8_t seed = 0x5a) {
35 for (size_t i = 0; i < bytes; ++i) {
36 if (data[i] != pattern(offset + i, seed))
37 return fail("data mismatch");
38 }
39 return true;
40}
41struct Worker {
42 NvmeDisk* disk;
43 size_t index;
44 bool passed;
45};
46int readWorker(void* parameter) {
47 auto* worker = static_cast<Worker*>(parameter);
48 worker->passed = start.acquireForCompletion(1, 30);
49 DiskUse lease;
50 worker->passed &= worker->disk->acquireUse(lease);
51 for (size_t i = 0; worker->passed && i < 16; ++i) {
52 const uint64_t offset =
53 !i ? 4 * 1024 * 1024 : 2 * 1024 * 1024 + worker->index * 65536 + i * 4096;
54 const BufferView view = worker->disk->read(offset);
55 if (!view) {
56 worker->passed = false;
57 break;
58 }
59 worker->passed = view.size() == 4096 &&
60 checkPattern(static_cast<const uint8_t*>(view.data()), offset, view.size());
61 worker->disk->unpin(offset);
62 }
63 return worker->passed ? 0 : 1;
64}
65bool concurrency(NvmeDisk& disk) {
66 Worker workers[Workers]{};
67 Thread* threads[Workers]{};
68 auto* controller = disk.controller();
69 const size_t queueCount = controller->ioQueueCount();
70 const CpuAffinityMask online = Scheduler::onlineAffinity();
71 size_t processors[Nvme::MaxIoQueues] = {};
72 size_t before[Nvme::MaxIoQueues] = {};
73 size_t processorCount = 0;
74 for (size_t cpu = 0; cpu < CpuAffinityMask::MaximumCpus && processorCount < queueCount; ++cpu) {
75 if (online.contains(cpu)) {
76 processors[processorCount++] = cpu;
77 }
78 }
79 if (!queueCount || processorCount != queueCount) {
80 return fail("I/O queue processor placement");
81 }
82 for (size_t i = 0; i < queueCount; ++i) {
83 before[i] = controller->ioQueueInterruptCompletions(i);
84 }
85 for (size_t i = 0; i < Workers; ++i) {
86 workers[i] = {&disk, i, false};
87 ThreadPlacement placement;
88 placement.allowed.set(processors[i % processorCount]);
89 threads[i] = new Thread(Scheduler::instance().getKernelProcess(), readWorker, &workers[i],
90 nullptr, false, false, true, &placement);
91 threads[i]->setName("NVMe smoke read");
92 if (!threads[i]->start())
93 FATAL("NVME-SMOKE: worker start failed");
94 }
95 start.release(Workers);
96 bool passed = true;
97 for (size_t i = 0; i < Workers; ++i) {
98 if (!threads[i]->joinForCompletion())
99 FATAL("NVME-SMOKE: worker join failed");
100 passed &= workers[i].passed;
101 }
102 for (size_t i = 0; i < queueCount; ++i) {
103 const size_t completed = controller->ioQueueInterruptCompletions(i) - before[i];
104 NOTICE("NVME-SMOKE: I/O queue=" << Dec << i + 1 << " interrupt-completions=" << completed);
105 passed &= completed != 0;
106 }
107 NOTICE("NVME-SMOKE: concurrent-read maximum-per-queue-outstanding="
108 << Dec << controller->maximumOutstanding() << " queues=" << queueCount);
109 return (passed && controller->maximumOutstanding() > 1) ||
110 fail("concurrent cached reads, inactive queue, or no overlapping commands");
111}
112bool run(NvmeDisk& disk) {
113 DiskUse lease;
114 if (!disk.acquireUse(lease))
115 return fail("scratch admission");
116 auto* controller = disk.controller();
117 const size_t blockBytes = disk.getNativeBlockSize();
118 const uint32_t nsid = disk.namespaceId();
119 const size_t initialInterrupts = controller->interruptCompletions();
120 uint8_t* buffer = new uint8_t[Nvme::MaxTransfer];
121 bool passed = controller->readWrite(nsid, 0, 4096 / blockBytes, buffer, 4096, false);
122 for (size_t i = 0; passed && i < 4096; ++i) {
123 uint8_t expected = 0;
124 if (i < sizeof(Magic) - 1)
125 expected = Magic[i];
126 else if (i == 32)
127 expected = 1;
128 else if (i >= 40 && i < 48)
129 expected = DiskBytes >> ((i - 40) * 8);
130 else if (i >= 48 && i < 52)
131 expected = blockBytes >> ((i - 48) * 8);
132 passed = buffer[i] == expected;
133 }
134 if (!passed) {
135 delete[] buffer;
136 return fail("exact disposable fixture header");
137 }
138 NOTICE("NVME-SMOKE: PASS fixture nsid=" << Dec << nsid << " block-bytes=" << blockBytes);
139 const size_t transfer = controller->maxTransfer();
140 passed = controller->readWrite(nsid, (1024 * 1024 + 4096) / blockBytes, transfer / blockBytes,
141 buffer, transfer, false) &&
142 checkPattern(buffer, 1024 * 1024 + 4096, transfer);
143 passed &= !controller->readWrite(nsid, disk.getBlockCount(), 1, buffer, blockBytes, false);
144 passed &=
145 !controller->readWrite(nsid, disk.getBlockCount() - 1, 2, buffer, 2 * blockBytes, false);
146 passed &= !controller->readWrite(nsid, 0, 0, buffer, 0, false);
147 const BufferView end = disk.read(DiskBytes);
148 if (end) {
149 disk.unpin(DiskBytes);
150 passed = false;
151 }
152 if (!passed || !concurrency(disk)) {
153 delete[] buffer;
154 return fail("read, PRP list, or range rejection");
155 }
156 const BufferView subBlock = disk.read(1024);
157 passed = bool(subBlock) && subBlock.size() == 3072;
158 if (subBlock) {
159 for (size_t i = 0; passed && i < subBlock.size(); ++i)
160 passed = subBlock[i] == 0;
161 disk.unpin(1024);
162 }
163 if (!passed) {
164 delete[] buffer;
165 return fail("sub-block byte-offset read");
166 }
167 NOTICE("NVME-SMOKE: PASS reads-prps-ranges-concurrency nsid=" << Dec << nsid);
168 const uint64_t rawOffsets[] = {8 * 1024 * 1024, DiskBytes - blockBytes};
169 for (uint64_t offset : rawOffsets) {
170 const size_t bytes = offset == rawOffsets[0] ? transfer : blockBytes;
171 for (size_t i = 0; i < bytes; ++i)
172 buffer[i] = pattern(offset + i, 0xa5);
173 passed =
174 controller->readWrite(nsid, offset / blockBytes, bytes / blockBytes, buffer, bytes, true) &&
175 controller->flush(nsid);
176 if (passed)
177 passed = controller->readWrite(nsid, offset / blockBytes, bytes / blockBytes, buffer, bytes,
178 false) &&
179 checkPattern(buffer, offset, bytes, 0xa5);
180 if (!passed)
181 break;
182 }
183 constexpr uint64_t CachedOffset = 16 * 1024 * 1024;
184 if (passed) {
185 const BufferView view = disk.read(CachedOffset);
186 passed = bool(view) && view.size() == 4096;
187 if (view) {
188 for (size_t i = 0; i < view.size(); ++i)
189 view[i] = pattern(CachedOffset + i, 0xa5);
190 disk.unpin(CachedOffset);
191 passed &= disk.syncPages(&CachedOffset, 1) && disk.sync(CachedOffset, false);
192 }
193 passed &= disk.syncAll() && disk.retireCachePage(CachedOffset);
194 if (passed) {
195 const BufferView reread = disk.read(CachedOffset);
196 passed = bool(reread) && checkPattern(static_cast<const uint8_t*>(reread.data()),
197 CachedOffset, reread.size(), 0xa5);
198 if (reread)
199 disk.unpin(CachedOffset);
200 }
201 }
202 delete[] buffer;
203 const size_t interrupts = controller->interruptCompletions() - initialInterrupts;
204 if (!passed || !interrupts || !disk.hasNoCacheLoans())
205 return fail("write/flush/reread, interrupts, or leaked cache pins");
206 NOTICE("NVME-SMOKE: PASS writes-flush-reread nsid=" << Dec << nsid
207 << " interrupt-completions=" << interrupts);
208 return true;
209}
210Device* visit(Device* device) {
211 if (device->getSpecificType() == "nvme-controller") {
212 auto* controller = static_cast<NvmeController*>(device);
213 if (String(controller->model()) == "PEDIGREE-SRIOV-SMOKE") {
214 virtualizationFixture = controller;
215 } else if (String(controller->model()) == "PEDIGREE-HOTPLUG-SMOKE") {
216 hotplugFixture = true;
217 }
218 }
219 if (device->getSpecificType() == String("nvme-disk") && device != rootDisk &&
220 static_cast<NvmeDisk*>(device)->getSize() == DiskBytes) {
221 const String model(static_cast<NvmeDisk*>(device)->controller()->model());
222 if (model == "PEDIGREE-SRIOV-SMOKE" || model == "PEDIGREE-HOTPLUG-SMOKE") {
223 return device;
224 }
225 if (success)
226 success = run(*static_cast<NvmeDisk*>(device));
227 ++tested;
228 }
229 return device;
230}
231
232bool sriov(NvmeController& original) {
233 using namespace Nvme;
234 if (!original.supportsVirtualizationManagement() || original.getNumChildren() ||
235 !original.prepareDiskRemoval()) {
236 return fail("SR-IOV fixture PF must have no attached namespaces");
237 }
238 uint16_t changed = 0;
239 bool passed =
240 original.virtualizationManagement(original.controllerId(), VirtualResource::Queue,
241 VirtualizationAction::AllocatePrimary, 0, &changed) &&
242 !changed &&
243 original.virtualizationManagement(original.controllerId(), VirtualResource::Interrupt,
244 VirtualizationAction::AllocatePrimary, 0, &changed) &&
245 !changed;
246 Device* pf = original.pciDevice();
247 original.shutdown();
248 PciFunctionState::State resources;
249 if (!PciBus::instance().inspectFunction(pf, resources, false) || (resources.command & 4U)) {
250 return fail("SR-IOV stopped primary resources");
251 }
252 struct Config {
253 Device* device;
254 bool read8(uint16_t offset, uint8_t& value) {
255 return PciBus::instance().readConfig8(device, offset, value);
256 }
257 bool read16(uint16_t offset, uint16_t& value) {
258 return PciBus::instance().readConfig16(device, offset, value);
259 }
260 bool read32(uint16_t offset, uint32_t& value) {
261 return PciBus::instance().readConfig32(device, offset, value);
262 }
263 bool write8(uint16_t offset, uint8_t value) {
264 return PciBus::instance().writeConfig8(device, offset, value);
265 }
266 bool write16(uint16_t offset, uint16_t value) {
267 return PciBus::instance().writeConfig16(device, offset, value);
268 }
269 bool write32(uint16_t offset, uint32_t value) {
270 return PciBus::instance().writeConfig32(device, offset, value);
271 }
272 } config{pf};
273 auto delay = [](size_t milliseconds) {
274 const auto deadline = Time::getTicks() + milliseconds * Time::Multiplier::Millisecond;
275 while (Time::getTicks() < deadline) {
276 Time::delay(Time::Multiplier::Millisecond);
277 }
278 return true;
279 };
280 if (!passed || PciExpress::resetFunction(config, delay) != PciExpress::FlrResult::Complete) {
281 return fail("SR-IOV primary resource reset");
282 }
283 if (!PciBus::instance().updateCommand(pf, 7U, 0)) {
284 return fail("SR-IOV primary decoding after reset");
285 }
286 // Restore assigned resources after FLR, with each 64-bit BAR's high half first.
287 for (size_t i = resources.barCount; i; --i) {
288 if (!PciFunctionState::writeVerified32(config, 0x10 + 4 * (i - 1), resources.bars[i - 1])) {
289 return fail("SR-IOV primary BAR restore");
290 }
291 }
292 if (!config.write8(0x3c, resources.interruptLine) ||
293 !PciFunctionState::resourcesUnchanged(config, resources)) {
294 return fail("SR-IOV primary resource restore");
295 }
296 PciVirtualFunctions group(pf);
297 for (size_t cycle = 0; cycle < 2; ++cycle) {
298 if (!group.enable(1)) {
299 return fail("SR-IOV enable");
300 }
301 auto* primary = new NvmeController(pf);
302 passed = primary->initialiseController();
303 SecondaryControllerList controllers{};
304 uint16_t secondaryId = 0xffff;
305 if (passed && primary->secondaryControllers(0, controllers)) {
306 for (size_t i = 0; i < controllers.count; ++i) {
307 if (controllers.entries[i].virtualFunction == 1 &&
308 controllers.entries[i].primaryControllerId == primary->controllerId()) {
309 secondaryId = controllers.entries[i].controllerId;
310 }
311 }
312 }
313 passed = passed && secondaryId != 0xffff &&
314 primary->virtualizationManagement(secondaryId, VirtualResource::Queue,
315 VirtualizationAction::Offline, 0) &&
316 primary->virtualizationManagement(secondaryId, VirtualResource::Queue,
317 VirtualizationAction::Assign, 2, &changed) &&
318 changed == 2 &&
319 primary->virtualizationManagement(secondaryId, VirtualResource::Interrupt,
320 VirtualizationAction::Assign, 2, &changed) &&
321 changed == 2 &&
322 primary->virtualizationManagement(secondaryId, VirtualResource::Queue,
323 VirtualizationAction::Online, 0) &&
324 primary->setNamespaceAttachment(1, secondaryId, true);
325 Device* vf = group.function(0);
326 auto* secondary = new NvmeController(vf);
327 if (passed && secondary->initialiseController()) {
328 vf->addChild(secondary);
329 secondary->setParent(vf);
330 passed = !group.disable() && secondary->getNumChildren() == 1 &&
331 static_cast<NvmeDisk*>(secondary->getChild(0))->getSize() == DiskBytes &&
332 run(*static_cast<NvmeDisk*>(secondary->getChild(0)));
333 } else {
334 passed = false;
335 }
336 secondary->shutdown();
337 vf->removeChild(secondary);
338 secondary->setParent(nullptr);
339 delete secondary;
340 if (secondaryId != 0xffff) {
341 passed &= primary->setNamespaceAttachment(1, secondaryId, false);
342 passed &= primary->virtualizationManagement(secondaryId, VirtualResource::Queue,
343 VirtualizationAction::Offline, 0);
344 }
345 primary->shutdown();
346 delete primary;
347 passed &= group.disable() && !group.count();
348 if (!passed) {
349 return fail("SR-IOV secondary I/O or retirement");
350 }
351 NOTICE("NVME-SMOKE: PASS sriov-cycle=" << Dec << cycle + 1);
352 }
353 return true;
354}
355
356bool hotplugPresent(DiskUse* use = nullptr) {
357 bool present = false;
358 auto find = [&](Device* device) -> Device* {
359 if (device->getSpecificType() == "nvme-controller") {
360 auto* controller = static_cast<NvmeController*>(device);
361 present |= String(controller->model()) == "PEDIGREE-HOTPLUG-SMOKE";
362 } else if (use && !*use && device->getSpecificType() == "nvme-disk") {
363 auto* disk = static_cast<NvmeDisk*>(device);
364 if (device != rootDisk && disk->getSize() == DiskBytes && disk->getNativeBlockSize() == 512 &&
365 String(disk->controller()->model()) == "PEDIGREE-HOTPLUG-SMOKE") {
366 disk->acquireUse(*use);
367 }
368 }
369 return device;
370 };
371 auto callback = pedigree_std::make_callable(find);
372 Device::foreach (callback, nullptr);
373 return present;
374}
375
376void hotplugDelay(size_t seconds) {
377 const auto deadline = Time::getTicks() + seconds * Time::Multiplier::Second;
378 while (Time::getTicks() < deadline) {
379 Time::delay(50 * Time::Multiplier::Millisecond);
380 }
381}
382
383bool waitHotplug(bool present) {
384 const auto deadline = Time::getTicks() + 30 * Time::Multiplier::Second;
385 do {
386 if (hotplugPresent() == present) {
387 return true;
388 }
389 Time::delay(100 * Time::Multiplier::Millisecond);
390 } while (Time::getTicks() < deadline);
391 return false;
392}
393
394bool hotplug() {
395 for (size_t cycle = 1; cycle <= 2; ++cycle) {
396 DiskUse use;
397 if (!hotplugPresent(&use) || !use) {
398 return fail("hotplug fixture admission");
399 }
400 auto* disk = static_cast<NvmeDisk*>(use.get());
401 if (!run(*disk)) {
402 return false;
403 }
404 NOTICE("NVME-SMOKE: READY hotplug-busy cycle=" << Dec << cycle);
405 hotplugDelay(8);
406 constexpr uint64_t offset = 5 * 1024 * 1024;
407 uint8_t buffer[4096];
408 if (!disk->controller()->readWrite(disk->namespaceId(), offset / 512, 8, buffer, sizeof(buffer),
409 false) ||
410 !checkPattern(buffer, offset, sizeof(buffer))) {
411 return fail("hotplug busy device lost I/O");
412 }
413 use.reset();
414 NOTICE("NVME-SMOKE: PASS hotplug-busy cycle=" << Dec << cycle);
415 NOTICE("NVME-SMOKE: READY hotplug-remove cycle=" << Dec << cycle);
416 if (!waitHotplug(false)) {
417 return fail("hotplug removal timed out");
418 }
419 NOTICE("NVME-SMOKE: PASS hotplug-removed cycle=" << Dec << cycle);
420 // Node retirement precedes the slot worker's power-off completion.
421 hotplugDelay(2);
422 if (cycle == 1) {
423 NOTICE("NVME-SMOKE: READY hotplug-insert");
424 if (!waitHotplug(true)) {
425 return fail("hotplug insertion timed out");
426 }
427 }
428 }
429 return true;
430}
431bool entry() {
432#if CRIPPLE_HDD
433 return fail("CRIPPLE_HDD must be disabled");
434#else
435 auto* filesystem = VFS::instance().getRootFilesystem();
436 rootDisk = filesystem ? filesystem->getDisk() : nullptr;
437 rootDisk = rootDisk ? rootDisk->physicalDisk() : nullptr;
438 if (rootDisk && rootDisk->getSpecificType() == String("nvme-disk"))
439 NOTICE("NVME-SMOKE: root namespace=" << Dec << static_cast<NvmeDisk*>(rootDisk)->namespaceId());
440 Device::foreach (visit);
441 if (success && virtualizationFixture) {
442 success = sriov(*virtualizationFixture);
443 }
444 if (success && hotplugFixture) {
445 success = hotplug();
446 }
447 if (!tested || !success)
448 return fail("scratch namespace checks");
449 NOTICE("NVME-SMOKE: PASS complete namespaces=" << Dec << tested);
450 return true;
451#endif
452}
453void exit() {}
454} // namespace
455MODULE_INFO("nvme-smoke", &entry, &exit, "nvme", "mountroot", "scsi");
size_t getNumChildren()
Definition Device.cc:143
static void foreach(Callback callback, Device *root=0)
Definition Device.cc:107
void addChild(Device *pDevice)
Definition Device.cc:135
virtual const String & getSpecificType()
Definition Device.h:183
void removeChild(size_t n)
Definition Device.cc:147
Definition Disk.h:35
size_t getSize() const override
Gets the size of the disk.
Definition NvmeDisk.h:12
size_t getNativeBlockSize() const override
Definition NvmeDisk.h:19
static Scheduler & instance()
Definition Scheduler.h:96
virtual MUST_USE_RESULT bool retireCachePage(uint64_t location)
Definition ScsiDisk.cc:1150
bool syncPages(const uint64_t *locations, size_t count) override
Definition ScsiDisk.cc:1036
virtual void unpin(uint64_t location)
Definition ScsiDisk.cc:1595
virtual MUST_USE_RESULT bool syncAll()
Definition ScsiDisk.cc:1122
virtual BufferView read(uint64_t location)
Definition ScsiDisk.cc:460
virtual bool sync(uint64_t location, bool async)
Definition ScsiDisk.cc:984
Filesystem * getRootFilesystem() const
Definition VFS.cc:742
static VFS & instance()
Definition VFS.cc:311
@ Dec
Definition Log.h:126