972 lines
41 KiB
C
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 = ¤tThread()->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;
|
|
}
|
|
}
|