#include "ulib.h" #include "fs.h" /* * Real ring-3 proof of directory-level permission enforcement (vfs.c's * lookupNode/vfsMkdir/resolveOpen, sfs.c's lookupFrom/createNode/sfsRemove/ * sfsLink) -- the gap FALA 44 explicitly flagged as stored-but-unenforced: * before this fala, a directory's own mode bits were reported by stat/ls -l * but NEVER consulted by anything, so ANY uid could create/remove entries * in ANY directory and traverse ANY directory tree regardless of its mode. * Fully synchronous like identtest.c/permtest.c/rmtest.c: no injected * keystrokes, no shared shell channel, so there is no scheduling-timing * race to flake on regardless of host load. * * Three distinct real POSIX bits are proven separately, not just "denied * vs allowed" as one blob: * - write on a PARENT directory governs creating/removing entries in it * (not the entry's own mode -- real unlink(2)'s own rule). * - exec (search) on every ANCESTOR directory governs resolving a NAME * inside it, checked on every directory a path walks through. * - read on a directory ITSELF governs listing its entries (getdents), * a SEPARATE bit from exec -- a directory can be searchable (you can * open a file inside it by name) without being listable, and this test * proves that distinction concretely, not just asserts it in a comment. */ int main(void) { print("dirpermtest: start, pid="); printDec(userGetpid()); print("\n"); if (userGetuid() != 0) { print("dirpermtest: FAIL boot-time uid is not 0\n"); return 1; } /* --- setup, entirely as root --- */ if (userMkdir("/tmp/dpd") != 0) { print("dirpermtest: FAIL mkdir /tmp/dpd\n"); return 1; } int sfd = (int)userOpen("/tmp/dpd/shared.txt", oReadWrite | oCreate); if (sfd < 0 || userWrite(sfd, "s", 1) != 1) { print("dirpermtest: FAIL create shared.txt\n"); return 1; } userClose(sfd); /* noexec: root removes ALL bits (0000) after creating one file inside it -- no owner or other exec at all, so NOBODY (not even root) can search inside it afterward. */ if (userMkdir("/tmp/dpd/noexec") != 0) { print("dirpermtest: FAIL mkdir noexec\n"); return 1; } int ifd = (int)userOpen("/tmp/dpd/noexec/inside.txt", oReadWrite | oCreate); if (ifd < 0 || userWrite(ifd, "i", 1) != 1) { print("dirpermtest: FAIL create inside.txt\n"); return 1; } userClose(ifd); if (userChmod("/tmp/dpd/noexec", 0000) != 0) { print("dirpermtest: FAIL chmod noexec to 0000\n"); return 1; } /* noread: owner keeps full rwx, other gets search (x) but not list (r) -- proves exec and read are genuinely separate bits, not aliases. */ if (userMkdir("/tmp/dpd/noread") != 0) { print("dirpermtest: FAIL mkdir noread\n"); return 1; } int vfd = (int)userOpen("/tmp/dpd/noread/visible.txt", oReadWrite | oCreate); if (vfd < 0 || userWrite(vfd, "v", 1) != 1) { print("dirpermtest: FAIL create visible.txt\n"); return 1; } userClose(vfd); if (userChmod("/tmp/dpd/noread", 0711) != 0) { print("dirpermtest: FAIL chmod noread to 0711\n"); return 1; } /* writable: full rwx for owner AND other -- proves a non-owner CAN create/remove entries when the other-write bit genuinely allows it, not that this feature is a blanket deny. */ if (userMkdir("/tmp/dpd/writable") != 0) { print("dirpermtest: FAIL mkdir writable\n"); return 1; } if (userChmod("/tmp/dpd/writable", 0777) != 0) { print("dirpermtest: FAIL chmod writable to 0777\n"); return 1; } /* owned: reassigned to uid 1000 (real chown(2)) BEFORE this process ever drops privilege, mirroring identtest.c's owned.txt -- after the drop, this process really is the OWNER, so the owner bits (not the other bits) govern. */ if (userMkdir("/tmp/dpd/owned") != 0) { print("dirpermtest: FAIL mkdir owned\n"); return 1; } if (userChown("/tmp/dpd/owned", 1000) != 0) { print("dirpermtest: FAIL chown owned to 1000\n"); return 1; } /* rootRO: root removes its OWN write bit (0555) -- proves no uid-0 superuser bypass exists for directory writes either, matching vfsPermitted's own file-level rule (identtest.c's chmod-nonowner- denied-ok proved the same "no bypass" story for file bits). Checked WHILE STILL ROOT, before the drop below. */ if (userMkdir("/tmp/dpd/rootRO") != 0) { print("dirpermtest: FAIL mkdir rootRO\n"); return 1; } if (userChmod("/tmp/dpd/rootRO", 0555) != 0) { print("dirpermtest: FAIL chmod rootRO to 0555\n"); return 1; } print("dirpermtest: setup-ok\n"); if (userOpen("/tmp/dpd/rootRO/x", oWriteOnly | oCreate) >= 0) { print("dirpermtest: FAIL root created a file with its own write bit off\n"); return 1; } print("dirpermtest: root-self-write-denied-ok\n"); /* --- real, permanent privilege drop --- */ if (userSetuid(1000) != 0 || userGetuid() != 1000) { print("dirpermtest: FAIL setuid(1000)\n"); return 1; } print("dirpermtest: drop-to-1000-ok\n"); /* /tmp/dpd itself is root-owned, default 0755 (other = r-x, no w) -- a non-owner cannot create a new entry in it. */ if (userOpen("/tmp/dpd/newfile.txt", oWriteOnly | oCreate) >= 0) { print("dirpermtest: FAIL non-owner created entry in default-mode dir\n"); return 1; } print("dirpermtest: parent-write-denied-ok\n"); /* writable/ is 0777 -- other-write is genuinely granted, so create AND remove must both succeed for this non-owner. */ int nfd = (int)userOpen("/tmp/dpd/writable/n.txt", oWriteOnly | oCreate); if (nfd < 0) { print("dirpermtest: FAIL non-owner denied create in 0777 dir\n"); return 1; } userClose(nfd); print("dirpermtest: other-write-allowed-ok\n"); if (userUnlink("/tmp/dpd/writable/n.txt") != 0) { print("dirpermtest: FAIL non-owner denied unlink in 0777 dir\n"); return 1; } print("dirpermtest: other-unlink-allowed-ok\n"); /* noexec/ has NO exec bit for anyone -- resolving inside.txt inside it must fail at the ancestor search-permission check, not at some later stage. */ if (userOpen("/tmp/dpd/noexec/inside.txt", oReadOnly) >= 0) { print("dirpermtest: FAIL traversed into a no-exec directory\n"); return 1; } print("dirpermtest: traverse-denied-ok\n"); /* /tmp/dpd itself is still searchable (other-exec=1 from the default 0755) even though this same process cannot WRITE into it -- proves the denial above was specific to noexec's own missing bit, not a blanket failure of every directory this non-owner touches. */ char buf[4]; int rfd = (int)userOpen("/tmp/dpd/shared.txt", oReadOnly); if (rfd < 0 || userRead(rfd, buf, 1) != 1) { print("dirpermtest: FAIL could not read shared.txt through a searchable dir\n"); return 1; } userClose(rfd); print("dirpermtest: traverse-allowed-ok\n"); /* noread/ grants other-exec but not other-read: opening the DIRECTORY itself for listing must fail... */ if (userOpen("/tmp/dpd/noread", oReadOnly) >= 0) { print("dirpermtest: FAIL listed a directory with no read bit\n"); return 1; } print("dirpermtest: list-denied-ok\n"); /* ...but opening a FILE inside it BY NAME must still succeed, since that only needs search (exec), never list (read), on noread itself -- the exact distinction real Unix's `ls somedir/knownfile` (works without +r on somedir) vs `ls somedir` (needs +r) draws. */ int nrfd = (int)userOpen("/tmp/dpd/noread/visible.txt", oReadOnly); if (nrfd < 0) { print("dirpermtest: FAIL could not open known file via exec-only dir\n"); return 1; } userClose(nrfd); print("dirpermtest: search-without-list-ok\n"); /* owned/ was chowned to 1000 before the drop -- this process really is the owner now, so default 0755's OWNER bits (full rwx) govern, not the other bits a stranger would be limited to. */ int ofd = (int)userOpen("/tmp/dpd/owned/mine.txt", oWriteOnly | oCreate); if (ofd < 0) { print("dirpermtest: FAIL owner denied create in its own directory\n"); return 1; } userClose(ofd); if (userUnlink("/tmp/dpd/owned/mine.txt") != 0) { print("dirpermtest: FAIL owner denied unlink in its own directory\n"); return 1; } print("dirpermtest: owner-write-allowed-ok\n"); print("dirpermtest: PASS\n"); return 0; }