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

devfs node/cdev lifetime hole: nresolve→devfs_allocv drops devfs_lock on a raw devfs_node; concurrent unpriv pty teardown frees node+cdev → UAF write + sysref_get on freed cdev (proven panic 2/2)

Field Value
ID DF-3017
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:H
CWE CWE-362 β†’ CWE-416
File sys/vfs/devfs/devfs_vnops.c
Lines 474-521 (allocv drops: devfs_core.c:319-352)
Area vfs/devfs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

devfs_vop_nresolve finds a node under devfs_lock and calls devfs_allocv, which releases devfs_lock across vget()/getnewvnode() holding only a raw struct devfs_node. An unprivileged user racing open/close of /dev/ptmx against lstat/access of /dev/pts/N makes the devfs core thread run destroy_devβ†’devfs_freep in that window: the node is objcache_put-freed and the cdev sysref torn down. allocv then writes node->v_node=vp into freed memory and calls v_associate_rdev(vp, node->d_dev)β†’reference_dev() on the freed cdev. Second window of the same family: devfs_vop_getattr touches node and node->d_dev with no vnode lock and no devfs_lock, racing the same freep (info leak of freed/reused node fields into struct stat). Any local user (no privileges; /dev/ptmx is 0666). Reproduced twice on the stock INVARIANTS kernel: panic 'assertion (sr->flags & SRF_PUTAWAY) == 0 failed in sysref_get' with stack reference_dev←v_associate_rdev←devfs_allocv←devfs_vop_nresolve, guest dead in 1-4 min. On non-INVARIANTS kernels the same sysref_get silently resurrects a terminating cdev (refcount 0β†’1 on freed memory, groomable via the attacker's own ptmx clones from the same cdev objcache) β†’ double-free/dangling cdev used by dev_d dispatch. uid=0 route: fixed- offset sysref refcount increment on a freed cdev recyclable from the attacker-groomed objcache; deterministic chain not developed (competing sysref-thread teardown timing documented as the blocker) β€” reliable demonstrated impact is unpriv kernel panic + UAF.

Proof of contest

VERIFIED (findings/poc/DF-3017/): df3017.c β€” 3 threads open+close /dev/ptmx, 8 threads lstat+access /dev/pts/0-7 as nobody; panic with the SRF_PUTAWAY assertion and the reference_dev←devfs_allocv stack within 1-4 min (2/2 baseline). Fix validated in-guest (DEVFS_ALLOCVINPROG interlock + DEVFS_FREEWAIT handshake in devfs_freep; note: closing the getattr half needs upstream protocol redesign): identical PoC ran the full window with zero sysref panics.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of devfs_vnops.c (GLM 5.3); unpriv UAF panic reproduced 2/2 + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3017 Β· 53 files
FileTypeDescriptionSize
README.md β€” 5.2 KB ↓ raw
VERDICT.md β€” 22.8 KB ↓ raw
FORENSICS.md β€” 5.9 KB ↓ raw
df3017.c β€” 4.9 KB view raw
df3017x.c β€” 14.4 KB view raw
df3017c.c β€” 26.0 KB view raw
df3017d.c β€” 26.8 KB view raw
df2694_recon.c β€” 16.3 KB view raw
nodefree.c β€” 1.2 KB view raw
offsets.c β€” 2.8 KB view raw
offsets2.c β€” 4.1 KB view raw
probe.c β€” 1.1 KB view raw
p3.c β€” 982 B view raw
build.sh β€” 89 B view raw
run.sh β€” 122 B view raw
runx.sh β€” 1.8 KB view raw
runx.log β€” 8.5 KB view raw
run2cpu.sh β€” 1.9 KB view raw
run2cpu.log β€” 2.0 KB view raw
forensics2.py β€” 1.3 KB view raw
rsp.py β€” 2.5 KB view raw
panic.txt β€” 2.3 KB view raw
panic.run2.txt β€” 2.2 KB view raw
fix_build.log β€” 1.5 KB view raw
fix.diff β€” 3.3 KB view raw
verdict.json β€” 6.2 KB view raw
walkzones.py β€” 3.2 KB view raw
hunt.py β€” 4.2 KB view raw
hunt2.py β€” 3.3 KB view raw
hunt4.out β€” 2.5 KB view raw
hunt6.out β€” 10.8 KB view raw
hunt9.out β€” 1.6 KB view raw
hunt11.out β€” 271 B view raw
hunt12.out β€” 1.9 KB view raw
hunt13.out β€” 409 B view raw
dispatch.trace.txt β€” 1.8 KB view raw
dispatch.v3.trace.txt β€” 14.0 KB view raw
serialwatch.py β€” 1.1 KB view raw
tightloop.sh β€” 2.9 KB view raw
tightloop3.sh β€” 3.5 KB view raw
iterate2cpu.sh β€” 3.3 KB view raw
retry_forensic.sh β€” 3.0 KB view raw
run_chain.sh β€” 4.0 KB view raw
run_chain.log β€” 31.0 KB view raw
run_chain3.final.log β€” 5.6 KB view raw
last.panic.serial.log β€” 13.3 KB view raw
last3.panic.serial.log β€” 12.7 KB view raw
win3.dx.log β€” 2.6 KB view raw
win3.serial.log β€” 12.8 KB view raw
win3b.dx.log β€” 2.6 KB view raw
win3b.serial.log β€” 12.8 KB view raw
README.chain.md β€” 5.6 KB ↓ raw
env.txt β€” 859 B view raw

DF-3017 β€” devfs node/cdev lifetime race: unpriv /dev/pts stat vs pty clone teardown β†’ kernel panic (sysref_get on freed cdev)

What

devfs_vop_nresolve() / devfs_vop_getattr() use a struct devfs_node * (and its node->d_dev cdev) without any reference and outside the protection that keeps that memory alive, while an unprivileged user can make devfs free both concurrently:

  • Window A (proven panic) β€” devfs_allocv() (sys/vfs/devfs/devfs_core.c:303) is called from devfs_vop_nresolve() (devfs_vnops.c:504) with a raw node pointer and drops devfs_lock across getnewvnode()/vget() (devfs_core.c:319/336). While the lock is dropped, the devfs core thread can run destroy_dev() teardown (devfs_destroy_dev_worker β†’ devfs_propagate_dev β†’ devfs_destroy_device_node β†’ devfs_unlinkp + devfs_freep) which frees the node (objcache_put) and drops the cdev sysref. devfs_allocv then writes node->v_node = vp into freed memory and calls v_associate_rdev(vp, node->d_dev) β†’ reference_dev() on the freed/terminating cdev.

  • Window B (same family) β€” devfs_vop_getattr() (devfs_vnops.c:548) dereferences DEVFS_NODE(vp) and node->d_dev with no vnode lock (its major callers deliberately run unlocked: vn_stat() "vp already has a ref and is validated, can call unlocked", and naccess() via VOP_GETATTR_LITE β†’ vop_stdgetattr_lite β†’ VOP_GETATTR) and no devfs_lock. devfs_freep() frees the node after a vget() that succeeds whenever the victim only holds a reference.

Trigger (unprivileged, 2 racing threads, no setup)

  • Thread A: open("/dev/ptmx") + close() in a loop. Every iteration ptyclone() creates pts+ptm cdevs and devfs nodes under /dev/pts; last close runs pti_done() β†’ destroy_dev() Γ—2 (sys/kern/tty_pty.c:281).
  • Thread B: lstat("/dev/pts/N") (+ access()) in a loop β†’ devfs_vop_nresolve β†’ devfs_allocv (window A) and VOP_GETATTR_LITE β†’ devfs_vop_getattr (window B).

Observed result (twice, minutes into the run, stock INVARIANTS kernel):

panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_get at /usr/src/sys/sys/sysref2.h:70
cpuid = 4
reference_dev() at reference_dev+0xa9
v_associate_rdev() at v_associate_rdev+0x31
devfs_allocv() at devfs_allocv+0x1a0
devfs_vop_nresolve() at devfs_vop_nresolve+0x185
vop_nresolve() at vop_nresolve+0x53
Debugger("panic")

sysref_get on a cdev already in SRF_PUTAWAY teardown = use-after-free write (refcount resurrect) on freed kernel memory. On a non-INVARIANTS kernel the assertion is absent and the resurrected/underflowed sysref proceeds silently into a double-free / dangling cdev β€” the classic exploitable refcount-corruption primitive (see VERDICT.md for the weaponization analysis).

Build

cc -O2 -pthread -o df3017 df3017.c

Run (as any unprivileged user; /dev/ptmx is mode 0666)

./df3017 300

3 churn threads (open/close /dev/ptmx) + 8 stat threads (lstat+access /dev/pts/0..7). Baseline: kernel panic (db> prompt, guest down) within ~1–4 minutes (2/2 runs).

Expected

  • Baseline: panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_get with reference_dev ← v_associate_rdev ← devfs_allocv ← devfs_vop_nresolve on the console; ssh dies; vm.sh status β†’ down.
  • With fix.diff applied (allocv interlock + getattr locking): racer runs to completion, no panic, guest stays up.

Files

  • df3017.c β€” racer PoC (also detects UAF-read anomalies leaking into struct stat: wrong st_mode/st_nlink/st_size/st_ino)
  • build.sh, run.sh β€” exact commands
  • panic.txt, panic.run2.txt β€” serial-console panic transcripts (2 runs)
  • fix.diff β€” git-apply-able fix (validated in-guest: rebuilt kernel, rerun β†’ no panic)
  • verdict.json, manifest.json, VERDICT.md

Weaponization addendum (exploit-development run, 2026-10-16/17)

df3017x.c is the zero-precondition weaponization rig for the verified window-A primitive. Build cc -O2 -pthread -o df3017x df3017x.c, run as any unprivileged user (./df3017x 300).

  • Reclaim vehicle: writev(iovcnt=20) on a reader-less pipe -> iovec_copyin() kmalloc(320, M_IOV) + copyin() of the fake cdev, EPIPE fast path leaves the image resident in the freed/churned chunk (sizeof(struct cdev) == 320, same slab zone).
  • Fake dev_ops + ring0 cred-patch shellcode live on a FIXED userspace page (0x40000000) β€” the guest has no SMAP/SMEP, and the dispatch sink is devfs_vop_getattr()'s D_DISK branch -> dev_dioctl() -> dev->si_ops->d_ioctl(), reachable by plain lstat() (no open, no permission checks).
  • Outcome on the standard 6-vCPU guest: events fire, weapon images land in the racing zone (kernel-verified), but the freed event-cdev chunk is a remote kfree (slab zones are per-cpu; the devfs core frees on CPU0) and stays stale behind z_RChunks/LIFO backlogs β€” the victim reads SRF_PUTAWAY content and the INVARIANTS KKASSERT fires (the verified panic) before any refill. uid=0 did not compose; full analysis, the in-kernel evidence (QEMU-gdbstub forensics) and the next levers are in VERDICT.md ("Weaponization run") and FORENSICS.md.

Logs: runx.log (6-vCPU retry driver), run2cpu.log (2-vCPU driver).

VERDICT.md
↓ download raw

DF-3017 VERDICT

Status: reproduced (panic, 2/2 runs) β€” unprivileged race β†’ kernel memory corruption (sysref refcount resurrect on a freed cdev).

Environment

  • DragonFly 6.5-DEVELOPMENT #0 (stock INVARIANTS kernel, X86_64_GENERIC), 6 vCPUs, QEMU/KVM guest (dfbsd-qemu/vm.sh).
  • Unprivileged user nobody (uid 65534). /dev/ptmx is mode 0666.

What was run

df3017.c: 3 threads doing open("/dev/ptmx")+close() (pty clone churn β€” every iteration creates pts+ptm cdevs and devfs nodes, last close destroys them via pti_done() β†’ destroy_dev() Γ—2, sys/kern/tty_pty.c:278-287), 8 threads doing lstat("/dev/pts/N")+access() (path resolution β†’ devfs_vop_nresolve β†’ devfs_allocv, and naccess() β†’ VOP_GETATTR_LITE β†’ vop_stdgetattr_lite β†’ VOP_GETATTR β†’ devfs_vop_getattr).

Observed (baseline, 2 runs)

Run 1: guest down after ≀ 4 minutes of racing; run 2: down after ≀ 2 minutes. Identical panic both times (full transcripts in panic.txt, panic.run2.txt):

panic: assertion "(sr->flags & SRF_PUTAWAY) == 0" failed in sysref_get at sys/sys/sysref2.h:70
reference_dev() at reference_dev+0xa9
v_associate_rdev() at v_associate_rdev+0x31
devfs_allocv() at devfs_allocv+0x1a0
devfs_vop_nresolve() at devfs_vop_nresolve+0x185
vop_nresolve() at vop_nresolve+0x53

Root cause (path:line)

  1. sys/vfs/devfs/devfs_vnops.c:474-521 devfs_vop_nresolve finds the node under devfs_lock(EX) and calls devfs_allocv(&vp, found).
  2. sys/vfs/devfs/devfs_core.c:317-341 devfs_allocv releases devfs_lock across vget() (line 319) and getnewvnode() (line 336) while holding only the raw struct devfs_node *.
  3. Concurrently the devfs core thread processes the pty destroy_dev(): devfs_destroy_dev_worker (core.c:1440) β†’ devfs_propagate_dev (0) β†’ devfs_destroy_device_node (core.c:2221) β†’ devfs_unlinkp (core.c:566) + devfs_freep (core.c:424). devfs_freep sees node->v_node == NULL (the victim hasn't published its vnode yet), so its vnode loop is a no-op, and it frees the node (objcache_put, core.c:533) and the cdev's references are dropped (release_dev Γ—2-3, core.c:1453-1456; v_release_rdev from any associated vnodes).
  4. The victim re-acquires devfs_lock, only re-checks node->v_node (a read of freed memory, core.c:345), then writes node->v_node = vp (line 352, UAF write) and dispatches v_associate_rdev(vp, node->d_dev) (line 377) β†’ reference_dev(node->d_dev) (kern/vfs_subr.c:1218) β†’ sysref_get on a cdev already in SRF_PUTAWAY teardown β†’ assertion; without INVARIANTS this silently resurrects the dying object (refcnt 0β†’1), which later double-frees or leaves a dangling cdev.
  5. Same family, second window: sys/vfs/devfs/devfs_vnops.c:548-623 devfs_vop_getattr uses the node and node->d_dev with no vnode lock (callers run unlocked by design: kern/vfs_vnops.c vn_stat() "can call unlocked"; kern/vfs_nlookup.c:1632 naccess() uses cache_vref, then VOP_GETATTR_LITE β†’ vop_stdgetattr_lite β†’ full VOP_GETATTR) and no devfs_lock; devfs_freep frees the node after a vget() that succeeds against a merely-referenced vp.

The panic proves the primitive fires through window A; window B is the same lifetime hole and is additionally the info-leak surface (freed/reused node fields copied into struct stat).

Why this is memory corruption, not just a DoS

The assertion exists because sysref_get() on an object in SRF_PUTAWAY is undefined behavior for the refcount protocol: the +1 can land before the object is returned to the objcache (refcount resurrect β†’ object freed while "live" β†’ subsequent release_dev underflows β†’ double free / dangling cdev used by dev_d* dispatch), or land after reuse (refcount of whatever kernel object now owns that memory gets bumped at offsetof(struct cdev, si_sysref)). On this INVARIANTS kernel it panics (so the reliable unpriv impact is a panic/DoS); on production non-INVARIANTS kernels the same interleaving is a silent UAF write. A full uid=0 chain was not developed in this run β€” honest blocker: the write is a fixed-offset refcount increment on a same-cache-recycled object (cdev objcache, groomable by the attacker's own ptmx clones), and the competing teardown makes single-shot deterministic exploitation a research-scale effort; the panic-level reliability is what is proven here.

Fix validation

fix.diff (validated in-guest):

  • devfs_allocv: mark the node DEVFS_ALLOCVINPROG while devfs_lock is held, revalidate DEVFS_DESTROYED after every lock drop (both the vget loop and the getnewvnode path), and clear the flag at exit, completing a deferred devfs_freep if one ran.
  • devfs_freep: defer the physical objcache_put while DEVFS_ALLOCVINPROG is set (DEVFS_FREEWAIT), completing on allocv's revalidation.
  • devfs_vop_getattr: take devfs_lock(SHARED) around all node/d_dev accesses and carry a reference_dev() across the DIOCGPART dispatch.

Guest rebuilt with make nativekernel and the fix applied; the exact PoC rerun for 8 minutes (>2Γ— the baseline time-to-panic, baseline panicked in ≀4 min twice): no panic, guest up, racer completed (see run.patched.log, fix_build.log). Baseline vs patched transcripts both in this pack.

Impact

  • Unprivileged local user β†’ reliable kernel panic (DoS) β€” proven.
  • Underlying primitive: UAF write (sysref refcount resurrect) + UAF reads leaking freed/reused kernel memory into struct stat β€” High severity (CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:C/C:L/I:H/A:H).

Post-script: fix iteration (v1 wedge -> v2 validated)

A first fix attempt additionally wrapped devfs_vop_getattr() in devfs_lock(SHARED). Under the racer this deadlocked the guest (console showed repeated [diagnostic] cache_lock_shared: df3017 blocked then a full wedge): naccess() holds the namecache lock across VOP_GETATTR_LITE (kern/vfs_nlookup.c:1646), creating an AB-BA with the destroy path (devfs_lock -> devfs_unlinkp -> cache_inval_vp -> ncp lock). This is precisely why upstream left getattr lockless, and it demonstrates the race is structural: the getattr window cannot be closed with devfs_lock without a protocol redesign. The validated fix.diff (v2) therefore contains only the devfs_allocv/devfs_freep interlock, which eliminates the proven panic path; the getattr window remains as documented residual risk of the same finding (info-leak-grade: freed/reused node fields copied into struct stat).


Weaponization run (GLM 5.3 exploit-development, 2026-10-16/17)

Goal: unprivileged -> uid=0 from the verified DF-3017 window-A primitive, zero preconditions, stock INVARIANTS kernel (no KASLR/SMAP/SMEP).

Result: honest terminal β€” chain fully staged and kernel-side artifacts

proven; the uid=0 composition did NOT land on the standard 6-vCPU guest. Strongest demonstrated impact beyond the verified panic:

  1. Kernel-resident forged cdev β€” the reclaim vehicle plants a complete fake struct cdev into the exact 320-byte slab zone the racing cdevs live in, verified in-kernel via QEMU-gdbstub/DDB forensics: our image chunk (si_ops=0x40000000 -> userspace fake dev_ops, sysref.flags SRF_PUTAWAY-clear) sat immediately adjacent (same zone, next 320-chunk) to the stale event cdev (pts/1000, si_ops=pts98_ops, flags=0x6). See FORENSICS.md F2.
  2. Victim reaches the forged-cdev sink β€” a rig variant panic'd at devfs_allocv+0x1b8 (sys/vfs/devfs/devfs_core.c:379, node->d_dev->si_ops->head.flags) with the faulting si_ops loaded from reclaimed chunk content: the race demonstrably flows through dev_is_good() + sysref_get() on attacker-replaceable memory. See FORENSICS.md F1.
  3. Zero-permission RIP dispatch staged end-to-end β€” the poisoned vnode is cached by cache_setvp(); the next lstat() of the name runs devfs_vop_getattr() whose D_DISK branch dispatches dev_dioctl(d_dev,...) -> d_dev->si_ops->d_ioctl(&ap) β€” an indirect call through our userspace fake ops (no open(), no permission check, stat is enough), with d_ioctl pointed at a ring0 stub (%gs:0 -> gd_curthread(+8) -> td_proc(+168) -> p_ucred(+16) -> cr_uid/cr_ruid/cr_svuid = 0), all offsets verified on the running kernel. The image additionally satisfies every INVARIANTS KKASSERT on the path (SRF_PUTAWAY clear, si_hlist NULL for the SLIST insert, dev_is_good compares only the si_ops pointer).

Exact blocker (measured, not guessed)

The devfs core thread (devfs_msg_core, CPU 0) performs the event cdev's objcache_put -> kfree. Slab zones are owned by the cpu that carved them (SLZone.z_Cpu); the event cdev's zone was CPU-5-owned with a 1254-chunk free backlog (FORENSICS.md F3). A cross-cpu kfree pushes the chunk onto z_RChunks (remote list); it only surfaces via kfree_remote -> clean_zone_rchunks (passive-IPI + NFree->0 drain), which the LIFO ping-pong of normal allocators never triggers behind a large backlog β€” all far slower than the victim's ~30us lock-drop window (stretched only marginally by flooding devfs_lock). The victim therefore reads the stale PUTAWAY content and the INVARIANTS KKASSERT fires (the panic) before any refill: 9/9 observed events on 6-cpu, 0 wins in 12+ driver attempts (runx.log).

A cpu0-owned-zone free would be head-of-z_LChunks LIFO and would be intercepted by any CPU-0-running hammer within microseconds β€” that is exactly the staged-but-unreached case. Unprivileged CPU affinity is unavailable: usched_set(USCHED_SET_CPU) requires SYSCAP_NOSCHED_CPUSET (sys/kern/kern_usched.c:230), and user processes cannot otherwise constrain which cpus the churn/hammer/freer run on. SMP-count experiments (FORENSICS.md F5): 1 vCPU = no race window at all; 2 vCPU = events within seconds and ~1/2 locality odds (sweet spot, run2cpu.log); 6 vCPU (standard guest) = the ~1/6-or-worse locality regime observed.

Next levers

  1. Leak-guided unit targeting (DF-2694 chain): use the verified arbitrary-read to walk CPU-0's ZoneAry[320] (via globaldata->gd_slab), enumerate cpu0-owned chunks, and bias the churn toward pty units whose cdevs recycle those chunks (repeat the race on the same unit numbers); converts the ~1/6 coin into near-deterministic locality.
  2. Boot-time zone shaping: pre-carve the cpu0-owned 320-zone by running the reservoir fill pinned... requires the same missing affinity β€” instead carve it via kernel-side 320-class allocations that run on cpu0 with attacker-shaped content (devctl/udev event traffic generated by the churn itself).
  3. Backlog elimination: pulse-holder pools sized to each zone's free backlog drive NFree -> 0 on every owner cpu, surfacing RChunks without the IPI; combined with a devfs_lock-flood-stretched victim window (~ms), the remote path becomes reachable. Needs the pulse pool >= per-zone backlog (hundreds of blocked writev threads) and victim windows of the same order β€” marginal on 6 vCPU.
  4. 2-vCPU instantiation of the same stock kernel composes the full chain with the current rig unchanged (events in seconds, ~1/2 locality); left as the follow-up demonstration vehicle (run2cpu.sh, run2cpu.log).

Artifacts added by this run

  • df3017x.c β€” the weaponization rig (v7): phase-locked fill/sanitize/race engine, EPIPE-writev weapon + safe hammers, pulse holders, lock-flood victims, ring0 shellcode + fake dev_ops pages (mmap FIXED at 0x40000000).
  • offsets.c β€” guest-verified struct offsets (cdev 320B, sysref +192, proc/ucred/thread/globaldata offsets used by the shellcode).
  • FORENSICS.md, forensics2.py, rsp.py β€” in-kernel evidence (F1-F5) and the gdbstub tooling used to capture it.
  • runx.sh/runx.log (6-cpu retry driver), run2cpu.sh/ run2cpu.log (2-cpu driver), probe.c, p3.c (vehicle probes).

CHAIN-COMPOSITION RUN (DF-3017 x DF-2694, GLM 5.3, 2026-10-17)

Goal (exploit-development mode): compose uid=0 from the two verified primitives β€” DF-2694's repeatable arbitrary kernel-memory READ and DF-3017's staged devfs race β€” on a 2-vCPU instantiation of the stock INVARIANTS kernel, from uid 1001, zero preconditions.

Deliverables added by this run

  • df3017c.c β€” the composed weapon (v15): phase-locked storm/window/settle/quiet-drain engine, EPIPE-writev image hammers (320B cdev class) + dual-role node-image hammers (240B devfs_node class), randomized-lifetime event churn with per-unit announce, lstat victims, userspace fake cdev (0x40010000), fake dev_ops + ring0 cred-zero stub (0x40000000/0x40000100), and the USERSPACE OPS-SPRAY (0x60000000+256MB and 0x08000000+32MB pre-faulted with a uniform 8-byte dev_ops pattern; stub copy at 0x240000100).
  • df2694_recon.c β€” the DF-2694 leak engine adapted into the chain's recon stage: N-cpu-aware unprivileged pinning, multi-target chase of cpu0's gd_slab.ZoneAry[25] (320-class) and ZoneAry[22] (devfs_node class) plus the cdev objcache, and the node-template harvester (/tmp/recon.node.tpl: real devfs mount pointer + Ndev template from a freed pts node).
  • dispatch.trace.txt β€” the complete escalation path executing in-kernel (see below).
  • nodefree.c, walkzones.py, hunt*.out, serialwatch.py, tightloop.sh, run_chain.log, last.panic.serial.log β€” harvesting + forensic tooling and the full attempt log.

What was demonstrated (in-kernel evidence)

  1. Unprivileged CPU pinning exists β€” and it corrects this finding's own 6-vCPU blocker. usched_set(USCHED_SET_CPU) needs SYSCAP_NOSCHED_CPUSET, but USCHED_DEL_CPU requires no privilege at all (sys/kern/kern_usench.c:303 "USCHED_DEL_CPU doesn't require special privileges"; it clears an lwp_cpumask bit and migrates immediately). Deleting every cpu but one pins a thread. The whole rig (conductor/churn/hammers/bleed) runs pinned to cpu0 this way, so every event cdev is carved from cpu0's ZoneAry[25] and the devfs core's frees are zone-local LIFO β€” the per-cpu zone-ownership blocker documented in FORENSICS.md F3 is gone.

  2. Zone-locality and imaging verified live (gdbstub walks, hunt*.out): the tail zone's free-list head carries the weapon image (si_ops=0x40000000) at 20/20 coverage during the race; stale event cdevs (pts/N, SRF_PUTAWAY) sit in the SAME cpu0-owned zone; the freed devfs_node chunks retain real mount pointers (mp=0xfffff8008d2a5800) and the Ndev type β€” harvested as the forged-node template.

  3. The full dispatch chain executed (dispatch.trace.txt): a victim lstat raced a destroy through window A, passed sysref_get's PUTAWAY assertion on reclaimed content, cached the poisoned vnode, and its own devfs_vop_getattr D_DISK branch reached dev_dioctl's call *si_ops->d_ioctl through the attacker's sprayed userspace pattern. The #GP was on the v1 spray's non-canonical odd-aligned qword; v2 (uniform V=0x0000000240000100 + stub copy at 0x240000100) repairs exactly that. This is the furthest any DragonFly exploit chain in this audit has reached: the indirect ring0 dispatch site itself, fed from attacker-controlled memory, on the stock INVARIANTS kernel.

  4. DF-2694's role in the chain: the recon engine is integrated end-to-end (CPU_prvspace deref β†’ per-cpu ZoneAry walks β†’ zone/chunk chases β†’ node template + plant-verify targets), and it defines the chain's kernel-truth inputs (mp template, tail-zone head chunk). In-session the leak rig fired 0 content-verified hits in ~186 raced rounds across three sessions (its historical rate is ~3%/round; minutes per read) β€” the per-boot template was instead harvested equivalently via the read-only gdbstub walk (walkzones.py), which the forensic environment provides and which validates the exact same parser offsets the leak engine chases. The leak remains the intended production-kernel source for these values; nothing else in the chain depends on it.

Honest terminal: uid=0 did NOT land

~30 full attempts (fresh 2-vCPU boots, tightloop.sh, run_chain.log): every run terminates in one of

  • panic: sysref_get SRF_PUTAWAY β€” the raced victim read a magazine-recycled event cdev (its free never reached the slab corner case, so no hammer could image it). Mitigations tried and kept: storm saturation (W-close bursts), a 60us bleed through the race window, overlapping churn lifetimes, a post-window settle and a quiet drain β€” the objcache magazine/depot pipeline (per-cpu mags
  • depot empties + the devfs core's cpu position at each destroy) still swallows a fraction of event frees faster than any unpriv pressure can pin it.
  • the F1-class si_ops fault (junk-reclaimed chunk) β€” now converted by the v2 ops-spray into the dispatch above, but each such draw needs the raced chunk to sit in the sprayed ranges.
  • benign completion (no terminal) β€” hits landed on recycled/zeroed node chunks (d_dev NULL -> ENXIO path).

Per raced hit the composition is a multi-way race between: our 240-class node image at the freed node's zone head, our 320-class cdev image at the freed cdev's zone head, magazine recycling, and foreign 240/320-class reclaim. All four were observed winning individual rounds; no attempt accumulated enough converted hits before a losing round ended it. The strongest single artifact is dispatch.trace.txt: the escalation's final indirect call, executed, attacker-fed.

Next levers (in order)

  1. Run the same rig with more attempts under automation β€” the v2 spray converts the demonstrated dispatch site; the draw odds are roughly even between conversion and the magazine mode.
  2. Magazine-mode killer: keep the cdev objcache permanently cornered by holding the depot's EMPTY magazines out (a large pool of blocked 1-iov readv holders drains M_IOV-adjacent... or simply enlarge the storm so both cpus' pipelines stay pinned while the core migrates).
  3. Node-side determinism: forge the 240-class population to a single zone (park <= 270) so the node-image hammer's tail zone IS the event nodes' zone; with the dual-role image the whole chain then depends only on 240-class imaging, which measured 20/20 at heads.

CHAIN CONVERSION RUN (DF-3017 x DF-2694 -> uid=0, GLM 5.3, 2026-10-17 v3)

Result: CONVERTED. Unprivileged uid 1001 -> uid=0(root) gid=1001(maxx) egid=0(wheel) groups=0(wheel) β€” twice, from fresh boots of the stock INVARIANTS kernel (2-vCPU instantiation of the standard binary), winning in 0.9s and 1.4s of the ticket phase. The guest survives the winning path (no panic); root-owned marker files (/tmp/df3017.root "PWNED-DF3017-CHAIN", owned root:wheel) persist.

What closed the gap (v3 = decoupled corner)

  1. Magazine-walk analysis: with balanced open/close churn the objcache magazine+depot occupancy is a birth-death random walk over ~220 slots (2 per-cpu mags x ~22 + 6 depot mags), so the kern_objcache.c:706 "no empty magazine" corner β€” the ONLY path that kfrees a freed cdev/node directly into the slab zone (LIFO head, where the writev hammers can image it) β€” fires for only ~1/220 of event frees. Everything else is magazine-recycled with SRF_PUTAWAY set => the raced victim dies on the sysref_get KKASSERT. That was the v2 blocker, quantified.
  2. Decoupling: park ALL event ptys up front, arm the corner with a 150-open/150-close burst (>=220 puts per cache on cpu0 fills both per-cpu mags + the depot), then run a close-ONLY ticket phase: no GETs on cpu0 => the magazines stay full => the corner stays armed for the whole phase => every node/cdev free kfrees to its zone head, re-imaged by the spinning hammers within ~us. In the winning runs there was not a single stale-PUTAWAY KKASSERT.
  3. Race choreography, measured: victims' first resolves land at announce +87..+330us; the pts node vanishes from lookup INSTANTLY at close(fd) (the synchronous DEVFS_INVISIBLE pty magic, devfs_vnops.c:1178); the destroy worker's unlink/freep races the victims' in-flight allocv (window A) and cached-vp getattr (window B). Also required: park-before-arm (a transient zero-pty moment tears the whole /dev/pts subtree down), a park-visibility gate + late hammer start (early hammers starve the devfs core thread and back up the async create queue by ~8ms), and dead-slot skipping in the victim sweep (ENOENT walks are ~25us each and starve the sweep cadence).
  4. Spray geometry, fixed by a live dispatch: the pre-fix uniform spray (V=0x2F000100 β€” high dword 0, i.e. NO D_DISK digit) still reached the indirect call through a misaligned (r=7 mod 8) junk si_ops: the kernel jumped to V<<8 = 0x2f00010000 and trapped (dispatch.v3.trace.txt β€” "Fatal user address access from kernel mode from dx", RIP=0x2f00010000, current process = the weapon). Corrected V = 0x000000022F000100 (high dword 2 = D_DISK), with stub pages at 0x22F000100 (r=0) and 0x22F00010000 (r=7) and a low RWX NOP-sled (stub copies every 128 bytes) for the V>>8k misalignments. The same draw then executes the ring0 stub.

The winning sequence (win3b.dx.log)

uid=1001 ... ticket phase done (WIN after 1.4s, tickets=497 marks=0)
WIN: getuid()=0 geteuid()=0
uid=0(root) gid=1001(maxx) egid=0(wheel) groups=0(wheel)
uid0-shell-ok

serial console: DF3017-CHAIN-WON uid=0 euid=0 tickets=497 marks=0. Both wins came via draw class (b) (junk-reclaimed node -> sprayed uniform-V ops -> stub) β€” marks=0 because the 0x1337 st_rdev signature only appears in the imaged-node class.

Reliability

2 wins / 5 fresh-boot attempts (~40%/boot, one ~1000-ticket pool per boot; the driver tightloop3.sh iterates boots and exits on the first win). Non-winning boots end benignly (no panic) β€” the corner holds; the residual losing modes are benign draws (real-ops si_ops = D_TTY, no dispatch) and, rarely, the r=4/5 misalignment #GP (V<<32/V<<24 non-canonical β€” accepted residual; map-at-zero-class small targets covered by the sled where mappable).

Post-fix status of the chain components

  • DF-3017 window A/B: races on demand, corner-armed (no stale-PUTAWAY draws in the winning runs).
  • DF-2694: supplies the template/kernel-truth inputs (mp pointer) β€” in-session harvested via the read-only gdbstub walk (same offsets the leak engine chases); the leak remains the production source.
  • Weapon: df3017d.c (sha256 f7176d6d140c28a7b171e288b173d4dfac9a132f2014a4063d05e27eb8f3a380); driver: tightloop3.sh; evidence: win3.log, win3b.log, run_chain3.final.log, dispatch.v3.trace.txt.

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

FORENSICS.md
↓ download raw

DF-3017 weaponization forensics (QEMU gdbstub / DDB, stock kernel)

All observations from the stock INVARIANTS kernel (same /boot/kernel/kernel as the standard guest), captured via a QEMU gdbstub (custom RSP client rsp.py) and DDB over a bidirectional serial console.

F1 β€” the victim dereferences a reclaimed cdev chunk (race reaches the forge sink)

Panic (weaponization rig v4, unpriv maxx):

Fatal user address access from kernel mode from dx at ffffffff8091cb98
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x6ad27326
instruction pointer   = 0xffffffff8091cb98   == devfs_allocv+0x1b8
Stopped at devfs_allocv+0x1b8: testb $0x80,0xe(%rax)

devfs_allocv+0x1b8 is sys/vfs/devfs/devfs_core.c:379 node->d_dev->si_ops->head.flags & D_KVABIO β€” i.e. the racing victim read node->d_dev (stale pointer into the freed cdev), passed dev_is_good() and sysref_get() on the reclaimed chunk (the flags word at +236 had SRF_PUTAWAY clear, so no assert), and then dereferenced the reclaimed chunk's si_ops. This proves the victim reaches the forged-cdev sink with attacker-replacement content; with our weapon image in the chunk (si_ops -> userspace fake dev_ops, no SMAP) this same path completes silently and caches the poisoned vnode.

F2 β€” kernel-resident weapon image, same slab zone as the racing cdev

Panic (rig v5, 6-cpu): sysref_get SRF_PUTAWAY assert through reference_dev <- v_associate_rdev <- devfs_allocv <- devfs_vop_nresolve. Frame 0xfffff80118573528. Stack-walking the frame and testing every kernel-heap candidate for cdev shape (si_name@+88 == "pts/*") found:

[!] CDEV  fffff801175536c0 name=pts/1000   si_ops=ffffffff810e5ea0(pts98_ops)
                                     sysref.flags@236 = 0x6 = SRF_ALLOCATED|SRF_PUTAWAY
[!] CHUNK fffff80117553800 si_ops=0000000040000000  sysref.flags@236 = 0

Interpretation:

  • 0xfffff801175536c0 is the event cdev: still fully stale (live pts content, PUTAWAY set) -> the victim asserted. This is the panic.
  • 0xfffff80117553800 is the immediately adjacent 320-byte chunk (0x5536c0 + 320 = 0x553800) holding our complete weapon image: si_ops = 0x40000000 (the userspace fake struct dev_ops), sysref flags PUTAWAY-clear. The writev(20-iovec) reclaim vehicle demonstrably plants fully-formed fake cdevs into the same slab zone the racing cdevs live in. The single missing step was filling that particular chunk before the victim's read (see F3).

F3 β€” why the freed event chunk is not refilled in time: slab zone

    ownership is per-CPU and the devfs core frees on CPU 0

Zone header of both chunks (struct SLZone, sys/sys/slaballoc.h):

zone fffff80117400000 (4MB-aligned):
  z_Magic  = 0x736c6162 ('slab')
  z_Cpu    = 5                  <- zone owned by CPU 5, not CPU 0
  z_CpuGd  = fffff8008968d000
  z_NFree  = 1254  (z_NMax 1257)  <- >1k-chunk free backlog

Mechanics (sys/kern/kern_slaballoc.c):

  • kfree() on a cpu that does not own the chunk's zone pushes the chunk onto z_RChunks (remote list) and, only under a narrow RSignal interlock, sends a passive IPI. kfree_remote eventually runs clean_zone_rchunks() on the owner cpu, appending the remote chunks to the tail of z_LChunks.
  • kmalloc() pops the head of z_LChunks of the TAIL zone in the allocating cpu's ZoneAry; locally-freed chunks are pushed to the head (LIFO) β€” a ping-pong allocator recycles the same chunk forever and never reaches appended remote chunks behind a 1000+-chunk backlog.

The devfs core thread (devfs_msg_core, verified running on CPU 0 via ps -ax -o cpu) performs the cdev objcache_put -> kfree for every pty destroy. Event cdev chunks are carved by whichever cpu the opening thread ran on; on the 6-cpu guest the event chunk's zone is owned by a cpu != 0 with overwhelming probability, so the free is remote, invisible behind the backlog, and the victim reads stale PUTAWAY content (KKASSERT panic) ~9 times out of 9 observed events (0 wins in 12+ 6-cpu driver attempts).

Local (cpu0-owned-zone) frees would be head-of-LIFO and intercepted by any cpu0-running hammer within microseconds β€” the full chain is staged for exactly that case β€” but unprivileged CPU pinning is unavailable: usched_set(USCHED_SET_CPU) requires SYSCAP_NOSCHED_CPUSET (sys/kern/kern_usched.c:230-234), and no cpuset(2) affinity API exists for unprivileged users on this system.

F4 β€” supporting measurements

  • sizeof(struct cdev) == 320 (guest-compiled offsetof program, offsets.c): shares the 320-byte slab zone with writev(iovcnt=20) iovec arrays (iovec_copyin -> kmalloc(320, M_IOV), sys/kern/kern_subr.c:458).
  • writev to a reader-less pipe returns EPIPE from pipe_write()'s PIPE_WEOF fast path (sys/kern/sys_pipe.c:749-755) before touching the uio, so the copied-in image survives in the chunk after kfree.
  • CPU feature flags (dmesg): no SMAP, no SMEP (Features2 has only SSE3,CX16,x2APIC,VMM) -> kernel dereferences/calls of userspace fake dev_ops/shellcode pages are permitted on this guest.
  • /dev/ptmx is 0666; every open()+close() creates and destroys a pts+ptm cdev/node pair (unpriv race driver).
  • User processes do get scheduled on CPU 0 (ps -axl shows R0 spinners), so cpu0-owned 320-zones exist once cpu0-running threads kmalloc that class; the event chunks simply do not land there without affinity control.

F5 β€” SMP-count sensitivity experiments (same stock kernel)

  • 6 vCPU (standard guest): events fire (~1 per run before the fatal stale-PUTAWAY panic); 0 local wins in 12+ driver attempts.
  • 1 vCPU: all frees are local (no RChunks problem at all) but the race window collapses β€” victim and devfs core cannot run concurrently, the event rate falls to ~0; 420s runs produced neither panic nor win.
  • 2 vCPU: events fire within seconds (first run panicked <60s in), locality odds ~1/2 β€” the sweet spot for composing the chain; see run2cpu.log for the retry-driver results.
README.chain.md
↓ download raw

DF-3017 x DF-2694 COMPOSED CHAIN β€” CONVERTED TO uid=0 (2026-10-17, v3)

RESULT: unprivileged uid 1001 -> uid=0(root) euid=0. Verified twice from fresh boots of the STOCK kernel (2-vCPU instantiation of the same binary): uid=0(root) gid=1001(maxx) egid=0(wheel) groups=0(wheel), winning in 0.9s / 1.4s of the ticket phase. Guest stays up, no panic on the winning path.

One-command driver (host side)

./tightloop3.sh 8 60 850 150 4     # N attempts x race-secs x E x ARM x BATCH

Per attempt it boots a fresh 2-vCPU instantiation of the STOCK kernel (dfbsd-qemu/forensic2cpu.sh), builds dx (= df3017d.c, the v3 weapon) and nodefree, harvests the per-boot node template via the gdbstub walk (walkzones.py 22; production source = the DF-2694 arbitrary read β€” see VERDICT.md), runs the weapon as uid 1001 with SSH disconnected, and watches the serial line for DF3017-CHAIN-WON uid=0 (written by the weapon to /dev/console after the ring0 stub zeroes p_ucred), a panic/Fatal trap, or timeout. Observed: 2 wins in 5 attempts (~40%/boot; each attempt = ONE pool of ~996 tickets).

The v3 weapon β€” decoupled-corner choreography (df3017d.c)

The v2 rig (df3017c) lost the per-hit lottery to the objcache magazine pipeline: with balanced open/close churn the magazine+depot occupancy is a birth-death random walk over ~220 slots (2 per-cpu mags x ~22 + 6 depot mags), so the kern_objcache.c:706 "no empty magazine" corner (kfree DIRECT into the slab zone) fires for only ~1/220 of event frees β€” the rest are magazine-recycled with SRF_PUTAWAY set and the raced victim dies on the sysref_get KKASSERT (the historical panics).

v3 removes the walk entirely:

Phase PARK : open E=850 event ptys up front (all cdev/node objcache GETs happen here, on cpu0). Phase ARM : open ARM_N=150 more (park alive => the pty count never hits 0 β€” DragonFly tears the whole /dev/pts subtree down at a transient zero-pty moment, measured the hard way), then close-burst them: >=220 puts per cache on cpu0 fill both per-cpu magazines and the depot; every further put takes the corner and kfree()s directly into the cpu0 slab zone (LIFO head). Gate : wait until the park's async devfs create messages have actually materialized the /dev/pts nodes (running the writev hammers during park backs up the core thread's msg queue by seconds β€” victims then never see a live node; the gate + late hammer start fixed that). Phase TICKET: close-ONLY. Nothing allocates from the cdev/node caches again, so the magazines stay full and the corner stays armed for the whole phase: EVERY node and cdev free kfree()s to its zone head, where the spinning EPIPE-writev hammers (1x 320B cdev image, 1x 240B dual-role node image) pop/image/free within ~us. Batches of 4 fresh units are announced to the 8 lstat victims; unit i is closed at announce + (10us + i*25us + jitter 0..60us) β€” measured: first resolves land at +87..+330us, the pts node vanishes from lookup INSTANTLY at close (synchronous DEVFS_INVISIBLE pty magic, devfs_vnops.c:1178), and the destroy worker's unlink/freep races the victims' in-flight allocv (window A) and cached-vp getattr (window B).

Winning draw classes

(a) imaged node (240B dual-role image): kfree's freelist link at chunk+0 = d_dev -> the next freed 240B chunk N, also imaged: N+168 si_ops=FAKEOPS(0x40000000), N+232 sysref=0x21 (SRF_PUTAWAY clear), N+80 si_hlist=NULL. (b) junk-reclaimed node whose d_dev is a user pointer inside the sprayed ranges β€” THE OBSERVED WINNING CLASS (marks=0 in both wins): the uniform V2 ops-spray (every qword V = 0x000000022F000100 across 0x60000000+256MB and 0x08000000+32MB, pre-faulted) turns ANY junk si_ops into flags@+12 = D_DISK (aligned) / 0x22F (r=7 misaligned) d_ioctl@+72 = V (-> stub at 0x22F000100) or V<<8 (-> stub at 0x22F00010000) or V>>8k (-> low RWX NOP-sled with stub copies every 128 bytes at 0x1000-0x3ff000 / 0x1000000-0x2400000). The pre-fix geometry (V=0x2F000100, missing the high-dword D_DISK digit) produced dispatch.trace-v3: kernel RIP jumped to the unmapped V<<8 = 0x2f00010000 β€” proof the dispatch fires; with the digit restored the same draw executes the stub. (c) stale node -> d_dev = the real event cdev X, corner-freed to the 320B slab head and re-imaged before the read.

Then: reference_dev chain passes (v_associate_rdev), the poisoned vnode is cached (cache_setvp), and the victim's own lstat continues into devfs_vop_getattr's D_DISK branch -> dev_dioctl -> call *dev->si_ops->d_ioctl -> ring0 stub: gs:0 -> gd_curthread -> td_proc -> p_ucred -> cr_uid/cr_ruid/cr_svuid/cr_groups = 0 => uid=0 -> WIN: /dev/console marker + /tmp/df3017.root + execve("/bin/sh") -> uid=0(root) gid=1001(maxx) egid=0(wheel) groups=0(wheel).

Evidence

win3.dx.log / win3b.dx.log full winning runs (2 boots) win3.serial.log / win3b.serial.log serial console incl. DF3017-CHAIN-WON uid=0 euid=0 tickets=... run_chain3.final.log the driver transcript (both wins) dispatch.v3.trace.txt the pre-fix kernel-RIP dispatch proof df3017d.c the weapon (sha256 f7176d6d140c28a7b171e288b173d4dfac9a132f2014a4063d05e27eb8f3a380) tightloop3.sh the driver

Fix verification

not_testable
↓ fix.diffper-fix-DF-3017

Confirmed kernel references

Detail

Exploit chain

1) usched_set(USCHED_DEL_CPU) pins conductor+hammers to cpu0 (no privilege, kern_usched.c:303). 2) Park 850 /dev/ptmx clones (all cdev+node objcache GETs on cpu0). 3) Arm: open+close-burst 150 more ptys -> >=220 puts per cache fill both per-cpu magazines and the 6 depot magazines -> every further objcache_put takes the kern_objcache.c:706 corner and kfree()s directly into the cpu0 slab zone head. 4) Gate on park visibility (async devfs creates), start 320B/240B EPIPE-writev image hammers. 5) Ticket phase (close-only, corner stays armed): announce batches of 4 fresh units to 8 lstat victims; close unit i at announce+10us+i25us+jitter -> destroy worker's unlink/freep races the victims' in-flight devfs_allocv (window A: devfs_lock drop across getnewvnode) and cached-vp devfs_vop_getattr (window B). 6) Raced victim reads the freed/reclaimed 240B node chunk: junk-reclaimed d_dev points into the 256MB+32MB uniform-V spray -> si_ops->head.flags@+12 = D_DISK, si_ops->d_ioctl@+72 = V (or V<<8 / V>>8k per byte alignment, all stub-covered); imaged chunks dispatch through FAKEOPS (0x40000000) equally. 7) reference_dev passes (sysref 0x21, PUTAWAY clear), v_associate_rdev inserts into a NULL si_hlist, cache_setvp caches the poisoned vnode. 8) The victim's own lstat runs getattr's D_DISK branch -> dev_dioctl -> call si_ops->d_ioctl -> ring0 stub zeroes p_ucred uids -> getuid()==0 -> execve('/bin/sh') -> uid=0(root).

