1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Pseudoterminal operations.
//!
//! # References
//!
//!  - [Linux]
//!  - [FreeBSD]
//!
//! [Linux]: https://man7.org/linux/man-pages/man7/pty.7.html
//! [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=pty&sektion=4

#![no_std]

extern crate alloc;

#[cfg(any(target_os = "android", target_os = "linux"))]
use alloc::vec::Vec;
use rustix::fd::{AsFd, OwnedFd};
#[cfg(any(target_os = "android", target_os = "linux"))] // for `RawDir`
use rustix::fd::{AsRawFd, RawFd};
use rustix::io;
use rustix::termios::{Termios, Winsize};

// Re-export our public dependency on rustix.
pub use rustix;

/// A pair of file descriptors representing a pseudoterminal.
pub struct Pty {
    /// The controller of the pseudoterminal.
    pub controller: OwnedFd,

    /// The user side of the pseudoterminal that applications can connect
    /// to and be controlled by.
    pub user: OwnedFd,
}

/// Open a pseudoterminal.
///
/// The `termios` and `winsize` arguments specify `Termios` and `Winsize`
/// settings to configure the user file descriptor with.
///
/// The returned file descriptors have the `CLOEXEC` flag set, though not all
/// platforms supporting setting it atomically.
///
/// On many platforms, this includes a call to [`libc::grantpt`], which has
/// unspecified behavior if the calling process has a `SIGCHLD` signal handler
/// installed.
///
/// # References
///  - [Linux]
///  - [Apple]
///  - [FreeBSD]
///  - [glibc]
///
/// [Linux]: https://man7.org/linux/man-pages/man3/openpty.3.html
/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/openpty.3.html
/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=openpty&sektion=3
/// [glibc]: https://www.gnu.org/software/libc/manual/html_node/Pseudo_002dTerminal-Pairs.html#index-openpty
/// [`libc::grantpt`]: https://docs.rs/libc/latest/libc/fn.grantpt.html
pub fn openpty(termios: Option<&Termios>, winsize: Option<&Winsize>) -> io::Result<Pty> {
    // On non-Linux platforms, use `libc::openpty`. This doesn't have any way
    // to set `CLOEXEC` so we do it non-atomically.
    #[cfg(not(any(target_os = "android", target_os = "linux")))]
    {
        use core::mem::{align_of, size_of, MaybeUninit};
        use core::ptr::{null, null_mut};
        use rustix::fd::FromRawFd;

        assert_eq!(size_of::<Termios>(), size_of::<libc::termios>());
        assert_eq!(align_of::<Termios>(), align_of::<libc::termios>());

        let termios: *const libc::termios = match termios {
            Some(termios) => {
                let termios: *const Termios = termios;
                termios.cast()
            }
            None => null(),
        };
        let winsize: *const libc::winsize = match winsize {
            Some(winsize) => winsize,
            None => null(),
        };

        let mut controller = MaybeUninit::<libc::c_int>::uninit();
        let mut user = MaybeUninit::<libc::c_int>::uninit();
        unsafe {
            if libc::openpty(
                controller.as_mut_ptr(),
                user.as_mut_ptr(),
                null_mut(),
                termios as _,
                winsize as _,
            ) == 0
            {
                let controller = OwnedFd::from_raw_fd(controller.assume_init());
                let user = OwnedFd::from_raw_fd(user.assume_init());

                set_cloexec(&controller)?;
                set_cloexec(&user)?;

                Ok(Pty { controller, user })
            } else {
                Err(io::Errno::from_raw_os_error(errno::errno().0))
            }
        }
    }

    // On Linux platforms, use `rustix::pty`. Linux has an `openpty` function,
    // but we use `rustix::pty` instead so that we can set the `CLOEXEC` flag
    // atomically.
    #[cfg(any(target_os = "android", target_os = "linux"))]
    {
        use rustix::pty::{grantpt, openpt, unlockpt, OpenptFlags};
        use rustix::termios::{tcsetattr, tcsetwinsize, OptionalActions};

        let flags = OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC;
        let controller = openpt(flags)?;

        grantpt(&controller)?;
        unlockpt(&controller)?;

        let user = open_user(&controller, flags)?;

        if let Some(termios) = termios {
            tcsetattr(&user, OptionalActions::Now, termios)?;
        }
        if let Some(winsize) = winsize {
            tcsetwinsize(&user, *winsize)?;
        }

        Ok(Pty { controller, user })
    }
}

