// Copyright 2020 The Chromium OS Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use std::collections::BTreeMap; use std::fmt::{self, Display}; use std::fs::{File, OpenOptions}; use std::io::{self, stdin, stdout}; use std::os::unix::io::{AsRawFd, RawFd}; use std::path::PathBuf; use std::str::FromStr; use std::sync::Arc; use devices::{Bus, ProxyDevice, Serial, SerialDevice}; use io_jail::Minijail; use sync::Mutex; use sys_util::{read_raw_stdin, syslog, EventFd}; use crate::DeviceRegistrationError; #[derive(Debug)] pub enum Error { CloneEventFd(sys_util::Error), FileError(std::io::Error), InvalidSerialHardware(String), InvalidSerialType(String), PathRequired, Unimplemented(SerialType), } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::Error::*; match self { CloneEventFd(e) => write!(f, "unable to clone an EventFd: {}", e), FileError(e) => write!(f, "unable to open/create file: {}", e), InvalidSerialHardware(e) => write!(f, "invalid serial hardware: {}", e), InvalidSerialType(e) => write!(f, "invalid serial type: {}", e), PathRequired => write!(f, "serial device type file requires a path"), Unimplemented(e) => write!(f, "serial device type {} not implemented", e.to_string()), } } } /// Enum for possible type of serial devices #[derive(Clone, Debug)] pub enum SerialType { File, Stdout, Sink, Syslog, UnixSocket, // NOT IMPLEMENTED } impl Display for SerialType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match &self { SerialType::File => "File".to_string(), SerialType::Stdout => "Stdout".to_string(), SerialType::Sink => "Sink".to_string(), SerialType::Syslog => "Syslog".to_string(), SerialType::UnixSocket => "UnixSocket".to_string(), }; write!(f, "{}", s) } } impl FromStr for SerialType { type Err = Error; fn from_str(s: &str) -> std::result::Result { match s { "file" | "File" => Ok(SerialType::File), "stdout" | "Stdout" => Ok(SerialType::Stdout), "sink" | "Sink" => Ok(SerialType::Sink), "syslog" | "Syslog" => Ok(SerialType::Syslog), "unix" | "UnixSocket" => Ok(SerialType::UnixSocket), _ => Err(Error::InvalidSerialType(s.to_string())), } } } /// Serial device hardware types #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum SerialHardware { Serial, // Standard PC-style (8250/16550 compatible) UART VirtioConsole, // virtio-console device } impl Display for SerialHardware { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let s = match &self { SerialHardware::Serial => "serial".to_string(), SerialHardware::VirtioConsole => "virtio-console".to_string(), }; write!(f, "{}", s) } } impl FromStr for SerialHardware { type Err = Error; fn from_str(s: &str) -> std::result::Result { match s { "serial" => Ok(SerialHardware::Serial), "virtio-console" => Ok(SerialHardware::VirtioConsole), _ => Err(Error::InvalidSerialHardware(s.to_string())), } } } /// Holds the parameters for a serial device #[derive(Clone, Debug)] pub struct SerialParameters { pub type_: SerialType, pub hardware: SerialHardware, pub path: Option, pub input: Option, pub num: u8, pub console: bool, pub earlycon: bool, pub stdin: bool, } impl SerialParameters { /// Helper function to create a serial device from the defined parameters. /// /// # Arguments /// * `evt_fd` - eventfd used for interrupt events /// * `keep_fds` - Vector of FDs required by this device if it were sandboxed in a child /// process. `evt_fd` will always be added to this vector by this function. pub fn create_serial_device( &self, evt_fd: &EventFd, keep_fds: &mut Vec, ) -> std::result::Result { let evt_fd = evt_fd.try_clone().map_err(Error::CloneEventFd)?; keep_fds.push(evt_fd.as_raw_fd()); let input: Option> = if let Some(input_path) = &self.input { let input_file = File::open(input_path.as_path()).map_err(Error::FileError)?; keep_fds.push(input_file.as_raw_fd()); Some(Box::new(input_file)) } else if self.stdin { keep_fds.push(stdin().as_raw_fd()); // This wrapper is used in place of the libstd native version because we don't want // buffering for stdin. struct StdinWrapper; impl io::Read for StdinWrapper { fn read(&mut self, out: &mut [u8]) -> io::Result { read_raw_stdin(out).map_err(|e| e.into()) } } Some(Box::new(StdinWrapper)) } else { None }; let output: Option> = match self.type_ { SerialType::Stdout => { keep_fds.push(stdout().as_raw_fd()); Some(Box::new(stdout())) } SerialType::Sink => None, SerialType::Syslog => { syslog::push_fds(keep_fds); Some(Box::new(syslog::Syslogger::new( syslog::Priority::Info, syslog::Facility::Daemon, ))) } SerialType::File => match &self.path { Some(path) => { let file = OpenOptions::new() .append(true) .create(true) .open(path.as_path()) .map_err(Error::FileError)?; keep_fds.push(file.as_raw_fd()); Some(Box::new(file)) } None => return Err(Error::PathRequired), }, SerialType::UnixSocket => return Err(Error::Unimplemented(SerialType::UnixSocket)), }; Ok(T::new(evt_fd, input, output, keep_fds.to_vec())) } } /// Add the default serial parameters for serial ports that have not already been specified. /// /// This ensures that `serial_parameters` will contain parameters for each of the four PC-style /// serial ports (COM1-COM4). /// /// It also sets the first `SerialHardware::Serial` to be the default console device if no other /// serial parameters exist with console=true and the first serial device has not already been /// configured explicitly. pub fn set_default_serial_parameters( serial_parameters: &mut BTreeMap<(SerialHardware, u8), SerialParameters>, ) { // If no console device exists and the first serial port has not been specified, // set the first serial port as a stdout+stdin console. let default_console = (SerialHardware::Serial, 1); if !serial_parameters.iter().any(|(_, p)| p.console) { serial_parameters .entry(default_console) .or_insert(SerialParameters { type_: SerialType::Stdout, hardware: SerialHardware::Serial, path: None, input: None, num: 1, console: true, earlycon: false, stdin: true, }); } // Ensure all four of the COM ports exist. // If one of these four SerialHardware::Serial port was not configured by the user, // set it up as a sink. for num in 1..=4 { let key = (SerialHardware::Serial, num); serial_parameters.entry(key).or_insert(SerialParameters { type_: SerialType::Sink, hardware: SerialHardware::Serial, path: None, input: None, num, console: false, earlycon: false, stdin: false, }); } } /// Address for Serial ports in x86 pub const SERIAL_ADDR: [u64; 4] = [0x3f8, 0x2f8, 0x3e8, 0x2e8]; /// Adds serial devices to the provided bus based on the serial parameters given. /// /// Only devices with hardware type `SerialHardware::Serial` are added by this function. /// /// # Arguments /// /// * `io_bus` - Bus to add the devices to /// * `com_evt_1_3` - eventfd for com1 and com3 /// * `com_evt_1_4` - eventfd for com2 and com4 /// * `io_bus` - Bus to add the devices to /// * `serial_parameters` - definitions of serial parameter configurations. /// All four of the traditional PC-style serial ports (COM1-COM4) must be specified. pub fn add_serial_devices( io_bus: &mut Bus, com_evt_1_3: &EventFd, com_evt_2_4: &EventFd, serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>, serial_jail: Option, ) -> Result<(), DeviceRegistrationError> { for x in 0..=3 { let com_evt = match x { 0 => com_evt_1_3, 1 => com_evt_2_4, 2 => com_evt_1_3, 3 => com_evt_2_4, _ => com_evt_1_3, }; let param = serial_parameters .get(&(SerialHardware::Serial, x + 1)) .ok_or(DeviceRegistrationError::MissingRequiredSerialDevice(x + 1))?; let mut preserved_fds = Vec::new(); let com = param .create_serial_device::(&com_evt, &mut preserved_fds) .map_err(DeviceRegistrationError::CreateSerialDevice)?; match serial_jail.as_ref() { Some(jail) => { let com = Arc::new(Mutex::new( ProxyDevice::new(com, &jail, preserved_fds) .map_err(DeviceRegistrationError::ProxyDeviceCreation)?, )); io_bus .insert(com.clone(), SERIAL_ADDR[x as usize], 0x8, false) .unwrap(); } None => { let com = Arc::new(Mutex::new(com)); io_bus .insert(com.clone(), SERIAL_ADDR[x as usize], 0x8, false) .unwrap(); } } } Ok(()) } #[derive(Debug)] pub enum GetSerialCmdlineError { KernelCmdline(kernel_cmdline::Error), UnsupportedEarlyconHardware(SerialHardware), } impl Display for GetSerialCmdlineError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use self::GetSerialCmdlineError::*; match self { KernelCmdline(e) => write!(f, "error appending to cmdline: {}", e), UnsupportedEarlyconHardware(hw) => { write!(f, "hardware {} not supported as earlycon", hw) } } } } pub type GetSerialCmdlineResult = std::result::Result; /// Add serial options to the provided `cmdline` based on `serial_parameters`. /// `serial_io_type` should be "io" if the platform uses x86-style I/O ports for serial devices /// or "mmio" if the serial ports are memory mapped. pub fn get_serial_cmdline( cmdline: &mut kernel_cmdline::Cmdline, serial_parameters: &BTreeMap<(SerialHardware, u8), SerialParameters>, serial_io_type: &str, ) -> GetSerialCmdlineResult<()> { match serial_parameters .iter() .filter(|(_, p)| p.console) .map(|(k, _)| k) .next() { Some((SerialHardware::Serial, num)) => { cmdline .insert("console", &format!("ttyS{}", num - 1)) .map_err(GetSerialCmdlineError::KernelCmdline)?; } Some((SerialHardware::VirtioConsole, num)) => { cmdline .insert("console", &format!("hvc{}", num - 1)) .map_err(GetSerialCmdlineError::KernelCmdline)?; } None => {} } match serial_parameters .iter() .filter(|(_, p)| p.earlycon) .map(|(k, _)| k) .next() { Some((SerialHardware::Serial, num)) => { if let Some(addr) = SERIAL_ADDR.get(*num as usize - 1) { cmdline .insert( "earlycon", &format!("uart8250,{},0x{:x}", serial_io_type, addr), ) .map_err(GetSerialCmdlineError::KernelCmdline)?; } } Some((hw, _num)) => { return Err(GetSerialCmdlineError::UnsupportedEarlyconHardware(*hw)); } None => {} } Ok(()) } #[cfg(test)] mod tests { use super::*; use kernel_cmdline::Cmdline; #[test] fn get_serial_cmdline_default() { let mut cmdline = Cmdline::new(4096); let mut serial_parameters = BTreeMap::new(); set_default_serial_parameters(&mut serial_parameters); get_serial_cmdline(&mut cmdline, &serial_parameters, "io") .expect("get_serial_cmdline failed"); let cmdline_str = cmdline.as_str(); assert!(cmdline_str.contains("console=ttyS0")); } #[test] fn get_serial_cmdline_virtio_console() { let mut cmdline = Cmdline::new(4096); let mut serial_parameters = BTreeMap::new(); // Add a virtio-console device with console=true. serial_parameters.insert( (SerialHardware::VirtioConsole, 1), SerialParameters { type_: SerialType::Stdout, hardware: SerialHardware::VirtioConsole, path: None, input: None, num: 1, console: true, earlycon: false, stdin: true, }, ); set_default_serial_parameters(&mut serial_parameters); get_serial_cmdline(&mut cmdline, &serial_parameters, "io") .expect("get_serial_cmdline failed"); let cmdline_str = cmdline.as_str(); assert!(cmdline_str.contains("console=hvc0")); } #[test] fn get_serial_cmdline_virtio_console_serial_earlycon() { let mut cmdline = Cmdline::new(4096); let mut serial_parameters = BTreeMap::new(); // Add a virtio-console device with console=true. serial_parameters.insert( (SerialHardware::VirtioConsole, 1), SerialParameters { type_: SerialType::Stdout, hardware: SerialHardware::VirtioConsole, path: None, input: None, num: 1, console: true, earlycon: false, stdin: true, }, ); // Override the default COM1 with an earlycon device. serial_parameters.insert( (SerialHardware::Serial, 1), SerialParameters { type_: SerialType::Stdout, hardware: SerialHardware::Serial, path: None, input: None, num: 1, console: false, earlycon: true, stdin: false, }, ); set_default_serial_parameters(&mut serial_parameters); get_serial_cmdline(&mut cmdline, &serial_parameters, "io") .expect("get_serial_cmdline failed"); let cmdline_str = cmdline.as_str(); assert!(cmdline_str.contains("console=hvc0")); assert!(cmdline_str.contains("earlycon=uart8250,io,0x3f8")); } #[test] fn get_serial_cmdline_virtio_console_invalid_earlycon() { let mut cmdline = Cmdline::new(4096); let mut serial_parameters = BTreeMap::new(); // Try to add a virtio-console device with earlycon=true (unsupported). serial_parameters.insert( (SerialHardware::VirtioConsole, 1), SerialParameters { type_: SerialType::Stdout, hardware: SerialHardware::VirtioConsole, path: None, input: None, num: 1, console: false, earlycon: true, stdin: true, }, ); set_default_serial_parameters(&mut serial_parameters); get_serial_cmdline(&mut cmdline, &serial_parameters, "io") .expect_err("get_serial_cmdline succeeded"); } }