DF-4322 / verdict.json
{ "finding_id": "DF-4322", "title": "dsp_clone() hands clone cdevs through a shared `static struct cdev *dev` -- concurrent clone handlers (one per pcm unit) race; an open of /dev/dspN can be given another cards clone cdev (demonstrated unprivileged), and the stale static can hand devfs a destroyed cdev during pcm_unregister (traced UAF)", "status": "reproduced", "reproduced": 1, "impact": "none", "confidence": "certain", "verdict": "Reproduced on DragonFly 6.5-DEVELOPMENT (X86_64_GENERIC) with two QEMU sound cards (pcm0=ES1370, pcm1=intel-hda). dsp_clone() stores the outgoing cdev in a function-static global (dsp.c:2381); devfs runs clone handlers with devfs_lock dropped (devfs_core.c:2329-2336) and each pcm unit has independent locks, so handlers for different units run concurrently while sharing that global. An unprivileged open loop (4+4 threads, ~118k opens) produced a /dev/dsp1 open whose fd carried card 0s clone cdev (minor unit byte 0, SNDCTL_AUDIOINFO devnode /dev/dsp0.vp0, channel pcm0:virtual:dsp0.vp0). Three further ~36-56k-open runs were hit-free, characterizing the race as rare (~1e-5 per open) but real. Demonstrated impact is cross-card channel confusion; both dsp nodes are 0666 by default so no permission boundary is crossed on a default box, hence impact=none in the enum (logic defect). Traced but not demonstrated: snd_clone_alloc()s early NULL returns (clone.c:627-629) do not set *dev, so a dsp_clone() racing pcm_unregister()s snd_clone_disable() (sound.c:1200) can return the stale static -- potentially a cdev already destroyed by snd_clone_destroy/GC (sound.c:1226, clone.c:431/457) -- handing devfs a freed cdev (UAF/panic ceiling, requires root-side detach concurrency).", "exploit_chain": "Unprivileged: open(\"/dev/dsp0\") and open(\"/dev/dsp1\") in parallel loops -> devfs drops devfs_lock and invokes both dsp_clone() handlers concurrently -> shared static `dev` last-writer-wins -> wrong cards clone cdev returned to devfs -> open(\"/dev/dsp1\") yields a pcm0 channel (proved via fstat minor + SNDCTL_AUDIOINFO devnode).", "evidence": "run.log (decisive run: RACE HIT: opened /dev/dsp1, fd is clone of card 0 (devnode /dev/dsp0.vp0, name pcm0:virtual:dsp0.vp0), 117879 cycles, 1 mismatch); run.2/3/4.log (hit-free characterization); VERDICT.md (full trace with dsp.c/clone.c/sound.c/devfs_core.c line refs); fix.diff (one-line fix: make `dev` local, matching FreeBSD)", "kernel_refs": [ "sys/dev/sound/pcm/dsp.c:2381", "sys/dev/sound/pcm/dsp.c:2495", "sys/dev/sound/pcm/dsp.c:2506", "sys/dev/sound/pcm/dsp.c:2520", "sys/dev/sound/clone.c:627", "sys/dev/sound/clone.c:794", "sys/vfs/devfs/devfs_core.c:2329", "sys/dev/sound/pcm/sound.c:1138", "sys/dev/sound/pcm/sound.c:1200", "sys/dev/sound/pcm/sound.c:1226" ], "poc_changes": "none -- PoC authored fresh for this finding (fstat minor-byte check per open for speed; SNDCTL_AUDIOINFO confirmation on mismatch)", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 540, "guest_dirty": 1, "build_cmd": "cc -O2 -pthread -o /tmp/dsp_race /tmp/dsp_race.c", "run_cmd": "/tmp/dsp_race (as unprivileged user, two pcm units registered)", "code_hash": "28250295ae4cbb00a1ffa0c2d019a9b9cf965f57df072db9631ee1797f673d3f", "notes": "Guest launched with dfbsd-qemu/vm_snd.sh (ES1370 + intel-hda + hda-duplex); modules loaded as root (kldload snd_es137x snd_hda); the PoC itself runs unprivileged (uid 1001). Fix validation skipped as impractical: the race fires ~1e-5 per open, so proving the patched negative in one kernel build cycle is not statistically meaningful; the fix is structural (removes the shared state entirely, identical to FreeBSDs code).", "recommended_fix": "Make the outgoing cdev a local variable in dsp_clone(): `struct cdev *dev = NULL;` instead of `static struct cdev *dev = NULL;` (dsp.c:2381), matching FreeBSD.", "fix_status": "not_testable", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Structural one-line fix (localize `dev`) eliminates the shared state; kernel-build fix validation was not performed because the baseline race probability (~1 hit per 1e5 opens) makes a single patched run statistically uninformative.", "fix_evidence": "fix.diff; VERDICT.md root-cause section" } |