From baece1a2b0b3356512d931b190cbd43b8ec71baa Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 20:02:02 +0200 Subject: [PATCH] feat: expand ulib --- user/ulib.h | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) diff --git a/user/ulib.h b/user/ulib.h index 6060d43..a37970d 100644 --- a/user/ulib.h +++ b/user/ulib.h @@ -222,8 +222,211 @@ long userKill(long pid); 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); + +/* Real pidfd_open(2): opens a handle on process `pid` that stays correct + even if `pid`'s own numeric slot is later reaped and handed to a + completely different, unrelated process (pidfd.h/sched.h's + Thread.generation) -- unlike userKill/userSigSend/userWait above, which + all still take a raw pid the exact same way real kill(2)/waitpid(2) do + (and so share their exact same well-known pid-reuse race, deliberately + left unfixed for them, see sched.c's allocSlot doc comment). Returns a + small non-negative fd on success (closed with the ordinary userClose), + or -1 for pid 0, an out-of-range pid, an already-dead-and-reaped or + never-existed pid, or a full pidfd table. Opening a pidfd does NOT keep + the target alive in any way -- it can still exit and be reaped by + anyone else in the meantime, exactly like a raw pid could; the whole + point of this handle is only ever noticing that when it happens, instead + of silently acting on whoever now sits at that same number. */ +long userPidfdOpen(long pid); + +/* Sends signal `signum` to whichever process `fd` (from userPidfdOpen) + still safely refers to. 0 on success, -1 for a bad signum, an `fd` that + is not an open pidfd at all, or one whose original target has since + been reaped or recycled -- the failure userSigSend(cachedPid, signum) + can never distinguish from "the signal was delivered to a live but + completely unrelated process that happens to share that old pid now". + Otherwise identical semantics to userSigSend's own single-target path + (sigKill still unconditional and uncatchable, signum still delivered on + the target's own next return to user code). */ +long userPidfdSend(long fd, int signum); + +/* Real (partial) waitid(2)-over-a-pidfd: blocks until the process `fd` + still safely refers to actually exits, then reaps it and returns its + exit code -- the same real blocking semantics as userWait(pid), except + -1 also covers "the original target was reaped by someone else while + this call was waiting", which userWait(pid) has no way to report (it + would simply keep waiting on whichever unrelated thread now occupies + that pid instead). See user/pidfdtest.c for a live ring-3 demonstration + of a pidfd correctly reporting a just-reaped target as gone through the + real syscall path (not just the in-kernel unit tests), and + schedTest.c's pidfd tests for a deterministic, create/discard/recreate + reproduction of the full scenario -- a stale handle whose slot has + since been handed to a brand new, unrelated, live thread -- that a live + boot cannot reliably force on demand (allocSlot's own round-robin + headroom makes hitting one specific freed slot again take dozens-to- + hundreds of unrelated allocations first, see its doc comment, sched.c). */ +long userPidfdWait(long fd); + +/* Real mmap(2): addrHint of 0 asks the kernel to pick the address itself + (mmap.c's mmapReserve, first-fit inside this process's own mmap region); + a nonzero addrHint asks for that EXACT address, refused (not silently + relocated) if it does not fit -- there is no MAP_FIXED bit here, every + addrHint already behaves like real mmap's own weaker "hint" narrowed + down to "exact or refused". prot is fs.h's mmapProtRead/Write/Exec + (mmapProtExec IS hardware-enforced -- executing a mapping that never + asked for it takes a real SIGSEGV -- see fs.h's own doc comment for the + exact scope, including the CPU-support gate and the same enforcement + applied to a binary's own ELF image and its stack/brk); flags is fs.h's + mmapAnonymous or 0. When mmapAnonymous is NOT + set, `fd` must already be open on a real seekable file (fdFile/fdSfs/ + fdBlockDev -- the same three kinds userLseek's own vfsLseek already + restricts to) and `offset` must be frameSize-aligned; both are ignored + for an anonymous mapping. No physical memory is actually touched until + the first real access to a given page -- see mmap.h's own file comment + for the full demand-paging contract, including why closing `fd` before + every page has been faulted in leaves the rest of this mapping unable + to fill (an honestly scoped limitation, not a hidden bug). Returns the + mapped base address, or (void*)-1 (compare against userMmapFailed) on + any failure: bad alignment, a range outside this process's own mmap + region, overlap with an existing mapping, a bad/wrong-kind fd for a + file-backed mapping, or no mapping slot/free region left. */ +void* userMmap(void* addrHint, unsigned long length, int prot, int flags, int fd, long offset); + +/* Sentinel userMmap returns on failure, real MAP_FAILED's own value -- + never a real mapped address, since every real one falls inside this + process's own bounded 256 MiB elfSlotSize slot (elf.h), nowhere near + the top of the address space. */ +#define userMmapFailed ((void*)-1) + +/* Real munmap(2): see mmap.h's own mmapRelease for the full contract + (partial unmap shrinks the mapping in place, a range entirely inside + one mapping's interior splits it, already-unmapped memory is a no-op). + 0 on success, -1 for a misaligned addr, a zero length, or (a real, + if unlikely, resource failure) no free mapping slot left to hold a + split's own second half. */ +long userMunmap(void* addr, unsigned long length); + +/* Installs `handler` as the calling process's own disposition for + `signum` (nullptr/sigHandlerDefault restores the default action, + sigHandlerIgnore silently discards it), `mask` as its sa_mask -- the + additional signals blocked for as long as THIS handler is running (on top + of whatever was already blocked and signum's own bit, always self-blocked + like real Unix's implicit default) -- and `flags` as its sa_flags + (signalnum.h's sigFlagRestart, or 0). A signal NOT covered by `mask` can + still interrupt this handler with a genuinely nested one of its own + (see signal.h's file comment) rather than waiting for it to return. + Without sigFlagRestart, a signal that interrupts THIS process blocked in + userRead/userWrite/userWait makes that call return errInterrupted once + the handler returns instead of transparently resuming it (real EINTR); + sigFlagRestart opts back into the transparent-resume behavior every + handler had before this flag existed. Returns the PREVIOUS disposition, + or -1 for a bad signum or an attempt to touch sigKill (uncatchable, like + real EINVAL on SIGKILL). `handler` is called with the signal number as + its only argument; it must return normally (not userExit, not + longjmp-style tricks this kernel does not support) for execution to + resume exactly where the signal interrupted. */ +long userSigAction(int signum, SigHandler handler, unsigned long mask, unsigned long flags); + +/* Identical wire format to userSigAction (same syscall, same signum/mask + validation, same PREVIOUS-disposition return value) -- the only + difference is `handler`'s real C type, SigInfoHandler instead of + SigHandler, so a caller does not need its own cast at the call site. The + kernel itself does not distinguish the two function-pointer shapes at + all (it only ever stores/calls a raw address, see signal.c's + sigConsumePending): what actually determines whether the handler is + CALLED with one argument or three is whether `flags` includes + sigFlagSigInfo (signalnum.h) -- the caller must include it here + deliberately, exactly like real sigaction()'s SA_SIGINFO. Passing a + SigInfoHandler through this function WITHOUT that flag set (or, equally, + passing a plain SigHandler through this function WITH it set) is a + caller bug this kernel does not protect against, mirroring real C's own + union-based sa_handler/sa_sigaction footgun -- the handler would run + with whatever raw rsi/rdx happened to hold, not a valid SigInfo + pointer. */ +long userSigActionInfo(int signum, SigInfoHandler handler, unsigned long mask, unsigned long flags); + +/* Blocks (sigMaskBlock), unblocks (sigMaskUnblock), or replaces + (sigMaskSetMask) the calling process's blocked-signal mask, returning + the mask as it was immediately before this call. A blocked signal still + gets raised (sigPending) but is not delivered until unblocked. */ +unsigned long userSigProcMask(int how, unsigned long mask); + +/* Real setpgid(2): moves process `pid` (0 means the caller itself) into + process group `pgid` (0 means "become a new group leader, using pid's + own resolved id as the group id" -- the same "0 means self/derive from + self" shape sysSetFgPgrp does NOT have, since a foreground group is + named directly, never derived). 0 on success, -1 for a pid that does not + name any currently live thread (sched.h's threadSetPgid; id 0, the idle + thread, is also always rejected -- see that function's own doc comment). + No session/permission check at all -- see threadSetPgid's own honestly- + documented omission. */ +long userSetpgid(long pid, long pgid); + +/* Real getpgid(2): returns process `pid`'s current process group (0 means + the caller itself), or -1 for a pid that does not name any currently + live thread. */ +long userGetpgid(long pid); + +/* Real setpriority(PRIO_PROCESS, pid, ...)/nice(2)'s underlying mechanism: + sets process `pid`'s (0 means the caller itself) CFS-lite scheduling + weight (sched.h's threadSetPriority) to `weight` directly -- higher runs + proportionally more often, exactly like kmain.c's own kernel-internal + workerA/workerB demo already proves for a kernel-created thread, now + reachable for any real userspace process including one the caller did + not create itself. Unlike real nice(), takes an absolute weight rather + than a relative increment, and has no privilege check at all (any + process may renice any other) -- this kernel's already-documented, + honest lack of a uid/permission model (see threadSetPgid's own comment + for the same omission). 0 on success, -1 for a `pid` that does not name + any currently live thread, the idle thread (pid 0's OWN id, not the + `pid == 0` self-convention above), or a `weight` of 0. */ +long userNice(long pid, long weight); + +/* Real getpriority(PRIO_PROCESS, pid)-equivalent: returns process `pid`'s + (0 means the caller itself) current CFS-lite scheduling weight, or -1 + for a pid that does not name any currently live thread -- unambiguous + because a real weight is always > 0 (userNice above always rejects + weight 0, and every thread starts at schedDefaultWeight, sched.h). */ +long userGetPriority(long pid); + +/* Real time(NULL): seconds since the Unix epoch, off the kernel's real + CMOS RTC (rtc.h/syscall.c's sysTime) -- every other notion of time this + ulib exposes (userSleep, userAlarm) is relative to boot, not the + calendar. Never fails; a hardware read failure comes back as 0 + (rtc.c's rtcNow contract), the same "epoch itself" sentinel real + time(2) would return for 1970-01-01T00:00:00Z. */ +long userTime(void); + +/* Real tcsetpgrp(3): names `pgid` as the process group keyboard.c's real + Ctrl-C/Ctrl-Z (SIGINT/SIGTSTP) is delivered to from now on -- see + keyboard.h's keyboardSetForegroundPgid for the full contract, including + why 0 (before anything ever calls this) safely means "nobody". Always + succeeds (returns 0): unlike a real terminal driver, this kernel has no + session/controlling-terminal ownership to check the caller against. */ +long userSetForegroundPgid(long pgid); + +/* Real tcgetpgrp(3): the pgid userSetForegroundPgid (by anyone) last set, + or 0 if nobody ever has since boot. */ +long userGetForegroundPgid(void); + void print(const char* text); void printDec(long value); unsigned long stringLength(const char* text); +/* Writes value's decimal representation (with a leading '-' if negative) + into out, nul-terminated -- unlike printDec, which prints it directly, + this is for building an argv string to hand a value (e.g. a shared pipe + handle, see userPipeCreate/userSpawn) to a child process. out must have + room for at least 21 bytes (20 digits for INT64_MIN plus the sign, plus + the terminator). */ +void decToString(long value, char* out); + +/* Parses the decimal integer (optionally '-'-prefixed) at the start of + text, stopping at the first non-digit -- the inverse of decToString, for + a child process to recover a value (e.g. a shared pipe handle) passed to + it as an argv string. No error signaling: text that starts with no + digits at all (after an optional sign) parses as 0, matching how a + deliberately small, trusted set of internal callers uses it. */ +long parseDec(const char* text); + #endif