Files
2026-09-02 19:05:09 +02:00

342 lines
19 KiB
C

#ifndef FROSTBYTE_VFS_H
#define FROSTBYTE_VFS_H
#include "types.h"
#include "fs.h"
struct VfsNode;
typedef enum {
fdFree = 0,
fdConsole = 1,
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
void vfsInit(void);
void vfsMountUstar(uint64 start, uint64 end);
void vfsMountTmpfs(const char* path);
void vfsFdInit(FileDescriptor* fds);
int vfsOpen(const char* path, uint32 flags);
int vfsClose(int fd);
int64 vfsRead(int fd, void* buffer, uint64 length);
int64 vfsWrite(int fd, const void* buffer, uint64 length);
int64 vfsLseek(int fd, int64 offset, int whence);
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
path after boot (or after every previous session's ends have gone away),
same mechanism as the "/tmp"-style case above, just keyed by inode
number instead of living on a VfsNode (see vfs.c's sfsFifoHandleFor). */
int vfsMkfifo(const char* path);
/* "/dev/disk0" is a reserved path (not part of the mounted tree, same as
pipes) that opens the raw ATA disk as a byte-addressable device: reads
and writes at arbitrary offsets are translated to sector I/O through
ata.h, with read-modify-write for accesses that are not sector-aligned. */
/* "/mnt" (and everything under it) is a second reserved path, backed by the
persistent on-disk filesystem in sfs.h/sfs.c rather than the kmalloc-based
VfsNode tree: open/read/write/close/lseek/stat/mkdir/unlink/getdents on a
"/mnt..." path are all dispatched to sfs.c, which stores the data on the
same ATA disk (a different LBA range than /dev/disk0's raw byte access,
see sfs.h). Data written there survives vfsInit() being called again,
because it lives on disk, not in this process's heap. */
/* Real per-process chdir(2)/getcwd(2) (sched.h's Thread.cwd). Resolves
`path` against the CALLING thread's current cwd first (vfsResolvePath
below), so a relative argument works exactly like a real chdir() call,
then requires the result to already name a real directory -- under
"/mnt" (sfs.h) or in the ordinary VfsNode tree, checked the same way
vfsStat/vfsMkdir already dispatch on isSfsPath. Returns 0 on success (the
calling thread's own cwd is now that resolved absolute path) or -1,
changing nothing, if it does not name an existing directory. For a
VfsNode-tree directory (ustar/tmpfs), the stored result is canonicalized
by walking the resolved node's real parent chain (vfsPath below), so
"."/".." components the caller typed never survive into a later
vfsGetcwd(). "/mnt" (sfs.h) has no equivalent parent-walk primitive, so
THAT branch stores vfsResolvePath's own plain string join verbatim --
every path-taking entry point below already tolerates embedded "."/".."
wherever they appear regardless (see vfs.c's lookupNode/sfsLookup), so
later relative resolution still behaves correctly either way; only
vfsGetcwd's own output could then look non-canonical (e.g.
"/mnt/foo/../bar") for an "/mnt" cwd reached that way -- an
honestly-scoped simplification, not a fake canonicalization. */
int vfsChdir(const char* path);
/* Copies the calling thread's current working directory (always already
absolute, see vfsChdir above) into `out`, nul-terminated, and returns its
length -- or -1, writing nothing, if it does not fit `cap` (real
getcwd(2)'s own ERANGE case). */
int64 vfsGetcwd(char* out, uint64 cap);
/* Resolves `path` against the CALLING thread's own cwd (sched.h's
currentThreadCwd) into `out`: copied as-is (truncated to `cap`-1 if
necessary) if it already starts with '/', otherwise joined onto the cwd
exactly like a real relative open() would -- the same "cwd + '/' +
argument" shape shell.c's own long-standing buildPath already used for
its builtins, now the ONE place that logic lives. Every path-taking
public entry point below (vfsOpen, vfsOpenInto, vfsMkdir, vfsUnlink,
vfsRmdir, vfsStat, vfsSymlink's `path` argument -- never its `target`,
which is stored verbatim like a real symlink(2) -- vfsReadlink, vfsLink,
vfsMkfifo, vfsChmod) calls this internally before doing anything else, so an
already-absolute path built by hand (shell.c's own callers, unchanged)
keeps working exactly as before. Exposed so vfsChdir can resolve ITS OWN
argument the same way before ever storing it, keeping Thread.cwd always
fully absolute. */
void vfsResolvePath(const char* path, char* out, uint64 cap);
typedef struct VfsNode VfsNode;
VfsNode* vfsLookup(const char* path);
VfsNode* vfsChild(VfsNode* dir);
VfsNode* vfsNext(VfsNode* node);
const char* vfsName(VfsNode* node);
bool vfsIsDir(VfsNode* node);
uint64 vfsSize(VfsNode* node);
const void* vfsFileData(VfsNode* node);
/* Real owner rwx permission bits (fs.h's fsModeOwner*) for a tmpfs VfsNode
-- the exact same value vfsStat's tmpfs branch reports as FbStat.mode.
Exposed as its own accessor (VfsNode is opaque outside vfs.c, same reason
vfsSize/vfsFileData exist) so jobctl.c's jobctlSpawn can check
fsModeOwnerExec on an already-vfsLookup'd node before spawning it,
without a redundant vfsStat + path re-resolve. */
uint32 vfsMode(VfsNode* node);
/* Real owner identity (fs.h's FbStat.uid) for a tmpfs VfsNode -- the uid
that was currentThreadUid() at nodeAlloc time (vfs.c), mirroring
vfsMode's own "opaque outside vfs.c" exposure exactly, for the same
reason: syscall.c's loadImageByPath and jobctl.c's jobctlSpawn need this
on an already-vfsLookup'd node before deciding whether to spawn it. */
uint32 vfsUid(VfsNode* node);
/* Real group identity (fs.h's FbStat.gid) for a tmpfs VfsNode -- the gid
that was currentThreadGid() at nodeAlloc time, mirroring vfsUid's own
"opaque outside vfs.c" exposure for the same reason (syscall.c's
loadImageByPath, jobctl.c's jobctlSpawn). */
uint32 vfsGid(VfsNode* node);
/* Real Unix owner/group/other permission check (vfs.c): does the caller
(`callerUid`/`callerGid`) have `wantOwnerBit` (one of fs.h's fsModeOwner*
constants) against a node whose bits are `mode` and whose owner is
`ownerUid`/`ownerGid`? A matching uid checks the owner bit; else a
matching gid checks the corresponding "group" bit (fs.h's fsModeGroup*,
wantOwnerBit >> 3); else the corresponding "other" bit (fs.h's
fsModeOther*, wantOwnerBit >> 6) -- deliberately no uid-0-bypasses-
everything superuser rule (see vfsPermitted's own doc comment in vfs.c
for why). Exported (not just used internally by resolveOpen) so
syscall.c's loadImageByPath and jobctl.c's jobctlSpawn can run the
identical exec-bit check instead of each re-deriving this algorithm.
Pure function of its six arguments -- callers pass currentThreadUid()/
currentThreadGid() in explicitly, this never reads a live thread
itself. */
bool vfsPermitted(uint32 mode, uint32 ownerUid, uint32 ownerGid, uint32 callerUid, uint32 callerGid, uint32 wantOwnerBit);
void vfsPath(VfsNode* node, char* out, uint64 cap);
#endif