xref: /cloud-hypervisor/vmm/src/console_devices.rs (revision 08cf983d420af7bce0cd67f34e660324ef219de6)
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 #[cfg(target_arch = "x86_64")]
27 use std::io::stdout;
28 use std::mem::zeroed;
29 use std::os::fd::AsRawFd;
30 use std::os::fd::FromRawFd;
31 use std::os::fd::IntoRawFd;
32 use std::os::fd::RawFd;
33 use std::os::unix::fs::OpenOptionsExt;
34 use std::os::unix::net::UnixListener;
35 use std::path::PathBuf;
36 use std::result;
37 use std::sync::Arc;
38 use std::sync::Mutex;
39 use thiserror::Error;
40 
41 const TIOCSPTLCK: libc::c_int = 0x4004_5431;
42 const TIOCGPTPEER: libc::c_int = 0x5441;
43 
44 /// Errors associated with console devices
45 #[derive(Debug, Error)]
46 pub enum ConsoleDeviceError {
47     /// Error creating console device
48     #[error("Error creating console device: {0}")]
49     CreateConsoleDevice(#[source] io::Error),
50 
51     /// Error setting pty raw mode
52     #[error("Error setting pty raw mode: {0}")]
53     SetPtyRaw(#[source] vmm_sys_util::errno::Error),
54 
55     /// Cannot duplicate file descriptor
56     #[error("Cannot duplicate file descriptor: {0}")]
57     DupFd(#[source] vmm_sys_util::errno::Error),
58 
59     /// Error starting sigwinch listener
60     #[error("Error starting sigwinch listener: {0}")]
61     StartSigwinchListener(#[source] std::io::Error),
62 }
63 
64 type ConsoleDeviceResult<T> = result::Result<T, ConsoleDeviceError>;
65 
66 #[derive(Default, Clone)]
67 pub struct ConsoleInfo {
68     // For each of File, Pty, Tty and Socket modes, below fields  hold the FD
69     // of console, serial and debug devices.
70     pub console_main_fd: Option<RawFd>,
71     pub serial_main_fd: Option<RawFd>,
72     #[cfg(target_arch = "x86_64")]
73     pub debug_main_fd: Option<RawFd>,
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 pub(crate) fn pre_create_console_devices(vmm: &mut Vmm) -> ConsoleDeviceResult<ConsoleInfo> {
170     let vm_config = vmm.vm_config.as_mut().unwrap().clone();
171     let mut vmconfig = vm_config.lock().unwrap();
172     let mut console_info = ConsoleInfo::default();
173 
174     match vmconfig.console.mode {
175         ConsoleOutputMode::File => {
176             let file = File::create(vmconfig.console.file.as_ref().unwrap())
177                 .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
178             console_info.console_main_fd = Some(file.into_raw_fd());
179         }
180         ConsoleOutputMode::Pty => {
181             let (main_fd, sub_fd, path) =
182                 create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
183             console_info.console_main_fd = Some(main_fd.into_raw_fd());
184             set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
185             vmconfig.console.file = Some(path.clone());
186             vmm.console_resize_pipe = Some(
187                 listen_for_sigwinch_on_tty(
188                     sub_fd,
189                     &vmm.seccomp_action,
190                     vmm.hypervisor.hypervisor_type(),
191                 )
192                 .map_err(ConsoleDeviceError::StartSigwinchListener)?,
193             );
194         }
195         ConsoleOutputMode::Tty => {
196             // Duplicating the file descriptors like this is needed as otherwise
197             // they will be closed on a reboot and the numbers reused
198 
199             // SAFETY: FFI call to dup. Trivially safe.
200             let stdout = unsafe { libc::dup(libc::STDOUT_FILENO) };
201             if stdout == -1 {
202                 return vmm_sys_util::errno::errno_result().map_err(ConsoleDeviceError::DupFd);
203             }
204             // SAFETY: stdout is valid and owned solely by us.
205             let stdout = unsafe { File::from_raw_fd(stdout) };
206 
207             // SAFETY: FFI call. Trivially safe.
208             if unsafe { libc::isatty(stdout.as_raw_fd()) } == 1 {
209                 vmm.console_resize_pipe = Some(
210                     listen_for_sigwinch_on_tty(
211                         stdout.try_clone().unwrap(),
212                         &vmm.seccomp_action,
213                         vmm.hypervisor.hypervisor_type(),
214                     )
215                     .map_err(ConsoleDeviceError::StartSigwinchListener)?,
216                 );
217             }
218 
219             // Make sure stdout is in raw mode, if it's a terminal.
220             set_raw_mode(&stdout, vmm.original_termios_opt.clone())?;
221             console_info.console_main_fd = Some(stdout.into_raw_fd());
222         }
223         ConsoleOutputMode::Null | ConsoleOutputMode::Socket | ConsoleOutputMode::Off => {}
224     }
225 
226     match vmconfig.serial.mode {
227         ConsoleOutputMode::File => {
228             let file = File::create(vmconfig.serial.file.as_ref().unwrap())
229                 .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
230             console_info.serial_main_fd = Some(file.into_raw_fd());
231         }
232         ConsoleOutputMode::Pty => {
233             let (main_fd, sub_fd, path) =
234                 create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
235             console_info.serial_main_fd = Some(main_fd.into_raw_fd());
236             set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
237             vmconfig.serial.file = Some(path.clone());
238         }
239         ConsoleOutputMode::Tty => {
240             // During vm_shutdown, when serial device is closed, FD#2(STDOUT)
241             // will be closed and FD#2 could be reused in a future boot of the
242             // guest by a different file.
243             //
244             // To ensure FD#2 always points to STANDARD OUT, a `dup` of STDOUT
245             // is passed to serial device. Doing so, even if the serial device
246             // were to be closed, FD#2 will continue to point to STANDARD OUT.
247 
248             // SAFETY: FFI call to dup. Trivially safe.
249             let stdout = unsafe { libc::dup(libc::STDOUT_FILENO) };
250             if stdout == -1 {
251                 return vmm_sys_util::errno::errno_result().map_err(ConsoleDeviceError::DupFd);
252             }
253             // SAFETY: stdout is valid and owned solely by us.
254             let stdout = unsafe { File::from_raw_fd(stdout) };
255 
256             // SAFETY: FFI call. Trivially safe.
257             if unsafe { libc::isatty(stdout.as_raw_fd()) } == 1 {
258                 vmm.console_resize_pipe = Some(
259                     listen_for_sigwinch_on_tty(
260                         stdout.try_clone().unwrap(),
261                         &vmm.seccomp_action,
262                         vmm.hypervisor.hypervisor_type(),
263                     )
264                     .map_err(ConsoleDeviceError::StartSigwinchListener)?,
265                 );
266             }
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             console_info.serial_main_fd = Some(stdout.into_raw_fd());
271         }
272         ConsoleOutputMode::Socket => {
273             let listener = UnixListener::bind(vmconfig.serial.socket.as_ref().unwrap())
274                 .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
275             console_info.serial_main_fd = Some(listener.into_raw_fd());
276         }
277         ConsoleOutputMode::Null | ConsoleOutputMode::Off => {}
278     }
279 
280     #[cfg(target_arch = "x86_64")]
281     match vmconfig.debug_console.mode {
282         ConsoleOutputMode::File => {
283             let file = File::create(vmconfig.debug_console.file.as_ref().unwrap())
284                 .map_err(ConsoleDeviceError::CreateConsoleDevice)?;
285             console_info.debug_main_fd = Some(file.into_raw_fd());
286         }
287         ConsoleOutputMode::Pty => {
288             let (main_fd, sub_fd, path) =
289                 create_pty().map_err(ConsoleDeviceError::CreateConsoleDevice)?;
290             console_info.debug_main_fd = Some(main_fd.into_raw_fd());
291             set_raw_mode(&sub_fd.as_raw_fd(), vmm.original_termios_opt.clone())?;
292             vmconfig.debug_console.file = Some(path.clone());
293         }
294         ConsoleOutputMode::Tty => {
295             let out = stdout();
296             console_info.debug_main_fd = Some(out.as_raw_fd());
297             set_raw_mode(&out, vmm.original_termios_opt.clone())?;
298         }
299         ConsoleOutputMode::Null | ConsoleOutputMode::Socket | ConsoleOutputMode::Off => {}
300     }
301 
302     Ok(console_info)
303 }
304