Files
frostbyte/src/syscall.c
T
2026-09-08 18:44:16 +02:00

972 lines
41 KiB
C

#include "syscall.h"
#include "isr.h"
#include "console.h"
#include "sched.h"
#include "vfs.h"
#include "fs.h"
#include "string.h"
#include "elf.h"
#include "paging.h"
#include "signal.h"
#include "keyboard.h"
#include "pit.h"
#include "poll.h"
#include "rtc.h"
#include "mmap.h"
#define userSpaceMin 0x0000000040000000ull
#define userSpaceMax 0x0000800000000000ull
/* Matches kmain.c's userStackSize -- both sides map exactly one slot's
worth of stack for a freshly loaded binary, whether it was spawned at
boot (kmain.c's spawnUser, addresses picked by hand) or at runtime
(doSpawn/doExec below, addresses derived from elfSlotStackTop). */
#define execStackSize 0x4000
static bool userRangeOk(uint64 pointer, uint64 length)
{
if (pointer < userSpaceMin)
return false;
if (pointer + length < pointer)
return false;
if (pointer + length > userSpaceMax)
return false;
return true;
}
static bool copyUserPath(uint64 pointer, char* dst, uint64 max)
{
if (pointer < userSpaceMin || pointer >= userSpaceMax)
return false;
const char* src = (const char*)pointer;
for (uint64 i = 0; i < max; i++) {
dst[i] = src[i];
if (src[i] == 0)
return true;
}
dst[max - 1] = 0;
return true;
}
#define execArgMaxLen 256
/* Copies a NULL-terminated array of user-space `const char*` (the argv
parameter of sysExec/sysSpawn -- userExec/userSpawn, ulib.c) into kernel
memory: up to elfMaxArgs (elf.h) pointers are read out of userspace one
at a time (each individually range-checked, since the array itself is
just user memory a buggy or hostile caller controls) and each string
copied via copyUserPath into `storage`, which the caller owns for as
long as it needs argvPtrs to stay valid. A null `argvPointer` (the
common "no arguments" case) is not an error -- it yields argc 0, letting
userExec(path, nullptr) mean the same as a bare execve(path) with an
otherwise-empty argv. Fails (leaving *outArgc untouched) on a bad
pointer, a string that doesn't fit `execArgMaxLen`, or an argv longer
than elfMaxArgs entries that never hits its NULL terminator. */
static bool copyUserArgv(uint64 argvPointer, char storage[elfMaxArgs][execArgMaxLen],
const char* argvPtrs[elfMaxArgs], int* outArgc)
{
if (argvPointer == 0) {
*outArgc = 0;
return true;
}
if (!userRangeOk(argvPointer, sizeof(uint64) * (elfMaxArgs + 1)))
return false;
const uint64* userArgv = (const uint64*)argvPointer;
int argc = 0;
while (argc < elfMaxArgs && userArgv[argc] != 0) {
if (!copyUserPath(userArgv[argc], storage[argc], execArgMaxLen))
return false;
argvPtrs[argc] = storage[argc];
argc++;
}
if (argc == elfMaxArgs && userArgv[argc] != 0)
return false;
*outArgc = argc;
return true;
}
static int64 doWrite(uint64 fd, uint64 buffer, uint64 length)
{
if (!userRangeOk(buffer, length))
return -1;
return vfsWrite((int)fd, (const void*)buffer, length);
}
static int64 doRead(uint64 fd, uint64 buffer, uint64 length)
{
if (!userRangeOk(buffer, length))
return -1;
return vfsRead((int)fd, (void*)buffer, length);
}
typedef struct LoadedImage {
uint64 entry;
uint64 stackTop;
uint64 brkStart;
} LoadedImage;
/* Shared by doExec and doSpawn: resolves `path` through the VFS, loads its
LOAD segments via elfLoad (which maps them at whatever fixed virtual
address they were linked at -- see elf.c), then derives and maps a fresh
stack for that slot from nothing but the ELF's own entry point (see
elfSlotStackTop/elf.h). Leaves the caller's thread state untouched;
callers decide what to do with a successfully loaded image. */
static bool loadImageByPath(const char* path, LoadedImage* out)
{
VfsNode* node = vfsLookup(path);
if (!node)
return false;
/* Real EACCES-shaped refusal: a file without the right exec bit for the
CALLING thread's identity (vfsPermitted, owner-vs-other-vs-root) can
never be exec'd or spawned, exactly like real Unix's own execve()
check -- jobctl.c's own jobctlSpawn (the shell's separate, parallel
loading path) enforces the identical check the identical way. */
if (!vfsPermitted(vfsMode(node), vfsUid(node), vfsGid(node), currentThreadUid(), currentThreadGid(), fsModeOwnerExec))
return false;
const void* image = vfsFileData(node);
uint64 size = vfsSize(node);
uint64 entry = elfLoad(image, size);
if (entry == 0)
return false;
out->entry = entry;
out->stackTop = elfSlotStackTop(entry);
out->brkStart = elfSlotBrkStart(entry);
return pagingMapUserRangeFlags(out->stackTop - execStackSize, execStackSize, pageNoExecute);
}
/* execve()-lite: replaces the calling thread's own program image with a
different one, in place -- same pid, same open file descriptors (POSIX's
default, no O_CLOEXEC support to opt out of here), new code/stack/brk.
Mutates the live interrupt frame's rip/rsp directly (the same trick
threadCreateUser uses to bootstrap a brand new thread's very first
frame): on success this syscall does not "return" to the caller in any
meaningful sense, since regs->rip now points at the new image's entry
point instead of back into the old one's code -- exactly real exec()
semantics. General-purpose registers are zeroed so the new program never
observes leftover state from a syscall it never made. Failure (bad path,
not a valid ELF, thread table/frame mapping issue) leaves regs untouched
apart from the -1 return value: the caller keeps running its old image,
also matching real exec().
Also reclaims the OLD image's slot: frostbyte's flat single-address-space
design (see doSpawn below) means a program that exec()s into a binary
linked at a different fixed base -- the common case, since two distinct
binaries can never share a slot -- would otherwise leak its entire
elfSlotSize worth of physical frames every time, forever. Skipped only
when the new image happens to land in the SAME slot as the old one (a
binary exec()ing itself): unmapping first would free frames the new
image's own loadImageByPath call had already mapped moments earlier.
argv/argc are already kernel-side copies (copyUserArgv, called from the
sysExec dispatch case below, BEFORE this runs) -- critical ordering,
since they may point into the OLD image's own memory (a process's argv
commonly lives in its own .rodata/stack), which this function is about
to unmap out from under it. */
static bool doExec(const char* path, const char* const* argv, int argc, Registers* regs)
{
uint32 pid = currentThreadId();
uint64 oldSlotBase = threadUserSlotBase(pid);
LoadedImage img;
if (!loadImageByPath(path, &img))
return false;
uint64 initialRsp;
if (!elfBuildInitialStack(img.stackTop, execStackSize, argv, argc, &initialRsp))
return false;
uint64 newSlotBase = elfSlotBase(img.entry);
if (oldSlotBase != 0 && oldSlotBase != newSlotBase) {
uint64 stackProbe = oldSlotBase + elfSlotStackOffset - 1;
bool wasMapped = pagingResolve(stackProbe) != 0;
pagingUnmapUserRange(oldSlotBase, elfSlotSize);
bool nowFreed = pagingResolve(stackProbe) == 0;
kprintf("[exec] pid=%u freed old slot %p (was-mapped=%s now-freed=%s) -> %s\n",
pid, (void*)oldSlotBase, wasMapped ? "yes" : "no", nowFreed ? "yes" : "no",
(wasMapped && nowFreed) ? "exec-old-slot-freed-ok" : "exec-old-slot-freed-FAIL");
} else if (oldSlotBase != 0) {
/* Self-exec (or the pid==0/no-prior-slot case, unreachable for a
real ring-3 caller): the branch above is skipped precisely to
avoid unmapping the NEW image's own LOAD segments/stack, which
already landed on these same pages -- but any mmap mapping from
the OLD image is a disjoint sub-range (mmap.h's own mmapReserve
region, [slot+elfSlotStackOffset, slot+elfSlotSize)) that
loadImageByPath/elfBuildInitialStack never touch, so it still
needs reclaiming here explicitly or its frames would leak
forever every time a binary exec()s itself. */
pagingUnmapUserRange(oldSlotBase + elfSlotStackOffset, elfSlotSize - elfSlotStackOffset);
}
if (oldSlotBase != 0)
mmapReleaseAll(threadAt(pid));
threadSetUserSlotBase(pid, newSlotBase);
threadSetName(pid, path);
threadSetBrk(pid, img.brkStart);
regs->rip = img.entry;
regs->rsp = initialRsp;
regs->rbp = 0;
regs->rdi = 0;
regs->rsi = 0;
regs->rdx = 0;
regs->rcx = 0;
regs->rbx = 0;
regs->rax = 0;
regs->r8 = 0;
regs->r9 = 0;
regs->r10 = 0;
regs->r11 = 0;
regs->r12 = 0;
regs->r13 = 0;
regs->r14 = 0;
regs->r15 = 0;
return true;
}
/* posix_spawn()-lite: loads `path` into a brand new thread/process rather
than replacing the caller, and returns its pid immediately without
waiting -- the caller joins it later with sysWait (threadJoin). This is
deliberately not a literal fork(): frostbyte has one shared address space
system-wide (no per-process page tables/CR3, see paging.c), so there is
no address space to duplicate, only a fresh slot to load a binary into.
As with any binary in this kernel, spawning two live instances of the
same path (or of two paths whose fixed link addresses happen to share a
slot) at once would map both onto the same pages and corrupt each other;
this is an accepted limitation of the flat single-address-space design,
not something doSpawn can detect from a path string alone. */
static int64 doSpawn(const char* path, const char* const* argv, int argc)
{
LoadedImage img;
if (!loadImageByPath(path, &img))
return -1;
uint64 initialRsp;
if (!elfBuildInitialStack(img.stackTop, execStackSize, argv, argc, &initialRsp))
return -1;
uint32 pid = threadCreateUser(path, img.entry, initialRsp);
if (pid == threadInvalidId)
return -1;
threadSetBrk(pid, img.brkStart);
return (int64)pid;
}
uint64 syscallDispatch(Registers* regs)
{
/* This thread is not (yet, or any more) parked mid a retryable
syscall's own block-and-retry pattern the instant ANY dispatch for
it begins -- whether this dispatch turns out to BE that exact retry
re-executing (sysRead/sysWrite/sysWait's own cases set it true again
below if they block once more) or a completely unrelated syscall.
See sched.h's Thread.sigInRetryableSyscall and signal.c's
sigConsumePending for why this precise a window matters: without it,
a signal delivered while this thread is doing perfectly ordinary
work could be mistaken for one interrupting a slow syscall. */
currentThread()->sigInRetryableSyscall = false;
switch (regs->rax) {
case sysWrite: {
int64 result = doWrite(regs->rdi, regs->rsi, regs->rdx);
if (result == -1) {
/* A "would block" result on a fdFlagBlocking pipe end (see
vfs.h) is retried, not returned: park on the pipe's channel
and rewind rip by the 2-byte `int $0x80` this trap came from
so the whole syscall re-executes once schedWakeChannel makes
this thread Runnable again (pipeWrite, called from doWrite
above, already does that whenever it frees or fills space) --
the exact restart-syscall pattern sysWait uses for
threadJoin, for the same reason: halt()ing here directly
would freeze the whole machine, not just this thread, since
this handler runs with interrupts disabled. A non-blocking
pipe end (vfsPipe, e.g. pipetest.c) or a permanently broken
one gets nullptr back and the -1 passes through untouched. */
const void* channel = vfsBlockingChannel((int)regs->rdi, true);
if (channel) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(channel);
regs->rip -= 2;
return schedule((uint64)regs);
}
}
regs->rax = (uint64)result;
return (uint64)regs;
}
case sysRead: {
int64 result = doRead(regs->rdi, regs->rsi, regs->rdx);
if (result == -1) {
/* Covers a fdFlagBlocking pipe/fifo end (see sysWrite above) and
fd 0's console reader queue (keyboard.h) alike -- both report
"would block" as -1 from doRead, and vfsBlockingChannel picks
the right wait channel for whichever kind fd actually is. */
const void* channel = vfsBlockingChannel((int)regs->rdi, false);
if (channel) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(channel);
regs->rip -= 2;
return schedule((uint64)regs);
}
}
regs->rax = (uint64)result;
return (uint64)regs;
}
case sysGetpid:
regs->rax = currentThreadId();
return (uint64)regs;
case sysSbrk:
regs->rax = currentThreadSbrk((int64)regs->rdi);
return (uint64)regs;
case sysYield:
regs->rax = 0;
return schedule((uint64)regs);
case sysSleep:
threadSleep(regs->rdi);
regs->rax = 0;
return schedule((uint64)regs);
case sysExit:
threadExit(regs->rdi);
return schedule((uint64)regs);
case sysOpen: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsOpen(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysClose:
regs->rax = (uint64)(int64)vfsClose((int)regs->rdi);
return (uint64)regs;
case sysLseek:
regs->rax = (uint64)vfsLseek((int)regs->rdi, (int64)regs->rsi, (int)regs->rdx);
return (uint64)regs;
case sysStat: {
char path[256];
FbStat st;
if (copyUserPath(regs->rdi, path, sizeof(path)) &&
userRangeOk(regs->rsi, sizeof(FbStat)) &&
vfsStat(path, &st) == 0) {
memcpy((void*)regs->rsi, &st, sizeof(FbStat));
regs->rax = 0;
} else {
regs->rax = (uint64)-1;
}
return (uint64)regs;
}
case sysGetdents:
if (!userRangeOk(regs->rsi, regs->rdx))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)vfsGetdents((int)regs->rdi, (void*)regs->rsi, regs->rdx);
return (uint64)regs;
case sysMkdir: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsMkdir(path);
return (uint64)regs;
}
case sysChdir: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChdir(path);
return (uint64)regs;
}
case sysGetcwd:
if (!userRangeOk(regs->rdi, regs->rsi))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)vfsGetcwd((char*)regs->rdi, regs->rsi);
return (uint64)regs;
case sysGetEnv: {
char name[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name)) || !userRangeOk(regs->rsi, regs->rdx)) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
const char* value = currentThreadGetEnv(name);
if (!value) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
uint64 len = strlen(value);
if (len >= regs->rdx) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
memcpy((void*)regs->rsi, value, len + 1);
regs->rax = (uint64)len;
return (uint64)regs;
}
case sysSetEnv: {
char name[envEntryMax];
char value[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name)) ||
!copyUserPath(regs->rsi, value, sizeof(value))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
regs->rax = currentThreadSetEnv(name, value) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysUnsetEnv: {
/* Real unsetenv(3): a no-op success whether or not `name` actually
had an entry -- ONLY a bad user pointer/path is a real failure
here, unlike sysSetEnv above where "table full"/"too long" are
genuine failures. currentThreadUnsetEnv's own bool return (which
DOES distinguish "removed" from "was not there", for a kernel-
side caller like shell.c that might want to) is deliberately
discarded at this syscall boundary. */
char name[envEntryMax];
if (!copyUserPath(regs->rdi, name, sizeof(name))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
currentThreadUnsetEnv(name);
regs->rax = 0;
return (uint64)regs;
}
case sysUnlink: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsUnlink(path);
return (uint64)regs;
}
case sysRmdir: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsRmdir(path);
return (uint64)regs;
}
case sysChmod: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChmod(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysChown: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChown(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysGetuid:
/* Real getuid(2): no arguments, always succeeds, straight through
to the calling thread's own credential. */
regs->rax = (uint64)currentThreadUid();
return (uint64)regs;
case sysSetuid:
/* Real setuid(2): currentThreadSetUid (sched.h) itself enforces the
actual privilege rule (uid 0 may become anyone; anyone else may
only pass their own current uid back in) -- this dispatch case
does no additional validation of its own, matching sysChdir's own
"the underlying function already checks everything" split. */
regs->rax = currentThreadSetUid((uint32)regs->rdi) ? 0 : (uint64)-1;
return (uint64)regs;
case sysGetgid:
/* Real getgid(2): same shape as sysGetuid above. */
regs->rax = (uint64)currentThreadGid();
return (uint64)regs;
case sysSetgid:
/* Real setgid(2): currentThreadSetGid (sched.h) itself enforces the
actual privilege rule (gated by this thread's own UID, not GID --
see that function's own doc comment). */
regs->rax = currentThreadSetGid((uint32)regs->rdi) ? 0 : (uint64)-1;
return (uint64)regs;
case sysChgrp: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsChgrp(path, (uint32)regs->rsi);
return (uint64)regs;
}
case sysPidfdOpen:
/* vfsPidfdOpen (vfs.c) already does every validity check pidfd.h's
own pidfdOpen needs (id 0/out-of-range/already-unused target) and
the fd-table installation vfsPipe/vfsPipeAttach's own dispatch
cases above delegate the same way -- nothing left for this
handler to add. */
regs->rax = (uint64)vfsPidfdOpen((uint32)regs->rdi);
return (uint64)regs;
case sysPidfdSend: {
/* vfsPidfdTarget folds "is this fd even a live pidfd" and "has the
original target since been reaped/recycled" (pidfd.h's own
generation check) into one call -- a stale handle is rejected
right here, before sigRaiseInfo ever sees a pid, which is
exactly the ABA-safety pidfd.h exists to provide over a raw
userSigSend(pid, signum) on a cached pid (see pidfd.h's own file
comment). Otherwise identical to sysSigSend's own single-target
path just above (same siCodeUser/senderPid convention, same
sigKill/sigStop bypasses already living inside sigRaiseInfo). */
uint32 target = vfsPidfdTarget((int)regs->rdi);
if (target == threadInvalidId) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
int signum = (int)regs->rsi;
bool ok = sigRaiseInfo(target, signum, siCodeUser, (int)currentThreadId(), 0);
regs->rax = ok ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysPidfdWait: {
/* Same retry-the-syscall park pattern as sysWait's own specific-pid
path below, just resolving the target through a pidfd instead of
a raw pid -- re-resolved on EVERY retry (not just the first
check), so a target that gets reaped by someone else while this
thread is blocked is caught the instant this thread wakes back
up, rather than only at the moment sysPidfdWait was first called.
threadJoin(target) below only ever reaps a thread this exact
check just proved is threadDead AND still the same incarnation
this handle was opened on, so there is no risk of it silently
reaping (or reporting the exit code of) an unrelated thread that
happens to have landed on the same numeric slot since. */
uint32 target = vfsPidfdTarget((int)regs->rdi);
if (target == threadInvalidId) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
Thread* targetThread = threadAt(target);
if (targetThread->state != threadDead) {
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(targetThread);
regs->rip -= 2;
return schedule((uint64)regs);
}
regs->rax = (uint64)threadJoin(target);
return (uint64)regs;
}
case sysSymlink: {
char target[256];
char path[256];
if (!copyUserPath(regs->rdi, target, sizeof(target)) ||
!copyUserPath(regs->rsi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsSymlink(path, target);
return (uint64)regs;
}
case sysReadlink: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)) || !userRangeOk(regs->rsi, regs->rdx))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)vfsReadlink(path, (char*)regs->rsi, regs->rdx);
return (uint64)regs;
}
case sysLink: {
char oldPath[256];
char newPath[256];
if (!copyUserPath(regs->rdi, oldPath, sizeof(oldPath)) ||
!copyUserPath(regs->rsi, newPath, sizeof(newPath)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsLink(oldPath, newPath);
return (uint64)regs;
}
case sysPipe: {
if (!userRangeOk(regs->rdi, 2 * sizeof(int))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
int* userFds = (int*)regs->rdi;
int readFd, writeFd;
if (vfsPipe(&readFd, &writeFd) < 0) {
regs->rax = (uint64)-1;
} else {
userFds[0] = readFd;
userFds[1] = writeFd;
regs->rax = 0;
}
return (uint64)regs;
}
case sysPipeCreate:
regs->rax = (uint64)vfsPipeCreateHandle();
return (uint64)regs;
case sysPipeAttach:
regs->rax = (uint64)vfsPipeAttach((int)regs->rdi, regs->rsi != 0);
return (uint64)regs;
case sysKill: {
/* Real kill(2)'s own pid<0/pid==0 convention (userKill's `pid` is
already a signed `long`, see ulib.h -- only this kernel-side
interpretation of the raw bit pattern was missing): pid>0 is the
single-target path this always had; pid==0 targets the caller's
OWN process group (threadKillGroup, sched.c -- naturally kills
no one else if this is the group's only OTHER member, since
threadKill itself already excludes the caller's own Running
thread); pid<0 targets group -pid, exactly like real
kill(-pgid, SIGKILL). */
int64 rawPid = (int64)regs->rdi;
bool ok;
if (rawPid > 0)
ok = threadKill((uint32)rawPid);
else
ok = threadKillGroup(rawPid == 0 ? currentThread()->pgid : (uint32)(-rawPid));
regs->rax = ok ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysSigSend: {
/* Same pid>0/pid==0/pid<0 convention as sysKill just above, this
time through sigRaiseGroup (signal.h) for the two group cases --
see that function's own doc comment for why every per-target
special case (SIGKILL/SIGSTOP/SIGCONT bypasses, early-wake,
SigInfo) still applies exactly as it already does for a plain,
single-target send. */
int64 rawPid = (int64)regs->rdi;
int signum = (int)regs->rsi;
uint32 senderPid = currentThreadId();
bool isGroup = (rawPid <= 0);
uint32 targetPgid = isGroup ? (rawPid == 0 ? currentThread()->pgid : (uint32)(-rawPid)) : 0;
/* siCodeUser/senderPid: a real userSigSend always has a genuine
sending process, unlike sched.c's own kernel-internal raises
(SIGALRM/SIGCHLD) or keyboard.c's terminal-generated ones --
exactly real Unix's own SI_USER, si_pid the caller's own pid,
exactly like real kill(2). */
bool ok = isGroup
? sigRaiseGroup(targetPgid, signum, siCodeUser, (int)senderPid)
: sigRaiseInfo((uint32)rawPid, signum, siCodeUser, (int)senderPid, 0);
regs->rax = ok ? 0 : (uint64)-1;
/* Every OTHER outcome of sigRaise/sigRaiseGroup either changes
nothing about the CALLING thread's own state (a signal aimed at
someone else) or, for an ordinary catchable signal aimed at
self, is already caught by isrDispatch's unconditional
deliverPendingSignals call right after this handler returns
(see isr.c) -- self-signaling sigUsr1 and having its handler
fire before ever reaching userspace again already works today
without any special case here. SIGSTOP against the caller's own
pid (or a group that includes it) is the one exception:
threadStop (called from sigRaise above, once per group member
for the group case) already flipped this very thread's state
away from Running, but sigRaise/sysSigSend never touch
sigPending for it (see signal.h), so deliverPendingSignals'
pending-bit scan would find nothing and simply hand `regs`
straight back -- returning to ring 3 and continuing to run a
thread the scheduler no longer considers Runnable, until the
next timer tick eventually notices. Explicit reschedule here
closes that gap, the same "flip state, then let schedule() do
the actual waiting" pattern sysSleep/sysExit already use for
their own self-directed state changes. The group case checks
targetPgid against the caller's OWN pgid rather than an exact id
match, since the caller itself -- not just some other member --
is who this reschedule is actually about. */
bool selfTargeted = isGroup ? (targetPgid == currentThread()->pgid) : ((uint32)rawPid == senderPid);
if (ok && signum == sigStop && selfTargeted)
return schedule((uint64)regs);
return (uint64)regs;
}
case sysSigAction:
regs->rax = sigSetHandler(currentThread(), (int)regs->rdi, regs->rsi, regs->rdx, regs->r10);
return (uint64)regs;
case sysSigProcMask:
regs->rax = sigProcMask(currentThread(), (int)regs->rdi, regs->rsi);
return (uint64)regs;
case sysSigReturn:
sigOnSigReturn(currentThread(), regs);
return (uint64)regs;
case sysAlarm:
regs->rax = threadSetAlarm(regs->rdi);
return (uint64)regs;
case sysSetpgid: {
/* Real setpgid(2)'s own "0 means self"/"0 means use pid itself as
the new group id" resolution -- threadSetPgid (sched.h) takes an
already-fully-resolved value, exactly like threadSetBrk/
threadSetAlarm above do for THEIR raw syscall arguments. */
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 pgid = (regs->rsi == 0) ? pid : (uint32)regs->rsi;
regs->rax = threadSetPgid(pid, pgid) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysGetpgid: {
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 pgid = threadPgid(pid);
regs->rax = (pgid == threadInvalidId) ? (uint64)-1 : (uint64)pgid;
return (uint64)regs;
}
case sysNice: {
/* Real setpriority(PRIO_PROCESS, ...)'s own "0 means the caller
itself" convention, same resolution shape as sysSetpgid's `pid`
argument just above -- rdi/pid, rsi/weight, straight through to
threadSetPriority (sched.h), which now does the SAME id==0/
out-of-range/Dead-or-Unused rejection threadSetPgid already did
(see that function's own updated doc comment for why it had to
catch up once this syscall made it reachable from ring 3). */
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 weight = (uint32)regs->rsi;
regs->rax = threadSetPriority(pid, weight) ? 0 : (uint64)-1;
return (uint64)regs;
}
case sysGetPriority: {
uint32 pid = (regs->rdi == 0) ? currentThreadId() : (uint32)regs->rdi;
uint32 weight = threadPriority(pid);
regs->rax = (weight == 0) ? (uint64)-1 : (uint64)weight;
return (uint64)regs;
}
case sysTime:
/* Real time(2)'s job: seconds since the Unix epoch, off the real
CMOS RTC (rtc.c) -- the first syscall in this kernel answering
"what day is it" instead of "how long since boot" (sysSleep,
userAlarm). No arguments, nothing to validate. */
regs->rax = (uint64)rtcNow();
return (uint64)regs;
case sysSetFgPgrp:
/* Real tcsetpgrp(3)'s job -- names which process group keyboard.c's
real Ctrl-C/Ctrl-Z (SIGINT/SIGTSTP) is scoped to next. No
permission check at all (this kernel has no notion of "the
controlling terminal of MY session" to check ownership against,
the same honest, documented omission threadSetPgid's own doc
comment already gives for setpgid's session check) -- any
process can claim the foreground for any pgid, including one it
is not even a member of. */
keyboardSetForegroundPgid((uint32)regs->rdi);
regs->rax = 0;
return (uint64)regs;
case sysGetFgPgrp:
regs->rax = (uint64)keyboardForegroundPgid();
return (uint64)regs;
case sysPoll: {
/* Real poll(2): rdi/PollFd array, rsi/nfds, rdx/timeoutMs (signed,
-1 = wait forever, 0 = report current readiness without ever
blocking, like every real poll(2) implementation). Flat address
space (this kernel's honest fork()-less design, see doSpawn's own
comment) means the array can be read and written in place, the
same way sysStat writes straight into its caller's FbStat*
without a separate copy-out step. */
uint64 nfds = regs->rsi;
int64 timeoutMs = (int64)regs->rdx;
if (nfds > maxPollFds || !userRangeOk(regs->rdi, nfds * sizeof(PollFd))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
PollFd* userFds = (PollFd*)regs->rdi;
Thread* self = currentThread();
/* First entry into this call (not a retry re-executing after a
park below) picks the deadline once and remembers it in
pollDeadline -- a later retry must NOT recompute it from the
still-unchanged rdx, or a poll that keeps almost-but-not-quite
finding a ready fd could have its effective timeout pushed out
indefinitely instead of counting down real elapsed time. */
if (!self->pollActive && timeoutMs != 0) {
self->pollActive = true;
if (timeoutMs < 0) {
self->pollDeadline = 0;
} else {
uint64 ticks = ((uint64)timeoutMs * (uint64)pitFrequency()) / 1000;
if (ticks == 0)
ticks = 1;
self->pollDeadline = pitTicks() + ticks;
}
}
int64 ready = 0;
for (uint64 i = 0; i < nfds; i++) {
int16 revents = 0;
if (vfsPollReady(userFds[i].fd, userFds[i].events, &revents))
ready++;
userFds[i].revents = revents;
}
bool expired = self->pollActive && self->pollDeadline != 0 &&
pitTicks() >= self->pollDeadline;
if (ready > 0 || timeoutMs == 0 || expired) {
self->pollActive = false;
self->pollDeadline = 0;
regs->rax = (uint64)ready;
return (uint64)regs;
}
/* None ready yet, timeout not yet reached: park on the one shared
channel every poller blocks on (schedPollChannel, sched.h) and
rewind rip so the whole syscall re-executes -- the same
restart-the-syscall trick sysRead/sysWrite/sysWait already use,
except schedule()'s own expiry scan (sched.c) is what guarantees
this retry eventually happens even if no fd ever becomes ready. */
self->sigInRetryableSyscall = true;
schedBlockCurrent(schedPollChannel());
regs->rip -= 2;
return schedule((uint64)regs);
}
case sysMkfifo: {
char path[256];
if (!copyUserPath(regs->rdi, path, sizeof(path)))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)(int64)vfsMkfifo(path);
return (uint64)regs;
}
case sysExec: {
char path[256];
char argvStorage[elfMaxArgs][execArgMaxLen];
const char* argvPtrs[elfMaxArgs];
int argc;
if (!copyUserPath(regs->rdi, path, sizeof(path)) ||
!copyUserArgv(regs->rsi, argvStorage, argvPtrs, &argc) ||
!doExec(path, argvPtrs, argc, regs))
regs->rax = (uint64)-1;
return (uint64)regs;
}
case sysSpawn: {
char path[256];
char argvStorage[elfMaxArgs][execArgMaxLen];
const char* argvPtrs[elfMaxArgs];
int argc;
if (!copyUserPath(regs->rdi, path, sizeof(path)) ||
!copyUserArgv(regs->rsi, argvStorage, argvPtrs, &argc))
regs->rax = (uint64)-1;
else
regs->rax = (uint64)doSpawn(path, argvPtrs, argc);
return (uint64)regs;
}
case sysWait: {
uint32 id = (uint32)regs->rdi;
if (id == waitAnyPid) {
/* waitpid(-1)-style "wait for ANY child" -- see
threadJoinAnyChild's own doc comment (sched.h) for the full
contract. regs->rsi is an optional user `long*` pointer
(userWaitAny's outPid, ulib.c) that receives which pid
actually got reaped; nullptr means the caller does not care.
Written as a full 8-byte store below to match `long`'s real
width, not truncated to sizeof(uint32) -- a narrower store
would leave the user variable's upper 4 bytes as whatever
uninitialized garbage the caller's stack already had. */
if (regs->rsi != 0 && !userRangeOk(regs->rsi, sizeof(uint64))) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
uint32 reapedId;
int64 code;
WaitScanResult scan = threadJoinAnyChild(currentThreadId(), &reapedId, &code);
if (scan == waitScanNoChildren) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
if (scan == waitScanNoneDead) {
/* Same retry-syscall dance as the specific-pid path below,
just parked on our OWN slot instead of a named child's --
threadExit/threadKill wake this channel for EVERY dying
child of ours, not just one in particular (see
threadJoinAnyChild's doc comment), so re-scanning from
scratch on each retry is what actually picks up whichever
one died. */
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(currentThread());
regs->rip -= 2;
return schedule((uint64)regs);
}
if (regs->rsi != 0)
*(uint64*)regs->rsi = (uint64)reapedId;
regs->rax = (uint64)code;
return (uint64)regs;
}
if (id == 0 || id >= threadCapacity()) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
Thread* target = threadAt(id);
if (target->state == threadUnused) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
if (target->state != threadDead) {
/* Not dead yet: parking here and calling halt() the way
threadJoin (sched.c) does for a plain kernel thread body
would hang the whole machine, not just this thread -- this
handler runs with interrupts disabled (int 0x80 is an
interrupt gate, idt.c) on the single shared bootstrap kernel
stack (isrStubs.S/tss.c), not the calling thread's own
persistent one, so unlike kmain.c's joinerThread it cannot
literally halt in place and pick its own C call frame back
up later: hlt with IF=0 never wakes, since nothing can ever
deliver the timer interrupt that would run schedule() again.
Instead: block on the same channel threadExit wakes
(schedWakeChannel(&threads[id])), rewind rip by the 2 bytes
of the `int $0x80` opcode this trap just came from so it
re-executes once this thread is scheduled again, and hand
the CPU to schedule() -- the same "flip state, then let the
interrupt-return path do the actual waiting" technique
sysSleep already uses. The retry re-enters this exact case
with the same rax/rdi; since threadExit will have already
flipped the target to Dead before waking this thread back
up, it takes the branch below next time and completes
without blocking again. */
currentThread()->sigInRetryableSyscall = true;
schedBlockCurrent(target);
regs->rip -= 2;
return schedule((uint64)regs);
}
regs->rax = (uint64)threadJoin(id);
return (uint64)regs;
}
case sysMmap: {
/* addrHint==0 means "kernel picks" (mmapReserve's own first-fit,
real mmap(NULL, ...) shape); a nonzero addrHint is taken as an
exact placement request, refused (not silently relocated) if it
does not fit -- there is no MAP_FIXED bit to distinguish the two,
every addrHint here behaves like real mmap's own weaker "hint"
contract already narrowed down to "exact or refused". fd/offset
are read (and validated below) even when MAP_ANONYMOUS is set,
harmlessly ignored by mmapReserve itself in that case, exactly
like real mmap(2)'s own documented behavior. */
uint64 addrHint = regs->rdi;
uint64 length = regs->rsi;
uint32 prot = (uint32)regs->rdx;
uint32 flags = (uint32)regs->r10;
int fd = (int)regs->r8;
uint64 offset = regs->r9;
bool anonymous = (flags & mmapAnonymous) != 0;
if (!anonymous) {
/* Same three seekable, real-data fd kinds vfsLseek itself
already restricts to (vfs.c) -- a pipe/fifo/console/pidfd fd
has no fixed byte range a page fault could ever re-read
deterministically, so mmap refuses them here rather than
letting mmapFaultHandler discover that failure lazily, one
page at a time, later. */
if (fd < 0 || fd >= maxOpenFiles) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
FileDescriptor* fdEntry = &currentThread()->fds[fd];
if (fdEntry->kind != fdFile && fdEntry->kind != fdSfs && fdEntry->kind != fdBlockDev) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
}
uint64 mappedAddr;
if (!mmapReserve(currentThread(), addrHint, length, prot, anonymous, fd, offset, &mappedAddr)) {
regs->rax = (uint64)-1;
return (uint64)regs;
}
regs->rax = mappedAddr;
return (uint64)regs;
}
case sysMunmap:
regs->rax = mmapRelease(currentThread(), regs->rdi, regs->rsi) ? 0 : (uint64)-1;
return (uint64)regs;
default:
regs->rax = (uint64)-1;
return (uint64)regs;
}
}