Skip to main content

conmonrs/
terminal.rs

1//! Terminal console functionalities.
2
3use crate::{
4    async_fd::{self, Fd},
5    fs_util::TempFile,
6    listener,
7    mutex::lock,
8};
9use anyhow::{Context as _, Result};
10use libc::{TIOCSWINSZ, winsize};
11use nix::{
12    cmsg_space,
13    errno::Errno,
14    fcntl::{self, FcntlArg, OFlag},
15    sys::{
16        socket::{self, ControlMessageOwned, MsgFlags},
17        termios::{self, OutputFlags, SetArg, SpecialCharacterIndices},
18    },
19};
20use std::{
21    io::{self, IoSliceMut, Read, Write},
22    os::{
23        fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
24        unix::io::RawFd,
25    },
26    path::{Path, PathBuf},
27    pin::Pin,
28    sync::{Arc, Mutex, Weak},
29    task::{Context, Poll, ready},
30};
31use tokio::{
32    fs,
33    io::{AsyncRead, AsyncWrite, AsyncWriteExt, Interest, ReadBuf, unix::AsyncFd},
34    net::{UnixListener, UnixStream},
35    select,
36    sync::mpsc::{self, Receiver, Sender},
37    task,
38};
39use tracing::{Instrument, debug, debug_span, error};
40
41/// The terminal of a process, which the runtime passes over a console
42/// socket.
43#[derive(Debug)]
44pub(crate) struct Terminal {
45    /// The socket the runtime connects to, removed with the terminal.
46    path: TempFile,
47    connected_rx: Receiver<OwnedFd>,
48    resizer: TerminalResizer,
49}
50
51impl Terminal {
52    /// The console socket the runtime connects to.
53    pub(crate) fn path(&self) -> &Path {
54        self.path.path()
55    }
56
57    /// Resizes the terminal also while something else holds it, like the
58    /// runtime call which waits for the connection.
59    pub(crate) fn resizer(&self) -> TerminalResizer {
60        self.resizer.clone()
61    }
62
63    /// The terminal while it is connected and open.
64    fn tty(&self) -> Option<Arc<TerminalFd>> {
65        match &*lock(&self.resizer.0) {
66            Tty::Pending(_) => None,
67            Tty::Connected(tty) => tty.upgrade(),
68        }
69    }
70}
71
72/// The state of a terminal, which a resize may come for before the runtime
73/// connected it.
74#[derive(Debug)]
75enum Tty {
76    /// Not connected yet, with the latest width and height requested
77    /// meanwhile, which the terminal gets once it is.
78    Pending(Option<(u16, u16)>),
79
80    /// The terminal once connected, as long as the loops which read and
81    /// write it keep it open.
82    Connected(Weak<TerminalFd>),
83}
84
85/// Resizes a [`Terminal`], see [`TerminalResizer::resize`].
86#[derive(Clone, Debug)]
87pub(crate) struct TerminalResizer(Arc<Mutex<Tty>>);
88
89impl Default for TerminalResizer {
90    fn default() -> Self {
91        Self(Arc::new(Mutex::new(Tty::Pending(None))))
92    }
93}
94
95impl TerminalResizer {
96    /// Resize the terminal width and height. Clients send their size right
97    /// away, a terminal which the runtime did not connect yet gets it once
98    /// it does, before the process usually started, see
99    /// [`Terminal::connect`].
100    pub(crate) fn resize(&self, width: u16, height: u16) -> Result<()> {
101        debug!("Resizing terminal to width {width} and height {height}");
102        let mut tty = lock(&self.0);
103        match &*tty {
104            Tty::Pending(_) => {
105                debug!("Terminal not connected yet, resizing it once it is");
106                *tty = Tty::Pending(Some((width, height)));
107                Ok(())
108            }
109            Tty::Connected(fd) => {
110                let fd = fd.upgrade().context("terminal closed")?;
111                Self::set_size(&fd, width, height)
112            }
113        }
114    }
115
116    /// Take the connected terminal and give it the size requested before.
117    /// Under the lock, so that a resize which comes meanwhile is not lost.
118    fn connect(&self, fd: &Arc<TerminalFd>) {
119        let mut tty = lock(&self.0);
120        if let Tty::Pending(Some((width, height))) = *tty {
121            debug!("Resizing terminal to width {width} and height {height} requested before");
122            // Not worth failing the process for, a later resize may fix it.
123            if let Err(e) = Self::set_size(fd, width, height) {
124                debug!("Unable to resize the terminal: {e:#}");
125            }
126        }
127        *tty = Tty::Connected(Arc::downgrade(fd));
128    }
129
130    #[expect(unsafe_code, reason = "the TIOCSWINSZ ioctl")]
131    fn set_size(fd: &TerminalFd, width: u16, height: u16) -> Result<()> {
132        let ws = winsize {
133            ws_row: height,
134            ws_col: width,
135            ws_xpixel: 0,
136            ws_ypixel: 0,
137        };
138        // SAFETY: TIOCSWINSZ reads a `winsize` from the pointer, which points
139        // to a live local of exactly that type, and the descriptor is kept
140        // open by the borrow for the duration of the call.
141        match unsafe { libc::ioctl(fd.as_raw_fd(), TIOCSWINSZ, &ws) } {
142            0 => Ok(()),
143            _ => Err(io::Error::last_os_error().into()),
144        }
145    }
146
147    /// The size requested before the terminal got connected.
148    #[cfg(test)]
149    pub(crate) fn pending(&self) -> Option<(u16, u16)> {
150        match &*lock(&self.0) {
151            Tty::Pending(size) => *size,
152            Tty::Connected(_) => None,
153        }
154    }
155}
156
157#[derive(Debug)]
158struct Config {
159    path: PathBuf,
160    connected_tx: Sender<OwnedFd>,
161}
162
163impl Terminal {
164    /// Setup a new terminal instance.
165    pub(crate) fn new() -> Result<Self> {
166        debug!("Creating new terminal");
167        // The guard removes the socket again, also if the setup below fails.
168        // In the temporary directory, not the runtime directory of the server:
169        // runc dials the console socket by its path, which a Unix socket
170        // address limits to 108 bytes, and the runtime directory of CRI-O is
171        // far too long for that.
172        let temp_file = TempFile::new(None, "conmon-term-", ".sock");
173        let path = temp_file.path();
174
175        // Bind synchronously so that the socket exists once this returns,
176        // without blocking on the listener task to get scheduled.
177        debug!("Listening terminal socket on {}", path.display());
178        let listener = listener::bind_long_path(path, |path| {
179            let fd = listener::listen_private(&path)?;
180            UnixListener::from_std(fd.into())
181        })
182        .context("bind terminal socket")?;
183
184        // It carries the single descriptor of the terminal.
185        let (connected_tx, connected_rx) = mpsc::channel(1);
186        let config = Config {
187            path: path.to_path_buf(),
188            connected_tx,
189        };
190
191        task::spawn(
192            async move {
193                if let Err(e) = Self::listen(listener, config).await {
194                    error!("Unable to listen on terminal: {e:#}");
195                }
196            }
197            .instrument(debug_span!("listen"))
198            .in_current_span(),
199        );
200
201        Ok(Self {
202            path: temp_file,
203            connected_rx,
204            resizer: TerminalResizer::default(),
205        })
206    }
207
208    /// Wait for the runtime to hand over the terminal, for the loops which
209    /// read and write it, see
210    /// [`ContainerIO::connect_terminal`](crate::container_io::ContainerIO::connect_terminal).
211    /// A wait which gets dropped leaves the descriptor queued for the next
212    /// one. The terminal gets the size requested before, see
213    /// [`TerminalResizer::resize`].
214    pub(crate) async fn connect(&mut self) -> Result<TerminalIo> {
215        debug!("Waiting for terminal socket connection");
216        let fd = self
217            .connected_rx
218            .recv()
219            .await
220            .context("receive connected channel")?;
221        let fd = Arc::new(TerminalFd::new(fd)?);
222
223        debug!("Changing terminal settings");
224        let mut term = termios::tcgetattr(&fd)?;
225        term.output_flags |= OutputFlags::ONLCR;
226        termios::tcsetattr(&fd, SetArg::TCSANOW, &term)?;
227        self.resizer.connect(&fd);
228        Ok(TerminalIo(fd))
229    }
230
231    /// The end of file character of the terminal, ^D unless the process
232    /// changed it.
233    pub(crate) fn eof_char(&self) -> u8 {
234        const DEFAULT: u8 = 0x04;
235        let Some(tty) = self.tty() else {
236            return DEFAULT;
237        };
238        match termios::tcgetattr(&*tty) {
239            Ok(term) => term.control_chars[SpecialCharacterIndices::VEOF as usize],
240            Err(e) => {
241                debug!("Unable to get the terminal settings: {e}");
242                DEFAULT
243            }
244        }
245    }
246
247    /// Resize the terminal width and height, see [`TerminalResizer::resize`].
248    pub(crate) fn resize(&self, width: u16, height: u16) -> Result<()> {
249        self.resizer.resize(width, height)
250    }
251
252    /// Accept the connection of the runtime and receive the terminal from it.
253    /// The runtime never connects or never sends the descriptor if it fails
254    /// early. The terminal gets dropped then, which closes the channel, so do
255    /// not keep the listener around forever.
256    async fn listen(listener: UnixListener, config: Config) -> Result<()> {
257        let dropped = config.connected_tx.clone();
258        select! {
259            res = Self::accept(listener, config) => res,
260            () = dropped.closed() => {
261                debug!("Terminal dropped before receiving a descriptor, stopping listener");
262                Ok(())
263            }
264        }
265    }
266
267    async fn accept(listener: UnixListener, config: Config) -> Result<()> {
268        let stream = listener.accept().await?.0;
269        debug!("Got terminal socket stream: {stream:?}");
270
271        Self::handle_fd_receive(stream, config).await
272    }
273
274    async fn handle_fd_receive(mut stream: UnixStream, config: Config) -> Result<()> {
275        let fd = stream
276            .async_io(Interest::READABLE, || Self::recv_fd(stream.as_fd()))
277            .await
278            .context("receive terminal descriptor")?;
279
280        // Hand the terminal over before cleaning up, so that a size which
281        // waits for it gets applied sooner.
282        let sent = async {
283            let fd = fd.context("got no file descriptor")?;
284            debug!("Received terminal file descriptor");
285            config
286                .connected_tx
287                .send(fd)
288                .await
289                .context("send connected channel")
290        }
291        .await;
292
293        // Allow only one single connection. Failing to clean up must not
294        // drop the terminal, which the runtime handed over already.
295        let path = &config.path;
296        debug!("Removing socket path {}", path.display());
297        if let Err(e) = fs::remove_file(path).await {
298            debug!("Unable to remove terminal socket: {e}");
299        }
300        debug!("Shutting down receiver stream");
301        if let Err(e) = stream.shutdown().await {
302            debug!("Unable to shut down terminal socket stream: {e}");
303        }
304
305        sent?;
306        debug!("Shutting down listener task");
307        Ok(())
308    }
309
310    /// Receive the descriptor of the terminal. It gets close-on-exec right
311    /// away, the runtimes which other threads spawn meanwhile would inherit
312    /// the terminal of the container otherwise.
313    #[expect(unsafe_code, reason = "taking received descriptors")]
314    fn recv_fd(socket: BorrowedFd) -> io::Result<Option<OwnedFd>> {
315        // The kernel truncates a message with more descriptors than fit,
316        // which nix reports as an error before they could be closed. One
317        // message carries at most SCM_MAX_FD of them. A server at its limit
318        // of open files gets a truncated message as well, the descriptors
319        // received up to then leak.
320        let mut cmsg_buffer = cmsg_space!([RawFd; 253]);
321        let mut iov = [IoSliceMut::new(&mut [])];
322        let msg = socket::recvmsg::<()>(
323            socket.as_raw_fd(),
324            &mut iov,
325            Some(&mut cmsg_buffer),
326            MsgFlags::MSG_CMSG_CLOEXEC,
327        )?;
328        let mut fds = vec![];
329        for cmsg in msg.cmsgs()? {
330            if let ControlMessageOwned::ScmRights(raw_fds) = cmsg {
331                // SAFETY: the descriptors were just received over the socket
332                // and nothing else owns them, taking ownership right away
333                // closes the ones which are not used.
334                fds.extend(
335                    raw_fds
336                        .into_iter()
337                        .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }),
338                );
339            }
340        }
341        Ok(fds.into_iter().next())
342    }
343}
344
345#[derive(Debug)]
346struct TerminalFd(AsyncFd<Fd<std::fs::File>>);
347
348impl TerminalFd {
349    fn new(fd: OwnedFd) -> io::Result<Self> {
350        let flags = fcntl::fcntl(&fd, FcntlArg::F_GETFL)?;
351        let flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK;
352        fcntl::fcntl(&fd, FcntlArg::F_SETFL(flags))?;
353        let file = std::fs::File::from(fd);
354        Ok(Self(async_fd::register(
355            file,
356            Interest::READABLE | Interest::WRITABLE,
357        )?))
358    }
359}
360
361impl AsRawFd for TerminalFd {
362    fn as_raw_fd(&self) -> RawFd {
363        self.0.as_raw_fd()
364    }
365}
366
367impl AsFd for TerminalFd {
368    fn as_fd(&self) -> BorrowedFd<'_> {
369        self.0.as_fd()
370    }
371}
372
373/// The terminal for the loops which read and write it, which keep it open.
374#[derive(Clone, Debug)]
375pub(crate) struct TerminalIo(Arc<TerminalFd>);
376
377impl TerminalIo {
378    /// Open the other end of the terminal, in the devpts instance of the
379    /// container, which keeps the terminal from hanging up while it is open.
380    #[expect(unsafe_code, reason = "the TIOCGPTPEER ioctl")]
381    pub(crate) fn open_peer(&self) -> nix::Result<OwnedFd> {
382        // SAFETY: TIOCGPTPEER takes the flags of the new descriptor as an
383        // integer and returns it or a negative error, it does not touch any
384        // memory of ours, and the descriptor is kept open by the borrow for
385        // the duration of the call.
386        let fd = unsafe {
387            libc::ioctl(
388                self.0.as_raw_fd(),
389                libc::TIOCGPTPEER,
390                libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC,
391            )
392        };
393        let fd = Errno::result(fd)?;
394        // SAFETY: the descriptor was just returned by the kernel and is not
395        // owned by anything else.
396        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
397    }
398}
399
400impl AsyncRead for TerminalIo {
401    fn poll_read(
402        self: Pin<&mut Self>,
403        cx: &mut Context,
404        buf: &mut ReadBuf,
405    ) -> Poll<io::Result<()>> {
406        loop {
407            let mut guard = ready!(self.0.0.poll_read_ready(cx))?;
408            match guard.try_io(|inner| (&**inner.get_ref()).read(buf.initialize_unfilled())) {
409                Ok(result) => {
410                    buf.advance(result?);
411                    break Poll::Ready(Ok(()));
412                }
413                Err(_would_block) => {}
414            }
415        }
416    }
417}
418
419impl AsyncWrite for TerminalIo {
420    fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<io::Result<usize>> {
421        loop {
422            let mut guard = ready!(self.0.0.poll_write_ready(cx))?;
423            match guard.try_io(|inner| (&**inner.get_ref()).write(buf)) {
424                Ok(result) => break Poll::Ready(result),
425                Err(_would_block) => {}
426            }
427        }
428    }
429
430    fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<io::Result<()>> {
431        Poll::Ready(Ok(()))
432    }
433
434    fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<io::Result<()>> {
435        Poll::Ready(Ok(()))
436    }
437}
438
439#[cfg(test)]
440pub(crate) mod tests;