Skip to content

Commit bc4fa30

Browse files
authored
Fix tcgetpgrp and tcsetpgrp on Linux. (#910)
On Linux, fix how the pid argument is passed to the `TIOCSPGRP` ioctl used by `tcsetgrp`. And, on Linux, it appears `tcgetpgrp` can return a pid of 0 when the fd is a pseudo-terminal device. This isn't documented behavior, and isn't compatible with rustix's current signaure for `tcgetpgrp` since it returns a `Pid` which can't be zero, so handle this situation by having it return `Errno::OPNOTSUPP` for now.
1 parent cf2ad7f commit bc4fa30

File tree

7 files changed

+211
-2
lines changed

7 files changed

+211
-2
lines changed

src/backend/libc/termios/syscalls.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ pub(crate) fn tcgetattr(fd: BorrowedFd<'_>) -> io::Result<Termios> {
115115
pub(crate) fn tcgetpgrp(fd: BorrowedFd<'_>) -> io::Result<Pid> {
116116
unsafe {
117117
let pid = ret_pid_t(c::tcgetpgrp(borrowed_fd(fd)))?;
118+
119+
// This doesn't appear to be documented, but on Linux, it appears
120+
// `tcsetpgrp` can succceed and set the pid to 0 if we pass it a
121+
// pseudo-terminal device fd. For now, translate it into `OPNOTSUPP`.
122+
#[cfg(linux_kernel)]
123+
if pid == 0 {
124+
return Err(io::Errno::OPNOTSUPP);
125+
}
126+
118127
Ok(Pid::from_raw_unchecked(pid))
119128
}
120129
}

src/backend/linux_raw/termios/syscalls.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ pub(crate) fn tcgetpgrp(fd: BorrowedFd<'_>) -> io::Result<Pid> {
8686
let mut result = MaybeUninit::<c::pid_t>::uninit();
8787
ret(syscall!(__NR_ioctl, fd, c_uint(TIOCGPGRP), &mut result))?;
8888
let pid = result.assume_init();
89+
90+
// This doesn't appear to be documented, but it appears `tcsetpgrp` can
91+
// succceed and set the pid to 0 if we pass it a pseudo-terminal device
92+
// fd. For now, fail with `OPNOTSUPP`.
93+
if pid == 0 {
94+
return Err(io::Errno::OPNOTSUPP);
95+
}
96+
8997
Ok(Pid::from_raw_unchecked(pid))
9098
}
9199
}
@@ -177,7 +185,15 @@ pub(crate) fn tcsetwinsize(fd: BorrowedFd<'_>, winsize: Winsize) -> io::Result<(
177185

178186
#[inline]
179187
pub(crate) fn tcsetpgrp(fd: BorrowedFd<'_>, pid: Pid) -> io::Result<()> {
180-
unsafe { ret(syscall!(__NR_ioctl, fd, c_uint(TIOCSPGRP), pid)) }
188+
let raw_pid: c::c_int = pid.as_raw_nonzero().get();
189+
unsafe {
190+
ret(syscall_readonly!(
191+
__NR_ioctl,
192+
fd,
193+
c_uint(TIOCSPGRP),
194+
by_ref(&raw_pid)
195+
))
196+
}
181197
}
182198

183199
/// A wrapper around a conceptual `cfsetspeed` which handles an arbitrary

src/termios/tc.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::fd::AsFd;
2-
use crate::pid::Pid;
32
#[cfg(not(target_os = "espidf"))]
43
use crate::termios::{Action, OptionalActions, QueueSelector, Termios, Winsize};
54
use crate::{backend, io};
65

6+
pub use crate::pid::Pid;
7+
78
/// `tcgetattr(fd)`—Get terminal attributes.
89
///
910
/// Also known as the `TCGETS` (or `TCGETS2` on Linux) operation with `ioctl`.
@@ -44,6 +45,11 @@ pub fn tcgetwinsize<Fd: AsFd>(fd: Fd) -> io::Result<Winsize> {
4445
///
4546
/// Also known as the `TIOCGPGRP` operation with `ioctl`.
4647
///
48+
/// On Linux, if `fd` is a pseudo-terminal, the underlying system call here can
49+
/// return a pid of 0, which rustix's `Pid` type doesn't support. So rustix
50+
/// instead handles this case by failing with [`io::Errno::OPNOTSUPP`] if the
51+
/// pid is 0.
52+
///
4753
/// # References
4854
/// - [POSIX]
4955
/// - [Linux]

tests/termios/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
#[cfg(not(windows))]
66
mod isatty;
7+
#[cfg(not(any(windows, target_os = "wasi")))]
8+
mod pgrp;
9+
#[cfg(not(any(windows, target_os = "wasi")))]
10+
mod sid;
711
#[cfg(all(not(windows), feature = "pty"))]
812
mod termios;
913
#[cfg(not(any(windows, target_os = "fuchsia")))]

tests/termios/pgrp.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use rustix::io::Errno;
2+
use rustix::termios::{tcgetpgrp, tcsetpgrp, Pid};
3+
use tempfile::tempdir;
4+
5+
#[cfg(feature = "fs")]
6+
#[test]
7+
fn pgrp_notty() {
8+
let tmpdir = tempdir().unwrap();
9+
let fd = rustix::fs::open(
10+
tmpdir.path(),
11+
rustix::fs::OFlags::RDONLY,
12+
rustix::fs::Mode::empty(),
13+
)
14+
.unwrap();
15+
16+
// A file is not a tty.
17+
assert_eq!(tcgetpgrp(&fd), Err(Errno::NOTTY));
18+
assert_eq!(tcsetpgrp(&fd, Pid::INIT), Err(Errno::NOTTY));
19+
}
20+
21+
// Disable on illumos where `tcgetattr` doesn't appear to support
22+
// pseudoterminals.
23+
#[cfg(not(target_os = "illumos"))]
24+
#[cfg(feature = "pty")]
25+
#[test]
26+
fn pgrp_pseudoterminal() {
27+
use rustix::pty::*;
28+
use rustix::termios::*;
29+
30+
let pty = match openpt(OpenptFlags::NOCTTY) {
31+
Ok(pty) => pty,
32+
Err(rustix::io::Errno::NOSYS) => return,
33+
Err(e) => Err(e).unwrap(),
34+
};
35+
36+
// Linux's `tcgetpgrp` returns 0 here, which is not documented, so rustix
37+
// translates it into `OPNOTSUPP`.
38+
#[cfg(linux_kernel)]
39+
assert_eq!(tcgetpgrp(&pty), Err(rustix::io::Errno::OPNOTSUPP));
40+
41+
// FreeBSD's `tcgetpgrp` returns 100000 here, or presumably some other
42+
// number if that number is already taken, which is documented behavior,
43+
// but impossible to test for reliably.
44+
#[cfg(not(linux_kernel))]
45+
assert!(matches!(tcgetpgrp(&pty), Ok(_)));
46+
47+
// We shouldn't be able to set the process group to pid 1.
48+
match tcsetpgrp(&pty, rustix::termios::Pid::INIT).unwrap_err() {
49+
#[cfg(freebsdlike)]
50+
rustix::io::Errno::PERM => {}
51+
#[cfg(any(apple, linux_kernel))]
52+
rustix::io::Errno::NOTTY => {}
53+
err => Err(err).unwrap(),
54+
}
55+
}

tests/termios/sid.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
use rustix::io::Errno;
2+
use rustix::termios::tcgetsid;
3+
use tempfile::tempdir;
4+
5+
#[cfg(feature = "fs")]
6+
#[test]
7+
fn sid_notty() {
8+
let tmpdir = tempdir().unwrap();
9+
let fd = rustix::fs::open(
10+
tmpdir.path(),
11+
rustix::fs::OFlags::RDONLY,
12+
rustix::fs::Mode::empty(),
13+
)
14+
.unwrap();
15+
16+
// A file is not a tty.
17+
assert_eq!(tcgetsid(&fd), Err(Errno::NOTTY));
18+
}
19+
20+
#[cfg(all(feature = "stdio", feature = "process"))]
21+
#[test]
22+
fn sid_match() {
23+
match tcgetsid(rustix::stdio::stdin()) {
24+
Ok(sid) => assert_eq!(sid, rustix::process::getsid(None).unwrap()),
25+
Err(_err) => {}
26+
}
27+
}

tests/termios/termios.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,86 @@
1+
#[test]
2+
fn test_termios_flush() {
3+
use rustix::pty::*;
4+
use rustix::termios::*;
5+
6+
let pty = match openpt(OpenptFlags::empty()) {
7+
Ok(pty) => pty,
8+
Err(rustix::io::Errno::NOSYS) => return,
9+
Err(e) => Err(e).unwrap(),
10+
};
11+
let tio = match tcgetattr(&pty) {
12+
Ok(tio) => tio,
13+
Err(rustix::io::Errno::NOSYS) => return,
14+
#[cfg(apple)]
15+
Err(rustix::io::Errno::NOTTY) => return,
16+
Err(e) => Err(e).unwrap(),
17+
};
18+
tcsetattr(&pty, OptionalActions::Now, &tio).unwrap();
19+
20+
tcflush(&pty, QueueSelector::IOFlush).unwrap();
21+
}
22+
23+
#[test]
24+
fn test_termios_drain() {
25+
use rustix::pty::*;
26+
use rustix::termios::*;
27+
28+
let pty = match openpt(OpenptFlags::empty()) {
29+
Ok(pty) => pty,
30+
Err(rustix::io::Errno::NOSYS) => return,
31+
Err(e) => Err(e).unwrap(),
32+
};
33+
let tio = match tcgetattr(&pty) {
34+
Ok(tio) => tio,
35+
Err(rustix::io::Errno::NOSYS) => return,
36+
#[cfg(apple)]
37+
Err(rustix::io::Errno::NOTTY) => return,
38+
Err(e) => Err(e).unwrap(),
39+
};
40+
tcsetattr(&pty, OptionalActions::Now, &tio).unwrap();
41+
42+
tcdrain(&pty).unwrap();
43+
}
44+
45+
#[test]
46+
fn test_termios_winsize() {
47+
use rustix::pty::*;
48+
use rustix::termios::*;
49+
50+
let pty = match openpt(OpenptFlags::empty()) {
51+
Ok(pty) => pty,
52+
Err(rustix::io::Errno::NOSYS) => return,
53+
Err(e) => Err(e).unwrap(),
54+
};
55+
56+
// Sizes for a pseudoterminal start out 0.
57+
let mut sizes = match tcgetwinsize(&pty) {
58+
Ok(sizes) => sizes,
59+
// Apple doesn't appear to support `tcgetwinsize` on a pty.
60+
#[cfg(apple)]
61+
Err(rustix::io::Errno::NOTTY) => return,
62+
Err(err) => Err(err).unwrap(),
63+
};
64+
assert_eq!(sizes.ws_row, 0);
65+
assert_eq!(sizes.ws_col, 0);
66+
assert_eq!(sizes.ws_xpixel, 0);
67+
assert_eq!(sizes.ws_ypixel, 0);
68+
69+
// Set some arbitrary sizes.
70+
sizes.ws_row = 28;
71+
sizes.ws_col = 82;
72+
sizes.ws_xpixel = 365;
73+
sizes.ws_ypixel = 794;
74+
tcsetwinsize(&pty, sizes).unwrap();
75+
76+
// Check that the sizes roundtripped.
77+
let check_sizes = tcgetwinsize(&pty).unwrap();
78+
assert_eq!(check_sizes.ws_row, sizes.ws_row);
79+
assert_eq!(check_sizes.ws_col, sizes.ws_col);
80+
assert_eq!(check_sizes.ws_xpixel, sizes.ws_xpixel);
81+
assert_eq!(check_sizes.ws_ypixel, sizes.ws_ypixel);
82+
}
83+
184
// Disable on illumos where `tcgetattr` doesn't appear to support
285
// pseudoterminals.
386
#[cfg(not(target_os = "illumos"))]
@@ -86,3 +169,12 @@ fn test_termios_speeds() {
86169
assert_eq!(new_tio.output_speed(), speed::B110);
87170
}
88171
}
172+
173+
#[test]
174+
fn test_termios_tcgetattr_not_tty() {
175+
let file = tempfile::tempfile().unwrap();
176+
assert_eq!(
177+
rustix::termios::tcgetattr(&file).unwrap_err(),
178+
rustix::io::Errno::NOTTY
179+
);
180+
}

0 commit comments

Comments
 (0)