From 2709025a74eb67166b18c28119fe90c42d4a5145 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 9 Sep 2026 17:05:50 +0200 Subject: [PATCH] feat: add onUsr1, streq and printOut --- user/sigtarget.c | 95 +++++++++++++++++++++++++++++++++++++++++++++ user/spawntarget.c | 13 +++++++ user/stderrreader.c | 53 +++++++++++++++++++++++++ user/stderrwriter.c | 40 +++++++++++++++++++ 4 files changed, 201 insertions(+) create mode 100644 user/sigtarget.c create mode 100644 user/spawntarget.c create mode 100644 user/stderrreader.c create mode 100644 user/stderrwriter.c diff --git a/user/sigtarget.c b/user/sigtarget.c new file mode 100644 index 0000000..3202afd --- /dev/null +++ b/user/sigtarget.c @@ -0,0 +1,95 @@ +#include "ulib.h" + +/* + * Only ever spawned by sigtest.c, with a shared pipe handle as argv[1] + * (decToString'd, exactly like fifomaster.c's children -- see that file). + * Proves real, catchable signal delivery end to end: install a handler for + * sigUsr1 through userSigAction, wait to actually be interrupted by one + * arriving from a completely separate process (sigtest.c, via + * userSigSend -- not the kernel injecting it directly), and keep running + * normally afterwards -- the handler firing AND the loop below correctly + * resuming (not corrupting registers, not re-entering main, not looping + * forever) is the round trip through signal.c's sigConsumePending, the + * shared trampoline page (sigTrampoline.S), and sysSigReturn all actually + * working together, not just each piece compiling. + * + * The pipe exists purely to close a real race sigtest.c originally papered + * over with a fixed userSleep: sigUsr1's default disposition (no handler + * installed) is Terminate, not Ignore, so a signal that arrives before + * userSigAction below has actually run would kill this process instead of + * being caught -- observed for real under host load (sigtarget killed by + * "uncaught signal 10" before it ever printed "waiting for signal"). + * Writing one byte AFTER installing the handler, and having sigtest.c + * block on reading it before ever calling userSigSend, makes "handler + * installed" happen-before "signal sent" for real, the same guarantee + * fifomaster.c's children get from their own shared pipe. + */ + +static volatile int handlerFired = 0; +static volatile int handlerSignum = 0; + +static void onUsr1(int signum) +{ + handlerFired = 1; + handlerSignum = signum; + print("sigtarget: handler invoked signum="); + printDec(signum); + print("\n"); +} + +int main(int argc, char** argv) +{ + print("sigtarget: start, pid="); + printDec(userGetpid()); + print("\n"); + + if (argc != 2) { + print("sigtarget: FAIL expected one argv (pipe handle)\n"); + return 1; + } + + if (userSigAction(sigUsr1, onUsr1, 0, 0) == (long)sigSetFailed) { + print("sigtarget: FAIL install handler\n"); + return 1; + } + + long handle = parseDec(argv[1]); + long readyFd = userPipeAttach(handle, 1); + if (readyFd < 0) { + print("sigtarget: FAIL attach ready pipe\n"); + return 1; + } + if (userWrite((int)readyFd, "r", 1) != 1) { + print("sigtarget: FAIL write ready byte\n"); + return 1; + } + userClose((int)readyFd); + + print("sigtarget: waiting for signal\n"); + + /* Several short sleeps rather than one long one: this kernel only + checks for a pending signal when the target thread actually returns + to ring 3 (isr.c's deliverPendingSignals) -- a thread that is still + genuinely Sleeping is not scheduled at all, so a shorter sleep gives + sigtest.c's userSigSend more chances to land promptly, without + depending on exact timing between the two independent processes. */ + for (int i = 0; i < 40 && !handlerFired; i++) + userSleep(20); + + if (!handlerFired) { + print("sigtarget: FAIL handler never fired\n"); + return 1; + } + if (handlerSignum != sigUsr1) { + print("sigtarget: FAIL wrong signum delivered\n"); + return 1; + } + print("sigtarget: handler-fired-ok\n"); + + /* Reached only if sysSigReturn correctly restored the exact + pre-signal context: a corrupted resume (wrong rip/rsp/registers) + would have crashed, hung, or skipped straight past this line + instead of returning here from the interrupted sleep loop above. */ + print("sigtarget: resumed-after-handler-ok\n"); + return 42; +} diff --git a/user/spawntarget.c b/user/spawntarget.c new file mode 100644 index 0000000..1e5f045 --- /dev/null +++ b/user/spawntarget.c @@ -0,0 +1,13 @@ +#include "ulib.h" + +/* A plain child program: only ever launched via proctest.c's userSpawn, so + its pid is a genuinely different, freshly created thread rather than the + caller's own (unlike exectarget.c, which replaces the caller in place). */ + +int main(void) +{ + print("spawntarget: child running, pid="); + printDec(userGetpid()); + print("\n"); + return 3; +} diff --git a/user/stderrreader.c b/user/stderrreader.c new file mode 100644 index 0000000..d2ec0d1 --- /dev/null +++ b/user/stderrreader.c @@ -0,0 +1,53 @@ +#include "ulib.h" + +/* + * Companion to stderrwriter.c -- reads fd 0 (redirected by the shell to a + * REAL file via `<`, jobctlApplyRedirects) in a loop until a real EOF, + * exactly like redirreader.c does, then compares byte-for-byte against two + * back-to-back copies of stderrwriter.c's own payload -- proof that `2>` + * (create/truncate) followed by `2>>` (append) on the SAME path produced + * exactly that file, not a truncated/duplicated/reordered one. + * + * This process's own fd 1 (stdout) is NOT redirected -- print() (ulib.h) + * reaches the real console exactly like any unredirected demo, so the + * needle below is exactly as observable to ci-test.sh as every other one. + */ + +static int streq(const char* a, const char* b) +{ + while (*a && *a == *b) { + a++; + b++; + } + return *a == *b; +} + +int main(void) +{ + print("stderrreader: reading redirected stdin\n"); + + char buffer[80]; + unsigned long total = 0; + for (;;) { + long got = userRead(0, buffer + total, sizeof(buffer) - 1 - total); + if (got < 0) { + print("stderrreader: FAIL read\n"); + return 1; + } + if (got == 0) + break; + total += (unsigned long)got; + if (total >= sizeof(buffer) - 1) + break; + } + buffer[total] = 0; + + print("stderrreader: got: "); + print(buffer); + print("\n"); + + int ok = streq(buffer, "stderr-payload-okstderr-payload-ok"); + print(ok ? "stderrreader: payload-ok\n" : "stderrreader: payload-FAIL\n"); + + return ok ? 0 : 1; +} diff --git a/user/stderrwriter.c b/user/stderrwriter.c new file mode 100644 index 0000000..e630789 --- /dev/null +++ b/user/stderrwriter.c @@ -0,0 +1,40 @@ +#include "ulib.h" + +/* + * Live ring-3 proof for shell.c's new `cmd 2> file` / `cmd 2>> file` stderr + * redirection (jobctlApplyRedirects/jobctlSpawnForegroundRedirected, + * jobctl.h): the exact mirror image of redirwriter.c's own `>`/`>>` proof, + * with fd 1 and fd 2's roles swapped -- this program writes its payload to + * fd 2 (the fd the shell redirects here) and its own diagnostics to fd 1 + * (which stays on the real console in this demo, since only fd 2 is ever + * redirected). Writing the diagnostics through fd 2 would corrupt the exact + * bytes stderrreader.c later compares, the same hazard redirwriter.c avoids + * by keeping its own diagnostics off fd 1. + * + * kmain.c's stderrDriverThread runs this program TWICE against the same + * path -- once through `2>` (truncate/create) and once through `2>>` + * (append) -- so the resulting file holds two back-to-back copies of this + * same payload with no separator, proving `2>>` genuinely continues from + * the end of what `2>` already wrote rather than overwriting it. + */ +#define stderrPayload "stderr-payload-ok" + +static void printOut(const char* text) +{ + userWrite(1, text, stringLength(text)); +} + +int main(void) +{ + printOut("stderrwriter: writing payload to stderr (redirected)\n"); + + unsigned long length = stringLength(stderrPayload); + long written = userWrite(2, stderrPayload, length); + if (written != (long)length) { + printOut("stderrwriter: FAIL short/failed write\n"); + return 1; + } + + printOut("stderrwriter: wrote payload\n"); + return 0; +}