// Copyright 2017 The ChromiumOS Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. //! Runs a virtual machine //! //! ## Feature flags #![doc = document_features::document_features!()] #[cfg(any(feature = "composite-disk", feature = "qcow"))] use std::fs::OpenOptions; use std::path::Path; use anyhow::anyhow; use anyhow::Context; use anyhow::Result; use argh::FromArgs; use base::error; use base::info; use base::syslog; use base::syslog::LogConfig; use cmdline::RunCommand; use cmdline::UsbAttachCommand; mod crosvm; use crosvm::cmdline; #[cfg(feature = "plugin")] use crosvm::config::executable_is_plugin; use crosvm::config::Config; use devices::virtio::vhost::user::device::run_block_device; #[cfg(unix)] use devices::virtio::vhost::user::device::run_net_device; #[cfg(feature = "composite-disk")] use disk::create_composite_disk; #[cfg(feature = "composite-disk")] use disk::create_disk_file; #[cfg(feature = "composite-disk")] use disk::create_zero_filler; #[cfg(feature = "composite-disk")] use disk::ImagePartitionType; #[cfg(feature = "composite-disk")] use disk::PartitionInfo; #[cfg(feature = "qcow")] use disk::QcowFile; mod sys; use crosvm::cmdline::Command; use crosvm::cmdline::CrossPlatformCommands; use crosvm::cmdline::CrossPlatformDevicesCommands; #[cfg(windows)] use sys::windows::metrics; #[cfg(feature = "gpu")] use vm_control::client::do_gpu_display_add; #[cfg(feature = "gpu")] use vm_control::client::do_gpu_display_list; #[cfg(feature = "gpu")] use vm_control::client::do_gpu_display_remove; use vm_control::client::do_modify_battery; use vm_control::client::do_usb_attach; use vm_control::client::do_usb_detach; use vm_control::client::do_usb_list; use vm_control::client::handle_request; use vm_control::client::vms_request; #[cfg(feature = "gpu")] use vm_control::client::ModifyGpuResult; use vm_control::client::ModifyUsbResult; #[cfg(feature = "balloon")] use vm_control::BalloonControlCommand; use vm_control::DiskControlCommand; use vm_control::HotPlugDeviceInfo; use vm_control::HotPlugDeviceType; use vm_control::UsbControlResult; use vm_control::VmRequest; #[cfg(feature = "balloon")] use vm_control::VmResponse; use crate::sys::error_to_exit_code; use crate::sys::init_log; #[cfg(feature = "scudo")] #[global_allocator] static ALLOCATOR: scudo::GlobalScudoAllocator = scudo::GlobalScudoAllocator; #[repr(i32)] #[derive(Clone, Copy)] /// Exit code from crosvm, enum CommandStatus { /// Exit with success. Also used to mean VM stopped successfully. SuccessOrVmStop = 0, /// VM requested reset. VmReset = 32, /// VM crashed. VmCrash = 33, /// VM exit due to kernel panic in guest. GuestPanic = 34, /// Invalid argument was given to crosvm. InvalidArgs = 35, /// VM exit due to vcpu stall detection. WatchdogReset = 36, } impl CommandStatus { fn message(&self) -> &'static str { match self { Self::SuccessOrVmStop => "exiting with success", Self::VmReset => "exiting with reset", Self::VmCrash => "exiting with crash", Self::GuestPanic => "exiting with guest panic", Self::InvalidArgs => "invalid argument", Self::WatchdogReset => "exiting with watchdog reset", } } } fn to_command_status(result: Result) -> Result { match result { Ok(sys::ExitState::Stop) => { info!("crosvm has exited normally"); Ok(CommandStatus::SuccessOrVmStop) } Ok(sys::ExitState::Reset) => { info!("crosvm has exited normally due to reset request"); Ok(CommandStatus::VmReset) } Ok(sys::ExitState::Crash) => { info!("crosvm has exited due to a VM crash"); Ok(CommandStatus::VmCrash) } Ok(sys::ExitState::GuestPanic) => { info!("crosvm has exited due to a kernel panic in guest"); Ok(CommandStatus::GuestPanic) } Ok(sys::ExitState::WatchdogReset) => { info!("crosvm has exited due to watchdog reboot"); Ok(CommandStatus::WatchdogReset) } Err(e) => { error!("crosvm has exited with error: {:#}", e); Err(e) } } } fn run_vm(cmd: RunCommand, log_config: LogConfig) -> Result where F: Fn(&mut syslog::fmt::Formatter, &log::Record<'_>) -> std::io::Result<()> + Sync + Send, { let cfg = match TryInto::::try_into(cmd) { Ok(cfg) => cfg, Err(e) => { eprintln!("{}", e); return Err(anyhow!("{}", e)); } }; #[cfg(feature = "plugin")] if executable_is_plugin(&cfg.executable_path) { let res = match crosvm::plugin::run_config(cfg) { Ok(_) => { info!("crosvm and plugin have exited normally"); Ok(CommandStatus::SuccessOrVmStop) } Err(e) => { eprintln!("{:#}", e); Err(e) } }; return res; } #[cfg(feature = "crash-report")] crosvm::sys::setup_emulator_crash_reporting(&cfg)?; #[cfg(windows)] metrics::setup_metrics_reporting()?; init_log(log_config, &cfg)?; let exit_state = crate::sys::run_config(cfg); to_command_status(exit_state) } fn stop_vms(cmd: cmdline::StopCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Exit, cmd.socket_path) } fn suspend_vms(cmd: cmdline::SuspendCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Suspend, cmd.socket_path) } fn resume_vms(cmd: cmdline::ResumeCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Resume, cmd.socket_path) } fn powerbtn_vms(cmd: cmdline::PowerbtnCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Powerbtn, cmd.socket_path) } fn sleepbtn_vms(cmd: cmdline::SleepCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Sleepbtn, cmd.socket_path) } fn inject_gpe(cmd: cmdline::GpeCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::Gpe(cmd.gpe), cmd.socket_path) } #[cfg(feature = "balloon")] fn balloon_vms(cmd: cmdline::BalloonCommand) -> std::result::Result<(), ()> { let command = BalloonControlCommand::Adjust { num_bytes: cmd.num_bytes, }; vms_request(&VmRequest::BalloonCommand(command), cmd.socket_path) } #[cfg(feature = "balloon")] fn balloon_stats(cmd: cmdline::BalloonStatsCommand) -> std::result::Result<(), ()> { let command = BalloonControlCommand::Stats {}; let request = &VmRequest::BalloonCommand(command); let response = handle_request(request, cmd.socket_path)?; match serde_json::to_string_pretty(&response) { Ok(response_json) => println!("{}", response_json), Err(e) => { error!("Failed to serialize into JSON: {}", e); return Err(()); } } match response { VmResponse::BalloonStats { .. } => Ok(()), _ => Err(()), } } fn modify_battery(cmd: cmdline::BatteryCommand) -> std::result::Result<(), ()> { do_modify_battery( cmd.socket_path, &cmd.battery_type, &cmd.property, &cmd.target, ) } fn modify_vfio(cmd: cmdline::VfioCrosvmCommand) -> std::result::Result<(), ()> { let (request, socket_path, vfio_path) = match cmd.command { cmdline::VfioSubCommand::Add(c) => { let request = VmRequest::HotPlugCommand { device: HotPlugDeviceInfo { device_type: HotPlugDeviceType::EndPoint, path: c.vfio_path.clone(), hp_interrupt: true, }, add: true, }; (request, c.socket_path, c.vfio_path) } cmdline::VfioSubCommand::Remove(c) => { let request = VmRequest::HotPlugCommand { device: HotPlugDeviceInfo { device_type: HotPlugDeviceType::EndPoint, path: c.vfio_path.clone(), hp_interrupt: false, }, add: false, }; (request, c.socket_path, c.vfio_path) } }; if !vfio_path.exists() || !vfio_path.is_dir() { error!("Invalid host sysfs path: {:?}", vfio_path); return Err(()); } handle_request(&request, socket_path)?; Ok(()) } #[cfg(feature = "composite-disk")] fn create_composite(cmd: cmdline::CreateCompositeCommand) -> std::result::Result<(), ()> { use std::fs::File; use std::path::PathBuf; let composite_image_path = &cmd.path; let zero_filler_path = format!("{}.filler", composite_image_path); let header_path = format!("{}.header", composite_image_path); let footer_path = format!("{}.footer", composite_image_path); let mut composite_image_file = OpenOptions::new() .create(true) .read(true) .write(true) .truncate(true) .open(&composite_image_path) .map_err(|e| { error!( "Failed opening composite disk image file at '{}': {}", composite_image_path, e ); })?; create_zero_filler(&zero_filler_path).map_err(|e| { error!( "Failed to create zero filler file at '{}': {}", &zero_filler_path, e ); })?; let mut header_file = OpenOptions::new() .create(true) .read(true) .write(true) .truncate(true) .open(&header_path) .map_err(|e| { error!( "Failed opening header image file at '{}': {}", header_path, e ); })?; let mut footer_file = OpenOptions::new() .create(true) .read(true) .write(true) .truncate(true) .open(&footer_path) .map_err(|e| { error!( "Failed opening footer image file at '{}': {}", footer_path, e ); })?; let partitions = cmd .partitions .into_iter() .map(|partition_arg| { if let [label, path] = partition_arg.split(":").collect::>()[..] { let partition_file = File::open(path) .map_err(|e| error!("Failed to open partition image: {}", e))?; // Sparseness for composite disks is not user provided on Linux // (e.g. via an option), and it has no runtime effect. let size = create_disk_file( partition_file, /* is_sparse_file= */ true, disk::MAX_NESTING_DEPTH, Path::new(path), ) .map_err(|e| error!("Failed to create DiskFile instance: {}", e))? .get_len() .map_err(|e| error!("Failed to get length of partition image: {}", e))?; Ok(PartitionInfo { label: label.to_owned(), path: Path::new(path).to_owned(), partition_type: ImagePartitionType::LinuxFilesystem, writable: false, size, }) } else { error!( "Must specify label and path for partition '{}', like LABEL:PATH", partition_arg ); Err(()) } }) .collect::, _>>()?; create_composite_disk( &partitions, &PathBuf::from(zero_filler_path), &PathBuf::from(header_path), &mut header_file, &PathBuf::from(footer_path), &mut footer_file, &mut composite_image_file, ) .map_err(|e| { error!( "Failed to create composite disk image at '{}': {}", composite_image_path, e ); })?; Ok(()) } #[cfg(feature = "qcow")] fn create_qcow2(cmd: cmdline::CreateQcow2Command) -> std::result::Result<(), ()> { if !(cmd.size.is_some() ^ cmd.backing_file.is_some()) { println!( "Create a new QCOW2 image at `PATH` of either the specified `SIZE` in bytes or with a '--backing_file'." ); return Err(()); } let file = OpenOptions::new() .create(true) .read(true) .write(true) .truncate(true) .open(&cmd.file_path) .map_err(|e| { error!("Failed opening qcow file at '{}': {}", cmd.file_path, e); })?; match (cmd.size, cmd.backing_file) { (Some(size), None) => QcowFile::new(file, size).map_err(|e| { error!("Failed to create qcow file at '{}': {}", cmd.file_path, e); })?, (None, Some(backing_file)) => { QcowFile::new_from_backing(file, &backing_file, disk::MAX_NESTING_DEPTH).map_err( |e| { error!("Failed to create qcow file at '{}': {}", cmd.file_path, e); }, )? } _ => unreachable!(), }; Ok(()) } fn start_device(opts: cmdline::DeviceCommand) -> std::result::Result<(), ()> { let result = match opts.command { cmdline::DeviceSubcommand::CrossPlatform(command) => match command { CrossPlatformDevicesCommands::Block(cfg) => run_block_device(cfg), #[cfg(unix)] CrossPlatformDevicesCommands::Net(cfg) => run_net_device(cfg), }, cmdline::DeviceSubcommand::Sys(command) => sys::start_device(command), }; result.map_err(|e| { error!("Failed to run device: {:#}", e); }) } fn disk_cmd(cmd: cmdline::DiskCommand) -> std::result::Result<(), ()> { match cmd.command { cmdline::DiskSubcommand::Resize(cmd) => { let request = VmRequest::DiskCommand { disk_index: cmd.disk_index, command: DiskControlCommand::Resize { new_size: cmd.disk_size, }, }; vms_request(&request, cmd.socket_path) } } } fn make_rt(cmd: cmdline::MakeRTCommand) -> std::result::Result<(), ()> { vms_request(&VmRequest::MakeRT, cmd.socket_path) } #[cfg(feature = "gpu")] fn gpu_display_add(cmd: cmdline::GpuAddDisplaysCommand) -> ModifyGpuResult { do_gpu_display_add(cmd.socket_path, cmd.gpu_display) } #[cfg(feature = "gpu")] fn gpu_display_list(cmd: cmdline::GpuListDisplaysCommand) -> ModifyGpuResult { do_gpu_display_list(cmd.socket_path) } #[cfg(feature = "gpu")] fn gpu_display_remove(cmd: cmdline::GpuRemoveDisplaysCommand) -> ModifyGpuResult { do_gpu_display_remove(cmd.socket_path, cmd.display_id) } #[cfg(feature = "gpu")] fn modify_gpu(cmd: cmdline::GpuCommand) -> std::result::Result<(), ()> { let result = match cmd.command { cmdline::GpuSubCommand::AddDisplays(cmd) => gpu_display_add(cmd), cmdline::GpuSubCommand::ListDisplays(cmd) => gpu_display_list(cmd), cmdline::GpuSubCommand::RemoveDisplays(cmd) => gpu_display_remove(cmd), }; match result { Ok(response) => { println!("{}", response); Ok(()) } Err(e) => { println!("error {}", e); Err(()) } } } fn usb_attach(cmd: UsbAttachCommand) -> ModifyUsbResult { let dev_path = Path::new(&cmd.dev_path); do_usb_attach(cmd.socket_path, dev_path) } fn usb_detach(cmd: cmdline::UsbDetachCommand) -> ModifyUsbResult { do_usb_detach(cmd.socket_path, cmd.port) } fn usb_list(cmd: cmdline::UsbListCommand) -> ModifyUsbResult { do_usb_list(cmd.socket_path) } fn modify_usb(cmd: cmdline::UsbCommand) -> std::result::Result<(), ()> { let result = match cmd.command { cmdline::UsbSubCommand::Attach(cmd) => usb_attach(cmd), cmdline::UsbSubCommand::Detach(cmd) => usb_detach(cmd), cmdline::UsbSubCommand::List(cmd) => usb_list(cmd), }; match result { Ok(response) => { println!("{}", response); Ok(()) } Err(e) => { println!("error {}", e); Err(()) } } } #[allow(clippy::unnecessary_wraps)] fn pkg_version() -> std::result::Result<(), ()> { const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); const PKG_VERSION: Option<&'static str> = option_env!("PKG_VERSION"); print!("crosvm {}", VERSION.unwrap_or("UNKNOWN")); match PKG_VERSION { Some(v) => println!("-{}", v), None => println!(), } Ok(()) } // Returns true if the argument is a flag (e.g. `-s` or `--long`). // // As a special case, `-` is not treated as a flag, since it is typically used to represent // `stdin`/`stdout`. fn is_flag(arg: &str) -> bool { arg.len() > 1 && arg.starts_with('-') } // Perform transformations on `args_iter` to produce arguments suitable for parsing by `argh`. fn prepare_argh_args>(args_iter: I) -> Vec { let mut args: Vec = Vec::default(); // http://b/235882579 for arg in args_iter { match arg.as_str() { "--host_ip" => { eprintln!("`--host_ip` option is deprecated!"); eprintln!("Please use `--host-ip` instead"); args.push("--host-ip".to_string()); } "--balloon_bias_mib" => { eprintln!("`--balloon_bias_mib` option is deprecated!"); eprintln!("Please use `--balloon-bias-mib` instead"); args.push("--balloon-bias-mib".to_string()); } "-h" => args.push("--help".to_string()), arg if is_flag(arg) => { // Split `--arg=val` into `--arg val`, since argh doesn't support the former. if let Some((key, value)) = arg.split_once("=") { args.push(key.to_string()); args.push(value.to_string()); } else { args.push(arg.to_string()); } } arg => args.push(arg.to_string()), } } args } fn crosvm_main() -> Result { let _library_watcher = sys::get_library_watcher(); // The following panic hook will stop our crashpad hook on windows. // Only initialize when the crash-pad feature is off. #[cfg(not(feature = "crash-report"))] sys::set_panic_hook(); // Ensure all processes detach from metrics on exit. #[cfg(windows)] let _metrics_destructor = metrics::get_destructor(); let args = prepare_argh_args(std::env::args()); let args = args.iter().map(|s| s.as_str()).collect::>(); let args = match crosvm::cmdline::CrosvmCmdlineArgs::from_args(&args[..1], &args[1..]) { Ok(args) => args, Err(e) => { eprintln!("arg parsing failed: {}", e.output); return Ok(CommandStatus::InvalidArgs); } }; let extended_status = args.extended_status; info!("CLI arguments parsed."); let mut log_config = LogConfig { filter: &args.log_level, proc_name: args.syslog_tag.unwrap_or("crosvm".to_string()), syslog: !args.no_syslog, ..Default::default() }; if let Some(async_executor) = args.async_executor { cros_async::Executor::set_default_executor_kind(async_executor) .context("Failed to set the default async executor")?; } let ret = match args.command { Command::CrossPlatform(command) => { // Past this point, usage of exit is in danger of leaking zombie processes. if let CrossPlatformCommands::Run(cmd) = command { if let Some(syslog_tag) = &cmd.syslog_tag { log_config.proc_name = syslog_tag.clone(); } // We handle run_vm separately because it does not simply signal success/error // but also indicates whether the guest requested reset or stop. run_vm(cmd, log_config) } else if let CrossPlatformCommands::Device(cmd) = command { // On windows, the device command handles its own logging setup, so we can't handle it below // otherwise logging will double init. if cfg!(unix) { syslog::init_with(log_config).context("failed to initialize syslog")?; } start_device(cmd) .map_err(|_| anyhow!("start_device subcommand failed")) .map(|_| CommandStatus::SuccessOrVmStop) } else { syslog::init_with(log_config).context("failed to initialize syslog")?; match command { #[cfg(feature = "balloon")] CrossPlatformCommands::Balloon(cmd) => { balloon_vms(cmd).map_err(|_| anyhow!("balloon subcommand failed")) } #[cfg(feature = "balloon")] CrossPlatformCommands::BalloonStats(cmd) => { balloon_stats(cmd).map_err(|_| anyhow!("balloon_stats subcommand failed")) } CrossPlatformCommands::Battery(cmd) => { modify_battery(cmd).map_err(|_| anyhow!("battery subcommand failed")) } #[cfg(feature = "composite-disk")] CrossPlatformCommands::CreateComposite(cmd) => create_composite(cmd) .map_err(|_| anyhow!("create_composite subcommand failed")), #[cfg(feature = "qcow")] CrossPlatformCommands::CreateQcow2(cmd) => { create_qcow2(cmd).map_err(|_| anyhow!("create_qcow2 subcommand failed")) } CrossPlatformCommands::Device(_) => unreachable!(), CrossPlatformCommands::Disk(cmd) => { disk_cmd(cmd).map_err(|_| anyhow!("disk subcommand failed")) } #[cfg(feature = "gpu")] CrossPlatformCommands::Gpu(cmd) => { modify_gpu(cmd).map_err(|_| anyhow!("gpu subcommand failed")) } CrossPlatformCommands::MakeRT(cmd) => { make_rt(cmd).map_err(|_| anyhow!("make_rt subcommand failed")) } CrossPlatformCommands::Resume(cmd) => { resume_vms(cmd).map_err(|_| anyhow!("resume subcommand failed")) } CrossPlatformCommands::Run(_) => unreachable!(), CrossPlatformCommands::Stop(cmd) => { stop_vms(cmd).map_err(|_| anyhow!("stop subcommand failed")) } CrossPlatformCommands::Suspend(cmd) => { suspend_vms(cmd).map_err(|_| anyhow!("suspend subcommand failed")) } CrossPlatformCommands::Powerbtn(cmd) => { powerbtn_vms(cmd).map_err(|_| anyhow!("powerbtn subcommand failed")) } CrossPlatformCommands::Sleepbtn(cmd) => { sleepbtn_vms(cmd).map_err(|_| anyhow!("sleepbtn subcommand failed")) } CrossPlatformCommands::Gpe(cmd) => { inject_gpe(cmd).map_err(|_| anyhow!("gpe subcommand failed")) } CrossPlatformCommands::Usb(cmd) => { modify_usb(cmd).map_err(|_| anyhow!("usb subcommand failed")) } CrossPlatformCommands::Version(_) => { pkg_version().map_err(|_| anyhow!("version subcommand failed")) } CrossPlatformCommands::Vfio(cmd) => { modify_vfio(cmd).map_err(|_| anyhow!("vfio subcommand failed")) } } .map(|_| CommandStatus::SuccessOrVmStop) } } cmdline::Command::Sys(command) => { // On windows, the sys commands handle their own logging setup, so we can't handle it // below otherwise logging will double init. if cfg!(unix) { syslog::init_with(log_config).context("failed to initialize syslog")?; } sys::run_command(command).map(|_| CommandStatus::SuccessOrVmStop) } }; sys::cleanup(); // WARNING: Any code added after this point is not guaranteed to run // since we may forcibly kill this process (and its children) above. ret.map(|s| { if extended_status { s } else { CommandStatus::SuccessOrVmStop } }) } fn main() { syslog::early_init(); info!("crosvm started."); let res = crosvm_main(); let exit_code = match &res { Ok(code) => { info!("{}", code.message()); *code as i32 } Err(e) => { let exit_code = error_to_exit_code(&res); error!("exiting with error {}:{:?}", exit_code, e); exit_code } }; std::process::exit(exit_code); } #[cfg(test)] mod tests { use super::*; #[test] fn args_is_flag() { assert!(is_flag("--test")); assert!(is_flag("-s")); assert!(!is_flag("-")); assert!(!is_flag("no-leading-dash")); } // TODO(b/238361778) this doesn't work on Windows because is_flag isn't called yet. #[cfg(unix)] #[test] fn args_split_long() { assert_eq!( prepare_argh_args( ["crosvm", "run", "--something=options", "vm_kernel"].map(|x| x.to_string()) ), ["crosvm", "run", "--something", "options", "vm_kernel"] ); } // TODO(b/238361778) this doesn't work on Windows because is_flag isn't called yet. #[cfg(unix)] #[test] fn args_split_short() { assert_eq!( prepare_argh_args( ["crosvm", "run", "-p=init=/bin/bash", "vm_kernel"].map(|x| x.to_string()) ), ["crosvm", "run", "-p", "init=/bin/bash", "vm_kernel"] ); } #[test] fn args_host_ip() { assert_eq!( prepare_argh_args( ["crosvm", "run", "--host_ip", "1.2.3.4", "vm_kernel"].map(|x| x.to_string()) ), ["crosvm", "run", "--host-ip", "1.2.3.4", "vm_kernel"] ); } #[test] fn args_balloon_bias_mib() { assert_eq!( prepare_argh_args( ["crosvm", "run", "--balloon_bias_mib", "1234", "vm_kernel"].map(|x| x.to_string()) ), ["crosvm", "run", "--balloon-bias-mib", "1234", "vm_kernel"] ); } #[test] fn args_h() { assert_eq!( prepare_argh_args(["crosvm", "run", "-h"].map(|x| x.to_string())), ["crosvm", "run", "--help"] ); } #[test] fn args_battery_option() { assert_eq!( prepare_argh_args( [ "crosvm", "run", "--battery", "type=goldfish", "-p", "init=/bin/bash", "vm_kernel" ] .map(|x| x.to_string()) ), [ "crosvm", "run", "--battery", "type=goldfish", "-p", "init=/bin/bash", "vm_kernel" ] ); } }