diff --git a/src/coreTest.c b/src/coreTest.c index 9825329..1e70ed2 100644 --- a/src/coreTest.c +++ b/src/coreTest.c @@ -314,3 +314,161 @@ KTEST(coreVfsResolveOpenRequiresFileReadPermission) KT_GE(t, wfd2, 0); /* write-only still works -- the two bits are independent */ vfsClose(wfd2); + KT_EQ(t, vfsChmod("/tmp/readf", fsModeDefaultFile), 0); + int rfd2 = vfsOpen("/tmp/readf", oReadOnly); + KT_GE(t, rfd2, 0); + vfsClose(rfd2); + + vfsUnlink("/tmp/readf"); +} + +/* chmod on a missing path, or on the never-writable ustar-loaded /bin + content, is rejected exactly like every other tmpfs mutation already + gates on node->writable (vfsUnlink, vfsWrite's write path) -- chmod is a + metadata mutation too, so it obeys the same rule. Also proves every + ustar-loaded binary really does get the owner-exec bit by default + (vfs.c's ustarAdd), the fact loadImageByPath/jobctlSpawn's own exec- + permission check depends on for every existing spawnUser/userSpawn call + in this kernel to keep working at all. */ +KTEST(coreVfsChmodRejectsMissingAndReadonlyBin) +{ + KT_EQ(t, vfsChmod("/tmp/doesNotExist", 0644), -1); + KT_EQ(t, vfsChmod("/bin/false", 0777), -1); + + FbStat st; + KT_EQ(t, vfsStat("/bin/false", &st), 0); + KT_EQ(t, st.mode, fsModeDefaultExec); +} + +/* Same round trip on the persistent SFS mount, PLUS the "survives + unmount+remount" proof every other SFS-persisted field (mtime/ctime, + FALA 39) already has -- mode genuinely lives on disk, not just in the + in-memory inode cache. */ +KTEST(coreVfsChmodSfsPersistsAcrossRemount) +{ + KT_EQ(t, vfsMkdir("/mnt/chmodd"), 0); + int fd = vfsOpen("/mnt/chmodd/f", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); + + FbStat st; + KT_EQ(t, vfsStat("/mnt/chmodd/f", &st), 0); + KT_EQ(t, st.mode, fsModeDefaultFile); + + KT_EQ(t, vfsChmod("/mnt/chmodd/f", 0400), 0); + KT_EQ(t, vfsOpen("/mnt/chmodd/f", oWriteOnly), -1); + + sfsUnmount(); + KT_TRUE(t, sfsMount()); + + KT_EQ(t, vfsStat("/mnt/chmodd/f", &st), 0); + KT_EQ(t, st.mode, 0400u); + KT_EQ(t, vfsOpen("/mnt/chmodd/f", oWriteOnly), -1); + + KT_EQ(t, vfsChmod("/mnt/chmodd/f", fsModeDefaultFile), 0); + int wfd = vfsOpen("/mnt/chmodd/f", oWriteOnly); + KT_GE(t, wfd, 0); + vfsClose(wfd); + + vfsUnlink("/mnt/chmodd/f"); + vfsRmdir("/mnt/chmodd"); +} + +/* Real Unix owner/group/other permission algorithm (vfs.c's vfsPermitted) + -- pure-logic coverage of the whole decision matrix, since the function + takes every credential as an explicit argument instead of reading a live + thread (see its own doc comment). Deliberately includes uid 0 on BOTH + sides of the owner/other split (coreVfsPermittedUidZeroIsNotASuperuser + below) -- uid 0 gets no special treatment at all, the one property that + distinguishes this from real Unix's root and the exact regression this + test guards: an earlier version of vfsPermitted let uid 0 bypass every + bit unconditionally, which silently broke every OTHER permission test in + this file (coreVfsChmodTmpfsWritePermission, + coreVfsChmodSfsPersistsAcrossRemount both run as ktestRunAll's own idle + thread, uid 0) the moment chmod took away the write bit on a file that + same uid-0 caller had just created. Every "hits the other branch" case + below deliberately gives a MISMATCHED gid too (not just a mismatched + uid) -- otherwise an incidental gid match would silently redirect the + check into the new group branch instead of exercising other at all. */ +KTEST(coreVfsPermittedOwnerMatchChecksOwnerBitOnly) +{ + /* Owner-uid match short-circuits before gid is ever consulted, so gid + values here are deliberately irrelevant -- 0 on both sides. */ + /* Owner bit set, other bit clear: an owning caller passes. */ + KT_TRUE(t, vfsPermitted(0600, 1000, 0, 1000, 0, fsModeOwnerWrite)); + /* Owner bit clear, other bit set: an owning caller still fails -- + owning the file never grants what the OTHER bits happen to allow. */ + KT_FALSE(t, vfsPermitted(0004, 1000, 0, 1000, 0, fsModeOwnerWrite)); +} + +KTEST(coreVfsPermittedNonOwnerChecksOtherBitOnly) +{ + /* Other-write set, owner-write clear, DIFFERENT uid AND gid: passes on + the other bit even though the owner bit alone would refuse. */ + KT_TRUE(t, vfsPermitted(0002, 1000, 10, 2000, 20, fsModeOwnerWrite)); + /* Neither bit helps a stranger if the other bit is the one clear. */ + KT_FALSE(t, vfsPermitted(0600, 1000, 10, 2000, 20, fsModeOwnerWrite)); + KT_FALSE(t, vfsPermitted(0000, 1000, 10, 2000, 20, fsModeOwnerExec)); +} + +KTEST(coreVfsPermittedUidZeroIsNotASuperuser) +{ + /* uid 0 as OWNER: governed by the owner bit exactly like any other + uid, matching every existing chmod test's own assumption (every + boot-time process, including ktestRunAll itself, starts at uid 0). */ + KT_TRUE(t, vfsPermitted(0600, 0, 0, 0, 0, fsModeOwnerWrite)); + KT_FALSE(t, vfsPermitted(0000, 0, 0, 0, 0, fsModeOwnerWrite)); + /* uid 0 as a NON-owner (and non-group-member): governed by the other + bit, no bypass at all -- a mode of 0 denies even uid 0 against + someone else's file. */ + KT_FALSE(t, vfsPermitted(0700, 1000, 10, 0, 20, fsModeOwnerWrite)); + KT_TRUE(t, vfsPermitted(0702, 1000, 10, 0, 20, fsModeOwnerWrite)); + /* And symmetrically: owning a file as a non-zero uid is not somehow + MORE privileged than uid 0 owning one -- same algorithm either way. */ + KT_FALSE(t, vfsPermitted(0600, 42, 10, 0, 20, fsModeOwnerWrite)); +} + +/* Every fsModeOwner/fsModeOther pair used by a real choke point + (resolveOpen's write check, loadImageByPath/jobctlSpawn's exec check) + round-trips through the exact bit-shift vfsPermitted derives one from + the other with -- not just fsModeOwnerWrite from the test above. */ +KTEST(coreVfsPermittedExecBitFollowsSameOwnerOtherSplit) +{ + KT_TRUE(t, vfsPermitted(fsModeOtherExec, 1, 10, 2, 20, fsModeOwnerExec)); + KT_FALSE(t, vfsPermitted(fsModeOwnerExec, 1, 10, 2, 20, fsModeOwnerExec)); + KT_TRUE(t, vfsPermitted(fsModeOwnerExec, 1, 10, 1, 10, fsModeOwnerExec)); +} + +/* The new middle branch: a caller whose UID does not match the owner but + whose GID matches the node's group is governed by the fsModeGroup* bit + (wantOwnerBit >> 3), never falling all the way through to other -- and a + group bit set does NOT imply the other bit is, proving this is a + genuinely separate check, not just "not owner" collapsing to one + fallback like the pre-group-model version of this function used to. */ +KTEST(coreVfsPermittedGroupMatchChecksGroupBitOnly) +{ + /* Group-write set, owner/other-write clear: different uid, same gid. */ + KT_TRUE(t, vfsPermitted(0020, 1000, 10, 2000, 10, fsModeOwnerWrite)); + /* Group bit clear: a gid match alone grants nothing by itself. */ + KT_FALSE(t, vfsPermitted(0602, 1000, 10, 2000, 10, fsModeOwnerWrite)); + /* Every fsModeOwner/fsModeGroup pair round-trips through the >>3 shift, + not just write above. */ + KT_TRUE(t, vfsPermitted(fsModeGroupExec, 1, 10, 2, 10, fsModeOwnerExec)); + /* Owner-uid match takes priority over a coincidental gid match: the + caller here is BOTH the owner AND shares the owner's own gid, but the + decision must still come from the owner bit, not group. */ + KT_TRUE(t, vfsPermitted(0100, 1, 10, 1, 10, fsModeOwnerExec)); + KT_FALSE(t, vfsPermitted(0010, 1, 10, 1, 10, fsModeOwnerExec)); +} + +/* Real owner identity (fs.h's FbStat.uid, vfs.c's vfsUid): a fresh tmpfs + node is stamped with whoever created it -- ktestRunAll always runs as + the permanent idle thread (id 0, uid 0, "root"), the same context + schedTest.c's own pgid-default tests already document relying on, so + every node created here is root-owned by construction. */ +KTEST(coreVfsTmpfsNodeStampsCreatorAsOwner) +{ + int fd = vfsOpen("/tmp/uidf", oReadWrite | oCreate); + KT_GE(t, fd, 0); + vfsClose(fd); +