From a8819f99c6e6481dcd85c1eef6db64214a0def63 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 8 Sep 2026 23:28:34 +0200 Subject: [PATCH] feat: add jobctlfg and jobtarget --- user/jobctlfg.c | 54 ++++++++++++++++++++++++++++++++++ user/jobtarget.c | 75 +++++++++++++++++++++++++++++++++++++++++++++++ user/killvictim.c | 38 ++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 user/jobctlfg.c create mode 100644 user/jobtarget.c create mode 100644 user/killvictim.c diff --git a/user/jobctlfg.c b/user/jobctlfg.c new file mode 100644 index 0000000..efae425 --- /dev/null +++ b/user/jobctlfg.c @@ -0,0 +1,54 @@ +#include "ulib.h" + +/* + * Never spawned directly by kmain.c the way every other boot demo in this + * kernel is -- it just sits in /bin like any other binary until kmain.c's + * jobctlDriverThread types "jobctlfg" at the REAL interactive shell + * (shell.c) through injected keystrokes (same "no automated way to drive a + * real PS/2 device under headless QEMU" workaround every other keyboard + * demo in this file already uses). shell.c's runProgram falls back to + * jobctlSpawnForeground (jobctl.c) for any word that isn't one of its + * built-ins, which is the first time this kernel has ever loaded a binary + * on demand from user input rather than eagerly at boot -- a real shell's + * $PATH lookup, not another pre-arranged spawnUser call. + * + * jobctlSpawnForeground makes this process its own new group leader and + * calls keyboardSetForegroundPgid on its behalf BEFORE the shell blocks + * waiting for it (jobctlWaitForeground) -- so this file's proof is exactly + * pgroupfg.c's own (an uncaught, real, injected Ctrl-C actually reaches + * "the foreground" and terminates it by default disposition), but now + * "the foreground" was granted by the SHELL's own runtime job-control + * decision instead of a single hardcoded kmain.c call. No handler + * installed for sigInt at all: the real-world default disposition + * (signal.c's sigDefaultIsIgnore does not list sigInt) is Terminate, + * exactly like a real uncaught Ctrl-C killing a foreground program. + */ + +int main(void) +{ + print("jobctlfg: start, pid="); + printDec(userGetpid()); + print("\n"); + + long victimHandle = userPipeCreate(); + if (victimHandle < 0) { + print("jobctlfg: FAIL create victim pipe\n"); + return 1; + } + long victimFd = userPipeAttach(victimHandle, 0); + if (victimFd < 0) { + print("jobctlfg: FAIL attach victim pipe\n"); + return 1; + } + + print("jobctlfg: ready, blocking on go pipe\n"); + char buffer[4]; + userRead((int)victimFd, buffer, sizeof(buffer)); + + /* Only reached if userRead somehow returned on its own -- nothing ever + writes to the victim pipe, so the shell-granted foreground Ctrl-C + never arriving (or arriving but not actually terminating this + process) is the only way to get here. */ + print("jobctlfg: FAIL read returned without being interrupted\n"); + return 1; +} diff --git a/user/jobtarget.c b/user/jobtarget.c new file mode 100644 index 0000000..f344c3a --- /dev/null +++ b/user/jobtarget.c @@ -0,0 +1,75 @@ +#include "ulib.h" + +/* + * Only ever spawned by jobtest.c, with two shared pipe handles as argv[1] + * (the "ready" pipe, this process writes) and argv[2] (the "go" pipe, this + * process reads) -- decToString'd exactly like fifomaster.c's/sigtarget.c's + * children. Proves job control (signal.h's SIGSTOP/SIGCONT) works end to + * end against a thread that is genuinely Blocked mid-syscall, not just + * Runnable: this process parks in a real blocking userRead on the go pipe, + * jobtest.c stops it there with a real userSigSend(target, sigStop) (not a + * kernel-internal shortcut), writes the byte it is waiting for WHILE it is + * stopped, and only then sends sigCont -- the read can only ever complete + * through the exact restart-syscall retry loop sysRead already uses + * (syscall.c), which only runs again once threadContinue has moved this + * thread back to threadRunnable. + * + * No sleep/timing guess anywhere in this file or jobtest.c: every step is + * ordered by a real blocking read or a real userWait, the same + * happens-before discipline sigtarget.c's rendezvous pipe already + * established for a different race. + */ + +int main(int argc, char** argv) +{ + print("jobtarget: start, pid="); + printDec(userGetpid()); + print("\n"); + + if (argc != 3) { + print("jobtarget: FAIL expected two argv (ready handle, go handle)\n"); + return 1; + } + + long readyHandle = parseDec(argv[1]); + long goHandle = parseDec(argv[2]); + + long goFd = userPipeAttach(goHandle, 0); + if (goFd < 0) { + print("jobtarget: FAIL attach go pipe as reader\n"); + return 1; + } + + long readyFd = userPipeAttach(readyHandle, 1); + if (readyFd < 0) { + print("jobtarget: FAIL attach ready pipe as writer\n"); + return 1; + } + if (userWrite((int)readyFd, "r", 1) != 1) { + print("jobtarget: FAIL write ready byte\n"); + return 1; + } + userClose((int)readyFd); + print("jobtarget: ready, blocking on go pipe\n"); + + char goByte = 0; + /* Real blocking read: parks this thread (threadBlocked) until a byte is + actually available. jobtest.c raises SIGSTOP against this exact + blocking read, then writes the byte, then raises SIGCONT -- the read + below can only return once BOTH have happened, since sysRead's own + restart-syscall retry (syscall.c) never runs again while this thread + is threadStopped instead of threadRunnable. */ + if (userRead((int)goFd, &goByte, 1) != 1) { + print("jobtarget: FAIL read go byte\n"); + return 1; + } + userClose((int)goFd); + + if (goByte != 'g') { + print("jobtarget: FAIL wrong go byte\n"); + return 1; + } + print("jobtarget: received go\n"); + + return 55; +} diff --git a/user/killvictim.c b/user/killvictim.c new file mode 100644 index 0000000..1af5fc1 --- /dev/null +++ b/user/killvictim.c @@ -0,0 +1,38 @@ +#include "ulib.h" + +/* + * Only ever spawned by killtest.c. Attaches as the READ end of a shared + * pipe (userPipeCreate/userPipeAttach) that no writer will ever attach to, + * so the userRead call below can only ever return by being killed -- + * proving threadKill (sched.c) genuinely recovers a process that would + * otherwise be stuck forever now that sysRead can really block (see + * pipe.h/vfs.h's shared-pipe machinery, added the same wave as this). + */ + +int main(void) +{ + print("killvictim: start, pid="); + printDec(userGetpid()); + print("\n"); + + long handle = userPipeCreate(); + if (handle < 0) { + print("killvictim: FAIL pipe create\n"); + return 1; + } + + long fd = userPipeAttach(handle, 0); + if (fd < 0) { + print("killvictim: FAIL attach\n"); + return 1; + } + + print("killvictim: blocking on read forever\n"); + char buffer[4]; + userRead((int)fd, buffer, sizeof(buffer)); + + /* Only reached if userRead somehow returned on its own -- killtest.c + never writes to this pipe, so that would itself be a bug. */ + print("killvictim: FAIL read returned without being killed\n"); + return 1; +}