From d589332de4bd4bd2cfec9cef1afd0852d6dd9994 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 19:29:15 +0200 Subject: [PATCH] test: cover proctest and restarttest --- user/proctest.c | 83 ++++++++++++++++++++++++++++++++ user/restarttest.c | 115 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+) create mode 100644 user/proctest.c create mode 100644 user/restarttest.c diff --git a/user/proctest.c b/user/proctest.c new file mode 100644 index 0000000..edfb169 --- /dev/null +++ b/user/proctest.c @@ -0,0 +1,83 @@ +#include "ulib.h" + +/* + * Exercises the process-management syscalls added this wave: sysSpawn + * (posix_spawn()-lite -- loads a binary into a brand new thread) and + * sysWait (threadJoin, sched.c/sched.h) together, then sysExec (execve()- + * lite -- replaces this very process's image in place, see syscall.c + * doExec). Split across two helper binaries deliberately: spawntarget.c is + * only ever spawned (never exec'd), exectarget.c is only ever exec'd + * (never spawned) -- frostbyte has one shared address space system-wide + * (paging.c has a single page table, no per-process CR3), so two binaries + * whose fixed link addresses collide can never be genuinely live at the + * same time. Keeping "the spawn target" and "the exec target" as distinct + * binaries, and waiting for the spawned child before ever calling exec, + * means this test never asks the kernel to do something the architecture + * cannot support. + * + * This function never returns on its own success path: the final call is + * userExec("/bin/exectarget"), which -- if the syscall does its job -- + * replaces this thread's own code/stack/pid identity with exectarget's, so + * "proctest" never gets to print anything past that call. kmain.c's + * execReaperThread separately threadJoin()s this same pid and checks it + * eventually reports exectarget's exit code, which is the only way to + * observe that the exec() actually happened from outside this process. + */ + +int main(void) +{ + print("proctest: start, pid="); + printDec(userGetpid()); + print("\n"); + + long child = userSpawn("/bin/spawntarget", 0); + if (child < 0) { + print("proctest: FAIL spawn /bin/spawntarget\n"); + return 1; + } + + long status = userWait(child); + if (status != 3) { + print("proctest: FAIL wait returned unexpected status\n"); + return 1; + } + print("proctest: spawn+wait ok\n"); + + if (userSpawn("/bin/does-not-exist", 0) != -1) { + print("proctest: FAIL spawn of missing binary should fail\n"); + return 1; + } + print("proctest: spawn bad path rejected\n"); + + if (userWait(999999) != -1) { + print("proctest: FAIL wait on bogus pid should fail immediately\n"); + return 1; + } + print("proctest: wait bad pid rejected\n"); + + /* argtest.c checks its own argc/argv against exactly this list -- + proves the whole chain (copyUserArgv copying these out of THIS + process's memory, elfBuildInitialStack laying them onto the child's + brand new stack, crt.S's _start reading them back off of it) rather + than just that spawn/wait still works with a null argv. */ + char* argtestArgv[] = { "/bin/argtest", "alpha", "beta", "gamma", 0 }; + long argChild = userSpawn("/bin/argtest", argtestArgv); + if (argChild < 0) { + print("proctest: FAIL spawn /bin/argtest\n"); + return 1; + } + + long argStatus = userWait(argChild); + if (argStatus != 0) { + print("proctest: FAIL argtest reported bad argv\n"); + return 1; + } + print("proctest: spawn with argv ok\n"); + + userExec("/bin/exectarget", 0); + + /* Only reached if userExec failed outright (bad path, load failure) -- + a successful exec never returns here at all. */ + print("proctest: FAIL exec did not take over\n"); + return 1; +} diff --git a/user/restarttest.c b/user/restarttest.c new file mode 100644 index 0000000..3acda58 --- /dev/null +++ b/user/restarttest.c @@ -0,0 +1,115 @@ +#include "ulib.h" + +/* + * Boot-time driver for user/restarttarget.c -- see that file's own comment + * for the full two-phase SA_RESTART/EINTR story. Four real happens-before + * rendezvous pipes (ready/announce1/announce2/data), never a sleep guess, + * the same discipline nesttest.c already established for a comparable + * race: read announce1 before sending the no-restart signal (sigUsr1), then + * read announce2 -- which restarttarget.c only ever writes AFTER phase 1's + * read has already come back as errInterrupted and been validated -- before + * sending the restart-flagged signal (sigUsr2) and writing the real data + * byte phase 2 expects to receive back. + */ + +int main(void) +{ + print("restarttest: start, pid="); + printDec(userGetpid()); + print("\n"); + + long readyHandle = userPipeCreate(); + long announce1Handle = userPipeCreate(); + long announce2Handle = userPipeCreate(); + long dataHandle = userPipeCreate(); + if (readyHandle < 0 || announce1Handle < 0 || announce2Handle < 0 || dataHandle < 0) { + print("restarttest: FAIL create pipes\n"); + return 1; + } + + long readyFd = userPipeAttach(readyHandle, 0); + long announce1Fd = userPipeAttach(announce1Handle, 0); + long announce2Fd = userPipeAttach(announce2Handle, 0); + long dataFd = userPipeAttach(dataHandle, 1); + if (readyFd < 0 || announce1Fd < 0 || announce2Fd < 0 || dataFd < 0) { + print("restarttest: FAIL attach pipes\n"); + return 1; + } + + char readyHandleStr[24]; + char announce1HandleStr[24]; + char announce2HandleStr[24]; + char dataHandleStr[24]; + decToString(readyHandle, readyHandleStr); + decToString(announce1Handle, announce1HandleStr); + decToString(announce2Handle, announce2HandleStr); + decToString(dataHandle, dataHandleStr); + + char* targetArgv[] = { "/bin/restarttarget", readyHandleStr, announce1HandleStr, + announce2HandleStr, dataHandleStr, 0 }; + long target = userSpawn("/bin/restarttarget", targetArgv); + if (target < 0) { + print("restarttest: FAIL spawn restarttarget\n"); + return 1; + } + + char byte; + if (userRead((int)readyFd, &byte, 1) != 1) { + print("restarttest: FAIL read ready byte\n"); + return 1; + } + userClose((int)readyFd); + print("restarttest: restarttarget ready\n"); + + /* Real blocking read: does not return until restarttarget has actually + written announce1, right before it calls its own victim read -- the + happens-before that makes "sigUsr1 lands while genuinely blocked" + meaningful rather than a coincidence of timing (same guarantee + nesttest.c's handlerReadyFd already gives a different race). */ + if (userRead((int)announce1Fd, &byte, 1) != 1) { + print("restarttest: FAIL read announce1 byte\n"); + return 1; + } + userClose((int)announce1Fd); + print("restarttest: restarttarget about to block (phase1)\n"); + + if (userSigSend(target, sigUsr1) != 0) { + print("restarttest: FAIL send sigUsr1 to restarttarget\n"); + return 1; + } + print("restarttest: sent sigUsr1 (no restart) to restarttarget\n"); + + /* restarttarget only ever writes announce2 after phase 1's read has + already come back as errInterrupted and been validated, so this read + doubles as confirmation phase 1 actually passed. */ + if (userRead((int)announce2Fd, &byte, 1) != 1) { + print("restarttest: FAIL read announce2 byte\n"); + return 1; + } + userClose((int)announce2Fd); + print("restarttest: restarttarget about to block (phase2)\n"); + + if (userSigSend(target, sigUsr2) != 0) { + print("restarttest: FAIL send sigUsr2 to restarttarget\n"); + return 1; + } + print("restarttest: sent sigUsr2 (restart) to restarttarget\n"); + + if (userWrite((int)dataFd, "d", 1) != 1) { + print("restarttest: FAIL write data byte\n"); + return 1; + } + userClose((int)dataFd); + + long status = userWait(target); + print("restarttest: restarttarget exit status="); + printDec(status); + print("\n"); + if (status != 55) { + print("restarttest: FAIL expected restarttarget exit status 55\n"); + return 1; + } + print("restarttest: sa-restart-eintr-distinction-ok\n"); + + return 0; +}