From 02b1d5127f37683448ef35798b3a6310d1d38761 Mon Sep 17 00:00:00 2001 From: Cen Zhang Date: Sun, 25 Oct 2026 00:49:50 +0800 Subject: [PATCH] ALSA: opl3: Clear voice channels before freeing their sets The card-owned OPL3 voice array survives sequencer-driver unbind, but its chan pointers refer to the native or OSS port's channel set. Neither port's private_free callback clears those pointers before freeing its set. A direct NOTEON in the initial SYNTH mode can leave a voice allocated without a subscription. Even with fix_dur zero and no timer, unbind and rebind followed by direct PITCHBEND can read the old channel: Sequencer driver unbind/rebind Direct PITCHBEND ----------------------------- ---------------- snd_seq_delete_kernel_client() port_free() free channel set snd_opl3_seq_probe() allocate new channel set snd_opl3_pitch_ctrl() snd_opl3_update_pitch() old voice->chan is non-NULL snd_opl3_calc_pitch() read freed channel Clearing only note_off_check or the voice state does not protect this path: snd_opl3_update_pitch() tests the channel pointer itself. Key off active voices before clearing their channel pointers, delayed- note flags and state under voice_lock. Direct events need no subscription, so port deletion need not call synth_unuse() to reset the hardware. Merely marking a keyed-on voice off would retain hardware KEYON and cause a later note to miss the key-on transition. Reuse snd_opl3_kill_voice() before either channel set is freed. Visit higher-numbered voices first. In initial SYNTH mode, direct NOTEON events map channels to fixed voices without reserving them through the allocator: a 4-op primary can coexist with an independently keyed-on 2-op partner at voice + 3. Killing the primary clears that partner's software state without keying off its register. Ascending cleanup would then skip the partner. Retiring the partner first guarantees its key-off before the primary can change its state, on both register banks. The lock also excludes a timer or pitch update already using a voice. Both private_free callbacks must do the cleanup because an in-flight OSS event can refill the shared voice array after the native port has been drained and freed. That event can only reference its own still-live set; the OSS callback's final cleanup retires that reference before freeing its set as well. Assisted-by: LLM Signed-off-by: Cen Zhang Signed-off-by: Takashi Iwai Link: https://patch.msgid.link/pm-pmb-4552c4e605507d8d9b16-v9-70247b8436e10e26d324-2@gmail.com --- sound/drivers/opl3/opl3_midi.c | 15 +++++++++++++++ sound/drivers/opl3/opl3_oss.c | 1 + sound/drivers/opl3/opl3_seq.c | 1 + sound/drivers/opl3/opl3_voice.h | 1 + 4 files changed, 18 insertions(+) diff --git a/sound/drivers/opl3/opl3_midi.c b/sound/drivers/opl3/opl3_midi.c index 6d3c5b5a35ff..de611214cdac 100644 --- a/sound/drivers/opl3/opl3_midi.c +++ b/sound/drivers/opl3/opl3_midi.c @@ -632,6 +632,21 @@ static void snd_opl3_kill_voice(struct snd_opl3 *opl3, int voice) } +void snd_opl3_clear_voices(struct snd_opl3 *opl3) +{ + int i; + + guard(spinlock_irqsave)(&opl3->voice_lock); + /* A 4-op primary clears its higher-numbered partner's state. */ + for (i = MAX_OPL3_VOICES - 1; i >= 0; i--) { + if (opl3->voices[i].state > 0) + snd_opl3_kill_voice(opl3, i); + opl3->voices[i].chan = NULL; + opl3->voices[i].note_off_check = 0; + opl3->voices[i].state = SNDRV_OPL3_ST_OFF; + } +} + /* * Release a note in response to a midi note off. */ diff --git a/sound/drivers/opl3/opl3_oss.c b/sound/drivers/opl3/opl3_oss.c index 6d39b2b77b80..1f01d27955fe 100644 --- a/sound/drivers/opl3/opl3_oss.c +++ b/sound/drivers/opl3/opl3_oss.c @@ -41,6 +41,7 @@ static void snd_opl3_oss_free_port(void *private_data) { struct snd_opl3 *opl3 = private_data; + snd_opl3_clear_voices(opl3); snd_midi_channel_free_set(opl3->oss_chset); } diff --git a/sound/drivers/opl3/opl3_seq.c b/sound/drivers/opl3/opl3_seq.c index 2bacefc3201d..e03c88377bb6 100644 --- a/sound/drivers/opl3/opl3_seq.c +++ b/sound/drivers/opl3/opl3_seq.c @@ -152,6 +152,7 @@ static void snd_opl3_synth_free_port(void *private_data) { struct snd_opl3 *opl3 = private_data; + snd_opl3_clear_voices(opl3); snd_midi_channel_free_set(opl3->chset); } diff --git a/sound/drivers/opl3/opl3_voice.h b/sound/drivers/opl3/opl3_voice.h index be9ccca2d952..aa81512d23c9 100644 --- a/sound/drivers/opl3/opl3_voice.h +++ b/sound/drivers/opl3/opl3_voice.h @@ -13,6 +13,7 @@ int snd_opl3_synth_use_inc(struct snd_opl3 * opl3); void snd_opl3_synth_use_dec(struct snd_opl3 * opl3); int snd_opl3_synth_setup(struct snd_opl3 * opl3); void snd_opl3_synth_cleanup(struct snd_opl3 * opl3); +void snd_opl3_clear_voices(struct snd_opl3 *opl3); /* Prototypes for opl3_midi.c */ void snd_opl3_note_on(void *p, int note, int vel, struct snd_midi_channel *chan);