194 lines
7.8 KiB
C
194 lines
7.8 KiB
C
#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;
|
|
}
|