rustix_openpty/
lib.rs

1//! Pseudoterminal operations.
2//!
3//! # References
4//!
5//!  - [Linux]
6//!  - [FreeBSD]
7//!
8//! [Linux]: https://man7.org/linux/man-pages/man7/pty.7.html
9//! [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=pty&sektion=4
10
11#![no_std]
12
13extern crate alloc;
14
15#[cfg(any(target_os = "android", target_os = "linux"))]
16use alloc::vec::Vec;
17use rustix::fd::{AsFd, OwnedFd};
18#[cfg(any(target_os = "android", target_os = "linux"))] // for `RawDir`
19use rustix::fd::{AsRawFd, RawFd};
20use rustix::io;
21use rustix::termios::{Termios, Winsize};
22
23// Re-export our public dependency on rustix.
24pub use rustix;
25
26/// A pair of file descriptors representing a pseudoterminal.
27pub struct Pty {
28    /// The controller of the pseudoterminal.
29    pub controller: OwnedFd,
30
31    /// The user side of the pseudoterminal that applications can connect
32    /// to and be controlled by.
33    pub user: OwnedFd,
34}
35
36/// Open a pseudoterminal.
37///
38/// The `termios` and `winsize` arguments specify `Termios` and `Winsize`
39/// settings to configure the user file descriptor with.
40///
41/// The returned file descriptors have the `CLOEXEC` flag set, though not all
42/// platforms supporting setting it atomically.
43///
44/// On many platforms, this includes a call to [`libc::grantpt`], which has
45/// unspecified behavior if the calling process has a `SIGCHLD` signal handler
46/// installed.
47///
48/// # References
49///  - [Linux]
50///  - [Apple]
51///  - [FreeBSD]
52///  - [glibc]
53///
54/// [Linux]: https://man7.org/linux/man-pages/man3/openpty.3.html
55/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/openpty.3.html
56/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=openpty&sektion=3
57/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Pseudo_002dTerminal-Pairs.html#index-openpty
58/// [`libc::grantpt`]: https://docs.rs/libc/latest/libc/fn.grantpt.html
59pub fn openpty(termios: Option<&Termios>, winsize: Option<&Winsize>) -> io::Result<Pty> {
60    // On non-Linux platforms, use `libc::openpty`. This doesn't have any way
61    // to set `CLOEXEC` so we do it non-atomically.
62    #[cfg(not(any(target_os = "android", target_os = "linux")))]
63    {
64        use core::mem::{align_of, size_of, MaybeUninit};
65        use core::ptr::{null, null_mut};
66        use rustix::fd::FromRawFd;
67
68        assert_eq!(size_of::<Termios>(), size_of::<libc::termios>());
69        assert_eq!(align_of::<Termios>(), align_of::<libc::termios>());
70
71        let termios: *const libc::termios = match termios {
72            Some(termios) => {
73                let termios: *const Termios = termios;
74                termios.cast()
75            }
76            None => null(),
77        };
78        let mut libc_winsize: libc::winsize;
79        let winsize: *mut libc::winsize = match winsize {
80            Some(winsize) => {
81                libc_winsize = libc::winsize {
82                    ws_row: winsize.ws_row,
83                    ws_col: winsize.ws_col,
84                    ws_xpixel: winsize.ws_xpixel,
85                    ws_ypixel: winsize.ws_ypixel,
86                };
87                &mut libc_winsize
88            }
89            None => null_mut(),
90        };
91
92        let mut controller = MaybeUninit::<libc::c_int>::uninit();
93        let mut user = MaybeUninit::<libc::c_int>::uninit();
94        unsafe {
95            if libc::openpty(
96                controller.as_mut_ptr(),
97                user.as_mut_ptr(),
98                null_mut(),
99                termios as _,
100                winsize,
101            ) == 0
102            {
103                let controller = OwnedFd::from_raw_fd(controller.assume_init());
104                let user = OwnedFd::from_raw_fd(user.assume_init());
105
106                set_cloexec(&controller)?;
107                set_cloexec(&user)?;
108
109                Ok(Pty { controller, user })
110            } else {
111                Err(io::Errno::from_raw_os_error(errno::errno().0))
112            }
113        }
114    }
115
116    // On Linux platforms, use `rustix::pty`. Linux has an `openpty` function,
117    // but we use `rustix::pty` instead so that we can set the `CLOEXEC` flag
118    // atomically.
119    #[cfg(any(target_os = "android", target_os = "linux"))]
120    {
121        use rustix::pty::{grantpt, openpt, unlockpt, OpenptFlags};
122        use rustix::termios::{tcsetattr, tcsetwinsize, OptionalActions};
123
124        let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC;
125        let controller = openpt(flags)?;
126
127        grantpt(&controller)?;
128        unlockpt(&controller)?;
129
130        let user = open_user(&controller, flags | OpenptFlags::CLOEXEC)?;
131
132        if let Some(termios) = termios {
133            tcsetattr(&user, OptionalActions::Now, termios)?;
134        }
135        if let Some(winsize) = winsize {
136            tcsetwinsize(&user, *winsize)?;
137        }
138
139        Ok(Pty { controller, user })
140    }
141}
142
143/// Open a pseudoterminal, without setting `OFlags::CLOEXEC`.
144///
145/// This is identical to [`openpty`], except that it does not set
146/// `OFlags::CLOEXEC` on the controller file descriptor. This more closely
147/// matches how `libc::openpty` works.
148pub fn openpty_nocloexec(termios: Option<&Termios>, winsize: Option<&Winsize>) -> io::Result<Pty> {
149    // On non-Linux platforms, use `libc::openpty`.
150    #[cfg(not(any(target_os = "android", target_os = "linux")))]
151    {
152        use core::mem::{align_of, size_of, MaybeUninit};
153        use core::ptr::{null, null_mut};
154        use rustix::fd::FromRawFd;
155
156        assert_eq!(size_of::<Termios>(), size_of::<libc::termios>());
157        assert_eq!(align_of::<Termios>(), align_of::<libc::termios>());
158
159        let termios: *const libc::termios = match termios {
160            Some(termios) => {
161                let termios: *const Termios = termios;
162                termios.cast()
163            }
164            None => null(),
165        };
166        let mut libc_winsize: libc::winsize;
167        let winsize: *mut libc::winsize = match winsize {
168            Some(winsize) => {
169                libc_winsize = libc::winsize {
170                    ws_row: winsize.ws_row,
171                    ws_col: winsize.ws_col,
172                    ws_xpixel: winsize.ws_xpixel,
173                    ws_ypixel: winsize.ws_ypixel,
174                };
175                &mut libc_winsize
176            }
177            None => null_mut(),
178        };
179
180        let mut controller = MaybeUninit::<libc::c_int>::uninit();
181        let mut user = MaybeUninit::<libc::c_int>::uninit();
182        unsafe {
183            if libc::openpty(
184                controller.as_mut_ptr(),
185                user.as_mut_ptr(),
186                null_mut(),
187                termios as _,
188                winsize,
189            ) == 0
190            {
191                let controller = OwnedFd::from_raw_fd(controller.assume_init());
192                let user = OwnedFd::from_raw_fd(user.assume_init());
193
194                Ok(Pty { controller, user })
195            } else {
196                Err(io::Errno::from_raw_os_error(errno::errno().0))
197            }
198        }
199    }
200
201    // On Linux platforms, use `rustix::pty`.
202    #[cfg(any(target_os = "android", target_os = "linux"))]
203    {
204        use rustix::pty::{grantpt, openpt, unlockpt, OpenptFlags};
205        use rustix::termios::{tcsetattr, tcsetwinsize, OptionalActions};
206
207        let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY;
208        let controller = openpt(flags)?;
209
210        grantpt(&controller)?;
211        unlockpt(&controller)?;
212
213        let user = open_user(&controller, flags)?;
214
215        if let Some(termios) = termios {
216            tcsetattr(&user, OptionalActions::Now, termios)?;
217        }
218        if let Some(winsize) = winsize {
219            tcsetwinsize(&user, *winsize)?;
220        }
221
222        Ok(Pty { controller, user })
223    }
224}
225
226#[cfg(not(any(target_os = "android", target_os = "linux")))]
227fn set_cloexec<Fd: AsFd>(fd: Fd) -> io::Result<()> {
228    use rustix::io::{fcntl_getfd, fcntl_setfd, FdFlags};
229
230    let fd = fd.as_fd();
231    fcntl_setfd(fd, fcntl_getfd(fd)? | FdFlags::CLOEXEC)
232}
233
234#[cfg(any(target_os = "android", target_os = "linux"))]
235fn open_user(controller: &OwnedFd, flags: rustix::pty::OpenptFlags) -> io::Result<OwnedFd> {
236    use rustix::fs::{openat, Mode, CWD};
237
238    // On Linux 4.13, we can use `ioctl_tiocgptpeer` as an optimization. But
239    // don't try this on Android because Android's seccomp kills processes that
240    // try to optimize.
241    #[cfg(not(target_os = "android"))]
242    {
243        match rustix::pty::ioctl_tiocgptpeer(controller, flags) {
244            Ok(fd) => return Ok(fd),
245            Err(io::Errno::NOSYS) | Err(io::Errno::PERM) => {}
246            Err(e) => return Err(e),
247        }
248    }
249
250    // Get the user device file name and open it.
251    let name = rustix::pty::ptsname(controller, Vec::new())?;
252
253    openat(CWD, name, flags.into(), Mode::empty())
254}
255
256/// Prepare for a login on the given terminal.
257///
258/// # References
259///  - [Linux]
260///  - [Apple]
261///  - [FreeBSD]
262///
263/// [Linux]: https://man7.org/linux/man-pages/man3/login_tty.3.html
264/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/login_tty.3.html
265/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=login_tty&sektion=3
266#[cfg(not(any(target_os = "fuchsia", target_os = "illumos", target_os = "solaris")))]
267pub fn login_tty<Fd: Into<OwnedFd>>(fd: Fd) -> io::Result<()> {
268    _login_tty(fd.into())
269}
270
271#[cfg(not(any(target_os = "fuchsia", target_os = "illumos", target_os = "solaris")))]
272fn _login_tty(fd: OwnedFd) -> io::Result<()> {
273    #[cfg(not(any(target_os = "android", target_os = "linux")))]
274    unsafe {
275        if libc::login_tty(rustix::fd::IntoRawFd::into_raw_fd(fd)) != 0 {
276            return Err(io::Errno::from_raw_os_error(errno::errno().0));
277        }
278    }
279
280    #[cfg(any(target_os = "android", target_os = "linux"))]
281    {
282        // Create a new session.
283        rustix::process::setsid().ok();
284
285        // Set up `fd` as the controlling terminal.
286        rustix::process::ioctl_tiocsctty(&fd)?;
287
288        // Install `fd` as our stdio.
289        rustix::stdio::dup2_stdin(&fd).ok();
290        rustix::stdio::dup2_stdout(&fd).ok();
291        rustix::stdio::dup2_stderr(&fd).ok();
292
293        // If we overwrote the `fd` with our `dup2`s, don't close it now.
294        if rustix::fd::AsRawFd::as_raw_fd(&fd) <= 2 {
295            core::mem::forget(fd);
296        }
297    }
298
299    Ok(())
300}
301
302/// Close all open file descriptors that are at least as great as `from`.
303///
304/// # Safety
305///
306/// This can close files out from underneath libraries, leaving them holding
307/// dangling file descriptors. It's meant for use in spawning new processes
308/// where the existing process state is about to be overwritten anyway.
309#[cfg(any(target_os = "android", target_os = "linux"))] // for `RawDir`
310pub unsafe fn closefrom(from: RawFd) {
311    use core::mem::MaybeUninit;
312    use core::str;
313    use rustix::fs::{openat, Mode, OFlags, RawDir, CWD};
314
315    let dir = openat(
316        CWD,
317        rustix::cstr!("/dev/fd"),
318        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
319        Mode::empty(),
320    )
321    .unwrap();
322    let dir_raw_fd = dir.as_fd().as_raw_fd();
323
324    // We can use a fixed-sized buffer because `/dev/fd` names are only so big.
325    let mut buf = [MaybeUninit::uninit(); 1024];
326    let mut iter = RawDir::new(dir, &mut buf);
327    while let Some(entry) = iter.next() {
328        let entry = entry.unwrap();
329        let name_bytes = entry.file_name().to_bytes();
330        if name_bytes == b"." || name_bytes == b".." {
331            continue;
332        }
333        let name = str::from_utf8(name_bytes).unwrap();
334        let num = name.parse::<RawFd>().unwrap();
335
336        if num >= from && num != dir_raw_fd {
337            io::close(num);
338        }
339    }
340}