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