DF-3880 / verdict.json
{ "finding_id": "DF-3880", "status": "not_reproduced", "reproduced": 0, "impact": "none", "confidence": "likely", "verdict": "Source-certain use-after-free race in dsp_oss_syncstart (sys/dev/sound/pcm/dsp.c:2992-3082), the SNDCTL_DSP_SYNCSTART handler, triggerable by any unprivileged user on machines with a PCM sound device (/dev/dsp* 0666). The member-walk retry loop calls lksleep(sm, &snd_pcm_syncgroups_mtx, PCATCH, \"pcmsg\", 5ms) at dsp.c:3042-3043 when CHN_TRYLOCK(sm->ch) fails; lksleep DROPS the syncgroup lock while asleep, so a concurrent successful SYNCSTART of the same sg_id on another thread can complete the whole teardown - start every member, SLIST_REMOVE_HEAD + kfree() each sm and SLIST_REMOVE + kfree() the sg itself (dsp.c:3056-3069) - inside the 5ms window. The plain timeout wake (lksleep returns 0, no signal) executes needlocks = 1; ret = 0; (:3047-3048) and the inner SLIST_FOREACH then CONTINUES from the freed sm: SLIST_NEXT(sm, link) reads freed M_DEVBUF memory and CHN_TRYLOCK(sm->ch) loads and locks a channel pointer read out of freed memory - kernel UAF (panic or exploitable corruption via heap re-spray). Only the EINTR/ERESTART signal path breaks out safely. The trylock failure that forces the sleep is attacker-arranged by keeping the member channels lock contended (a looping/blocking write(2) on the same channel, as in the PoCs writer thread). Distinct from known DF-1263 (syncgroup error-path free without unlink - M_WAITOK-unreachable); this is the sleep-path lifetime bug on the START side. Kernel-path reproduction impossible on this guest: no audio PCI class, no hw.snd, no /dev/dsp* (env.txt), open() returns ENOENT (run.log) - hence not_reproduced; the PoC (100k re-armed iterations with two SYNCSTART racers + a lock-contention writer) is complete and would run verbatim on sound-equipped hardware. Confidence likely rather than certain only because the race window is timing-dependent; the code path mechanics are certain from source.", "exploit_chain": "unpriv user on sound-equipped box -> open /dev/dsp (0666) -> thread W loops write(2) on the fd (contends the member channel CHN lock) -> SNDCTL_DSP_SYNCGROUP (create group with that channel, dsp.c:2832-2977) -> thread T1: SNDCTL_DSP_SYNCSTART -> SG lock, walks members, CHN_TRYLOCK fails on the contended channel, releases earlier members, lksleep 5ms (drops SG lock, dsp.c:3034-3049) -> thread T2: SNDCTL_DSP_SYNCSTART same id -> SG lock free -> locks all members, starts them, kfrees all sm + sg (dsp.c:3056-3069) -> T1 5ms timeout fires, lksleep returns 0, walk resumes at freed sm -> SLIST_NEXT on freed chunk + CHN_TRYLOCK(freed->ch) -> UAF read / lock on wild pointer -> panic, or controlled corruption after M_DEVBUF re-spray (kmalloc-128-class grooming between teardown and wake).", "evidence": "run.log (open /dev/dsp ENOENT - device gate on this guest); VERDICT.md (annotated race timeline with dsp.c line refs, lksleep lock-drop semantics, teardown window, safe-EINTR vs buggy-timeout paths); syncstart_race.c (complete two-racer + contention-writer harness); env.txt (no PCM device); fix.diff (git-apply-clean: break the stale walk after any sleep, rescan from list head)", "kernel_refs": [ "sys/dev/sound/pcm/dsp.c:3027", "sys/dev/sound/pcm/dsp.c:3042", "sys/dev/sound/pcm/dsp.c:3047", "sys/dev/sound/pcm/dsp.c:3050", "sys/dev/sound/pcm/dsp.c:3056", "sys/dev/sound/pcm/dsp.c:3068", "sys/dev/sound/pcm/dsp.c:2056" ], "poc_changes": "Authored from scratch (no seed). Two SYNCSTART racers re-armed with a fresh SYNCGROUP per iteration (the group is consumed by a successful start), plus a writer thread for deterministic CHN lock contention; documented device gate for this guest.", "attempts": 2, "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": 90, "guest_dirty": 0, "build_cmd": "vm.sh run_root cc -O2 -Wall -o /tmp/syncrace /tmp/syncstart_race.c -lpthread", "run_cmd": "vm.sh run_user /tmp/syncrace; echo exit=$? (/tmp cleaned)", "code_hash": "c70d2175db0be32f4b0fe652ff0409f243f570c179a61260c0b22511753e4f4c", "notes": "Pass-2 depth item (multi-call state machine / lock lifetime): SYNCGROUP->SYNCSTART interaction across threads. SG-before-CHN lock order is consistent across dsp.c (syncgroup :2850/:2862, syncstart :3001/:3028, dsp_close :765/:771) - no inversion found. Known DF-1263 not re-reported (M_WAITOK branch unreachable).", "recommended_fix": "After lksleep() returns in dsp_oss_syncstart, break out of the stale SLIST_FOREACH and let the do-while rescan the syncgroup list from the head (the lock was dropped during the sleep, so sg/sm may be freed)", "fix_status": "not_testable", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff adds the post-sleep break so the walk restarts from the live list head and is git-apply-clean against the read-only sys/ tree; a patched-kernel build cannot be exercised on this guest (no PCM device, the racing ioctls cannot run), so fix validation is not_testable here", "fix_evidence": "fix.diff; VERDICT.md; env.txt (hardware gate)" } |