# VERDICT — DF-2779: unlocked sigev_signo re-read race → ksignal(p, attacker int)

**Status: REPRODUCED** · impact: panic (INVARIANTS; OOB atomic kernel write on
production builds) · confidence: certain

## What was proven on the guest (stock kernel, build #0 2026-07-02)

Two independent manifestations of the same race:

1. **signo=64 build** (~15 s of racing): the racing process was killed by the
   kernel with *"Unknown signal: 64"* (`EXIT=192` = 128+64) — the smuggled,
   never-validated `sigev_signo=64` (registered via `SIGEV_NONE`, which skips
   validation at `sys_mqueue.c:973-975`) reached `ksignal()` through
   `mq_send1()`'s unlocked re-read at `sys_mqueue.c:905`, was stored by
   `SIGADDSET_ATOMIC` (64 < `_SIG_MAXSIG`=128 → in-set) and delivered to
   userland.
2. **signo=0x4000001 build** (< 180 s of racing): **kernel panic**
   `panic: lwpsignal: invalid signal 67108865` (67108865 == 0x4000001, the
   exact attacker constant), stack `lwpsignal ← lwpsignal ← mq_send1 ←
   sys_xsyscall ← syscall2` (panic.txt). Guest down at `db>`.

On a production (non-INVARIANTS) kernel the same call path executes
`SIGISMEMBER(p->p_sigignore, sig)` and `SIGADDSET_ATOMIC(p->p_siglist, sig)`
with word index `((sig-1)>>5)` (`kern_sig.c:1183,1367`; `signal.h:65-67` — no
bounds mask): an **out-of-bounds atomic bit-set write at an attacker-chosen
offset up to ~256 MB past the sigset** (8 MB for the demonstrated constant).

## Race mechanics (why it wins)

Sender consumes the registration and clears `mq_notify_proc` under `mq_mtx`,
releases the lock, then re-reads `mq_sig_notify.sigev_signo` without it. The
stealer thread (re-registering `SIGEV_NONE{bad}` / unregistering in a tight
loop) acquires the just-released `mq_mtx` and `memcpy`s its unvalidated
sigevent before the sender's read; the closer thread racing `close(fd)` into
the send window forces the sender's internal `fdrop()` onto the slow
last-reference path (spinlock + `fo_close` → `mqlist_mtx`/`mq_mtx`
acquisitions), stretching the release→read window from ~100 ns into the
microseconds-plus range, and the contender thread keeps `mqlist_mtx`
contended so that slow path sleeps. 8 threads total.

## Fix validation (patched kernel, build #1)

`fix.diff` snapshots `notify_signo` under `mq_mtx`. Patched kernel:
- build #1 04:10: 600 s, **7.0 M sends / 537 M arm-registrations / 321 M
  steal-registrations / 145 M closes** — no panic, clean exit;
- build #1 04:57: 600 s, **337 K sends / 675 M arms / 384 M steals** — no
  panic, guest up.
The window was exercised hundreds of millions of times with zero smuggles.

fix_status: **fixed** (fix_baseline_reproduced=1, fix_patched_reproduced=0).
