feat: add onUsr2 and onUsr1

This commit is contained in:
dualfroz committed 2026-09-09 10:39:21 +02:00
1 parent a8819f99c6
commit 8962d40688
1 file changed
+182
+182
View File
@@ -0,0 +1,182 @@
#include "ulib.h"
/*
* Only ever spawned by masktest.c, with three shared pipe handles as argv
* (ready/handlerReady/go, decToString'd -- the exact same convention
* nesttarget.c already established, and for the same reason: an extra
* pipe beyond the usual two is needed to announce "the handler has
* genuinely entered and is about to block" from INSIDE a signal handler,
* not main()).
*
* Proves the OPPOSITE half of nesttarget.c's story for real, ring-3
* delivery (signal.h's per-signal sa_mask): unlike nesttarget's onUsr1
* (mask 0, so sigUsr2 genuinely nests), THIS onUsr1 installs sa_mask =
* sigUsr2's bit, so sigUsr2 arriving while onUsr1 is active must stay
* pending, NOT nest -- sigTest.c's
* sigConsumePendingSaMaskDefersDifferentSignalUntilHandlerReturns already
* proves this deterministically by driving sigConsumePending/sigOnSigReturn
* directly; this is its live ring-3 counterpart, plus the one thing a pure
* logic test cannot show: sigUsr2, once deferred, is actually delivered
* the INSTANT onUsr1 returns -- riding the very same trap-return sysSigReturn
* itself takes (isr.c's isrDispatch calls deliverPendingSignals
* unconditionally right after syscallDispatch handles sysSigReturn), with
* no further syscall or scheduling needed to notice it. No sleep/timing
* guess anywhere: the three pipes make "handler installed", "handler
* entered, mask active, about to block", and "safe to unblock it" all real
* happens-before rendezvous, the same discipline nesttarget.c/jobtest.c
* already established for different races.
*/
static volatile int handlerUsr1Entered = 0;
static volatile int handlerUsr2Fired = 0;
static volatile int stillMaskedCheckOk = 0;
static long handlerReadyFd = -1;
static long goFd = -1;
static void onUsr2(int signum)
{
(void)signum;
/* Only ever expected to run once onUsr1 has already returned and
lifted its sa_mask's grip on sigUsr2 -- see the check inside onUsr1
below for what catches it firing any earlier than that. */
print("masktarget: usr2-handler-fired-after-unmask\n");
handlerUsr2Fired = 1;
}
static void onUsr1(int signum)
{
(void)signum;
print("masktarget: usr1-handler-enter\n");
handlerUsr1Entered = 1;
/* Announces "I am about to block on goFd, with sigUsr2 now masked" --
masktest.c will not send sigUsr2 until it reads this byte, so
sigUsr2 can never race ahead of onUsr1's sa_mask actually being in
effect. */
if (userWrite((int)handlerReadyFd, "h", 1) != 1) {
print("masktarget: FAIL write handler-ready byte\n");
return;
}
userClose((int)handlerReadyFd);
char goByte = 0;
/* Real blocking read, issued BY THE HANDLER ITSELF, exactly like
nesttarget.c's own goFd read -- the moment it completes, control
returns to ring 3 through the same int 0x80 path any syscall does,
which isr.c's deliverPendingSignals always re-checks. Unlike
nesttarget.c, sigUsr2 is NOT deliverable here: onUsr1's own sa_mask
still covers it, so this return-to-ring3 must dismiss it and resume
onUsr1's own code, not enter a nested handler. */
if (userRead((int)goFd, &goByte, 1) != 1) {
print("masktarget: FAIL read go byte inside handler\n");
return;
}
userClose((int)goFd);
/* The actual proof: if sigUsr2 had genuinely nested (mask ineffective),
onUsr2 would have already run and set this flag BEFORE control ever
reached here (a nested handler's own sigreturn is what lets THIS
read call's return value reach onUsr1's C code at all) -- so seeing
it still 0 here is not a guess, it is the direct consequence of
what deliverPendingSignals did (or did not do) at the read's own
return-to-ring3 point. */
if (handlerUsr2Fired) {
print("masktarget: FAIL usr2 fired while masked by usr1's sa_mask\n");
return;
}
stillMaskedCheckOk = 1;
print("masktarget: still-masked-after-read-ok\n");
/* Returning from here is sysSigReturn -- sigOnSigReturn restores
sigBlocked to what it was before onUsr1 (no sigUsr2 bit), and
isrDispatch's deliverPendingSignals immediately re-runs against the
just-restored context, in the SAME trap return, before this thread
ever resumes anywhere else -- sigUsr2, still pending since masktest
sent it, is delivered right here as its own top-level handler. */
}
int main(int argc, char** argv)
{
print("masktarget: start, pid=");
printDec(userGetpid());
print("\n");
if (argc != 4) {
print("masktarget: FAIL expected three argv (ready/handlerReady/go handles)\n");
return 1;
}
long readyHandle = parseDec(argv[1]);
long handlerReadyHandle = parseDec(argv[2]);
long goHandle = parseDec(argv[3]);
/* sigUsr1's sa_mask covers sigUsr2 -- this is the whole point of this
demo, the opposite of nesttarget.c's mask-0 onUsr1. sigUsr2 itself
has no mask (nothing is meant to interrupt IT here). sigUsr1's own
delivery always interrupts main()'s plain userSleep loop below
(never a retryable syscall), and sigUsr2 never interrupts a
retryable syscall of its own either (it fires on a sigreturn's own
trap-return, not mid a sysRead/sysWrite/sysWait retry), so sa_flags
is irrelevant for both. */
if (userSigAction(sigUsr1, onUsr1, (1ull << sigUsr2), 0) == (long)sigSetFailed ||
userSigAction(sigUsr2, onUsr2, 0, 0) == (long)sigSetFailed) {
print("masktarget: FAIL install handlers\n");
return 1;
}
/* Attach the pipes THIS process will use from inside a handler before
ever installing/receiving anything that could reach them -- same
"attach first" discipline sigtest.c/jobtest.c/nesttarget.c already
document at length, so neither slot can be reclaimed out from under
a late attach. */
handlerReadyFd = userPipeAttach(handlerReadyHandle, 1);
goFd = userPipeAttach(goHandle, 0);
if (handlerReadyFd < 0 || goFd < 0) {
print("masktarget: FAIL attach handler-ready/go pipes\n");
return 1;
}
long readyFd = userPipeAttach(readyHandle, 1);
if (readyFd < 0) {
print("masktarget: FAIL attach ready pipe\n");
return 1;
}
if (userWrite((int)readyFd, "r", 1) != 1) {
print("masktarget: FAIL write ready byte\n");
return 1;
}
userClose((int)readyFd);
print("masktarget: ready, waiting for sigUsr1\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), the same reasoning
nesttarget.c's own poll loop documents. Once onUsr1 fires, ALL the
real work (the deferred check, then the deferred-until-now onUsr2)
happens synchronously before it ever returns here. */
for (int i = 0; i < 80 && !handlerUsr2Fired; i++)
userSleep(20);
if (!handlerUsr1Entered) {
print("masktarget: FAIL usr1 handler never fired\n");
return 1;
}
if (!stillMaskedCheckOk) {
print("masktarget: FAIL usr1 handler did not confirm usr2 stayed masked\n");
return 1;
}
if (!handlerUsr2Fired) {
print("masktarget: FAIL usr2 handler never fired after usr1 returned\n");
return 1;
}
print("masktarget: mask-defers-then-delivers-ok\n");
/* Reached only if BOTH sysSigReturn calls (onUsr1's own, then the
deferred onUsr2's) correctly restored their respective saved
contexts -- a corrupted pop at either depth would have crashed,
hung, or resumed the wrong saved trapframe instead of returning
here from main()'s own interrupted sleep. */
print("masktarget: resumed-after-deferred-usr2-ok\n");
return 78;
}