All of lore.kernel.org
 help / color / mirror / Atom feed
From: Samuel Thibault <samuel.thibault@ens-lyon.org>
To: Hongyu Xie <xiehongyu1@kylinos.cn>
Cc: speakup@linux-speakup.org, linux-kernel@vger.kernel.org,
	gregkh@linuxfoundation.org, chris@the-brannons.com,
	kirk@reisers.ca, w.d.hubbs@gmail.com, stable@vger.kernel.org
Subject: Re: [PATCH v2] speakup: Serialize synth clearing against locked readers
Date: Tue, 3 Nov 2026 01:01:50 +0100	[thread overview]
Message-ID: <aukkbqLJLzjGy9e8@end> (raw)
In-Reply-To: <179363263139.43100.8043437977710970575@kylinos.cn>

Hello,

Hongyu Xie, le lun. 02 nov. 2026 23:17:11 +0800, a ecrit:
> speakup_key() validates the global synth pointer before taking
> speakup_info.spinlock, but do_spkup() and its handlers run inside
> that lock and dereference synth: speech_kill() calls
> synth->is_alive(), and the flush path reaches synth_start()
> reading synth->alive.  synth_release() clears synth under
> spk_mutex, which does not serialize against the key path, so a
> speakup key pressed while the synth is being removed can pass the
> unlocked check and oops on the stale pointer:
> 
>     CPU0 (speakup key)             CPU1 (rmmod -> synth_release)
>     ------------------             ----------------------------
>     speakup_key():
>       if (!synth)                  mutex_lock(&spk_mutex)
>         return 0  /* passes */     synth_release():
>     spin_lock(&speakup_info.         synth->alive = 0   (spinlock)
>         spinlock)                   ... drain in-flight catch_up() ...
>                                     synth->release(synth)
>     do_spkup():
>       speech_kill():
>         synth->is_alive()
>         /* dereferences a synth whose release() has
>            already torn down its I/O */
> 
> Closing this window needs both sides of the race:
> 
> - Recheck synth at do_spkup() entry, inside the lock, so the whole
>   handler table runs only while a synth is still registered.
>   speakup_con_write() and speakup_con_update() used to gate on synth
>   before their trylock; they now recheck it after acquiring the lock
>   instead, so these readers no longer validate outside the critical
>   section that dereferences.
> 
> - Clear synth under speakup_info.spinlock in synth_release() and
>   in the do_synth_init() probe-failure path, so a clear can never
>   land between a locked reader's check and its dereference.
> 
> With the check and the clear under the same lock they are
> mutually exclusive for these paths, instead of merely close
> together:
> 
>     CPU0 (speakup key)             CPU1 (rmmod -> synth_release)
>     ------------------             ----------------------------
>     spin_lock(&speakup_info.       ... drain in-flight catch_up() ...
>         spinlock)                  spin_lock(&speakup_info.spinlock)
>     do_spkup():                    synth = NULL
>       if (!synth)                  spin_unlock(...)
>         return  /* in-lock recheck */
>       speech_kill():
>         synth->is_alive()  /* cannot interleave with a clear */
> 
> The console notifier paths had the same window between the
> pre-trylock check and the in-lock use:
> 
>     CPU0 (vt notifier)             CPU1 (rmmod -> synth_release)
>     ------------------             ----------------------------
>     speakup_con_write():
>       if (!synth)                  (the clear lands between
>         return  /* pre-trylock */    the pre-trylock check
>     spin_trylock(&speakup_info.      and the in-lock use)
>         spinlock)
>     ... uses synth ...  /* stale */
> 
> and now recheck synth after the trylock, in the lock, exactly
> like do_spkup().
> 
> The remaining locked readers had the same window between an
> unlocked check and the in-lock dereference, and gain the same
> recheck:
> 
> - do_handle_shift() and do_handle_spec() used to gate on synth
>   before taking speakup_info.spinlock and dereference it via
>   synth_printf() inside the lock.
> - speakup_key()'s special-handler dispatch dereferences synth
>   inside the lock speakup_key() holds (handle_goto(),
>   edit_bits(), the help handler).
> - synth_direct_store() used to validate synth before the lock
>   while its synth_write() call runs inside it.
> - the /dev/synth and /dev/synthu write paths used to validate
>   synth once before their copy_from_user() loop while every round
>   calls synth_write() (or synth_buffer_add() plus
>   synth_start()) inside the lock.
> 
> With these rechecks the check and the clear are mutually
> exclusive for every reader that dereferences synth inside
> speakup_info.spinlock, and the unlocked synth checks that used
> to gate these paths are dropped: they only duplicate the in-lock
> recheck, with a window a clear can still slip through.  An empty
> write to /dev/synth or /dev/synthu with no synth loaded now
> returns 0 instead of -ENODEV, which is harmless.
> 
> The speakup thread keeps its existing fence: synth_release()
> drains the catch-up count before clearing.
> 
> Two sites of this shape remain, preexisting.  The cursor_done()
> timer callback dereferences synth inside the lock with no synth
> check at all, and closing it needs the cursor_timer lifetime
> handled against synth teardown, which is beyond this series.
> The serial read buffer handler (synth_readbuf_handler()) also
> dereferences synth inside the lock with no synth check.  It
> cannot run in this tree: the only registration chain goes
> through spk_serial_synth_probe(), which has no callers, and
> any registration from outside the tree is still serialized by
> the free_irq() in the serial synth release path, which runs
> before the in-lock clear.
> 
> Fixes: 2067fd92d75b6 ("staging/speakup: Move out of staging")
> Suggested-by: Samuel Thibault <samuel.thibault@ens-lyon.org>
> Cc: stable@vger.kernel.org
> Cc: Samuel Thibault <samuel.thibault@ens-lyon.org>

