From 33578598cad51d2952316982a6931033e1bc7408 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 2 Sep 2026 17:22:18 +0200 Subject: [PATCH] feat: extend vfs --- include/vfs.h | 197 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 1 deletion(-) diff --git a/include/vfs.h b/include/vfs.h index e9832c8..6d95059 100644 --- a/include/vfs.h +++ b/include/vfs.h @@ -9,14 +9,36 @@ struct VfsNode; typedef enum { fdFree = 0, fdConsole = 1, - fdFile = 2 + fdFile = 2, + fdPipeRead = 3, + fdPipeWrite = 4, + fdBlockDev = 5, + fdSfs = 6, + /* pidfd.h handle wrapped as an ordinary fd (FileDescriptor.pipeHandle + reused to store the pidfd.c table index, same field-reuse convention + fdPipeRead/fdPipeWrite already use for a pipe.c handle) -- unlike + anonymous pipes, a pidfd never needs the cross-process + pipeCreateDetached/vfsPipeAttach dance: it is opened by, and lives + entirely within, the SAME process that calls vfsPidfdOpen, exactly + like real Linux's pidfd_open(2). */ + fdPidfd = 7 } FdKind; +/* FileDescriptor.flags bit for a pipe end installed by vfsPipeAttach: reads/ + writes on it should really block (via syscall.c's sysRead/sysWrite retry) + rather than report "would block" (-1) the way vfsPipe's anonymous, + single-process pipe ends always have (see pipetest.c, which depends on + that -1 -- unflagged, unchanged). File-kind descriptors already store + their vfsOpen flags (oReadOnly etc, fs.h) in this same field, but never a + pipe fd, so the two uses never collide. */ +#define fdFlagBlocking 0x1 + typedef struct FileDescriptor { struct VfsNode* node; uint64 offset; uint32 kind; uint32 flags; + int32 pipeHandle; } FileDescriptor; #define maxOpenFiles 16 @@ -35,6 +57,179 @@ int vfsStat(const char* path, FbStat* out); int64 vfsGetdents(int fd, void* buffer, uint64 length); int vfsMkdir(const char* path); int vfsUnlink(const char* path); +int vfsRmdir(const char* path); + +/* Real chmod(2)/chown(2) privilege rule, factored out as a pure predicate + exactly like vfsPermitted itself (see that function's own doc comment for + why): true if `callerUid` may change `ownerUid`-owned metadata, i.e. is + either the owner itself or uid 0 ("root", administrative bypass -- unlike + vfsPermitted's read/write/exec check, which deliberately has none). Only + chmod uses this; chown is stricter still (root-only, see vfsChown) since + real Unix's default _POSIX_CHOWN_RESTRICTED denies even a file's own + owner the right to give it away. Pure and explicit-argument like + vfsPermitted so it stays testable without needing a live thread's own + uid -- KTEST always runs on the idle thread (permanently uid 0), which + could never otherwise exercise the "denied" side of this check. */ +bool vfsOwnerOrRoot(uint32 ownerUid, uint32 callerUid); + +/* Real chmod(2): overwrites `path`'s whole owner-rwx mode (fs.h's + fsModeOwner*) with `mode` -- no symbolic +x-style merge, matching real + chmod's own numeric-mode form. Refuses unless the caller owns the file or + is uid 0 (vfsOwnerOrRoot above) -- a real, previously-missing check: every + uid could chmod any file's bits until this was added. Dispatches on + isSfsPath like every other entry point above (sfsChmod for "/mnt", the + tmpfs VfsNode's own `mode` field otherwise). Returns -1 for a missing + path, or (tmpfs only) a node under a non-writable mount -- chmod is a + metadata mutation, gated by the exact same `writable` flag every other + tmpfs mutation (vfsUnlink, vfsWrite's write path) already respects, so a + ustar-loaded /bin/ *.elf file can never have its mode changed at runtime, + matching its read-only backing store. 0 on success. */ +int vfsChmod(const char* path, uint32 mode); + +/* Real chown(2): overwrites `path`'s owner uid outright. Root-only + (currentThreadUid() == 0), full stop -- unlike vfsChmod, ownership is + never delegated to the file's own owner (real Unix's + _POSIX_CHOWN_RESTRICTED default: an owner who could give a file away + could use that to defeat disk-quota accounting, so only root may). Same + isSfsPath dispatch and missing-path/non-writable-mount refusal as + vfsChmod. 0 on success. */ +int vfsChown(const char* path, uint32 newUid); + +/* Real chgrp(2): overwrites `path`'s group gid. Unlike vfsChown (root-only, + full stop), chgrp is delegated to the file's own owner too -- but only to + set the group to the CALLER's own current gid (currentThreadGid()), never + an arbitrary one: this kernel tracks exactly one gid per thread (no + supplementary-groups list), so "a group the caller belongs to" collapses + to "the caller's own current gid", matching real chgrp(2)'s own + membership requirement for a non-privileged caller. Root (uid 0) may set + any gid, same administrative bypass vfsOwnerOrRoot already grants chmod. + Same isSfsPath dispatch and missing-path/non-writable-mount refusal as + vfsChown. 0 on success. */ +int vfsChgrp(const char* path, uint32 newGid); + +/* Symlinks and hardlinks are only supported on the persistent "/mnt" + filesystem (sfs.h already has the on-disk machinery for both); any other + path (the ustar/tmpfs VfsNode tree, "/dev/disk0") rejects these with -1, + the same way a real filesystem that lacks the feature would refuse the + syscall rather than fake it. */ +int vfsSymlink(const char* path, const char* target); +int64 vfsReadlink(const char* path, char* buffer, uint64 cap); +int vfsLink(const char* existingPath, const char* newPath); + +/* Creates an anonymous pipe and installs its two ends into the current + thread's fd table. On success readFd/writeFd receive the descriptors and + 0 is returned; on failure (fd table or pipe table exhausted) -1 is + returned and neither output is written. */ +int vfsPipe(int* readFd, int* writeFd); + +/* Creates a pipeCreateDetached handle (pipe.h) with neither end attached to + anyone's fd table and returns its raw number, or -1 on exhaustion. Meant + to be handed to two independently spawned processes (typically as a + decimal argv string, see user/fifomaster.c) so they can rendezvous over a + real blocking pipe without needing fork() to share an fd -- see + vfsPipeAttach. */ +int vfsPipeCreateHandle(void); + +/* Installs `handle` (from vfsPipeCreateHandle, or in principle any live + pipe handle) into a fresh slot in the CALLING thread's own fd table, as a + read end (wantWrite false) or write end (wantWrite true), flagged + fdFlagBlocking so syscall.c's sysRead/sysWrite give it real blocking + semantics. Returns the new fd, or -1 if the handle is not a live pipe or + the fd table is full. */ +int vfsPipeAttach(int handle, bool wantWrite); + +/* Same as vfsPipeAttach, but installs into fd slot `fd` of an EXPLICIT fd + table instead of allocating a free slot in the CALLING thread's own + table -- jobctl.c's own way to bake a real pipe end into a brand new + child's stdin/stdout (fd 0/1) before that child has run a single + instruction, so there is never a window where it could be scheduled with + its default console fds instead (see jobctlSpawnPiped, jobctl.h). `fd` is + trusted to already name a valid slot (0/1/2) in `targetFds` -- unlike + vfsPipeAttach, this never allocates a fresh one, so the caller decides + exactly which fd is overwritten. Silently does nothing if `handle` is not + a live pipe. */ +void vfsPipeAttachInto(FileDescriptor* targetFds, int fd, int handle, bool wantWrite); + +/* Opens a pidfd (pidfd.h) on `targetId` and installs it as a brand new + fdPidfd descriptor in the CALLING thread's own fd table -- the + fd-table-wrapping half of pidfd.h's raw pidfdOpen, exactly the same + split vfsPipe/vfsPipeAttach already have over pipe.c's raw + pipeCreate/pipeCreateDetached. Returns the new fd, or -1 if `targetId` + is not a valid, currently-live thread (pidfdOpen's own contract) or the + calling thread's own fd table is full. */ +int vfsPidfdOpen(uint32 targetId); + +/* Resolves fd `fd` (which must be an fdPidfd this thread's own vfsPidfdOpen + returned) to the live target thread id it still safely refers to, or + threadInvalidId if `fd` is not an open pidfd at all, or the original + target has since been reaped/recycled (pidfdTarget's own contract, + pidfd.h) -- the single check syscall.c's sysPidfdSend/sysPidfdWait both + need before ever touching a target by number. */ +uint32 vfsPidfdTarget(int fd); + +/* Same as vfsOpen, but installs into fd slot `fd` of an EXPLICIT fd table + instead of allocating a free slot in the CALLING thread's own table -- + the file-redirection counterpart of vfsPipeAttachInto above, used by + jobctl.c's own jobctlApplyRedirects to bake a real file open (not a + pipe) into a freshly spawned child's stdin/stdout for shell.c's new + `cmd < file` / `cmd > file` / `cmd >> file` syntax, before that child has + run a single instruction. `fd` is trusted to already name a valid slot + (0/1/2) in `targetFds`, exactly like vfsPipeAttachInto. Returns 0 on + success, -1 on the same failures vfsOpen itself can have (missing path + without oCreate, unwritable parent directory, a fifo opened with a bad + mode, exhausted pipe/inode resources) -- leaves `targetFds[fd]` + completely untouched on failure, the same "never marks a fd used when it + fails" contract vfsOpen already has for its own newly allocated fd. */ +int vfsOpenInto(FileDescriptor* targetFds, int fd, const char* path, uint32 flags); + +/* nullptr unless `fd` is a fdFlagBlocking pipe/fifo end or a console (fd 0) + read whose next read/write would have to report "try again" for a + resolvable reason (a pipe buffer empty but a writer could still show up, + full but a reader could still drain it, or a console queue simply not + having a keystroke queued yet) -- in which case it is the channel + (sched.h) that end's condition changing will wake. syscall.c's + sysRead/sysWrite call this after a -1 from doRead/doWrite to decide + whether to park-and-retry the syscall (schedBlockCurrent + rewind rip, + same trick sysWait uses for threadJoin) or simply hand -1 back to + userspace as-is (a non-blocking pipe end, a console write, or a + permanently broken/at-EOF pipe that retrying could never fix). */ +const void* vfsBlockingChannel(int fd, bool forWrite); + +/* poll.h's sysPoll (syscall.c): fills *outRevents with the subset of + `events` (poll.h's pollIn/pollOut) `fd` could satisfy right now without + blocking -- pollNval instead if `fd` is not open at all, pollErr for a + pipe write end whose only reader has gone away (a real write there would + fail immediately, not block, so it counts as "ready" the same way a + readable EOF pipe does). A regular file, "/dev/disk0", or "/mnt" fd + always reports every requested bit ready, matching real poll(2): none of + this kernel's local-storage reads/writes ever return "would block". + Returns true iff *outRevents ended up nonzero, i.e. this fd alone would + already satisfy a poll call. */ +bool vfsPollReady(int fd, int16 events, int16* outRevents); + +/* Named pipe: creates a real filesystem entry of type fsTypeFifo at `path` + (must live under a writable directory, e.g. the "/tmp" tmpfs mount -- + same requirement makeChildFile already has for oCreate) backed by a fresh + pipeCreateDetached handle. Unlike vfsPipeCreateHandle/vfsPipeAttach, which + need SOME cooperating parent to pass the raw handle number to both sides + (typically as an argv string, see user/fifomaster.c), a fifo node lets any + two processes that simply agree on a path -- no common ancestor, no + handle to pass -- rendezvous by both calling vfsOpen on it: whichever + opens with oWriteOnly gets a blocking write end, whichever opens with + oReadOnly gets a blocking read end, exactly like vfsPipeAttach's fds. + oReadWrite (or any other flag combination) is rejected -- this kernel's + pipe end is inherently one-directional, so a dual-direction fifo fd is + out of scope, same as vfsPipeAttach's wantWrite being a single bool. + Returns 0 on success, -1 if the path already exists, its parent is not a + writable directory, or the pipe table is exhausted. Not supported under + "/dev/disk0". + + Under "/mnt" this creates a REAL, persistent on-disk entry (sfs.h's + sfsTypeFifo) that survives sfsUnmount()+sfsMount() -- only the special + file itself persists, never any buffered bytes (exactly like a real + mkfifo(2) special file survives a reboot while its pipe buffer does + not): the live pipe.h handle two openers actually exchange bytes + through is created lazily, in memory, the first time anyone opens the typedef struct VfsNode VfsNode; VfsNode* vfsLookup(const char* path);