Merge "virtualizationmanager: use raw file instead of qcow" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 33d46dd..db0b43a 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -3,6 +3,12 @@
 {
   "avf-presubmit": [
     {
+      "name": "AvfRkpdAppIntegrationTests"
+    },
+    {
+      "name": "AvfRkpdVmAttestationTestApp"
+    },
+    {
       "name": "MicrodroidHostTestCases"
     },
     {
@@ -56,16 +62,8 @@
       "name": "AVFHostTestCases"
     },
     {
-      // TODO(b/325610326): Add this target to presubmit once there is enough
-      // SLO data for it.
-      "name": "AvfRkpdAppIntegrationTests"
-    },
-    {
       "name": "AvfRkpdAppGoogleIntegrationTests",
       "keywords": ["internal"]
-    },
-    {
-      "name": "AvfRkpdVmAttestationTestApp"
     }
   ],
   "postsubmit": [
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index d0ca026..6914380 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -24,7 +24,10 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
-    VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
+    VirtualMachineAppConfig::{
+        CustomConfig::CustomConfig, DebugLevel::DebugLevel, Payload::Payload,
+        VirtualMachineAppConfig,
+    },
     VirtualMachineConfig::VirtualMachineConfig,
 };
 use anyhow::{anyhow, bail, Context, Result};
@@ -116,6 +119,11 @@
             VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
         };
 
+        // The CompOS VM doesn't need to be updatable (by design it should run exactly twice,
+        // with the same APKs and APEXes each time). And having it so causes some interesting
+        // circular dependencies when run at boot time by odsign: b/331417880.
+        let custom_config = Some(CustomConfig { wantUpdatable: false, ..Default::default() });
+
         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
             name: parameters.name.clone(),
             apk: Some(apk_fd),
@@ -128,6 +136,7 @@
             protectedVm: protected_vm,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
             cpuTopology: cpu_topology,
+            customConfig: custom_config,
             ..Default::default()
         });
 
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 270ea36..b218a5e 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -1,6 +1,9 @@
 # Custom VM
 
-You can spawn your own custom VMs by passing a JSON config file to the
+## Headless VMs
+
+If your VM is headless (i.e. console in/out is the primary way of interacting
+with it), you can spawn it by passing a JSON config file to the
 VirtualizationService via the `vm` tool on a rooted AVF-enabled device. If your
 device is attached over ADB, you can run:
 
@@ -21,3 +24,225 @@
 
 The `vm` command also has other subcommands for debugging; run
 `/apex/com.android.virt/bin/vm help` for details.
