Pass serial device to VM to report failure reason.

Add DeathReason variants for various verification failure possibilities.

Bug: 220071963
Change-Id: I2b014fcd48fa571d9e2089fde09f8199955a9e7f
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 981a041..9b2b740 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -34,6 +34,7 @@
         "liblog_rust",
         "libmicrodroid_metadata",
         "libmicrodroid_payload_config",
+        "libnix",
         "libonce_cell",
         "libregex",
         "librustutils",
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
index d736f1b..7b80fc9 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/DeathReason.aidl
@@ -34,4 +34,12 @@
     REBOOT = 5,
     /** The VM or crosvm crashed. */
     CRASH = 6,
+    /** The pVM firmware failed to verify the VM because the public key doesn't match. */
+    PVM_FIRMWARE_PUBLIC_KEY_MISMATCH = 7,
+    /** The pVM firmware failed to verify the VM because the instance image changed. */
+    PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED = 8,
+    /** The bootloader failed to verify the VM because the public key doesn't match. */
+    BOOTLOADER_PUBLIC_KEY_MISMATCH = 9,
+    /** The bootloader failed to verify the VM because the instance image changed. */
+    BOOTLOADER_INSTANCE_IMAGE_CHANGED = 10,
 }
diff --git a/virtualizationservice/src/crosvm.rs b/virtualizationservice/src/crosvm.rs
index 94cb78f..f1b179e 100644
--- a/virtualizationservice/src/crosvm.rs
+++ b/virtualizationservice/src/crosvm.rs
@@ -20,12 +20,13 @@
 use command_fds::CommandFdExt;
 use log::{debug, error, info};
 use semver::{Version, VersionReq};
+use nix::{fcntl::OFlag, unistd::pipe2};
 use shared_child::SharedChild;
 use std::fs::{remove_dir_all, File};
-use std::io;
+use std::io::{self, Read};
 use std::mem;
 use std::num::NonZeroU32;
-use std::os::unix::io::{AsRawFd, RawFd};
+use std::os::unix::io::{AsRawFd, RawFd, FromRawFd};
 use std::path::PathBuf;
 use std::process::{Command, ExitStatus};
 use std::sync::{Arc, Mutex};
