xref: /cloud-hypervisor/vmm/src/console_devices.rs (revision 88a9f799449c04180c6b9a21d3b9c0c4b57e2bd6)
1 // Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 //
3 // Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
4 // Use of this source code is governed by a BSD-style license that can be
5 // found in the LICENSE-BSD-3-Clause file.
6 //
7 // Copyright © 2019 Intel Corporation
8 // Copyright © 2024 Microsoft Corporation
9 //
10 // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause
11 //
12 
13 use std::fs::read_link;
14 use std::fs::File;
15 use std::fs::OpenOptions;
16 use std::io;
17 use std::mem::zeroed;
18 use std::os::fd::AsRawFd;
19 use std::os::fd::FromRawFd;
20 use std::os::fd::RawFd;
21 use std::os::unix::fs::OpenOptionsExt;
22 use std::os::unix::net::UnixListener;
23 use std::path::PathBuf;
24 use std::result;
25 use std::sync::Arc;
26 use std::sync::Mutex;
27 
28 use libc::cfmakeraw;
29 use libc::isatty;
30 use libc::tcgetattr;
31 use libc::tcsetattr;
32 use libc::termios;
33 use libc::TCSANOW;
34 use thiserror::Error;
35 
36 use crate::sigwinch_listener::listen_for_sigwinch_on_tty;
37 use crate::vm_config::ConsoleOutputMode;
38 use crate::Vmm;
39 
40 const TIOCSPTLCK: libc::c_int = 0x4004_5431;
41 const TIOCGPTPEER: libc::c_int = 0x5441;
42 
43 /// Errors associated with console devices
44 #[derive(Debug, Error)]
45 pub enum ConsoleDeviceError {
46     /// Error creating console device
47     #[error("Error creating console device: {0}")]
48     CreateConsoleDevice(#[source] io::Error),
49 
50     /// No socket option support for console device
51     #[error("No socket option support for console device")]
52     NoSocketOptionSupportForConsoleDevice,
53 
54     /// Error setting pty raw mode
55     #[error("Error setting pty raw mode: {0}")]
56     SetPtyRaw(#[source] vmm_sys_util::errno::Error),
57 
58     /// Cannot duplicate file descriptor
59     #[error("Cannot duplicate file descriptor: {0}")]
60     DupFd(#[source] vmm_sys_util::errno::Error),
61 
62     /// Error starting sigwinch listener
63     #[error("Error starting sigwinch listener: {0}")]
64     StartSigwinchListener(#[source] std::io::Error),
65 }
66 
67 type ConsoleDeviceResult<T> = result::Result<T, ConsoleDeviceError>;
68 
69 #[derive(Clone)]
70 pub enum ConsoleOutput {
71     File(Arc<File>),
72     Pty(Arc<File>),
73     Tty(Arc<File>),
74     Null,
75     Socket(Arc<UnixListener>),
76     Off,
77 }
78 
79 #[derive(Clone)]
80 pub struct ConsoleInfo {
81     pub console_main_fd: ConsoleOutput,
82     pub serial_main_fd: ConsoleOutput,
83     #[cfg(target_arch = "x86_64")]
84     pub debug_main_fd: ConsoleOutput,
85 }
86 
87 fn modify_mode<F: FnOnce(&mut termios)>(
88     fd: RawFd,
89     f: F,
90     original_termios_opt: Arc<Mutex<Option<termios>>>,
91 ) -> vmm_sys_util::errno::Result<()> {
92     // SAFETY: safe because we check the return value of isatty.
93     if unsafe { isatty(fd) } != 1 {
94         return Ok(());
95     }
96 
97     // SAFETY: The following pair are safe because termios gets totally overwritten by tcgetattr
98     // and we check the return result.
99     let mut termios: termios = unsafe { zeroed() };
100     // SAFETY: see above
101     let ret = unsafe { tcgetattr(fd, &mut termios as *mut _) };
102     if ret < 0 {
103         return vmm_sys_util::errno::errno_result();
104     }
105     let mut original_termios_opt = original_termios_opt.lock().unwrap();
106     if original_termios_opt.is_none() {
107         original_termios_opt.replace(termios);
108     }
109 
110     f(&mut termios);
111     // SAFETY: Safe because the syscall will only read the extent of termios and we check
112     // the return result.
113     let ret = unsafe { tcsetattr(fd, TCSANOW, &termios as *const _) };
114     if ret < 0 {
115         return vmm_sys_util::errno::errno_result();
116     }
117 
118     Ok(())
119 }
120 
121 fn set_raw_mode(
122     f: &dyn AsRawFd,
123     original_termios_opt: Arc<Mutex<Option<termios>>>,
124 ) -> ConsoleDeviceResult<()> {
125     modify_mode(
126         f.as_raw_fd(),
127         // SAFETY: FFI call. Variable t is guaranteed to be a valid termios from modify_mode.
128         |t| unsafe { cfmakeraw(t) },
129         original_termios_opt,
130     )
131     .map_err(ConsoleDeviceError::SetPtyRaw)
132 }
133 
134 fn create_pty() -> io::Result<(File, File, PathBuf)> {
135     // Try to use /dev/pts/ptmx first then fall back to /dev/ptmx
136     // This is done to try and use the devpts filesystem that
137     // could be available for use in the process's namespace first.
138     // Ideally these are all the same file though but different
139     // kernels could have things setup differently.
140     // See https://www.kernel.org/doc/Documentation/filesystems/devpts.txt
141     // for further details.
142 
143     let custom_flags = libc::O_NONBLOCK;
144     let main = match OpenOptions::new()
145         .read(true)
146         .write(true)
147         .custom_flags(custom_flags)
148         .open("/dev/pts/ptmx")
149     {
150         Ok(f) => f,
151         _ => OpenOptions::new()
152             .read(true)
153             .write(true)
154             .custom_flags(custom_flags)
155             .open("/dev/ptmx")?,
156     };
157     let mut unlock: libc::c_ulong = 0;
158     // SAFETY: FFI call into libc, trivially safe
159     unsafe { libc::ioctl(main.as_raw_fd(), TIOCSPTLCK as _, &mut unlock) };
160 
161     // SAFETY: FFI call into libc, trivially safe
162     let sub_fd = unsafe {
163         libc::ioctl(
164             main.as_raw_fd(),
165             TIOCGPTPEER as _,
166             libc::O_NOCTTY | libc::O_RDWR,
167         )
168     };
169     if sub_fd == -1 {
170         return vmm_sys_util::errno::errno_result().map_err(|e| e.into());
171     }
172 
173     let proc_path = PathBuf::from(format!("/proc/self/fd/{sub_fd}"));
174     let path = read_link(proc_path)?;
175 
176     // SAFETY: sub_fd is checked to be valid before being wrapped in File
177     Ok((main, unsafe { File::from_raw_fd(sub_fd) }, path))
178 }
179 
180 fn dup_stdout() -> vmm_sys_util::errno::Result<File> {
181     // SAFETY: FFI call to dup. Trivially safe.
182     let stdout = unsafe { libc::dup(libc::STDOUT_FILENO) };
183     if stdout == -1 {
184         return vmm_sys_util::errno::errno_result();
185     }
186     // SAFETY: stdout is valid and owned solely by us.
187     Ok(unsafe { File::from_raw_fd(stdout) })
188 }
189 
190 pub(crate) fn pre_create_console_devices(vmm: &mut Vmm) -> ConsoleDeviceResult<ConsoleInfo> {
191     let vm_config = vmm.vm_config.as_mut().unwrap().clone();
192     let mut vmconfig = vm_config.lock().unwrap();
193 
194     let console_info = ConsoleInfo {
195         console_main_fd: match vmconfig.console.mode {
196             ConsoleOutputMode::File => {
197                 let file = File::create(vmconfig.console.file.as_ref().unwrap())
198                     .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
199                 ConsoleOutput::File(Arc::new(file))
200             }
201             ConsoleOutputMode::Pty => {
202                 let (main_fd, sub_fd, path) =
203                     create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
204                 set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
205                 vmconfig.console.file = Some(path.clone());
206                 vmm.console_resize_pipe = Some(Arc::new(
207                     listen_for_sigwinch_on_tty(
208                         sub_fd,
209                         &vmm.seccomp_action,
210                         vmm.hypervisor.hypervisor_type(),
211                     )
212                     .map_err(ConsoleDeviceError::StartSigwinchListener)?,
213                 ));
214                 ConsoleOutput::Pty(Arc::new(main_fd))
215             }
216             ConsoleOutputMode::Tty => {
217                 // Duplicating the file descriptors like this is needed as otherwise
218                 // they will be closed on a reboot and the numbers reused
219 
220                 let stdout = dup_stdout().map_err(ConsoleDeviceError::DupFd)?;
221 
222                 // SAFETY: FFI call. Trivially safe.
223                 if unsafe { libc::isatty(stdout.as_raw_fd()) } == 1 {
224                     vmm.console_resize_pipe = Some(Arc::new(
225                         listen_for_sigwinch_on_tty(
226                             stdout.try_clone().unwrap(),
227                             &vmm.seccomp_action,
228                             vmm.hypervisor.hypervisor_type(),
229                         )
230                         .map_err(ConsoleDeviceError::StartSigwinchListener)?,
231                     ));
232                 }
233 
234                 // Make sure stdout is in raw mode, if it's a terminal.
235                 set_raw_mode(&stdout, vmm.original_termios_opt.clone())?;
236                 ConsoleOutput::Tty(Arc::new(stdout))
237             }
238             ConsoleOutputMode::Socket => {
239                 return Err(ConsoleDeviceError::NoSocketOptionSupportForConsoleDevice)
240             }
241             ConsoleOutputMode::Null => ConsoleOutput::Null,
242             ConsoleOutputMode::Off => ConsoleOutput::Off,
243         },
244         serial_main_fd: match vmconfig.serial.mode {
245             ConsoleOutputMode::File => {
246                 let file = File::create(vmconfig.serial.file.as_ref().unwrap())
247                     .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
248                 ConsoleOutput::File(Arc::new(file))
249             }
250             ConsoleOutputMode::Pty => {
251                 let (main_fd, sub_fd, path) =
252                     create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
253                 set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
254                 vmconfig.serial.file = Some(path.clone());
255                 ConsoleOutput::Pty(Arc::new(main_fd))
256             }
257             ConsoleOutputMode::Tty => {
258                 // During vm_shutdown, when serial device is closed, FD#2(STDOUT)
259                 // will be closed and FD#2 could be reused in a future boot of the
260                 // guest by a different file.
261                 //
262                 // To ensure FD#2 always points to STANDARD OUT, a `dup` of STDOUT
263                 // is passed to serial device. Doing so, even if the serial device
264                 // were to be closed, FD#2 will continue to point to STANDARD OUT.
265 
266                 let stdout = dup_stdout().map_err(ConsoleDeviceError::DupFd)?;
267 
268                 // Make sure stdout is in raw mode, if it's a terminal.
269                 set_raw_mode(&stdout, vmm.original_termios_opt.clone())?;
270 
271                 ConsoleOutput::Tty(Arc::new(stdout))
272             }
273             ConsoleOutputMode::Socket => {
274                 let listener = UnixListener::bind(vmconfig.serial.socket.as_ref().unwrap())
275                     .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
276                 ConsoleOutput::Socket(Arc::new(listener))
277             }
278             ConsoleOutputMode::Null => ConsoleOutput::Null,
279             ConsoleOutputMode::Off => ConsoleOutput::Off,
280         },
281         #[cfg(target_arch = "x86_64")]
282         debug_main_fd: match vmconfig.debug_console.mode {
283             ConsoleOutputMode::File => {
284                 let file = File::create(vmconfig.debug_console.file.as_ref().unwrap())
285                     .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
286                 ConsoleOutput::File(Arc::new(file))
287             }
288             ConsoleOutputMode::Pty => {
289                 let (main_fd, sub_fd, path) =
290                     create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
291                 set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
292                 vmconfig.debug_console.file = Some(path.clone());
293                 ConsoleOutput::Pty(Arc::new(main_fd))
294             }
295             ConsoleOutputMode::Tty => {
296                 let out =
297                     dup_stdout().map_err(|e| ConsoleDeviceError::CreateConsoleDevice(e.into()))?;
298                 set_raw_mode(&out, vmm.original_termios_opt.clone())?;
299                 ConsoleOutput::Tty(Arc::new(out))
300             }
301             ConsoleOutputMode::Socket => {
302                 return Err(ConsoleDeviceError::NoSocketOptionSupportForConsoleDevice)
303             }
304             ConsoleOutputMode::Null => ConsoleOutput::Null,
305             ConsoleOutputMode::Off => ConsoleOutput::Off,
306         },
307     };
308 
309     Ok(console_info)
310 }
311