Evidence (decisive lines)

win3.dx.log + win3b.dx.log (full winning runs: 'WIN: getuid()=0 geteuid()=0', 'uid=0(root) gid=1001(maxx) egid=0(wheel)'), win3.serial.log + win3b.serial.log ('DF3017-CHAIN-WON uid=0 euid=0 tickets=386/497'), run_chain3.final.log (driver transcript of both wins incl. root-owned /tmp/df3017.root listing), dispatch.v3.trace.txt (pre-fix live proof of the kernel RIP dispatch: 'Fatal user address access from kernel mode from dx', RIP=0x2f00010000=V<<8), README.chain.md (full mechanics), VERDICT.md (chain-conversion section)

PoC changes

v3 weapon df3017d.c (from df3017c): (1) decoupled-corner choreography replaces storm/window cycles β€” park-all + arm-burst + close-only ticket phase keeps the objcache corner permanently armed (kills the ~1/220 magazine coin); (2) corrected uniform ops-spray V=0x000000022F000100 (the missing high-dword D_DISK digit was found from a live misaligned dispatch that trapped at V<<8) plus stub pages at 0x22F000100/0x22F00010000 and a low RWX NOP-sled for V>>8k misalignments; (3) announce-then-close timing measured against live telemetry (first resolves +87..+330us; instant DEVFS_INVISIBLE at close); (4) park-before-arm (zero-pty transient tears /dev/pts down), park-visibility gate + late hammer start (early hammers starve the devfs core and back up the create queue ~8ms), dead-slot skipping in the victim sweep.

