# DF-2693 — postsig() KASSERT race with concurrent sigaction(SIG_IGN)

**File:** `sys/kern/kern_sig.c` — `postsig()` (lines 2253-2356)
**Class:** CWE-367 TOCTOU on signal disposition → INVARIANTS kernel panic
**Reach:** unprivileged local user (own process, two or more threads)
**Verdict:** REPRODUCED on the stock INVARIANTS kernel (`panic: postsig
action`, symbolically pinned to kern_sig.c:2309).

## Root cause

`userret()` (trap.c:278-281) decides deliverability with
`CURSIG_LCK_TRACE()` — based on `p_sigcatch` — and then calls
`postsig(sig, ptok)`.  For a signal that was pending on the **lwp** list
(the common case: `lwpsignal()` routes `kill()`-style process signals to a
specific LWP, kern_sig.c:1425-1428), `haveptok == 0` and `postsig()` reads

```c
action = ps->ps_sigact[_SIG_IDX(sig)];           /* kern_sig.c:2281 */
```

**without holding `p->p_token`**.  A concurrent `sigaction(sig, SIG_IGN)`
from another thread of the same process takes `p_token`
(kern_sig.c:258-377), clears `p_sigcatch`, deletes the pending signal from
all lists, and stores `SIG_IGN` into `ps_sigact[sig]`.  Interleaving the
two yields `action == SIG_IGN` at the

```c
KASSERT(action != SIG_IGN && !SIGISMEMBER(lp->lwp_sigmask, sig),
    ("postsig action"));                          /* kern_sig.c:2309-2310 */
```

→ `panic: postsig action` on INVARIANTS kernels.  The same race is visible
from the other side as `issignal()`'s `"should not hit signal %d!"`
warning (kern_sig.c:2210-2220) — both messages appear interleaved on the
panicked console.

## Reproduction

* `build.sh`: `cc -O2 -Wall -pthread -o postsig_race postsig_race.c`
* `run.sh`: `./postsig_race 150` as an unprivileged user
* Expected (stock INVARIANTS kernel): `panic: postsig action`, stack
  `postsig+0x3ae ← userret ← syscall2`.  Hit within 150 s in both attempts.

## Impact

* INVARIANTS/debug kernels (like the shipped X86_64_GENERIC target of this
  audit): reliable-enough unprivileged kernel panic = local DoS.
* Stock/production kernels: `sv_sendsig` installs handler == SIG_IGN
  (`(void *)1`); the thread returns to userland at address 1 and takes a
  self-inflicted SIGSEGV.  No cross-privilege impact — the process only
  kills itself — but signal-state corruption (mask/handler mismatch) is
  possible.

## Fix

`fix.diff`: tolerate the lost race instead of asserting — if the handler
became SIG_IGN (or the signal became masked) between the CURSIG decision
and now, simply return (the racing sigaction() already cleared the pending
state; the token bookkeeping is already finished at that point).  Not
kernel-rebuilt (Low/DoS class): fix_status=not_tested.

## References

* sys/kern/kern_sig.c:2281 (unlocked `ps_sigact` read)
* sys/kern/kern_sig.c:2309-2310 (KASSERT)
* sys/kern/kern_sig.c:281-375 (racing sigaction under p_token)
* sys/platform/pc64/x86_64/trap.c:278-281 (CURSIG → postsig window)
