From 28a39f4234a5eaa0f74d85fd8a392d21699c3066 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 18:57:05 +0200 Subject: [PATCH] test: test more of sfsTest --- src/sfsTest.c | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/src/sfsTest.c b/src/sfsTest.c index 0b531e0..63ce0af 100644 --- a/src/sfsTest.c +++ b/src/sfsTest.c @@ -624,3 +624,160 @@ KTEST(sfsChownReassignsOwnerAndBumpsCtime) KT_FALSE(t, sfsChown(9999, 1000)); } +/* Same real point as sfsPersistsUidAcrossUnmountRemount above, but for a + uid REASSIGNED by sfsChown after creation, not just the creation-time + default -- proves the chown write path itself lands on disk, not just + that createNode's own initial stamp does. */ +KTEST(sfsPersistsChownedUidAcrossUnmountRemount) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + int32 f = sfsCreateFile("chownpersist.txt"); + if (!KT_TRUE(t, f > 0)) + return; + KT_TRUE(t, sfsChown(f, 8123)); + + sfsUnmount(); + KT_TRUE(t, sfsMount()); + + int32 reloaded = sfsLookup("chownpersist.txt"); + KT_EQ(t, reloaded, f); + KT_EQU(t, sfsUidOf(reloaded), 8123u); +} + +/* Real chgrp(2)'s on-disk half (sfsChgrp), mirroring + sfsChownReassignsOwnerAndBumpsCtime exactly one field over -- same "no + privilege check of its own, ctime bumps, invalid inode fails the same + way" shape. */ +KTEST(sfsChgrpReassignsGroupAndBumpsCtime) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + int32 f = sfsCreateFile("chgrpfile.txt"); + if (!KT_TRUE(t, f > 0)) + return; + KT_EQU(t, sfsGidOf(f), 0u); + + uint64 ctimeBefore = sfsCtimeOf(f); + int64 before = rtcNow(); + KT_TRUE(t, sfsChgrp(f, 9002)); + int64 after = rtcNow(); + + KT_EQU(t, sfsGidOf(f), 9002u); + KT_GE(t, sfsCtimeOf(f), ctimeBefore); + KT_GE(t, sfsCtimeOf(f), before); + KT_LE(t, sfsCtimeOf(f), after); + + KT_FALSE(t, sfsChgrp(-1, 1000)); + KT_FALSE(t, sfsChgrp(9999, 1000)); +} + +/* Same real point as sfsPersistsChownedUidAcrossUnmountRemount above, one + field over: proves the chgrp write path itself lands on disk. */ +KTEST(sfsPersistsChgrpedGidAcrossUnmountRemount) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + int32 f = sfsCreateFile("chgrppersist.txt"); + if (!KT_TRUE(t, f > 0)) + return; + KT_TRUE(t, sfsChgrp(f, 7124)); + + sfsUnmount(); + KT_TRUE(t, sfsMount()); + + int32 reloaded = sfsLookup("chgrppersist.txt"); + KT_EQ(t, reloaded, f); + KT_EQU(t, sfsGidOf(reloaded), 7124u); +} + +/* Real directory-write enforcement at the on-disk choke point (sfs.c's + createNode, shared by sfsCreateFile/sfsCreateDir/sfsCreateFifo/ + sfsCreateSymlink) -- mirrors coreTest.c's tmpfs + coreVfsMkdirAndCreateRequireParentWrite for the SFS backing store. + ktestRunAll always runs as uid 0, and vfsPermitted has no uid-0 bypass, + so removing the parent's own owner-write bit denies even its own + creator/owner. */ +KTEST(sfsCreateNodeRequiresParentWrite) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + KT_TRUE(t, sfsCreateDir("dwparent") > 0); + KT_TRUE(t, sfsChmod(sfsLookup("dwparent"), 0555)); /* r-x, no owner-write */ + + KT_EQ(t, sfsCreateFile("dwparent/f"), -1); + KT_EQ(t, sfsCreateDir("dwparent/sub"), -1); + + KT_TRUE(t, sfsChmod(sfsLookup("dwparent"), fsModeDefaultDir)); + KT_TRUE(t, sfsCreateFile("dwparent/f") > 0); + KT_TRUE(t, sfsCreateDir("dwparent/sub") > 0); +} + +/* Same parent-write rule applied to sfsLink -- adding a new hardlink NAME + is a directory-entry creation exactly like sfsCreateFile, so it needs + write permission on the PARENT of the new name, not any permission on + the existing target being linked to. */ +KTEST(sfsLinkRequiresParentWriteOnNewName) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + KT_TRUE(t, sfsCreateFile("lwtarget.txt") > 0); + KT_TRUE(t, sfsCreateDir("lwparent") > 0); + KT_TRUE(t, sfsChmod(sfsLookup("lwparent"), 0555)); + + KT_EQ(t, sfsLink("lwtarget.txt", "lwparent/alias"), -1); + + KT_TRUE(t, sfsChmod(sfsLookup("lwparent"), fsModeDefaultDir)); + KT_TRUE(t, sfsLink("lwtarget.txt", "lwparent/alias") > 0); +} + +/* Real unlink(2)/rmdir(2) semantics at the on-disk choke point (sfs.c's + sfsRemove) -- mirrors coreTest.c's tmpfs + coreVfsUnlinkAndRmdirRequireParentWrite: removing an entry needs write + permission on the PARENT, never the entry's own mode. */ +KTEST(sfsRemoveRequiresParentWrite) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + KT_TRUE(t, sfsCreateDir("rwparent") > 0); + KT_TRUE(t, sfsCreateFile("rwparent/f") > 0); + + KT_TRUE(t, sfsChmod(sfsLookup("rwparent"), 0555)); + KT_FALSE(t, sfsRemove("rwparent/f")); + + KT_TRUE(t, sfsChmod(sfsLookup("rwparent"), fsModeDefaultDir)); + KT_TRUE(t, sfsRemove("rwparent/f")); +} + +/* Real POSIX search permission at the on-disk choke point (sfs.c's + lookupFrom, checked on every directory ANCESTOR a path walks through) -- + mirrors coreTest.c's tmpfs coreVfsLookupRequiresAncestorExec. Removing + the exec bit on an ancestor makes every lookup THROUGH it fail even + though the target inode itself never changed. */ +KTEST(sfsLookupRequiresAncestorExec) +{ + if (!KT_TRUE(t, sfsFormat())) + return; + + KT_TRUE(t, sfsCreateDir("xancestor") > 0); + int32 leaf = sfsCreateFile("xancestor/leaf"); + KT_TRUE(t, leaf > 0); + + KT_TRUE(t, sfsChmod(sfsLookup("xancestor"), 0600)); /* rw-, no exec */ + KT_EQ(t, sfsLookup("xancestor/leaf"), -1); + + KT_TRUE(t, sfsChmod(sfsLookup("xancestor"), fsModeDefaultDir)); + KT_EQ(t, sfsLookup("xancestor/leaf"), leaf); +} + +KTEST(sfsSymlinkResolvesToTargetFile) +{ + if (!KT_TRUE(t, sfsFormat())) + return; +