From b3b01cc38ffe7bbba463bb0e16313809328ded93 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 20:01:46 +0200 Subject: [PATCH] feat: extend ulib --- user/ulib.h | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) diff --git a/user/ulib.h b/user/ulib.h index 72967f4..6060d43 100644 --- a/user/ulib.h +++ b/user/ulib.h @@ -3,6 +3,19 @@ #include "syscall.h" #include "fs.h" +#include "signalnum.h" +#include "poll.h" + +typedef void (*SigHandler)(int signum); + +/* Real sigaction()'s sa_sigaction shape: signum (same as SigHandler's own + argument, kept for parity -- see signalnum.h's SigInfo.siSignum comment), + a pointer to the real SigInfo describing THIS delivery, and a context + pointer this kernel always hands back as NULL (see signalnum.h's + sigFlagSigInfo comment for why). Only ever meaningful for a handler + installed through userSigActionInfo below, WITH sigFlagSigInfo actually + set in its flags -- see that function's own doc comment. */ +typedef void (*SigInfoHandler)(int signum, SigInfo* info, void* context); long userWrite(int fd, const char* buffer, unsigned long length); long userRead(int fd, char* buffer, unsigned long length); @@ -12,6 +25,22 @@ long userGetpid(void); void* userSbrk(long increment); void userSleep(unsigned long milliseconds); +/* Real alarm(2): schedules sigAlrm against the calling process `seconds` + real seconds from now, replacing any alarm already pending; 0 cancels + instead of scheduling. Returns the whole seconds remaining on whatever + alarm was pending before this call (rounded up), or 0 if none was. See + sched.h's threadSetAlarm for the full contract. */ +unsigned long userAlarm(unsigned long seconds); + +/* Real poll(2): waits for any of `nfds` PollFd entries (poll.h) to become + ready, filling each one's revents in place. timeoutMs < 0 waits forever, + 0 reports current readiness without ever blocking, > 0 blocks at most + that many milliseconds (rounded up to this kernel's ~10ms PIT tick + granularity -- see sched.c's threadSetAlarm for the same rounding on a + different timer). Returns the number of entries with a nonzero revents + (0 on timeout, never negative except -1 for a bad nfds/pointer). */ +long userPoll(PollFd* fds, unsigned long nfds, long timeoutMs); + long userOpen(const char* path, unsigned int flags); long userClose(int fd); long userLseek(int fd, long offset, int whence); @@ -20,6 +49,179 @@ long userGetdents(int fd, FbDirent* out, unsigned long length); long userMkdir(const char* path); long userUnlink(const char* path); +/* Real rmdir(2): the counterpart userUnlink above refuses a directory + (EISDIR); this refuses anything that is NOT an empty directory + (ENOTDIR/ENOTEMPTY, both collapsed to -1 here, matching userUnlink's own + "just -1" error shape). */ +long userRmdir(const char* path); + +/* Real chmod(2): overwrites `path`'s whole owner-rwx mode (fs.h's + fsModeOwner*, e.g. 0644) with `mode` -- no symbolic +x-style merge, + matching real chmod's own numeric-mode form. 0 on success, -1 for a + missing path or (tmpfs only) a node under a non-writable mount, same + restriction userUnlink/userRmdir already have on the ustar-loaded + backing store. Takes effect immediately for any FUTURE userOpen/ + userSpawn/userExec on this path -- an fd already open, or a process + already running, is never affected (real Unix only checks permission at + open/exec time). */ +long userChmod(const char* path, unsigned int mode); + +/* Real chown(2) (vfs.h's vfsChown): reassigns `path`'s owner uid outright. + Root-only (this process's own userGetuid() must be 0), full stop, unlike + userChmod -- real Unix's _POSIX_CHOWN_RESTRICTED default means not even a + file's own owner may give it away. -1 for a missing path, a node under a + non-writable mount, or a non-root caller; the target's uid is left + completely unchanged on any failure. */ +long userChown(const char* path, unsigned int uid); + +/* Real getuid(2)/setuid(2) (sched.h's Thread.uid, currentThreadUid/ + currentThreadSetUid): userGetuid always succeeds, returning THIS + process's own current uid. userSetuid enforces the real privilege rule + itself (no additional check happens at the syscall layer) -- 0 ("root") + may become any uid; any other uid may only pass its own current value + back in (a no-op that still returns 0, matching real setuid(getuid())), + and any other target returns -1 with the credential unchanged. There is + no way back to uid 0 once dropped short of already being uid 0 at the + time of the call -- a real, permanent privilege drop for the lifetime of + this process, exactly like a daemon that setuid()s away from root once + at startup. */ +long userGetuid(void); +long userSetuid(unsigned int uid); + +/* Real getgid(2)/setgid(2) (sched.h's Thread.gid, currentThreadGid/ + currentThreadSetGid): userGetgid always succeeds, returning THIS + process's own current gid. userSetgid's privilege gate is THIS process's + own userGetuid() (== 0), not its gid -- matching real setgid(2)'s + CAP_SETGID-shaped rule -- any non-root caller may only pass its own + current gid back in (a no-op that still returns 0), any other target + returns -1 unchanged. */ +long userGetgid(void); +long userSetgid(unsigned int gid); + +/* Real chgrp(2) (vfs.h's vfsChgrp): reassigns `path`'s group gid. Unlike + userChown (root-only, full stop), the file's own owner may also call + this -- but only to set the group to THIS process's own current gid + (userGetgid()), never an arbitrary one, since this kernel tracks exactly + one gid per process (no supplementary-groups list). Root may set any + gid. -1 for a missing path, a node under a non-writable mount, a + non-owner non-root caller, or a non-root caller naming any gid other + than its own. */ +long userChgrp(const char* path, unsigned int gid); + +/* Real chdir(2)/getcwd(2) (sched.h's Thread.cwd, vfs.h's vfsChdir/ + vfsGetcwd): userChdir resolves a relative `path` against THIS process's + own current cwd, requires the result to already be a real directory, and + makes it this process's new cwd -- 0 on success, -1 (nothing changed) if + it does not name a directory. A child spawned by userSpawn/userExec + AFTER this call inherits the new value; this process's parent, and any + already-spawned sibling, are never affected (each process's cwd is an + independent copy, not shared state). userGetcwd copies the current + value into `buffer` (nul-terminated) and returns its length, or -1 + (buffer untouched) if it does not fit `cap` -- real getcwd(2)'s own + ERANGE case. */ +long userChdir(const char* path); +long userGetcwd(char* buffer, unsigned long cap); + +/* Real getenv(3)/setenv(3) (sched.h's Thread.env, syscall.c's sysGetEnv/ + sysSetEnv): userGetEnv copies THIS process's own value for `name` into + `buffer` (nul-terminated) and returns its length, or -1 (buffer + untouched) if `name` has no entry or the value does not fit `cap`. + userSetEnv overwrites an existing entry with a matching `name`, or + creates a new one, in THIS process's own table only -- 0 on success, -1 + if the combined "NAME=VALUE" would not fit the kernel's fixed entry size + or the table is already full. A child spawned by userSpawn/userExec + AFTER a userSetEnv call inherits the new value (a real, independent + copy, same as userChdir above); this process's parent, and any + already-spawned sibling, are never affected. */ +long userGetEnv(const char* name, char* buffer, unsigned long cap); +long userSetEnv(const char* name, const char* value); + +/* Real unsetenv(3): removes THIS process's own entry named `name` from its + table, if any, so a following userGetEnv on the same name returns -1 + again -- 0 whether or not an entry actually existed (matching real + unsetenv(3), a no-op success either way), never touches the parent's or + any sibling's own table (same independent-copy shape as userSetEnv). */ +long userUnsetEnv(const char* name); + +long userPipe(int fds[2]); + +/* Shared pipes: unlike userPipe (both ends always land in the caller's own + fd table), userPipeCreate returns a raw handle number that means nothing + until an fd table actually attaches to it (userPipeAttach) -- meant to be + handed to a different, independently spawned process (e.g. as an argv + string) so the two can share a pipe without needing fork(). Once + attached, userRead/userWrite on that fd really block (park the whole + process, not busy-retry) instead of returning -1 for "try again" the way + an unshared userPipe end does. */ +long userPipeCreate(void); +long userPipeAttach(long handle, int wantWrite); +long userSymlink(const char* target, const char* path); +long userReadlink(const char* path, char* buffer, unsigned long cap); +long userLink(const char* existingPath, const char* newPath); + +/* Named pipe: creates a fifo at `path` (see vfsMkfifo, vfs.h). Unlike + userPipeCreate/userPipeAttach, which need a raw handle number passed to + both ends (e.g. as an argv string, see fifomaster.c), any two processes + that simply agree on a path can rendezvous by both calling userOpen on it + -- oWriteOnly for a blocking write end, oReadOnly for a blocking read + end. 0 on success, -1 if the path already exists or its parent is not a + writable directory. */ +long userMkfifo(const char* path); +/* argv is a NULL-terminated array of C strings, exactly like execv(3)'s -- + pass nullptr for "no arguments" (argc 0 on the other side). */ +long userExec(const char* path, char* const argv[]); +long userSpawn(const char* path, char* const argv[]); +long userWait(long pid); + +/* waitpid(-1)-style "wait for ANY child, not one specific pid": reaps + whichever child of the caller happens to already be dead (or blocks, the + same real blocking semantics as userWait(pid), until one dies) and + returns its exit code. Writes the reaped child's own pid to *outPid + (ignored if NULL). Returns -1 immediately, without blocking, if the + caller currently has no children at all, dead or alive -- the real + waitpid(-1)'s ECHILD case. See user/waitanytest.c for a live demo. */ +long userWaitAny(long* outPid); + +/* Unconditionally terminates another process by pid -- the only way to + recover a process genuinely stuck forever (e.g. blocked in userRead on a + shared pipe whose writer never shows up, see userPipeAttach). 0 on + success, -1 for an invalid pid, an already-dead one, or your own. The + killed process gets no chance to run any cleanup -- its parent's + userWait still reports its exit status, which will be exactly 9. Unlike + userSigSend below, this can never be caught, blocked, or ignored -- + internally it goes straight to threadKill (sched.c), the same as + userSigSend(pid, sigKill) does (signal.h). + + Real kill(2)'s own pid<=0 convention (sched.h's Thread.pgid) also + applies here: pid==0 kills every OTHER member of the caller's own + process group (never the caller itself -- a Running thread is never one + of threadKill's accepted states, the same reason plain self-targeting + already does nothing special); pid<0 kills every member of group -pid. + 0 is still returned for success as long as AT LEAST ONE target thread + was actually killed (real kill(2)'s own ESRCH-if-nobody-matched + semantics for the group form). */ +long userKill(long pid); + +/* Real, catchable signal delivery (signal.h) -- see that header for the + full design (a shared trampoline page, delivery on return-to-ring3, + sysSigReturn to resume exactly where a handler interrupted). */ + +/* Raises signal `signum` (signal.h's sigHup..sigTstp) against `pid`. 0 on + success, -1 for a bad signum or an invalid/dead/unused target. sigKill + is delegated straight to the same unconditional path userKill uses -- + it is never caught, blocked, or ignored, matching real kill(2). Any + other signal is only actually delivered once the target next returns to + user code on its own (this kernel does not wake a sleeping/blocked + target early -- see signal.h's file comment). + + Same pid<=0 group convention as userKill above: pid==0 targets the + caller's own process group (INCLUDING itself, unlike userKill's group + form -- sigRaiseInfo has no "never self" exclusion the way threadKill + does), pid<0 targets group -pid. See sched.h's Thread.pgid and + signal.h's sigRaiseGroup for the full design, and user/pgroupfg.c/ + user/pgroupbg.c for a live demo of the SAME scoping rule applied by + keyboard.c's own real Ctrl-C. */ +long userSigSend(long pid, int signum); void print(const char* text); void printDec(long value); unsigned long stringLength(const char* text);