fix(tty): preserve raw flags across termios ABI conversions (#2267)

TCSETS and the legacy TCSETA family discard flag bits that are not named
in the kernel bitflags definitions. This differs from Linux 6.6, which
copies the raw flag words and preserves the upper 16 bits when merging
legacy termio settings.

Preserve all four raw flag words at both ABI conversion boundaries while
leaving hardware normalization to the existing driver callbacks. Keep
legacy control-character merging and baud-rate extraction unchanged.

Use bitflags difference() when restoring old hardware settings after an
unsupported driver callback. Unlike the bitflags complement operator,
it preserves unnamed bits outside the software-controlled mask, matching
tty_termios_copy_hw().

Extend the existing dunitest suite to cover all six set commands, raw
flag preservation, legacy control-character tails, input flush behavior,
EFAULT without state changes, the 18-byte page-boundary termio ABI and
zeroed padding, and console stdin settings.

Validation:
- Reproduced raw flag loss before the fix: 25 of 26 tty_termios tests
  passed, with only the new flag-preservation case failing.
- After the fix, QEMU x86_64 passed tty_termios (26), tty_tcflush (6),
  tty_pty_hangup (36), and the existing C termios smoke test.
- Verified TCSAFLUSH and legacy settings on ttyS0 stdin, plus a successful
  apt install of a local test package without the TCSAFLUSH error.
- All five added tests passed on host Linux with a PTY.
- Kernel build, Rust formatting and whitespace checks passed.

Signed-off-by: longjin <longjin@dragonos.org>
This commit is contained in:
LoGin
2026-09-08 21:33:05 +08:00
committed by GitHub
parent 6a7c3656c6
commit ba1694602a
3 changed files with 194 additions and 19 deletions
+20 -17
View File
@@ -67,12 +67,13 @@ impl PosixTermios {
}
pub fn to_kernel_termios(self) -> Termios {
// TODO:这里没有考虑非规范模式
// Linux copies the raw flag words, including unnamed bits. Preserve
// them at the ABI boundary; drivers normalize hardware settings.
Termios {
input_mode: InputMode::from_bits_truncate(self.c_iflag),
output_mode: OutputMode::from_bits_truncate(self.c_oflag),
control_mode: ControlMode::from_bits_truncate(self.c_cflag),
local_mode: LocalMode::from_bits_truncate(self.c_lflag),
input_mode: InputMode { bits: self.c_iflag },
output_mode: OutputMode { bits: self.c_oflag },
control_mode: ControlMode { bits: self.c_cflag },
local_mode: LocalMode { bits: self.c_lflag },
control_characters: self.c_cc,
line: LineDisciplineType::from_line(self.c_line),
c_line_abi: self.c_line,
@@ -472,22 +473,24 @@ impl PosixTermio {
let mut cc = old.control_characters;
cc[..NCC].copy_from_slice(&self.c_cc);
let control_mode = ControlMode::from_bits_truncate(
(old.control_mode.bits & 0xffff_0000) | self.c_cflag as u32,
);
// As with termios, neither half of the merged ABI flags may be
// truncated to the set of bits currently named by the kernel.
let control_mode = ControlMode {
bits: (old.control_mode.bits & 0xffff_0000) | self.c_cflag as u32,
};
let (input_speed, output_speed) = Self::speeds_from_cflag(control_mode);
Termios {
input_mode: InputMode::from_bits_truncate(
(old.input_mode.bits & 0xffff_0000) | self.c_iflag as u32,
),
output_mode: OutputMode::from_bits_truncate(
(old.output_mode.bits & 0xffff_0000) | self.c_oflag as u32,
),
input_mode: InputMode {
bits: (old.input_mode.bits & 0xffff_0000) | self.c_iflag as u32,
},
output_mode: OutputMode {
bits: (old.output_mode.bits & 0xffff_0000) | self.c_oflag as u32,
},
control_mode,
local_mode: LocalMode::from_bits_truncate(
(old.local_mode.bits & 0xffff_0000) | self.c_lflag as u32,
),
local_mode: LocalMode {
bits: (old.local_mode.bits & 0xffff_0000) | self.c_lflag as u32,
},
control_characters: cc,
line: LineDisciplineType::from_line(self.c_line),
c_line_abi: self.c_line,
+5 -2
View File
@@ -651,8 +651,11 @@ impl TtyCore {
let mut termios = tty.core().termios_write();
if ret.is_err() {
termios.control_mode &= ControlMode::HUPCL | ControlMode::CREAD | ControlMode::CLOCAL;
termios.control_mode |= old_termios.control_mode
& !(ControlMode::HUPCL | ControlMode::CREAD | ControlMode::CLOCAL);
// bitflags' !mask truncates unnamed bits. Linux preserves all
// old hardware bits outside this software-controlled mask.
termios.control_mode |= old_termios
.control_mode
.difference(ControlMode::HUPCL | ControlMode::CREAD | ControlMode::CLOCAL);
termios.input_speed = old_termios.input_speed;
termios.output_speed = old_termios.output_speed;
}
@@ -15,6 +15,7 @@
#include <stdint.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
@@ -33,6 +34,8 @@ struct TermioCompat {
unsigned char _pad = 0; /* match kernel PosixTermio layout */
};
static_assert(sizeof(TermioCompat) == 18, "Linux generic termio ABI");
#ifndef TCGETA
#define TCGETA 0x5405
#endif
@@ -46,6 +49,28 @@ struct TermioCompat {
#define TCSETAF 0x5408
#endif
// TCGETS/TCSETS use 19 control characters, even when libc NCCS is larger.
constexpr size_t kKernelNccs = 19;
constexpr int kSetCommands[] = {TCSETS, TCSETSW, TCSETSF, TCSETA, TCSETAW, TCSETAF};
bool IsLegacySet(int cmd) {
return cmd == TCSETA || cmd == TCSETAW || cmd == TCSETAF;
}
int SetAttributes(int fd, int cmd, const struct termios& term) {
if (!IsLegacySet(cmd)) {
return ioctl(fd, cmd, &term);
}
TermioCompat legacy = {};
legacy.c_iflag = term.c_iflag;
legacy.c_oflag = term.c_oflag;
legacy.c_cflag = term.c_cflag;
legacy.c_lflag = term.c_lflag;
legacy.c_line = term.c_line;
memcpy(legacy.c_cc, term.c_cc, kNcc);
return ioctl(fd, cmd, &legacy);
}
class UniqueFd {
public:
UniqueFd() = default;
@@ -802,6 +827,150 @@ TEST(TtyTermios, TermioMergeHighBits) {
<< "TCSETA should apply low-16-bit change";
}
TEST(TtyTermios, SetFamilyPreservesRawFlagsAndLegacyTail) {
// Unnamed bits must survive the ABI boundary. Avoid flags normalized by
// the PTY driver and the core (CSIZE, PARENB, CREAD, baud masks and ADDRB).
constexpr tcflag_t high = 0x80000000u;
constexpr tcflag_t unnamed_input_low = 0x8000u;
for (int cmd : kSetCommands) {
SCOPED_TRACE(cmd);
auto pty = OpenRawPty();
ASSERT_GE(pty.slave.get(), 0);
struct termios initial = {};
ASSERT_EQ(tcgetattr(pty.slave.get(), &initial), 0);
initial.c_iflag |= high;
initial.c_oflag |= high;
initial.c_cflag |= high;
initial.c_lflag |= high;
for (size_t i = kNcc; i < kKernelNccs; ++i) {
initial.c_cc[i] = static_cast<cc_t>(0x60 + i);
}
// Exercise the kernel ABI directly: libc may reserve flag bits for
// its own speed bookkeeping before issuing TCSETS.
ASSERT_EQ(ioctl(pty.slave.get(), TCSETS, &initial), 0);
struct termios requested = initial;
requested.c_iflag |= unnamed_input_low;
requested.c_lflag |= ECHONL;
requested.c_cc[VINTR] = 0x1c;
ASSERT_EQ(SetAttributes(pty.slave.get(), cmd, requested), 0) << strerror(errno);
struct termios back = {};
ASSERT_EQ(tcgetattr(pty.slave.get(), &back), 0);
EXPECT_EQ(back.c_iflag & (high | unnamed_input_low), high | unnamed_input_low);
EXPECT_EQ(back.c_oflag & high, high);
EXPECT_EQ(back.c_cflag & high, high);
EXPECT_EQ(back.c_lflag & (high | ECHONL), high | ECHONL);
EXPECT_EQ(back.c_cc[VINTR], requested.c_cc[VINTR]);
for (size_t i = kNcc; i < kKernelNccs; ++i) {
EXPECT_EQ(back.c_cc[i], initial.c_cc[i]) << "c_cc[" << i << "]";
}
}
}
TEST(TtyTermios, SetFamilyFlushesOnlyForFlushCommands) {
for (int cmd : kSetCommands) {
SCOPED_TRACE(cmd);
auto pty = OpenRawPty();
ASSERT_GE(pty.slave.get(), 0);
ASSERT_TRUE(SetNonBlocking(pty.slave.get()));
struct termios term = {};
ASSERT_EQ(tcgetattr(pty.slave.get(), &term), 0);
const char old_input = 'a';
ASSERT_EQ(write(pty.master.get(), &old_input, 1), 1);
struct pollfd ready = {pty.slave.get(), POLLIN, 0};
ASSERT_EQ(poll(&ready, 1, 1000), 1);
ASSERT_NE(ready.revents & POLLIN, 0);
ASSERT_EQ(SetAttributes(pty.slave.get(), cmd, term), 0) << strerror(errno);
char ch = 0;
if (cmd == TCSETSF || cmd == TCSETAF) {
errno = 0;
EXPECT_EQ(read(pty.slave.get(), &ch, 1), -1);
EXPECT_TRUE(errno == EAGAIN || errno == EWOULDBLOCK) << strerror(errno);
} else {
ASSERT_EQ(read(pty.slave.get(), &ch, 1), 1);
EXPECT_EQ(ch, old_input);
}
const char new_input = 'b';
ASSERT_EQ(write(pty.master.get(), &new_input, 1), 1);
ASSERT_EQ(ReadEventually(pty.slave.get(), &ch, 1), 1);
EXPECT_EQ(ch, new_input);
}
}
TEST(TtyTermios, BadSetPointerDoesNotChangeAttributesOrFlushInput) {
for (int cmd : kSetCommands) {
SCOPED_TRACE(cmd);
auto pty = OpenRawPty();
ASSERT_GE(pty.slave.get(), 0);
struct termios before = {};
ASSERT_EQ(tcgetattr(pty.slave.get(), &before), 0);
const char marker = 'x';
ASSERT_EQ(write(pty.master.get(), &marker, 1), 1);
struct pollfd ready = {pty.slave.get(), POLLIN, 0};
ASSERT_EQ(poll(&ready, 1, 1000), 1);
ASSERT_NE(ready.revents & POLLIN, 0);
errno = 0;
EXPECT_EQ(ioctl(pty.slave.get(), cmd, nullptr), -1);
EXPECT_EQ(errno, EFAULT);
struct termios after = {};
ASSERT_EQ(tcgetattr(pty.slave.get(), &after), 0);
EXPECT_EQ(after.c_iflag, before.c_iflag);
EXPECT_EQ(after.c_oflag, before.c_oflag);
EXPECT_EQ(after.c_cflag, before.c_cflag);
EXPECT_EQ(after.c_lflag, before.c_lflag);
EXPECT_EQ(after.c_line, before.c_line);
EXPECT_EQ(memcmp(after.c_cc, before.c_cc, kKernelNccs), 0);
char ch = 0;
ASSERT_EQ(ReadEventually(pty.slave.get(), &ch, 1), 1);
EXPECT_EQ(ch, marker);
}
}
TEST(TtyTermios, LegacyAbiAtPageBoundaryAndZeroPadding) {
auto pty = OpenRawPty();
ASSERT_GE(pty.slave.get(), 0);
const long page_size = sysconf(_SC_PAGESIZE);
ASSERT_GT(page_size, static_cast<long>(sizeof(TermioCompat)));
void* mapping = mmap(nullptr, 2 * page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
ASSERT_NE(mapping, MAP_FAILED);
// Release the mapping even if a fatal assertion below returns early.
struct UnmapOnExit {
void* addr;
size_t size;
~UnmapOnExit() { munmap(addr, size); }
} unmap{mapping, static_cast<size_t>(2 * page_size)};
ASSERT_EQ(mprotect(static_cast<char*>(mapping) + page_size, page_size, PROT_NONE), 0);
auto* legacy = reinterpret_cast<TermioCompat*>(
static_cast<char*>(mapping) + page_size - sizeof(TermioCompat));
for (int cmd : {TCSETA, TCSETAW, TCSETAF}) {
SCOPED_TRACE(cmd);
memset(legacy, 0xa5, sizeof(*legacy));
ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, legacy), 0) << strerror(errno);
EXPECT_EQ(legacy->_pad, 0);
legacy->_pad = 0xa5;
legacy->c_line = 42;
ASSERT_EQ(ioctl(pty.slave.get(), cmd, legacy), 0) << strerror(errno);
ASSERT_EQ(ioctl(pty.slave.get(), TCGETA, legacy), 0);
EXPECT_EQ(legacy->c_line, 42);
EXPECT_EQ(legacy->_pad, 0);
}
}
TEST(TtyTermios, StdinFlushAndLegacySettings) {
if (!isatty(STDIN_FILENO)) {
GTEST_SKIP() << "stdin is not a TTY; run from the QEMU console for acceptance";
}
struct termios original = {};
ASSERT_EQ(tcgetattr(STDIN_FILENO, &original), 0) << strerror(errno);
TermiosRestorer restore(STDIN_FILENO, original);
ASSERT_EQ(tcsetattr(STDIN_FILENO, TCSAFLUSH, &original), 0) << strerror(errno);
for (int cmd : {TCSETA, TCSETAW, TCSETAF}) {
EXPECT_EQ(SetAttributes(STDIN_FILENO, cmd, original), 0) << cmd << ": " << strerror(errno);
}
}
/* --------------------------------------------------------------------------
* Serial8250 termios must reach the UART hardware callback. Before the
* regression fix the default ENOSYS callback made the TTY core restore all