Skip to content

Commit

Permalink
Try TCGETS before trying TCGETS2.
Browse files Browse the repository at this point in the history
Switch `tcgetattr`/`tcsetattr` from using `TCGETS2`/`TCSETS2` first to using
`TCGETS`/`TCSETS` first. Have `tcgetattr` fall back to `TCGETS2` if the
`TCGETS` flags indicate that a custom speed is in used.

glibc and musl have not yet migrated to `TCGETS2`/`TCSETS2`, and as a
result, seccomp sandboxes and Linux-like environments such as WSL don't always
support them.

Also, fix some bugs in QEMU related to the handling of termios syscalls.
This eliminates the need for having rustix do extra fixups on PowerPC.

This is expected to fix crossterm-rs/crossterm#912.
  • Loading branch information
sunfishcode committed Sep 3, 2024
1 parent 6f2ec0c commit 9c007f1
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 190 deletions.
87 changes: 81 additions & 6 deletions ci/tcgets2-tcsets2.patch
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,22 @@ This implements the `TCGETS2` and `TCSETS2` ioctls.
diff -ur a/linux-user/ioctls.h b/linux-user/ioctls.h
--- a/linux-user/ioctls.h
+++ b/linux-user/ioctls.h
@@ -2,6 +2,8 @@
@@ -1,5 +1,15 @@
/* emulated ioctl list */

IOCTL(TCGETS, IOC_R, MK_PTR(MK_STRUCT(STRUCT_termios)))
IOCTL(TCSETS, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios)))
+ /*
+ * Put `TCGETS2`/`TCGETS2` before `TCGETS`/`TCSETS` so that on targets
+ * where these have the same value, we find the `TCGETS2`/`TCSETS2`
+ * entries first, which ensures that we set the `c_ispeed` and `c_ospeed`
+ * fields if the target needed them.
+ */
+ IOCTL(TCGETS2, IOC_R, MK_PTR(MK_STRUCT(STRUCT_termios2)))
+ IOCTL(TCSETS2, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios2)))
+ IOCTL(TCSETSF2, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios2)))
+ IOCTL(TCSETSW2, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios2)))
IOCTL(TCGETS, IOC_R, MK_PTR(MK_STRUCT(STRUCT_termios)))
IOCTL(TCSETS, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios)))
IOCTL(TCSETSF, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios)))
IOCTL(TCSETSW, IOC_W, MK_PTR(MK_STRUCT(STRUCT_termios)))
IOCTL(TIOCGWINSZ, IOC_R, MK_PTR(MK_STRUCT(STRUCT_winsize)))
diff -ur a/linux-user/ppc/termbits.h b/linux-user/ppc/termbits.h
--- a/linux-user/ppc/termbits.h
+++ b/linux-user/ppc/termbits.h
Expand Down Expand Up @@ -53,12 +60,14 @@ diff -ur a/linux-user/ppc/termbits.h b/linux-user/ppc/termbits.h

#define TARGET_CSIZE 00001400
#define TARGET_CS5 00000000
@@ -178,6 +192,8 @@
@@ -178,6 +192,10 @@
#define TARGET_TCSETS TARGET_IOW('t', 20, struct target_termios)
#define TARGET_TCSETSW TARGET_IOW('t', 21, struct target_termios)
#define TARGET_TCSETSF TARGET_IOW('t', 22, struct target_termios)
+#define TARGET_TCGETS2 TARGET_TCGETS
+#define TARGET_TCSETS2 TARGET_TCSETS
+#define TARGET_TCSETSW2 TARGET_TCSETSW
+#define TARGET_TCSETSF2 TARGET_TCSETSF

#define TARGET_TCGETA TARGET_IOR('t', 23, struct target_termio)
#define TARGET_TCSETA TARGET_IOW('t', 24, struct target_termio)
Expand Down Expand Up @@ -157,6 +166,15 @@ diff -ur a/linux-user/strace.c b/linux-user/strace.c

#undef UNUSED

@@ -4161,7 +4161,7 @@
int target_size;

for (ie = ioctl_entries; ie->target_cmd != 0; ie++) {
- if (ie->target_cmd == arg1) {
+ if (ie->target_cmd == (int)arg1) {
break;
}
}
diff -ur a/linux-user/syscall.c b/linux-user/syscall.c
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
Expand Down Expand Up @@ -326,3 +344,60 @@ diff -ur a/linux-user/user-internals.h b/linux-user/user-internals.h

/* ARM EABI and MIPS expect 64bit types aligned even on pairs or registers */
#ifdef TARGET_ARM
diff -ur -x build -x roms a/linux-user/mips/termbits.h b/linux-user/mips/termbits.h
--- a/linux-user/mips/termbits.h
+++ b/linux-user/mips/termbits.h
@@ -18,6 +18,17 @@
target_cc_t c_cc[TARGET_NCCS]; /* control characters */
};

