From 25f50f156a315593f0f65c5e573833e6b73f43a3 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 20:49:05 +0200 Subject: [PATCH] feat: add pidfd and pipe --- include/pidfd.h | 67 ++++++++++++++++++++++++ include/pipe.h | 134 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 include/pidfd.h create mode 100644 include/pipe.h diff --git a/include/pidfd.h b/include/pidfd.h new file mode 100644 index 0000000..f6adf89 --- /dev/null +++ b/include/pidfd.h @@ -0,0 +1,67 @@ +#ifndef FROSTBYTE_PIDFD_H +#define FROSTBYTE_PIDFD_H + +#include "types.h" + +#define maxPidfds 32 + +/* + * Real Linux precedent: pidfd_open(2) (Linux 5.3+) exists for exactly the + * ABA-style race sched.c's own allocSlot doc comment describes and + * deliberately leaves unfixed for plain pid arguments -- killtest.c's own + * comment already admits it ("killing it again must fail cleanly, not + * resurrect a slot that has since been reused") without ever guarding + * against it. A raw numeric pid can always be reassigned to a completely + * unrelated process once its original holder is reaped, so a pid cached + * from an earlier spawn/lookup is fundamentally unsafe to act on later + * with no way to tell "still the same process" apart from "coincidentally + * the same number now". Real Linux's own fix was not to make pid itself + * opaque -- every existing kill(2)/waitpid(2)/setpgid(2) caller still + * takes a raw pid, completely unchanged, forever -- but to add a SEPARATE, + * opt-in handle type that pins one specific incarnation of a slot instead. + * This file mirrors that shape exactly: pidfdOpen captures the target + * thread's CURRENT sched.c Thread.generation (the same "distinguish this + * specific occupant from any future occupant of the same numeric slot" + * idea pipe.c's own pipeIncarnation already uses for pipe handles), and + * every later pidfd operation re-checks it before ever touching the + * target -- if the original thread has since been reaped (its generation + * reset to 0 by threadReap/threadDiscard's memset) or the slot has been + * handed to a brand new, unrelated thread (a different, fresh nonzero + * generation), the pidfd cleanly reports "gone" (threadInvalidId) instead + * of silently operating on whichever live thread now happens to occupy + * that slot number. sched.c's raw pid-based threadKill/threadJoin/ + * sigRaiseInfo/threadSetPgid/threadSetPriority are deliberately untouched + * by this file -- see allocSlot's own doc comment (sched.c) for why that + * remains a separate, much larger, deferred topic. + */ + +/* Opens a new pidfd handle on thread `targetId`, pinning its CURRENT + generation (sched.h). Returns a small non-negative handle for + pidfdTarget/pidfdClose below, or -1 for id 0 (idle, never a valid + target), an out-of-range id, an already-unused slot, or a full pidfd + table (maxPidfds handles system-wide, mirroring pipe.c's own fixed-size + maxPipes table). Does not itself keep the target alive or pinned in any + way -- it can still exit, be killed, and be reaped by anyone (this + kernel's pids are never ownership-scoped, see sched.h's parentId doc + comment) while a pidfd on it is open; that is exactly the case + pidfdTarget below exists to detect. */ +int pidfdOpen(uint32 targetId); + +/* Returns the live target thread id this handle still safely refers to, + or threadInvalidId if the handle itself is invalid/closed OR the + original target has been reaped or recycled since pidfdOpen -- the one + check every pidfd-based operation needs, folded into a single call so + no caller can accidentally act on a stale target by forgetting a + separate validity check. A target that has exited but not yet been + reaped (threadDead) still returns its real id here -- exactly like a + raw pid, waiting on it should still be able to observe the real exit + status once, via whatever reaps it. */ +uint32 pidfdTarget(int handle); + +/* Frees a pidfd handle. No-op on an already-invalid handle. Never affects + the target thread itself in any way -- a pidfd, like a real Linux one, + is purely an observer/signaler, not a reference that keeps anything + alive. */ +void pidfdClose(int handle); + +#endif diff --git a/include/pipe.h b/include/pipe.h new file mode 100644 index 0000000..48ba863 --- /dev/null +++ b/include/pipe.h @@ -0,0 +1,134 @@ +#ifndef FROSTBYTE_PIPE_H +#define FROSTBYTE_PIPE_H + +#include "types.h" + +#define maxPipes 32 +#define pipeBufferSize 256 + +/* + * Anonymous pipes: a fixed table of ring-buffer-backed byte queues, each + * shared between one read end and one write end. The raw pipeRead/pipeWrite + * primitives are always non-blocking, matching real O_NONBLOCK semantics: + * + * pipeRead: + * >0 bytes actually read + * 0 end-of-file: buffer drained AND the write end is closed + * -1 no data right now but the write end is still open (retry later) + * + * pipeWrite: + * >0 bytes actually written + * 0 length was 0 + * -1 broken pipe (no read end left), or no free space right now + * (retry later) + * + * pipeReadBlocking/pipeWriteBlocking are built on top: they park the calling + * kernel thread on the pipe's wait channel (sched.h) instead of returning + * "try again", and are woken as soon as the other end changes the condition + * (data arrives, space frees up, or either end closes). They only make sense + * from kernel-thread context (see the producer/consumer demo in kmain.c) - + * a single process's own fd table cannot hand one pipe end to a second, + * independent execution context by itself. + * + * pipeCreateDetached + pipeAddReader/pipeAddWriter (below) close that gap + * without needing fork(): a handle from pipeCreateDetached starts with + * NEITHER end attached (unlike pipeCreate, which assumes its caller + * immediately owns both), so it can be handed to two entirely separate, + * independently spawned processes -- typically as a decimal string in argv + * (see user/fifomaster.c) -- each of which attaches its own end via + * vfsPipeAttach (vfs.h). syscall.c's sysRead/sysWrite give a real blocking + * read()/write() to whichever of those two ends is waiting, using the same + * restart-the-syscall trick sysWait already uses for threadJoin. + */ + +int pipeCreate(void); + +/* Like pipeCreate, but neither end starts "attached": readers and writers + both begin at 0, and pipeRead/pipeWrite treat "nobody has attached that + end YET" as "try again" rather than the true EOF/broken-pipe result they + report once an end that WAS attached goes away (see everHadReader/ + everHadWriter, pipe.c) -- otherwise a writer that attaches before any + reader does would see an immediately, permanently "broken" pipe, and a + reader that attaches first would see immediate spurious EOF. */ +int pipeCreateDetached(void); + +/* Registers a new, independent reader/writer on an existing (typically + pipeCreateDetached'd) handle -- vfsPipeAttach (vfs.h) calls these when a + process attaches its end. No-op on an invalid handle. */ +void pipeAddReader(int handle); +void pipeAddWriter(int handle); + +/* True if a write to this pipe can never succeed again: either the handle + is not valid, or a reader was attached at some point and every attached + reader has since gone away. False for a pipeCreateDetached handle that + simply has not had its reader attach yet -- that is "try again", not + broken. Lets syscall.c's sysWrite tell "keep retrying" apart from "give + up and return -1" without reaching into pipe.c's own Pipe struct. */ +bool pipeWriteBroken(int handle); + +/* The address schedBlockCurrent/schedWakeChannel use for this pipe's wait + queue -- the same one pipeReadBlocking/pipeWriteBlocking already block + on. Exposed so a syscall handler (syscall.c) can park on it via the + restart-the-syscall pattern instead of calling halt() directly (which, + run from inside an interrupt-gate syscall handler with IF=0, would freeze + the whole machine rather than just the calling thread -- see sysWait's + own doc comment in syscall.c for the same hazard). Returns nullptr for + an invalid handle. */ +const void* pipeChannel(int handle); + +bool pipeValid(int handle); + +/* Opaque token identifying THIS specific pipe living at `handle`'s slot, + distinct from any other pipe that has occupied (or will ever occupy) the + same numeric handle before or after -- this fixed-size table reuses a + slot number the moment both its ends are gone (pipeCloseRead/ + pipeCloseWrite's pipeReleaseIfUnused), so pipeValid(handle) alone cannot + tell a caller that cached a handle across such a release "the same pipe I + bound to earlier" apart from "a different, unrelated pipe that happens to + have been assigned the same number since" (see vfs.c's SFS-backed named + fifo bindings, sfsFifoHandleFor, the motivating caller). Always non-zero + for any handle pipeCreate/pipeCreateDetached has ever handed out; 0 for + an invalid handle, which can never equal a real incarnation, so a naive + "compare and treat 0 as impossible" check is always safe. */ +uint32 pipeIncarnation(int handle); + +int64 pipeRead(int handle, void* buffer, uint64 length); +int64 pipeWrite(int handle, const void* buffer, uint64 length); + +/* Non-consuming readiness peeks for poll.h's vfsPollReady (vfs.c): report + exactly the same two conditions pipeRead/pipeWrite's own success paths + check (data queued or true EOF; free space in the ring), without moving + a single byte or touching readers/writers bookkeeping. False for an + invalid handle either way. */ +bool pipeHasData(int handle); +bool pipeHasSpace(int handle); + +/* Blocks (parks the calling thread and halts) until data is available or + true end-of-file is reached. Returns exactly what pipeRead would once the + wait is over; -1 only for an invalid handle (never blocks in that case). */ +int64 pipeReadBlocking(int handle, void* buffer, uint64 length); + +/* Blocks until free space is available. Returns exactly what pipeWrite would + once the wait is over; returns -1 immediately without blocking for an + invalid handle or a broken pipe (no reader left), since neither condition + can ever resolve by waiting. */ +int64 pipeWriteBlocking(int handle, const void* buffer, uint64 length); + +void pipeCloseRead(int handle); +void pipeCloseWrite(int handle); + +/* Frees a pipeCreateDetached handle that nobody has ever attached a reader + or writer to (readers==0 and writers==0) -- the one case pipeCloseRead/ + pipeCloseWrite's own release-when-both-zero check (pipeReleaseIfUnused) + can never reach on its own, since neither is ever called for an end that + was never attached in the first place. Needed by a named pipe (vfs.h's + vfsMkfifo/vfsUnlink) created but never opened by anyone: without this, its + underlying handle would sit "used" in the fixed-size pipe table forever + once the vfs node naming it is removed. No-op (and reports false) if the + handle is invalid or still has a reader or writer attached -- unlinking a + named pipe that IS currently open must leave it alone, matching real + unlink-while-open semantics (existing fds keep working off the raw handle + number, not the vfs node). */ +bool pipeDestroyIfUnattached(int handle); + +#endif