Reviewed-by: Samuel Thibault <samuel.thibault@ens-lyon.org>

Thanks!

Samuel

> Signed-off-by: Hongyu Xie <xiehongyu1@kylinos.cn>
> ---
> Changes in v2: per Samuel's review, drop the now-duplicate unlocked
> synth checks on all the paths this patch rechecks inside
> speakup_info.spinlock:
> 
> - speakup_file_write() and speakup_file_writeu() in devsynth.c
> - synth_direct_store() in kobjects.c
> - do_handle_shift() and do_handle_spec(), which now only test
>   up_flag and spk_killed before taking the lock
> - speakup_con_write() and speakup_con_update(), which now only test
>   their other preconditions before the trylock
> - speakup_key(), whose pre-lock entry check is gone; the in-lock
>   recheck returns the same 0 via the existing "goto out"
> 
> The only behavior difference I could find from dropping them is that
> an empty write to /dev/synth or /dev/synthu with no synth loaded now
> returns 0 instead of -ENODEV, which should be harmless.
> 
> Also exercised the in-lock recheck at runtime: with the whole series
> applied in a QEMU boot test, a write through a stale /dev/synth fd
> kept open across the synth removal returns -ENODEV.
> 
>  drivers/accessibility/speakup/devsynth.c | 13 ++++++----
>  drivers/accessibility/speakup/kobjects.c |  8 +++---
>  drivers/accessibility/speakup/main.c     | 31 ++++++++++++++++++------
>  drivers/accessibility/speakup/synth.c    |  4 +++
>  4 files changed, 41 insertions(+), 15 deletions(-)
> 
> diff --git a/drivers/accessibility/speakup/devsynth.c b/drivers/accessibility/speakup/devsynth.c
> index e3d909bd0480..5a32d3bbf7b8 100644
> --- a/drivers/accessibility/speakup/devsynth.c
> +++ b/drivers/accessibility/speakup/devsynth.c
> @@ -20,8 +20,6 @@ static ssize_t speakup_file_write(struct file *fp, const char __user *buffer,
>  	unsigned long flags;
>  	u_char buf[256];
>  
> -	if (!synth)
> -		return -ENODEV;
>  	while (count > 0) {
>  		bytes = min(count, sizeof(buf));
>  		if (copy_from_user(buf, ptr, bytes))
> @@ -29,6 +27,10 @@ static ssize_t speakup_file_write(struct file *fp, const char __user *buffer,
>  		count -= bytes;
>  		ptr += bytes;
>  		spin_lock_irqsave(&speakup_info.spinlock, flags);
> +		if (!synth) {
> +			spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +			return -ENODEV;
> +		}
>  		synth_write(buf, bytes);
>  		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
>  	}
> @@ -47,9 +49,6 @@ static ssize_t speakup_file_writeu(struct file *fp, const char __user *buffer,
>  	u16 ubuf[256];
>  	size_t in, out;
>  
> -	if (!synth)
> -		return -ENODEV;
> -
>  	want = 1;
>  	while (count >= want) {
>  		/* Copy some UTF-8 piece from userland */
> @@ -84,6 +83,10 @@ static ssize_t speakup_file_writeu(struct file *fp, const char __user *buffer,
>  		/* And speak this up */
>  		if (out) {
>  			spin_lock_irqsave(&speakup_info.spinlock, flags);
> +			if (!synth) {
> +				spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +				return -ENODEV;
> +			}
>  			for (in = 0; in < out; in++)
>  				synth_buffer_add(ubuf[in]);
>  			synth_start();
> diff --git a/drivers/accessibility/speakup/kobjects.c b/drivers/accessibility/speakup/kobjects.c
> index e1860f8d9d42..45a8e9fbd8de 100644
> --- a/drivers/accessibility/speakup/kobjects.c
> +++ b/drivers/accessibility/speakup/kobjects.c
> @@ -416,9 +416,6 @@ static ssize_t synth_direct_store(struct kobject *kobj,
>  	char *unescaped;
>  	unsigned long flags;
>  
> -	if (!synth)
> -		return -EPERM;
> -
>  	unescaped = kstrdup(buf, GFP_KERNEL);
>  	if (!unescaped)
>  		return -ENOMEM;
> @@ -426,6 +423,11 @@ static ssize_t synth_direct_store(struct kobject *kobj,
>  	string_unescape_any_inplace(unescaped);
>  
>  	spin_lock_irqsave(&speakup_info.spinlock, flags);
> +	if (!synth) {
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +		kfree(unescaped);
> +		return -EPERM;
> +	}
>  	synth_write(unescaped, strlen(unescaped));
>  	spin_unlock_irqrestore(&speakup_info.spinlock, flags);
>  
> diff --git a/drivers/accessibility/speakup/main.c b/drivers/accessibility/speakup/main.c
> index e9b7c2761f6f..60be4d4df363 100644
> --- a/drivers/accessibility/speakup/main.c
> +++ b/drivers/accessibility/speakup/main.c
> @@ -1166,9 +1166,13 @@ static void do_handle_shift(struct vc_data *vc, u_char value, char up_flag)
>  {
>  	unsigned long flags;
>  
> -	if (!synth || up_flag || spk_killed)
> +	if (up_flag || spk_killed)
>  		return;
>  	spin_lock_irqsave(&speakup_info.spinlock, flags);
> +	if (!synth) {
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +		return;
> +	}
>  	if (cursor_track == read_all_mode) {
>  		switch (value) {
>  		case KVAL(K_SHIFT):
> @@ -1763,11 +1767,15 @@ static void speakup_con_write(struct vc_data *vc, u16 *str, int len)
>  {
>  	unsigned long flags;
>  
> -	if ((vc->vc_num != fg_console) || spk_shut_up || !synth)
> +	if ((vc->vc_num != fg_console) || spk_shut_up)
>  		return;
>  	if (!spin_trylock_irqsave(&speakup_info.spinlock, flags))
>  		/* Speakup output, discard */
>  		return;
> +	if (!synth) {
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +		return;
> +	}
>  	if (spk_bell_pos && spk_keydown && (vc->state.x == spk_bell_pos - 1))
>  		bleep(3);
>  	if ((is_cursor) || (cursor_track == read_all_mode)) {
> @@ -1792,11 +1800,15 @@ static void speakup_con_update(struct vc_data *vc)
>  {
>  	unsigned long flags;
>  
> -	if (!speakup_console[vc->vc_num] || spk_parked || !synth)
> +	if (!speakup_console[vc->vc_num] || spk_parked)
>  		return;
>  	if (!spin_trylock_irqsave(&speakup_info.spinlock, flags))
>  		/* Speakup output, discard */
>  		return;
> +	if (!synth) {
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +		return;
> +	}
>  	speakup_date(vc);
>  	if (vc->vc_mode == KD_GRAPHICS && !spk_paused && spk_str_pause[0]) {
>  		synth_printf("%s", spk_str_pause);
> @@ -1811,9 +1823,13 @@ static void do_handle_spec(struct vc_data *vc, u_char value, char up_flag)
>  	int on_off = 2;
>  	char *label;
>  
> -	if (!synth || up_flag || spk_killed)
> +	if (up_flag || spk_killed)
>  		return;
>  	spin_lock_irqsave(&speakup_info.spinlock, flags);
> +	if (!synth) {
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
> +		return;
> +	}
>  	spk_shut_up &= 0xfe;
>  	if (spk_no_intr)
>  		spk_do_flush();
> @@ -2087,6 +2103,8 @@ static spkup_hand spkup_handler[] = {
>  
>  static void do_spkup(struct vc_data *vc, u_char value)
>  {
> +	if (!synth)
> +		return;
>  	if (spk_killed && value != SPEECH_KILL)
>  		return;
>  	spk_keydown = 0;
> @@ -2115,10 +2133,9 @@ speakup_key(struct vc_data *vc, int shift_state, int keycode, u16 keysym,
>  	u_char shift_info, offset;
>  	int ret = 0;
>  
> -	if (!synth)
> -		return 0;
> -
>  	spin_lock_irqsave(&speakup_info.spinlock, flags);
> +	if (!synth)
> +		goto out;
>  	tty = vc->port.tty;
>  	if (type >= 0xf0)
>  		type -= 0xf0;
> diff --git a/drivers/accessibility/speakup/synth.c b/drivers/accessibility/speakup/synth.c
> index 1af80eb116af..aef482f8b14e 100644
> --- a/drivers/accessibility/speakup/synth.c
> +++ b/drivers/accessibility/speakup/synth.c
> @@ -519,7 +519,9 @@ static int do_synth_init(struct spk_synth *in_synth)
>  	pr_warn("synth probe\n");
>  	if (synth->probe(synth) < 0) {
>  		pr_warn("%s: device probe failed\n", in_synth->name);
> +		spin_lock_irqsave(&speakup_info.spinlock, flags);
>  		synth = NULL;
> +		spin_unlock_irqrestore(&speakup_info.spinlock, flags);
>  		return -ENODEV;
>  	}
>  	synth_time_vars[0].u.n.value =
> @@ -585,7 +587,9 @@ void synth_release(void)
>  	for (var = synth->vars; var->var_id != MAXVARS; var++)
>  		speakup_unregister_var(var->var_id);
>  	synth->release(synth);
> +	spin_lock_irqsave(&speakup_info.spinlock, flags);
>  	synth = NULL;
> +	spin_unlock_irqrestore(&speakup_info.spinlock, flags);
>  }
>  
>  /* called by: all_driver_init() */
> -- 
> 2.32.0
> 

-- 
Samuel
 Les roots ne sont plus ce qu'ils étaient...Maintenant il sont dioxinés,
 c'est de la m... ! Avant on les élevaient avec du bon unix mais ça été
 remplacé par des farines industrielles nouvelles technologies (NT).
 -+- JdK in NPC : Exigez un root élevé sous la mère ! -+-

      reply	other threads:[~2026-11-03  0:02 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-14  7:17 [PATCH 0/4] speakup: fix hang and use-after-free around synth removal Hongyu Xie
2026-10-14  7:17 ` [PATCH 1/4] speakup: Stop catch_up when the synth is not alive Hongyu Xie
2026-10-18 18:24   ` Samuel Thibault
2026-10-14  7:17 ` [PATCH 2/4] speakup: Do not hold spk_mutex across catch_up() Hongyu Xie
2026-10-18 18:26   ` Samuel Thibault
2026-10-19 11:15     ` Hongyu Xie
2026-11-02 15:17       ` Hongyu Xie
2026-11-03  0:16   ` Samuel Thibault
2026-10-14  7:17 ` [PATCH 3/4] speakup: Guard late sysfs operations on speakup_kobj Hongyu Xie
2026-10-18 18:27   ` Samuel Thibault
2026-10-14  7:17 ` [PATCH 4/4] speakup: Serialize synth clearing against locked readers Hongyu Xie
2026-10-18 19:40   ` Samuel Thibault
2026-11-02 15:17     ` [PATCH v2] " Hongyu Xie
2026-11-03  0:01       ` Samuel Thibault [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=aukkbqLJLzjGy9e8@end \
    --to=samuel.thibault@ens-lyon.org \
    --cc=chris@the-brannons.com \
    --cc=gregkh@linuxfoundation.org \
    --cc=kirk@reisers.ca \
    --cc=linux-kernel@vger.kernel.org \
    --cc=speakup@linux-speakup.org \
    --cc=stable@vger.kernel.org \
    --cc=w.d.hubbs@gmail.com \
    --cc=xiehongyu1@kylinos.cn \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.