#include "ktest.h" #include "vfs.h" #include "pipe.h" #include "fs.h" #include "string.h" #include "klib.h" #include "sfs.h" /* * Coverage for named pipes (vfsMkfifo, vfs.h/vfs.c): a real filesystem entry * of type fsTypeFifo that lets two processes rendezvous over a blocking * pipe just by agreeing on a path, with no common ancestor and no handle * number to pass through argv the way vfsPipeCreateHandle/vfsPipeAttach * (pipeShareTest.c) require. The genuine cross-process park/wake handshake * over such a fifo only runs for real at boot (namedreader.c/namedwriter.c, * two independently spawned kmain.c programs, needles "namedreader: * opened, blocking on read"/"namedreader: payload-ok" in tools/ci-test.sh) * -- what is deterministic and scheduler-free (open/read/write never * actually parking a thread) lives here, matching the division of labor * pipeShareTest.c already established for the handle-based flavor. */ KTEST(vfsMkfifoCreatesFifoNodeVisibleViaStat) { KT_EQ(t, vfsMkfifo("/tmp/fifoBasic"), 0); FbStat st; KT_EQ(t, vfsStat("/tmp/fifoBasic", &st), 0); KT_EQ(t, st.type, fsTypeFifo); /* Same path again must fail, like mkdir/open(O_CREAT|O_EXCL) would -- mkfifo never silently succeeds on an existing name. */ KT_EQ(t, vfsMkfifo("/tmp/fifoBasic"), -1); KT_EQ(t, vfsUnlink("/tmp/fifoBasic"), 0); KT_EQ(t, vfsStat("/tmp/fifoBasic", &st), -1); } KTEST(vfsMkfifoRejectsReservedAndInvalidPaths) { /* The one reserved, non-tree path mkfifo never supports -- a raw byte device has no notion of a named directory entry at all, unlike "/mnt" (see the sfsCreateFifo-backed tests below), which does. */ KT_EQ(t, vfsMkfifo("/dev/disk0"), -1); /* Parent directory does not exist at all. */ KT_EQ(t, vfsMkfifo("/doesNotExist/fifo"), -1); /* Root itself is never writable -- only vfsMountTmpfs-created directories (e.g. "/tmp") are, same rule makeChildFile enforces for oCreate. */ KT_EQ(t, vfsMkfifo("/rootFifo"), -1); } KTEST(vfsFifoOpenRejectsDualDirectionModes) { KT_EQ(t, vfsMkfifo("/tmp/fifoModes"), 0); /* This kernel's pipe end is inherently one-directional (fdPipeRead XOR fdPipeWrite) -- a fifo fd can never be both, unlike Linux's permissive (POSIX-undefined) O_RDWR on a FIFO. */ KT_EQ(t, vfsOpen("/tmp/fifoModes", oReadWrite), -1); KT_EQ(t, vfsOpen("/tmp/fifoModes", oReadWrite | oWriteOnly), -1); KT_EQ(t, vfsUnlink("/tmp/fifoModes"), 0); } KTEST(vfsFifoRendezvousByPathAloneNoSharedHandle) { KT_EQ(t, vfsMkfifo("/tmp/fifoRendezvous"), 0); /* Two independent vfsOpen calls on the SAME PATH -- no handle number, no argv, no common caller -- is exactly what namedreader.c/namedwriter.c (two kmain.c boot programs with no parent-child relationship) rely on. */ int rfd = vfsOpen("/tmp/fifoRendezvous", oReadOnly); KT_GE(t, rfd, 0); /* Nobody has opened the write end yet: a read must report "try again" (-1), never spurious end-of-file, exactly like a freshly pipeCreateDetached handle (pipeShareTest.c). */ char probe[4]; KT_EQ(t, vfsRead(rfd, probe, sizeof(probe)), -1); KT_NOTNULL(t, vfsBlockingChannel(rfd, false)); int wfd = vfsOpen("/tmp/fifoRendezvous", oWriteOnly); KT_GE(t, wfd, 0); KT_NE(t, rfd, wfd); KT_EQ(t, vfsWrite(wfd, "hi", 2), 2); char buffer[4]; memset(buffer, 0, sizeof(buffer)); KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2); KT_TRUE(t, memcmp(buffer, "hi", 2) == 0); vfsClose(rfd); vfsClose(wfd); KT_EQ(t, vfsUnlink("/tmp/fifoRendezvous"), 0); } KTEST(vfsFifoUnlinkWhileOpenLeavesLiveFdsWorking) { KT_EQ(t, vfsMkfifo("/tmp/fifoUnlinkLive"), 0); int rfd = vfsOpen("/tmp/fifoUnlinkLive", oReadOnly); int wfd = vfsOpen("/tmp/fifoUnlinkLive", oWriteOnly); KT_GE(t, rfd, 0); KT_GE(t, wfd, 0); /* Real unlink-while-open semantics: removing the name must not disturb fds opened before the unlink -- they keep working off their own copy of the raw pipe handle, never the freed vfs node. */ KT_EQ(t, vfsUnlink("/tmp/fifoUnlinkLive"), 0); FbStat st; KT_EQ(t, vfsStat("/tmp/fifoUnlinkLive", &st), -1); KT_EQ(t, vfsWrite(wfd, "ok", 2), 2); char buffer[4]; memset(buffer, 0, sizeof(buffer)); KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2); KT_TRUE(t, memcmp(buffer, "ok", 2) == 0); vfsClose(rfd); vfsClose(wfd); } KTEST(vfsUnlinkOnNeverOpenedFifoReclaimsPipeSlot) { /* Drives the pipe table (pipe.h's fixed-size `pipes[maxPipes]`, shared with every other pipe user in the kernel) to exhaustion using fifos that are never opened by anyone -- the one case pipeCloseRead/ pipeCloseWrite's own cleanup can never reach, since neither is ever called for an end nobody attached. Does not assume the table starts fully empty (kmain.c's own vfsMkfifo("/tmp/rendezvous") already holds one slot for the rest of boot by the time ktests run) -- just proves whatever headroom exists gets used up, and reclaimed, correctly. */ char paths[maxPipes][24]; int madeCount = 0; for (int i = 0; i < (int)maxPipes; i++) { snprintf(paths[i], sizeof(paths[i]), "/tmp/fifoLeak%d", i); if (vfsMkfifo(paths[i]) != 0) break; madeCount++; } KT_GT(t, madeCount, 0); /* Table is now genuinely full: one more must fail. */ KT_EQ(t, vfsMkfifo("/tmp/fifoOverflow"), -1); /* Unlinking one never-opened fifo must reclaim its slot immediately (pipeDestroyIfUnattached), not leave it dangling until some later, unrelated event. */ KT_EQ(t, vfsUnlink(paths[0]), 0); KT_EQ(t, vfsMkfifo("/tmp/fifoOverflow"), 0); /* Leave the shared pipe table exactly as free as it was found, so later boot-time pipe users (the pipeProducer/pipeConsumer demo, fifomaster's handle, killtest's shared pipe, namedreader/ namedwriter's own fifo) are never starved by this test having run. */ vfsUnlink("/tmp/fifoOverflow"); for (int i = 1; i < madeCount; i++) vfsUnlink(paths[i]); } /* * Coverage for PERSISTENT named pipes: vfsMkfifo under "/mnt" now creates a * real, on-disk sfs.h entry (sfsTypeFifo) instead of being rejected, so the * special file itself -- unlike everything above, which lives only in this * process's kmalloc heap -- survives sfsUnmount()+sfsMount(). The live * pipe.h byte stream two openers exchange data through is still purely * in-memory (vfs.c's sfsFifoHandleFor), created lazily on first open, since * buffered bytes never persist across a real mkfifo(2)'s reboot either -- * only the rendezvous point's name and type do. Every test below starts * with its own sfsFormat() (same discipline sfsTest.c uses) so results * never depend on whatever an earlier test (here or in sfsTest.c, same * boot, same disk) left behind. */ KTEST(sfsBackedMkfifoCreatesPersistentFifoVisibleViaStat) { KT_TRUE(t, sfsFormat()); KT_EQ(t, vfsMkfifo("/mnt/fifoBasic"), 0); FbStat st; KT_EQ(t, vfsStat("/mnt/fifoBasic", &st), 0); KT_EQ(t, st.type, fsTypeFifo); KT_EQU(t, st.size, 0u); /* Same path again must fail, exactly like the tmpfs case above. */ KT_EQ(t, vfsMkfifo("/mnt/fifoBasic"), -1); KT_EQ(t, vfsUnlink("/mnt/fifoBasic"), 0); KT_EQ(t, vfsStat("/mnt/fifoBasic", &st), -1); } KTEST(sfsBackedFifoRendezvousByPathAloneNoSharedHandle) { KT_TRUE(t, sfsFormat()); KT_EQ(t, vfsMkfifo("/mnt/fifoRendezvous"), 0); int rfd = vfsOpen("/mnt/fifoRendezvous", oReadOnly); KT_GE(t, rfd, 0); /* Nobody has opened the write end yet: "try again" (-1), never spurious end-of-file, exactly like the tmpfs fifo and a freshly pipeCreateDetached handle both already guarantee. */ char probe[4]; KT_EQ(t, vfsRead(rfd, probe, sizeof(probe)), -1); KT_NOTNULL(t, vfsBlockingChannel(rfd, false)); int wfd = vfsOpen("/mnt/fifoRendezvous", oWriteOnly); KT_GE(t, wfd, 0); KT_NE(t, rfd, wfd); KT_EQ(t, vfsWrite(wfd, "hi", 2), 2); char buffer[4]; memset(buffer, 0, sizeof(buffer)); KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2); KT_TRUE(t, memcmp(buffer, "hi", 2) == 0); vfsClose(rfd); vfsClose(wfd); KT_EQ(t, vfsUnlink("/mnt/fifoRendezvous"), 0); } KTEST(sfsBackedFifoUnlinkWhileOpenLeavesLiveFdsWorking) { KT_TRUE(t, sfsFormat()); KT_EQ(t, vfsMkfifo("/mnt/fifoUnlinkLive"), 0); int rfd = vfsOpen("/mnt/fifoUnlinkLive", oReadOnly); int wfd = vfsOpen("/mnt/fifoUnlinkLive", oWriteOnly); KT_GE(t, rfd, 0); KT_GE(t, wfd, 0); /* Real unlink-while-open semantics, same guarantee as the tmpfs fifo: removing the directory entry must not disturb fds opened before the unlink -- they keep working off their own copy of the raw pipe handle number. */ KT_EQ(t, vfsUnlink("/mnt/fifoUnlinkLive"), 0); FbStat st; KT_EQ(t, vfsStat("/mnt/fifoUnlinkLive", &st), -1); KT_EQ(t, vfsWrite(wfd, "ok", 2), 2); char buffer[4]; memset(buffer, 0, sizeof(buffer)); KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2); KT_TRUE(t, memcmp(buffer, "ok", 2) == 0); vfsClose(rfd); vfsClose(wfd); } KTEST(sfsBackedFifoEntryPersistsAcrossUnmountRemount) { KT_TRUE(t, sfsFormat()); KT_EQ(t, vfsMkfifo("/mnt/fifoPersist"), 0); /* The real point of this test (mirrors sfsTest.c's own sfsMountReloadsExistingFilesystem-style coverage): drop EVERY in-memory sfs.c structure (bitmaps, superblock) and reconstruct them purely from what sfsFormat/sfsCreateFifo actually wrote to disk -- proving the fifo is a genuine on-disk entry, not a kmalloc node that would evaporate the instant this process's heap did (like the tmpfs fifo above, which vfsMkfifo("/mnt/...") deliberately never was). */ sfsUnmount(); KT_TRUE(t, sfsMount()); FbStat st; KT_EQ(t, vfsStat("/mnt/fifoPersist", &st), 0); KT_EQ(t, st.type, fsTypeFifo); /* Still fully functional after the remount, not just present in a directory listing: a fresh open/write/read round trip works exactly as it would have before the remount. */ int rfd = vfsOpen("/mnt/fifoPersist", oReadOnly); int wfd = vfsOpen("/mnt/fifoPersist", oWriteOnly); KT_GE(t, rfd, 0); KT_GE(t, wfd, 0); KT_EQ(t, vfsWrite(wfd, "yo", 2), 2); char buffer[4]; memset(buffer, 0, sizeof(buffer)); KT_EQ(t, vfsRead(rfd, buffer, sizeof(buffer)), 2); KT_TRUE(t, memcmp(buffer, "yo", 2) == 0); vfsClose(rfd); vfsClose(wfd); KT_EQ(t, vfsUnlink("/mnt/fifoPersist"), 0); } KTEST(sfsBackedFifoReopenAfterCompletedRoundTripGetsFreshWorkingSession) { /* Ordinary fifo usage, not a corner case: open both ends, exchange data, close both, then reopen the SAME persistent path later for a second, unrelated conversation -- exactly what a real named pipe is for. The first round trip fully releases pipe.h's underlying slot back to its general pool (pipeReleaseIfUnused, once both readers and writers hit 0), so the second round's vfs.c-level binding (sfsFifoHandleFor) MUST detect that its remembered handle number no longer refers to the same pipe (pipeIncarnation, pipe.h) and mint a genuinely fresh one -- otherwise this second write would silently target a slot nobody has attached to (or, worse, one some unrelated pipe elsewhere in the kernel grabbed in between), and would report -1 forever instead of actually delivering the byte. */ KT_TRUE(t, sfsFormat()); KT_EQ(t, vfsMkfifo("/mnt/fifoRoundTrip"), 0); int rfd1 = vfsOpen("/mnt/fifoRoundTrip", oReadOnly); int wfd1 = vfsOpen("/mnt/fifoRoundTrip", oWriteOnly); KT_GE(t, rfd1, 0); KT_GE(t, wfd1, 0); KT_EQ(t, vfsWrite(wfd1, "one", 3), 3); char first[4]; memset(first, 0, sizeof(first)); KT_EQ(t, vfsRead(rfd1, first, sizeof(first)), 3); KT_TRUE(t, memcmp(first, "one", 3) == 0); vfsClose(rfd1); vfsClose(wfd1); /* Second, independent session on the same path. */ int rfd2 = vfsOpen("/mnt/fifoRoundTrip", oReadOnly); KT_GE(t, rfd2, 0);