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