A NOTE has been added to this issue. ====================================================================== https://austingroupbugs.net/view.php?id=1731 ====================================================================== Reported By: geoffclare Assigned To: ====================================================================== Project: 1003.1(2016/18)/Issue7+TC2 Issue ID: 1731 Category: System Interfaces Type: Clarification Requested Severity: Objection Priority: normal Status: New Name: Geoff Clare Organization: The Open Group User Reference: Section: pthread_sigmask() Page Number: 1734 Line Number: 56243 Interp Status: --- Final Accepted Text: ====================================================================== Date Submitted: 2023-05-23 09:43 UTC Last Modified: 2023-05-31 19:50 UTC ====================================================================== Summary: pthread_sigmask() pending signal requirement time paradox ======================================================================
---------------------------------------------------------------------- (0006296) kre (reporter) - 2023-05-31 19:50 https://austingroupbugs.net/view.php?id=1731#c6296 ---------------------------------------------------------------------- Re https://austingroupbugs.net/view.php?id=1731#c6294 we should probably also look at a version of BSD from before it started to diverge into different variants. That's easy, this is the implementation of the system call in 4.4_lite (or something around that time - from the final CSRG SCCS files). /* * Manipulate signal mask. * Note that we receive new mask, not pointer, * and return old mask as return value; * the library stub does the rest. */ int sigprocmask(p, uap, retval) register struct proc *p; struct sigprocmask_args /* { syscallarg(int) how; syscallarg(sigset_t) mask; } */ *uap; register_t *retval; { int error = 0; *retval = p->p_sigmask; (void) splhigh(); switch (SCARG(uap, how)) { case SIG_BLOCK: p->p_sigmask |= SCARG(uap, mask) &~ sigcantmask; break; case SIG_UNBLOCK: p->p_sigmask &= ~SCARG(uap, mask); break; case SIG_SETMASK: p->p_sigmask = SCARG(uap, mask) &~ sigcantmask; break; default: error = EINVAL; break; } (void) spl0(); return (error); } That's it, There are also compat sys calls, for the earlier implementation where there were separate sys calls for the functions, rather than one with an arg saying what to do (as above). int compat_43_sigblock(p, uap, retval) register struct proc *p; struct compat_43_sigblock_args /* { syscallarg(int) mask; } */ *uap; register_t *retval; { (void) splhigh(); *retval = p->p_sigmask; p->p_sigmask |= SCARG(uap, mask) &~ sigcantmask; (void) spl0(); return (0); } int compat_43_sigsetmask(p, uap, retval) struct proc *p; struct compat_43_sigsetmask_args /* { syscallarg(int) mask; } */ *uap; register_t *retval; { (void) splhigh(); *retval = p->p_sigmask; p->p_sigmask = SCARG(uap, mask) &~ sigcantmask; (void) spl0(); return (0); } The latter was used to accomplish unblock (generally by setting the mask to what was returned by sigblock()). These are called from machine depenendent code (since the way sys calls are encoded and their args passed, varies from architecture to architecture). Taking the vax as the original, and oldest, sys calls ar handled (big surprise) by the syscall() function ... I'm not going to include all of the code for that, it is fairly long, and most of what it is doing isn't relevant, but ... It starts with code that checks the syscall() call orjginated in user mode (if already in the kernel, it panics), then it works out what the PC was (the length of the instr) which varies depending upon the sys call number (how many bytes it takes to represent that) - deals with syscall(0) (the indirect sys call) and then checks the sys call number is in range. Once that is known, it can copy in the expected args (from the sys call table, which says how many there should be) after which (ignoring some ktrace noise) it does: rval[0] = 0; rval[1] = locr0[R1]; error = (*callp->sy_call)(u.u_procp, &args, rval); if (error == ERESTART) pc = opc; else if (error != EJUSTRETURN) { if (error) { locr0[R0] = error; locr0[PS] |= PSL_C; /* carry bit */ } else { locr0[R0] = rval[0]; locr0[R1] = rval[1]; locr0[PS] &= ~PSL_C; } } rval is where the results get passed back, and is init'd t what will be returned in the normal case (if a syscall has no particular result to return, and succeeds). The indirect function call calls one of the functions above in the cases of interest. If the system call indicates it should be restarted, the pc is pucked backwards (opc is the calculated pc of the system call instruction from earlier). Otherwise if error isn' EJUSTRETURN (which a sys call function can return to indicate it has already set up the result) then if there was an error, R0 (the result) gets set to the error number (which userland code will store in errno) and the carry bit is set to indicate an error occurred, otherwise the results are moved back to the user's register save area, and the carry bit is cleared. That's followed by the most relevant part for this discussion: /* * Reinitialize proc pointer `p' as it may be different * if this is a child returning from fork syscall. */ p = u.u_procp; if (i = CURSIG(p)) postsig(i); (no, not the update of 'p') - CURSIG is a macro: /* * Determine signal that should be delivered to process p, the current * process, 0 if none. If there is a pending stop signal with default * action, the process stops in issignal(). */ #define CURSIG(p) \ (((p)->p_siglist == 0 || \ ((p)->p_flag & P_TRACED) == 0 && \ ((p)->p_siglist & ~(p)->p_sigmask) == 0) ? \ 0 : issignal(p)) It could just be issignal(p) but that would mean calling the function (function calls were relatively expensive on the vax) in cases where it is obvious that all the function will do is return 0 ... so the macro extracts the most likely tests to cause that to happen, and has them evaluated in line, so we can avoid calling issignal() in a relativelu fast path piece of code (executed for every sys call made) if it obviously won't be needed. In the cases we're interested in, that won't work, and we have to call issignal() anyway. If the return from all of this is != 0, it is a signal number to deliver to the application (and postsig() does that). I'll get to issignal() soon. The rest of syscall() is just finishing up, first we check whether this process should give up the CPU (there was only one of them) or not - if we do we repeat the if (i = CURSIG(p)) postsig(i); after the context switch was made - when we get back to here in that case, there might have been a signal delivered while we weren't on the CPU, and we want that to be delivered as well). Then there's some profiling related code (adding the time spent in the sys call to the time spent at the sys call instruction for the purposes of measuring where the application is spending its time) and more ktrace stuff. Then we're done, and the application runs again. postsig() does the machine independent part of delivering a signal, handling it if the action is just to kill the process (if it was to be ignored, it would have been dropped when the signal was first generated) plus dealing with sigaction and such - to block signals while the handler is running, if needed, then calls the machine dependent sendsig() if a user signal handler needs to be called. sendsig() just establishes the application stack state, and pc, so it looks as if a call to the signal handler was just made, and arranges for when that returns (if it does) the correct actions get taken. None of that is relevant right now. All that is left is issignal() which does the work of selecting which signal to deliver. /* * If the current process has received a signal (should be caught or cause * termination, should interrupt current syscall), return the signal number. * Stop signals with default action are processed immediately, then cleared; * they aren't returned. This is checked after each entry to the system for * a syscall or trap (though this can usually be done without calling issignal * by checking the pending signal masks in the CURSIG macro.) The normal call * sequence is * * while (signum = CURSIG(curproc)) * postsig(signum); */ int issignal(p) register struct proc *p; { register int signum, mask, prop; for (;;) { mask = p->p_siglist & ~p->p_sigmask; if (p->p_flag & P_PPWAIT) mask &= ~stopsigmask; if (mask == 0) /* no signal to send */ return (0); signum = ffs((long)mask); mask = sigmask(signum); prop = sigprop[signum]; /* * We should see pending but ignored signals * only if P_TRACED was on when they were posted. */ if (mask & p->p_sigignore && (p->p_flag & P_TRACED) == 0) { p->p_siglist &= ~mask; continue; } if (p->p_flag & P_TRACED && (p->p_flag & P_PPWAIT) == 0) { /* * If traced, always stop, and stay * stopped until released by the parent. * * Note that we must clear the pending signal * before we call trace_req since that routine * might cause a fault, calling tsleep and * leading us back here again with the same signal. * Then we would be deadlocked because the tracer * would still be blocked on the ipc struct from * the initial request. */ p->p_xstat = signum; p->p_siglist &= ~mask; psignal(p->p_pptr, SIGCHLD); do { stop(p); mi_switch(); } while (!trace_req(p) && p->p_flag & P_TRACED); /* * If parent wants us to take the signal, * then it will leave it in p->p_xstat; * otherwise we just look for signals again. */ signum = p->p_xstat; if (signum == 0) continue; /* * Put the new signal into p_siglist. If the * signal is being masked, look for other signals. */ mask = sigmask(signum); p->p_siglist |= mask; if (p->p_sigmask & mask) continue; /* * If the traced bit got turned off, go back up * to the top to rescan signals. This ensures * that p_sig* and ps_sigact are consistent. */ if ((p->p_flag & P_TRACED) == 0) continue; } /* * Decide whether the signal should be returned. * Return the signal's number, or fall through * to clear it from the pending mask. */ switch ((long)p->p_sigacts->ps_sigact[signum]) { case (long)SIG_DFL: /* * Don't take default actions on system processes. */ if (p->p_pid <= 1) { #ifdef DIAGNOSTIC /* * Are you sure you want to ignore SIGSEGV * in init? XXX */ printf("Process (pid %d) got signal %d\n", p->p_pid, signum); #endif break; /* == ignore */ } /* * If there is a pending stop signal to process * with default action, stop here, * then clear the signal. However, * if process is member of an orphaned * process group, ignore tty stop signals. */ if (prop & SA_STOP) { if (p->p_flag & P_TRACED || (p->p_pgrp->pg_jobc == 0 && prop & SA_TTYSTOP)) break; /* == ignore */ p->p_xstat = signum; stop(p); if ((p->p_pptr->p_flag & P_NOCLDSTOP) == 0) psignal(p->p_pptr, SIGCHLD); mi_switch(); break; } else if (prop & SA_IGNORE) { /* * Except for SIGCONT, shouldn't get here. * Default action is to ignore; drop it. */ break; /* == ignore */ } else return (signum); /*NOTREACHED*/ case (long)SIG_IGN: /* * Masking above should prevent us ever trying * to take action on an ignored signal other * than SIGCONT, unless process is traced. */ if ((prop & SA_CONT) == 0 && (p->p_flag & P_TRACED) == 0) printf("issignal\n"); break; /* == ignore */ default: /* * This signal has an action, let * postsig() process it. */ return (signum); } p->p_siglist &= ~mask; /* take the signal! */ } That is it. In that the important lines are signum = ffs((long)mask); where "mask" is the list of signals that are pending, with any that are blocked removed. ffs() is "find first set" - which finds the lowest bit set, and returns its number (so if SIGHUP (#1) is set, and not masked, that one comes first). P_TRACED related to ptrace() - ie: running under a debugger, we can forget that case here (code that requires P_TRACED to be set should just be skipped). The code then (if not ptrace'd) simply ignores the signal if it should be ignored, and goes and finds another (after removing this one from the list of pending signals). That would happen (for example) if a signal was pending, but the current system call had just instructed it to be ignored. If it had already been ignored, it wouldn't have been pending (ptraced'd processed excepted). The switch() looks at the user's intended disposition of the signal. Here, the only one we care about is "default" (ie: not SIG_DFL or SIG_IGN) where we just do return (signum); and we're done. The signal returned is the lowest numbered pending one. It is completely irrelevant whether this is one of the ones that was just unblocked (nothing in issignal() - which makes the choice) has any idea what those might have been, and the system calls don't save that anywhere, other than by the effect that they have on p_sigmask (the list of blocked signals - manipulated by the system calls, then used here). When I said in https://austingroupbugs.net/view.php?id=1731#c6292 As to how implementations work - it has been a while since I got down and dirty at the syscall interface this vintage code is what I was referring to. I already knew (without looking) that it worked like this. I would find it surprising if any implementation was significantly different. The basic strategy of signal delivery on syscall exit, except without masked signals, dates back to the early Bell Labs releases (at least 5th edition, the earliest I ever worked with). The system call function for sigprocmask in NetBSD is: /* * Manipulate signal mask. * Note that we receive new mask, not pointer, * and return old mask as return value; * the library stub does the rest. */ int sigprocmask(p, uap, retval) register struct proc *p; struct sigprocmask_args /* { syscallarg(int) how; syscallarg(sigset_t) mask; } */ *uap; register_t *retval; { int error = 0; *retval = p->p_sigmask; (void) splhigh(); switch (SCARG(uap, how)) { case SIG_BLOCK: p->p_sigmask |= SCARG(uap, mask) &~ sigcantmask; break; case SIG_UNBLOCK: p->p_sigmask &= ~SCARG(uap, mask); break; case SIG_SETMASK: p->p_sigmask = SCARG(uap, mask) &~ sigcantmask; break; default: error = EINVAL; break; } (void) spl0(); return (error); } which is rather similar to what was done in the CSRG BSD code. Nothing at all there about arranging to deliver one of the unblocked signals in the SIG_UNBLOCK case - just remove bits from sigmask, then let issignal() do its thing. The same general thing also happens after traps - but those can't be altering the signal mask, but are also far more likely to have generated a signal to deliver). Very similar code exists in NetBSD today, issignal() is still there, though ffs() has been renamed to firstsig() (the code now needs to be able to deal with more signals than fit as bits in an int). Otherwise it all looks quite similar. Issue History Date Modified Username Field Change ====================================================================== 2023-05-23 09:43 geoffclare New Issue 2023-05-23 09:43 geoffclare Name => Geoff Clare 2023-05-23 09:43 geoffclare Organization => The Open Group 2023-05-23 09:43 geoffclare Section => pthread_sigmask() 2023-05-23 09:43 geoffclare Page Number => 1734 2023-05-23 09:43 geoffclare Line Number => 56243 2023-05-23 09:43 geoffclare Interp Status => --- 2023-05-23 21:08 kre Note Added: 0006287 2023-05-25 08:40 geoffclare Note Added: 0006288 2023-05-25 18:26 kre Note Added: 0006292 2023-05-25 18:32 kre Note Edited: 0006292 2023-05-25 18:44 kre Note Added: 0006293 2023-05-30 11:14 geoffclare Note Added: 0006294 2023-05-31 19:50 kre Note Added: 0006296 ======================================================================