@@ -114,12 +115,14 @@
     fn start(&mut self, instance: Arc<VmInstance>) -> Result<(), Error> {
         let state = mem::replace(self, VmState::Failed);
         if let VmState::NotStarted { config } = state {
+            let (failure_pipe_read, failure_pipe_write) = create_pipe()?;
+
             // If this fails and returns an error, `self` will be left in the `Failed` state.
-            let child = Arc::new(run_vm(config)?);
+            let child = Arc::new(run_vm(config, failure_pipe_write)?);
 
             let child_clone = child.clone();
             thread::spawn(move || {
-                instance.monitor(child_clone);
+                instance.monitor(child_clone, failure_pipe_read);
             });
 
             // If it started correctly, update the state.
@@ -198,7 +201,7 @@
     ///
     /// This takes a separate reference to the `SharedChild` rather than using the one in
     /// `self.vm_state` to avoid holding the lock on `vm_state` while it is running.
-    fn monitor(&self, child: Arc<SharedChild>) {
+    fn monitor(&self, child: Arc<SharedChild>, mut failure_pipe_read: File) {
         let result = child.wait();
         match &result {
             Err(e) => error!("Error waiting for crosvm({}) instance to die: {}", child.id(), e),
@@ -210,7 +213,16 @@
         // Ensure that the mutex is released before calling the callbacks.
         drop(vm_state);
 
-        self.callbacks.callback_on_died(self.cid, death_reason(&result));
+        let mut failure_string = String::new();
+        let failure_read_result = failure_pipe_read.read_to_string(&mut failure_string);
+        if let Err(e) = &failure_read_result {
+            error!("Error reading VM failure reason from pipe: {}", e);
+        }
+        if !failure_string.is_empty() {
+            info!("VM returned failure reason '{}'", failure_string);
+        }
+
+        self.callbacks.callback_on_died(self.cid, death_reason(&result, &failure_string));
 
         // Delete temporary files.
         if let Err(e) = remove_dir_all(&self.temporary_directory) {
@@ -250,8 +262,21 @@
     }
 }
 
-fn death_reason(result: &Result<ExitStatus, io::Error>) -> DeathReason {
+fn death_reason(result: &Result<ExitStatus, io::Error>, failure_reason: &str) -> DeathReason {
     if let Ok(status) = result {
+        match failure_reason {
+            "PVM_FIRMWARE_PUBLIC_KEY_MISMATCH" => {
+                return DeathReason::PVM_FIRMWARE_PUBLIC_KEY_MISMATCH
+            }
+            "PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED" => {
+                return DeathReason::PVM_FIRMWARE_INSTANCE_IMAGE_CHANGED
+            }
+            "BOOTLOADER_PUBLIC_KEY_MISMATCH" => return DeathReason::BOOTLOADER_PUBLIC_KEY_MISMATCH,
+            "BOOTLOADER_INSTANCE_IMAGE_CHANGED" => {
+                return DeathReason::BOOTLOADER_INSTANCE_IMAGE_CHANGED
+            }
+            _ => {}
+        }
         match status.code() {
             None => DeathReason::KILLED,
             Some(0) => DeathReason::SHUTDOWN,
@@ -266,7 +291,7 @@
 }
 
 /// Starts an instance of `crosvm` to manage a new VM.
-fn run_vm(config: CrosvmConfig) -> Result<SharedChild, Error> {
+fn run_vm(config: CrosvmConfig, failure_pipe_write: File) -> Result<SharedChild, Error> {
     validate_config(&config)?;
 
     let mut command = Command::new(CROSVM_PATH);
@@ -306,27 +331,25 @@
 
     // Setup the serial devices.
     // 1. uart device: used as the output device by bootloaders and as early console by linux
-    // 2. virtio-console device: used as the console device where kmsg is redirected to
-    // 3. virtio-console device: used as the androidboot.console device (not used currently)
-    // 4. virtio-console device: used as the logcat output
+    // 2. uart device: used to report the reason for the VM failing.
+    // 3. virtio-console device: used as the console device where kmsg is redirected to
+    // 4. virtio-console device: used as the androidboot.console device (not used currently)
+    // 5. virtio-console device: used as the logcat output
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
     // written there is discarded.
-    let mut format_serial_arg = |fd: &Option<File>| {
-        let path = fd.as_ref().map(|fd| add_preserved_fd(&mut preserved_fds, fd));
-        let type_arg = path.as_ref().map_or("type=sink", |_| "type=file");
-        let path_arg = path.as_ref().map_or(String::new(), |path| format!(",path={}", path));
-        format!("{}{}", type_arg, path_arg)
-    };
-    let console_arg = format_serial_arg(&config.console_fd);
-    let log_arg = format_serial_arg(&config.log_fd);
+    let console_arg = format_serial_arg(&mut preserved_fds, &config.console_fd);
+    let log_arg = format_serial_arg(&mut preserved_fds, &config.log_fd);
+    let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
 
     // Warning: Adding more serial devices requires you to shift the PCI device ID of the boot
     // disks in bootconfig.x86_64. This is because x86 crosvm puts serial devices and the block
     // devices in the same PCI bus and serial devices comes before the block devices. Arm crosvm
     // doesn't have the issue.
     // /dev/ttyS0
-    command.arg(format!("--serial={},hardware=serial", &console_arg));
+    command.arg(format!("--serial={},hardware=serial,num=1", &console_arg));
+    // /dev/ttyS1
+    command.arg(format!("--serial=type=file,path={},hardware=serial,num=2", &failure_serial_path));
     // /dev/hvc0
     command.arg(format!("--serial={},hardware=virtio-console,num=1", &console_arg));
     // /dev/hvc1 (not used currently)
@@ -393,3 +416,22 @@
     preserved_fds.push(fd);
     format!("/proc/self/fd/{}", fd)
 }
+
+/// Adds the file descriptor for `file` (if any) to `preserved_fds`, and returns the appropriate
+/// string for a crosvm `--serial` flag. If `file` is none, creates a dummy sink device.
+fn format_serial_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+    if let Some(file) = file {
+        format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
+    } else {
+        "type=sink".to_string()
+    }
+}
+
+/// Creates a new pipe with the `O_CLOEXEC` flag set, and returns the read side and write side.
+fn create_pipe() -> Result<(File, File), Error> {
+    let (raw_read, raw_write) = pipe2(OFlag::O_CLOEXEC)?;
+    // SAFETY: We are the sole owners of these fds as they were just created.
+    let read_fd = unsafe { File::from_raw_fd(raw_read) };
+    let write_fd = unsafe { File::from_raw_fd(raw_write) };
+    Ok((read_fd, write_fd))
+}