β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2778

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2778 Β· 11 files
FileTypeDescriptionSize
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.md
↓ download 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:

  1. signo=10 (SIGUSR1): innocent sleeping victim killed by alien SIGUSR1 β€” dmesg: pid 880 (uaf_notify), uid 1001: exited on signal 10 (core dumped).
  2. handler mode: [VICTIM] *** ALIEN SIGUSR1 RECEIVED THROUGH FREED struct proc ***
  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Tue Sep 1 04:57:57 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.