bootconfig is part of VM identity

VM's identity should be changed if debug level is changed. Debug level
is implemented using bootconfig. So changing the bootconfig should
invalidate the previous identity. This change implements the policy by
adding the bootconfig to MicrodroidData and refuses to boot if the saved
bootconfig is not the same as the current bootconfig.

Bug: 208639280
Test: atest MicrodroidTestApp on oriole_pkvm
Change-Id: I63ac0c2ecd7514d9193e655c9258c305c87e2a08
diff --git a/microdroid_manager/Android.bp b/microdroid_manager/Android.bp
index f427966..c69d875 100644
--- a/microdroid_manager/Android.bp
+++ b/microdroid_manager/Android.bp
@@ -24,6 +24,7 @@
         "libmicrodroid_metadata",
         "libmicrodroid_payload_config",
         "libnix",
+        "libonce_cell",
         "libprotobuf",
         "libring",
         "librustutils",
diff --git a/microdroid_manager/src/instance.rs b/microdroid_manager/src/instance.rs
index 47230e3..8ba6f51 100644
--- a/microdroid_manager/src/instance.rs
+++ b/microdroid_manager/src/instance.rs
@@ -315,6 +315,7 @@
 pub struct MicrodroidData {
     pub apk_data: ApkData,
     pub apex_data: Vec<ApexData>,
+    pub bootconfig: Box<[u8]>,
 }
 
 #[derive(Debug, Serialize, Deserialize, PartialEq)]
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 99ebc51..283ecb9 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -27,6 +27,7 @@
 use log::{error, info, warn};
 use microdroid_metadata::{write_metadata, Metadata};
 use microdroid_payload_config::{Task, TaskType, VmPayloadConfig};
+use once_cell::sync::OnceCell;
 use payload::{get_apex_data_from_payload, load_metadata, to_metadata};
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
@@ -215,6 +216,13 @@
 ) -> Result<MicrodroidData> {
     let start_time = SystemTime::now();
 
+    if let Some(saved_bootconfig) = saved_data.map(|d| &d.bootconfig) {
+        ensure!(
+            saved_bootconfig.as_ref() == get_bootconfig()?.as_slice(),
+            MicrodroidError::PayloadChanged(String::from("Bootconfig has changed."))
+        );
+    }
+
     let root_hash = saved_data.map(|d| &d.apk_data.root_hash);
     let root_hash_from_idsig = get_apk_root_hash_from_idsig()?;
     let root_hash_trustful = root_hash == Some(&root_hash_from_idsig);
@@ -275,6 +283,7 @@
     Ok(MicrodroidData {
         apk_data: ApkData { root_hash: root_hash_from_idsig, pubkey: apk_pubkey },
         apex_data: apex_data_from_payload,
+        bootconfig: get_bootconfig()?.clone().into_boxed_slice(),
     })
 }
 
@@ -297,6 +306,13 @@
     Ok(idsig.hashing_info.raw_root_hash)
 }
 
+fn get_bootconfig() -> Result<&'static Vec<u8>> {
+    static VAL: OnceCell<Vec<u8>> = OnceCell::new();
+    VAL.get_or_try_init(|| {
+        fs::read("/proc/bootconfig").context("Failed to read bootconfig")
+    })
+}
+
 fn load_config(path: &Path) -> Result<VmPayloadConfig> {
     info!("loading config from {:?}...", path);
     let file = ioutil::wait_for_file(path, WAIT_TIMEOUT)?;
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 61c3edc..0e99745 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -15,14 +15,21 @@
  */
 package com.android.microdroid.test;
 
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeNoException;
+import static org.junit.Assume.assumeThat;
+
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 import android.content.Context;
 import android.os.ParcelFileDescriptor;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineCallback;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineConfig.DebugLevel;
 import android.system.virtualmachine.VirtualMachineException;
 import android.system.virtualmachine.VirtualMachineManager;
 
@@ -36,6 +43,9 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
@@ -148,4 +158,65 @@
                 };
         listener.runToFinish(mInner.mVm);
     }
+
+    @Test
+    public void changingDebugLevelInvalidatesVmIdentity()
+            throws VirtualMachineException, InterruptedException, IOException {
+        assumeThat("Skip on Cuttlefish. b/195765441",
+                android.os.Build.DEVICE, is(not("vsoc_x86_64")));
+
+        VirtualMachineConfig.Builder builder =
+                new VirtualMachineConfig.Builder(mInner.mContext, "assets/vm_config.json");
+        VirtualMachineConfig normalConfig = builder.debugLevel(DebugLevel.NONE).build();
+        mInner.mVm = mInner.mVmm.getOrCreate("test_vm", normalConfig);
+        VmEventListener listener =
+                new VmEventListener() {
+                    @Override
+                    public void onPayloadReady(VirtualMachine vm) {
+                        // TODO(b/208639280): remove this sleep. For now, we need to wait for a few
+                        // seconds so that crosvm can actually persist instance.img.
+                        try {
+                            Thread.sleep(30 * 1000);
+                        } catch (InterruptedException e) { }
+                        forceStop(vm);
+                    }
+                };
+        listener.runToFinish(mInner.mVm);
+
+        // Launch the same VM with different debug level. The Java API prohibits this (thankfully).
+        // For testing, we do that by creating another VM with debug level, and copy the config file
+        // from the new VM directory to the old VM directory.
+        VirtualMachineConfig debugConfig = builder.debugLevel(DebugLevel.FULL).build();
+        VirtualMachine newVm  = mInner.mVmm.getOrCreate("test_debug_vm", debugConfig);
+        File vmRoot = new File(mInner.mContext.getFilesDir(), "vm");
+        File newVmConfig = new File(new File(vmRoot, "test_debug_vm"), "config.xml");
+        File oldVmConfig = new File(new File(vmRoot, "test_vm"), "config.xml");
+        Files.copy(newVmConfig.toPath(), oldVmConfig.toPath(), REPLACE_EXISTING);
+        newVm.delete();
+        mInner.mVm = mInner.mVmm.get("test_vm"); // re-load with the copied-in config file.
+        listener =
+                new VmEventListener() {
+                    private boolean mPayloadStarted = false;
+                    private boolean mErrorOccurred = false;
+
+                    @Override
+                    public void onPayloadStarted(VirtualMachine vm, ParcelFileDescriptor stream) {
+                        mPayloadStarted = true;
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onError(VirtualMachine vm, int errorCode, String message) {
+                        mErrorOccurred = true;
+                        forceStop(vm);
+                    }
+
+                    @Override
+                    public void onDied(VirtualMachine vm) {
+                        assertFalse(mPayloadStarted);
+                        assertTrue(mErrorOccurred);
+                    }
+                };
+        listener.runToFinish(mInner.mVm);
+    }
 }