From: sashiko-bot@kernel.org
To: "Kwanghoon Son" <asdfkwang@gmail.com>
Cc: devicetree@vger.kernel.org, linux-serial@vger.kernel.org,
robh@kernel.org, conor+dt@kernel.org, ojeda@kernel.org,
gary@garyguo.net
Subject: Re: [PATCH RFC 06/12] rust: serial: Add minimal serial-core polling console support
Date: Thu, 29 Oct 2026 05:37:40 +0000 [thread overview]
Message-ID: <sashiko-outbox-188759@kernel.org> (raw)
In-Reply-To: <20261029-rust-bl808-console-v1-6-b736ee920078@gmail.com>
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] rust: serial: use-after-free of uart_state on Driver::drop() with open tty
- [Medium] rust: serial: xmit_buf memory leak on TTY open when startup() fails
- [Medium] rust: serial: console_setup() and configure() corrupt console->ispeed
- [Low] rust: serial: missing INVARIANT comment in PortData::with_port()
- [Low] rust: serial: missing #[inline] annotations in serial abstractions
--
commit b586ab9682dee028e1eb2b244ed7a9b0fb3bdd94
Author: Kwanghoon Son <asdfkwang@gmail.com>
rust: serial: Add minimal serial-core polling console support
This commit adds serial-core abstractions in Rust for memory-mapped, two-wire
polling console drivers, including Driver, SerialOps, Port, LockedPort, and
Termios. It manages port lifetime via devres and provides callback adapters
for console output and termios configuration.
> diff --git a/rust/kernel/serial.rs b/rust/kernel/serial.rs
> new file mode 100644
> index 0000000000000..f24c685d8592f
> --- /dev/null
> +++ b/rust/kernel/serial.rs
> @@ -0,0 +1,505 @@
[ ... ]
> +#[pinned_drop]
> +impl<T: SerialOps> PinnedDrop for Driver<T> {
> + fn drop(self: Pin<&mut Self>) {
> + // SAFETY: Registration succeeded and every port holds an Arc to this driver.
> + unsafe { bindings::uart_unregister_driver(self.inner.get()) };
> + }
> +}
[Severity: Critical]
Can dropping Driver<T> after port removal cause a use-after-free of
drv->state when a TTY file descriptor is still open?
Driver::new() registers a struct tty_driver via uart_register_driver() and
sets console.device to uart_console_device, while register_port() publishes
the TTY device via uart_add_one_port(). Although SerialAdapter::startup()
returns -EOPNOTSUPP, uart_port_startup() in drivers/tty/serial/serial_core.c
turns a negative return value into 1 when capable(CAP_SYS_ADMIN) is true:
drivers/tty/serial/serial_core.c:uart_port_startup() {
...
retval = uport->ops->startup(uport);
if (retval == 0) {
...
}
if (retval && capable(CAP_SYS_ADMIN))
return 1;
return retval;
}
uart_open() -> tty_port_open() -> uart_port_activate() -> uart_startup()
then converts that positive return value into 0, allowing open() to succeed
with TTY_IO_ERROR set. When console_on_rootfs() opens /dev/console at boot,
or when a privileged userspace process opens /dev/console or the TTY device
node, it obtains an open struct tty_struct with tty->driver_data =
&drv->state[line] and tty->port = &drv->state[line].port. Even for
unprivileged callers, tty_init_dev() attaches tty->port before uart_open()
fails and tty_release() runs.
Because uart_register_driver() leaves tty_driver->owner set to serial_core's
THIS_MODULE rather than drv->owner, open TTY file descriptors do not pin the
driver module or Arc<Driver<T>>. When a device unbinds and Registration::drop()
calls uart_remove_one_port(), serial_core only calls tty_port_tty_vhangup()
(which does not free open tty_struct instances and skips /dev/console
redirected files) and waits on state->refcount (which only tracks transient
uart_port_ref() sections).
When the last Arc<Driver<T>> is then dropped, Driver::drop() calls
uart_unregister_driver(), which immediately frees drv->state:
drivers/tty/serial/serial_core.c:uart_unregister_driver() {
...
for (i = 0; i < drv->nr; i++)
tty_port_destroy(&drv->state[i].port);
kfree(drv->state);
}
When the open /dev/console or TTY file descriptor is later used or closed in
tty_release() -> uart_close() and release_tty() (which sets
tty->port->itty = NULL and calls tty_buffer_cancel_work(tty->port)), won't
the TTY layer dereference and mutate the freed drv->state memory?
[ ... ]
> + /// # Safety
> + /// `ptr` must be a PortData<T> port in a serial-core callback. Registration
> + /// owns its allocation until uart_remove_one_port returns; removal waits for
> + /// console_srcu readers and serial-core users before this port is freed.
> + /// Other device-managed resources may already have been revoked.
> + unsafe fn with_port<R>(
> + ptr: *mut bindings::uart_port,
> + f: impl for<'a> FnOnce(&'a Self, &Port<'a>) -> R,
> + ) -> R {
> + // SAFETY: The private_data type is fixed by Driver<T>/register_port<T>.
> + let state = unsafe { &*(*ptr).private_data.cast::<Self>() };
> + f(
> + state,
> + &Port {
> + raw: ptr,
> + device: &state.device,
> + },
> + )
> + }
> +}
[Severity: Low]
Since Port<'a> declares a type invariants section in its documentation,
should this Port construction in PortData::with_port() have an // INVARIANT:
comment explaining why raw and device satisfy the invariants?
[ ... ]
> + // Required TTY operations remain inactive for this output-only console.
> + extern "C" fn inactive(_ptr: *mut bindings::uart_port) {}
> +
> + extern "C" fn startup(_ptr: *mut bindings::uart_port) -> i32 {
> + EOPNOTSUPP.to_errno()
> + }
[Severity: Medium]
Does returning -EOPNOTSUPP from SerialAdapter::startup() leak
state->port.xmit_buf whenever the TTY or /dev/console is opened?
When a process opens /dev/console (including console_on_rootfs() during boot)
or the registered TTY device node, uart_open() -> tty_port_open() ->
uart_port_activate() -> uart_startup() -> uart_port_startup() allocates a
page for state->port.xmit_buf before invoking uport->ops->startup(uport):
drivers/tty/serial/serial_core.c:uart_port_startup() {
...
retval = uart_alloc_xmit_buf(&state->port);
if (retval)
return retval;
retval = uport->ops->startup(uport);
...
}
Because SerialAdapter::startup() returns -EOPNOTSUPP, uart_port_startup()
returns a non-zero value without freeing state->port.xmit_buf. In
tty_port_open(), a non-zero return from port->ops->activate() causes an early
return before tty_port_set_initialized(port, true) is called:
drivers/tty/tty_port.c:tty_port_open() {
...
if (!tty_port_initialized(port)) {
clear_bit(TTY_IO_ERROR, &tty->flags);
retval = port->ops->activate(port, tty);
if (retval) {
mutex_unlock(&port->mutex);
return retval;
}
tty_port_set_initialized(port, true);
}
...
}
Because tty_port_initialized(port) remains false, neither
tty_port_shutdown_locked() on close nor serial_core_remove_one_port() on port
removal calls uart_shutdown() to free state->port.xmit_buf:
drivers/tty/serial/serial_core.c:serial_core_remove_one_port() {
...
scoped_guard(mutex, &port->mutex) {
port->console = false;
if (tty_port_initialized(port))
uart_shutdown(NULL, state);
}
...
}
And when Driver::drop() later calls uart_unregister_driver(), drv->state is
freed without freeing state->port.xmit_buf.
> diff --git a/rust/kernel/serial/console.rs b/rust/kernel/serial/console.rs
> new file mode 100644
> index 0000000000000..3562191eb7a93
> --- /dev/null
> +++ b/rust/kernel/serial/console.rs
> @@ -0,0 +1,107 @@
[ ... ]
> + /// # Safety
> + ///
> + /// Console core must hold console_list_lock and supply this driver's live
> + /// console. `options` must be null or a valid NUL-terminated option string.
> + pub(super) unsafe extern "C" fn console_setup(
> + console: *mut bindings::console,
> + options: *mut crate::ffi::c_char,
> + ) -> i32 {
> + // SAFETY: Console core holds console_list_lock; serial core has published the port
> + // before registering this console. uart_set_options calls our termios adapter.
> + unsafe {
> + let port = Self::console_port(console);
> + if port.is_null() {
> + return ENODEV.to_errno();
> + }
> + let mut baud = (*console).ispeed as i32;
> + let (mut parity, mut bits, mut flow) = (i32::from(b'n'), 8, i32::from(b'n'));
> + if !options.is_null() {
> + bindings::uart_parse_options(options, &mut baud, &mut parity, &mut bits, &mut flow);
> + }
> + bindings::uart_set_options(port, console, baud, parity, bits, flow)
> + }
> + }
[Severity: Medium]
Can ConsoleAdapter::console_setup() and LockedPort::configure() lose
config.default_baud and overwrite console->ispeed and console->ospeed with 0?
First, if non-null console options do not start with a positive baud rate,
uart_parse_options() calls simple_strtoul() and sets baud to 0, overwriting
the initial value read from (*console).ispeed rather than preserving
config.default_baud.
Second, uart_set_options() in drivers/tty/serial/serial_core.c constructs a
zeroed dummy ktermios and calls port->ops->set_termios(port, &termios, &dummy):
drivers/tty/serial/serial_core.c:uart_set_options() {
struct ktermios termios, dummy;
...
memset(&termios, 0, sizeof(struct ktermios));
memset(&dummy, 0, sizeof(struct ktermios));
...
port->ops->set_termios(port, &termios, &dummy);
...
co->cflag = termios.c_cflag;
co->ispeed = termios.c_ispeed;
co->ospeed = termios.c_ospeed;
return 0;
}
When a driver's set_termios callback calls LockedPort::baud_rate() with an
out-of-range baud rate (or baud == 0 from uart_parse_options()),
uart_get_baud_rate() falls back to the old termios (&dummy, which has baud
rate 0), encodes 0 into new.0, and clamps its return value to at least 9600.
When the driver then passes that non-zero baud rate to
LockedPort::configure(), the check:
if bindings::tty_termios_baud_rate(&new.0) != 0 {
bindings::tty_termios_encode_baud_rate(&mut new.0, baud, baud);
}
sees tty_termios_baud_rate(&new.0) == 0 and skips encoding the actual baud
rate into new.0. uart_set_options() then copies termios.c_ispeed (0) and
termios.c_ospeed (0) into co->ispeed and co->ospeed, so any subsequent
console_setup() or port resume reads (*console).ispeed == 0 instead of
config.default_baud.
> diff --git a/rust/kernel/serial/termios.rs b/rust/kernel/serial/termios.rs
> new file mode 100644
> index 0000000000000..449662d9b6b5e
> --- /dev/null
> +++ b/rust/kernel/serial/termios.rs
> @@ -0,0 +1,104 @@
[ ... ]
> + /// Whether parity generation and checking are enabled.
> + pub fn parity_enabled(&self) -> bool {
> + self.0.c_cflag & bindings::PARENB != 0
> + }
> +
> + /// Whether odd parity, or mark parity in mark/space mode, is selected.
> + pub fn odd_parity(&self) -> bool {
> + self.0.c_cflag & bindings::PARODD != 0
> + }
> +
> + /// Whether two stop bits are requested.
> + pub fn two_stop_bits(&self) -> bool {
> + self.0.c_cflag & bindings::CSTOPB != 0
> + }
> +
> + /// Disables mark/space parity without changing parity enable or odd/even selection.
> + pub fn disable_mark_space_parity(&mut self) {
> + self.0.c_cflag &= !bindings::CMSPAR;
> + }
> +
> + /// Disables RTS/CTS hardware flow control.
> + pub fn disable_hardware_flow_control(&mut self) {
> + self.0.c_cflag &= !bindings::CRTSCTS;
> + }
> +
> + /// Data bits per character.
> + pub fn char_size(&self) -> u32 {
> + // SAFETY: c_cflag is a valid termios control flag value.
> + unsafe { bindings::tty_get_char_size(self.0.c_cflag).into() }
> + }
> +}
> +
> +impl LockedPort<'_> {
> + /// Selects a supported baud rate using serial-core termios rules.
> + ///
> + /// Returns `None` if serial core fails to select a rate. Termios changes
> + /// made by serial core are preserved, including on failure.
> + pub fn baud_rate(
> + &self,
> + new: &mut Termios,
> + old: Option<&Termios>,
> + min: u32,
> + max: u32,
> + ) -> Option<NonZeroU32> {
> + // SAFETY: The port is locked, and the termios references are valid for this callback.
> + let baud = unsafe {
> + bindings::uart_get_baud_rate(
> + self.raw,
> + &mut new.0,
> + old.map_or(core::ptr::null(), |t| &t.0),
> + min,
> + max,
> + )
> + };
> + NonZeroU32::new(baud)
> + }
> +
> + /// Updates serial-core timeout and the actual console baud rate.
> + pub fn configure(&self, new: &mut Termios, baud: NonZeroU32) {
> + let baud = baud.get();
> + // SAFETY: All port fields are accessed under the UART lock. new is exclusively borrowed.
> + unsafe {
> + bindings::uart_update_timeout(self.raw, new.0.c_cflag, baud);
> + if bindings::tty_termios_baud_rate(&new.0) != 0 {
> + bindings::tty_termios_encode_baud_rate(&mut new.0, baud, baud);
> + }
> + }
> + }
> +}
[Severity: Low]
This isn't a bug, but should the small accessor methods and C binding
wrappers in the kernel crate (Termios::from_raw, Termios::from_raw_mut,
Termios::parity_enabled, Termios::odd_parity, Termios::two_stop_bits,
Termios::disable_mark_space_parity, Termios::disable_hardware_flow_control,
Termios::char_size, LockedPort::baud_rate, and LockedPort::configure here, as
well as Port::device and LockedPort::device in rust/kernel/serial.rs) be
annotated with #[inline]?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261029-rust-bl808-console-v1-0-b736ee920078@gmail.com?part=6
next prev parent reply other threads:[~2026-10-29 5:37 UTC|newest]
Thread overview: 15+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 5:05 [PATCH RFC 00/12] Add BL808 UART3 support with a Rust serial-core polling console Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 01/12] dt-bindings: vendor-prefixes: Add Bouffalo Lab Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 02/12] dt-bindings: interrupt-controller: Add BL808 PLIC Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 03/12] dt-bindings: timer: Add BL808 CLINT Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 04/12] dt-bindings: riscv: Add Bouffalo Lab BL808 Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 05/12] dt-bindings: serial: Add BL808 UART Kwanghoon Son
2026-10-29 5:15 ` sashiko-bot
2026-10-29 5:05 ` [PATCH RFC 06/12] rust: serial: Add minimal serial-core polling console support Kwanghoon Son
2026-10-29 5:37 ` sashiko-bot [this message]
2026-10-29 5:05 ` [PATCH RFC 07/12] serial: bflb: Add Rust BL808 polling console Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 08/12] riscv: Add Bouffalo Lab SoC selection Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 09/12] riscv: dts: bouffalolab: Add BL808 SoC Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 10/12] riscv: dts: bouffalolab: Add Sipeed M1s Dock Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 11/12] riscv: defconfig: Enable Bouffalo Lab SoCs Kwanghoon Son
2026-10-29 5:05 ` [PATCH RFC 12/12] MAINTAINERS: Add Bouffalo Lab BL808 support Kwanghoon Son
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=sashiko-outbox-188759@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=asdfkwang@gmail.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=gary@garyguo.net \
--cc=linux-serial@vger.kernel.org \
--cc=ojeda@kernel.org \
--cc=robh@kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/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