From 17ccaadc24b7eaeedac578b87ddca4491c48b25f Mon Sep 17 00:00:00 2001 From: Trent Begin Date: Wed, 17 Apr 2019 13:51:25 -0600 Subject: [PATCH] crosvm: add cmdline flags for configuring serial outputs in guest machine This change allows an output to be set for each serial device for a guest machine (stdout, syslog, or sink). BUG=chromium:953983 TEST=FEATURES=test emerge-sarien crosvm; cd sys_util; cargo test; ./build_test; manual testing on x86_64 and aarch_64 Change-Id: I9e7fcb0b296c0f8a5aa8d54b1a74ae801f6badc8 Reviewed-on: https://chromium-review.googlesource.com/1572813 Commit-Ready: Trent Begin Tested-by: kokoro Tested-by: Trent Begin Reviewed-by: Dylan Reid --- aarch64/src/fdt.rs | 4 +- aarch64/src/lib.rs | 65 ++++++++++------- arch/src/lib.rs | 60 ++++++++++++++- devices/src/lib.rs | 5 +- devices/src/serial.rs | 148 ++++++++++++++++++++++++++++++++++++- src/linux.rs | 45 +++++++----- src/main.rs | 129 ++++++++++++++++++++++++++++++++ sys_util/src/syslog.rs | 162 ++++++++++++++++++++++++++++++----------- x86_64/src/lib.rs | 100 ++++++++++++------------- 9 files changed, 572 insertions(+), 146 deletions(-) diff --git a/aarch64/src/fdt.rs b/aarch64/src/fdt.rs index 74a3fdcde3..bb82382797 100644 --- a/aarch64/src/fdt.rs +++ b/aarch64/src/fdt.rs @@ -28,8 +28,8 @@ use crate::AARCH64_RTC_SIZE; use devices::pl030::PL030_AMBA_ID; // These are serial device related constants. +use crate::AARCH64_SERIAL_1_3_IRQ; use crate::AARCH64_SERIAL_ADDR; -use crate::AARCH64_SERIAL_IRQ; use crate::AARCH64_SERIAL_SIZE; use crate::AARCH64_SERIAL_SPEED; @@ -135,7 +135,7 @@ fn create_serial_node(fdt: &mut Vec) -> Result<()> { let serial_reg_prop = generate_prop64(&[AARCH64_SERIAL_ADDR, AARCH64_SERIAL_SIZE]); let irq = generate_prop32(&[ GIC_FDT_IRQ_TYPE_SPI, - AARCH64_SERIAL_IRQ, + AARCH64_SERIAL_1_3_IRQ, IRQ_TYPE_EDGE_RISING, ]); diff --git a/aarch64/src/lib.rs b/aarch64/src/lib.rs index 9c412572d0..5e19d5dc2f 100644 --- a/aarch64/src/lib.rs +++ b/aarch64/src/lib.rs @@ -2,16 +2,20 @@ // 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::error::Error as StdError; use std::ffi::{CStr, CString}; use std::fmt::{self, Display}; use std::fs::File; -use std::io::{self, stdout}; +use std::io; use std::os::unix::io::FromRawFd; use std::sync::Arc; use arch::{RunnableLinuxVm, VmComponents}; -use devices::{Bus, BusError, PciConfigMmio, PciDevice, PciInterruptPin}; +use devices::{ + get_serial_tty_string, Bus, BusError, PciConfigMmio, PciDevice, PciInterruptPin, + SerialParameters, +}; use io_jail::Minijail; use remain::sorted; use resources::SystemAllocator; @@ -84,7 +88,8 @@ const AARCH64_SERIAL_SIZE: u64 = 0x8; const AARCH64_SERIAL_SPEED: u32 = 1843200; // The serial device gets the first interrupt line // Which gets mapped to the first SPI interrupt (physical 32). -const AARCH64_SERIAL_IRQ: u32 = 0; +const AARCH64_SERIAL_1_3_IRQ: u32 = 0; +const AARCH64_SERIAL_2_4_IRQ: u32 = 2; // Place the RTC device at page 2 const AARCH64_RTC_ADDR: u64 = 0x2000; @@ -101,8 +106,8 @@ const AARCH64_PCI_CFG_SIZE: u64 = 0x1000000; const AARCH64_MMIO_BASE: u64 = 0x1010000; // Size of the whole MMIO region. const AARCH64_MMIO_SIZE: u64 = 0x100000; -// Virtio devices start at SPI interrupt number 2 -const AARCH64_IRQ_BASE: u32 = 2; +// Virtio devices start at SPI interrupt number 3 +const AARCH64_IRQ_BASE: u32 = 3; #[sorted] #[derive(Debug)] @@ -115,6 +120,7 @@ pub enum Error { CreateGICFailure(sys_util::Error), CreateKvm(sys_util::Error), CreatePciRoot(arch::DeviceRegistrationError), + CreateSerialDevices(arch::DeviceRegistrationError), CreateSocket(io::Error), CreateVcpu(sys_util::Error), CreateVm(sys_util::Error), @@ -145,6 +151,7 @@ impl Display for Error { CreateGICFailure(e) => write!(f, "failed to create GIC: {}", e), CreateKvm(e) => write!(f, "failed to open /dev/kvm: {}", e), CreatePciRoot(e) => write!(f, "failed to create a PCI root hub: {}", e), + CreateSerialDevices(e) => write!(f, "unable to create serial devices: {}", e), CreateSocket(e) => write!(f, "failed to create socket: {}", e), CreateVcpu(e) => write!(f, "failed to create VCPU: {}", e), CreateVm(e) => write!(f, "failed to create vm: {}", e), @@ -187,6 +194,7 @@ impl arch::LinuxArch for AArch64 { fn build_vm( mut components: VmComponents, _split_irqchip: bool, + serial_parameters: &BTreeMap, create_devices: F, ) -> Result where @@ -220,7 +228,6 @@ impl arch::LinuxArch for AArch64 { let vcpu_affinity = components.vcpu_affinity; let irq_chip = Self::create_irq_chip(&vm)?; - let mut cmdline = Self::get_base_linux_cmdline(); let mut mmio_bus = devices::Bus::new(); @@ -236,7 +243,22 @@ impl arch::LinuxArch for AArch64 { // ARM doesn't really use the io bus like x86, so just create an empty bus. let io_bus = devices::Bus::new(); - let stdio_serial = Self::add_arch_devs(&mut vm, &mut mmio_bus)?; + Self::add_arch_devs(&mut vm, &mut mmio_bus)?; + + let com_evt_1_3 = EventFd::new().map_err(Error::CreateEventFd)?; + let com_evt_2_4 = EventFd::new().map_err(Error::CreateEventFd)?; + let (stdio_serial_num, stdio_serial) = arch::add_serial_devices( + &mut mmio_bus, + &com_evt_1_3, + &com_evt_2_4, + &serial_parameters, + ) + .map_err(Error::CreateSerialDevices)?; + + vm.register_irqfd(&com_evt_1_3, AARCH64_SERIAL_1_3_IRQ) + .map_err(Error::RegisterIrqfd)?; + vm.register_irqfd(&com_evt_2_4, AARCH64_SERIAL_2_4_IRQ) + .map_err(Error::RegisterIrqfd)?; mmio_bus .insert( @@ -247,6 +269,7 @@ impl arch::LinuxArch for AArch64 { ) .map_err(Error::RegisterPci)?; + let mut cmdline = Self::get_base_linux_cmdline(stdio_serial_num); for param in components.extra_kernel_params { cmdline.insert_str(¶m).map_err(Error::Cmdline)?; } @@ -343,9 +366,13 @@ impl AArch64 { } /// This returns a base part of the kernel command for this architecture - fn get_base_linux_cmdline() -> kernel_cmdline::Cmdline { + fn get_base_linux_cmdline(stdio_serial_num: Option) -> kernel_cmdline::Cmdline { let mut cmdline = kernel_cmdline::Cmdline::new(sys_util::pagesize()); - cmdline.insert_str("console=ttyS0 panic=-1").unwrap(); + if stdio_serial_num.is_some() { + let tty_string = get_serial_tty_string(stdio_serial_num.unwrap()); + cmdline.insert("console", &tty_string).unwrap(); + } + cmdline.insert_str("panic=-1").unwrap(); cmdline } @@ -365,30 +392,16 @@ impl AArch64 { /// /// * `vm` - The vm to add irqs to. /// * `bus` - The bus to add devices to. - fn add_arch_devs(vm: &mut Vm, bus: &mut Bus) -> Result>> { + fn add_arch_devs(vm: &mut Vm, bus: &mut Bus) -> Result<()> { let rtc_evt = EventFd::new().map_err(Error::CreateEventFd)?; vm.register_irqfd(&rtc_evt, AARCH64_RTC_IRQ) .map_err(Error::RegisterIrqfd)?; - let com_evt_1_3 = EventFd::new().map_err(Error::CreateEventFd)?; - vm.register_irqfd(&com_evt_1_3, AARCH64_SERIAL_IRQ) - .map_err(Error::RegisterIrqfd)?; - let serial = Arc::new(Mutex::new(devices::Serial::new_out( - com_evt_1_3.try_clone().map_err(Error::CloneEventFd)?, - Box::new(stdout()), - ))); - bus.insert( - serial.clone(), - AARCH64_SERIAL_ADDR, - AARCH64_SERIAL_SIZE, - false, - ) - .expect("failed to add serial device"); - let rtc = Arc::new(Mutex::new(devices::pl030::Pl030::new(rtc_evt))); bus.insert(rtc, AARCH64_RTC_ADDR, AARCH64_RTC_SIZE, false) .expect("failed to add rtc device"); - Ok(serial) + + Ok(()) } /// The creates the interrupt controller device and optionally returns the fd for it. diff --git a/arch/src/lib.rs b/arch/src/lib.rs index 31a0e19b9d..b9791c8694 100644 --- a/arch/src/lib.rs +++ b/arch/src/lib.rs @@ -16,7 +16,7 @@ use std::sync::Arc; use devices::virtio::VirtioDevice; use devices::{ Bus, BusDevice, BusError, PciDevice, PciDeviceError, PciInterruptPin, PciRoot, ProxyDevice, - Serial, + Serial, SerialParameters, DEFAULT_SERIAL_PARAMS, SERIAL_ADDR, }; use io_jail::Minijail; use kvm::{IoeventAddress, Kvm, Vcpu, Vm}; @@ -42,7 +42,7 @@ pub struct RunnableLinuxVm { pub vm: Vm, pub kvm: Kvm, pub resources: SystemAllocator, - pub stdio_serial: Arc>, + pub stdio_serial: Option>>, pub exit_evt: EventFd, pub vcpus: Vec, pub vcpu_affinity: Vec, @@ -69,10 +69,12 @@ pub trait LinuxArch { /// /// * `components` - Parts to use to build the VM. /// * `split_irqchip` - whether to use a split IRQ chip (i.e. userspace PIT/PIC/IOAPIC) + /// * `serial_parameters` - definitions for how the serial devices should be configured. /// * `create_devices` - Function to generate a list of devices. fn build_vm( components: VmComponents, split_irqchip: bool, + serial_parameters: &BTreeMap, create_devices: F, ) -> Result where @@ -91,6 +93,8 @@ pub enum DeviceRegistrationError { AllocateIrq, /// Could not create the mmio device to wrap a VirtioDevice. CreateMmioDevice(sys_util::Error), + // Unable to create serial device from serial parameters + CreateSerialDevice(devices::SerialError), /// Could not create an event fd. EventFdCreate(sys_util::Error), /// Could not add a device to the mmio bus. @@ -120,6 +124,7 @@ impl Display for DeviceRegistrationError { AllocateDeviceAddrs(e) => write!(f, "Allocating device addresses: {}", e), AllocateIrq => write!(f, "Allocating IRQ number"), CreateMmioDevice(e) => write!(f, "failed to create mmio device: {}", e), + CreateSerialDevice(e) => write!(f, "failed to create serial device: {}", e), Cmdline(e) => write!(f, "unable to add device to kernel command line: {}", e), EventFdCreate(e) => write!(f, "failed to create eventfd: {}", e), MmioInsert(e) => write!(f, "failed to add to mmio bus: {}", e), @@ -212,6 +217,57 @@ pub fn generate_pci_root( Ok((root, pci_irqs, pid_labels)) } +/// Adds serial devices to the provided bus based on the serial parameters given. Returns the serial +/// port number and serial device to be used for stdout if defined. +/// +/// # 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 configuationis. If a setting is not +/// provided for a port, then it will use the default configuation. +pub fn add_serial_devices( + io_bus: &mut Bus, + com_evt_1_3: &EventFd, + com_evt_2_4: &EventFd, + serial_parameters: &BTreeMap, +) -> Result<(Option, Option>>), DeviceRegistrationError> { + let mut stdio_serial_num = None; + let mut stdio_serial = None; + + 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(&(x + 1)) + .unwrap_or(&DEFAULT_SERIAL_PARAMS[x as usize]); + + let com = Arc::new(Mutex::new( + param + .create_serial_device(&com_evt) + .map_err(DeviceRegistrationError::CreateSerialDevice)?, + )); + io_bus + .insert(com.clone(), SERIAL_ADDR[x as usize], 0x8, false) + .unwrap(); + + if param.console { + stdio_serial_num = Some(x + 1); + stdio_serial = Some(com.clone()); + } + } + + Ok((stdio_serial_num, stdio_serial)) +} + /// Errors for image loading. #[derive(Debug)] pub enum LoadImageError { diff --git a/devices/src/lib.rs b/devices/src/lib.rs index 3b2c0b262a..bc9c8c1943 100644 --- a/devices/src/lib.rs +++ b/devices/src/lib.rs @@ -34,7 +34,10 @@ pub use self::pit::{Pit, PitError}; pub use self::pl030::Pl030; pub use self::proxy::Error as ProxyError; pub use self::proxy::ProxyDevice; -pub use self::serial::Serial; +pub use self::serial::Error as SerialError; +pub use self::serial::{ + get_serial_tty_string, Serial, SerialParameters, SerialType, DEFAULT_SERIAL_PARAMS, SERIAL_ADDR, +}; pub use self::usb::host_backend::host_backend_device_provider::HostBackendDeviceProvider; pub use self::usb::xhci::xhci_controller::XhciController; pub use self::virtio::VirtioPciDevice; diff --git a/devices/src/serial.rs b/devices/src/serial.rs index 7cb7611755..4d125125da 100644 --- a/devices/src/serial.rs +++ b/devices/src/serial.rs @@ -3,9 +3,12 @@ // found in the LICENSE file. use std::collections::VecDeque; -use std::io; +use std::fmt::{self, Display}; +use std::io::{self, stdout}; +use std::path::PathBuf; +use std::str::FromStr; -use sys_util::{error, EventFd, Result}; +use sys_util::{error, syslog, EventFd, Result}; use crate::BusDevice; @@ -44,6 +47,147 @@ const DEFAULT_MODEM_CONTROL: u8 = 0x8; // Auxiliary output 2 const DEFAULT_MODEM_STATUS: u8 = 0x20 | 0x10 | 0x80; // data ready, clear to send, carrier detect const DEFAULT_BAUD_DIVISOR: u16 = 12; // 9600 bps +#[derive(Debug)] +pub enum Error { + CloneEventFd(sys_util::Error), + InvalidSerialType(String), + Unimplemented(SerialType), +} + +impl Display for Error { + #[remain::check] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Error::*; + + #[sorted] + match self { + CloneEventFd(e) => write!(f, "unable to clone an EventFd: {}", e), + InvalidSerialType(e) => write!(f, "invalid serial type: {}", e), + Unimplemented(e) => write!(f, "serial device type {} not implemented", e.to_string()), + } + } +} + +/// Enum for possible type of serial devices +#[derive(Debug)] +pub enum SerialType { + File, // NOT IMPLEMENTED + 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" => return Ok(SerialType::File), + "stdout" | "Stdout" => return Ok(SerialType::Stdout), + "sink" | "Sink" => return Ok(SerialType::Sink), + "syslog" | "Syslog" => return Ok(SerialType::Syslog), + "unix" | "UnixSocket" => return Ok(SerialType::UnixSocket), + _ => Err(Error::InvalidSerialType(s.to_string())), + } + } +} + +/// Holds the parameters for a serial device +#[derive(Debug)] +pub struct SerialParameters { + pub type_: SerialType, + pub path: Option, + pub num: u8, + pub console: bool, +} + +impl SerialParameters { + /// Helper function to create a serial device from the defined parameters. + /// + /// # Arguments + /// * `evt_fd` - eventfd used for interrupt events + pub fn create_serial_device(&self, evt_fd: &EventFd) -> std::result::Result { + match self.type_ { + SerialType::Stdout => Ok(Serial::new_out( + evt_fd.try_clone().map_err(Error::CloneEventFd)?, + Box::new(stdout()), + )), + SerialType::Sink => Ok(Serial::new_sink( + evt_fd.try_clone().map_err(Error::CloneEventFd)?, + )), + SerialType::Syslog => Ok(Serial::new_out( + evt_fd.try_clone().map_err(Error::CloneEventFd)?, + Box::new(syslog::Syslogger::new( + syslog::Priority::Info, + syslog::Facility::Daemon, + )), + )), + SerialType::File => Err(Error::Unimplemented(SerialType::File)), + SerialType::UnixSocket => Err(Error::Unimplemented(SerialType::UnixSocket)), + } + } +} + +// Structure for holding the default configuration of the serial devices. +pub const DEFAULT_SERIAL_PARAMS: [SerialParameters; 4] = [ + SerialParameters { + type_: SerialType::Stdout, + path: None, + num: 1, + console: true, + }, + SerialParameters { + type_: SerialType::Sink, + path: None, + num: 2, + console: false, + }, + SerialParameters { + type_: SerialType::Sink, + path: None, + num: 3, + console: false, + }, + SerialParameters { + type_: SerialType::Sink, + path: None, + num: 4, + console: false, + }, +]; + +/// Address for Serial ports in x86 +pub const SERIAL_ADDR: [u64; 4] = [0x3f8, 0x2f8, 0x3e8, 0x2e8]; + +/// String representations of serial devices +pub const SERIAL_TTY_STRINGS: [&str; 4] = ["ttyS0", "ttyS1", "ttyS2", "ttyS3"]; + +/// Helper function to get the tty string of a serial device based on the port number. Will default +/// to ttyS0 if an invalid number is given. +pub fn get_serial_tty_string(stdio_serial_num: u8) -> String { + match stdio_serial_num { + 1 => SERIAL_TTY_STRINGS[0].to_string(), + 2 => SERIAL_TTY_STRINGS[1].to_string(), + 3 => SERIAL_TTY_STRINGS[2].to_string(), + 4 => SERIAL_TTY_STRINGS[3].to_string(), + _ => SERIAL_TTY_STRINGS[0].to_string(), + } +} + /// Emulates serial COM ports commonly seen on x86 I/O ports 0x3f8/0x2f8/0x3e8/0x2e8. /// /// This can optionally write the guest's output to a Write trait object. To send input to the diff --git a/src/linux.rs b/src/linux.rs index 803a82ea20..979666e54e 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -789,7 +789,7 @@ fn create_virtio_devices( } fn create_devices( - cfg: Config, + cfg: &Config, mem: &GuestMemory, exit_evt: &EventFd, wayland_device_socket: WlControlRequestSocket, @@ -1180,17 +1180,22 @@ pub fn run_config(cfg: Config) -> Result<()> { } let sandbox = cfg.sandbox; - let linux = Arch::build_vm(components, cfg.split_irqchip, |m, e| { - create_devices( - cfg, - m, - e, - wayland_device_socket, - balloon_device_socket, - &mut disk_device_sockets, - usb_provider, - ) - }) + let linux = Arch::build_vm( + components, + cfg.split_irqchip, + &cfg.serial_parameters, + |m, e| { + create_devices( + &cfg, + m, + e, + wayland_device_socket, + balloon_device_socket, + &mut disk_device_sockets, + usb_provider, + ) + }, + ) .map_err(Error::BuildVm)?; let _render_node_host = (); @@ -1401,13 +1406,15 @@ fn run_control( warn!("error while reading stdin: {}", e); let _ = poll_ctx.delete(&stdin_handle); } - Ok(count) => { - linux - .stdio_serial - .lock() - .queue_input_bytes(&out[..count]) - .expect("failed to queue bytes into serial port"); - } + Ok(count) => match linux.stdio_serial { + Some(ref stdio_serial) => { + stdio_serial + .lock() + .queue_input_bytes(&out[..count]) + .expect("failed to queue bytes into serial port"); + } + None => {} + }, } } Token::ChildSignal => { diff --git a/src/main.rs b/src/main.rs index 48c1edd1e9..6b948e6639 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ pub mod panic_hook; #[cfg(feature = "plugin")] pub mod plugin; +use std::collections::BTreeMap; use std::fmt; use std::fs::{File, OpenOptions}; use std::net; @@ -20,6 +21,7 @@ use std::string::String; use std::thread::sleep; use std::time::Duration; +use devices::{SerialParameters, SerialType}; use msg_socket::{MsgReceiver, MsgSender, MsgSocket}; use qcow::QcowFile; use sys_util::{ @@ -102,6 +104,8 @@ pub struct Config { software_tpm: bool, cras_audio: bool, null_audio: bool, + serial_parameters: BTreeMap, + syslog_tag: Option, virtio_single_touch: Option, virtio_trackpad: Option, virtio_mouse: Option, @@ -141,6 +145,8 @@ impl Default for Config { seccomp_policy_dir: PathBuf::from(SECCOMP_POLICY_DIR), cras_audio: false, null_audio: false, + serial_parameters: BTreeMap::new(), + syslog_tag: None, virtio_single_touch: None, virtio_trackpad: None, virtio_mouse: None, @@ -221,6 +227,58 @@ fn parse_cpu_set(s: &str) -> argument::Result> { Ok(cpuset) } +fn parse_serial_options(s: &str) -> argument::Result { + let mut serial_setting = SerialParameters { + type_: SerialType::Sink, + path: None, + num: 0, + console: false, + }; + + let opts = s + .split(",") + .map(|frag| frag.split("=")) + .map(|mut kv| (kv.next().unwrap_or(""), kv.next().unwrap_or(""))); + + for (k, v) in opts { + match k { + "type" => { + serial_setting.type_ = v + .parse::() + .map_err(|e| argument::Error::UnknownArgument(format!("{}", e)))? + } + "num" => { + let num = v.parse::().map_err(|e| { + argument::Error::Syntax(format!("serial device number is not parsable: {}", e)) + })?; + if num < 1 || num > 4 { + return Err(argument::Error::InvalidValue { + value: num.to_string(), + expected: "Serial port num must be between 1 - 4", + }); + } + serial_setting.num = num; + } + "console" => { + serial_setting.console = v.parse::().map_err(|e| { + argument::Error::Syntax(format!( + "serial device console is not parseable: {}", + e + )) + })? + } + _ => { + return Err(argument::Error::UnknownArgument(format!( + "serial parameter {}", + k + ))); + } + } + } + + Ok(serial_setting) +} + fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument::Result<()> { match name { "" => { @@ -312,6 +370,38 @@ fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument:: "null-audio" => { cfg.null_audio = true; } + "serial" => { + let serial_params = parse_serial_options(value.unwrap())?; + let num = serial_params.num; + if cfg.serial_parameters.contains_key(&num) { + return Err(argument::Error::TooManyArguments(format!( + "serial num {}", + num + ))); + } + + if serial_params.console { + for (_, params) in &cfg.serial_parameters { + if params.console { + return Err(argument::Error::TooManyArguments(format!( + "serial device {} already set as console", + params.num + ))); + } + } + } + + cfg.serial_parameters.insert(num, serial_params); + } + "syslog-tag" => { + if cfg.syslog_tag.is_some() { + return Err(argument::Error::TooManyArguments( + "`syslog-tag` already given".to_owned(), + )); + } + syslog::set_proc_name(value.unwrap()); + cfg.syslog_tag = Some(value.unwrap().to_owned()); + } "root" | "disk" | "rwdisk" | "qcow" | "rwqcow" => { let disk_path = PathBuf::from(value.unwrap()); if !disk_path.exists() { @@ -702,6 +792,15 @@ fn run_vm(args: std::env::Args) -> std::result::Result<(), ()> { Argument::value("mac", "MAC", "MAC address for VM."), Argument::flag("cras-audio", "Add an audio device to the VM that plays samples through CRAS server"), Argument::flag("null-audio", "Add an audio device to the VM that plays samples to /dev/null"), + Argument::value("serial", + "type=TYPE,[path=PATH,num=NUM,console]", + "Comma seperated key=value pairs for setting up serial devices. Can be given more than once. + Possible key values: + type=(stdout,syslog,sink) - Where to route the serial device + num=(1,2,3,4) - Serial Device Number + console - Use this serial device as the guest console. Can only be given once. Will default to first serial port if not provided. + "), + Argument::value("syslog-tag", "TAG", "When logging to syslog, use the provided tag."), Argument::value("wayland-sock", "PATH", "Path to the Wayland socket to use."), #[cfg(feature = "wl-dmabuf")] Argument::flag("wayland-dmabuf", "Enable support for DMABufs in Wayland device."), @@ -1271,4 +1370,34 @@ mod tests { fn parse_cpu_set_extra_comma() { parse_cpu_set("0,1,2,").expect_err("parse should have failed"); } + + #[test] + fn parse_serial_vaild() { + parse_serial_options("type=syslog,num=1,console=true").expect("parse should have succeded"); + } + + #[test] + fn parse_serial_invalid_type() { + parse_serial_options("type=wormhole,num=1").expect_err("parse should have failed"); + } + + #[test] + fn parse_serial_invalid_num_upper() { + parse_serial_options("type=syslog,num=5").expect_err("parse should have failed"); + } + + #[test] + fn parse_serial_invalid_num_lower() { + parse_serial_options("type=syslog,num=0").expect_err("parse should have failed"); + } + + #[test] + fn parse_serial_invalid_num_string() { + parse_serial_options("type=syslog,num=number3").expect_err("parse should have failed"); + } + + #[test] + fn parse_serial_invalid_option() { + parse_serial_options("type=syslog,speed=lightspeed").expect_err("parse should have failed"); + } } diff --git a/sys_util/src/syslog.rs b/sys_util/src/syslog.rs index 41b4b97581..da23229dd3 100644 --- a/sys_util/src/syslog.rs +++ b/sys_util/src/syslog.rs @@ -83,6 +83,7 @@ impl Display for Priority { /// The facility of a syslog message. /// /// See syslog man pages for information on their semantics. +#[derive(Copy, Clone)] pub enum Facility { Kernel = 0, User = 1 << 3, @@ -387,8 +388,8 @@ fn get_localtime() -> tm { /// # Arguments /// * `pri` - The `Priority` (i.e. severity) of the log message. /// * `fac` - The `Facility` of the log message. Usually `Facility::User` should be used. -/// * `file_name` - Name of the file that generated the log. -/// * `line` - Line number within `file_name` that generated the log. +/// * `file_line` - Optional tuple of the name of the file that generated the +/// log and the line number within that file. /// * `args` - The log's message to record, in the form of `format_args!()` return value /// /// # Examples @@ -402,12 +403,11 @@ fn get_localtime() -> tm { /// # } /// syslog::log(syslog::Priority::Error, /// syslog::Facility::User, -/// file!(), -/// line!(), +/// Some((file!(), line!())), /// format_args!("hello syslog")); /// # } /// ``` -pub fn log(pri: Priority, fac: Facility, file_name: &str, line: u32, args: fmt::Arguments) { +pub fn log(pri: Priority, fac: Facility, file_line: Option<(&str, u32)>, args: fmt::Arguments) { const MONTHS: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; @@ -417,46 +417,52 @@ pub fn log(pri: Priority, fac: Facility, file_name: &str, line: u32, args: fmt:: if let Some(socket) = &state.socket { let tm = get_localtime(); let prifac = (pri as u8) | (fac as u8); - let (res, len) = { + let res = { let mut buf_cursor = Cursor::new(&mut buf[..]); - ( - write!( - &mut buf_cursor, - "<{}>{} {:02} {:02}:{:02}:{:02} {}[{}]: [{}:{}] {}", - prifac, - MONTHS[tm.tm_mon as usize], - tm.tm_mday, - tm.tm_hour, - tm.tm_min, - tm.tm_sec, - state.proc_name.as_ref().map(|s| s.as_ref()).unwrap_or("-"), - getpid(), - file_name, - line, - args - ), - buf_cursor.position() as usize, + write!( + &mut buf_cursor, + "<{}>{} {:02} {:02}:{:02}:{:02} {}[{}]: ", + prifac, + MONTHS[tm.tm_mon as usize], + tm.tm_mday, + tm.tm_hour, + tm.tm_min, + tm.tm_sec, + state.proc_name.as_ref().map(|s| s.as_ref()).unwrap_or("-"), + getpid() ) + .and_then(|()| { + if let Some((file_name, line)) = &file_line { + write!(&mut buf_cursor, " [{}:{}] ", file_name, line) + } else { + Ok(()) + } + }) + .and_then(|()| write!(&mut buf_cursor, "{}", args)) + .and_then(|()| Ok(buf_cursor.position() as usize)) }; - if res.is_ok() { - send_buf(&socket, &buf[..len]); + if let Ok(len) = &res { + send_buf(&socket, &buf[..*len]) } } - let (res, len) = { + let res = { let mut buf_cursor = Cursor::new(&mut buf[..]); - ( - writeln!(&mut buf_cursor, "[{}:{}:{}] {}", pri, file_name, line, args), - buf_cursor.position() as usize, - ) + if let Some((file_name, line)) = &file_line { + write!(&mut buf_cursor, "[{}:{}:{}] ", pri, file_name, line) + } else { + Ok(()) + } + .and_then(|()| writeln!(&mut buf_cursor, "{}", args)) + .and_then(|()| Ok(buf_cursor.position() as usize)) }; - if res.is_ok() { + if let Ok(len) = &res { if let Some(file) = &mut state.file { - let _ = file.write_all(&buf[..len]); + let _ = file.write_all(&buf[..*len]); } if state.stderr { - let _ = stderr().write_all(&buf[..len]); + let _ = stderr().write_all(&buf[..*len]); } } } @@ -467,7 +473,7 @@ pub fn log(pri: Priority, fac: Facility, file_name: &str, line: u32, args: fmt:: #[macro_export] macro_rules! log { ($pri:expr, $($args:tt)+) => ({ - $crate::syslog::log($pri, $crate::syslog::Facility::User, file!(), line!(), format_args!($($args)+)) + $crate::syslog::log($pri, $crate::syslog::Facility::User, Some((file!(), line!())), format_args!($($args)+)) }) } @@ -503,6 +509,44 @@ macro_rules! debug { ($($args:tt)+) => ($crate::log!($crate::syslog::Priority::Debug, $($args)*)) } +// Struct that implements io::Write to be used for writing directly to the syslog +pub struct Syslogger { + buf: String, + priority: Priority, + facility: Facility, +} + +impl Syslogger { + pub fn new(p: Priority, f: Facility) -> Syslogger { + Syslogger { + buf: String::new(), + priority: p, + facility: f, + } + } +} + +impl io::Write for Syslogger { + fn write(&mut self, buf: &[u8]) -> io::Result { + let parsed_str = String::from_utf8_lossy(buf); + self.buf.push_str(&parsed_str); + + if let Some(last_newline_idx) = self.buf.rfind('\n') { + for line in self.buf[..last_newline_idx].lines() { + log(self.priority, self.facility, None, format_args!("{}", line)); + } + + self.buf.drain(..=last_newline_idx); + } + + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -535,8 +579,7 @@ mod tests { log( Priority::Error, Facility::User, - file!(), - line!(), + Some((file!(), line!())), format_args!("hello syslog"), ); } @@ -547,16 +590,14 @@ mod tests { log( Priority::Error, Facility::User, - file!(), - line!(), + Some((file!(), line!())), format_args!("before proc name"), ); set_proc_name("sys_util-test"); log( Priority::Error, Facility::User, - file!(), - line!(), + Some((file!(), line!())), format_args!("after proc name"), ); } @@ -579,8 +620,7 @@ mod tests { log( Priority::Error, Facility::User, - file!(), - line!(), + Some((file!(), line!())), format_args!("{}", TEST_STR), ); @@ -600,4 +640,42 @@ mod tests { info!("this is info {}", true); debug!("this is debug info {:?}", Some("helpful stuff")); } + + #[test] + fn syslogger_char() { + init().unwrap(); + let mut syslogger = Syslogger::new(Priority::Info, Facility::Daemon); + + let string = "Writing chars to syslog"; + for c in string.chars() { + syslogger.write(&[c as u8]).expect("error writing char"); + } + + syslogger + .write(&['\n' as u8]) + .expect("error writing newline char"); + } + + #[test] + fn syslogger_line() { + init().unwrap(); + let mut syslogger = Syslogger::new(Priority::Info, Facility::Daemon); + + let s = "Writing string to syslog\n"; + syslogger + .write(&s.as_bytes()) + .expect("error writing string"); + } + + #[test] + fn syslogger_partial() { + init().unwrap(); + let mut syslogger = Syslogger::new(Priority::Info, Facility::Daemon); + + let s = "Writing partial string"; + // Should not log because there is no newline character + syslogger + .write(&s.as_bytes()) + .expect("error writing string"); + } } diff --git a/x86_64/src/lib.rs b/x86_64/src/lib.rs index 23df5ea578..eafdc61dc9 100644 --- a/x86_64/src/lib.rs +++ b/x86_64/src/lib.rs @@ -54,18 +54,19 @@ mod mptable; mod regs; mod smbios; +use std::collections::BTreeMap; use std::error::Error as StdError; use std::ffi::{CStr, CString}; use std::fmt::{self, Display}; use std::fs::File; -use std::io::{self, stdout}; +use std::io; use std::mem; use std::sync::Arc; use crate::bootparam::boot_params; use crate::bootparam::E820_RAM; use arch::{RunnableLinuxVm, VmComponents}; -use devices::{PciConfigIo, PciDevice, PciInterruptPin}; +use devices::{get_serial_tty_string, PciConfigIo, PciDevice, PciInterruptPin, SerialParameters}; use io_jail::Minijail; use kvm::*; use remain::sorted; @@ -87,6 +88,7 @@ pub enum Error { CreatePciRoot(arch::DeviceRegistrationError), CreatePit(sys_util::Error), CreatePitDevice(devices::PitError), + CreateSerialDevices(arch::DeviceRegistrationError), CreateSocket(io::Error), CreateVcpu(sys_util::Error), CreateVm(sys_util::Error), @@ -130,6 +132,7 @@ impl Display for Error { CreatePciRoot(e) => write!(f, "failed to create a PCI root hub: {}", e), CreatePit(e) => write!(f, "unable to create PIT: {}", e), CreatePitDevice(e) => write!(f, "unable to make PIT device: {}", e), + CreateSerialDevices(e) => write!(f, "unable to create serial devices: {}", e), CreateSocket(e) => write!(f, "failed to create socket: {}", e), CreateVcpu(e) => write!(f, "failed to create VCPU: {}", e), CreateVm(e) => write!(f, "failed to create VM: {}", e), @@ -172,6 +175,8 @@ const ZERO_PAGE_OFFSET: u64 = 0x7000; const KERNEL_START_OFFSET: u64 = 0x200000; const CMDLINE_OFFSET: u64 = 0x20000; const CMDLINE_MAX_SIZE: u64 = KERNEL_START_OFFSET - CMDLINE_OFFSET; +const X86_64_SERIAL_1_3_IRQ: u32 = 4; +const X86_64_SERIAL_2_4_IRQ: u32 = 3; const X86_64_IRQ_BASE: u32 = 5; fn configure_system( @@ -296,6 +301,7 @@ impl arch::LinuxArch for X8664arch { fn build_vm( mut components: VmComponents, split_irqchip: bool, + serial_parameters: &BTreeMap, create_devices: F, ) -> Result where @@ -329,7 +335,6 @@ impl arch::LinuxArch for X8664arch { let vcpu_affinity = components.vcpu_affinity; let irq_chip = Self::create_irq_chip(&vm)?; - let mut cmdline = Self::get_base_linux_cmdline(); let mut mmio_bus = devices::Bus::new(); @@ -342,13 +347,17 @@ impl arch::LinuxArch for X8664arch { .map_err(Error::CreatePciRoot)?; let pci_bus = Arc::new(Mutex::new(PciConfigIo::new(pci))); - let (io_bus, stdio_serial) = Self::setup_io_bus( + let mut io_bus = Self::setup_io_bus( &mut vm, split_irqchip, exit_evt.try_clone().map_err(Error::CloneEventFd)?, Some(pci_bus.clone()), )?; + let (stdio_serial_num, stdio_serial) = + Self::setup_serial_devices(&mut vm, &mut io_bus, &serial_parameters)?; + + let mut cmdline = Self::get_base_linux_cmdline(stdio_serial_num); for param in components.extra_kernel_params { cmdline.insert_str(¶m).map_err(Error::Cmdline)?; } @@ -533,11 +542,14 @@ impl X8664arch { } /// This returns a minimal kernel command for this architecture - fn get_base_linux_cmdline() -> kernel_cmdline::Cmdline { + fn get_base_linux_cmdline(stdio_serial_num: Option) -> kernel_cmdline::Cmdline { let mut cmdline = kernel_cmdline::Cmdline::new(CMDLINE_MAX_SIZE as usize); - cmdline - .insert_str("console=ttyS0 noacpi reboot=k panic=-1") - .unwrap(); + if stdio_serial_num.is_some() { + let tty_string = get_serial_tty_string(stdio_serial_num.unwrap()); + cmdline.insert("console", &tty_string).unwrap(); + } + cmdline.insert_str("noacpi reboot=k panic=-1").unwrap(); + cmdline } @@ -565,7 +577,7 @@ impl X8664arch { split_irqchip: bool, exit_evt: EventFd, pci: Option>>, - ) -> Result<(devices::Bus, Arc>)> { + ) -> Result<(devices::Bus)> { struct NoDevice; impl devices::BusDevice for NoDevice { fn debug_label(&self) -> String { @@ -575,46 +587,6 @@ impl X8664arch { let mut io_bus = devices::Bus::new(); - let com_evt_1_3 = EventFd::new().map_err(Error::CreateEventFd)?; - let com_evt_2_4 = EventFd::new().map_err(Error::CreateEventFd)?; - let stdio_serial = Arc::new(Mutex::new(devices::Serial::new_out( - com_evt_1_3.try_clone().map_err(Error::CloneEventFd)?, - Box::new(stdout()), - ))); - let nul_device = Arc::new(Mutex::new(NoDevice)); - io_bus - .insert(stdio_serial.clone(), 0x3f8, 0x8, false) - .unwrap(); - io_bus - .insert( - Arc::new(Mutex::new(devices::Serial::new_sink( - com_evt_2_4.try_clone().map_err(Error::CloneEventFd)?, - ))), - 0x2f8, - 0x8, - false, - ) - .unwrap(); - io_bus - .insert( - Arc::new(Mutex::new(devices::Serial::new_sink( - com_evt_1_3.try_clone().map_err(Error::CloneEventFd)?, - ))), - 0x3e8, - 0x8, - false, - ) - .unwrap(); - io_bus - .insert( - Arc::new(Mutex::new(devices::Serial::new_sink( - com_evt_2_4.try_clone().map_err(Error::CloneEventFd)?, - ))), - 0x2e8, - 0x8, - false, - ) - .unwrap(); io_bus .insert(Arc::new(Mutex::new(devices::Cmos::new())), 0x70, 0x2, false) .unwrap(); @@ -629,6 +601,7 @@ impl X8664arch { ) .unwrap(); + let nul_device = Arc::new(Mutex::new(NoDevice)); if split_irqchip { let pit_evt = EventFd::new().map_err(Error::CreateEventFd)?; let pit = Arc::new(Mutex::new( @@ -664,12 +637,35 @@ impl X8664arch { .unwrap(); } - vm.register_irqfd(&com_evt_1_3, 4) + Ok(io_bus) + } + + /// Sets up the serial devices for this platform. Returns the serial port number and serial + /// device to be used for stdout + /// + /// # Arguments + /// + /// * - `vm` the vm object + /// * - `io_bus` the I/O bus to add the devices to + /// * - `serial_parmaters` - definitions for how the serial devices should be configured + fn setup_serial_devices( + vm: &mut Vm, + io_bus: &mut devices::Bus, + serial_parameters: &BTreeMap, + ) -> Result<(Option, Option>>)> { + let com_evt_1_3 = EventFd::new().map_err(Error::CreateEventFd)?; + let com_evt_2_4 = EventFd::new().map_err(Error::CreateEventFd)?; + + let (stdio_serial_num, stdio_serial) = + arch::add_serial_devices(io_bus, &com_evt_1_3, &com_evt_2_4, &serial_parameters) + .map_err(Error::CreateSerialDevices)?; + + vm.register_irqfd(&com_evt_1_3, X86_64_SERIAL_1_3_IRQ) .map_err(Error::RegisterIrqfd)?; - vm.register_irqfd(&com_evt_2_4, 3) + vm.register_irqfd(&com_evt_2_4, X86_64_SERIAL_2_4_IRQ) .map_err(Error::RegisterIrqfd)?; - Ok((io_bus, stdio_serial)) + Ok((stdio_serial_num, stdio_serial)) } /// Configures the vcpu and should be called once per vcpu from the vcpu's thread.