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