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")]
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")]
45 SetPtyRaw(#[source] vmm_sys_util::errno::Error),
46
47 /// Cannot duplicate file descriptor
48 #[error("Cannot duplicate file descriptor")]
49 DupFd(#[source] vmm_sys_util::errno::Error),
50
51 /// Error starting sigwinch listener
52 #[error("Error starting sigwinch listener")]
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
modify_mode<F: FnOnce(&mut termios)>( fd: RawFd, f: F, original_termios_opt: Arc<Mutex<Option<termios>>>, ) -> vmm_sys_util::errno::Result<()>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
set_raw_mode( f: &dyn AsRawFd, original_termios_opt: Arc<Mutex<Option<termios>>>, ) -> ConsoleDeviceResult<()>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
create_pty() -> io::Result<(File, File, PathBuf)>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
dup_stdout() -> vmm_sys_util::errno::Result<File>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
pre_create_console_devices(vmm: &mut Vmm) -> ConsoleDeviceResult<ConsoleInfo>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