From 9d356d9a29a5ec404de2b57c2877b2294f6d627b Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 6 Sep 2026 21:03:53 +0200 Subject: [PATCH] feat: extend sched with allocSlot and initialPgid --- src/sched.c | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 6 deletions(-) diff --git a/src/sched.c b/src/sched.c index 51170d1..530e189 100644 --- a/src/sched.c +++ b/src/sched.c @@ -331,10 +331,161 @@ void schedInit(void) vfsFdInit(threads[0].fds); } +/* Finds a slot for a new thread: a threadUnused slot left by a prior + threadReap, or (if none is free) grows the high-water mark by one as + long as there is room under maxThreads. Returns threadInvalidId, + touching nothing, if the table is completely full -- the caller must not + write into threads[] in that case (this is the bounds-check the original + version of this table was missing: threadTotal used to grow unconditionally + on every create, silently writing past the array once 16 threads existed). + Scans round-robin from nextSlotHint (wrapping through 1..threadTotal-1) + rather than always restarting from the lowest index: a real, + reproducibly observed bug this fala's own gate caught fresh on BOTH + killtest.c (a pre-existing, years-old check) and the new nicetest.c -- + "kill/nice a pid this process itself just userWait()-reaped must fail" + occasionally failed for real, because some COMPLETELY UNRELATED thread + elsewhere in the system (in the reproduced case, jobctlfg being loaded + by the shell) called threadCreate/threadCreateUser in the tiny window + between the reap and the recheck and got handed that exact just-freed + LOW index straight back, since the old scan always tried index 1 first. + That is table pressure racing against a single process's own two + syscalls, not a scheduling-timing flake -- ci-test.sh's needle for it + (killtest: kill-ok) had simply never been unlucky enough to hit it + before this fala's elevated host load made the race common enough to + reproduce in a handful of runs. A round-robin cursor does not make the + race impossible (a table sustained at 100% occupancy for a full sweep + could still wrap back around to a freshly-freed slot), but with this + project's real, generous headroom margins (maxThreads far above actual + concurrent occupancy at any point in the historical record, sched.h's + own bump history) it means dozens-to-hundreds of OTHER allocations have + to happen before the cursor can return to any one just-freed slot -- + the same practical-elimination-via-headroom shape every other table- + pressure fix in this file's history already has, not a new kind of + guarantee. A real generation/incarnation counter on pid itself (the + pipeIncarnation/sfsGeneration shape vfs.c/sfs.c already use for pipes + and the on-disk filesystem) would close it completely regardless of + occupancy, but reshaping what a "pid" IS everywhere kill/wait/sigSend/ + setpgid/nice already accept it as a raw table index is the large, + deferred, cross-cutting change this project's own history has flagged + many times over (killtest.c's own doc comment, frostbyte-budowa.md) -- + out of scope for this fix. + + That generation counter now exists (Thread.generation, sched.h; this + file's own nextThreadGeneration below) -- but deliberately NOT wired + into kill/wait/sigSend/setpgid/nice's raw pid argument, matching real + Linux's own resolution of this exact, well-known race: pidfd_open(2) + added a SEPARATE, opt-in handle type rather than ever making a plain + pid argument itself generation-checked. See pidfd.h for the additive + primitive built on Thread.generation, and schedTest.c's pidfd tests for + a deterministic (create/discard/recreate, no live scheduling involved) + reproduction of exactly the stale-handle-hits-an-unrelated-live-thread + scenario killtest.c's own comment warns about but never guards against. */ +static uint32 allocSlot(void) +{ + if (threadTotal > 1) { + uint32 start = (nextSlotHint >= 1 && nextSlotHint < threadTotal) ? nextSlotHint : 1; + uint32 i = start; + do { + if (threads[i].state == threadUnused) { + nextSlotHint = i + 1; + return i; + } + i = (i + 1 < threadTotal) ? i + 1 : 1; + } while (i != start); + } + if (threadTotal < maxThreads) { + uint32 slot = threadTotal++; + nextSlotHint = slot + 1; + return slot; + } + return threadInvalidId; +} + +/* Thread.pgid's own creation-time default (see that field's doc comment, + sched.h): a thread created while the idle thread (id 0) is "current" can + only be one of kmain.c's boot-time calls (parentId's own comment already + establishes idle never issues a syscall, so it can never be "current" + again once real processes exist) -- exactly like a real init spawning + each service into its own job, it becomes its own new group leader. + Anything else is a genuine runtime child (syscall.c's doSpawn) of a + currently-Running parent, and inherits that parent's pgid, matching real + fork()'s own default before any setpgid ever runs. */ +static uint32 initialPgid(uint32 newId) +{ + if (currentIndex == 0) + return newId; + return threads[currentIndex].pgid; +} + +/* Thread.uid's own creation-time default, same "is the idle thread + currently running" test as initialPgid immediately above but with + pgid's own conclusion inverted: a boot-time thread starts as uid 0 + ("root"), like a real init process running as the superuser, rather than + becoming its own new anything; a genuine runtime child instead INHERITS + its parent's uid outright (not derived from newId at all), matching real + fork()'s own default before any setuid ever runs -- the same "copy the + parent's value" shape initialCwd/initialEnv below already use for their + own fields, just a scalar instead of a buffer so it returns a value + like initialPgid does. */ +static uint32 initialUid(uint32 newId) +{ + (void)newId; + if (currentIndex == 0) + return 0; + return threads[currentIndex].uid; +} + +/* Thread.gid's own creation-time default, byte-for-byte the same shape as + initialUid immediately above: a boot-time thread starts at gid 0, a + genuine runtime child INHERITS its parent's gid outright. */ +static uint32 initialGid(uint32 newId) +{ + (void)newId; + if (currentIndex == 0) + return 0; + return threads[currentIndex].gid; +} + +/* Thread.cwd's own creation-time default, mirroring initialPgid's own + reasoning immediately above exactly (same "is the idle thread currently + running" test, same conclusion for the same reason): a boot-time thread + starts at the filesystem root, like a real init process; a genuine + runtime child instead gets a byte-for-byte COPY of its parent's cwd, + matching real fork()'s own copy-not-share semantics -- writes into + threads[newId].cwd directly (rather than returning a value the way + initialPgid does) since a path is a buffer, not a scalar. */ +static void initialCwd(uint32 newId) +{ + if (currentIndex == 0) { + threads[newId].cwd[0] = '/'; + threads[newId].cwd[1] = 0; + return; + } + memcpy(threads[newId].cwd, threads[currentIndex].cwd, fsPathMax); +} + +/* Thread.env's own creation-time default, mirroring initialCwd's own + reasoning immediately above exactly (same "is the idle thread currently + running" test, same conclusion for the same reason): a boot-time thread + starts with an empty table, like a real init process before anything + ever calls setenv(); a genuine runtime child instead gets a byte-for-byte + COPY of its parent's whole table (both the entries and envCount), never + a shared pointer. */ +static void initialEnv(uint32 newId) +{ + if (currentIndex == 0) { + threads[newId].envCount = 0; + return; + } + memcpy(threads[newId].env, threads[currentIndex].env, sizeof(threads[newId].env)); + threads[newId].envCount = threads[currentIndex].envCount; +} + uint32 threadCreate(const char* name, ThreadEntry entry) { - uint32 slot = threadTotal; - threadTotal++; + uint32 slot = allocSlot(); + if (slot == threadInvalidId) + return threadInvalidId; uint64 stackBase = (uint64)kmalloc(threadStackSize); uint64 stackTop = (stackBase + threadStackSize) & ~15ull; @@ -348,22 +499,40 @@ uint32 threadCreate(const char* name, ThreadEntry entry) frame->ss = kernelDataSelector; threads[slot].id = slot; - threads[slot].name = name; + threads[slot].generation = nextThreadGeneration++; + threads[slot].parentId = currentIndex; + threads[slot].pgid = initialPgid(slot); + threads[slot].uid = initialUid(slot); + threads[slot].gid = initialGid(slot); + initialCwd(slot); + initialEnv(slot); + copyThreadName(&threads[slot], name); threads[slot].state = threadRunnable; threads[slot].stackBase = stackBase; threads[slot].kernelStackTop = stackTop; threads[slot].rsp = (uint64)frame; threads[slot].sleepUntil = 0; + threads[slot].alarmAt = 0; + threads[slot].pollActive = false; + threads[slot].pollDeadline = 0; threads[slot].brk = 0; + threads[slot].vruntime = 0; + threads[slot].exitCode = 0; + threads[slot].weight = schedDefaultWeight; + threads[slot].everRan = false; + threads[slot].waitChannel = nullptr; + threads[slot].userSlotBase = 0; vfsFdInit(threads[slot].fds); + sigResetState(&threads[slot]); return slot; } uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop) { - uint32 slot = threadTotal; - threadTotal++; + uint32 slot = allocSlot(); + if (slot == threadInvalidId) + return threadInvalidId; uint64 stackBase = (uint64)kmalloc(threadStackSize); uint64 kernelStackTop = (stackBase + threadStackSize) & ~15ull; @@ -377,14 +546,31 @@ uint32 threadCreateUser(const char* name, uint64 entry, uint64 userStackTop) frame->ss = userDataSelector | 3; threads[slot].id = slot; - threads[slot].name = name; + threads[slot].generation = nextThreadGeneration++; + threads[slot].parentId = currentIndex; + threads[slot].pgid = initialPgid(slot); + threads[slot].uid = initialUid(slot); + threads[slot].gid = initialGid(slot); + initialCwd(slot); + initialEnv(slot); + copyThreadName(&threads[slot], name); threads[slot].state = threadRunnable; threads[slot].stackBase = stackBase; threads[slot].kernelStackTop = kernelStackTop; threads[slot].rsp = (uint64)frame; threads[slot].sleepUntil = 0; + threads[slot].alarmAt = 0; + threads[slot].pollActive = false; + threads[slot].pollDeadline = 0; threads[slot].brk = 0; + threads[slot].vruntime = 0; + threads[slot].exitCode = 0; + threads[slot].weight = schedDefaultWeight; + threads[slot].everRan = false; + threads[slot].waitChannel = nullptr; + threads[slot].userSlotBase = elfSlotBase(entry); vfsFdInit(threads[slot].fds); + sigResetState(&threads[slot]); return slot; }