+struct target_termios2 {
+ target_tcflag_t c_iflag; /* input mode flags */
+ target_tcflag_t c_oflag; /* output mode flags */
+ target_tcflag_t c_cflag; /* control mode flags */
+ target_tcflag_t c_lflag; /* local mode flags */
+ target_cc_t c_line; /* line discipline */
+ target_cc_t c_cc[TARGET_NCCS]; /* control characters */
+ target_speed_t c_ispeed; /* input speed */
+ target_speed_t c_ospeed; /* output speed */
+};
+
/* c_iflag bits */
#define TARGET_IGNBRK 0000001
#define TARGET_BRKINT 0000002
@@ -117,6 +128,7 @@
#define TARGET_B3500000 0010016
#define TARGET_B4000000 0010017
#define TARGET_CIBAUD 002003600000 /* input baud rate (not used) */
+#define TARGET_IBSHIFT 16
#define TARGET_CMSPAR 010000000000 /* mark or space (stick) parity */
#define TARGET_CRTSCTS 020000000000 /* flow control */

@@ -217,9 +229,9 @@
#define TARGET_TIOCSETP 0x7409
#define TARGET_TIOCSETN 0x740a /* TIOCSETP wo flush */

-/* #define TARGET_TIOCSETA TARGET_IOW('t', 20, struct termios) set termios struct */
-/* #define TARGET_TIOCSETAW TARGET_IOW('t', 21, struct termios) drain output, set */
-/* #define TARGET_TIOCSETAF TARGET_IOW('t', 22, struct termios) drn out, fls in, set */
+/* #define TARGET_TIOCSETA TARGET_IOW('t', 20, struct target_termios) set termios struct */
+/* #define TARGET_TIOCSETAW TARGET_IOW('t', 21, struct target_termios) drain output, set */
+/* #define TARGET_TIOCSETAF TARGET_IOW('t', 22, struct target_termios) drn out, fls in, set */
/* #define TARGET_TIOCGETD TARGET_IOR('t', 26, int) get line discipline */
/* #define TARGET_TIOCSETD TARGET_IOW('t', 27, int) set line discipline */
/* 127-124 compat */
@@ -227,10 +239,10 @@
#define TARGET_TIOCSBRK 0x5427 /* BSD compatibility */
#define TARGET_TIOCCBRK 0x5428 /* BSD compatibility */
#define TARGET_TIOCGSID 0x7416 /* Return the session ID of FD */
-#define TARGET_TCGETS2 TARGET_IOR('T', 0x2A, struct termios2)
-#define TARGET_TCSETS2 TARGET_IOW('T', 0x2B, struct termios2)
-#define TARGET_TCSETSW2 TARGET_IOW('T', 0x2C, struct termios2)
-#define TARGET_TCSETSF2 TARGET_IOW('T', 0x2D, struct termios2)
+#define TARGET_TCGETS2 TARGET_IOR('T', 0x2A, struct target_termios2)
+#define TARGET_TCSETS2 TARGET_IOW('T', 0x2B, struct target_termios2)
+#define TARGET_TCSETSW2 TARGET_IOW('T', 0x2C, struct target_termios2)
+#define TARGET_TCSETSF2 TARGET_IOW('T', 0x2D, struct target_termios2)
#define TARGET_TIOCGRS485 TARGET_IOR('T', 0x2E, struct serial_rs485)
#define TARGET_TIOCSRS485 TARGET_IOWR('T', 0x2F, struct serial_rs485)
#define TARGET_TIOCGPTN TARGET_IOR('T',0x30, unsigned int) /* Get Pty Number (of pty-mux device) */
19 changes: 13 additions & 6 deletions src/backend/libc/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,19 +102,26 @@ pub(crate) const O_LARGEFILE: c_int = 0x2000;
pub(crate) const MAP_DROPPABLE: u32 = 0x8;

// On PowerPC, the regular `termios` has the `termios2` fields and there is no
// `termios2`. linux-raw-sys has aliases `termios2` to `termios` to cover this
// difference, but we still need to manually import it since `libc` doesn't
// have this.
// `termios2`, so we define aliases.
#[cfg(all(
linux_kernel,
feature = "termios",
any(target_arch = "powerpc", target_arch = "powerpc64")
))]
pub(crate) use {
linux_raw_sys::general::{termios2, CIBAUD},
linux_raw_sys::ioctl::{TCGETS2, TCSETS2, TCSETSF2, TCSETSW2},
pub(crate) use libc::{
termios as termios2, TCGETS as TCGETS2, TCSETS as TCSETS2, TCSETSF as TCSETSF2,
TCSETSW as TCSETSW2,
};

// And PowerPC doesn't define `CIBAUD`, but it does define `IBSHIFT`, so we can
// compute `CIBAUD` ourselves.
#[cfg(all(
linux_kernel,
feature = "termios",
any(target_arch = "powerpc", target_arch = "powerpc64")
))]
pub(crate) const CIBAUD: u32 = libc::CBAUD << libc::IBSHIFT;