+
+## Graphical VMs
+
+To run OSes with graphics support, follow the instruction below.
+
+### Prepare a guest image
+
+As of today (April 2024), ChromiumOS is the only officially supported guest
+payload. We will be adding more OSes in the future.
+
+#### Build ChromiumOS for VM
+
+First, check out source code from the ChromiumOS and Chromium projects.
+
+* Checking out ChromiumOS: https://www.chromium.org/chromium-os/developer-library/guides/development/developer-guide/
+* Checking out Chromium: https://g3doc.corp.google.com/chrome/chromeos/system_services_team/dev_instructions/g3doc/setup_checkout.md?cl=headless
+
+Important: When you are at the step “Set up gclient args” in the Chromium checkout instruction, configure .gclient as follows.
+
+```
+$ cat ~/chromium/.gclient
+solutions = [
+  {
+    "name": "src",
+    "url": "https://chromium.googlesource.com/chromium/src.git",
+    "managed": False,
+    "custom_deps": {},
+    "custom_vars": {
+      "checkout_src_internal": True,
+    },
+  },
+]
+target_os = ['chromeos']
+```
+
+In this doc, it is assumed that ChromiumOS is checked out at `~/chromiumos` and
+Chromium is at `~/chromium`. If you downloaded to different places, you can
+create symlinks.
+
+Then enter into the cros sdk.
+
+```
+$ cd ~/chromiumos
+$ cros_sdk --chrome-root=$(readlink -f ~/chromium)
+```
+
+Now you are in the cros sdk. `(cr)` below means that the commands should be
+executed inside the sdk.
+
+First, choose the target board. `ferrochrome` is the name of the virtual board
+for AVF-compatible VM.
+
+```
+(cr) setup_board --board=ferrochrome
+```
+
+Then, tell the cros sdk that you want to build chrome (the browser) from the
+local checkout and also with your local modifications instead of prebuilts.
+
+```
+(cr) CHROME_ORIGIN=LOCAL_SOURCE
+(cr) ACCEPT_LICENSES='*'
+(cr) cros workon -b ferrochrome start \
+chromeos-base/chromeos-chrome \
+chromeos-base/chrome-icu
+(cr) cros_workon_make --board ferrochrome chromeos-chrome
+```
+
+Optionally, if you have touched the kernel source code (which is under
+~/chromiumos/src/third_party/kernel/v5.15), you have to tell the cros sdk that
+you want it also to be built from the modified source code, not from the
+official HEAD.
+
+```
+(cr) cros workon -b ferrochrome start chromeos-kernel-5_15
+```
+
+Finally, build individual packages, and build the disk image out of the packages.
+
+```
+(cr) cros build-packages --board=ferrochrome --chromium --accept-licenses='*'
+(cr) cros build-image --board=ferrochrome --no-enable-rootfs-verification test
+```
+
+This takes some time. When the build is done, exit from the sdk.
+
+Note: If build-packages doesn’t seem to include your local changes, try
+invoking emerge directly:
+
+```
+(cr) emerge-ferrochrome -av chromeos-base/chromeos-chrome
+```
+
+Don’t forget to call `build-image` afterwards.
+
+You need two outputs:
+
+* ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
+* The kernel: ~/chromiumos/out/build/ferrochrome/boot/vmlinuz
+
+### Create a guest VM configuration
+
+Push the kernel and the main image to the Android device.
+
+```
+$ adb push  ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin /data/local/tmp/
+$ adb push ~/chromiumos/out/build/ferrochrome/boot/vmlinuz /data/local/tmp/kernel
+```
+
+Create a VM config file as below.
+
+```
+$ cat > vm_config.json; adb push vm_config.json /data/local/tmp
+{
+    "name": "cros",
+    "kernel": "/data/local/tmp/kernel",
+    "disks": [
+        {
+            "image": "/data/local/tmp/chromiumos_test_image.bin",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib" : 8096
+}
+```
+
+### Running the VM
+
+First, enable the `VmLauncherApp` app. This needs to be done only once. In the
+future, this step won't be necesssary.
+
+```
+$ adb root
+$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
+$ adb unroot
+```
+
+Then execute the below to set up the network. In the future, this step won't be necessary.
+
+```
+$ cat > setup_network.sh; adb push setup_network.sh /data/local/tmp
+#!/system/bin/sh
+
+set -e
+
+TAP_IFACE=crosvm_tap
+TAP_ADDR=192.168.1.1
+TAP_NET=192.168.1.0
+
+function setup_network() {
+  local WAN_IFACE=$(ip route get 8.8.8.8 2> /dev/null | awk -- '{printf $5}')
+  if [ "${WAN_IFACE}" == "" ]; then
+    echo "No network. Connect to a WiFi network and start again"
+    return 1
+  fi
+
+  if ip link show ${TAP_IFACE} &> /dev/null ; then
+    echo "TAP interface ${TAP_IFACE} already exists"
+    return 1
+  fi
+
+  ip tuntap add mode tap group virtualmachine vnet_hdr ${TAP_IFACE}
+  ip addr add ${TAP_ADDR}/24 dev ${TAP_IFACE}
+  ip link set ${TAP_IFACE} up
+  ip rule flush
+  ip rule add from all lookup ${WAN_IFACE}
+  ip route add ${TAP_NET}/24 dev ${TAP_IFACE} table ${WAN_IFACE}
+  sysctl net.ipv4.ip_forward=1
+  iptables -t filter -F
+  iptables -t nat -A POSTROUTING -s ${TAP_NET}/24 -j MASQUERADE
+}
+
+function setup_if_necessary() {
+  if [ "$(getprop ro.crosvm.network.setup.done)" == 1 ]; then
+    return
+  fi
+  echo "Setting up..."
+  check_privilege
+  setup_network
+  setenforce 0
+  chmod 666 /dev/tun
+  setprop ro.crosvm.network.setup.done 1
+}
+
+function check_privilege() {
+  if [ "$(id -u)" -ne 0 ]; then
+    echo "Run 'adb root' first"
+    return 1
+  fi
+}
+
+setup_if_necessary
+^D
+
+adb root; adb shell /data/local/tmp/setup_network.sh
+```
+
+Then, finally tap the VmLauncherApp app from the launcher UI. You will see
+Ferrochrome booting!
+
+If it doesn’t work well, try
+
+```
+$ adb shell pm clear com.android.virtualization.vmlauncher
+```
+
+### Inside guest OS (for ChromiumOS only)
+
+Go to the network setting and configure as below.
+
+* IP: 192.168.1.2 (other addresses in the 192.168.1.0/24 subnet also works)
+* netmask: 255.255.255.0
+* gateway: 192.168.1.1
+* DNS: 8.8.8.8 (or any DNS server you know)
+
+These settings are persistent; stored in chromiumos_test_image.bin. So you
+don’t have to repeat this next time.`
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 98a541f..ff17ed1 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -181,28 +181,28 @@
     filename: "init.rc",
     src: "init.rc",
     relative_install_path: "init/hw",
-    installable: false, // avoid collision with system partition's init.rc
+    no_full_install: true, // avoid collision with system partition's init.rc
 }
 
 prebuilt_etc {
     name: "microdroid_ueventd_rc",
     filename: "ueventd.rc",
     src: "ueventd.rc",
-    installable: false, // avoid collision with system partition's ueventd.rc
+    no_full_install: true, // avoid collision with system partition's ueventd.rc
 }
 
 prebuilt_etc {
     name: "microdroid_etc_passwd",
     src: "microdroid_passwd",
     filename: "passwd",
-    installable: false,
+    no_full_install: true,
 }
 
 prebuilt_etc {
     name: "microdroid_etc_group",
     src: "microdroid_group",
     filename: "group",
-    installable: false,
+    no_full_install: true,
 }
 
 prebuilt_root {
@@ -217,7 +217,7 @@
             src: ":microdroid_build_prop_gen_arm64",
         },
     },
-    installable: false,
+    no_full_install: true,
 }
 
 genrule {
@@ -389,7 +389,7 @@
     name: "microdroid_fstab",
     src: "fstab.microdroid",
     filename: "fstab.microdroid",
-    installable: false,
+    no_full_install: true,
 }
 
 // python -c "import hashlib; print(hashlib.sha256(b'bootloader').hexdigest())"
@@ -441,14 +441,14 @@
     src: "microdroid_manifest.xml",
     filename: "manifest.xml",
     relative_install_path: "vintf",
-    installable: false,
+    no_full_install: true,
 }
 
 prebuilt_etc {
     name: "microdroid_event-log-tags",
     src: "microdroid_event-log-tags",
     filename: "event-log-tags",
-    installable: false,
+    no_full_install: true,
 }
 
 filegroup {
diff --git a/microdroid/kdump/Android.bp b/microdroid/kdump/Android.bp
index 6c85c43..cd68539 100644
--- a/microdroid/kdump/Android.bp
+++ b/microdroid/kdump/Android.bp
@@ -7,7 +7,7 @@
     defaults: ["avf_build_flags_cc"],
     stem: "kexec_load",
     srcs: ["kexec.c"],
-    installable: false,
+    no_full_install: true,
     static_executable: true, // required because this runs before linkerconfig
     compile_multilib: "64",
 }
@@ -18,7 +18,7 @@
     stem: "crashdump",
     srcs: ["crashdump.c"],
     static_executable: true,
-    installable: false,
+    no_full_install: true,
     compile_multilib: "64",
     sanitize: {
         hwaddress: false, // HWASAN setup fails when run as init process
diff --git a/microdroid/kdump/kernel/Android.bp b/microdroid/kdump/kernel/Android.bp
index 0705875..2bab6a8 100644
--- a/microdroid/kdump/kernel/Android.bp
+++ b/microdroid/kdump/kernel/Android.bp
@@ -21,5 +21,5 @@
             src: "x86_64/kernel-5.15",
         },
     },
-    installable: false,
+    no_full_install: true,
 }
diff --git a/rialto/src/main.rs b/rialto/src/main.rs
index 025edff..11e67cb 100644
--- a/rialto/src/main.rs
+++ b/rialto/src/main.rs
@@ -234,4 +234,4 @@
 }
 
 main!(main);
-configure_heap!(SIZE_128KB);
+configure_heap!(SIZE_128KB * 2);
diff --git a/service_vm/requests/src/rkp.rs b/service_vm/requests/src/rkp.rs
index 08ee08e..cdbd60e 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/service_vm/requests/src/rkp.rs
@@ -28,7 +28,7 @@
 use core::result;
 use coset::{iana, AsCborValue, CoseSign1, CoseSign1Builder, HeaderBuilder};
 use diced_open_dice::{derive_cdi_leaf_priv, kdf, sign, DiceArtifacts, PrivateKey};
-use log::error;
+use log::{debug, error};
 use service_vm_comm::{EcdsaP256KeyPair, GenerateCertificateRequestParams, RequestProcessingError};
 use zeroize::Zeroizing;
 
@@ -78,6 +78,8 @@
         let public_key = validate_public_key(&key_to_sign, hmac_key.as_ref())?;
         public_keys.push(public_key.to_cbor_value()?);
     }
+    debug!("Successfully validated all '{}' public keys.", public_keys.len());
+
     // Builds `CsrPayload`.
     let csr_payload = cbor!([
         Value::Integer(CSR_PAYLOAD_SCHEMA_V3.into()),
@@ -91,6 +93,7 @@
     let signed_data_payload =
         cbor!([Value::Bytes(params.challenge.to_vec()), Value::Bytes(csr_payload)])?;
     let signed_data = build_signed_data(&signed_data_payload, dice_artifacts)?.to_cbor_value()?;
+    debug!("Successfully signed the CSR payload.");
 
     // Builds `AuthenticatedRequest<CsrPayload>`.
     // Currently `UdsCerts` is left empty because it is only needed for Samsung devices.
@@ -104,6 +107,7 @@
         dice_cert_chain,
         signed_data,
     ])?;
+    debug!("Successfully built the CBOR authenticated request.");
     Ok(cbor_util::serialize(&auth_req)?)
 }
 
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index b176cfc..0280652 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -30,6 +30,7 @@
 
 import com.android.microdroid.test.common.MetricsProcessor;
 import com.android.microdroid.test.host.CommandRunner;
+import com.android.microdroid.test.host.KvmHypTracer;
 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
 import com.android.tradefed.device.DeviceNotAvailableException;
 import com.android.tradefed.device.ITestDevice;
@@ -37,6 +38,7 @@
 import com.android.tradefed.log.LogUtil.CLog;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
 import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.SimpleStats;
 
 import org.junit.After;
 import org.junit.Before;
@@ -118,6 +120,20 @@
     }
 
     @Test
+    public void testNoLongHypSections() throws Exception {
+        assumeTrue("Skip without hypervisor tracing", KvmHypTracer.isSupported(getDevice()));
+
+        KvmHypTracer tracer = new KvmHypTracer(getDevice());
+        String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
+        assertWithMessage("Failed to test compilation VM.")
+                .that(result).ignoringCase().contains("all ok");
+
+        SimpleStats stats = tracer.getDurationStats();
+        reportMetric(stats.getData(), "hyp_sections", "s");
+        CLog.i("Hypervisor traces parsed successfully.");
+    }
+
+    @Test
     public void testCameraAppStartupTime() throws Exception {
         String[] launchIntentPackages = {
             "com.android.camera2",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
new file mode 100644
index 0000000..0d8ee96
--- /dev/null
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.microdroid.test.host;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.microdroid.test.host.CommandRunner;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.SimpleStats;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.BufferedReader;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.annotation.Nonnull;
+
+/** This class provides utilities to interact with the hyp tracing subsystem */
+public final class KvmHypTracer {
+
+    private static final String HYP_TRACING_ROOT = "/sys/kernel/tracing/hyp/";
+    private static final String HYP_EVENTS[] = { "hyp_enter", "hyp_exit" };
+    private static final int DEFAULT_BUF_SIZE_KB = 4 * 1024;
+    private static final Pattern LOST_EVENT_PATTERN = Pattern.compile(
+            "^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
+    private static final Pattern EVENT_PATTERN = Pattern.compile(
+            "^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (" + String.join("|", HYP_EVENTS) + ") (.*)");
+
+    private final CommandRunner mRunner;
+    private final ITestDevice mDevice;
+    private final int mNrCpus;
+
+    private final ArrayList<File> mTraces;
+
+    private void setNode(String node, int val) throws Exception {
+        mRunner.run("echo " + val + " > " + HYP_TRACING_ROOT + node);
+    }
+
+    private static String eventDir(String event) {
+        return "events/hyp/" + event + "/";
+    }
+
+    public static boolean isSupported(ITestDevice device) throws Exception {
+        for (String event: HYP_EVENTS) {
+            if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable"))
+                return false;
+        }
+        return true;
+    }
+
+    public KvmHypTracer(@Nonnull ITestDevice device) throws Exception {
+        assertWithMessage("Hypervisor tracing not supported")
+                .that(isSupported(device)).isTrue();
+
+        mDevice = device;
+        mRunner = new CommandRunner(mDevice);
+        mTraces = new ArrayList<File>();
+        mNrCpus = Integer.parseInt(mRunner.run("nproc"));
+    }
+
+    public String run(String payload_cmd) throws Exception {
+        mTraces.clear();
+
+        setNode("tracing_on", 0);
+        mRunner.run("echo 0 | tee " + HYP_TRACING_ROOT + "events/*/*/enable");
+        setNode("buffer_size_kb", DEFAULT_BUF_SIZE_KB);
+        for (String event: HYP_EVENTS)
+            setNode(eventDir(event) + "/enable", 1);
+        setNode("trace", 0);
+
+        /* Cat each per-cpu trace_pipe in its own tmp file in the background */
+        String cmd = "cd " + HYP_TRACING_ROOT + ";";
+        String trace_pipes[] = new String[mNrCpus];
+        for (int i = 0; i < mNrCpus; i++) {
+            trace_pipes[i] = mRunner.run("mktemp -t trace_pipe.cpu" + i + ".XXXXXXXXXX");
+            cmd += "cat per_cpu/cpu" + i + "/trace_pipe > " + trace_pipes[i] + " &";
+            cmd += "CPU" + i + "_TRACE_PIPE_PID=$!;";
+        }
+
+        /* Run the payload with tracing enabled */
+        cmd += "echo 1 > tracing_on;";
+        String cmd_stdout = mRunner.run("mktemp -t cmd_stdout.XXXXXXXXXX");
+        cmd += payload_cmd + " > " + cmd_stdout + ";";
+        cmd += "echo 0 > tracing_on;";
+
+        /* Actively kill the cat subprocesses as trace_pipe is blocking */
+        for (int i = 0; i < mNrCpus; i++)
+            cmd += "kill -9 $CPU" + i + "_TRACE_PIPE_PID;";
+        cmd += "wait";
+
+        /*
+         * The whole thing runs in a single command for simplicity as `adb
+         * shell` doesn't play well with subprocesses outliving their parent,
+         * and cat-ing a trace_pipe is blocking, so doing so from separate Java
+         * threads wouldn't be much easier as we would need to actively kill
+         * them too.
+         */
+        mRunner.run(cmd);
+
+        for (String t: trace_pipes) {
+            File trace = mDevice.pullFile(t);
+            assertNotNull(trace);
+            mTraces.add(trace);
+            mRunner.run("rm -f " + t);
+        }
+
+        String res = mRunner.run("cat " + cmd_stdout);
+        mRunner.run("rm -f " + cmd_stdout);
+        return res;
+    }
+
+    public SimpleStats getDurationStats() throws Exception {
+        SimpleStats stats = new SimpleStats();
+
+        for (File trace: mTraces) {
+            BufferedReader br = new BufferedReader(new FileReader(trace));
+            double last = 0.0, hyp_enter = 0.0;
+            String l, prev_event = "";
+            while ((l = br.readLine()) != null) {
+                Matcher matcher = LOST_EVENT_PATTERN.matcher(l);
+                if (matcher.find())
+                    throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
+
+                matcher = EVENT_PATTERN.matcher(l);
+                if (!matcher.find()) {
+                    CLog.w("Failed to parse hyp event: " + l);
+                    continue;
+                }
+
+                int cpu = Integer.parseInt(matcher.group(1));
+                if (cpu < 0 || cpu >= mNrCpus)
+                    throw new ParseException("Incorrect CPU number: " + cpu, 0);
+
+                double cur = Double.parseDouble(matcher.group(2));
+                if (cur < last)
+                    throw new ParseException("Time must not go backward: " + cur, 0);
+                last = cur;
+
+                String event = matcher.group(3);
+                if (event.equals(prev_event)) {
+                    throw new ParseException("Hyp event found twice in a row: " + trace + " - " + l,
+                                             0);
+                }
+
+                switch (event) {
+                    case "hyp_exit":
+                        if (prev_event.equals("hyp_enter"))
+                            stats.add(cur - hyp_enter);
+                        break;
+                    case "hyp_enter":
+                        hyp_enter = cur;
+                        break;
+                    default:
+                        throw new ParseException("Unexpected line in trace" + l, 0);
+                }
+                prev_event = event;
+            }
+        }
+
+        return stats;
+    }
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 28d47a4..f939678 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -435,7 +435,8 @@
         if cfg!(llpvm_changes) {
             instance_id = extract_instance_id(config);
             untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
-            if is_secretkeeper_supported() {
+            let want_updatable = extract_want_updatable(config);
+            if want_updatable && is_secretkeeper_supported() {
                 // Let guest know that it can defer rollback protection to Secretkeeper by setting
                 // an empty property in untrusted node in DT. This enables Updatable VMs.
                 untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
@@ -1375,6 +1376,16 @@
     }
 }
 
+fn extract_want_updatable(config: &VirtualMachineConfig) -> bool {
+    match config {
+        VirtualMachineConfig::RawConfig(_) => true,
+        VirtualMachineConfig::AppConfig(config) => {
+            let Some(custom) = &config.customConfig else { return true };
+            custom.wantUpdatable
+        }
+    }
+}
+
 fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
     match config {
         VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 890535b..417d5d3 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -118,6 +118,12 @@
 
         /** List of SysFS nodes of devices to be assigned */
         String[] devices;
+
+        /**
+         * Whether the VM should be able to keep its secret when updated, if possible. This
+         * should rarely need to be set false.
+         */
+        boolean wantUpdatable = true;
     }
 
     /** Configuration parameters guarded by android.permission.USE_CUSTOM_VIRTUAL_MACHINE */
diff --git a/vm/src/run.rs b/vm/src/run.rs
index ca3e857..f3a5987 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -149,7 +149,6 @@
     let payload_config_str = format!("{:?}!{:?}", config.apk, payload);
 
     let custom_config = CustomConfig {
-        customKernelImage: None,
         gdbPort: config.debug.gdb.map(u16::from).unwrap_or(0) as i32, // 0 means no gdb
         vendorImage: vendor,
         devices: config
@@ -160,6 +159,7 @@
                 x.to_str().map(String::from).ok_or(anyhow!("Failed to convert {x:?} to String"))
             })
             .collect::<Result<_, _>>()?,
+        ..Default::default()
     };
 
     let vm_config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index 80d289b..97d4649 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -72,7 +72,7 @@
     ],
     whole_static_libs: ["libvm_payload_impl"],
     export_static_lib_headers: ["libvm_payload_impl"],
-    installable: false,
+    no_full_install: true,
     version_script: "libvm_payload.map.txt",
     stubs: {
         symbol_file: "libvm_payload.map.txt",