mq_notify_proc is an unreferenced struct proc *: registrant exit with an inherited descriptor leaves a dangling pointer consumed by mq_send1 (PHOLD + ksignal on freed kernel heap) β deterministic UAF with cross-process signal injection
| Field | Value |
|---|---|
| ID | DF-2778 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-416 Use After Free |
| File | sys/kern/sys_mqueue.c |
| Lines | 988 (store), 388-389 (clear), 899-906 (consume) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sys_mq_notify() stores mq->mq_notify_proc = curproc with no reference. The only clearing site, mq_close_fop(), runs when the LAST file reference drops. If the registrant forks (child inherits the descriptor) and then exits and is reaped, its struct proc is kfree'd to the general kernel heap while mq_notify_proc still points at it. The next mq_send on the empty queue executes PHOLD(notify) (atomic_add_int on freed memory) and ksignal(notify) β lwpsignal (p_token acquisition, p_flags/p_sigignore reads, SIGADDSET_ATOMIC on p_siglist β all on freed/recycled memory). Reproduced round-0 in every run as an innocent fork victim receiving the alien signal β including SIGKILL β after its proc allocation recycled the freed chunk.
Threat model & preconditions
Any unprivileged local user; deterministic (no race); 100% reproduction. Primitive inventory per trigger: two atomic-increment writes, lwkt token writes, an atomic bit-set at a signo-derived sigset offset, and delivery of an arbitrary valid signal (up to SIGKILL) to whichever process reoccupies the freed ~2KB chunk β arbitrary-process kill plus fixed-offset atomic writes aimed at general-heap objects of the same size class. A complete uid0 slab-groom chain was not developed this run; the demonstrated primitives justify High.
Proof of concept
VERIFIED on the stock guest (findings/poc/DF-2778/uaf_notify.c, raw
syscalls): parent opens queue, forks registrant child; child registers
SIGEV_SIGNAL via inherited fd and exits while parent holds the fd;
parent wait4 (proc freed), forks sleeping victim (recycles chunk), then
mq_send 1 byte β victim receives the alien signal. ./uaf_notify 96 10
β "round 0: *** HIT: victim killed by alien sig 10 (UAF reproduced)
***"; SIGKILL variant kills an arbitrary innocent process. Round-0 hit
every run. Fix (mqueue_proc_exit() walked from exit1 after fdfree)
validated in-guest.
Recommended fix
Clear registrations at registrant exit β mqueue_proc_exit() called from exit1 after fdfree (full diff in findings/poc/DF-2778/fix.diff; validated).
Timeline
- 2026-08-31 Discovered during pass-2 audit of sys_mqueue.c (GLM 5.3); deterministic unpriv UAF reproduced round-0 + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2778 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| uaf_notify.c | β | 5.6 KB | view raw | |
| build.sh | β | 120 B | view raw | |
| run.sh | β | 194 B | view raw | |
| build.log | β | 151 B | view raw | |
| run.log | β | 1.4 KB | view raw | |
| run.2.log | β | 400 B | view raw | |
| fix_validation.log | β | 643 B | view raw | |
| VERDICT.md | β | 3.1 KB | β raw | |
| verdict.json | β | 3.6 KB | view raw | |
| fix.diff | β | 1.8 KB | view raw | |
| env.txt | β | 119 B | view raw |
VERDICT β DF-2778: mq_notify_proc dangling struct proc * (UAF)
Status: REPRODUCED Β· impact: dos (cross-process signal injection through freed kernel heap + UAF atomic writes; full uid0 chain not developed this run) Β· confidence: certain
What was proven on the guest (stock kernel, build #0 2026-07-02)
./uaf_notify hit on round 0, every run, in all three modes:
signo=10(SIGUSR1): innocent sleeping victim killed by alien SIGUSR1 β dmesg:pid 880 (uaf_notify), uid 1001: exited on signal 10 (core dumped).- handler mode:
[VICTIM] *** ALIEN SIGUSR1 RECEIVED THROUGH FREED struct proc *** signo=9: innocent victim killed by alien SIGKILL β cross-process signal-kill injection through a freed struct proc.
Mechanism (all unprivileged, deterministic, no race needed):
- registrant R registers SIGEV_SIGNAL via its inherited fd (sys_mqueue.c:988,
raw pointer, no PHOLD), exits while parent M still holds the descriptor;
- M's wait4(R) frees R's struct proc (kern_exit.c:1336, kfree M_PROC β
general kernel heap) β mq_close_fop never ran for R because it only runs at
LAST file reference (kern_descrip.c:3180-3181) and M still holds one;
- M forks victim V whose proc allocation recycles R's chunk (same CPU, LIFO
magazine β hence round-0 hits);
- M's mq_send on the empty queue executes the notify path
(sys_mqueue.c:875-885): notify = mq_notify_proc (freed), then
PHOLD(notify) = atomic_add_int(&p->p_lock, 1) on freed memory
(kern_proc.c:341), then ksignal(notify, signo) β lwpsignal takes
lwkt_gettoken(&p->p_token), reads p_flags/p_sigignore, executes
SIGADDSET_ATOMIC(p->p_siglist, sig) β all on the recycled chunk β and the
signal is delivered to V, the process now occupying the freed object.
Exploitability assessment
Primitive inventory per trigger: (a) two atomic-increment writes at fixed
offset p_lock; (b) lwkt token acquire/release writes on the freed chunk
(p_token); (c) atomic bit-set write at p_siglist.__bits[k], k
signo-derived; (d) cross-process delivery of an arbitrary valid signal
(1..63) to whichever process reoccupies the chunk β demonstrated kill of an
arbitrary innocent process. Because struct proc is served from the general
kernel heap (kmalloc(..., M_PROC, ...)), a groomed attacker can aim these
fixed-offset writes at a same-size-class victim object of a different type
and the SIGKILL at a privileged process. A complete uid0 chain (slab groom β
victim credential-adjacent object β controlled field corruption) was not
developed in this run; the demonstrated deterministic UAF-write + arbitrary
process kill stands on its own as High severity.
Fix validation (patched kernel, build #1 2026-09-01 04:57)
fix.diff adds mqueue_proc_exit() (clears registrations at exit1, after
fdfree). Same PoC, patched kernel: 0 hits in 116 rounds (build #1 04:10)
plus 104 rounds (build #1 04:57) across all three modes β victims
"undisturbed" every round, guest stable. Baseline vs patched in
fix_validation.log.
fix_status: fixed (fix_baseline_reproduced=1, fix_patched_reproduced=0).
Fix verification
fixedPatched kernel (mqueue_proc_exit called from exit1 after fdfree): identical PoC ran 116 + 104 rounds across all modes with zero hits (victims undisturbed every round), guest stable. Baseline hit round 0 every run.
fix_validation.log; /tmp/kbuild4.log BUILD-OK on guest
Confirmed kernel references
Detail
Exploit chain
mq_open+fork -> child registers SIGEV_SIGNAL notify and exits while parent holds fd -> wait4 frees child proc -> fork victim recycles chunk -> parent mq_send -> PHOLD/PRELE (atomic add at p_lock offset) + lwpsignal on freed/recycled chunk (p_token acquire, SIGADDSET_ATOMIC on p_siglist) -> alien signal (up to SIGKILL) delivered to the victim process; repeatable; general-heap reuse allows aiming fixed-offset atomic writes at arbitrary same-size-class objects.
Evidence (decisive lines)
["run.log: round-0 HIT in all 3 modes incl. '[VICTIM] *** ALIEN SIGUSR1 RECEIVED THROUGH FREED struct proc ***'", "run.2.log: SIGKILL variant 'victim killed by alien sig 9'", 'panic not required; dmesg core-dump line in run.log notes', 'fix_validation.log: 0 hits in 220 rounds on patched kernel']
PoC changes
Used raw syscall(SYS_mq_*) instead of any libc mqueue API; pipe-synchronized fork ordering (registrant exits while parent holds the fd); victim forks immediately after reap to recycle the freed proc chunk; three modes (SIGUSR1 kill, handler print, SIGKILL).
Verified recommended fix
Clear mq_notify registrations at registrant exit: mqueue_proc_exit() walk from exit1 (fix.diff), or per-proc list of registered queues (NetBSD model); never store an unreferenced proc pointer.
Verdict
Deterministic unprivileged UAF: mq_notify stores an unreferenced struct proc * (sys_mqueue.c:988); fo_close (and thus the clearing at :388-389) runs only at last file reference (kern_descrip.c:3180), so a registrant that exits while a fork child holds the descriptor leaves mq_notify_proc dangling after wait4 frees the proc (kern_exit.c:1336, kfree M_PROC general heap). The next send on the empty queue executes PHOLD(freed) (kern_proc.c:341) and ksignal(freed) (sys_mqueue.c:899-906) β reproduced round-0 in every mode: an innocent sleeping victim was signalled/killed (incl. SIGKILL) through the freed chunk its own fork recycled, proving atomic writes (p_lock), token ops and sigset bit-sets on freed memory plus arbitrary-process signal injection. uid0 chain not completed this run; primitives characterized in VERDICT.md.
No comments yet.