Files
frostbyte/src/vfs.c
T

1710 lines
57 KiB
C

#include "vfs.h"
#include "initrd.h"
#include "heap.h"
#include "string.h"
#include "console.h"
#include "keyboard.h"
#include "sched.h"
#include "pipe.h"
#include "pidfd.h"
#include "ata.h"
#include "sfs.h"
#include "poll.h"
#define tmpfsBlock 512
#define blockDevPath "/dev/disk0"
#define sfsMountPrefix "/mnt"
#define sfsMountPrefixLen 4
struct VfsNode {
char name[fsNameMax];
uint32 type;
bool writable;
/* Real owner rwx permission bits (fs.h's fsModeOwner*), an axis
independent of `writable` above: `writable` says whether this node's
CONTENT/CHILDREN may be mutated at all (a mount-level property, false
for every ustar-loaded node regardless of mode); `mode` is the real,
chmod(2)-settable per-node permission a caller must ALSO satisfy to
open the node for writing or (jobctl.c/syscall.c) exec it. Both gates
must pass, exactly like a real read-only mount PLUS real per-file
permission bits would. */
uint32 mode;
/* Real owner identity (POSIX uid, fs.h's FbStat.uid): whichever
currentThreadUid() was current at nodeAlloc time -- always 0 ("root")
for every ustar-loaded and boot-time tmpfs-mount node, since
vfsMountUstar/vfsMountTmpfs both run after schedInit but before any
real process exists (schedInit's idle thread, uid 0, is always
"current" then). Never retroactive, like mode's own default; there is
no chown-shaped call anywhere in this kernel to ever change it after
creation (see fs.h's FbStat.uid doc comment). */
uint32 uid;
/* Real group identity (fs.h's FbStat.gid): whichever currentThreadGid()
was current at nodeAlloc time, same creation-time-only, never-
retroactive shape as uid immediately above. */
uint32 gid;
const uint8* roData;
uint8* rwData;
uint64 size;
uint64 capacity;
/* Only meaningful for type == fsTypeFifo: the pipe.h handle every
vfsOpen on this path attaches a fresh end to (see makeChildFifo). -1
for every other node type. */
int32 pipeHandle;
struct VfsNode* parent;
struct VfsNode* firstChild;
struct VfsNode* nextSibling;
};
static VfsNode* root;
static VfsNode* nodeAlloc(const char* name, uint64 nameLen, uint32 type, bool writable, uint32 mode)
{
VfsNode* node = (VfsNode*)kmalloc(sizeof(VfsNode));
if (!node)
return nullptr;
memset(node, 0, sizeof(VfsNode));
if (nameLen >= fsNameMax)
nameLen = fsNameMax - 1;
memcpy(node->name, name, nameLen);
node->name[nameLen] = 0;
node->type = type;
node->writable = writable;
node->mode = mode;
node->uid = currentThreadUid();
node->gid = currentThreadGid();
node->pipeHandle = -1;
return node;
}
static void addChild(VfsNode* parent, VfsNode* child)
{
child->parent = parent;
child->nextSibling = parent->firstChild;
parent->firstChild = child;
}
static VfsNode* findChild(VfsNode* parent, const char* name, uint64 nameLen)
{
for (VfsNode* child = parent->firstChild; child; child = child->nextSibling)
if (strlen(child->name) == nameLen && memcmp(child->name, name, nameLen) == 0)
return child;
return nullptr;
}
static VfsNode* makeChildDir(VfsNode* parent, const char* name, uint64 nameLen, bool writable, uint32 mode)
{
VfsNode* node = nodeAlloc(name, nameLen, fsTypeDir, writable, mode);
if (node)
addChild(parent, node);
return node;
}
static VfsNode* makeChildFile(VfsNode* parent, const char* name, uint64 nameLen,
const uint8* data, uint64 size, bool writable, uint32 mode)
{
VfsNode* node = nodeAlloc(name, nameLen, fsTypeFile, writable, mode);
if (!node)
return nullptr;
if (writable) {
node->rwData = nullptr;
node->capacity = 0;
node->size = 0;
} else {
node->roData = data;
node->size = size;
}
addChild(parent, node);
return node;
}
static VfsNode* makeChildFifo(VfsNode* parent, const char* name, uint64 nameLen, int pipeHandle)
{
VfsNode* node = nodeAlloc(name, nameLen, fsTypeFifo, true, fsModeDefaultFile);
if (!node)
return nullptr;
node->pipeHandle = pipeHandle;
addChild(parent, node);
return node;
}
/* Real POSIX "search" permission: resolving a NAME inside a directory (as
opposed to "." / ".." above, which never look anything up) requires the
owner-or-other exec bit on that directory for the calling thread's own
uid, exactly like a real kernel's namei() checks X_OK on every directory
it walks through. Deliberately only gates the findChild() branch below,
not "."/".." -- an honest, minor scope boundary matching this codebase's
existing style of not fixing unrelated pre-existing gaps while adding a
new feature (see this file's many other "deliberately out of scope"
comments). Denial collapses to the same nullptr every other lookup
failure already returns, matching every other EACCES-shaped refusal in
this file (resolveOpen's write check, etc.) -- callers cannot tell
"does not exist" from "exists but not searchable", exactly like the rest
of this VFS layer's error handling. */
static VfsNode* lookupNode(const char* path)
{
if (!path)
return nullptr;
VfsNode* cur = root;
const char* p = path;
while (*p == '/')
p++;
while (*p) {
const char* start = p;
while (*p && *p != '/')
p++;
uint64 len = (uint64)(p - start);
if (len == 1 && start[0] == '.') {
/* stay */
} else if (len == 2 && start[0] == '.' && start[1] == '.') {
if (cur->parent)
cur = cur->parent;
} else {
if (cur->type != fsTypeDir)
return nullptr;
if (!vfsPermitted(cur->mode, cur->uid, cur->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerExec))
return nullptr;
VfsNode* child = findChild(cur, start, len);
if (!child)
return nullptr;
cur = child;
}
while (*p == '/')
p++;
}
return cur;
}
static VfsNode* splitParent(const char* path, char* leaf, uint64 leafCap)
{
uint64 len = strlen(path);
while (len > 1 && path[len - 1] == '/')
len--;
int64 slash = -1;
for (int64 i = (int64)len - 1; i >= 0; i--)
if (path[i] == '/') {
slash = i;
break;
}
const char* leafStart;
uint64 leafLen;
VfsNode* parent;
if (slash < 0) {
parent = root;
leafStart = path;
leafLen = len;
} else {
char parentBuf[256];
uint64 plen = (slash == 0) ? 1 : (uint64)slash;
if (plen > 255)
plen = 255;
memcpy(parentBuf, path, plen);
parentBuf[plen] = 0;
parent = lookupNode(parentBuf);
leafStart = path + slash + 1;
leafLen = len - (uint64)(slash + 1);
}
if (leafLen >= leafCap)
leafLen = leafCap - 1;
memcpy(leaf, leafStart, leafLen);
leaf[leafLen] = 0;
return parent;
}
static void ustarAdd(const char* rawPath, bool isDir, const uint8* data, uint64 size)
{
char path[256];
uint64 n = strlen(rawPath);
while (n > 0 && rawPath[n - 1] == '/')
n--;
if (n == 0)
return;
if (n > 255)
n = 255;
memcpy(path, rawPath, n);
path[n] = 0;
VfsNode* cur = root;
uint64 i = 0;
while (i < n) {
while (i < n && path[i] == '/')
i++;
uint64 start = i;
while (i < n && path[i] != '/')
i++;
uint64 len = i - start;
if (len == 0)
break;
bool last = true;
for (uint64 j = i; j < n; j++)
if (path[j] != '/') {
last = false;
break;
}
VfsNode* child = findChild(cur, path + start, len);
if (last) {
if (!child) {
if (isDir)
child = makeChildDir(cur, path + start, len, false, fsModeDefaultDir);
else {
/* Anything shipped under "bin/" (mktar's own stored-name
convention, no leading slash -- see tools/mktar.c) is a
real binary this kernel spawns by path (jobctlSpawn,
syscall.c's loadImageByPath): give it the owner-exec
bit by default, exactly as if someone had already run
`chmod 755` on it, since nothing in this kernel could
otherwise ever chmod a read-only ustar-loaded file
(vfsChmod refuses a non-writable node, same as every
other tmpfs mutation). Everything else defaults to
fsModeDefaultFile (no exec bit), matching a real
ordinary shipped data file. */
uint32 mode = (n > 4 && memcmp(path, "bin/", 4) == 0)
? fsModeDefaultExec
: fsModeDefaultFile;
child = makeChildFile(cur, path + start, len, data, size, false, mode);
}
}
} else if (!child) {
child = makeChildDir(cur, path + start, len, false, fsModeDefaultDir);
}
if (!child)
return;
cur = child;
}
}
void vfsInit(void)
{
root = nodeAlloc("/", 1, fsTypeDir, false, fsModeDefaultDir);
}
void vfsMountUstar(uint64 start, uint64 end)
{
initrdInit(start, end);
uint32 count = initrdCount();
uint32 files = 0;
for (uint32 i = 0; i < count; i++) {
UstarEntry entry;
if (!initrdAt(i, &entry))
break;
ustarAdd(entry.name, entry.isDir, entry.data, entry.size);
if (!entry.isDir)
files++;
}
kprintf("[vfs] mounted ustar at /, %u entries (%u files)\n", count, files);
}
void vfsMountTmpfs(const char* path)
{
char leaf[fsNameMax];
VfsNode* parent = splitParent(path, leaf, sizeof(leaf));
if (!parent || parent->type != fsTypeDir)
return;
if (findChild(parent, leaf, strlen(leaf)))
return;
makeChildDir(parent, leaf, strlen(leaf), true, fsModeDefaultDir);
kprintf("[vfs] mounted writable tmpfs at %s\n", path);
}
void vfsFdInit(FileDescriptor* fds)
{
for (uint32 i = 0; i < maxOpenFiles; i++) {
fds[i].node = nullptr;
fds[i].offset = 0;
fds[i].kind = fdFree;
fds[i].flags = 0;
fds[i].pipeHandle = -1;
}
fds[0].kind = fdConsole;
fds[1].kind = fdConsole;
fds[2].kind = fdConsole;
/* fd 0's own console reader queue (keyboard.h) -- reused field, same as
a pipe/fifo fd's handle into pipe.c's table. Every thread (kernel and
user alike) gets one, whether it ever reads stdin or not, matching
fd 0 already always being fdConsole for every thread regardless of
use. fdFlagBlocking makes syscall.c's sysRead retry-syscall path
treat it exactly like a blocking pipe end (vfsBlockingChannel below);
fd 1/2 (stdout/stderr) never read, so they get no reader and no
blocking flag. If the reader table is exhausted, fd 0 is left
non-blocking with an invalid handle -- reads on it then simply fail
(-1) rather than crash, the same fail-open behavior a fully exhausted
pipe/fifo table already has elsewhere in this file. */
fds[0].pipeHandle = keyboardOpenReader();
fds[0].flags = fdFlagBlocking;
}
static FileDescriptor* fdAt(int fd)
{
if (fd < 0 || fd >= (int)maxOpenFiles)
return nullptr;
FileDescriptor* d = &currentThread()->fds[fd];
if (d->kind == fdFree)
return nullptr;
return d;
}
static int allocFd(void)
{
FileDescriptor* fds = currentThread()->fds;
for (int i = 3; i < (int)maxOpenFiles; i++)
if (fds[i].kind == fdFree)
return i;
return -1;
}
static bool ensureCapacity(VfsNode* node, uint64 needed)
{
if (needed <= node->capacity)
return true;
uint64 newCap = (needed + (tmpfsBlock - 1)) & ~(uint64)(tmpfsBlock - 1);
uint8* fresh = (uint8*)kmalloc(newCap);
if (!fresh)
return false;
if (node->rwData) {
memcpy(fresh, node->rwData, node->size);
kfree(node->rwData);
}
node->rwData = fresh;
node->capacity = newCap;
return true;
}
static uint64 blockDevTotalBytes(void)
{
return ataSectorCount() * (uint64)ataSectorSize;
}
/* Reads/writes at most one sector's worth per iteration so a request that
straddles a sector boundary (or several) is handled as a sequence of
aligned sector transfers, each preceded by a read-modify-write step when
the access does not start or end on a sector boundary. */
static int64 blockDevRead(FileDescriptor* d, void* buffer, uint64 length)
{
uint64 total = blockDevTotalBytes();
if (d->offset >= total)
return 0;
uint64 want = length;
if (want > total - d->offset)
want = total - d->offset;
uint8* dst = (uint8*)buffer;
uint64 done = 0;
while (done < want) {
uint64 offset = d->offset + done;
uint64 lba = offset / ataSectorSize;
uint64 within = offset % ataSectorSize;
uint64 chunk = ataSectorSize - within;
if (chunk > want - done)
chunk = want - done;
uint8 sector[ataSectorSize];
if (!ataReadSectors(lba, 1, sector))
break;
memcpy(dst + done, sector + within, chunk);
done += chunk;
}
d->offset += done;
if (done > 0)
return (int64)done;
return want == 0 ? 0 : -1;
}
static int64 blockDevWrite(FileDescriptor* d, const void* buffer, uint64 length)
{
uint64 total = blockDevTotalBytes();
if (d->offset >= total)
return -1;
uint64 want = length;
if (want > total - d->offset)
want = total - d->offset;
const uint8* src = (const uint8*)buffer;
uint64 done = 0;
while (done < want) {
uint64 offset = d->offset + done;
uint64 lba = offset / ataSectorSize;
uint64 within = offset % ataSectorSize;
uint64 chunk = ataSectorSize - within;
if (chunk > want - done)
chunk = want - done;
uint8 sector[ataSectorSize];
if (chunk == ataSectorSize) {
memcpy(sector, src + done, ataSectorSize);
} else if (!ataReadSectors(lba, 1, sector)) {
break;
} else {
memcpy(sector + within, src + done, chunk);
}
if (!ataWriteSectors(lba, 1, sector))
break;
done += chunk;
}
d->offset += done;
if (done > 0)
return (int64)done;
return want == 0 ? 0 : -1;
}
/* True for "/mnt" itself and for anything under "/mnt/". relOut receives the
path with the prefix stripped: "" (or "/foo") for sfsLookup, which treats
an empty relative path as the sfs root. */
static bool isSfsPath(const char* path, const char** relOut)
{
if (strncmp(path, sfsMountPrefix, sfsMountPrefixLen) != 0)
return false;
char after = path[sfsMountPrefixLen];
if (after != '\0' && after != '/')
return false;
*relOut = path + sfsMountPrefixLen;
return true;
}
/* See vfs.h's own doc comment for the full contract -- this is the single
place shell.c's old per-builtin static cwd's logic now lives, shared by
every path-taking entry point below instead of only shell.c's own
handful of commands. */
void vfsResolvePath(const char* path, char* out, uint64 cap)
{
if (path[0] == '/') {
uint64 len = strlen(path);
if (len >= cap)
len = cap - 1;
memcpy(out, path, len);
out[len] = 0;
return;
}
const char* cwd = currentThreadCwd();
uint64 pos = strlen(cwd);
if (pos >= cap)
pos = cap - 1;
memcpy(out, cwd, pos);
if (pos == 0 || out[pos - 1] != '/') {
if (pos < cap - 1)
out[pos++] = '/';
}
uint64 i = 0;
while (path[i] && pos < cap - 1)
out[pos++] = path[i++];
out[pos] = 0;
}
static uint32 sfsTypeToFsType(uint32 sfsType)
{
if (sfsType == sfsTypeDir)
return fsTypeDir;
if (sfsType == sfsTypeFifo)
return fsTypeFifo;
return fsTypeFile;
}
/* Persistent (sfs.h) named-pipe bindings for "/mnt" fifos: sfs.c stores only
that a path IS a fifo (the inode's type), never a live byte stream -- the
actual pipe.h handle carrying bytes between two independent openers of
the same persistent path is purely in-memory here, exactly like the
VfsNode-resident one a tmpfs fifo carries on its own node forever (see
makeChildFifo above), except an SFS inode number has nowhere on-disk to
remember a handle. This small fixed table maps inode number -> live
handle, created lazily on the FIRST open() of either direction (or the
first open() since every previous session's ends have both gone away,
see sfsFifoForget) and reused by every later open() of the same inode
until then.
Slot 0 is never a real fifo inode (allocInodeNum, sfs.c, starts at 1;
inode 0 is permanently marked used by sfsFormat as a sentinel), so a
zero-initialized table already starts every slot correctly "free" --
inodeNum 0 doubles as "empty slot" the same way pipeHandle -1 already
means "no handle" everywhere else in this file.
Two INDEPENDENT staleness checks guard against two INDEPENDENT ways a
remembered (inodeNum, handle) pair can stop meaning what it used to:
- sfsGeneration() (sfs.h) catches "the whole filesystem underneath was
wiped since" -- every sfsTest.c test starts with its own sfsFormat()
call, which reuses inode numbers for completely unrelated entries
dozens of times a boot; without this check, a binding left over from
a wiped filesystem could silently hand a brand-new, unrelated
inode's opener the OLD fifo's (possibly still-leaked) handle.
- pipeIncarnation(handle) (pipe.h) catches "pipe.c's own table recycled
this NUMBER for an unrelated pipe since" -- an entirely ordinary
event that needs no filesystem format at all: the moment BOTH ends
of a completed fifo round-trip close, pipeReleaseIfUnused frees the
slot back to the general pool, and the very next unrelated
vfsPipe()/vfsMkfifo() anywhere in the kernel is free to claim that
exact number for something else entirely. pipeValid(handle) alone
cannot tell "still my pipe, nobody has closed it" apart from
"already reused by a stranger" -- only the incarnation counter can.
Either mismatch is treated exactly like "never bound": the stale slot is
evicted -- WITHOUT touching pipe.c, since whatever handle number it
remembers may already mean something else by now -- and a fresh handle
is created as if this were the first open ever. */
#define maxSfsFifoBindings 8
typedef struct SfsFifoBinding {
int32 inodeNum;
int32 pipeHandle;
uint32 incarnation;
uint64 generation;
} SfsFifoBinding;
static SfsFifoBinding sfsFifoBindings[maxSfsFifoBindings];
/* Finds inodeNum's live binding, discarding it first if it is stale by
either check above. Returns nullptr if there is no valid binding (never
bound, or just evicted as stale). */
static SfsFifoBinding* sfsFifoFind(int32 inodeNum)
{
uint64 gen = sfsGeneration();
for (uint32 i = 0; i < maxSfsFifoBindings; i++) {
SfsFifoBinding* b = &sfsFifoBindings[i];
if (b->inodeNum != inodeNum)
continue;
if (b->generation != gen || pipeIncarnation(b->pipeHandle) != b->incarnation) {
b->inodeNum = 0;
return nullptr;
}
return b;
}
return nullptr;
}
/* Returns inodeNum's live pipe handle, creating a fresh pipeCreateDetached
one (and a new table slot) the first time this inode is opened since the
last time nobody had it open -- exactly the "nobody attached yet, keep
retrying rather than report broken/EOF" contract pipeCreateDetached
itself already documents. -1 if pipe.c's own table is exhausted, or this
inode has no existing binding and the (much smaller) binding table above
is itself full. */
static int32 sfsFifoHandleFor(int32 inodeNum)
{
SfsFifoBinding* existing = sfsFifoFind(inodeNum);
if (existing)
return existing->pipeHandle;
int handle = pipeCreateDetached();
if (handle < 0)
return -1;
for (uint32 i = 0; i < maxSfsFifoBindings; i++) {
if (sfsFifoBindings[i].inodeNum == 0) {
sfsFifoBindings[i].inodeNum = inodeNum;
sfsFifoBindings[i].pipeHandle = handle;
sfsFifoBindings[i].incarnation = pipeIncarnation(handle);
sfsFifoBindings[i].generation = sfsGeneration();
return handle;
}
}
pipeDestroyIfUnattached(handle);
return -1;
}
/* Called when inodeNum's own directory entry is actually gone for good
(sfsRemove reached the last link, see vfsUnlink) -- evicts the table slot
FIRST, then attempts pipeDestroyIfUnattached on the handle it held:
unlinking a fifo that is currently open must leave the still-open ends
alone (they keep working off their own copy of the handle number,
matching the tmpfs fifo's own vfsUnlink comment below), so the destroy is
a no-op unless nobody ever attached to it. Evicting before destroying
(rather than after) closes the narrow window where a concurrent opener
of the SAME inode number could otherwise see the stale slot as still
valid and attach to a handle number pipe.c is about to hand out to
something completely unrelated. */
static void sfsFifoForget(int32 inodeNum)
{
for (uint32 i = 0; i < maxSfsFifoBindings; i++) {
if (sfsFifoBindings[i].inodeNum == inodeNum) {
int handle = sfsFifoBindings[i].pipeHandle;
sfsFifoBindings[i].inodeNum = 0;
pipeDestroyIfUnattached(handle);
return;
}
}
}
static int64 sfsFdRead(FileDescriptor* d, void* buffer, uint64 length)
{
int64 got = sfsReadAt(d->pipeHandle, d->offset, buffer, length);
if (got > 0)
d->offset += (uint64)got;
return got;
}
static int64 sfsFdWrite(FileDescriptor* d, const void* buffer, uint64 length)
{
int64 written = sfsWriteAt(d->pipeHandle, d->offset, buffer, length);
if (written > 0)
d->offset += (uint64)written;
return written;
}
int vfsMkfifo(const char* path)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
/* Persistent named pipe (sfs.h's sfsTypeFifo): only the directory entry
and its type are created here -- the live pipe.h handle two openers
actually rendezvous over is created lazily by vfsOpen the first time
anyone opens it (sfsFifoHandleFor above), the same "create on first
real use" split every other SFS-backed thing keeps between "what is
on disk" (sfs.c, no pipe.h dependency at all) and "what is live in
this boot's memory" (here). */
if (isSfsPath(path, &rel))
return sfsCreateFifo(rel) >= 0 ? 0 : -1;
if (strcmp(path, blockDevPath) == 0)
return -1;
if (lookupNode(path))
return -1;
char leaf[fsNameMax];
VfsNode* parent = splitParent(path, leaf, sizeof(leaf));
if (!parent || parent->type != fsTypeDir || !parent->writable
|| !vfsPermitted(parent->mode, parent->uid, parent->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
int handle = pipeCreateDetached();
if (handle < 0)
return -1;
if (!makeChildFifo(parent, leaf, strlen(leaf), handle)) {
pipeDestroyIfUnattached(handle);
return -1;
}
return 0;
}
/* Shared core of vfsOpen/vfsOpenInto below: resolves `path`+`flags` and, on
success, fills EVERY field of `d` (whichever FileDescriptor the caller
decided on -- a freshly allocated slot in the calling thread's own table
for vfsOpen, or an explicit slot in some other, not-yet-running thread's
table for vfsOpenInto) and returns 0. On failure, returns -1 having
written NOTHING to `d` at all -- every early return below is before the
first store into `d`, so a caller never has to clean up a half-written
descriptor. */
static int resolveOpen(FileDescriptor* d, const char* path, uint32 flags)
{
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0) {
if (!(flags & oCreate))
return -1;
inodeNum = sfsCreateFile(rel);
if (inodeNum < 0)
return -1;
} else if (sfsTypeOf(inodeNum) == sfsTypeFifo) {
/* One-directional only, exactly like the tmpfs fifo branch
below and vfsPipeAttach's wantWrite bool -- a real O_RDWR
fifo fd has no equivalent here, since a single fd can only be
fdPipeRead or fdPipeWrite, never both. */
uint32 mode = flags & (oWriteOnly | oReadWrite);
bool wantWrite;
if (mode == oWriteOnly)
wantWrite = true;
else if (mode == oReadOnly)
wantWrite = false;
else
return -1;
int handle = sfsFifoHandleFor(inodeNum);
if (handle < 0)
return -1;
d->node = nullptr;
d->offset = 0;
d->kind = wantWrite ? fdPipeWrite : fdPipeRead;
d->flags = fdFlagBlocking;
d->pipeHandle = handle;
if (wantWrite)
pipeAddWriter(handle);
else
pipeAddReader(handle);
return 0;
} else {
/* Real EACCES-shaped refusal: opening an EXISTING file for
writing without the right write bit for the CALLING thread's
identity (vfsPermitted, owner-vs-other-vs-root) fails at
open() time, before any fd (and therefore any write()) ever
exists -- matches real Unix, where permission is checked
once, at open, never re-checked against an fd already held.
A freshly oCreate'd file above always starts with
fsModeDefaultFile owned by its creator (has the owner-write
bit for exactly that uid), so this can never reject the
create-a-new-file path. */
if ((flags & (oWriteOnly | oReadWrite)) && sfsTypeOf(inodeNum) == sfsTypeFile
&& !vfsPermitted(sfsModeOf(inodeNum), sfsUidOf(inodeNum), sfsGidOf(inodeNum),
currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
/* Real EACCES-shaped refusal, the read-side counterpart of the
write check immediately above -- previously MISSING entirely
(a genuine, previously-undiscovered gap this fala's own
grouptest.c live demo found: a caller matching neither owner
nor group could still read a file with no read bit for
either, since nothing here ever consulted fsModeOwnerRead/
fsModeGroupRead/fsModeOtherRead for a plain file open, only
for a directory's own opendir()-shaped check below). Fires
for oReadOnly AND oReadWrite (real Unix's O_RDWR needs read
permission too, not just write) -- `!(flags & oWriteOnly)` is
the same 2-bit-access-mode test the fifo branch above already
uses to distinguish all three. */
if (!(flags & oWriteOnly) && sfsTypeOf(inodeNum) == sfsTypeFile
&& !vfsPermitted(sfsModeOf(inodeNum), sfsUidOf(inodeNum), sfsGidOf(inodeNum),
currentThreadUid(), currentThreadGid(), fsModeOwnerRead))
return -1;
/* Real opendir()-shaped refusal: listing a directory's own
entries (vfsGetdents below, on whatever fd this open()
eventually returns) needs read permission on the directory
ITSELF -- a separate bit from the exec/search permission
lookupNode/lookupFrom already require on every ANCESTOR
directory just to resolve this path down to here. */
if (sfsTypeOf(inodeNum) == sfsTypeDir
&& !vfsPermitted(sfsModeOf(inodeNum), sfsUidOf(inodeNum), sfsGidOf(inodeNum),
currentThreadUid(), currentThreadGid(), fsModeOwnerRead))
return -1;
if ((flags & oTrunc) && sfsTypeOf(inodeNum) == sfsTypeFile)
sfsTruncate(inodeNum, 0);
}
d->kind = fdSfs;
d->node = nullptr;
d->flags = flags;
d->pipeHandle = inodeNum;
d->offset = (flags & oAppend) ? sfsSizeOf(inodeNum) : 0;
return 0;
}
if (strcmp(path, blockDevPath) == 0) {
d->kind = fdBlockDev;
d->node = nullptr;
d->flags = flags;
d->offset = 0;
return 0;
}
VfsNode* node = lookupNode(path);
if (!node) {
if (!(flags & oCreate))
return -1;
char leaf[fsNameMax];
VfsNode* parent = splitParent(path, leaf, sizeof(leaf));
if (!parent || parent->type != fsTypeDir || !parent->writable
|| !vfsPermitted(parent->mode, parent->uid, parent->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
node = makeChildFile(parent, leaf, strlen(leaf), nullptr, 0, true, fsModeDefaultFile);
if (!node)
return -1;
} else if (node->type == fsTypeFifo) {
/* One-directional only, exactly like vfsPipeAttach's wantWrite bool
-- a real O_RDWR fifo fd (Linux allows it; POSIX leaves it
undefined) has no equivalent here, since a single fd can only be
fdPipeRead or fdPipeWrite, never both. */
uint32 mode = flags & (oWriteOnly | oReadWrite);
bool wantWrite;
if (mode == oWriteOnly)
wantWrite = true;
else if (mode == oReadOnly)
wantWrite = false;
else
return -1;
d->node = nullptr;
d->offset = 0;
d->kind = wantWrite ? fdPipeWrite : fdPipeRead;
d->flags = fdFlagBlocking;
d->pipeHandle = node->pipeHandle;
if (wantWrite)
pipeAddWriter(node->pipeHandle);
else
pipeAddReader(node->pipeHandle);
return 0;
} else {
/* Same real EACCES-shaped refusal as the SFS branch above, applied
to tmpfs -- see that branch's comment for the full reasoning. */
if ((flags & (oWriteOnly | oReadWrite)) && node->type == fsTypeFile
&& !vfsPermitted(node->mode, node->uid, node->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
/* Same real file-read check as the SFS branch above (its own
comment has the full "previously missing entirely" reasoning). */
if (!(flags & oWriteOnly) && node->type == fsTypeFile
&& !vfsPermitted(node->mode, node->uid, node->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerRead))
return -1;
/* Same opendir()-shaped read check as the SFS branch above. */
if (node->type == fsTypeDir
&& !vfsPermitted(node->mode, node->uid, node->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerRead))
return -1;
if ((flags & oTrunc) && node->type == fsTypeFile && node->writable)
node->size = 0;
}
d->kind = fdFile;
d->node = node;
d->flags = flags;
d->offset = (flags & oAppend) ? node->size : 0;
return 0;
}
int vfsOpen(const char* path, uint32 flags)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
int fd = allocFd();
if (fd < 0)
return -1;
if (resolveOpen(&currentThread()->fds[fd], resolvedPath, flags) < 0)
return -1;
return fd;
}
int vfsOpenInto(FileDescriptor* targetFds, int fd, const char* path, uint32 flags)
{
/* Resolved against the CALLING thread's own cwd (the spawning parent,
e.g. jobctl.c's redirect setup running before its not-yet-run child
ever executes) -- exactly the cwd `targetFds`'s own owner already
inherited a copy of at spawn time (initialCwd, sched.c), since
nothing chdirs in between a spawn and its immediate redirect setup. */
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
return resolveOpen(&targetFds[fd], resolvedPath, flags);
}
int vfsClose(int fd)
{
FileDescriptor* d = fdAt(fd);
if (!d)
return -1;
if (d->kind == fdConsole)
return 0;
if (d->kind == fdPipeRead)
pipeCloseRead(d->pipeHandle);
else if (d->kind == fdPipeWrite)
pipeCloseWrite(d->pipeHandle);
else if (d->kind == fdPidfd)
pidfdClose(d->pipeHandle);
d->kind = fdFree;
d->node = nullptr;
d->offset = 0;
d->flags = 0;
d->pipeHandle = -1;
return 0;
}
int vfsPipe(int* readFd, int* writeFd)
{
int rfd = allocFd();
if (rfd < 0)
return -1;
currentThread()->fds[rfd].kind = fdPipeRead;
int wfd = allocFd();
if (wfd < 0) {
currentThread()->fds[rfd].kind = fdFree;
return -1;
}
currentThread()->fds[wfd].kind = fdPipeWrite;
int handle = pipeCreate();
if (handle < 0) {
currentThread()->fds[rfd].kind = fdFree;
currentThread()->fds[wfd].kind = fdFree;
return -1;
}
currentThread()->fds[rfd].pipeHandle = handle;
currentThread()->fds[wfd].pipeHandle = handle;
*readFd = rfd;
*writeFd = wfd;
return 0;
}
int vfsPipeCreateHandle(void)
{
return pipeCreateDetached();
}
int vfsPipeAttach(int handle, bool wantWrite)
{
if (!pipeValid(handle))
return -1;
int fd = allocFd();
if (fd < 0)
return -1;
FileDescriptor* d = &currentThread()->fds[fd];
d->node = nullptr;
d->offset = 0;
d->kind = wantWrite ? fdPipeWrite : fdPipeRead;
d->flags = fdFlagBlocking;
d->pipeHandle = handle;
if (wantWrite)
pipeAddWriter(handle);
else
pipeAddReader(handle);
return fd;
}
void vfsPipeAttachInto(FileDescriptor* targetFds, int fd, int handle, bool wantWrite)
{
if (!pipeValid(handle))
return;
FileDescriptor* d = &targetFds[fd];
d->node = nullptr;
d->offset = 0;
d->kind = wantWrite ? fdPipeWrite : fdPipeRead;
d->flags = fdFlagBlocking;
d->pipeHandle = handle;
if (wantWrite)
pipeAddWriter(handle);
else
pipeAddReader(handle);
}
int vfsPidfdOpen(uint32 targetId)
{
int handle = pidfdOpen(targetId);
if (handle < 0)
return -1;
int fd = allocFd();
if (fd < 0) {
pidfdClose(handle);
return -1;
}
FileDescriptor* d = &currentThread()->fds[fd];
d->node = nullptr;
d->offset = 0;
d->kind = fdPidfd;
d->flags = 0;
d->pipeHandle = handle;
return fd;
}
uint32 vfsPidfdTarget(int fd)
{
FileDescriptor* d = fdAt(fd);
if (!d || d->kind != fdPidfd)
return threadInvalidId;
return pidfdTarget(d->pipeHandle);
}
const void* vfsBlockingChannel(int fd, bool forWrite)
{
FileDescriptor* d = fdAt(fd);
if (!d || !(d->flags & fdFlagBlocking))
return nullptr;
if (d->kind == fdPipeRead || d->kind == fdPipeWrite) {
if (forWrite) {
if (d->kind != fdPipeWrite || pipeWriteBroken(d->pipeHandle))
return nullptr;
} else if (d->kind != fdPipeRead) {
return nullptr;
}
return pipeChannel(d->pipeHandle);
}
if (d->kind == fdConsole) {
/* Console writes (kputchar) are never buffered and never block. */
if (forWrite)
return nullptr;
return keyboardReaderChannel(d->pipeHandle);
}
return nullptr;
}
bool vfsPollReady(int fd, int16 events, int16* outRevents)
{
int16 revents = 0;
FileDescriptor* d = fdAt(fd);
if (!d) {
*outRevents = pollNval;
return true;
}
switch (d->kind) {
case fdPipeRead:
if ((events & pollIn) && pipeHasData(d->pipeHandle))
revents |= pollIn;
break;
case fdPipeWrite:
if (pipeWriteBroken(d->pipeHandle))
revents |= pollErr;
else if ((events & pollOut) && pipeHasSpace(d->pipeHandle))
revents |= pollOut;
break;
case fdConsole:
if ((events & pollIn) && keyboardReaderHasData(d->pipeHandle))
revents |= pollIn;
/* Console writes (kputchar) are never buffered and never block --
see vfsBlockingChannel above. */
if (events & pollOut)
revents |= pollOut;
break;
default:
/* fdFile, fdBlockDev, fdSfs: every read/write on these runs to
completion synchronously (see vfsRead/vfsWrite below), exactly
like a real local filesystem always reports poll-ready. */
revents |= (int16)(events & (pollIn | pollOut));
break;
}
*outRevents = revents;
return revents != 0;
}
int64 vfsWrite(int fd, const void* buffer, uint64 length)
{
FileDescriptor* d = fdAt(fd);
if (!d)
return -1;
if (d->kind == fdConsole) {
const char* text = (const char*)buffer;
for (uint64 i = 0; i < length; i++)
kputchar(text[i]);
return (int64)length;
}
if (d->kind == fdPipeWrite)
return pipeWrite(d->pipeHandle, buffer, length);
if (d->kind == fdPipeRead)
return -1;
if (d->kind == fdBlockDev)
return blockDevWrite(d, buffer, length);
if (d->kind == fdSfs)
return sfsFdWrite(d, buffer, length);
VfsNode* node = d->node;
if (node->type != fsTypeFile || !node->writable)
return -1;
if (!ensureCapacity(node, d->offset + length))
return -1;
memcpy(node->rwData + d->offset, buffer, length);
if (d->offset + length > node->size)
node->size = d->offset + length;
d->offset += length;
return (int64)length;
}
int64 vfsRead(int fd, void* buffer, uint64 length)
{
FileDescriptor* d = fdAt(fd);
if (!d)
return -1;
if (d->kind == fdPipeRead)
return pipeRead(d->pipeHandle, buffer, length);
if (d->kind == fdPipeWrite)
return -1;
if (d->kind == fdBlockDev)
return blockDevRead(d, buffer, length);
if (d->kind == fdSfs)
return sfsFdRead(d, buffer, length);
if (d->kind == fdConsole) {
/* Non-blocking, like every other real fd kind above: -1 (queue
currently empty) is the "would block" signal syscall.c's sysRead
retries via vfsBlockingChannel's channel + the restart-syscall
trick, instead of this function looping/halting in place the way
the old single-global-buffer keyboardReadChar() used to. Returns
whatever is queued right now (possibly fewer than length bytes,
same partial-read contract pipeRead already has) rather than
insisting on a full line -- nothing in this kernel relied on the
old line-buffering behavior (grep shows no caller ever read fd 0
before this wave). */
return keyboardReaderRead(d->pipeHandle, buffer, length);
}
VfsNode* node = d->node;
if (node->type != fsTypeFile)
return -1;
const uint8* src = node->roData ? node->roData : node->rwData;
if (d->offset >= node->size || !src)
return 0;
uint64 available = node->size - d->offset;
uint64 take = length < available ? length : available;
memcpy(buffer, src + d->offset, take);
d->offset += take;
return (int64)take;
}
int64 vfsLseek(int fd, int64 offset, int whence)
{
FileDescriptor* d = fdAt(fd);
if (!d || (d->kind != fdFile && d->kind != fdBlockDev && d->kind != fdSfs))
return -1;
int64 base;
if (whence == seekSet)
base = 0;
else if (whence == seekCur)
base = (int64)d->offset;
else if (whence == seekEnd) {
if (d->kind == fdBlockDev)
base = (int64)blockDevTotalBytes();
else if (d->kind == fdSfs)
base = (int64)sfsSizeOf(d->pipeHandle);
else
base = (int64)d->node->size;
} else
return -1;
int64 target = base + offset;
if (target < 0)
return -1;
d->offset = (uint64)target;
return target;
}
int vfsStat(const char* path, FbStat* out)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0)
return -1;
out->size = sfsSizeOf(inodeNum);
out->type = sfsTypeToFsType(sfsTypeOf(inodeNum));
out->mode = sfsModeOf(inodeNum);
out->uid = sfsUidOf(inodeNum);
out->gid = sfsGidOf(inodeNum);
out->mtime = sfsMtimeOf(inodeNum);
out->ctime = sfsCtimeOf(inodeNum);
return 0;
}
if (strcmp(path, blockDevPath) == 0) {
out->size = blockDevTotalBytes();
out->type = fsTypeFile;
out->mode = 0666;
out->uid = 0;
out->gid = 0;
out->mtime = 0;
out->ctime = 0;
return 0;
}
VfsNode* node = lookupNode(path);
if (!node)
return -1;
out->size = node->size;
out->type = node->type;
out->mode = node->mode;
out->uid = node->uid;
out->gid = node->gid;
out->mtime = 0;
out->ctime = 0;
return 0;
}
int64 vfsGetdents(int fd, void* buffer, uint64 length)
{
FileDescriptor* d = fdAt(fd);
if (!d || (d->kind != fdFile && d->kind != fdSfs))
return -1;
if (d->kind == fdSfs) {
if (sfsTypeOf(d->pipeHandle) != sfsTypeDir)
return -1;
FbDirent* out = (FbDirent*)buffer;
uint64 written = 0;
char name[fsNameMax];
uint32 type;
while (written + sizeof(FbDirent) <= length &&
sfsReaddirAt(d->pipeHandle, (uint32)d->offset, name, sizeof(name), &type)) {
uint64 nameLen = strlen(name);
out->type = sfsTypeToFsType(type);
out->nameLength = (unsigned int)nameLen;
memcpy(out->name, name, nameLen + 1);
out++;
written += sizeof(FbDirent);
d->offset++;
}
return (int64)written;
}
VfsNode* node = d->node;
if (node->type != fsTypeDir)
return -1;
FbDirent* out = (FbDirent*)buffer;
uint64 written = 0;
VfsNode* child = node->firstChild;
for (uint64 skip = d->offset; child && skip > 0; skip--)
child = child->nextSibling;
while (child && written + sizeof(FbDirent) <= length) {
uint64 nameLen = strlen(child->name);
if (nameLen >= fsNameMax)
nameLen = fsNameMax - 1;
out->type = child->type;
out->nameLength = (unsigned int)nameLen;
memcpy(out->name, child->name, nameLen);
out->name[nameLen] = 0;
out++;
written += sizeof(FbDirent);
d->offset++;
child = child->nextSibling;
}
return (int64)written;
}
int vfsMkdir(const char* path)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel))
return sfsCreateDir(rel) >= 0 ? 0 : -1;
if (lookupNode(path))
return -1;
char leaf[fsNameMax];
VfsNode* parent = splitParent(path, leaf, sizeof(leaf));
if (!parent || parent->type != fsTypeDir || !parent->writable
|| !vfsPermitted(parent->mode, parent->uid, parent->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
return makeChildDir(parent, leaf, strlen(leaf), true, fsModeDefaultDir) ? 0 : -1;
}
int vfsUnlink(const char* path)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
/* Real unlink(2) semantics: EISDIR on a directory, even an empty
one -- rmdir(2) (vfsRmdir below) is the only sanctioned way to
remove one. sfsRemove itself has no opinion here (it is the raw
on-disk primitive, also used by vfsRmdir below), so the type
check has to happen at this dispatch layer, matching the tmpfs
branch below which has always refused a directory this way. */
if (inodeNum < 0 || sfsTypeOf(inodeNum) == sfsTypeDir)
return -1;
/* Only the LAST directory entry pointing at a persistent fifo's
inode should ever tear down its live pipe.h binding (see
sfsFifoForget above) -- a hardlinked fifo (sfsLink does not
special-case sfsTypeFifo, unlike sfsTypeDir) losing one of
several names must leave any live session alone, exactly like
sfsRemove itself only actually frees the inode once linkCount
drops to 0. Checked and captured BEFORE sfsRemove, since a
successful removal may itself free the inode (readInode on it
afterwards would read whatever unrelated data now occupies that
slot, if anything). */
bool lastLinkOfFifo = sfsTypeOf(inodeNum) == sfsTypeFifo &&
sfsLinkCountOf(inodeNum) <= 1;
if (!sfsRemove(rel))
return -1;
if (lastLinkOfFifo)
sfsFifoForget(inodeNum);
return 0;
}
VfsNode* node = lookupNode(path);
if (!node || !node->writable || (node->type != fsTypeFile && node->type != fsTypeFifo))
return -1;
/* Real unlink(2) semantics: removing a directory ENTRY needs write
permission on the PARENT directory, not on the entry itself -- a
real Unix caller can delete a read-only file it can write into the
containing directory of, matching real Unix's own "the file's own
mode is irrelevant to unlink" rule. */
VfsNode* parent = node->parent;
if (!vfsPermitted(parent->mode, parent->uid, parent->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
VfsNode** link = &parent->firstChild;
while (*link && *link != node)
link = &(*link)->nextSibling;
if (*link)
*link = node->nextSibling;
/* Any fd already opened on this fifo (fdPipeRead/fdPipeWrite) keeps
working off its own copy of node->pipeHandle -- it never dereferences
this node again, so freeing it here does not disturb a live reader or
writer, matching real unlink-while-open semantics. Only reclaim the
underlying pipe slot if nobody ever actually opened it; otherwise the
still-open ends must keep it alive until their own vfsClose. */
if (node->type == fsTypeFifo)
pipeDestroyIfUnattached(node->pipeHandle);
if (node->rwData)
kfree(node->rwData);
kfree(node);
return 0;
}
/* Real rmdir(2): the counterpart vfsUnlink above deliberately refuses. Only
ever removes a directory, and only an EMPTY one (ENOTDIR/ENOTEMPTY in
real errno terms, both collapsed to -1 here like every other vfs.c entry
point). root itself can never be removed -- root is created non-writable
(nodeAlloc("/", ...) in vfsInit), so the same `!node->writable` check
every other mutating entry point already relies on covers it for free,
no separate identity check needed. */
int vfsRmdir(const char* path)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0 || sfsTypeOf(inodeNum) != sfsTypeDir)
return -1;
/* sfsRemove's own `inode.type == sfsTypeDir && inode.size > 0`
check (sfs.c) still refuses a non-empty directory -- kept as
defense in depth rather than duplicated here. */
return sfsRemove(rel) ? 0 : -1;
}
VfsNode* node = lookupNode(path);
if (!node || !node->writable || node->type != fsTypeDir || node->firstChild)
return -1;
/* Same parent-directory write check as vfsUnlink above -- removing a
directory ENTRY needs write permission on the PARENT, not the
directory being removed itself. */
VfsNode* parent = node->parent;
if (!vfsPermitted(parent->mode, parent->uid, parent->gid, currentThreadUid(), currentThreadGid(), fsModeOwnerWrite))
return -1;
VfsNode** link = &parent->firstChild;
while (*link && *link != node)
link = &(*link)->nextSibling;
if (*link)
*link = node->nextSibling;
kfree(node);
return 0;
}
bool vfsOwnerOrRoot(uint32 ownerUid, uint32 callerUid)
{
return callerUid == 0 || callerUid == ownerUid;
}
int vfsChmod(const char* path, uint32 mode)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0)
return -1;
if (!vfsOwnerOrRoot(sfsUidOf(inodeNum), currentThreadUid()))
return -1;
return sfsChmod(inodeNum, mode) ? 0 : -1;
}
VfsNode* node = lookupNode(path);
if (!node || !node->writable)
return -1;
if (!vfsOwnerOrRoot(node->uid, currentThreadUid()))
return -1;
node->mode = mode;
return 0;
}
int vfsChown(const char* path, uint32 newUid)
{
if (currentThreadUid() != 0)
return -1;
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0)
return -1;
return sfsChown(inodeNum, newUid) ? 0 : -1;
}
VfsNode* node = lookupNode(path);
if (!node || !node->writable)
return -1;
node->uid = newUid;
return 0;
}
int vfsChgrp(const char* path, uint32 newGid)
{
uint32 callerUid = currentThreadUid();
bool isRoot = callerUid == 0;
/* Non-root: only the CALLER's own current gid may ever be assigned
(this kernel's one-gid-per-thread "membership"), and only by the
file's own owner -- both conditions checked up front so neither
branch below needs to repeat them. */
if (!isRoot && newGid != currentThreadGid())
return -1;
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (isSfsPath(path, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0)
return -1;
if (!isRoot && sfsUidOf(inodeNum) != callerUid)
return -1;
return sfsChgrp(inodeNum, newGid) ? 0 : -1;
}
VfsNode* node = lookupNode(path);
if (!node || !node->writable)
return -1;
if (!isRoot && node->uid != callerUid)
return -1;
node->gid = newGid;
return 0;
}
int vfsSymlink(const char* path, const char* target)
{
/* Only `path` (where the symlink itself is created) is resolved against
the calling thread's cwd -- `target` (the string stored INSIDE it) is
kept exactly as given, never resolved, matching real symlink(2)
(a relative target is only ever interpreted later, at traversal
time, relative to the symlink's own directory -- not the creating
process's cwd at creation time). */
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
const char* rel;
if (!isSfsPath(resolvedPath, &rel))
return -1;
return sfsCreateSymlink(rel, target) >= 0 ? 0 : -1;
}
int64 vfsReadlink(const char* path, char* buffer, uint64 cap)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
path = resolvedPath;
const char* rel;
if (!isSfsPath(path, &rel))
return -1;
int32 inodeNum = sfsLookupNoFollow(rel);
if (inodeNum < 0 || sfsTypeOf(inodeNum) != sfsTypeSymlink)
return -1;
if (!sfsReadlink(inodeNum, buffer, cap))
return -1;
return (int64)strlen(buffer);
}
int vfsLink(const char* existingPath, const char* newPath)
{
char resolvedExisting[fsPathMax];
char resolvedNew[fsPathMax];
vfsResolvePath(existingPath, resolvedExisting, sizeof(resolvedExisting));
vfsResolvePath(newPath, resolvedNew, sizeof(resolvedNew));
existingPath = resolvedExisting;
newPath = resolvedNew;
const char* existingRel;
const char* newRel;
/* Cross-filesystem hardlinks (one path under /mnt, the other not) are
not supported -- same scope limit as a real link(2) across different
mounted filesystems (EXDEV), just expressed as a flat rejection since
this kernel has exactly one linkable filesystem. */
if (!isSfsPath(existingPath, &existingRel) || !isSfsPath(newPath, &newRel))
return -1;
return sfsLink(existingRel, newRel) >= 0 ? 0 : -1;
}
int vfsChdir(const char* path)
{
char resolvedPath[fsPathMax];
vfsResolvePath(path, resolvedPath, sizeof(resolvedPath));
const char* rel;
if (isSfsPath(resolvedPath, &rel)) {
int32 inodeNum = sfsLookup(rel);
if (inodeNum < 0 || sfsTypeOf(inodeNum) != sfsTypeDir)
return -1;
/* No parent-walk primitive exists for sfs.h inodes (unlike vfsPath
below for the VfsNode tree), so the /mnt branch stores exactly
what vfsResolvePath produced -- the one honestly-scoped gap
vfsChdir's own doc comment (vfs.h) calls out. */
return currentThreadSetCwd(resolvedPath) ? 0 : -1;
}
VfsNode* node = lookupNode(resolvedPath);
if (!node || node->type != fsTypeDir)
return -1;
/* Canonicalizes away any "."/".." components the caller typed by
walking the resolved node's REAL parent chain (vfsPath), rather than
storing vfsResolvePath's own plain string join verbatim -- shell.c's
old changeDir already did exactly this before this subsystem
existed, so this keeps that behavior for the VfsNode-tree case. */
char canonical[fsPathMax];
vfsPath(node, canonical, sizeof(canonical));
return currentThreadSetCwd(canonical) ? 0 : -1;
}
int64 vfsGetcwd(char* out, uint64 cap)
{
const char* cwd = currentThreadCwd();
uint64 len = strlen(cwd);
if (len >= cap)
return -1;
memcpy(out, cwd, len + 1);
return (int64)len;
}
VfsNode* vfsLookup(const char* path)
{
return lookupNode(path);
}
VfsNode* vfsChild(VfsNode* dir)
{
return dir ? dir->firstChild : nullptr;
}
VfsNode* vfsNext(VfsNode* node)
{
return node ? node->nextSibling : nullptr;
}
const char* vfsName(VfsNode* node)
{
return node->name;
}
bool vfsIsDir(VfsNode* node)
{
return node->type == fsTypeDir;
}
uint64 vfsSize(VfsNode* node)
{
return node->size;
}
uint32 vfsMode(VfsNode* node)
{
return node->mode;
}
uint32 vfsUid(VfsNode* node)
{
return node->uid;
}
uint32 vfsGid(VfsNode* node)
{
return node->gid;
}
/* Real Unix owner/other permission algorithm, the one real choke-point
piece of logic resolveOpen's two branches (tmpfs, SFS) and syscall.c's
loadImageByPath/jobctl.c's jobctlSpawn all now share instead of each
re-deriving it -- previously all four sites just tested a single bit
unconditionally (`mode & fsModeOwnerWrite`), honest only because there
was no uid model yet to make owner-vs-other meaningful. Deliberately a
pure function of its four arguments, not of "whoever is currently
running" -- every real call site passes currentThreadUid() in explicitly
for `callerUid`, so this stays testable against arbitrary uid/mode/owner
combinations without ever touching a live thread's own credential (see
coreTest.c's own KTEST coverage). `wantOwnerBit` is always one of
fs.h's fsModeOwner* constants; the matching "other" bit is derived by
shifting it right 6 (fsModeOther*'s own doc comment explains why that is
exact, not approximate) rather than requiring a second argument.
Deliberately NO uid-0-bypasses-everything superuser rule, unlike real
Unix's root: every boot-time process starts at uid 0 (sched.h's
Thread.uid doc comment), so a blanket bypass would have silently undone
the entire pre-existing "chmod denies write even to the file's own
creator" contract every current permission test (this file's own
resolveOpen tests, permtest.c's live demo) already relies on -- caught
by exactly those tests going red the first time this function existed
with a bypass in it. uid 0 is simply this kernel's default starting
identity, gated by the same owner/other bits as any other uid; its only
real asymmetry is currentThreadSetUid's own one-way privilege-drop rule
(sched.h), not a free pass here. */
bool vfsPermitted(uint32 mode, uint32 ownerUid, uint32 ownerGid, uint32 callerUid, uint32 callerGid, uint32 wantOwnerBit)
{
if (callerUid == ownerUid)
return (mode & wantOwnerBit) != 0;
if (callerGid == ownerGid)
return (mode & (wantOwnerBit >> 3)) != 0;
return (mode & (wantOwnerBit >> 6)) != 0;
}
const void* vfsFileData(VfsNode* node)
{
if (!node)
return nullptr;
return node->roData ? (const void*)node->roData : (const void*)node->rwData;
}
void vfsPath(VfsNode* node, char* out, uint64 cap)
{
if (cap < 2)
return;
if (node == nullptr || node == root) {
out[0] = '/';
out[1] = 0;
return;
}
const char* parts[32];
uint32 count = 0;
for (VfsNode* n = node; n && n != root && count < 32; n = n->parent)
parts[count++] = n->name;
uint64 pos = 0;
for (int32 i = (int32)count - 1; i >= 0; i--) {
if (pos < cap - 1)
out[pos++] = '/';
const char* s = parts[i];
for (uint32 j = 0; s[j] && pos < cap - 1; j++)
out[pos++] = s[j];
}
if (pos == 0)
out[pos++] = '/';
out[pos] = 0;
}