The Pedigree Project 0.1
fat-sector-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/TargetInfo.h"
10#include "pedigree/kernel/machine/Disk.h"
11#include "pedigree/kernel/processor/PhysicalMemoryManager.h"
12#include "pedigree/kernel/utilities/StringView.h"
13#include "pedigree/kernel/utilities/utility.h"
14
15#include "modules/system/fat/FatFilesystem.h"
16#include "modules/system/vfs/VFS.h"
17
18namespace {
19class TrackingDisk final : public Disk {
20 public:
21 static constexpr size_t PageSize = TargetInfo::getPageSize();
22 static constexpr size_t DataSize = 2 * PageSize;
23
24 TrackingDisk()
25 : m_ReadCount(0),
26 m_WriteCount(0),
27 m_UnpinCount(0),
28 m_Data(new uint8_t[DataSize]),
29 m_BalanceError(false) {
30 ByteSet(m_Data, 0xA5, DataSize);
31 ByteSet(m_ReadLocations, 0, sizeof(m_ReadLocations));
32 ByteSet(m_WriteLocations, 0, sizeof(m_WriteLocations));
33 ByteSet(m_UnpinLocations, 0, sizeof(m_UnpinLocations));
34 ByteSet(m_PageReferences, 0, sizeof(m_PageReferences));
35 }
36
37 ~TrackingDisk() override {
38 delete[] m_Data;
39 }
40
41 BufferView read(uint64_t location) override {
42 if (location >= DataSize) {
43 return BufferView();
44 }
45
46 if (m_ReadCount < 4) {
47 m_ReadLocations[m_ReadCount] = location;
48 }
49 ++m_ReadCount;
50 ++m_PageReferences[location / PageSize];
51 return BufferView(m_Data + location, PageSize - (location % PageSize));
52 }
53
54 void write(uint64_t location) override {
55 if (m_WriteCount < 4) {
56 m_WriteLocations[m_WriteCount] = location;
57 }
58 ++m_WriteCount;
59 }
60
61 bool sync(uint64_t location, bool) override {
62 if (location >= DataSize) {
63 return false;
64 }
65 write(location);
66 return true;
67 }
68
69 size_t getSize() const override {
70 return DataSize;
71 }
72
73 size_t getBlockSize() const override {
74 // Device I/O extent size must not change native-page ownership.
75 return 65536;
76 }
77
78 bool pin(uint64_t location) override {
79 if (location >= DataSize) {
80 return false;
81 }
82 ++m_PageReferences[location / PageSize];
83 return true;
84 }
85
86 void unpin(uint64_t location) override {
87 if (m_UnpinCount < 4) {
88 m_UnpinLocations[m_UnpinCount] = location;
89 }
90 ++m_UnpinCount;
91
92 size_t& references = m_PageReferences[location / PageSize];
93 if (!references) {
94 m_BalanceError = true;
95 return;
96 }
97 --references;
98 }
99
100 bool balanced() const {
101 return !m_BalanceError && !m_PageReferences[0] && !m_PageReferences[1];
102 }
103
104 uint8_t* data() {
105 return m_Data;
106 }
107
108 size_t m_ReadCount;
109 size_t m_WriteCount;
110 size_t m_UnpinCount;
111 uint64_t m_ReadLocations[4];
112 uint64_t m_WriteLocations[4];
113 uint64_t m_UnpinLocations[4];
114
115 private:
116 uint8_t* m_Data;
117 size_t m_PageReferences[2];
118 bool m_BalanceError;
119};
120
121class FatFilesystemHarness : public FatFilesystem {
122 public:
123 void configure(Disk* disk) {
124 m_pDisk = disk;
125 m_Superblock.BPB_BytsPerSec = 512;
126 }
127
128 bool writeSectors(uint32_t sector, size_t size, uintptr_t source) {
129 return writeSectorBlock(sector, size, source);
130 }
131
132 void configureRemoval(Disk* disk, uintptr_t fatTable) {
133 m_pDisk = disk;
134 m_Type = FAT16;
135 m_BlockSize = 512;
136 m_ClusterCount = 254;
137 m_DataAreaStart = 8;
138 m_FatSector = 4;
139 m_Superblock.BPB_BytsPerSec = 512;
140 m_Superblock.BPB_SecPerClus = 1;
141 m_FatCache.insert(0, fatTable);
142 }
143
144 bool removeForTest(File* parent, const String& name, File* file) {
145 return remove(name.view(), parent, file);
146 }
147};
148
149class ShortReadFatFilesystem final : public FatFilesystem {
150 public:
151 ShortReadFatFilesystem() : m_ReadCalls(0) {}
152
153 uint64_t read(File*, uint64_t, uint64_t size, uintptr_t buffer, bool = true) override {
154 ++m_ReadCalls;
155 ByteSet(reinterpret_cast<void*>(buffer), 0x5A, size);
156 return m_ReadCalls == 1 ? size - 1 : size;
157 }
158
159 size_t m_ReadCalls;
160};
161
162class RemovalProbeFile final : public File {
163 public:
164 RemovalProbeFile(const String& name, Filesystem* filesystem, File* parent, size_t& destructions)
165 : File(name, 0, 0, 0, 0, filesystem, 0, parent), m_Destructions(destructions) {}
166
167 ~RemovalProbeFile() override {
168 ++m_Destructions;
169 }
170
171 private:
172 size_t& m_Destructions;
173};
174
175class RemovalDirectory final : public Directory {
176 public:
177 explicit RemovalDirectory(Filesystem* filesystem)
178 : Directory(String("removal-root"), 0, 0, 0, 0, filesystem, 0, nullptr) {}
179
180 void publish(const String& alias, File* file) {
181 addDirectoryEntry(alias, file);
182 }
183
184 bool contains(const String& alias) const {
185 return lookup(HashedStringView(alias)) != nullptr;
186 }
187
188 bool retire(const String& alias, File* file) {
189 return removeDirectoryEntry(HashedStringView(alias), file);
190 }
191};
192
193class RemovalFilesystem final : public Filesystem {
194 public:
195 RemovalFilesystem(size_t& destructions, bool selfRemoving)
196 : m_Root(nullptr),
197 m_First(nullptr),
198 m_Second(nullptr),
199 m_Destructions(destructions),
200 m_SelfRemoving(selfRemoving),
201 m_SecondObservedPriorRetirement(true),
202 m_RemoveCalls(0),
203 m_FirstAlias("cache-first"),
204 m_SecondAlias("cache-second"),
205 m_Label("directory-removal-test") {}
206
207 void configure(RemovalDirectory* root, File* first, File* second) {
208 m_Root = root;
209 m_First = first;
210 m_Second = second;
211 }
212
213 bool initialise(Disk*) override {
214 return true;
215 }
216
217 File* getRoot() const override {
218 return m_Root;
219 }
220
221 const String& getVolumeLabel() const override {
222 return m_Label;
223 }
224
225 bool removeNode(File* parent, const String& filename, File* file) override {
226 ++m_RemoveCalls;
227 if (m_RemoveCalls == 2 && m_Destructions != 1) {
228 m_SecondObservedPriorRetirement = false;
229 }
230
231 return static_cast<RemovalDirectory*>(parent)->retire(filename, file);
232 }
233
234 bool secondObservedPriorRetirement() const {
235 return m_SecondObservedPriorRetirement;
236 }
237
238 size_t removeCalls() const {
239 return m_RemoveCalls;
240 }
241
242 const String& firstAlias() const {
243 return m_FirstAlias;
244 }
245
246 const String& secondAlias() const {
247 return m_SecondAlias;
248 }
249
250 protected:
251 bool createFile(File*, const String&, uint32_t) override {
252 return false;
253 }
254
255 bool createDirectory(File*, const String&, uint32_t) override {
256 return false;
257 }
258
259 bool createSymlink(File*, const String&, const String&) override {
260 return false;
261 }
262
263 private:
264 RemovalDirectory* m_Root;
265 File* m_First;
266 File* m_Second;
267 size_t& m_Destructions;
268 bool m_SelfRemoving;
269 bool m_SecondObservedPriorRetirement;
270 size_t m_RemoveCalls;
271 String m_FirstAlias;
272 String m_SecondAlias;
273 String m_Label;
274};
275
276class RemovalFatDirectory final : public FatDirectory {
277 public:
278 RemovalFatDirectory(FatFilesystem* filesystem, FatFileInfo& info,
279 bool clearInodeAfterRemove = true)
280 : FatDirectory(String("fat-removal-root"), 3, filesystem, nullptr, info),
281 m_Alias("fat-cache-alias"),
282 m_ClearInodeAfterRemove(clearInodeAfterRemove),
283 m_RemoveCalls(0) {}
284
285 void publish(File* file) {
286 addDirectoryEntry(m_Alias, file);
287 }
288
289 bool removeEntry(const String& filename, File* file) override {
290 ++m_RemoveCalls;
291 removeDirectoryEntry(HashedStringView(filename), file);
292 if (m_ClearInodeAfterRemove) {
293 file->setInode(0);
294 }
295 return true;
296 }
297
298 void removeAlias() {
300 }
301
302 bool containsAlias() const {
303 return lookup(HashedStringView(m_Alias)) != nullptr;
304 }
305
306 const String& alias() const {
307 return m_Alias;
308 }
309
310 size_t removeCalls() const {
311 return m_RemoveCalls;
312 }
313
314 private:
315 String m_Alias;
316 bool m_ClearInodeAfterRemove;
317 size_t m_RemoveCalls;
318};
319
320class RetainedFatFile final : public FatFile {
321 public:
322 RetainedFatFile(FatFilesystem* filesystem, File* parent, size_t& destructions)
323 : FatFile(String("fat-intrinsic-name"), 0, 0, 0, 2, filesystem, 0, 3, 0, parent),
324 m_Destructions(destructions) {}
325
326 ~RetainedFatFile() override {
327 ++m_Destructions;
328 }
329
330 private:
331 size_t& m_Destructions;
332};
333
334bool fatSectorPageBoundary() {
335 constexpr size_t TransferSize = TrackingDisk::PageSize;
336 uint8_t* source = new uint8_t[TransferSize];
337 for (size_t i = 0; i < TransferSize; ++i) {
338 source[i] = static_cast<uint8_t>((i * 37) ^ (i >> 3));
339 }
340
341 TrackingDisk disk;
342 FatFilesystemHarness filesystem;
343 filesystem.configure(&disk);
344
345 const bool wrote = filesystem.writeSectors(1, TransferSize, reinterpret_cast<uintptr_t>(source));
346 const bool bytesMatch = !MemoryCompare(disk.data() + 512, source, TransferSize);
347 delete[] source;
348
349 const bool splitAtPage =
350 disk.m_ReadCount == 2 && disk.m_WriteCount == 2 && disk.m_ReadLocations[0] == 512 &&
351 disk.m_ReadLocations[1] == TrackingDisk::PageSize && disk.m_WriteLocations[0] == 512 &&
352 disk.m_WriteLocations[1] == TrackingDisk::PageSize;
353 const bool balanced = disk.m_UnpinCount == 2 && disk.m_UnpinLocations[0] == 512 &&
354 disk.m_UnpinLocations[1] == TrackingDisk::PageSize && disk.balanced();
355
356 const bool passed = wrote && bytesMatch && splitAtPage && balanced;
357 if (passed) {
358 NOTICE("HOSTED-WAIT-TEST: PASS fat-sector-page-boundary");
359 } else {
360 ERROR(
361 "HOSTED-WAIT-TEST: FAIL fat-sector-page-boundary: "
362 "unaligned transfer crossed an unowned cache page or leaked a "
363 "read reference");
364 }
365 return passed;
366}
367
368bool fatShortReadPublication() {
369 ShortReadFatFilesystem filesystem;
370 FatFile file(String("short-read"), 0, 0, 0, 1, &filesystem, 4096);
371
372 const uintptr_t failed = file.readBlock(0);
373 const uintptr_t retried = file.readBlock(0);
374 if (retried) {
375 file.unpinBlock(0);
376 }
377
378 const bool passed = !failed && retried && filesystem.m_ReadCalls == 2;
379 if (passed) {
380 NOTICE("HOSTED-WAIT-TEST: PASS fat-short-read-publication");
381 } else {
382 ERROR(
383 "HOSTED-WAIT-TEST: FAIL fat-short-read-publication: "
384 "a short read was published or its failed Editing page blocked "
385 "retry");
386 }
387 return passed;
388}
389
390bool directoryEmptyRemovalOwnership() {
391 size_t destructions = 0;
392 RemovalFilesystem filesystem(destructions, false);
393 RemovalDirectory directory(&filesystem);
394 RemovalProbeFile* first =
395 new RemovalProbeFile(String("intrinsic-first"), &filesystem, &directory, destructions);
396 RemovalProbeFile* second =
397 new RemovalProbeFile(String("intrinsic-second"), &filesystem, &directory, destructions);
398 filesystem.configure(&directory, first, second);
399 directory.publish(filesystem.firstAlias(), first);
400 directory.publish(filesystem.secondAlias(), second);
401
402 const bool emptied = directory.empty();
403 const bool retiresAsItGoes = emptied && filesystem.removeCalls() == 2 &&
404 filesystem.secondObservedPriorRetirement() && destructions == 2 &&
405 !directory.contains(filesystem.firstAlias()) &&
406 !directory.contains(filesystem.secondAlias());
407
408 bool selfRemovingSafe = false;
409 if (retiresAsItGoes) {
410 size_t selfRemovingDestructions = 0;
411 RemovalFilesystem selfRemovingFilesystem(selfRemovingDestructions, true);
412 RemovalDirectory selfRemovingDirectory(&selfRemovingFilesystem);
413 RemovalProbeFile* selfFirst =
414 new RemovalProbeFile(String("self-intrinsic-first"), &selfRemovingFilesystem,
415 &selfRemovingDirectory, selfRemovingDestructions);
416 RemovalProbeFile* selfSecond =
417 new RemovalProbeFile(String("self-intrinsic-second"), &selfRemovingFilesystem,
418 &selfRemovingDirectory, selfRemovingDestructions);
419 selfRemovingFilesystem.configure(&selfRemovingDirectory, selfFirst, selfSecond);
420 selfRemovingDirectory.publish(selfRemovingFilesystem.firstAlias(), selfFirst);
421 selfRemovingDirectory.publish(selfRemovingFilesystem.secondAlias(), selfSecond);
422
423 selfRemovingSafe = selfRemovingDirectory.empty() && selfRemovingFilesystem.removeCalls() == 2 &&
424 selfRemovingDestructions == 2 &&
425 !selfRemovingDirectory.contains(selfRemovingFilesystem.firstAlias()) &&
426 !selfRemovingDirectory.contains(selfRemovingFilesystem.secondAlias());
427 }
428
429 const bool passed = retiresAsItGoes && selfRemovingSafe;
430 if (passed) {
431 NOTICE("HOSTED-WAIT-TEST: PASS directory-empty-removal-ownership");
432 } else {
433 ERROR(
434 "HOSTED-WAIT-TEST: FAIL directory-empty-removal-ownership: "
435 "removed entries remained alive or were retired twice");
436 }
437 return passed;
438}
439
440bool fatRemoveRetirementOrder() {
441 TrackingDisk disk;
442 uint32_t fatTable[256] = {};
443 fatTable[1] = 0xFFFF;
444
445 FatFilesystemHarness filesystem;
446 filesystem.configureRemoval(&disk, reinterpret_cast<uintptr_t>(fatTable));
447
448 FatFileInfo info = {};
449 RemovalFatDirectory parent(&filesystem, info);
450 size_t destructions = 0;
451 RetainedFatFile* child = new RetainedFatFile(&filesystem, &parent, destructions);
452 parent.publish(child);
453
454 const bool retained = VFS::instance().retainTrackedFile(child);
455 const bool removed = retained && filesystem.removeForTest(&parent, parent.alias(), child);
456 const bool operationPassed =
457 removed && parent.removeCalls() == 1 && !destructions && !parent.containsAlias() &&
458 fatTable[1] == 0 && disk.m_ReadCount == 1 && disk.m_WriteCount == 1 &&
459 disk.m_UnpinCount == 1 && disk.m_ReadLocations[0] == 2048 &&
460 disk.m_WriteLocations[0] == 2048 && disk.m_UnpinLocations[0] == 2048 && disk.balanced();
461
462 bool emergencyWasFinal = false;
463 if (retained) {
464 emergencyWasFinal = VFS::instance().untrackFile(child, false);
465 if (emergencyWasFinal) {
466 delete child;
467 } else if (parent.containsAlias()) {
468 parent.removeAlias();
469 }
470 } else {
471 parent.removeAlias();
472 }
473
474 const bool passed = operationPassed && emergencyWasFinal && destructions == 1;
475 if (passed) {
476 NOTICE("HOSTED-WAIT-TEST: PASS fat-remove-retirement-order");
477 } else {
478 ERROR(
479 "HOSTED-WAIT-TEST: FAIL fat-remove-retirement-order: "
480 "FAT removal read a child after retiring its final cache owner");
481 }
482 return passed;
483}
484
485bool fatRemoveCommitsBeforeClusterReclaim() {
486 TrackingDisk disk;
487 uint32_t fatTable[256] = {};
488
489 FatFilesystemHarness filesystem;
490 filesystem.configureRemoval(&disk, reinterpret_cast<uintptr_t>(fatTable));
491
492 FatFileInfo info = {};
493 RemovalFatDirectory parent(&filesystem, info, false);
494 size_t destructions = 0;
495 RetainedFatFile* child = new RetainedFatFile(&filesystem, &parent, destructions);
496 parent.publish(child);
497
498 const bool retained = VFS::instance().retainTrackedFile(child);
499 const bool removed = retained && filesystem.removeForTest(&parent, parent.alias(), child);
500 const bool operationPassed = removed && parent.removeCalls() == 1 && !destructions &&
501 !parent.containsAlias() && fatTable[1] == 0;
502
503 bool retainedLeaseWasFinal = false;
504 if (retained) {
505 retainedLeaseWasFinal = VFS::instance().untrackFile(child, false);
506 if (retainedLeaseWasFinal) {
507 delete child;
508 } else if (parent.containsAlias()) {
509 parent.removeAlias();
510 }
511 } else {
512 parent.removeAlias();
513 }
514
515 const bool passed = operationPassed && retainedLeaseWasFinal && destructions == 1;
516 if (passed) {
517 NOTICE("HOSTED-WAIT-TEST: PASS fat-remove-commits-before-cluster-reclaim");
518 } else {
519 ERROR(
520 "HOSTED-WAIT-TEST: FAIL fat-remove-commits-before-cluster-reclaim: "
521 "a failed cluster reclaim made a completed namespace removal fail");
522 }
523 return passed;
524}
525} // namespace
526
527EXPORTED_PUBLIC bool runHostedFatSectorRegressions() {
528 bool passed = true;
529 passed &= fatSectorPageBoundary();
530 passed &= fatShortReadPublication();
531 passed &= directoryEmptyRemovalOwnership();
532 passed &= fatRemoveRetirementOrder();
533 passed &= fatRemoveCommitsBeforeClusterReclaim();
534 return passed;
535}
bool addDirectoryEntry(const String &name, File *pTarget)
Definition Directory.cc:817
void remove(const HashedStringView &s)
Definition Directory.cc:579
File * lookup(const HashedStringView &s) const
Definition Directory.cc:329
bool removeDirectoryEntry(const HashedStringView &name, File *expected)
Definition Directory.cc:876
Definition Disk.h:35
virtual bool sync(uint64_t location, bool async)
Definition Disk.cc:358
virtual BufferView read(uint64_t location)
Definition Disk.cc:163
virtual size_t getSize() const
Gets the size of the disk.
Definition Disk.cc:344
virtual void unpin(uint64_t location)=0
virtual void write(uint64_t location)
Definition Disk.cc:340
virtual MUST_USE_RESULT bool pin(uint64_t location)=0
Pins a cache page.
virtual size_t getBlockSize() const
Gets the preferred I/O extent of the disk.
Definition Disk.cc:348
virtual bool removeEntry(const String &filename, File *pFile)
bool writeSectorBlock(uint32_t sec, size_t size, uintptr_t buffer)
Definition File.h:74
virtual uintptr_t readBlock(uint64_t location)
Definition File.cc:1247
virtual void unpinBlock(uint64_t location)
Definition File.cc:1298
virtual bool removeNode(File *parent, const String &filename, File *file)=0
virtual bool initialise(Disk *pDisk)=0
virtual const String & getVolumeLabel() const =0
bool createSymlink(const StringView &path, const String &value, File *pStartNode=0)
bool createDirectory(const StringView &path, uint32_t mask, File *pStartNode=0)
virtual File * getRoot() const =0
bool createFile(const StringView &path, uint32_t mask, File *pStartNode=0)
bool remove(const StringView &path, File *pStartNode=0)
static constexpr size_t getPageSize() noexcept
Definition TargetInfo.h:40
MUST_USE_RESULT bool retainTrackedFile(File *pFile)
Definition VFS.cc:1455
bool untrackFile(File *pFile, bool destroy=true)
Definition VFS.cc:1477
static VFS & instance()
Definition VFS.cc:291