Technical Analysis of the Root Cause
1. Overview and Files Involved
- File:
drivers/tty/n_tty.c
- Functions:
n_tty_lookahead_flow_ctrl(), reset_buffer_flags() (inlined in n_tty_flush_buffer()), __receive_buf()
- Structs and Fields:
struct n_tty_data::lookahead_count, struct tty_struct::termios_rwsem
2. Synchronization Invariant and Mechanism
In the N_TTY line discipline (drivers/tty/n_tty.c), struct n_tty_data contains buffer pointers and counters that coordinate input processing between producers, consumers, and buffer flush operations. ldata->lookahead_count tracks how many incoming bytes have already been scanned ahead in tty_buffer to identify software flow control characters (START_CHAR / STOP_CHAR when IXON is enabled).
The synchronization rules for struct n_tty_data fields rely on tty->termios_rwsem:
- Read/Consumer path & Normal receive path:
n_tty_receive_buf_common() acquires down_read(&tty->termios_rwsem) before calling __receive_buf(). Inside __receive_buf(), ldata->lookahead_count is read (size_t la_count = min(ldata->lookahead_count, count);) and decremented (ldata->lookahead_count -= la_count;).
- Buffer reset & flush path:
n_tty_flush_buffer() acquires down_write(&tty->termios_rwsem) and calls reset_buffer_flags(), which resets ldata->lookahead_count = 0; alongside resetting head and tail indices.
3. Root Cause of the Data Race
When flush_to_ldisc() in drivers/tty/tty_buffer.c cannot push all characters to the line discipline (e.g. rcvd < count), it invokes lookahead_bufs() -> tty_port_default_lookahead_buf() -> n_tty_lookahead_flow_ctrl().
Unlike n_tty_receive_buf_common(), n_tty_lookahead_flow_ctrl() does not acquire tty->termios_rwsem (either for read or write):
static void n_tty_lookahead_flow_ctrl(struct tty_struct *tty, const u8 *cp,
const u8 *fp, size_t count)
{
struct n_tty_data *ldata = tty->disc_data;
u8 flag = TTY_NORMAL;
ldata->lookahead_count += count; /* Unsynchronized read-modify-write */
...
When a concurrent thread initiates hangup, close, or an ioctl flush (such as pty_close() -> tty_vhangup() -> __tty_hangup() -> tty_ldisc_hangup() -> n_tty_flush_buffer()), it executes:
static void reset_buffer_flags(struct n_tty_data *ldata)
{
...
ldata->lookahead_count = 0;
}
Because n_tty_lookahead_flow_ctrl() executes without tty->termios_rwsem, the write ldata->lookahead_count = 0 in reset_buffer_flags() and the read-modify-write ldata->lookahead_count += count in n_tty_lookahead_flow_ctrl() execute concurrently on different CPUs without synchronization, triggering the reported KCSAN data race.