diff --git a/src/coreTest.c b/src/coreTest.c index 04cd652..22a143f 100644 --- a/src/coreTest.c +++ b/src/coreTest.c @@ -630,3 +630,191 @@ KTEST(coreVfsChgrpRootReassignsArbitraryGroup) vfsClose(fd); KT_EQ(t, vfsChown("/tmp/chgrpForeign", 4242), 0); + KT_EQ(t, vfsChgrp("/tmp/chgrpForeign", 9999), 0); + FbStat st; + KT_EQ(t, vfsStat("/tmp/chgrpForeign", &st), 0); + KT_EQU(t, st.gid, 9999u); + + vfsUnlink("/tmp/chgrpForeign"); +} + +/* Same round trip on the persistent SFS mount, PLUS the "survives + unmount+remount" proof every other SFS metadata field already has -- + mirrors coreVfsChownSfsPersistsAcrossRemount exactly, one field over. */ +KTEST(coreVfsChgrpSfsPersistsAcrossRemount) +{ + KT_EQ(t, vfsMkdir("/mnt/chgrpd"), 0); + int fd = vfsOpen("/mnt/chgrpd/f", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + FbStat st; + KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0); + KT_EQU(t, st.gid, 0u); + + KT_EQ(t, vfsChgrp("/mnt/chgrpd/f", 6666), 0); + KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0); + KT_EQU(t, st.gid, 6666u); + + sfsUnmount(); + KT_TRUE(t, sfsMount()); + + KT_EQ(t, vfsStat("/mnt/chgrpd/f", &st), 0); + KT_EQU(t, st.gid, 6666u); + + vfsUnlink("/mnt/chgrpd/f"); + vfsRmdir("/mnt/chgrpd"); +} + +/* Real directory-write enforcement (vfs.c's vfsMkdir/resolveOpen create + branch), the gap FALA 44 explicitly flagged as stored-but-unenforced -- + creating a NEW entry (file or subdirectory) needs the owner-write bit on + the PARENT directory, checked against the CALLING thread's own uid, not + just "the parent is a writable tmpfs node". KTEST always runs as the + permanent idle thread's own uid 0 (coreVfsTmpfsNodeStampsCreatorAsOwner's + own doc comment), and vfsPermitted has no uid-0 bypass + (coreVfsPermittedUidZeroIsNotASuperuser), so removing root's OWN + owner-write bit on a directory it owns genuinely denies root itself -- + the same "no superuser bypass" story chmod's own tests already prove for + files, now proven for directories too, entirely from KTEST without + needing a live non-root caller. */ +KTEST(coreVfsMkdirAndCreateRequireParentWrite) +{ + KT_EQ(t, vfsMkdir("/tmp/dwParent"), 0); + KT_EQ(t, vfsChmod("/tmp/dwParent", 0555), 0); /* r-x, no owner-write */ + + KT_EQ(t, vfsMkdir("/tmp/dwParent/child"), -1); + KT_EQ(t, vfsOpen("/tmp/dwParent/f", oWriteOnly | oCreate), -1); + + KT_EQ(t, vfsChmod("/tmp/dwParent", fsModeDefaultDir), 0); + KT_EQ(t, vfsMkdir("/tmp/dwParent/child"), 0); + int fd = vfsOpen("/tmp/dwParent/f", oWriteOnly | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + vfsUnlink("/tmp/dwParent/f"); + vfsRmdir("/tmp/dwParent/child"); + vfsRmdir("/tmp/dwParent"); +} + +/* Real unlink(2)/rmdir(2) semantics: removing an entry needs write + permission on the PARENT directory, never the entry's own mode -- proven + here by a file that is itself still perfectly writable (0644) but whose + PARENT has lost its own write bit, matching real Unix's own "you can + delete a read-only file if you can write its directory, and you canNOT + delete a writable file if you can't" rule the file-level mode never + governs on its own. */ +KTEST(coreVfsUnlinkAndRmdirRequireParentWrite) +{ + KT_EQ(t, vfsMkdir("/tmp/dwParent2"), 0); + int fd = vfsOpen("/tmp/dwParent2/f", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + KT_EQ(t, vfsMkdir("/tmp/dwParent2/sub"), 0); + + KT_EQ(t, vfsChmod("/tmp/dwParent2", 0555), 0); + KT_EQ(t, vfsUnlink("/tmp/dwParent2/f"), -1); + KT_EQ(t, vfsRmdir("/tmp/dwParent2/sub"), -1); + + KT_EQ(t, vfsChmod("/tmp/dwParent2", fsModeDefaultDir), 0); + KT_EQ(t, vfsUnlink("/tmp/dwParent2/f"), 0); + KT_EQ(t, vfsRmdir("/tmp/dwParent2/sub"), 0); + KT_EQ(t, vfsRmdir("/tmp/dwParent2"), 0); +} + +/* Real POSIX search permission (vfs.c's lookupNode, checked on every + directory ANCESTOR a path walks through): removing the exec bit on a + directory makes every path THROUGH it fail, even though the target file + inside it never changed at all -- proven by chmod'ing the ancestor away + and back around an otherwise-untouched file. */ +KTEST(coreVfsLookupRequiresAncestorExec) +{ + KT_EQ(t, vfsMkdir("/tmp/dxAncestor"), 0); + int fd = vfsOpen("/tmp/dxAncestor/leaf", oReadWrite | oCreate); + KT_GE(t, fd, 0); + KT_EQ(t, vfsWrite(fd, "x", 1), 1); + vfsClose(fd); + + KT_EQ(t, vfsChmod("/tmp/dxAncestor", 0600), 0); /* rw-, no exec at all */ + FbStat st; + KT_EQ(t, vfsStat("/tmp/dxAncestor/leaf", &st), -1); + KT_EQ(t, vfsOpen("/tmp/dxAncestor/leaf", oReadOnly), -1); + + KT_EQ(t, vfsChmod("/tmp/dxAncestor", fsModeDefaultDir), 0); + KT_EQ(t, vfsStat("/tmp/dxAncestor/leaf", &st), 0); + int rfd = vfsOpen("/tmp/dxAncestor/leaf", oReadOnly); + KT_GE(t, rfd, 0); + vfsClose(rfd); + + vfsUnlink("/tmp/dxAncestor/leaf"); + vfsRmdir("/tmp/dxAncestor"); +} + +/* Real distinction between search (exec) and list (read) on a directory -- + the subtlest part of this feature, and the one most likely to be + accidentally conflated: a directory with exec but no read lets a caller + open a KNOWN child by name (resolveOpen never needs read on `cur` for + that, only exec on it as an ANCESTOR of the leaf) while refusing to open + the directory itself for listing (vfsGetdents). Mirrors dirpermtest.c's + own live list-denied-ok/search-without-list-ok pair, but here entirely + from KTEST since both sides are root-vs-its-own-directory, not root-vs- + another-uid. */ +KTEST(coreVfsOpendirRequiresDirReadNotJustExec) +{ + KT_EQ(t, vfsMkdir("/tmp/dxListable"), 0); + int fd = vfsOpen("/tmp/dxListable/known", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + KT_EQ(t, vfsChmod("/tmp/dxListable", 0311), 0); /* -wx, no owner-read */ + KT_EQ(t, vfsOpen("/tmp/dxListable", oReadOnly), -1); + int kfd = vfsOpen("/tmp/dxListable/known", oReadOnly); + KT_GE(t, kfd, 0); + vfsClose(kfd); + + KT_EQ(t, vfsChmod("/tmp/dxListable", fsModeDefaultDir), 0); + int dd = vfsOpen("/tmp/dxListable", oReadOnly); + KT_GE(t, dd, 0); + FbDirent ents[8]; + KT_TRUE(t, vfsGetdents(dd, ents, sizeof(ents)) >= (int64)sizeof(FbDirent)); + vfsClose(dd); + + vfsUnlink("/tmp/dxListable/known"); + vfsRmdir("/tmp/dxListable"); +} + +/* Same three checks as the four tmpfs tests directly above (parent-write + for create/remove, ancestor-exec for traversal, dir-read-vs-exec for + listing), run once through the SFS dispatch branch of vfs.c's own + choke points (resolveOpen/vfsMkdir/vfsUnlink/vfsRmdir/lookupNode's + isSfsPath fork) instead of sfs.c's raw primitives directly -- sfsTest.c's + sfsCreateNodeRequiresParentWrite/sfsRemoveRequiresParentWrite/ + sfsLookupRequiresAncestorExec already cover the on-disk layer itself; + this is the "does the /mnt dispatch actually reach it" parity proof, the + same relationship coreVfsChmodSfsPersistsAcrossRemount already has to + its own tmpfs sibling above. */ +KTEST(coreVfsSfsDirectoryPermissionParity) +{ + KT_EQ(t, vfsMkdir("/mnt/dpTop"), 0); + int fd = vfsOpen("/mnt/dpTop/leaf", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + KT_EQ(t, vfsChmod("/mnt/dpTop", 0555), 0); + KT_EQ(t, vfsOpen("/mnt/dpTop/newfile", oWriteOnly | oCreate), -1); + KT_EQ(t, vfsUnlink("/mnt/dpTop/leaf"), -1); + + KT_EQ(t, vfsChmod("/mnt/dpTop", 0600), 0); + FbStat st; + KT_EQ(t, vfsStat("/mnt/dpTop/leaf", &st), -1); + + KT_EQ(t, vfsChmod("/mnt/dpTop", 0311), 0); + KT_EQ(t, vfsOpen("/mnt/dpTop", oReadOnly), -1); + int kfd = vfsOpen("/mnt/dpTop/leaf", oReadOnly); + KT_GE(t, kfd, 0); + vfsClose(kfd); + + KT_EQ(t, vfsChmod("/mnt/dpTop", fsModeDefaultDir), 0); + KT_EQ(t, vfsUnlink("/mnt/dpTop/leaf"), 0); + KT_EQ(t, vfsRmdir("/mnt/dpTop"), 0); +}