The Pedigree Project 0.1
modules/system/ahci-smoke/main.cc
1/* Copyright (c) 2026, Pedigree Developers. SPDX-License-Identifier: ISC */
2
3#include "pedigree/kernel/Log.h"
4#include "pedigree/kernel/machine/Disk.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/String.h"
10#include "pedigree/kernel/utilities/new"
11
12#include "modules/Module.h"
13#include "modules/drivers/common/ahci/AhciController.h"
14#include "modules/drivers/common/ahci/AhciDisk.h"
15#include "modules/system/vfs/Filesystem.h"
16#include "modules/system/vfs/VFS.h"
17
18namespace {
19constexpr uint64_t DiskBytes = 32 * 1024 * 1024;
20constexpr size_t HeaderBytes = 4096;
21constexpr uint8_t BaseSeed = 0x5a;
22constexpr uint8_t WriteSeed = 0xa5;
23constexpr uint64_t ErrorOffset = 24 * 1024 * 1024;
24constexpr char Magic[] = "PEDIGREE-AHCI-SMOKE-v1";
25
26struct Range {
27 uint64_t offset;
28 size_t length;
29};
30constexpr Range ReadRanges[] = {
31 {4096, 4096}, {65536 - 512, 8192}, {1024 * 1024 - 512, 128 * 1024}, {DiskBytes - 4096, 4096}};
32constexpr Range WriteRanges[] = {
33 {8 * 1024 * 1024 + 512, 8192}, {16 * 1024 * 1024 - 512, 128 * 1024}, {DiskBytes - 512, 512}};
34
35AhciDisk* scratch = nullptr;
36bool duplicateScratch = false;
37uint8_t fixtureFlags = 0;
38
39bool fail(const char* reason) {
40 ERROR("AHCI-SMOKE: FAIL " << reason);
41 return false;
42}
43
44uint8_t pattern(uint64_t offset, uint8_t seed) {
45 return ((offset * 37) ^ (offset >> 8) ^ (offset >> 16) ^ seed) & 255;
46}
47
48uint8_t expected(uint64_t offset, bool written) {
49 if (offset < HeaderBytes) {
50 if (offset < sizeof(Magic) - 1)
51 return Magic[offset];
52 if (offset == 32)
53 return 1;
54 if (offset == 36)
55 return BaseSeed;
56 if (offset >= 40 && offset < 48)
57 return (DiskBytes >> ((offset - 40) * 8)) & 255;
58 if (offset == 48)
59 return fixtureFlags;
60 return 0;
61 }
62 if (written) {
63 for (const Range& range : WriteRanges) {
64 if (offset >= range.offset && offset < range.offset + range.length)
65 return pattern(offset, WriteSeed);
66 }
67 }
68 return pattern(offset, BaseSeed);
69}
70
71Device* findScratch(Device* device) {
72 if (device->getSpecificType() == String("ahci-disk")) {
73 AhciDisk* disk = static_cast<AhciDisk*>(device);
74 if (disk->port() == 1 && disk->getSize() == DiskBytes) {
75 if (scratch)
76 duplicateScratch = true;
77 else
78 scratch = disk;
79 }
80 }
81 return device;
82}
83
84bool checkRange(AhciDisk& disk, const Range& range, bool written) {
85 BufferView storage[40];
86 BufferViewSequence views(storage, 40);
87 if (!disk.readViews(range.offset, range.length, views))
88 return fail("readViews");
89 bool valid = true;
90 uint64_t offset = range.offset;
91 for (size_t viewIndex = 0; viewIndex < views.count(); ++viewIndex) {
92 const BufferView view = views[viewIndex];
93 for (size_t i = 0; i < view.size(); ++i) {
94 if (view[i] != expected(offset + i, written)) {
95 ERROR("AHCI-SMOKE: FAIL byte mismatch at " << Dec << (offset + i));
96 valid = false;
97 break;
98 }
99 }
100 offset += view.size();
101 }
102 disk.unpinViews(range.offset, views);
103 return valid;
104}
105
106constexpr size_t Workers = 12;
107Semaphore startReaders(0, false);
108struct Reader {
109 AhciDisk* disk;
110 size_t index;
111 bool success;
112};
113int readWorker(void* parameter) {
114 auto* reader = static_cast<Reader*>(parameter);
115 reader->success = startReaders.acquireForCompletion(1, 30);
116 for (size_t i = 0; reader->success && i < 16; ++i) {
117 const uint64_t offset =
118 i == 0 ? 2 * 1024 * 1024 : 3 * 1024 * 1024 + reader->index * 65536 + i * 4096;
119 reader->success = checkRange(*reader->disk, {offset, 4096}, false);
120 }
121 return reader->success ? 0 : 1;
122}
123bool concurrentReads(AhciDisk& disk) {
124 Reader readers[Workers]{};
125 Thread* threads[Workers]{};
126 for (size_t i = 0; i < Workers; ++i) {
127 readers[i] = {&disk, i, false};
128 threads[i] = new Thread(Scheduler::instance().getKernelProcess(), readWorker, &readers[i],
129 nullptr, false, false, true);
130 if (!threads[i]->start())
131 FATAL("AHCI-SMOKE: reader start failed");
132 }
133 startReaders.release(Workers);
134 bool passed = true;
135 for (size_t i = 0; i < Workers; ++i) {
136 if (!threads[i]->joinForCompletion())
137 FATAL("AHCI-SMOKE: reader join failed");
138 passed &= readers[i].success;
139 }
140 const size_t maximum = disk.controller()->maximumOutstanding(disk.port());
141 if (!passed || maximum < 2)
142 return fail("concurrent reads did not overlap commands");
143 NOTICE("AHCI-SMOKE: PASS concurrent-reads maximum-outstanding=" << Dec << maximum);
144 return true;
145}
146
147bool writeRange(AhciDisk& disk, const Range& range) {
148 BufferView storage[40];
149 uint64_t pageOffsets[40];
150 BufferViewSequence views(storage, 40);
151 if (!disk.readViews(range.offset, range.length, views))
152 return fail("write readViews");
153 const size_t count = views.count();
154 uint64_t offset = range.offset;
155 bool synced = true;
156 for (size_t viewIndex = 0; viewIndex < count; ++viewIndex) {
157 const BufferView view = views[viewIndex];
158 pageOffsets[viewIndex] = offset;
159 for (size_t i = 0; i < view.size(); ++i)
160 view[i] = pattern(offset + i, WriteSeed);
161 // Keep one legacy single-page flush covered while this view is writable.
162 if (!viewIndex && !disk.sync(offset, false))
163 synced = false;
164 offset += view.size();
165 }
166 disk.unpinViews(range.offset, views);
167 if (!synced || !disk.syncPages(pageOffsets, count) || !disk.syncAll())
168 return fail("checked disk sync");
169 // Retire only after dropping every pin, so the reread must reach the disk.
170 for (size_t i = 0; i < count; ++i) {
171 if (!disk.retireCachePage(pageOffsets[i]))
172 return fail("cache retirement");
173 }
174 return true;
175}
176
177bool checkRejectedRanges(AhciDisk& disk) {
178 const BufferView pastEnd = disk.read(DiskBytes);
179 if (pastEnd) {
180 disk.unpin(DiskBytes);
181 return fail("end-of-disk read accepted");
182 }
183 BufferView storage[1];
184 BufferViewSequence views(storage, 1);
185 const Range invalid[] = {{512, ~size_t{0}}, {DiskBytes - 512, 1024}};
186 for (const Range& range : invalid) {
187 const bool accepted = disk.readViews(range.offset, range.length, views);
188 const bool empty = views.empty();
189 disk.unpinViews(range.offset, views);
190 if (accepted || !empty)
191 return fail("invalid readViews range accepted or retained views");
192 }
193 if (!disk.hasNoCacheLoans())
194 return fail("cache pins retained after rejected ranges");
195 return true;
196}
197
198bool checkInjectedError(AhciDisk& disk, AhciDisk& root) {
199 DiskUse rootUse;
200 if (!root.acquireUse(rootUse))
201 return fail("root admission after persistence tests");
202 if (root.getSize() < 512 || root.getSize() % 512)
203 return fail("root tail-sector geometry");
204 const uint64_t rootLba = root.getSize() / 512 - 1;
205 uint8_t rootSector[512];
206 if (!root.controller()->readWrite(root.port(), rootLba, 1, rootSector, sizeof(rootSector), false))
207 return fail("direct root reference read");
208
209 constexpr uint64_t RetryOffset = 28 * 1024 * 1024;
210 if (!disk.retireCachePage(ErrorOffset) || !disk.retireCachePage(RetryOffset))
211 return fail("error-test cache retirement");
212 const auto started = Time::getTicks();
213 const BufferView failed = disk.read(ErrorOffset);
214 const auto elapsed = Time::getTicks() - started;
215 if (failed) {
216 disk.unpin(ErrorOffset);
217 return fail("injected read error returned data");
218 }
219 if (elapsed >= 5 * Time::Multiplier::Second || !disk.hasNoCacheLoans())
220 return fail("injected read error was slow or retained cache pins");
221 NOTICE("AHCI-SMOKE: PASS transport-error milliseconds="
222 << Dec << elapsed / Time::Multiplier::Millisecond);
223
224 const auto retryStarted = Time::getTicks();
225 const BufferView retry = disk.read(RetryOffset);
226 const auto retryElapsed = Time::getTicks() - retryStarted;
227 if (retry) {
228 disk.unpin(RetryOffset);
229 return fail("offline scratch disk accepted an uncached read");
230 }
231 if (retryElapsed >= 5 * Time::Multiplier::Second || !disk.hasNoCacheLoans())
232 return fail("offline rejection was slow or retained cache pins");
233 NOTICE("AHCI-SMOKE: PASS offline-rejection milliseconds="
234 << Dec << retryElapsed / Time::Multiplier::Millisecond);
235
236 // Mounted ext2 metadata retains cache pins. Direct transport reads prove
237 // port isolation without attempting to evict any filesystem-owned page.
238 uint8_t after[512];
239 if (!root.controller()->readWrite(root.port(), rootLba, 1, after, sizeof(after), false))
240 return fail("direct root read after scratch error");
241 bool unchanged = true;
242 for (size_t i = 0; unchanged && i < sizeof(rootSector); ++i)
243 unchanged = after[i] == rootSector[i];
244 if (!unchanged)
245 return fail("root sector changed after scratch error");
246 NOTICE("AHCI-SMOKE: PASS root-after-error port=0 direct-sector=" << Dec << rootLba);
247 return true;
248}
249
250bool entry() {
251#if CRIPPLE_HDD
252 return fail("CRIPPLE_HDD must be disabled");
253#else
255 Disk* root = filesystem ? filesystem->getDisk() : nullptr;
256 root = root ? root->physicalDisk() : nullptr;
257 if (!root || root->getSpecificType() != String("ahci-disk")) {
258 NOTICE("AHCI-SMOKE: skipped (root uses another transport)");
259 return true;
260 }
261 AhciDisk* rootDisk = static_cast<AhciDisk*>(root);
262 AhciController* controller = rootDisk->controller();
263 if (rootDisk->port() != 0 || !controller ||
264 controller->getSpecificType() != String("ahci-controller"))
265 return fail("root AHCI controller or port");
266
267 scratch = nullptr;
268 duplicateScratch = false;
269 Device::foreach (findScratch);
270 if (!scratch || duplicateScratch || scratch->controller() != controller || scratch == rootDisk ||
271 (scratch->getNativeBlockSize() != 512 && scratch->getNativeBlockSize() != 4096))
272 return fail("unique scratch disk on AHCI port 1");
273 DiskUse use;
274 if (!scratch->acquireUse(use))
275 return fail("scratch disk admission");
276 const size_t initialCompletions = controller->interruptCompletions();
277 const BufferView fixtureHeader = scratch->read(0);
278 if (!fixtureHeader)
279 return fail("fixture header read");
280 fixtureFlags = fixtureHeader.size() > 48 ? fixtureHeader[48] : 255;
281 scratch->unpin(0);
282 if (fixtureFlags > 1)
283 return fail("unsupported fixture flags");
284 // Every byte of the immutable fixture header must match before any write.
285 if (!checkRange(*scratch, {0, HeaderBytes}, false))
286 return false;
287 NOTICE("AHCI-SMOKE: PASS fixture-identification");
288 if (!checkRejectedRanges(*scratch))
289 return false;
290 NOTICE("AHCI-SMOKE: PASS range-rejection");
291 for (const Range& range : ReadRanges) {
292 if (!checkRange(*scratch, range, false))
293 return false;
294 }
295 NOTICE("AHCI-SMOKE: PASS patterned-reads");
296 for (size_t i = 0; i < 2; ++i) {
297 if (!scratch->sync(ReadRanges[0].offset, false) || !scratch->syncAll()) {
298 return fail("clean page sync");
299 }
300 }
301 NOTICE("AHCI-SMOKE: PASS clean-page-sync");
302 if (!concurrentReads(*scratch))
303 return false;
304 for (const Range& range : WriteRanges) {
305 if (!writeRange(*scratch, range))
306 return false;
307 }
308 NOTICE("AHCI-SMOKE: PASS writes-and-sync");
309 for (const Range& range : WriteRanges) {
310 if (!checkRange(*scratch, range, true))
311 return false;
312 }
313 for (const Range& range : ReadRanges) {
314 if (!checkRange(*scratch, range, true))
315 return false;
316 }
317 NOTICE("AHCI-SMOKE: PASS uncached-rereads");
318 NOTICE("AHCI-SMOKE: PASS ahci-root-mount port=0");
319 const size_t completions = controller->interruptCompletions() - initialCompletions;
320 if (!completions)
321 return fail("no interrupt completions during smoke");
322 NOTICE("AHCI-SMOKE: PASS interrupt-completions count=" << Dec << completions);
323 if (fixtureFlags && !checkInjectedError(*scratch, *rootDisk))
324 return false;
325 NOTICE("AHCI-SMOKE: PASS complete");
326 return true;
327#endif
328}
329
330void exit() {}
331} // namespace
332
333MODULE_INFO("ahci-smoke", &entry, &exit, "ahci", "mountroot", "scsi");
size_t getSize() const override
Gets the size of the disk.
Definition AhciDisk.cc:145
static void foreach(Callback callback, Device *root=0)
Definition Device.cc:107
virtual const String & getSpecificType()
Definition Device.h:183
Definition Disk.h:35
void unpinViews(uint64_t location, BufferViewSequence &views)
Definition Disk.cc:330
MUST_USE_RESULT bool readViews(uint64_t location, size_t length, BufferViewSequence &views)
Definition Disk.cc:303
Disk * getDisk()
Definition Filesystem.h:145
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
Definition waits.c:9