git: 5ac39263d825 - main - sound: Fix chn_trigger() and vchan_trigger() races
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Tue, 26 Nov 2024 14:48:55 UTC
The branch main has been updated by christos: URL: https://cgit.FreeBSD.org/src/commit/?id=5ac39263d825d7b2f8a89614a63fee90ffc77c07 commit 5ac39263d825d7b2f8a89614a63fee90ffc77c07 Author: Christos Margiolis <christos@FreeBSD.org> AuthorDate: 2024-11-26 14:48:18 +0000 Commit: Christos Margiolis <christos@FreeBSD.org> CommitDate: 2024-11-26 14:48:18 +0000 sound: Fix chn_trigger() and vchan_trigger() races Consider the following scenario: 1. CHN currently has its trigger set to PCMTRIG_STOP. 2. Thread A locks CHN, calls CHANNEL_TRIGGER(PCMTRIG_START), sets the trigger to PCMTRIG_START and unlocks. 3. Thread B picks up the lock, calls CHANNEL_TRIGGER(PCMTRIG_ABORT) and returns a non-zero value, so it returns from chn_trigger() as well. 4. Thread A picks up the lock and adds CHN to the list, which is _wrong_, because the last call to CHANNEL_TRIGGER() was with PCMTRIG_ABORT, meaning the channel is stopped, yet we are adding it to the list and marking it as started. Another problematic scenario: 1. Thread A locks CHN, sets the trigger to PCMTRIG_ABORT, and unlocks CHN. It then locks PCM and _removes_ CHN from the list. 2. In the meantime, since thread A unlocked CHN, thread B has locked it, set the trigger to PCMTRIG_START, unlocked it, and is now blocking on PCM held by thread A. 3. At the same time, thread C locks CHN, sets the trigger back to PCMTRIG_ABORT, unlocks CHN, and is also blocking on PCM. However, once thread A unlocks PCM, because thread C is higher-priority than thread B, it picks up the PCM lock instead of thread B, and because CHN is already removed from the list, and thread B hasn't added it back yet, we take a page fault in CHN_REMOVE() by trying to remove a non-existent element. To fix the former scenario, set the channel trigger before the call to CHANNEL_TRIGGER() (could also come after, doesn't really matter) and check if anything changed one we lock CHN back. To fix the latter scenario, use the SAFE variants of CHN_INSERT_HEAD() and CHN_REMOVE(). A similar scenario can occur in vchan_trigger(), so do the trigger setting after we've locked the parent channel. Sponsored by: The FreeBSD Foundation MFC after: 2 days Reviewed by: dev_submerge.ch Differential Revision: https://reviews.freebsd.org/D47461 --- sys/dev/sound/pcm/channel.c | 56 +++++++++++++++++++++++---------------------- sys/dev/sound/pcm/vchan.c | 11 ++++----- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/sys/dev/sound/pcm/channel.c b/sys/dev/sound/pcm/channel.c index eb70e910f51d..59cdb8cd07f5 100644 --- a/sys/dev/sound/pcm/channel.c +++ b/sys/dev/sound/pcm/channel.c @@ -2318,44 +2318,46 @@ chn_trigger(struct pcm_channel *c, int go) if (go == c->trigger) return (0); + if (snd_verbose > 3) { + device_printf(c->dev, "%s() %s: calling go=0x%08x , " + "prev=0x%08x\n", __func__, c->name, go, c->trigger); + } + + c->trigger = go; ret = CHANNEL_TRIGGER(c->methods, c->devinfo, go); if (ret != 0) return (ret); + CHN_UNLOCK(c); + PCM_LOCK(d); + CHN_LOCK(c); + + /* + * Do nothing if another thread set a different trigger while we had + * dropped the mutex. + */ + if (go != c->trigger) { + PCM_UNLOCK(d); + return (0); + } + + /* + * Use the SAFE variants to prevent inserting/removing an already + * existing/missing element. + */ switch (go) { case PCMTRIG_START: - if (snd_verbose > 3) - device_printf(c->dev, - "%s() %s: calling go=0x%08x , " - "prev=0x%08x\n", __func__, c->name, go, - c->trigger); - if (c->trigger != PCMTRIG_START) { - c->trigger = go; - CHN_UNLOCK(c); - PCM_LOCK(d); - CHN_INSERT_HEAD(d, c, channels.pcm.busy); - PCM_UNLOCK(d); - CHN_LOCK(c); - chn_syncstate(c); - } + CHN_INSERT_HEAD_SAFE(d, c, channels.pcm.busy); + PCM_UNLOCK(d); + chn_syncstate(c); break; case PCMTRIG_STOP: case PCMTRIG_ABORT: - if (snd_verbose > 3) - device_printf(c->dev, - "%s() %s: calling go=0x%08x , " - "prev=0x%08x\n", __func__, c->name, go, - c->trigger); - if (c->trigger == PCMTRIG_START) { - c->trigger = go; - CHN_UNLOCK(c); - PCM_LOCK(d); - CHN_REMOVE(d, c, channels.pcm.busy); - PCM_UNLOCK(d); - CHN_LOCK(c); - } + CHN_REMOVE_SAFE(d, c, channels.pcm.busy); + PCM_UNLOCK(d); break; default: + PCM_UNLOCK(d); break; } diff --git a/sys/dev/sound/pcm/vchan.c b/sys/dev/sound/pcm/vchan.c index ed13a3c55961..4165d0712b94 100644 --- a/sys/dev/sound/pcm/vchan.c +++ b/sys/dev/sound/pcm/vchan.c @@ -146,20 +146,19 @@ vchan_trigger(kobj_t obj, void *data, int go) int ret, otrigger; info = data; - - if (!PCMTRIG_COMMON(go) || go == info->trigger) - return (0); - c = info->channel; p = c->parentchannel; - otrigger = info->trigger; - info->trigger = go; CHN_LOCKASSERT(c); + if (!PCMTRIG_COMMON(go) || go == info->trigger) + return (0); CHN_UNLOCK(c); CHN_LOCK(p); + otrigger = info->trigger; + info->trigger = go; + switch (go) { case PCMTRIG_START: if (otrigger != PCMTRIG_START)