feat: extend syscall with LoadedImage and loadImageByPath
This commit is contained in:
1 parent
0a1f4868dd
commit
5149adf7ae
1 file changed
+193
-2
+193
-2
@@ -101,11 +101,202 @@ static int64 doRead(uint64 fd, uint64 buffer, uint64 length)
|
|||||||
return vfsRead((int)fd, (void*)buffer, length);
|
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)
|
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) {
|
switch (regs->rax) {
|
||||||
case sysWrite:
|
case sysWrite: {
|
||||||
regs->rax = (uint64)doWrite(regs->rdi, regs->rsi, regs->rdx);
|
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;
|
return (uint64)regs;
|
||||||
case sysRead:
|
case sysRead:
|
||||||
regs->rax = (uint64)doRead(regs->rdi, regs->rsi, regs->rdx);
|
regs->rax = (uint64)doRead(regs->rdi, regs->rsi, regs->rdx);
|
||||||
|
|||||||
Reference in new issue
Block a user