diff --git a/src/vfsFifoTest.c b/src/vfsFifoTest.c new file mode 100644 index 0000000..21d75c5 --- /dev/null +++ b/src/vfsFifoTest.c @@ -0,0 +1,162 @@ +#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]); +} +