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(Arc::new( 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(Arc::new( 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(Arc::new( 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