The Pedigree Project 0.1
vfs-callback-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/Atomic.h"
9#include "pedigree/kernel/Log.h"
10#include "pedigree/kernel/machine/Disk.h"
11#include "pedigree/kernel/process/ExecutionContext.h"
12#include "pedigree/kernel/process/Scheduler.h"
13#include "pedigree/kernel/process/Semaphore.h"
14#include "pedigree/kernel/process/Thread.h"
15#include "pedigree/kernel/processor/Processor.h"
16
17#include "modules/system/vfs/Filesystem.h"
18#include "modules/system/vfs/VFS.h"
19
20namespace {
21constexpr size_t Attempts = 10000;
22constexpr bool DontPickCore = HOSTED;
23
24class TestDisk final : public Disk {
25 public:
26 BufferView read(uint64_t) override {
27 return BufferView();
28 }
29
30 size_t getSize() const override {
31 return 0;
32 }
33
34 size_t getBlockSize() const override {
35 return 512;
36 }
37
38 bool pin(uint64_t) override {
39 return false;
40 }
41
42 void unpin(uint64_t) override {}
43};
44
45class BlockingNameDisk final : public Disk {
46 public:
47 BlockingNameDisk()
48 : nameEntered(0),
49 nameRelease(0),
50 nameCalls(0),
51 nameProcessor(static_cast<size_t>(-1)),
52 failures(0) {}
53
54 void getName(String& name) override {
55 const size_t call = nameCalls += 1;
56 if (call == static_cast<size_t>(1)) {
57 nameProcessor = Processor::id();
58 nameEntered.release();
59 if (!nameRelease.acquireForCompletion()) {
60 failures += 1;
61 }
62 }
63 name.assign("mount-table-disk");
64 }
65
66 BufferView read(uint64_t) override {
67 return BufferView();
68 }
69
70 size_t getSize() const override {
71 return 0;
72 }
73
74 size_t getBlockSize() const override {
75 return 512;
76 }
77
78 bool pin(uint64_t) override {
79 return false;
80 }
81
82 void unpin(uint64_t) override {}
83
84 Semaphore nameEntered;
85 Semaphore nameRelease;
86 Atomic<size_t> nameCalls;
87 Atomic<size_t> nameProcessor;
88 Atomic<size_t> failures;
89};
90
91class TestFilesystem final : public Filesystem {
92 public:
93 explicit TestFilesystem(Atomic<size_t>* destructions, VFS* registry = nullptr,
94 Atomic<size_t>* reentries = nullptr,
95 Atomic<size_t>* reentryMounts = nullptr)
96 : m_Destructions(destructions),
97 m_Registry(registry),
98 m_Reentries(reentries),
99 m_ReentryMounts(reentryMounts),
100 m_Label("vfs-callback-test") {}
101
102 ~TestFilesystem() override {
103 if (m_Registry) {
105 m_Registry->getMounts(mounts);
106 *m_Reentries += 1;
107 *m_ReentryMounts += mounts.count();
108 }
109 if (m_Destructions) {
110 *m_Destructions += 1;
111 }
112 }
113
114 bool initialise(Disk* disk) override {
115 m_pDisk = disk;
116 return true;
117 }
118
119 File* getRoot() const override {
120 return nullptr;
121 }
122
123 const String& getVolumeLabel() const override {
124 return m_Label;
125 }
126
127 protected:
128 bool createFile(File*, const String&, uint32_t) override {
129 return false;
130 }
131
132 bool createDirectory(File*, const String&, uint32_t) override {
133 return false;
134 }
135
136 bool createSymlink(File*, const String&, const String&) override {
137 return false;
138 }
139
140 bool removeNode(File*, const String&, File*) override {
141 return false;
142 }
143
144 private:
145 Atomic<size_t>* m_Destructions;
146 VFS* m_Registry;
147 Atomic<size_t>* m_Reentries;
148 Atomic<size_t>* m_ReentryMounts;
149 String m_Label;
150};
151
152bool check(bool condition, const char* test, const char* detail) {
153 if (condition) {
154 return true;
155 }
156#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
157 ERROR("QEMU-CONCURRENCY-TEST: FAIL " << test << ": " << detail);
158#else
159 ERROR("HOSTED-WAIT-TEST: FAIL " << test << ": " << detail);
160#endif
161 return false;
162}
163
164bool waitForCallbackDrain(Thread* thread, uintptr_t callback) {
165 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
166 Thread::WaitDebugInfo info = {};
167 uintptr_t debugAddress = 0;
168 if (thread->getWaitDebugInfo(info) && info.queue && info.channelOwner && info.queued &&
169 thread->getDebugState(debugAddress) == Thread::CallbackDrain && debugAddress == callback) {
170 return true;
171 }
173 }
174 return false;
175}
176
177bool waitForSemaphoreBlock(Thread* thread) {
178 for (size_t attempt = 0; attempt < Attempts; ++attempt) {
179 Thread::WaitDebugInfo info = {};
180 uintptr_t debugAddress = 0;
181 if (thread->getWaitDebugInfo(info) && info.queue && info.channelOwner && info.queued &&
182 thread->getDebugState(debugAddress) == Thread::SemWait) {
183 return true;
184 }
186 }
187 return false;
188}
189
190struct ProbeDrainContext {
191 ProbeDrainContext()
192 : callbackEntered(0),
193 callbackRelease(0),
194 callbackCalls(0),
195 callbackAfterRemoval(0),
196 removalReturned(0),
197 removalSucceeded(0),
198 destroyedFilesystems(0),
199 callbackProcessor(static_cast<size_t>(-1)),
200 removerProcessor(static_cast<size_t>(-1)),
201 failures(0),
202 mountResult(false),
203 mounted(nullptr) {}
204
205 VFS registry;
206 TestDisk disk;
207 Semaphore callbackEntered;
208 Semaphore callbackRelease;
209 Atomic<size_t> callbackCalls;
210 Atomic<size_t> callbackAfterRemoval;
211 Atomic<size_t> removalReturned;
212 Atomic<size_t> removalSucceeded;
213 Atomic<size_t> destroyedFilesystems;
214 Atomic<size_t> callbackProcessor;
215 Atomic<size_t> removerProcessor;
216 Atomic<size_t> failures;
217 bool mountResult;
218 Filesystem* mounted;
219};
220
221ProbeDrainContext* g_ProbeDrainContext = nullptr;
222
223Filesystem* blockingProbe(Disk*) {
224 ProbeDrainContext* context = g_ProbeDrainContext;
225 context->callbackCalls += 1;
226 context->callbackProcessor = Processor::id();
227 context->callbackEntered.release();
228 if (!context->callbackRelease.acquireForCompletion()) {
229 context->failures += 1;
230 }
231 if (context->removalReturned) {
232 context->callbackAfterRemoval += 1;
233 }
234 return new TestFilesystem(&context->destroyedFilesystems);
235}
236
237int dispatchBlockingProbe(void* parameter) {
238 ProbeDrainContext* context = reinterpret_cast<ProbeDrainContext*>(parameter);
239 String stableName;
240 context->mountResult = context->registry.mount(&context->disk, stableName, &context->mounted);
241 return 0;
242}
243
244int removeBlockingProbe(void* parameter) {
245 ProbeDrainContext* context = reinterpret_cast<ProbeDrainContext*>(parameter);
246 context->removerProcessor = Processor::id();
247 context->removalSucceeded = context->registry.removeProbeCallback(blockingProbe) ? 1 : 0;
248 context->removalReturned = 1;
249 return 0;
250}
251
252bool probeCallbackDrain(size_t& callbackProcessor, size_t& removerProcessor) {
253 ProbeDrainContext context;
254 g_ProbeDrainContext = &context;
255 context.registry.addProbeCallback(blockingProbe);
256
257 Process* process = Scheduler::instance().getKernelProcess();
258 Thread* dispatcher =
259 new Thread(process, dispatchBlockingProbe, &context, nullptr, false, DontPickCore);
260 dispatcher->setName("VFS blocking probe dispatcher");
261 const bool callbackEntered = context.callbackEntered.acquireForCompletion();
262 const bool interruptsWereEnabled = Processor::getInterrupts();
263 bool nonWaitableRemoval = false;
264 bool nonWaitableContextObserved = false;
265 {
266 ExecutionContextGuard nonWaitable(ExecutionContext::HardDeviceIrq);
267 nonWaitableRemoval = context.registry.removeProbeCallback(blockingProbe);
268 nonWaitableContextObserved = Processor::getInterrupts() &&
269 Processor::executionContext() == ExecutionContext::HardDeviceIrq;
270 }
271 const bool waitableContextRestored =
272 Processor::executionContext() == ExecutionContext::WaitableThread;
274 const bool nonYieldingRemoval = context.registry.removeProbeCallback(blockingProbe);
275 const bool interruptsStayedDisabled = !Processor::getInterrupts();
276 Processor::setInterrupts(interruptsWereEnabled);
277 Thread* remover =
278 new Thread(process, removeBlockingProbe, &context, nullptr, false, DontPickCore);
279 remover->setName("VFS blocking probe remover");
280 const bool observedDrain =
281 callbackEntered && waitForCallbackDrain(remover, reinterpret_cast<uintptr_t>(blockingProbe));
282 const bool returnedEarly = context.removalReturned != static_cast<size_t>(0);
283
284 context.callbackRelease.release();
285 const bool dispatcherJoined = dispatcher->joinForCompletion();
286 const bool removerJoined = remover->joinForCompletion();
287 const size_t callsAfterRemoval = context.callbackCalls;
288 String stableName;
289 Filesystem* unexpected = nullptr;
290 const bool mountedAfterRemoval = context.registry.mount(&context.disk, stableName, &unexpected);
291 g_ProbeDrainContext = nullptr;
292
293 if (unexpected) {
294 context.registry.unregisterFilesystem(unexpected);
295 }
296 callbackProcessor = context.callbackProcessor;
297 removerProcessor = context.removerProcessor;
298
299 const bool passed =
300 check(callbackEntered && interruptsWereEnabled && !nonWaitableRemoval &&
301 nonWaitableContextObserved && waitableContextRestored && !nonYieldingRemoval &&
302 interruptsStayedDisabled && observedDrain && !returnedEarly && dispatcherJoined &&
303 removerJoined && !context.failures &&
304 context.removalSucceeded == static_cast<size_t>(1),
305 "vfs-probe-callback-lifetime",
306 "probe removal blocked without yield permission or did not drain on retry") &&
307 check(!context.mountResult && !context.mounted && !mountedAfterRemoval && !unexpected &&
308 callsAfterRemoval == static_cast<size_t>(1) &&
309 context.callbackCalls == callsAfterRemoval && !context.callbackAfterRemoval &&
310 context.destroyedFilesystems == static_cast<size_t>(1),
311 "vfs-probe-callback-lifetime",
312 "a closed probe published or leaked its returned filesystem");
313 if (passed) {
314#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
315 NOTICE("QEMU-CONCURRENCY-TEST: PASS vfs-probe-callback-lifetime");
316#else
317 NOTICE("HOSTED-WAIT-TEST: PASS vfs-probe-callback-lifetime");
318#endif
319 }
320 return passed;
321}
322
323struct MountDrainContext {
324 MountDrainContext()
325 : callbackEntered(0),
326 callbackRelease(0),
327 callbackCalls(0),
328 callbackAfterRemoval(0),
329 removalReturned(0),
330 removalSucceeded(0),
331 probeRemovalReturned(0),
332 probeRemovalSucceeded(0),
333 destroyedFilesystems(0),
334 callbackProcessor(static_cast<size_t>(-1)),
335 removerProcessor(static_cast<size_t>(-1)),
336 failures(0),
337 mountResult(false),
338 mounted(nullptr) {}
339
340 VFS registry;
341 TestDisk disk;
342 Semaphore callbackEntered;
343 Semaphore callbackRelease;
344 Atomic<size_t> callbackCalls;
345 Atomic<size_t> callbackAfterRemoval;
346 Atomic<size_t> removalReturned;
347 Atomic<size_t> removalSucceeded;
348 Atomic<size_t> probeRemovalReturned;
349 Atomic<size_t> probeRemovalSucceeded;
350 Atomic<size_t> destroyedFilesystems;
351 Atomic<size_t> callbackProcessor;
352 Atomic<size_t> removerProcessor;
353 Atomic<size_t> failures;
354 bool mountResult;
355 Filesystem* mounted;
356};
357
358MountDrainContext* g_MountDrainContext = nullptr;
359
360Filesystem* successfulProbe(Disk*) {
361 return new TestFilesystem(&g_MountDrainContext->destroyedFilesystems);
362}
363
364void blockingMountCallback() {
365 MountDrainContext* context = g_MountDrainContext;
366 context->callbackCalls += 1;
367 context->callbackProcessor = Processor::id();
368 context->callbackEntered.release();
369 if (!context->callbackRelease.acquireForCompletion()) {
370 context->failures += 1;
371 }
372 if (context->removalReturned || context->probeRemovalReturned) {
373 context->callbackAfterRemoval += 1;
374 }
375}
376
377int dispatchBlockingMount(void* parameter) {
378 MountDrainContext* context = reinterpret_cast<MountDrainContext*>(parameter);
379 String stableName;
380 context->mountResult = context->registry.mount(&context->disk, stableName, &context->mounted);
381 return 0;
382}
383
384int removeBlockingMount(void* parameter) {
385 MountDrainContext* context = reinterpret_cast<MountDrainContext*>(parameter);
386 context->removerProcessor = Processor::id();
387 context->removalSucceeded = context->registry.removeMountCallback(blockingMountCallback) ? 1 : 0;
388 context->removalReturned = 1;
389 return 0;
390}
391
392int removeSuccessfulProbe(void* parameter) {
393 MountDrainContext* context = reinterpret_cast<MountDrainContext*>(parameter);
394 context->probeRemovalSucceeded = context->registry.removeProbeCallback(successfulProbe) ? 1 : 0;
395 context->probeRemovalReturned = 1;
396 return 0;
397}
398
399bool mountCallbackDrain(size_t& callbackProcessor, size_t& removerProcessor) {
400 MountDrainContext context;
401 g_MountDrainContext = &context;
402 context.registry.addProbeCallback(successfulProbe);
403 context.registry.addMountCallback(blockingMountCallback);
404 context.registry.addMountCallback(blockingMountCallback);
405
406 Process* process = Scheduler::instance().getKernelProcess();
407 Thread* dispatcher =
408 new Thread(process, dispatchBlockingMount, &context, nullptr, false, DontPickCore);
409 dispatcher->setName("VFS blocking mount dispatcher");
410 const bool callbackEntered = context.callbackEntered.acquireForCompletion();
411 Thread* remover =
412 new Thread(process, removeBlockingMount, &context, nullptr, false, DontPickCore);
413 remover->setName("VFS blocking mount remover");
414 Thread* probeRemover =
415 new Thread(process, removeSuccessfulProbe, &context, nullptr, false, DontPickCore);
416 probeRemover->setName("VFS committed probe remover");
417 const bool observedMountDrain =
418 callbackEntered &&
419 waitForCallbackDrain(remover, reinterpret_cast<uintptr_t>(blockingMountCallback));
420 const bool observedProbeDrain =
421 callbackEntered &&
422 waitForCallbackDrain(probeRemover, reinterpret_cast<uintptr_t>(successfulProbe));
423 const bool returnedEarly = context.removalReturned || context.probeRemovalReturned;
424
425 context.callbackRelease.release();
426 const bool dispatcherJoined = dispatcher->joinForCompletion();
427 const bool removerJoined = remover->joinForCompletion();
428 const bool probeRemoverJoined = probeRemover->joinForCompletion();
429 if (context.mounted) {
430 context.registry.unregisterFilesystem(context.mounted);
431 context.mounted = nullptr;
432 }
433
434 const size_t callsAfterRemoval = context.callbackCalls;
435 context.registry.addProbeCallback(successfulProbe);
436 String stableName;
437 Filesystem* secondMounted = nullptr;
438 const bool mountedAfterRemoval =
439 context.registry.mount(&context.disk, stableName, &secondMounted);
440 if (secondMounted) {
441 context.registry.unregisterFilesystem(secondMounted);
442 }
443 const bool probeRemoved = context.registry.removeProbeCallback(successfulProbe);
444 g_MountDrainContext = nullptr;
445
446 callbackProcessor = context.callbackProcessor;
447 removerProcessor = context.removerProcessor;
448 const bool passed =
449 check(callbackEntered && observedMountDrain && observedProbeDrain && !returnedEarly &&
450 dispatcherJoined && removerJoined && probeRemoverJoined && !context.failures &&
451 context.removalSucceeded == static_cast<size_t>(1) &&
452 context.probeRemovalSucceeded == static_cast<size_t>(1),
453 "vfs-mount-callback-lifetime",
454 "mount or committed probe removal did not drain through publication") &&
455 check(context.mountResult && mountedAfterRemoval && probeRemoved &&
456 callsAfterRemoval == static_cast<size_t>(1) &&
457 context.callbackCalls == callsAfterRemoval && !context.callbackAfterRemoval &&
458 context.destroyedFilesystems == static_cast<size_t>(2),
459 "vfs-mount-callback-lifetime",
460 "a retired mount callback ran again or leaked its test filesystem");
461 if (passed) {
462#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
463 NOTICE("QEMU-CONCURRENCY-TEST: PASS vfs-mount-callback-lifetime");
464#else
465 NOTICE("HOSTED-WAIT-TEST: PASS vfs-mount-callback-lifetime");
466#endif
467 }
468 return passed;
469}
470
471struct MountTableContext {
472 MountTableContext()
473 : disk(),
474 registrationReady(0),
475 registrationStart(0),
476 registrations(0),
477 unregisters(0),
478 unregisterReturned(0),
479 unregisterSucceeded(0),
480 destructions(0),
481 destructorReentries(0),
482 destructorReentryMounts(0),
483 failures(0),
484 firstRegisterProcessor(static_cast<size_t>(-1)),
485 secondRegisterProcessor(static_cast<size_t>(-1)),
486 snapshotProcessor(static_cast<size_t>(-1)),
487 unregisterProcessor(static_cast<size_t>(-1)),
488 registry(),
489 first(new TestFilesystem(&destructions, &registry, &destructorReentries,
490 &destructorReentryMounts)),
491 second(new TestFilesystem(&destructions, &registry, &destructorReentries,
492 &destructorReentryMounts)) {}
493
494 BlockingNameDisk disk;
495 Semaphore registrationReady;
496 Semaphore registrationStart;
497 Atomic<size_t> registrations;
498 Atomic<size_t> unregisters;
499 Atomic<size_t> unregisterReturned;
500 Atomic<size_t> unregisterSucceeded;
501 Atomic<size_t> destructions;
502 Atomic<size_t> destructorReentries;
503 Atomic<size_t> destructorReentryMounts;
504 Atomic<size_t> failures;
505 Atomic<size_t> firstRegisterProcessor;
506 Atomic<size_t> secondRegisterProcessor;
507 Atomic<size_t> snapshotProcessor;
508 Atomic<size_t> unregisterProcessor;
509 VFS registry;
510 TestFilesystem* first;
511 TestFilesystem* second;
512 String firstName;
513 String secondName;
515};
516
517struct MountRegistrationWork {
518 MountTableContext* context;
519 TestFilesystem* filesystem;
520 String* stableName;
521 Atomic<size_t>* processor;
522};
523
524int registerMountTableFilesystem(void* parameter) {
525 MountRegistrationWork* work = reinterpret_cast<MountRegistrationWork*>(parameter);
526 work->context->registrationReady.release();
527 if (!work->context->registrationStart.acquireForCompletion()) {
528 work->context->failures += 1;
529 return 0;
530 }
531 *work->processor = Processor::id();
532 *work->stableName = work->context->registry.registerFilesystem(work->filesystem, String("tmpfs"));
533 work->context->registrations += 1;
534 return 0;
535}
536
537int snapshotMountTable(void* parameter) {
538 MountTableContext* context = reinterpret_cast<MountTableContext*>(parameter);
539 context->snapshotProcessor = Processor::id();
540 context->registry.getMounts(context->snapshot);
541 return 0;
542}
543
544int unregisterMountTableFilesystem(void* parameter) {
545 MountTableContext* context = reinterpret_cast<MountTableContext*>(parameter);
546 context->unregisterProcessor = Processor::id();
547 if (context->registry.unregisterFilesystem(context->first)) {
548 context->unregisters += 1;
549 context->unregisterSucceeded = 1;
550 } else {
551 context->failures += 1;
552 }
553 context->unregisterReturned = 1;
554 return 0;
555}
556
557bool mountTableConcurrency(size_t& snapshotProcessor, size_t& unregisterProcessor,
558 size_t& firstRegisterProcessor, size_t& secondRegisterProcessor) {
559 MountTableContext context;
560 context.first->initialise(&context.disk);
561 context.second->initialise(&context.disk);
562
563 MountRegistrationWork firstWork = {&context, context.first, &context.firstName,
564 &context.firstRegisterProcessor};
565 MountRegistrationWork secondWork = {&context, context.second, &context.secondName,
566 &context.secondRegisterProcessor};
567 Process* process = Scheduler::instance().getKernelProcess();
568 Thread* firstWriter =
569 new Thread(process, registerMountTableFilesystem, &firstWork, nullptr, false, DontPickCore);
570 firstWriter->setName("VFS mount-table first registrar");
571 Thread* secondWriter =
572 new Thread(process, registerMountTableFilesystem, &secondWork, nullptr, false, DontPickCore);
573 secondWriter->setName("VFS mount-table second registrar");
574
575 const bool registrationReady = context.registrationReady.acquireForCompletion(2);
576 context.registrationStart.release(2);
577 const bool firstWriterJoined = firstWriter->joinForCompletion();
578 const bool secondWriterJoined = secondWriter->joinForCompletion();
579
580 Thread* snapshot =
581 new Thread(process, snapshotMountTable, &context, nullptr, false, DontPickCore);
582 snapshot->setName("VFS mount-table snapshot");
583 const bool snapshotBlocked = context.disk.nameEntered.acquireForCompletion();
584 Thread* unregister =
585 new Thread(process, unregisterMountTableFilesystem, &context, nullptr, false, DontPickCore);
586 unregister->setName("VFS mount-table unregister");
587 const bool unregisterBlocked = snapshotBlocked && waitForSemaphoreBlock(unregister);
588 const bool unregisterReturnedEarly = context.unregisterReturned != static_cast<size_t>(0);
589 const bool destroyedEarly = context.destructions != static_cast<size_t>(0);
590
591 context.disk.nameRelease.release();
592 const bool snapshotJoined = snapshot->joinForCompletion();
593 const bool unregisterJoined = unregister->joinForCompletion();
594 if (context.unregisterSucceeded) {
595 context.first = nullptr;
596 }
597 if (context.second && context.registry.unregisterFilesystem(context.second)) {
598 context.unregisters += 1;
599 context.second = nullptr;
600 }
601 if (context.second) {
602 String path;
603 if (!context.registry.getMountPath(context.second, path)) {
604 delete context.second;
605 }
606 context.second = nullptr;
607 }
608 if (context.first && context.registry.unregisterFilesystem(context.first)) {
609 context.unregisters += 1;
610 context.first = nullptr;
611 }
612 if (context.first) {
613 String path;
614 if (!context.registry.getMountPath(context.first, path)) {
615 delete context.first;
616 }
617 context.first = nullptr;
618 }
619
620 snapshotProcessor = context.snapshotProcessor;
621 unregisterProcessor = context.unregisterProcessor;
622 firstRegisterProcessor = context.firstRegisterProcessor;
623 secondRegisterProcessor = context.secondRegisterProcessor;
624
625 const bool stableNames =
626 (context.firstName == String("tmpfs") && context.secondName == String("tmpfs-2")) ||
627 (context.firstName == String("tmpfs-2") && context.secondName == String("tmpfs"));
628 bool snapshotCoherent = context.snapshot.count() == static_cast<size_t>(2);
629 bool sawTmpfs = false;
630 bool sawTmpfs2 = false;
631 for (const auto& mount : context.snapshot) {
632 if (mount.stableName == String("tmpfs") && mount.path == String("/media/tmpfs")) {
633 sawTmpfs = true;
634 } else if (mount.stableName == String("tmpfs-2") && mount.path == String("/media/tmpfs-2")) {
635 sawTmpfs2 = true;
636 } else {
637 snapshotCoherent = false;
638 }
639 snapshotCoherent &= mount.hasDisk && mount.diskParentName.length() == 0 &&
640 mount.diskName == String("mount-table-disk");
641 }
642 snapshotCoherent &= sawTmpfs && sawTmpfs2;
643
644 const bool passed =
645 check(registrationReady && firstWriterJoined && secondWriterJoined &&
646 context.registrations == static_cast<size_t>(2) && stableNames,
647 "vfs-mount-table-concurrency", "same-name registration was not one transaction") &&
648 check(snapshotBlocked && unregisterBlocked && !unregisterReturnedEarly && !destroyedEarly &&
649 snapshotJoined && unregisterJoined && !context.disk.failures,
650 "vfs-mount-table-concurrency",
651 "unregister did not wait behind the filesystem metadata snapshot") &&
652 check(snapshotCoherent && context.unregisters == static_cast<size_t>(2) &&
653 context.destructions == static_cast<size_t>(2) &&
654 context.destructorReentries == static_cast<size_t>(2) &&
655 context.destructorReentryMounts == static_cast<size_t>(1) && !context.failures,
656 "vfs-mount-table-concurrency",
657 "snapshot ownership or unpublished destructor reentry was incoherent");
658 if (passed) {
659#if !PEDIGREE_CONCURRENCY_SMOKE_TESTS
660 NOTICE("HOSTED-WAIT-TEST: PASS vfs-mount-table-concurrency");
661#endif
662 }
663 return passed;
664}
665
666struct SelfRemovalContext {
667 SelfRemovalContext() : calls(0), deferred(0), idleCalls(0), destroyedFilesystems(0) {}
668
669 VFS registry;
670 TestDisk disk;
671 Atomic<size_t> calls;
672 Atomic<size_t> deferred;
673 Atomic<size_t> idleCalls;
674 Atomic<size_t> destroyedFilesystems;
675};
676
677SelfRemovalContext* g_SelfRemovalContext = nullptr;
678
679Filesystem* selfRemovingProbe(Disk*) {
680 SelfRemovalContext* context = g_SelfRemovalContext;
681 const size_t call = context->calls += 1;
682 if (!context->registry.removeProbeCallback(selfRemovingProbe)) {
683 context->deferred += 1;
684 }
685 if (call == static_cast<size_t>(1)) {
686 context->registry.addProbeCallback(selfRemovingProbe);
687 }
688 return new TestFilesystem(&context->destroyedFilesystems);
689}
690
691Filesystem* idleProbe(Disk*) {
692 g_SelfRemovalContext->idleCalls += 1;
693 return nullptr;
694}
695
696bool probeSelfRemoval() {
697 SelfRemovalContext context;
698 g_SelfRemovalContext = &context;
699 context.registry.addProbeCallback(selfRemovingProbe);
700 context.registry.addProbeCallback(selfRemovingProbe);
701 context.registry.addProbeCallback(idleProbe);
702
703 bool idleNonWaitableRemoval = false;
704 bool hardContextObserved = false;
705 {
706 ExecutionContextGuard hardContext(ExecutionContext::HardDeviceIrq);
707 idleNonWaitableRemoval = context.registry.removeProbeCallback(idleProbe);
708 hardContextObserved = Processor::executionContext() == ExecutionContext::HardDeviceIrq;
709 }
710 const bool idleExternallyRetired = context.registry.removeProbeCallback(idleProbe);
711
712 String stableName;
713 Filesystem* mounted = nullptr;
714 const bool firstMounted = context.registry.mount(&context.disk, stableName, &mounted);
715 if (mounted) {
716 context.registry.unregisterFilesystem(mounted);
717 mounted = nullptr;
718 }
719 const bool secondMounted = context.registry.mount(&context.disk, stableName, &mounted);
720 const bool externallyRetired = context.registry.removeProbeCallback(selfRemovingProbe);
721 const bool thirdMounted = context.registry.mount(&context.disk, stableName, &mounted);
722 g_SelfRemovalContext = nullptr;
723
724 const bool passed = check(
725 firstMounted && !secondMounted && !thirdMounted && !mounted && !idleNonWaitableRemoval &&
726 hardContextObserved && idleExternallyRetired && externallyRetired &&
727 context.calls == static_cast<size_t>(2) && context.deferred == static_cast<size_t>(2) &&
728 !context.idleCalls && context.destroyedFilesystems == static_cast<size_t>(2),
729 "vfs-probe-self-removal",
730 "deferred removal did not revive once or retire on a waitable retry");
731 if (passed) {
732#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
733 NOTICE("QEMU-CONCURRENCY-TEST: PASS vfs-probe-self-removal");
734#else
735 NOTICE("HOSTED-WAIT-TEST: PASS vfs-probe-self-removal");
736#endif
737 }
738 return passed;
739}
740} // namespace
741
742bool runVfsCallbackLifetimeRegressions() {
743 size_t probeCallbackProcessor = static_cast<size_t>(-1);
744 size_t probeRemoverProcessor = static_cast<size_t>(-1);
745 size_t mountCallbackProcessor = static_cast<size_t>(-1);
746 size_t mountRemoverProcessor = static_cast<size_t>(-1);
747 size_t mountTableSnapshotProcessor = static_cast<size_t>(-1);
748 size_t mountTableUnregisterProcessor = static_cast<size_t>(-1);
749 size_t firstRegisterProcessor = static_cast<size_t>(-1);
750 size_t secondRegisterProcessor = static_cast<size_t>(-1);
751
752 const bool probePassed = probeCallbackDrain(probeCallbackProcessor, probeRemoverProcessor);
753 const bool mountPassed = mountCallbackDrain(mountCallbackProcessor, mountRemoverProcessor);
754 const bool mountTablePassed =
755 mountTableConcurrency(mountTableSnapshotProcessor, mountTableUnregisterProcessor,
756 firstRegisterProcessor, secondRegisterProcessor);
757 const bool selfRemovalPassed = probeSelfRemoval();
758
759#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
760 NOTICE("QEMU-CONCURRENCY-TEST: vfs-callback-cpus probe="
761 << Dec << probeCallbackProcessor << "/" << probeRemoverProcessor
762 << " mount=" << mountCallbackProcessor << "/" << mountRemoverProcessor);
763 NOTICE("QEMU-CONCURRENCY-TEST: vfs-mount-table-cpus register="
764 << Dec << firstRegisterProcessor << "/" << secondRegisterProcessor << " snapshot="
765 << mountTableSnapshotProcessor << " unregister=" << mountTableUnregisterProcessor);
766 const size_t invalidProcessor = static_cast<size_t>(-1);
767 const bool mountTableCpuSpread = firstRegisterProcessor != invalidProcessor &&
768 secondRegisterProcessor != invalidProcessor &&
769 mountTableSnapshotProcessor != invalidProcessor &&
770 mountTableUnregisterProcessor != invalidProcessor &&
771 firstRegisterProcessor != secondRegisterProcessor &&
772 mountTableSnapshotProcessor != mountTableUnregisterProcessor;
773 const bool cpuSpread =
774 check(probeCallbackProcessor != probeRemoverProcessor &&
775 mountCallbackProcessor != mountRemoverProcessor,
776 "vfs-callback-cpu-spread", "callback retirement did not cross test CPUs") &&
777 check(mountTableCpuSpread, "vfs-mount-table-cpu-spread",
778 "mount registration or snapshot/unregister handoff stayed on one test CPU");
779 if (mountTablePassed && mountTableCpuSpread) {
780 NOTICE("QEMU-CONCURRENCY-TEST: PASS vfs-mount-table-concurrency-smp");
781 }
782#else
783 const bool cpuSpread = true;
784#endif
785
786 const bool passed =
787 probePassed && mountPassed && mountTablePassed && selfRemovalPassed && cpuSpread;
788 if (passed) {
789#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
790 NOTICE("QEMU-CONCURRENCY-TEST: PASS vfs-callback-lifetime-smp");
791#else
792 NOTICE("HOSTED-WAIT-TEST: PASS vfs-callback-lifetime");
793#endif
794 }
795 return passed;
796}
Definition Disk.h:35
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 getName(String &str)
Definition Disk.cc:155
virtual void unpin(uint64_t location)=0
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
Definition File.h:74
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)
static bool getInterrupts()
static ProcessorId id()
static ExecutionContext executionContext()
Definition Processor.cc:109
static void setInterrupts(bool bEnable)
static Scheduler & instance()
Definition Scheduler.h:96
void yield()
Definition Scheduler.cc:226
bool getWaitDebugInfo(WaitDebugInfo &info)
Definition Thread.cc:3184
bool joinForCompletion()
Definition Thread.cc:2771
DebugState getDebugState(uintptr_t &address)
Definition Thread.h:570
Definition VFS.h:58
A vector / dynamic array.
Definition Vector.h:33
@ Dec
Definition Log.h:144
size_t count() const
Definition Vector.h:270