From afe8ddf044cbaf4d53062f26f743067daf684197 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 19:36:34 +0200 Subject: [PATCH] test: cover dirpermtest --- user/dirpermtest.c | 220 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 user/dirpermtest.c diff --git a/user/dirpermtest.c b/user/dirpermtest.c new file mode 100644 index 0000000..d00f156 --- /dev/null +++ b/user/dirpermtest.c @@ -0,0 +1,220 @@ +#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; +}