postsig() KASSERT panic: unlocked ps_sigact[] read races concurrent sigaction(SIG_IGN) from another LWP
| Field | Value |
|---|---|
| ID | DF-2693 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-367 TOCTOU |
| File | sys/kern/kern_sig.c |
| Lines | 2281 (unlocked read), 2309-2310 (KASSERT) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-30 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
userret() decides deliverability via CURSIG (from p_sigcatch), then postsig() re-reads ps_sigact[sig] (:2281) without p_token when the signal was lwp-pending (haveptok==0 β the common routing for kill() to multithreaded procs). A concurrent sigaction(SIG_IGN) (under p_token) makes action==SIG_IGN and the KASSERT at :2309-2310 fires. On non-INVARIANTS kernels the thread merely jumps to SIG_IGN (address 1) β self-inflicted SIGSEGV.
Threat model & preconditions
Unprivileged local user panics INVARIANTS/debug kernels (the shipped X86_64_GENERIC build target of this audit).
Proof of concept
VERIFIED (findings/poc/DF-2693/postsig_race.c): 4 threads loop
kill(getpid(), SIGUSR1), 4 threads loop sigaction(SIGUSR1,
SIG_IGNβhandler) β panic: postsig action within 150s; gdb pins
postsig+0x3ae to exactly kern_sig.c:2309.
Recommended fix
Replace the KASSERT with graceful handling of the lost race
(if (action == SIG_IGN || SIGISMEMBER(lp->lwp_sigmask, sig)) return;) β
fix.diff applies cleanly (not kernel-rebuilt for this Low/DoS finding).
Timeline
- 2026-08-30 Discovered during pass-2 audit of kern_sig.c (GLM 5.3); reproduced on stock same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2693 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| postsig_race.c | β | 2.5 KB | view raw | |
| build.sh | β | 115 B | view raw | |
| run.sh | β | 83 B | view raw | |
| run.log | β | 1.9 KB | view raw | |
| panic.txt | β | 522 B | view raw | |
| README.md | β | 2.8 KB | β raw | |
| fix.diff | β | 835 B | view raw | |
| verdict.json | β | 2.5 KB | view raw | |
| manifest.json | β | 708 B | view raw |
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
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
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.crun.sh:./postsig_race 150as an unprivileged user- Expected (stock INVARIANTS kernel):
panic: postsig action, stackpostsig+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_sendsiginstalls 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_sigactread) - 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)
Fix verification
not_testablefix.diff authored (KASSERT -> graceful return) and verified to apply cleanly, but no fixed kernel was built for this Low/DoS finding within this run.
['fix.diff']
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log - stock-kernel run, panic: postsig action, gdb line confirmation', "panic.txt - console capture incl. interleaved 'should not hit signal 30!' (issignal's warning, same race)", 'postsig_race.c - 4 sender threads + 4 flipper threads']
PoC changes
none - the sketch-level race program worked as designed on the first stock-kernel attempt.
Verified recommended fix
Replace the KASSERT with graceful handling: if action == SIG_IGN or the signal became masked between the CURSIG decision and now, simply return (the racing sigaction already cleared pending state)
Verdict
postsig() reads ps_sigact[] without p_token when the delivered signal was lwp-pending (haveptok==0, the common routing for kill() to multithreaded processes), racing a concurrent sigaction(SIG_IGN) from another thread; the KASSERT at kern_sig.c:2309 fires. Reproduced on the stock INVARIANTS kernel as an unprivileged 8-thread program: panic 'postsig action' within 150 s, stack postsig+0x3ae <- userret <- syscall2, symbolically confirmed via gdb to be exactly kern_sig.c:2309 (postsig+928..944). On non-INVARIANTS kernels the race degrades to the process jumping to SIG_IGN (address 1) - self-inflicted SIGSEGV, no cross-privilege impact.
No comments yet.