diff --git a/user/selfstoptest.c b/user/selfstoptest.c new file mode 100644 index 0000000..60378d1 --- /dev/null +++ b/user/selfstoptest.c @@ -0,0 +1,193 @@ +#include "ulib.h" + +/* + * Boot-time driver closing the one gap left open by jobtest.c/jobtarget.c + * (job control's OTHER live demo): a process stopping ITSELF (self-directed + * SIGSTOP through sysSigSend, syscall.c) rather than being stopped by a + * different one. Reasoned about but deliberately left without a live demo + * for many falas (see include/signal.h's own file comment) because proving + * it needs a second, independent process to race-freely learn "the target + * has now actually stopped" before sending SIGCONT -- exactly the kind of + * guess-free rendezvous sigtarget.c's own pipe-rendezvous comment already + * had to solve once for a different race. The rendezvous this file uses is + * sched.c's threadStop already raising a real SIGCHLD (siCodeChldStopped) + * against the PARENT the instant a child (self- or otherwise) actually + * transitions to threadStopped -- an event the kernel can only ever raise + * if threadStop genuinely ran, so a real SigInfo handler catching it here + * is a zero-guess, race-free signal that selfstoptarget.c's self-stop + * syscall did not silently no-op. + * + * That alone would only prove the STATE flipped, not that execution + * actually paused -- syscall.c's own comment describes exactly the bug + * class this demo also has to rule out (a self-stopped thread that falls + * through to its ordinary return-to-ring3 path instead of explicitly + * rescheduling would keep running, unnoticed, until the next timer tick). + * So this file adds a second check the moment the SIGCHLD confirms the + * stop: a non-blocking (timeoutMs 0) userPoll on the proof pipe + * selfstoptarget.c writes to only AFTER it resumes -- if that byte were + * already there, the self-stop never actually paused execution and + * selfstoptarget raced all the way to writing it before this process's own + * bounded SIGCHLD-wait loop even noticed. Only once BOTH checks pass does + * this send the real sigCont. + */ + +static volatile int stopFired = 0; +static volatile int stopPid = -1; +static volatile int stopStatus = -1; + +static void onChld(int signum, SigInfo* info, void* context) +{ + (void)signum; + (void)context; + if (info->siCode != siCodeChldStopped) + return; + stopPid = info->siPid; + stopStatus = info->siStatus; + stopFired = 1; +} + +int main(void) +{ + print("selfstoptest: start, pid="); + printDec(userGetpid()); + print("\n"); + + /* Installed BEFORE selfstoptarget even exists -- threadStop can raise + the real SIGCHLD the instant selfstoptarget's self-stop syscall + runs, which could be very soon after userSpawn returns below. */ + if (userSigActionInfo(sigChld, onChld, 0, sigFlagSigInfo) == (long)sigSetFailed) { + print("selfstoptest: FAIL install siginfo sigchld handler\n"); + return 1; + } + + long proofHandle = userPipeCreate(); + if (proofHandle < 0) { + print("selfstoptest: FAIL create proof pipe\n"); + return 1; + } + + /* Attach as reader before selfstoptarget exists, the same "attach + first" reason sigtest.c/jobtest.c's own comments already document at + length -- whichever side attaches second risks the pipe's slot + already being freed by pipeReleaseIfUnused. */ + long proofFd = userPipeAttach(proofHandle, 0); + if (proofFd < 0) { + print("selfstoptest: FAIL attach proof pipe as reader\n"); + return 1; + } + + char proofHandleStr[24]; + decToString(proofHandle, proofHandleStr); + + char* targetArgv[] = { "/bin/selfstoptarget", proofHandleStr, 0 }; + long target = userSpawn("/bin/selfstoptarget", targetArgv); + if (target < 0) { + print("selfstoptest: FAIL spawn selfstoptarget\n"); + return 1; + } + + /* Bounded poll for the real, kernel-raised SIGCHLD, same idiom + siginfotest.c/sigchldtest.c already use for the same reason (no + timing guess, just enough sleep-wake cycles for the return-to-ring3 + pending-signal scan, isr.c, to notice it whenever it actually + lands). */ + for (int i = 0; i < 80 && !stopFired; i++) + userSleep(20); + + if (!stopFired) { + print("selfstoptest: FAIL sigchld-on-self-stop handler never fired\n"); + return 1; + } + if (stopPid != (int)target) { + print("selfstoptest: FAIL si_pid="); + printDec(stopPid); + print(" does not match real child pid="); + printDec(target); + print("\n"); + return 1; + } + if (stopStatus != sigStop) { + print("selfstoptest: FAIL si_status did not report sigStop\n"); + return 1; + } + print("selfstoptest: self-stop-confirmed-via-sigchld-ok\n"); + + /* Non-blocking: must return immediately either way (timeoutMs 0), and + must report the proof pipe as NOT ready -- selfstoptarget.c only + ever writes to it AFTER resuming from its own self-stop syscall, + which has not happened yet (sigCont is not sent until after this + check). A self-stop that silently failed to actually pause execution + would have let selfstoptarget race ahead and write this byte well + before this process's own 80-iteration SIGCHLD-wait loop above even + finished, making this the one check that would catch that exact + regression. */ + PollFd proofPoll; + proofPoll.fd = (int)proofFd; + proofPoll.events = pollIn; + proofPoll.revents = 12345; + long ready = userPoll(&proofPoll, 1, 0); + if (ready != 0 || proofPoll.revents != 0) { + print("selfstoptest: FAIL proof pipe already had data before sigCont " + "(self-stop did not actually pause execution)\n"); + return 1; + } + print("selfstoptest: still-stopped-before-cont-ok\n"); + + if (userSigSend(target, sigCont) != 0) { + print("selfstoptest: FAIL send sigCont to selfstoptarget\n"); + return 1; + } + print("selfstoptest: sent sigCont to selfstoptarget\n"); + + /* onChld is installed WITHOUT sigFlagRestart (it needs to fire once, + immediately, for the stop notification above -- a restarting handler + would transparently retry, hiding the exact event this file is + trying to observe). That means it is ALSO live for a second, later + SIGCHLD: selfstoptarget's own exit, which can land at almost any + real time after sigCont resumes it. If that exit's SIGCHLD arrives + while this thread is genuinely parked mid the blocking userRead just + below, sigConsumePending (signal.c) rewrites the interrupted + context to return errInterrupted (-4) instead of letting the + retry-on-block loop run and find the byte selfstoptarget already + wrote -- the exact same self-signal-vs-own-blocking-syscall race + stopchldtest.c's own comment documents and retries past (see that + file for the full reasoning); a correctly written caller with a + non-restarting handler for the same signal its own blocking syscall + is waiting on must loop past EINTR the same way. */ + char proofByte = 0; + long readStatus; + for (;;) { + readStatus = userRead((int)proofFd, &proofByte, 1); + if (readStatus != errInterrupted) + break; + } + if (readStatus != 1) { + print("selfstoptest: FAIL read proof byte\n"); + return 1; + } + userClose((int)proofFd); + if (proofByte != 'p') { + print("selfstoptest: FAIL wrong proof byte\n"); + return 1; + } + print("selfstoptest: post-cont-proof-received-ok\n"); + + /* Same EINTR hazard as the read above, in case the exit SIGCHLD is + still unconsumed by the time this runs instead. */ + long status; + for (;;) { + status = userWait(target); + if (status != errInterrupted) + break; + } + print("selfstoptest: selfstoptarget exit status="); + printDec(status); + print("\n"); + if (status != 66) { + print("selfstoptest: FAIL expected selfstoptarget exit status 66\n"); + return 1; + } + print("selfstoptest: self-stop-continue-ok\n"); + + return 0; +}