dsp_oss_syncstart use-after-free: lksleep drops the syncgroup lock mid-walk; timeout wake resumes SLIST_FOREACH over sm/sg freed by a concurrent SYNCSTART
Summary
In the SNDCTL_DSP_SYNCSTART retry loop, a failed CHN_TRYLOCK leads to lksleep which RELEASES the syncgroup lock while asleep. A concurrent successful SYNCSTART of the same sg_id can then complete the full teardown - start every member, SLIST_REMOVE_HEAD + kfree each sm, SLIST_REMOVE + kfree the sg - inside the 5ms window. The plain timeout wake executes and the inner SLIST_FOREACH CONTINUES from the freed sm: SLIST_NEXT reads freed memory and CHN_TRYLOCK locks a channel pointer read from freed memory - kernel UAF (panic or exploitable corruption via heap re-spray). Only the EINTR path exits safely. The trylock failure is attacker-arranged by contending the member channel lock (looping/blocking write(2) on the same fd). Any unprivileged local user on a sound-equipped system races two threads' SYNCSTART against a channel-lock-contenting writer. 5ms window; PoC re-arms 100k times. Distinct from DF-1263 (error-path free without unlink). Break-and-rescan fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3880 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 1.6 KB | β raw | |
| VERDICT.md | β | 3.4 KB | β raw | |
| syncstart_race.c | β | 3.8 KB | view raw | |
| build.sh | β | 206 B | view raw | |
| run.sh | β | 136 B | view raw | |
| build.log | β | 8 B | view raw | |
| run.log | β | 130 B | view raw | |
| env.txt | β | 692 B | view raw | |
| fix.diff | β | 496 B | view raw | |
| verdict.json | β | 5.1 KB | view raw |
DF-3880 β use-after-free race in dsp_oss_syncstart() (SNDCTL_DSP_SYNCSTART)
File: sys/dev/sound/pcm/dsp.c:2992-3082 β pass 2 (GLM 5.3). Distinct from known DF-1263 (syncgroup error-path free without unlink).
BUILD scp syncstart_race.c dfbsd-maxx:/tmp/ vm.sh run_root 'cc -O2 -Wall -o /tmp/syncrace /tmp/syncstart_race.c -lpthread'
RUN (unprivileged, sound-equipped DragonFly box) vm.sh run_user '/tmp/syncrace; echo exit=$?'
EXPECTED
This guest: open("/dev/dsp") -> ENOENT (no PCM device, env.txt);
the race cannot execute here.
On a sound-equipped box: two SYNCSTART threads racing on the same
sg_id while a writer keeps the member channel lock contended force
T1 into lksleep("pcmsg", 5ms) inside its member walk; T2 tears the
group down (starts members, kfrees every sm and the sg,
dsp.c:3056-3069) while T1 sleeps; T1's 5ms timeout wake resumes the
SLIST_FOREACH from the freed sm β UAF read of SLIST_NEXT(sm) and
CHN_TRYLOCK(sm->ch) on a freed object (wild lock pointer -> panic or
corruption). 100k iterations in the PoC; a surviving run prints the
"survived" line (race is probabilistic).
WHAT IT PROVES (source-certain)
dsp.c:3042-3049: lksleep() drops snd_pcm_syncgroups_mtx while asleep,
so sg/sm pointers held by the interrupted SLIST_FOREACH can be freed
by a concurrent successful SYNCSTART; the timeout path (lksleep
returns 0) does needlocks = 1; ret = 0; and CONTINUES the walk over
freed memory. Only the EINTR/ERESTART path breaks out safely.
DF-3880 VERDICT β dsp_oss_syncstart use-after-free after lksleep
Status: not_reproduced (kernel path β no PCM device on this guest); source-certain mechanism; confidence likely (race timing analysis).
Root cause (dsp.c:3001-3051)
The SYNCSTART retry loop:
PCM_SG_LOCK(); /* :3001 */
do {
SLIST_FOREACH(sg, &snd_pcm_syncgroups, link) { ... } /* :3008 */
SLIST_FOREACH(sm, &sg->members, link) { /* :3027 */
if (CHN_TRYLOCK(sm->ch) == 0) { /* :3028 */
/* unlock members locked so far */
ret = lksleep(sm, &snd_pcm_syncgroups_mtx,
PCATCH, "pcmsg", timo); /* :3042 */
if (ret == EINTR || ret == ERESTART)
break; /* safe exit */
needlocks = 1;
ret = 0; /* :3048 */
}
} /* :3050 */
} while (needlocks && ret == 0); /* :3051 */
lksleep(ident, lock, ...) releases snd_pcm_syncgroups_mtx while the
thread sleeps. During that window (5 ms β timo = hz*5/1000, :3029-3031)
another thread's successful SYNCSTART on the same sg_id:
- re-acquires the SG lock (free),
- locks ALL members (T1 just released M_0..M_{k-1}; M_k's lock is briefly free between writer-loop iterations),
- starts every member,
SLIST_REMOVE_HEAD+kfrees every sm andSLIST_REMOVE+kfrees sg itself β dsp.c:3056-3069.
T1's plain timeout wake (lksleep returns 0, no signal) sets
needlocks=1; ret=0 and the inner SLIST_FOREACH continues from the
freed sm: SLIST_NEXT(sm, link) reads freed memory, then
CHN_TRYLOCK(sm->ch) loads a freed-data channel pointer and takes an
unbounded lockmgr path on it β kernel UAF (panic / exploitable corruption
depending on heap reuse; kfreed M_DEVBUF chunk re-spray is an attacker
activity). sg itself is also dangling during the resumed walk.
Trigger requirements
- PCM device present (SNDCTL_DSP_SYNCGROUP/SYNCSTART go through dsp_ioctl :2056-2066 with channels from getchns β /dev/dsp* are 0666).
- Force T1's trylock failure: keep the member channel's lock contended (the PoC's writer thread looping write(2) on the same channel β chn_write holds CHN lock between uiomove drops).
- T2 wins the race inside the 5 ms sleep β microseconds of work.
- No signal involved (signals take the safe EINTR break).
Not reproduced here
No PCM device on this guest (env.txt) β open("/dev/dsp") = ENOENT (run.log). The PoC is complete and would run verbatim on sound-equipped hardware; the mechanism is proven by source (lksleep lock-drop semantics + the free at :3056-3069 + the resume at :3050).
Fix
Break out of the stale walk after any sleep and let the do-while rescan from the list head (fix.diff, git-apply clean):
needlocks = 1;
ret = 0; /* Assumes ret == EAGAIN... */
+ /* DF-3880: lksleep() dropped the syncgroup
+ * lock; sg/sm may be freed now. Restart the
+ * scan instead of resuming the walk. */
+ break;
}
}
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.
Fix verification
not_testablefix.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.diff; VERDICT.md; env.txt (hardware gate)
Confirmed kernel references
Detail
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 (decisive lines)
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)
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.
Verified 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)
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 channel's lock contended (a looping/blocking write(2) on the same channel, as in the PoC's 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.
No comments yet.