// Automatically enable “large file” support (LFS) features.

#[cfg(target_os = "vxworks")]
Expand Down
172 changes: 100 additions & 72 deletions src/backend/libc/termios/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,69 @@ use {

#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
// If we have `TCGETS2`, use it, so that we fill in the `c_ispeed` and
// `c_ospeed` fields.
// On Linux, use `TCGETS`, and fall back to `TCGETS2` if we need to get the
// `c_ispeed` and `c_ospeed` field values.
//
// SAFETY: This invokes the `TCGETS` and optionally also `TCGETS2` ioctls.
// `TCGETS` doesn't initialize the `input_speed` and `output_speed` fields
// of the result, so in the case where we don't call `TCGETS2`, we infer
// their values from other fields and then manually initialize them (except
// on PowerPC where `TCGETS` does initialize those fields).
#[cfg(linux_kernel)]
{
unsafe {
use crate::termios::{ControlModes, InputModes, LocalModes, OutputModes, SpecialCodes};
use crate::utils::default_array;

let termios2 = unsafe {
let mut termios2 = MaybeUninit::<c::termios2>::uninit();
let mut termios2 = MaybeUninit::<c::termios2>::uninit();

// QEMU's `TCGETS2` doesn't currently set `input_speed` or
// `output_speed` on PowerPC, so zero out the fields ourselves.
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
{
termios2.write(core::mem::zeroed());
}
// Use the old `TCGETS`. Linux has supported `TCGETS2` since Linux
// 2.6.40, however glibc and musl haven't migrated to it yet, so it
// tends to get left out of seccomp sandboxes. It's also unsupported
// by WSL, and likely enough other things too, so we use the old
// `TCGETS` for compatibility.
ret(c::ioctl(
borrowed_fd(fd),
c::TCGETS as _,
termios2.as_mut_ptr(),
))?;

ret(c::ioctl(
borrowed_fd(fd),
c::TCGETS2 as _,
termios2.as_mut_ptr(),
))?;
// If `BOTHER` is set for either the input or output, that means
// there's a custom speed, which means we really do need to use
// `TCGETS2`. Unless we're on PowerPC, where `TCGETS` is the same
// as `TCGETS2`.
#[cfg(not(any(target_arch = "powerpc", target_arch = "powerpc64")))]
{
use crate::termios::speed;
use core::ptr::{addr_of, addr_of_mut};

// At this point, `termios2` is initialized except for the
// `c_ispeed` and `c_ospeed` fields, so we don't form a reference
// yet.
let ptr = termios2.as_mut_ptr();

let control_modes = addr_of!((*ptr).c_cflag).read();
let encoded_out = control_modes & c::CBAUD;
let encoded_in = (control_modes & c::CIBAUD) >> c::IBSHIFT;
if encoded_out == c::BOTHER || encoded_in == c::BOTHER {
ret(c::ioctl(borrowed_fd(fd), c::TCGETS2 as _, ptr))?;
} else {
// Otherwise, set the speeds from other fields.
let output_speed = speed::decode(encoded_out).unwrap();
addr_of_mut!((*ptr).c_ospeed).write(output_speed);

termios2.assume_init()
};
// For input speeds, `B0` is special-cased to mean the input
// speed is the same as the output speed.
let input_speed = if encoded_in == c::B0 {
output_speed
} else {
speed::decode(encoded_in).unwrap()
};
addr_of_mut!((*ptr).c_ispeed).write(input_speed);
}
}

// Now all the fields are set.
let termios2 = termios2.assume_init();

// Convert from the Linux `termios2` to our `Termios`.
let mut result = Termios {
Expand All @@ -68,32 +106,6 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
output_speed: termios2.c_ospeed,
};

// QEMU's `TCGETS2` doesn't currently set `input_speed` or
// `output_speed` on PowerPC, so set them manually if we can.
#[cfg(any(target_arch = "powerpc", target_arch = "powerpc64"))]
{
use crate::termios::speed;

if result.output_speed == 0 && (termios2.c_cflag & c::CBAUD) != c::BOTHER {
if let Some(output_speed) = speed::decode(termios2.c_cflag & c::CBAUD) {
result.output_speed = output_speed;
}
}
if result.input_speed == 0
&& ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) != c::BOTHER
{
// For input speeds, `B0` is special-cased to mean the input
// speed is the same as the output speed.
if ((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT) == c::B0 {
result.input_speed = result.output_speed;
} else if let Some(input_speed) =
speed::decode((termios2.c_cflag & c::CIBAUD) >> c::IBSHIFT)
{
result.input_speed = input_speed;
}
}
}

result.special_codes.0[..termios2.c_cc.len()].copy_from_slice(&termios2.c_cc);

Ok(result)
Expand Down Expand Up @@ -139,41 +151,50 @@ pub(crate) fn tcsetattr(
optional_actions: OptionalActions,
termios: &Termios,
) -> io::Result<()> {
// If we have `TCSETS2`, use it, so that we use the `c_ispeed` and
// `c_ospeed` fields.
#[cfg(linux_kernel)]
{
use crate::termios::speed;
use crate::utils::default_array;
use linux_raw_sys::general::{termios2, BOTHER, CBAUD, IBSHIFT};

#[cfg(not(any(target_arch = "sparc", target_arch = "sparc64")))]
use linux_raw_sys::ioctl::{TCSETS, TCSETS2};

// linux-raw-sys' ioctl-generation script for sparc isn't working yet,
// so as a temporary workaround, declare these manually.
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
const TCSETS: u32 = 0x8024_5409;
#[cfg(any(target_arch = "sparc", target_arch = "sparc64"))]
const TCSETS2: u32 = 0x802c_540d;

// Translate from `optional_actions` into an ioctl request code. On
// MIPS, `optional_actions` already has `TCGETS` added to it.
let request = TCSETS2
+ if cfg!(any(

// Similar to `tcgetattr`, use the old `TCSETS`, unless either input
// speed or output speed is custom, then we really have to use
// `TCSETS2`.
let encoded_out = termios.control_modes.bits() & c::CBAUD;
let encoded_in = (termios.control_modes.bits() & c::CIBAUD) >> c::IBSHIFT;
let request = if encoded_out == c::BOTHER || encoded_in == c::BOTHER {
// Translate from `optional_actions` into a `TCGETS2` ioctl request
// code. On MIPS, `optional_actions` already has `TCGETS` added to
// it.
c::TCSETS2 as c::c_ulong
+ if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6"
)) {
optional_actions as c::c_ulong - c::TCSETS as c::c_ulong
} else {
optional_actions as c::c_ulong
}
} else {
// Translate from `optional_actions` into a `TCGETS` ioctl request
// code. On MIPS, `optional_actions` already has `TCGETS` added to
// it.
if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6"
)) {
optional_actions as u32 - TCSETS
optional_actions as c::c_ulong
} else {
optional_actions as u32
};
optional_actions as c::c_ulong + c::TCSETS as c::c_ulong
}
};

let input_speed = termios.input_speed();
let output_speed = termios.output_speed();
let mut termios2 = termios2 {
let input_speed = termios.input_speed();
let mut termios2 = c::termios2 {
c_iflag: termios.input_modes.bits(),
c_oflag: termios.output_modes.bits(),
c_cflag: termios.control_modes.bits(),
Expand All @@ -183,18 +204,25 @@ pub(crate) fn tcsetattr(
c_ispeed: input_speed,
c_ospeed: output_speed,
};

// Ensure that our input and output speeds are set, as `libc`
// routines don't always support setting these separately.
termios2.c_cflag &= !CBAUD;
termios2.c_cflag |= speed::encode(output_speed).unwrap_or(BOTHER);
termios2.c_cflag &= !(CBAUD << IBSHIFT);
termios2.c_cflag |= speed::encode(input_speed).unwrap_or(BOTHER) << IBSHIFT;
termios2.c_cflag &= !c::CBAUD;
termios2.c_cflag |= speed::encode(output_speed).unwrap_or(c::BOTHER);
termios2.c_cflag &= !c::CIBAUD;
termios2.c_cflag |= speed::encode(input_speed).unwrap_or(c::BOTHER) << c::IBSHIFT;
let nccs = termios2.c_cc.len();
termios2
.c_cc
.copy_from_slice(&termios.special_codes.0[..nccs]);

unsafe { ret(c::ioctl(borrowed_fd(fd), request as _, &termios2)) }
unsafe {
ret(c::ioctl(
borrowed_fd(fd),
request as _,
crate::utils::as_ptr(&termios2),
))
}
}

#[cfg(not(linux_kernel))]
Expand Down
Loading

0 comments on commit 9c007f1

Please sign in to comment.