mirror of https://lore.kernel.org/lkml/
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®