# 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:

```c
	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` + `kfree`s every sm and
  `SLIST_REMOVE` + `kfree`s 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):

```diff
 				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.
