From f612337cf0e0135805e81afbee97bf81542614ce Mon Sep 17 00:00:00 2001 From: Melih Emik Date: Wed, 12 Aug 2026 13:46:36 +0300 Subject: [PATCH] stty: verify tcsetattr applied all requested settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POSIX says tcsetattr() may return success even when it could only partially apply the requested settings. GNU stty reads back the terminal state with tcgetattr() after every write and exits with an error if what the kernel stored differs from what was asked for. We now do the same. The new termios_eq() helper compares the four flag groups (input/output/control/local), all control characters, and both baud rates – the same fields GNU's eq_mode() checks. Platform- specific fields such as line_discipline are intentionally excluded because the kernel may normalise them independently. Fixes #10324 --- src/uu/stty/src/stty.rs | 81 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/uu/stty/src/stty.rs b/src/uu/stty/src/stty.rs index 193c863c3d3..11c0aef24f1 100644 --- a/src/uu/stty/src/stty.rs +++ b/src/uu/stty/src/stty.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag ispeed ospeed +// spell-checker:ignore clocal erange tcgetattr tcsetattr tcsanow tiocgwinsz tiocswinsz cfgetospeed cfsetospeed ushort vmin vtime cflag lflag ispeed ospeed cfgetispeed // spell-checker:ignore parenb parodd cmspar hupcl cstopb cread clocal crtscts CSIZE // spell-checker:ignore ignbrk brkint ignpar parmrk inpck istrip inlcr igncr icrnl ixoff ixon iuclc ixany imaxbel iutf // spell-checker:ignore opost olcuc ocrnl onlcr onocr onlret ofdel nldly crdly tabdly bsdly vtdly ffdly ofill @@ -30,7 +30,7 @@ use nix::libc::{TCGETS2, termios2}; use nix::sys::termios::{ ControlFlags, InputFlags, LocalFlags, OutputFlags, SetArg, SpecialCharacterIndices as S, - Termios, cfsetispeed, cfsetospeed, tcgetattr, tcsetattr, + Termios, cfgetispeed, cfgetospeed, cfsetispeed, cfsetospeed, tcgetattr, tcsetattr, }; use nix::{ioctl_read_bad, ioctl_write_ptr_bad}; use std::cmp::Ordering; @@ -437,6 +437,20 @@ fn stty(opts: &Options) -> UResult<()> { } } tcsetattr(opts.file.as_fd(), set_arg, &termios)?; + + // POSIX allows tcsetattr to return success even when it could only partially + // apply the requested settings. GNU stty re-reads with tcgetattr and compares + // to catch this; we do the same so that callers can rely on the exit code. + let applied = tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; + if !termios_eq(&termios, &applied) { + return Err(USimpleError::new( + 1, + format!( + "{}: unable to perform all requested operations", + opts.device_name + ), + )); + } } else { let termios = tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?; print_settings(&termios, opts)?; @@ -444,6 +458,20 @@ fn stty(opts: &Options) -> UResult<()> { Ok(()) } +/// Compare two `Termios` structs for equality the same way GNU's `eq_mode()` does: +/// input/output/control/local flags, all control characters, and both baud rates. +/// We deliberately skip any platform-specific fields (like `line_discipline`) that +/// the kernel may normalise on its own. +fn termios_eq(a: &Termios, b: &Termios) -> bool { + a.input_flags == b.input_flags + && a.output_flags == b.output_flags + && a.control_flags == b.control_flags + && a.local_flags == b.local_flags + && a.control_chars == b.control_chars + && cfgetispeed(a) == cfgetispeed(b) + && cfgetospeed(a) == cfgetospeed(b) +} + // The GNU implementation adds the --help message when the args are incorrectly formatted fn missing_arg(arg: &str) -> Result> { Err(UUsageError::new( @@ -1343,9 +1371,58 @@ impl TermiosFlag for LocalFlags { #[cfg(test)] mod tests { use super::*; + use nix::sys::termios::{cfsetispeed, cfsetospeed}; // Essential unit tests for complex internal parsing and logic functions. + // Tests for termios_eq + #[test] + fn test_termios_eq_identical() { + // Two default Termios values should be equal to themselves. + // We can't easily construct a Termios from scratch, so we use + // the kernel's own defaults by opening /dev/null... but that's + // not a tty. Instead, just verify that the function is reflexive + // on whatever a default Termios looks like by constructing two + // identical structs via the nix defaults. + // + // nix doesn't expose a Termios::new(); the easiest portable way + // to get a valid one is unsafe zeroing. That's fine for a unit + // test: we're testing the comparison logic, not kernel values. + let a: Termios = unsafe { std::mem::zeroed() }; + let b: Termios = unsafe { std::mem::zeroed() }; + assert!(termios_eq(&a, &b)); + } + + #[test] + fn test_termios_eq_flag_differs() { + let mut a: Termios = unsafe { std::mem::zeroed() }; + let mut b: Termios = unsafe { std::mem::zeroed() }; + // Flip one input flag so the two structs differ. + a.input_flags |= InputFlags::IGNBRK; + assert!(!termios_eq(&a, &b)); + b.input_flags |= InputFlags::IGNBRK; + assert!(termios_eq(&a, &b)); + } + + #[test] + fn test_termios_eq_control_char_differs() { + let mut a: Termios = unsafe { std::mem::zeroed() }; + let b: Termios = unsafe { std::mem::zeroed() }; + a.control_chars[S::VINTR as usize] = 3; // ^C + assert!(!termios_eq(&a, &b)); + } + + #[test] + fn test_termios_eq_baud_differs() { + let mut a: Termios = unsafe { std::mem::zeroed() }; + let mut b: Termios = unsafe { std::mem::zeroed() }; + // Set different output baud rates. + cfsetospeed(&mut a, nix::sys::termios::BaudRate::B9600).unwrap(); + cfsetospeed(&mut b, nix::sys::termios::BaudRate::B115200).unwrap(); + assert!(!termios_eq(&a, &b)); + } + + // Control character parsing tests #[test] fn test_string_to_control_char_undef() {