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"
17#include "modules/system/vfs/Filesystem.h"
18#include "modules/system/vfs/VFS.h"
21constexpr size_t Attempts = 10000;
22constexpr bool DontPickCore = HOSTED;
24class TestDisk final :
public Disk {
30 size_t getSize()
const override {
38 bool pin(uint64_t)
override {
42 void unpin(uint64_t)
override {}
45class BlockingNameDisk final :
public Disk {
51 nameProcessor(static_cast<size_t>(-1)),
55 const size_t call = nameCalls += 1;
56 if (call ==
static_cast<size_t>(1)) {
58 nameEntered.release();
59 if (!nameRelease.acquireForCompletion()) {
63 name.assign(
"mount-table-disk");
70 size_t getSize()
const override {
78 bool pin(uint64_t)
override {
82 void unpin(uint64_t)
override {}
91class TestFilesystem final :
public Filesystem {
96 : m_Destructions(destructions),
98 m_Reentries(reentries),
99 m_ReentryMounts(reentryMounts),
100 m_Label(
"vfs-callback-test") {}
102 ~TestFilesystem()
override {
105 m_Registry->getMounts(mounts);
107 *m_ReentryMounts += mounts.
count();
109 if (m_Destructions) {
110 *m_Destructions += 1;
152bool check(
bool condition,
const char* test,
const char* detail) {
156#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
157 ERROR(
"QEMU-CONCURRENCY-TEST: FAIL " << test <<
": " << detail);
159 ERROR(
"HOSTED-WAIT-TEST: FAIL " << test <<
": " << detail);
164bool waitForCallbackDrain(
Thread* thread, uintptr_t callback) {
165 for (
size_t attempt = 0; attempt < Attempts; ++attempt) {
167 uintptr_t debugAddress = 0;
168 if (thread->
getWaitDebugInfo(info) && info.queue && info.channelOwner && info.queued &&
169 thread->
getDebugState(debugAddress) == Thread::CallbackDrain && debugAddress == callback) {
177bool waitForSemaphoreBlock(
Thread* thread) {
178 for (
size_t attempt = 0; attempt < Attempts; ++attempt) {
180 uintptr_t debugAddress = 0;
181 if (thread->
getWaitDebugInfo(info) && info.queue && info.channelOwner && info.queued &&
190struct ProbeDrainContext {
192 : callbackEntered(0),
195 callbackAfterRemoval(0),
198 destroyedFilesystems(0),
199 callbackProcessor(static_cast<size_t>(-1)),
200 removerProcessor(static_cast<size_t>(-1)),
221ProbeDrainContext* g_ProbeDrainContext =
nullptr;
224 ProbeDrainContext* context = g_ProbeDrainContext;
225 context->callbackCalls += 1;
227 context->callbackEntered.release();
228 if (!context->callbackRelease.acquireForCompletion()) {
229 context->failures += 1;
231 if (context->removalReturned) {
232 context->callbackAfterRemoval += 1;
234 return new TestFilesystem(&context->destroyedFilesystems);
237int dispatchBlockingProbe(
void* parameter) {
238 ProbeDrainContext* context =
reinterpret_cast<ProbeDrainContext*
>(parameter);
240 context->mountResult = context->registry.mount(&context->disk, stableName, &context->mounted);
244int removeBlockingProbe(
void* parameter) {
245 ProbeDrainContext* context =
reinterpret_cast<ProbeDrainContext*
>(parameter);
247 context->removalSucceeded = context->registry.removeProbeCallback(blockingProbe) ? 1 : 0;
248 context->removalReturned = 1;
252bool probeCallbackDrain(
size_t& callbackProcessor,
size_t& removerProcessor) {
253 ProbeDrainContext context;
254 g_ProbeDrainContext = &context;
255 context.registry.addProbeCallback(blockingProbe);
259 new Thread(process, dispatchBlockingProbe, &context,
nullptr,
false, DontPickCore);
260 dispatcher->setName(
"VFS blocking probe dispatcher");
261 const bool callbackEntered = context.callbackEntered.acquireForCompletion();
263 bool nonWaitableRemoval =
false;
264 bool nonWaitableContextObserved =
false;
267 nonWaitableRemoval = context.registry.removeProbeCallback(blockingProbe);
271 const bool waitableContextRestored =
274 const bool nonYieldingRemoval = context.registry.removeProbeCallback(blockingProbe);
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);
284 context.callbackRelease.release();
287 const size_t callsAfterRemoval = context.callbackCalls;
290 const bool mountedAfterRemoval = context.registry.mount(&context.disk, stableName, &unexpected);
291 g_ProbeDrainContext =
nullptr;
294 context.registry.unregisterFilesystem(unexpected);
296 callbackProcessor = context.callbackProcessor;
297 removerProcessor = context.removerProcessor;
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");
314#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
315 NOTICE(
"QEMU-CONCURRENCY-TEST: PASS vfs-probe-callback-lifetime");
317 NOTICE(
"HOSTED-WAIT-TEST: PASS vfs-probe-callback-lifetime");
323struct MountDrainContext {
325 : callbackEntered(0),
328 callbackAfterRemoval(0),
331 probeRemovalReturned(0),
332 probeRemovalSucceeded(0),
333 destroyedFilesystems(0),
334 callbackProcessor(static_cast<size_t>(-1)),
335 removerProcessor(static_cast<size_t>(-1)),
358MountDrainContext* g_MountDrainContext =
nullptr;
361 return new TestFilesystem(&g_MountDrainContext->destroyedFilesystems);
364void blockingMountCallback() {
365 MountDrainContext* context = g_MountDrainContext;
366 context->callbackCalls += 1;
368 context->callbackEntered.release();
369 if (!context->callbackRelease.acquireForCompletion()) {
370 context->failures += 1;
372 if (context->removalReturned || context->probeRemovalReturned) {
373 context->callbackAfterRemoval += 1;
377int dispatchBlockingMount(
void* parameter) {
378 MountDrainContext* context =
reinterpret_cast<MountDrainContext*
>(parameter);
380 context->mountResult = context->registry.mount(&context->disk, stableName, &context->mounted);
384int removeBlockingMount(
void* parameter) {
385 MountDrainContext* context =
reinterpret_cast<MountDrainContext*
>(parameter);
387 context->removalSucceeded = context->registry.removeMountCallback(blockingMountCallback) ? 1 : 0;
388 context->removalReturned = 1;
392int removeSuccessfulProbe(
void* parameter) {
393 MountDrainContext* context =
reinterpret_cast<MountDrainContext*
>(parameter);
394 context->probeRemovalSucceeded = context->registry.removeProbeCallback(successfulProbe) ? 1 : 0;
395 context->probeRemovalReturned = 1;
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);
408 new Thread(process, dispatchBlockingMount, &context,
nullptr,
false, DontPickCore);
409 dispatcher->setName(
"VFS blocking mount dispatcher");
410 const bool callbackEntered = context.callbackEntered.acquireForCompletion();
412 new Thread(process, removeBlockingMount, &context,
nullptr,
false, DontPickCore);
413 remover->setName(
"VFS blocking mount remover");
415 new Thread(process, removeSuccessfulProbe, &context,
nullptr,
false, DontPickCore);
416 probeRemover->setName(
"VFS committed probe remover");
417 const bool observedMountDrain =
419 waitForCallbackDrain(remover,
reinterpret_cast<uintptr_t
>(blockingMountCallback));
420 const bool observedProbeDrain =
422 waitForCallbackDrain(probeRemover,
reinterpret_cast<uintptr_t
>(successfulProbe));
423 const bool returnedEarly = context.removalReturned || context.probeRemovalReturned;
425 context.callbackRelease.release();
429 if (context.mounted) {
430 context.registry.unregisterFilesystem(context.mounted);
431 context.mounted =
nullptr;
434 const size_t callsAfterRemoval = context.callbackCalls;
435 context.registry.addProbeCallback(successfulProbe);
438 const bool mountedAfterRemoval =
439 context.registry.mount(&context.disk, stableName, &secondMounted);
441 context.registry.unregisterFilesystem(secondMounted);
443 const bool probeRemoved = context.registry.removeProbeCallback(successfulProbe);
444 g_MountDrainContext =
nullptr;
446 callbackProcessor = context.callbackProcessor;
447 removerProcessor = context.removerProcessor;
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");
462#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
463 NOTICE(
"QEMU-CONCURRENCY-TEST: PASS vfs-mount-callback-lifetime");
465 NOTICE(
"HOSTED-WAIT-TEST: PASS vfs-mount-callback-lifetime");
471struct MountTableContext {
474 registrationReady(0),
475 registrationStart(0),
478 unregisterReturned(0),
479 unregisterSucceeded(0),
481 destructorReentries(0),
482 destructorReentryMounts(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)),
489 first(new TestFilesystem(&destructions, ®istry, &destructorReentries,
490 &destructorReentryMounts)),
491 second(new TestFilesystem(&destructions, ®istry, &destructorReentries,
492 &destructorReentryMounts)) {}
494 BlockingNameDisk disk;
510 TestFilesystem* first;
511 TestFilesystem* second;
517struct MountRegistrationWork {
518 MountTableContext* context;
519 TestFilesystem* filesystem;
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;
532 *work->stableName = work->context->registry.registerFilesystem(work->filesystem,
String(
"tmpfs"));
533 work->context->registrations += 1;
537int snapshotMountTable(
void* parameter) {
538 MountTableContext* context =
reinterpret_cast<MountTableContext*
>(parameter);
540 context->registry.getMounts(context->snapshot);
544int unregisterMountTableFilesystem(
void* parameter) {
545 MountTableContext* context =
reinterpret_cast<MountTableContext*
>(parameter);
547 if (context->registry.unregisterFilesystem(context->first)) {
548 context->unregisters += 1;
549 context->unregisterSucceeded = 1;
551 context->failures += 1;
553 context->unregisterReturned = 1;
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);
563 MountRegistrationWork firstWork = {&context, context.first, &context.firstName,
564 &context.firstRegisterProcessor};
565 MountRegistrationWork secondWork = {&context, context.second, &context.secondName,
566 &context.secondRegisterProcessor};
569 new Thread(process, registerMountTableFilesystem, &firstWork,
nullptr,
false, DontPickCore);
570 firstWriter->setName(
"VFS mount-table first registrar");
572 new Thread(process, registerMountTableFilesystem, &secondWork,
nullptr,
false, DontPickCore);
573 secondWriter->setName(
"VFS mount-table second registrar");
575 const bool registrationReady = context.registrationReady.acquireForCompletion(2);
576 context.registrationStart.release(2);
581 new Thread(process, snapshotMountTable, &context,
nullptr,
false, DontPickCore);
582 snapshot->setName(
"VFS mount-table snapshot");
583 const bool snapshotBlocked = context.disk.nameEntered.acquireForCompletion();
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);
591 context.disk.nameRelease.release();
592 const bool snapshotJoined = snapshot->joinForCompletion();
594 if (context.unregisterSucceeded) {
595 context.first =
nullptr;
597 if (context.second && context.registry.unregisterFilesystem(context.second)) {
598 context.unregisters += 1;
599 context.second =
nullptr;
601 if (context.second) {
603 if (!context.registry.getMountPath(context.second, path)) {
604 delete context.second;
606 context.second =
nullptr;
608 if (context.first && context.registry.unregisterFilesystem(context.first)) {
609 context.unregisters += 1;
610 context.first =
nullptr;
614 if (!context.registry.getMountPath(context.first, path)) {
615 delete context.first;
617 context.first =
nullptr;
620 snapshotProcessor = context.snapshotProcessor;
621 unregisterProcessor = context.unregisterProcessor;
622 firstRegisterProcessor = context.firstRegisterProcessor;
623 secondRegisterProcessor = context.secondRegisterProcessor;
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")) {
634 }
else if (mount.stableName ==
String(
"tmpfs-2") && mount.path ==
String(
"/media/tmpfs-2")) {
637 snapshotCoherent =
false;
639 snapshotCoherent &= mount.hasDisk && mount.diskParentName.length() == 0 &&
640 mount.diskName ==
String(
"mount-table-disk");
642 snapshotCoherent &= sawTmpfs && sawTmpfs2;
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");
659#if !PEDIGREE_CONCURRENCY_SMOKE_TESTS
660 NOTICE(
"HOSTED-WAIT-TEST: PASS vfs-mount-table-concurrency");
666struct SelfRemovalContext {
667 SelfRemovalContext() : calls(0), deferred(0), idleCalls(0), destroyedFilesystems(0) {}
677SelfRemovalContext* g_SelfRemovalContext =
nullptr;
680 SelfRemovalContext* context = g_SelfRemovalContext;
681 const size_t call = context->calls += 1;
682 if (!context->registry.removeProbeCallback(selfRemovingProbe)) {
683 context->deferred += 1;
685 if (call ==
static_cast<size_t>(1)) {
686 context->registry.addProbeCallback(selfRemovingProbe);
688 return new TestFilesystem(&context->destroyedFilesystems);
692 g_SelfRemovalContext->idleCalls += 1;
696bool probeSelfRemoval() {
697 SelfRemovalContext context;
698 g_SelfRemovalContext = &context;
699 context.registry.addProbeCallback(selfRemovingProbe);
700 context.registry.addProbeCallback(selfRemovingProbe);
701 context.registry.addProbeCallback(idleProbe);
703 bool idleNonWaitableRemoval =
false;
704 bool hardContextObserved =
false;
707 idleNonWaitableRemoval = context.registry.removeProbeCallback(idleProbe);
710 const bool idleExternallyRetired = context.registry.removeProbeCallback(idleProbe);
714 const bool firstMounted = context.registry.mount(&context.disk, stableName, &mounted);
716 context.registry.unregisterFilesystem(mounted);
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;
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");
732#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
733 NOTICE(
"QEMU-CONCURRENCY-TEST: PASS vfs-probe-self-removal");
735 NOTICE(
"HOSTED-WAIT-TEST: PASS vfs-probe-self-removal");
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);
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();
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");
783 const bool cpuSpread =
true;
787 probePassed && mountPassed && mountTablePassed && selfRemovalPassed && cpuSpread;
789#if PEDIGREE_CONCURRENCY_SMOKE_TESTS
790 NOTICE(
"QEMU-CONCURRENCY-TEST: PASS vfs-callback-lifetime-smp");
792 NOTICE(
"HOSTED-WAIT-TEST: PASS vfs-callback-lifetime");
virtual BufferView read(uint64_t location)
virtual size_t getSize() const
Gets the size of the disk.
virtual void getName(String &str)
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.
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 ExecutionContext executionContext()
static void setInterrupts(bool bEnable)
static Scheduler & instance()
bool getWaitDebugInfo(WaitDebugInfo &info)
DebugState getDebugState(uintptr_t &address)
A vector / dynamic array.