#[cfg(not(any(target_os = "android", target_os = "linux")))]
fn set_cloexec<Fd: AsFd>(fd: Fd) -> io::Result<()> {
    use rustix::fs::{fcntl_getfd, fcntl_setfd, FdFlags};

    let fd = fd.as_fd();
    fcntl_setfd(fd, fcntl_getfd(fd)? | FdFlags::CLOEXEC)
}

#[cfg(any(target_os = "android", target_os = "linux"))]
fn open_user(controller: &OwnedFd, flags: rustix::pty::OpenptFlags) -> io::Result<OwnedFd> {
    use rustix::fs::{openat, Mode, CWD};

    // On Linux 4.13, we can use `ioctl_tiocgptpeer` as an optimization. But
    // don't try this on Android because Android's seccomp kills processes that
    // try to optimize.
    #[cfg(all(linux_like, not(target_os = "android")))]
    {
        match rustix::pty::ioctl_tiocgptpeer(controller, flags) {
            Ok(fd) => return Ok(fd),
            Err(io::Errno::NOSYS) | Err(io::Errno::PERM) => {}
            Err(e) => return Err(e),
        }
    }

    // Get the user device file name and open it.
    let name = rustix::pty::ptsname(controller, Vec::new())?;

    openat(CWD, name, flags.into(), Mode::empty())
}

/// Prepare for a login on the given terminal.
///
/// # References
///  - [Linux]
///  - [Apple]
///  - [FreeBSD]
///
/// [Linux]: https://man7.org/linux/man-pages/man3/login_tty.3.html
/// [Apple]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/login_tty.3.html
/// [FreeBSD]: https://man.freebsd.org/cgi/man.cgi?query=login_tty&sektion=3
#[cfg(not(any(target_os = "fuchsia", target_os = "illumos", target_os = "solaris")))]
pub fn login_tty<Fd: Into<OwnedFd>>(fd: Fd) -> io::Result<()> {
    _login_tty(fd.into())
}

#[cfg(not(any(target_os = "fuchsia", target_os = "illumos", target_os = "solaris")))]
fn _login_tty(fd: OwnedFd) -> io::Result<()> {
    #[cfg(not(any(target_os = "android", target_os = "linux")))]
    unsafe {
        if libc::login_tty(rustix::fd::IntoRawFd::into_raw_fd(fd)) != 0 {
            return Err(io::Errno::from_raw_os_error(errno::errno().0));
        }
    }

    #[cfg(any(target_os = "android", target_os = "linux"))]
    {
        // Create a new session.
        rustix::process::setsid().ok();

        // Set up `fd` as the controlling terminal.
        rustix::process::ioctl_tiocsctty(&fd)?;

        // Install `fd` as our stdio.
        rustix::stdio::dup2_stdin(&fd).ok();
        rustix::stdio::dup2_stdout(&fd).ok();
        rustix::stdio::dup2_stderr(&fd).ok();

        // If we overwrote the `fd` with our `dup2`s, don't close it now.
        if rustix::fd::AsRawFd::as_raw_fd(&fd) <= 2 {
            core::mem::forget(fd);
        }
    }

    Ok(())
}

/// Close all open file descriptors that are at least as great as `from`.
///
/// # Safety
///
/// This can close files out from underneath libraries, leaving them holding
/// dangling file descriptors. It's meant for use in spawning new processes
/// where the existing process state is about to be overwritten anyway.
#[cfg(any(target_os = "android", target_os = "linux"))] // for `RawDir`
pub unsafe fn closefrom(from: RawFd) {
    use core::mem::MaybeUninit;
    use core::str;
    use rustix::fs::{openat, Mode, OFlags, RawDir, CWD};

    let dir = openat(
        CWD,
        rustix::cstr!("/dev/fd"),
        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
        Mode::empty(),
    )
    .unwrap();
    let dir_raw_fd = dir.as_fd().as_raw_fd();

    // We can use a fixed-sized buffer because `/dev/fd` names are only so big.
    let mut buf = [MaybeUninit::uninit(); 1024];
    let mut iter = RawDir::new(dir, &mut buf);
    while let Some(entry) = iter.next() {
        let entry = entry.unwrap();
        let name_bytes = entry.file_name().to_bytes();
        if name_bytes == b"." || name_bytes == b".." {
            continue;
        }
        let name = str::from_utf8(name_bytes).unwrap();
        let num = name.parse::<RawFd>().unwrap();

        if num >= from && num != dir_raw_fd {
            io::close(num);
        }
    }
}