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