342 lines
19 KiB
C
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
|