]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
serial: 8250_of: clear stuck empty-FIFO RX-timeout on LPC32xx
authorRyan Wilbur <rwilbur633@gmail.com>
Thu, 30 Jul 2026 19:39:20 +0000 (16:39 -0300)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Mon, 3 Aug 2026 14:31:07 +0000 (16:31 +0200)
The NXP LPC32xx UART (PORT_LPC3220) can latch an RX character-timeout
interrupt while the RX FIFO is empty: IIR reports UART_IIR_RX_TIMEOUT
(0x0c) but LSR.DR is clear. A character timeout is only cleared by
reading RHR, but serial8250_rx_chars() reads RHR only when LSR.DR is
set, so nothing ever clears the condition. The interrupt is
level-triggered and re-fires immediately, so on a single-core ARM926
the resulting interrupt storm livelocks the CPU.

It is reproducible when userspace repeatedly opens the front-panel port
(ttyS1): serial8250_do_set_termios() re-enables interrupts on unlock and
the handler then spins forever with iir=0xcc lsr=0x60 ier=0x05, tripping
the soft-lockup detector in serial8250_handle_irq_locked().

LPC32xx has no dedicated 8250 glue driver, it's driven by the generic
8250_of. Add a hardware specific handle_irq for PORT_LPC3220, wired up
in of_platform_serial_setup() the same way fsl8250_handle_irq is
installed. The handler follows dw8250_handle_irq(): on an RX timeout
with an empty FIFO (LSR.DR and LSR.BI clear) it does one throwaway RHR
read to clear the condition, then calls serial8250_handle_irq_locked().
No real received data is ever discarded, and it is a no-op on healthy
UARTs which never report a timeout with DR clear.

This is the same class of bug already worked around in other 8250 drivers;
see commit 424d79183af0 ("serial: 8250_dw: Avoid "too much work" from bogus rx timeout interrupt")
which reports the identical iir=0xcc/lsr=0x60. See also
UART_RX_TIMEOUT_QUIRK in 8250_omap, and the note in 8250_bcm7271.

Cc: stable <stable@kernel.org>
Assisted-by: Claude:Opus4.8
Signed-off-by: Ryan Wilbur <rwilbur633@gmail.com>
Link: https://patch.msgid.link/20260730193920.28954-1-rwilbur633@gmail.com
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/tty/serial/8250/8250_of.c

index 81644d40b09a187a597601790095dd94d0249d89..160f4c71de06d012a95022e14833c8747df773b5 100644 (file)
@@ -81,6 +81,40 @@ static int of_platform_serial_clk_notifier_cb(struct notifier_block *nb, unsigne
        return NOTIFY_DONE;
 }
 
+static int lpc32xx_handle_irq(struct uart_port *port)
+{
+       struct uart_8250_port *up = up_to_u8250p(port);
+       unsigned int iir;
+       u16 status;
+
+       guard(serial8250_rpm)(up);
+
+       iir = serial_port_in(port, UART_IIR);
+       if (iir & UART_IIR_NO_INT)
+               return 0;
+
+       guard(uart_port_lock_check_sysrq_irqsave)(port);
+
+       /*
+        * The LPC32xx UART can assert an RX character-timeout interrupt while
+        * the RX FIFO is empty: IIR reports UART_IIR_RX_TIMEOUT but LSR.DR is
+        * clear. The timeout is only cleared by reading RHR, but the core RX
+        * path skips that read when the FIFO is empty, so the level-triggered
+        * IRQ re-fires forever and livelocks this single-core SoC. Do one
+        * throwaway RHR read to clear it; a healthy UART never reports a
+        * timeout with DR/BI clear, so no received data is ever discarded.
+        */
+       if ((iir & 0x3f) == UART_IIR_RX_TIMEOUT) {
+               status = serial_lsr_in(up);
+               if (!(status & (UART_LSR_DR | UART_LSR_BI)))
+                       serial_port_in(port, UART_RX);
+       }
+
+       serial8250_handle_irq_locked(port, iir);
+
+       return 1;
+}
+
 /*
  * Fill a struct uart_port for a given device node
  */
@@ -173,6 +207,9 @@ static int of_platform_serial_setup(struct platform_device *ofdev,
        case PORT_NPCM:
                ret = npcm_setup(port);
                break;
+       case PORT_LPC3220:
+               port->handle_irq = lpc32xx_handle_irq;
+               break;
        default:
                /* Nothing to do */
                ret = 0;
@@ -369,6 +406,7 @@ static struct platform_driver of_platform_serial_driver = {
 
 module_platform_driver(of_platform_serial_driver);
 
+MODULE_IMPORT_NS("SERIAL_8250");
 MODULE_AUTHOR("Arnd Bergmann <arnd@arndb.de>");
 MODULE_LICENSE("GPL");
 MODULE_DESCRIPTION("Serial Port driver for Open Firmware platform devices");