diff --git a/src/coreTest.c b/src/coreTest.c index 04d29c2..9825329 100644 --- a/src/coreTest.c +++ b/src/coreTest.c @@ -155,3 +155,162 @@ KTEST(coreVfsMkdirGetdents) KT_EQ(t, vfsMkdir("/tmp/cd"), 0); KT_EQ(t, vfsMkdir("/tmp/cd"), -1); /* already exists */ + int fa = vfsOpen("/tmp/cd/a", oReadWrite | oCreate); + int fb = vfsOpen("/tmp/cd/b", oReadWrite | oCreate); + KT_GE(t, fa, 0); + KT_GE(t, fb, 0); + vfsClose(fa); + vfsClose(fb); + + int dd = vfsOpen("/tmp/cd", oReadOnly); + KT_GE(t, dd, 0); + FbDirent ents[8]; + int64 n = vfsGetdents(dd, ents, sizeof(ents)); + KT_TRUE(t, n >= (int64)(2 * sizeof(FbDirent))); + vfsClose(dd); + + vfsUnlink("/tmp/cd/a"); + vfsUnlink("/tmp/cd/b"); +} + +/* Real rmdir(2) on tmpfs: only an EMPTY directory may be removed, and the + directory must still be there (untouched) after a rejected attempt. */ +KTEST(coreVfsRmdirTmpfsEmptyCheck) +{ + KT_EQ(t, vfsMkdir("/tmp/rd"), 0); + + int fd = vfsOpen("/tmp/rd/f", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + KT_EQ(t, vfsRmdir("/tmp/rd"), -1); /* not empty yet */ + FbStat st; + KT_EQ(t, vfsStat("/tmp/rd", &st), 0); /* rejected attempt left it alone */ + + KT_EQ(t, vfsUnlink("/tmp/rd/f"), 0); + KT_EQ(t, vfsRmdir("/tmp/rd"), 0); /* empty now, succeeds */ + KT_EQ(t, vfsStat("/tmp/rd", &st), -1); /* gone */ +} + +/* Real unlink(2)/rmdir(2) split on tmpfs: each refuses the other's kind of + node outright, neither silently falls back to the other's behavior. */ +KTEST(coreVfsUnlinkAndRmdirRejectWrongTypeTmpfs) +{ + KT_EQ(t, vfsMkdir("/tmp/rdDir"), 0); + KT_EQ(t, vfsUnlink("/tmp/rdDir"), -1); /* EISDIR-shaped refusal */ + FbStat st; + KT_EQ(t, vfsStat("/tmp/rdDir", &st), 0); /* still there */ + KT_EQ(t, vfsRmdir("/tmp/rdDir"), 0); /* the sanctioned way */ + + int fd = vfsOpen("/tmp/rdFile", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + KT_EQ(t, vfsRmdir("/tmp/rdFile"), -1); /* ENOTDIR-shaped refusal */ + KT_EQ(t, vfsStat("/tmp/rdFile", &st), 0); /* still there */ + KT_EQ(t, vfsUnlink("/tmp/rdFile"), 0); +} + +/* Root can never be removed -- covered by the same `!node->writable` guard + every other mutating tmpfs entry point already relies on (root is + created non-writable in vfsInit). */ +KTEST(coreVfsRmdirRefusesRoot) +{ + KT_EQ(t, vfsRmdir("/"), -1); +} + +/* Same two checks as coreVfsRmdirTmpfsEmptyCheck/ + coreVfsUnlinkAndRmdirRejectWrongTypeTmpfs above, on the persistent SFS + mount instead of tmpfs -- proves vfs.c's dispatch layer enforces the + same real unlink(2)/rmdir(2) split on BOTH backing stores, not just the + in-memory one. Before this fala, vfsUnlink on an SFS path never checked + the inode's type at all, so unlinking an EMPTY directory under /mnt + silently succeeded (sfsRemove's own dir-emptiness check let it through) + -- this is the regression guard for that real, previously-shipped gap. */ +KTEST(coreVfsRmdirSfsParity) +{ + KT_EQ(t, vfsMkdir("/mnt/rdSfsEmpty"), 0); + KT_EQ(t, vfsUnlink("/mnt/rdSfsEmpty"), -1); /* the fixed bug */ + FbStat st; + KT_EQ(t, vfsStat("/mnt/rdSfsEmpty", &st), 0); /* still there */ + KT_EQ(t, vfsRmdir("/mnt/rdSfsEmpty"), 0); + KT_EQ(t, vfsStat("/mnt/rdSfsEmpty", &st), -1); + + KT_EQ(t, vfsMkdir("/mnt/rdSfsFull"), 0); + int fd = vfsOpen("/mnt/rdSfsFull/f", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + KT_EQ(t, vfsRmdir("/mnt/rdSfsFull"), -1); /* not empty */ + KT_EQ(t, vfsUnlink("/mnt/rdSfsFull/f"), 0); + KT_EQ(t, vfsRmdir("/mnt/rdSfsFull"), 0); + + fd = vfsOpen("/mnt/rdSfsFile", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + KT_EQ(t, vfsRmdir("/mnt/rdSfsFile"), -1); /* ENOTDIR-shaped refusal */ + KT_EQ(t, vfsUnlink("/mnt/rdSfsFile"), 0); +} + +/* Real chmod(2) + owner rwx permission bits (fs.h's fsModeOwner*) on tmpfs: + a fresh file gets fsModeDefaultFile (0644) by default; chmod to a mode + missing the owner-write bit makes a SUBSEQUENT open-for-write fail + (EACCES-shaped) while reads keep working, and chmod back restores write + access immediately -- permission is only checked at open() time, never + re-checked against an fd already held, matching real Unix. */ +KTEST(coreVfsChmodTmpfsWritePermission) +{ + int fd = vfsOpen("/tmp/chmodf", oReadWrite | oCreate); + KT_GE(t, fd, 0); + KT_EQ(t, vfsWrite(fd, "x", 1), 1); + vfsClose(fd); + + FbStat st; + KT_EQ(t, vfsStat("/tmp/chmodf", &st), 0); + KT_EQ(t, st.mode, fsModeDefaultFile); + + KT_EQ(t, vfsChmod("/tmp/chmodf", 0444), 0); + KT_EQ(t, vfsStat("/tmp/chmodf", &st), 0); + KT_EQ(t, st.mode, 0444u); + + KT_EQ(t, vfsOpen("/tmp/chmodf", oWriteOnly), -1); /* no owner-write bit */ + int rfd = vfsOpen("/tmp/chmodf", oReadOnly); + KT_GE(t, rfd, 0); + char rd; + KT_EQ(t, vfsRead(rfd, &rd, 1), 1); + vfsClose(rfd); + + KT_EQ(t, vfsChmod("/tmp/chmodf", fsModeDefaultFile), 0); + int wfd = vfsOpen("/tmp/chmodf", oWriteOnly); + KT_GE(t, wfd, 0); + vfsClose(wfd); + + vfsUnlink("/tmp/chmodf"); +} + +/* Real, previously-missing enforcement found by this fala's own grouptest.c + live demo, not review: resolveOpen (vfs.c) checked fsModeOwnerWrite for a + write-mode open and fsModeOwnerRead for opendir()-ing a DIRECTORY, but + NEVER checked any read bit at all for a plain FILE opened oReadOnly or + oReadWrite -- any caller could read any readable-by-someone file's + CONTENTS regardless of mode, since nothing gated it. Proven here entirely + from KTEST (unlike the cross-uid case, which needed a live process, + see grouptest.c's own "other-fallback-denied-ok"): chmod 0000 denies even + the file's OWNER its own read, exactly like coreVfsMkdirAndCreateRequire + ParentWrite already proved for root's own directory-write bit -- no + superuser/owner bypass either way. */ +KTEST(coreVfsResolveOpenRequiresFileReadPermission) +{ + int fd = vfsOpen("/tmp/readf", oReadWrite | oCreate); + KT_GE(t, fd, 0); + KT_EQ(t, vfsWrite(fd, "x", 1), 1); + vfsClose(fd); + + KT_EQ(t, vfsChmod("/tmp/readf", 0000), 0); /* no bits at all, even owner */ + KT_EQ(t, vfsOpen("/tmp/readf", oReadOnly), -1); + KT_EQ(t, vfsOpen("/tmp/readf", oReadWrite), -1); + + KT_EQ(t, vfsChmod("/tmp/readf", 0200), 0); /* owner-write only, no read */ + KT_EQ(t, vfsOpen("/tmp/readf", oReadOnly), -1); + int wfd2 = vfsOpen("/tmp/readf", oWriteOnly); + KT_GE(t, wfd2, 0); /* write-only still works -- the two bits are independent */ + vfsClose(wfd2); +