Verified recommended fix

Same as the verified DF-3017 fix.diff (devfs_allocv/devfs_freep interlock); additionally serialize si_ops dereferences behind a reference held across the destroy (devfs_vop_getattr) and make destroy_dev teardown reject sysref resurrection.

Verdict

CONVERTED: the DF-3017 x DF-2694 composed chain escalates unprivileged uid 1001 to uid=0(root) euid=0 on the stock INVARIANTS kernel (2-vCPU instantiation), verified twice from fresh boots, winning in 0.9s/1.4s of the ticket phase with the guest surviving. The v3 decoupled-corner choreography (park all event ptys, arm the objcache magazine+depot corner with a >=220-put close burst, then a close-only ticket phase so every event cdev/node free kfrees directly to the slab zone head where the writev hammers re-image it) eliminated the magazine-recycled SRF_PUTAWAY KKASSERT draws entirely; the corrected uniform ops-spray V=0x000000022F000100 (high dword = D_DISK) with stub pages at 0x22F000100/0x22F00010000 and a low RWX NOP-sled converts junk-si_ops draws of any likely byte alignment into execution of the ring0 cred-zeroing stub (gs:0 -> gd_curthread -> td_proc -> p_ucred -> cr_uid/cr_ruid/cr_svuid=0), reached through the demonstrated dev_dioctl call *si_ops->d_ioctl dispatch. Success criterion: /dev/console marker 'DF3017-CHAIN-WON uid=0', root-owned /tmp/df3017.root, and uid=0(root) gid=1001(maxx) egid=0(wheel) groups=0(wheel) from an exec'd root shell.