[PATCH v2] tty: serial: max3100: shut down timer before freeing port

Fan Wu <[email protected]> Sat, 1 Aug 2026 06:12:08 +0000
Newsgroups org.kernel.vger.linux-serial,org.kernel.vger.linux-kernel,org.kernel.vger.stable
Message-ID <[email protected]>
max3100_shutdown() stops the polling timer but returns early during
system suspend. If the SPI device is unbound before resume, the serial
core does not call max3100_shutdown() again, so max3100_remove() frees
the port while the timer remains armed. max3100_timeout() can then
access the freed port and re-arm the timer.

Add final timer teardown to max3100_remove() and use
timer_shutdown_sync() to prevent a racing callback from re-arming it.
Also drain the IRQ and workqueue before freeing the port.

Keep timer_delete_sync() in max3100_shutdown() so that a subsequent
open() can re-arm the timer.

Introduce an irq_registered flag to track whether the IRQ is registered,
independently of port->irq, so a failed request_irq() can be retried on
the next open().

Found by static analysis.

Fixes: 7831d56b0a35 ("tty: MAX3100")
Cc: [email protected] # 6.2+
Assisted-by: Codex:gpt-5.6
Signed-off-by: Fan Wu <[email protected]>
---
Changes since v1:
  - Drop the shared drain helper; call timer_shutdown_sync() only in
    max3100_remove(), keeping timer_delete_sync() in max3100_shutdown()
    so a later open() can re-arm the timer.
  - Track IRQ registration with a flag instead of clearing port->irq,
    so a failed request_irq() can be retried on the next open().

v1: https://lore.kernel.org/all/[email protected]/
---
 drivers/tty/serial/max3100.c | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/drivers/tty/serial/max3100.c b/drivers/tty/serial/max3100.c
index 44b745fa26c6..7bc3c5cfe886 100644
--- a/drivers/tty/serial/max3100.c
+++ b/drivers/tty/serial/max3100.c
@@ -107,6 +107,7 @@ struct max3100_port {
 	int  force_end_work;
 	/* need to know we are suspending to avoid deadlock on workqueue */
 	int suspending;
+	bool irq_registered;
 
 	struct timer_list	timer;
 };
@@ -538,8 +539,10 @@ static void max3100_shutdown(struct uart_port *port)
 		destroy_workqueue(s->workqueue);
 		s->workqueue = NULL;
 	}
-	if (port->irq)
+	if (s->irq_registered) {
 		free_irq(port->irq, s);
+		s->irq_registered = false;
+	}
 
 	/* set shutdown mode to save power */
 	max3100_sr(s, MAX3100_WC | MAX3100_SHDN, &rx);
@@ -575,12 +578,12 @@ static int max3100_startup(struct uart_port *port)
 	ret = request_irq(port->irq, max3100_irq, IRQF_TRIGGER_FALLING, "max3100", s);
 	if (ret < 0) {
 		dev_warn(&s->spi->dev, "cannot allocate irq %d\n", port->irq);
-		port->irq = 0;
 		destroy_workqueue(s->workqueue);
 		s->workqueue = NULL;
 		return -EBUSY;
 	}
 
+	s->irq_registered = true;
 	s->conf_commit = 1;
 	max3100_dowork(s);
 	/* wait for clock to settle */
@@ -752,6 +755,17 @@ static void max3100_remove(struct spi_device *spi)
 		if (max3100s[i] == s) {
 			dev_dbg(&spi->dev, "%s: removing port %d\n", __func__, i);
 			uart_remove_one_port(&max3100_uart_driver, &max3100s[i]->port);
+
+			s->force_end_work = 1;
+			timer_shutdown_sync(&s->timer);
+			if (s->irq_registered) {
+				free_irq(s->port.irq, s);
+				s->irq_registered = false;
+			}
+			if (s->workqueue) {
+				destroy_workqueue(s->workqueue);
+				s->workqueue = NULL;
+			}
 			kfree(max3100s[i]);
 			max3100s[i] = NULL;
 			break;
-- 
2.34.1