[automerger skipped] [vsr-test] Relax the pVM DICE chain verification test am: 9c237be297 -s ours

am skip reason: Merged-In I2d75a73c04d11e7b1adc70d9c8ed6c15404e689d with SHA-1 e7344de2a3 is already in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/modules/Virtualization/+/29100333

Change-Id: Ie753cfc14067d3ea1d7bb9ab9f0d094164d99620
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Android.bp b/Android.bp
index 3b6b8b5..a246e08 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,124 +1,3 @@
-//
-// Copyright (C) 2021 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 {
-    default_team: "trendy_team_virtualization",
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-soong_config_module_type {
-    name: "avf_flag_aware_rust_defaults",
-    module_type: "rust_defaults",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_device_assignment",
-        "release_avf_enable_dice_changes",
-        "release_avf_enable_llpvm_changes",
-        "release_avf_enable_multi_tenant_microdroid_vm",
-        "release_avf_enable_network",
-        "release_avf_enable_remote_attestation",
-        "release_avf_enable_vendor_modules",
-        "release_avf_enable_virt_cpufreq",
-        "release_avf_support_custom_vm_with_paravirtualized_devices",
-    ],
-    properties: [
-        "cfgs",
-    ],
-}
-
-avf_flag_aware_rust_defaults {
-    name: "avf_build_flags_rust",
-    soong_config_variables: {
-        release_avf_enable_device_assignment: {
-            cfgs: ["device_assignment"],
-        },
-        release_avf_enable_dice_changes: {
-            cfgs: ["dice_changes"],
-        },
-        release_avf_enable_llpvm_changes: {
-            cfgs: ["llpvm_changes"],
-        },
-        release_avf_enable_multi_tenant_microdroid_vm: {
-            cfgs: ["multi_tenant"],
-        },
-        release_avf_enable_network: {
-            cfgs: ["network"],
-        },
-        release_avf_enable_remote_attestation: {
-            cfgs: ["remote_attestation"],
-        },
-        release_avf_enable_vendor_modules: {
-            cfgs: ["vendor_modules"],
-        },
-        release_avf_enable_virt_cpufreq: {
-            cfgs: ["virt_cpufreq"],
-        },
-        release_avf_support_custom_vm_with_paravirtualized_devices: {
-            cfgs: ["paravirtualized_devices"],
-        },
-    },
-}
-
-soong_config_module_type {
-    name: "avf_flag_aware_cc_defaults",
-    module_type: "cc_defaults",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_dice_changes",
-        "release_avf_enable_vendor_modules",
-        "release_avf_enable_virt_cpufreq",
-    ],
-    properties: [
-        "cflags",
-    ],
-}
-
-avf_flag_aware_cc_defaults {
-    name: "avf_build_flags_cc",
-    soong_config_variables: {
-        release_avf_enable_dice_changes: {
-            cflags: ["-DAVF_OPEN_DICE_CHANGES=1"],
-        },
-        release_avf_enable_vendor_modules: {
-            cflags: ["-DAVF_ENABLE_VENDOR_MODULES=1"],
-        },
-        release_avf_enable_virt_cpufreq: {
-            cflags: ["-DAVF_ENABLE_VIRT_CPUFREQ=1"],
-        },
-    },
-}
-
-genrule_defaults {
-    name: "dts_to_dtb",
-    tools: ["dtc"],
-    cmd: "FILES=($(in)) && $(location dtc) -@ -I dts -O dtb $${FILES[-1]} -o $(out)",
-}
-
-// This is a temporary workaround until b/309090563 is implemented.
-aconfig_declarations {
-    name: "avf_aconfig_flags",
-    package: "com.android.system.virtualmachine.flags",
-    container: "com.android.virt",
-    srcs: [
-        "avf_flags.aconfig",
-    ],
-}
-
-java_aconfig_library {
-    name: "avf_aconfig_flags_java",
-    aconfig_declarations: "avf_aconfig_flags",
-    sdk_version: "module_current",
-    apex_available: ["com.android.virt"],
+    default_team: "trendy_team_android_kvm",
 }
diff --git a/README.md b/README.md
index f417b00..4a10c89 100644
--- a/README.md
+++ b/README.md
@@ -14,21 +14,21 @@
 For in-depth explanations about individual topics and components, visit the following links.
 
 AVF components:
-* [pVM firmware](pvmfw/README.md)
+* [pVM firmware](guest/pvmfw/README.md)
 * [Android Boot Loader (ABL)](docs/abl.md)
-* [Microdroid](microdroid/README.md)
-* [Microdroid kernel](microdroid/kernel/README.md)
-* [Microdroid payload](microdroid/payload/README.md)
-* [vmbase](vmbase/README.md)
-* [Encrypted Storage](encryptedstore/README.md)
+* [Microdroid](build/microdroid/README.md)
+* [Microdroid kernel](guest/kernel/README.md)
+* [Microdroid payload](libs/libmicrodroid_payload_metadata/README.md)
+* [vmbase](libs/libvmbase/README.md)
+* [Encrypted Storage](guest/encryptedstore/README.md)
 
 AVF APIs:
-* [Java API](java/framework/README.md)
-* [VM Payload API](vm_payload/README.md)
+* [Java API](libs/framework-virtualization/README.md)
+* [VM Payload API](libs/libvm_payload/README.md)
 
 How-Tos:
-* [Building and running a demo app in Java](demo/README.md)
-* [Building and running a demo app in C++](demo_native/README.md)
+* [Building and running a demo app in Java](android/MicrodroidDemoApp/README.md)
+* [Building and running a demo app in C++](android/vm_demo_native/README.md)
 * [Debugging](docs/debug)
 * [Using custom VM](docs/custom_vm.md)
 * [Device assignment](docs/device_assignment.md)
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 3651dfa..2112125 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -46,6 +46,9 @@
     },
     {
       "name": "libdice_driver_test"
+    },
+    {
+      "name": "vm_accessor_test"
     }
   ],
   "avf-postsubmit": [
@@ -78,13 +81,13 @@
   ],
   "imports": [
     {
-      "path": "packages/modules/Virtualization/apkdmverity"
+      "path": "packages/modules/Virtualization/guest/apkdmverity"
     },
     {
-      "path": "packages/modules/Virtualization/encryptedstore"
+      "path": "packages/modules/Virtualization/guest/encryptedstore"
     },
     {
-      "path": "packages/modules/Virtualization/virtualizationmanager"
+      "path": "packages/modules/Virtualization/android/virtmgr"
     },
     {
       "path": "packages/modules/Virtualization/libs/apexutil"
@@ -117,34 +120,34 @@
       "path": "packages/modules/Virtualization/authfs"
     },
     {
-      "path": "packages/modules/Virtualization/microdroid_manager"
+      "path": "packages/modules/Virtualization/guest/microdroid_manager"
     },
     {
-      "path": "packages/modules/Virtualization/pvmfw"
+      "path": "packages/modules/Virtualization/guest/pvmfw"
     },
     {
-      "path": "packages/modules/Virtualization/rialto"
+      "path": "packages/modules/Virtualization/guest/rialto"
     },
     {
-      "path": "packages/modules/Virtualization/service_vm/client_vm_csr"
+      "path": "packages/modules/Virtualization/libs/libclient_vm_csr"
     },
     {
-      "path": "packages/modules/Virtualization/service_vm/comm"
+      "path": "packages/modules/Virtualization/libs/libservice_vm_comm"
     },
     {
-      "path": "packages/modules/Virtualization/service_vm/requests"
+      "path": "packages/modules/Virtualization/libs/libservice_vm_requests"
     },
     {
-      "path": "packages/modules/Virtualization/virtualizationservice"
+      "path": "packages/modules/Virtualization/android/virtualizationservice"
     },
     {
-      "path": "packages/modules/Virtualization/vm"
+      "path": "packages/modules/Virtualization/android/vm"
     },
     {
-      "path": "packages/modules/Virtualization/vmbase"
+      "path": "packages/modules/Virtualization/tests/vmbase_example"
     },
     {
-      "path": "packages/modules/Virtualization/zipfuse"
+      "path": "packages/modules/Virtualization/guest/zipfuse"
     }
   ]
 }
diff --git a/apex/empty-payload-apk/Android.bp b/android/EmptyPayloadApp/Android.bp
similarity index 100%
rename from apex/empty-payload-apk/Android.bp
rename to android/EmptyPayloadApp/Android.bp
diff --git a/apex/empty-payload-apk/AndroidManifest.xml b/android/EmptyPayloadApp/AndroidManifest.xml
similarity index 100%
rename from apex/empty-payload-apk/AndroidManifest.xml
rename to android/EmptyPayloadApp/AndroidManifest.xml
diff --git a/apex/empty-payload-apk/empty_binary.cpp b/android/EmptyPayloadApp/empty_binary.cpp
similarity index 100%
rename from apex/empty-payload-apk/empty_binary.cpp
rename to android/EmptyPayloadApp/empty_binary.cpp
diff --git a/ferrochrome_app/Android.bp b/android/FerrochromeApp/Android.bp
similarity index 73%
rename from ferrochrome_app/Android.bp
rename to android/FerrochromeApp/Android.bp
index 9f0c735..3e4ad14 100644
--- a/ferrochrome_app/Android.bp
+++ b/android/FerrochromeApp/Android.bp
@@ -2,17 +2,22 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
+java_defaults {
+    name: "VmPayloadInstaller",
+    init_rc: [":custom_vm_setup.rc"],
+    required: ["custom_vm_setup"],
+    // TODO(b/348113995): move this app to product partition
+    system_ext_specific: true,
+    platform_apis: true,
+    privileged: true,
+}
+
 android_app {
     name: "FerrochromeApp",
     srcs: ["java/**/*.java"],
     resource_dirs: ["res"],
-    platform_apis: true,
-    // TODO(b/348113995): move this app to product partition
-    system_ext_specific: true,
-    privileged: true,
-    init_rc: ["custom_vm_setup.rc"],
+    defaults: ["VmPayloadInstaller"],
     required: [
-        "custom_vm_setup",
         "privapp-permissions-ferrochrome.xml",
     ],
 }
@@ -24,6 +29,11 @@
     system_ext_specific: true,
 }
 
+filegroup {
+    name: "custom_vm_setup.rc",
+    srcs: ["custom_vm_setup.rc"],
+}
+
 sh_binary {
     name: "custom_vm_setup",
     src: "custom_vm_setup.sh",
diff --git a/ferrochrome_app/AndroidManifest.xml b/android/FerrochromeApp/AndroidManifest.xml
similarity index 61%
rename from ferrochrome_app/AndroidManifest.xml
rename to android/FerrochromeApp/AndroidManifest.xml
index 62b065d..f6d3f6a 100644
--- a/ferrochrome_app/AndroidManifest.xml
+++ b/android/FerrochromeApp/AndroidManifest.xml
@@ -6,22 +6,38 @@
     <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
     <uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER" />
+
     <queries>
         <intent>
             <action android:name="android.virtualization.VM_LAUNCHER" />
         </intent>
+        <intent>
+            <action android:name="android.virtualization.FERROCHROME_DOWNLOADER" />
+        </intent>
     </queries>
     <application
         android:label="Ferrochrome">
         <activity android:name=".FerrochromeActivity"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
                   android:screenOrientation="landscape"
+                  android:resizeableActivity="false"
                   android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <activity android:name=".OpenUrlActivity"
+                  android:theme="@android:style/Theme.NoDisplay"
+                  android:launchMode="singleTask"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.SEND" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:mimeType="text/*" />
+            </intent-filter>
+        </activity>
     </application>
 
 </manifest>
diff --git a/ferrochrome_app/custom_vm_setup.rc b/android/FerrochromeApp/custom_vm_setup.rc
similarity index 100%
rename from ferrochrome_app/custom_vm_setup.rc
rename to android/FerrochromeApp/custom_vm_setup.rc
diff --git a/android/FerrochromeApp/custom_vm_setup.sh b/android/FerrochromeApp/custom_vm_setup.sh
new file mode 100644
index 0000000..df1a3a6
--- /dev/null
+++ b/android/FerrochromeApp/custom_vm_setup.sh
@@ -0,0 +1,32 @@
+#!/system/bin/sh
+
+function round_up() {
+  num=$1
+  div=$2
+  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
+}
+
+function install() {
+  src_dir=$(getprop debug.custom_vm_setup.path)
+  src_dir=${src_dir/#\/storage\/emulated\//\/data\/media\/}
+  dst_dir=/data/local/tmp/
+
+  cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
+  cp -u ${src_dir}/vm_config.json ${dst_dir}
+  chmod 666 ${dst_dir}/*
+
+  if [ -f ${dst_dir}state.img ]; then
+    # increase the size of state.img to the multiple of 4096
+    num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
+    required_num_blocks=$(round_up ${num_blocks} 4)
+    additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
+    dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
+  fi
+  rm ${src_dir}/images.tar.gz*
+  rm ${src_dir}/vm_config.json
+}
+
+setprop debug.custom_vm_setup.done false
+install
+setprop debug.custom_vm_setup.start false
+setprop debug.custom_vm_setup.done true
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
new file mode 100644
index 0000000..dba0078
--- /dev/null
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2024 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.virtualization.ferrochrome;
+
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class FerrochromeActivity extends Activity {
+    private static final String TAG = FerrochromeActivity.class.getName();
+    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+    private static final String ACTION_FERROCHROME_DOWNLOAD =
+            "android.virtualization.FERROCHROME_DOWNLOADER";
+    private static final String EXTRA_FERROCHROME_DEST_DIR = "dest_dir";
+    private static final String EXTRA_FERROCHROME_UPDATE_NEEDED = "update_needed";
+
+    private static final Path DEST_DIR =
+            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
+    private static final String ASSET_DIR = "ferrochrome";
+    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
+
+    private static final int REQUEST_CODE_VMLAUNCHER = 1;
+    private static final int REQUEST_CODE_FERROCHROME_DOWNLOADER = 2;
+
+    private ResolvedActivity mVmLauncher;
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (!isTaskRoot()) {
+            // In case we launched this activity multiple times, only start one instance of this
+            // activity by only starting this as the root activity in task.
+            finish();
+            Log.w(TAG, "Not starting because not task root");
+            return;
+        }
+        setContentView(R.layout.activity_ferrochrome);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+        // Find VM Launcher
+        mVmLauncher = ResolvedActivity.resolve(getPackageManager(), ACTION_VM_LAUNCHER);
+        if (mVmLauncher == null) {
+            updateStatus("Failed to resolve VM Launcher");
+            return;
+        }
+
+        // Clean up the existing vm launcher process if there is
+        ActivityManager am = getSystemService(ActivityManager.class);
+        am.killBackgroundProcesses(mVmLauncher.activityInfo.packageName);
+
+        executorService.execute(
+                () -> {
+                    if (hasLocalAssets()) {
+                        if (updateImageIfNeeded()) {
+                            updateStatus("Starting Ferrochrome...");
+                            runOnUiThread(
+                                    () ->
+                                            startActivityForResult(
+                                                    mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
+                        }
+                    } else {
+                        tryLaunchDownloader();
+                    }
+                });
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE_VMLAUNCHER) {
+            finishAndRemoveTask();
+        } else if (requestCode == REQUEST_CODE_FERROCHROME_DOWNLOADER) {
+            String destDir = data.getStringExtra(EXTRA_FERROCHROME_DEST_DIR);
+            boolean updateNeeded =
+                    data.getBooleanExtra(EXTRA_FERROCHROME_UPDATE_NEEDED, /* default= */ true);
+
+            if (resultCode != RESULT_OK || TextUtils.isEmpty(destDir)) {
+                Log.w(
+                        TAG,
+                        "Ferrochrome downloader returned error, code="
+                                + resultCode
+                                + ", dest="
+                                + destDir);
+                updateStatus("User didn't accepted ferrochrome download..");
+                return;
+            }
+
+            Log.w(TAG, "Ferrochrome downloader returned OK");
+
+            if (!updateNeeded) {
+                updateStatus("Starting Ferrochrome...");
+                startActivityForResult(mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER);
+            }
+
+            executorService.execute(
+                    () -> {
+                        if (!extractImages(destDir)) {
+                            updateStatus("Images from downloader looks bad..");
+                            return;
+                        }
+                        updateStatus("Starting Ferrochrome...");
+                        runOnUiThread(
+                                () ->
+                                        startActivityForResult(
+                                                mVmLauncher.intent, REQUEST_CODE_VMLAUNCHER));
+                    });
+        }
+    }
+
+    @WorkerThread
+    private boolean hasLocalAssets() {
+        try {
+            String[] files = getAssets().list(ASSET_DIR);
+            return files != null && files.length > 0;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    @WorkerThread
+    private boolean updateImageIfNeeded() {
+        if (!isUpdateNeeded()) {
+            Log.d(TAG, "No update needed.");
+            return true;
+        }
+
+        try {
+            if (Files.notExists(DEST_DIR)) {
+                Files.createDirectory(DEST_DIR);
+            }
+
+            updateStatus("Copying images...");
+            String[] files = getAssets().list("ferrochrome");
+            for (String file : files) {
+                updateStatus(file);
+                Path dst = Path.of(DEST_DIR.toString(), file);
+                updateFile(getAssets().open("ferrochrome/" + file), dst);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error while updating image: " + e);
+            updateStatus("Failed.");
+            return false;
+        }
+        updateStatus("Done.");
+
+        return extractImages(DEST_DIR.toAbsolutePath().toString());
+    }
+
+    @WorkerThread
+    private void tryLaunchDownloader() {
+        // TODO(jaewan): Add safeguard to check whether ferrochrome downloader is valid.
+        Log.w(TAG, "No built-in assets found. Try again with ferrochrome downloader");
+
+        ResolvedActivity downloader =
+                ResolvedActivity.resolve(getPackageManager(), ACTION_FERROCHROME_DOWNLOAD);
+        if (downloader == null) {
+            Log.d(TAG, "Ferrochrome downloader doesn't exist");
+            updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
+            return;
+        }
+        String pkgName = downloader.activityInfo.packageName;
+        Log.d(TAG, "Resolved Ferrochrome Downloader, pkgName=" + pkgName);
+        updateStatus("Launching Ferrochrome downloader for update");
+
+        // onActivityResult() will handle downloader result.
+        startActivityForResult(downloader.intent, REQUEST_CODE_FERROCHROME_DOWNLOADER);
+    }
+
+    @WorkerThread
+    private boolean extractImages(String destDir) {
+        updateStatus("Extracting images...");
+
+        if (TextUtils.isEmpty(destDir)) {
+            throw new RuntimeException("Internal error: destDir shouldn't be null");
+        }
+
+        SystemProperties.set("debug.custom_vm_setup.path", destDir);
+        SystemProperties.set("debug.custom_vm_setup.done", "false");
+        SystemProperties.set("debug.custom_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+            try {
+                Thread.sleep(1000);
+            } catch (Exception e) {
+                Log.e(TAG, "Error while extracting image: " + e);
+                updateStatus("Failed.");
+                return false;
+            }
+        }
+
+        updateStatus("Done.");
+        return true;
+    }
+
+    @WorkerThread
+    private boolean isUpdateNeeded() {
+        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
+        for (Path p : pathsToCheck) {
+            if (Files.notExists(p)) {
+                Log.d(TAG, p.toString() + " does not exist.");
+                return true;
+            }
+        }
+
+        try {
+            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
+            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
+            if (installedVer.equals(updatedVer)) {
+                return false;
+            }
+            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
+        } catch (IOException e) {
+            Log.e(TAG, "Error while checking version: " + e);
+        }
+        return true;
+    }
+
+    private static String readLine(InputStream input) throws IOException {
+        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
+            return reader.readLine();
+        } catch (IOException e) {
+            throw e;
+        }
+    }
+
+    private static void updateFile(InputStream input, Path path) throws IOException {
+        try {
+            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+        } finally {
+            input.close();
+        }
+    }
+
+    private void updateStatus(String line) {
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+
+    private static final class ResolvedActivity {
+        public final ActivityInfo activityInfo;
+        public final Intent intent;
+
+        private ResolvedActivity(ActivityInfo activityInfo, Intent intent) {
+            this.activityInfo = activityInfo;
+            this.intent = intent;
+        }
+
+        /* synthetic access */
+        static ResolvedActivity resolve(PackageManager pm, String action) {
+            Intent intent = new Intent(action).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+            List<ResolveInfo> resolveInfos =
+                    pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
+            if (resolveInfos == null || resolveInfos.size() != 1) {
+                Log.w(
+                        TAG,
+                        "Failed to resolve activity, action="
+                                + action
+                                + ", resolved="
+                                + resolveInfos);
+                return null;
+            }
+            ActivityInfo activityInfo = resolveInfos.get(0).activityInfo;
+            intent.setClassName(activityInfo.packageName, activityInfo.name);
+            return new ResolvedActivity(activityInfo, intent);
+        }
+    }
+}
diff --git a/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
new file mode 100644
index 0000000..433e89c
--- /dev/null
+++ b/android/FerrochromeApp/java/com/android/virtualization/ferrochrome/OpenUrlActivity.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.virtualization.ferrochrome;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+public class OpenUrlActivity extends Activity {
+    private static final String TAG = OpenUrlActivity.class.getSimpleName();
+
+    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        finish();
+
+        if (!Intent.ACTION_SEND.equals(getIntent().getAction())) {
+            return;
+        }
+        String text = getIntent().getStringExtra(Intent.EXTRA_TEXT);
+        if (text == null) {
+            return;
+        }
+        Uri uri = Uri.parse(text);
+        if (uri == null) {
+            return;
+        }
+        String scheme = uri.getScheme();
+        if (!("http".equals(scheme) || "https".equals(scheme) || "mailto".equals(scheme))) {
+            Log.e(TAG, "Unsupported URL scheme: " + scheme);
+            return;
+        }
+        Log.i(TAG, "Sending " + scheme + " URL to VM");
+        startActivity(
+                new Intent(ACTION_VM_OPEN_URL)
+                        .setFlags(
+                                Intent.FLAG_ACTIVITY_SINGLE_TOP
+                                        | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP)
+                        .putExtra(Intent.EXTRA_TEXT, text));
+    }
+}
diff --git a/ferrochrome_app/privapp-permissions-ferrochrome.xml b/android/FerrochromeApp/privapp-permissions-ferrochrome.xml
similarity index 100%
rename from ferrochrome_app/privapp-permissions-ferrochrome.xml
rename to android/FerrochromeApp/privapp-permissions-ferrochrome.xml
diff --git a/ferrochrome_app/repack.sh b/android/FerrochromeApp/repack.sh
similarity index 93%
rename from ferrochrome_app/repack.sh
rename to android/FerrochromeApp/repack.sh
index d47b529..b2a96dd 100755
--- a/ferrochrome_app/repack.sh
+++ b/android/FerrochromeApp/repack.sh
@@ -31,9 +31,6 @@
 echo Archiving. This can take long...
 tar czvS -f images.tar.gz *.img
 
-echo Calculating hash...
-hash=$(sha1sum images.tar.gz | cut -d' ' -f 1)
-
 echo Splitting...
 split -b 100M -d images.tar.gz images.tar.gz.part
 
@@ -45,6 +42,9 @@
 rm ${asset_dir}/images.tar.gz.part*
 mv ${tempdir}/images.tar.gz.part* ${asset_dir}
 sed -E s/GUID/${root_guid}/ ${vm_config_template} > ${asset_dir}/vm_config.json
+
+echo Calculating hash...
+hash=$(cat ${tempdir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
 echo ${hash} > ${asset_dir}/version
 
 echo Cleanup...
diff --git a/ferrochrome_app/res/layout/activity_ferrochrome.xml b/android/FerrochromeApp/res/layout/activity_ferrochrome.xml
similarity index 100%
rename from ferrochrome_app/res/layout/activity_ferrochrome.xml
rename to android/FerrochromeApp/res/layout/activity_ferrochrome.xml
diff --git a/ferrochrome_app/vm_config.json.template b/android/FerrochromeApp/vm_config.json.template
similarity index 68%
rename from ferrochrome_app/vm_config.json.template
rename to android/FerrochromeApp/vm_config.json.template
index cb968ec..380f016 100644
--- a/ferrochrome_app/vm_config.json.template
+++ b/android/FerrochromeApp/vm_config.json.template
@@ -29,9 +29,28 @@
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
     "memory_mib": 8096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "hvc0",
+    "network": true,
+    "input": {
+        "touchscreen": true,
+        "keyboard": true,
+        "mouse": true,
+        "trackpad": true,
+        "switches": true
+    },
+    "audio": {
+        "speaker": true,
+        "microphone": true
+    },
     "gpu": {
         "backend": "virglrenderer",
         "context_types": ["virgl2"]
     },
-    "console_input_device": "ttyS0"
+    "display": {
+        "scale": "0.77",
+        "refresh_rate": "30"
+    }
 }
diff --git a/android/LinuxInstaller/.gitignore b/android/LinuxInstaller/.gitignore
new file mode 100644
index 0000000..e81da29
--- /dev/null
+++ b/android/LinuxInstaller/.gitignore
@@ -0,0 +1,2 @@
+assets/*
+!assets/.gitkeep
diff --git a/android/LinuxInstaller/Android.bp b/android/LinuxInstaller/Android.bp
new file mode 100644
index 0000000..f70452d
--- /dev/null
+++ b/android/LinuxInstaller/Android.bp
@@ -0,0 +1,41 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "LinuxInstallerApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    asset_dirs: ["assets"],
+    manifest: "AndroidManifest.xml",
+    defaults: ["VmPayloadInstaller"],
+    overrides: ["LinuxInstallerAppStub"],
+    required: [
+        "privapp-permissions-linuxinstaller.xml",
+    ],
+    certificate: ":com.android.virtualization.linuxinstaller_certificate",
+}
+
+android_app {
+    name: "LinuxInstallerAppStub",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    manifest: "AndroidManifest_stub.xml",
+    defaults: ["VmPayloadInstaller"],
+    required: [
+        "privapp-permissions-linuxinstaller.xml",
+    ],
+    certificate: ":com.android.virtualization.linuxinstaller_certificate",
+}
+
+prebuilt_etc {
+    name: "privapp-permissions-linuxinstaller.xml",
+    src: "privapp-permissions-linuxinstaller.xml",
+    sub_dir: "permissions",
+    system_ext_specific: true,
+}
+
+android_app_certificate {
+    name: "com.android.virtualization.linuxinstaller_certificate",
+    certificate: "com_android_virtualization_linuxinstaller",
+}
diff --git a/android/LinuxInstaller/AndroidManifest.xml b/android/LinuxInstaller/AndroidManifest.xml
new file mode 100644
index 0000000..e5653f6
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.linuxinstaller"
+    android:versionCode="2100000000" >
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <queries>
+        <intent>
+            <action android:name="android.virtualization.VM_TERMINAL" />
+        </intent>
+    </queries>
+    <application
+        android:label="LinuxInstaller">
+        <activity android:name=".MainActivity"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/android/LinuxInstaller/AndroidManifest_stub.xml b/android/LinuxInstaller/AndroidManifest_stub.xml
new file mode 100644
index 0000000..49365ea
--- /dev/null
+++ b/android/LinuxInstaller/AndroidManifest_stub.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.linuxinstaller" >
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CHANGE_COMPONENT_ENABLED_STATE" />
+    <queries>
+        <intent>
+            <action android:name="android.virtualization.VM_TERMINAL" />
+        </intent>
+    </queries>
+    <application
+        android:label="LinuxInstaller">
+        <activity android:name=".MainActivity"
+                  android:exported="true">
+        </activity>
+    </application>
+
+</manifest>
diff --git a/microdroid/kdump/kernel/empty b/android/LinuxInstaller/assets/.gitkeep
similarity index 100%
copy from microdroid/kdump/kernel/empty
copy to android/LinuxInstaller/assets/.gitkeep
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8 b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
new file mode 100644
index 0000000..3f74303
--- /dev/null
+++ b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.pk8
Binary files differ
diff --git a/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
new file mode 100644
index 0000000..3ca64b7
--- /dev/null
+++ b/android/LinuxInstaller/com_android_virtualization_linuxinstaller.x509.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIIEETCCAvmgAwIBAgIUfBxyELS+ri3QErq8DXHu+47xx4EwDQYJKoZIhvcNAQEL
+BQAwgZYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQH
+DA1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKDAtHb29nbGUgSW5jLjEQMA4GA1UECwwH
+QW5kcm9pZDEyMDAGA1UEAwwpY29tX2FuZHJvaWRfdmlydHVhbGl6YXRpb25fbGlu
+dXhpbnN0YWxsZXIwIBcNMjQwODMwMTIyNjU2WhgPMjA1MjAxMTYxMjI2NTZaMIGW
+MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91
+bnRhaW4gVmlldzEUMBIGA1UECgwLR29vZ2xlIEluYy4xEDAOBgNVBAsMB0FuZHJv
+aWQxMjAwBgNVBAMMKWNvbV9hbmRyb2lkX3ZpcnR1YWxpemF0aW9uX2xpbnV4aW5z
+dGFsbGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8V/rH9ju6Wce
+1BdWuxfWaLmZJHGShXeDO6MB86Wrm10m26j9PFzd8/8FRKsZaujZphwNZsqBsdlt
+pWeNKts9T9luZn19Ci4E8A2EtgSxmfI8Fjwj/OJHHO0hG5+JcwIlUnmFQPcGtu/r
+EL3i7SfcF2ok+IC6aKYohnSbo+YkjyCSwb39i6POe6v6cPIZJtmOnecThS+fYCYR
+2yoMSSr3Bf8ayySrG0pJp7xZ1I5NixK6hUFZhQRLusyiv/KYTpAElMd+n1YJEYbf
+pW30DYAu+31S0hx8JXncFmI0uG3Zxx+LgNQwY8OPV6NPFfVwMPluZR6ep0tZ6q7e
+KIV2w5uC7QIDAQABo1MwUTAdBgNVHQ4EFgQU6FBYv7mW+9DR9q0c9uS4NNdX4Acw
+HwYDVR0jBBgwFoAU6FBYv7mW+9DR9q0c9uS4NNdX4AcwDwYDVR0TAQH/BAUwAwEB
+/zANBgkqhkiG9w0BAQsFAAOCAQEAj3bvUpwKjvpCggXzjMNkn7fAaQ0s1BubnkFe
+ge4zwz4tObP3OGRcxt5V9R5EZ7UY6bPcybA/rfg9FCzjcUQOBjmuepcQpbNHFW2I
+lasFa42UHkHSUFzeg2n9UC5iO3B+sclOr4EPaEE4HbG4B2vj++BYMW3C7PDyHc7R
+fq5ZsEEWcYUa8qZCO46I8AbMZ8iv1HpR4mZeQMkSxhD3uVHDQW+VqDTpzne/YBkJ
+yNfjpgFVZ/Y1E6BvvjzWZpBfj668fo7P3DekWHbvPPr/DiZ7OA6PCmAH1FBsi2c+
+xPgb9clDc2Zjb2Cd9lAoZdeB14zDOh6ZCF1c/i+qYt5tA9t+GA==
+-----END CERTIFICATE-----
diff --git a/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
new file mode 100644
index 0000000..1d875cb
--- /dev/null
+++ b/android/LinuxInstaller/java/com/android/virtualization/linuxinstaller/MainActivity.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2024 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.virtualization.linuxinstaller;
+
+import android.annotation.WorkerThread;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.TextView;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+    private static final String TAG = "LinuxInstaller";
+    private static final String ACTION_VM_TERMINAL = "android.virtualization.VM_TERMINAL";
+
+    private static final Path DEST_DIR =
+            Path.of(Environment.getExternalStorageDirectory().getPath(), "linux");
+
+    private static final String ASSET_DIR = "linux";
+    private static final String HASH_FILE_NAME = "hash";
+    private static final Path HASH_FILE = Path.of(DEST_DIR.toString(), HASH_FILE_NAME);
+
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        executorService.execute(this::installLinuxImage);
+    }
+
+    private void installLinuxImage() {
+        ComponentName vmTerminalComponent = resolve(getPackageManager(), ACTION_VM_TERMINAL);
+        if (vmTerminalComponent == null) {
+            updateStatus("Failed to resolve VM terminal");
+            return;
+        }
+
+        if (!hasLocalAssets()) {
+            updateStatus("No local assets");
+            return;
+        }
+        try {
+            updateImageIfNeeded();
+        } catch (IOException e) {
+            Log.e(TAG, "failed to update image", e);
+            return;
+        }
+        updateStatus("Enabling terminal app...");
+        getPackageManager()
+                .setComponentEnabledSetting(
+                        vmTerminalComponent,
+                        PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+                        PackageManager.DONT_KILL_APP);
+        updateStatus("Done.");
+    }
+
+    @WorkerThread
+    private boolean hasLocalAssets() {
+        try {
+            String[] files = getAssets().list(ASSET_DIR);
+            return files != null && files.length > 0;
+        } catch (IOException e) {
+            Log.e(TAG, "there is an error during listing up assets", e);
+            return false;
+        }
+    }
+
+    @WorkerThread
+    private void updateImageIfNeeded() throws IOException {
+        if (!isUpdateNeeded()) {
+            Log.d(TAG, "No update needed.");
+            return;
+        }
+
+        try {
+            if (Files.notExists(DEST_DIR)) {
+                Files.createDirectory(DEST_DIR);
+            }
+
+            updateStatus("Copying images...");
+            String[] files = getAssets().list(ASSET_DIR);
+            for (String file : files) {
+                updateStatus(file);
+                Path dst = Path.of(DEST_DIR.toString(), file);
+                updateFile(getAssets().open(ASSET_DIR + "/" + file), dst);
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Error while updating image: " + e);
+            updateStatus("Failed to update image.");
+            throw e;
+        }
+        extractImages(DEST_DIR.toAbsolutePath().toString());
+    }
+
+    @WorkerThread
+    private void extractImages(String destDir) throws IOException {
+        updateStatus("Extracting images...");
+
+        if (TextUtils.isEmpty(destDir)) {
+            throw new RuntimeException("Internal error: destDir shouldn't be null");
+        }
+
+        SystemProperties.set("debug.custom_vm_setup.path", destDir);
+        SystemProperties.set("debug.custom_vm_setup.done", "false");
+        SystemProperties.set("debug.custom_vm_setup.start", "true");
+        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "Error while extracting image: " + e);
+                updateStatus("Failed to extract image.");
+                throw new IOException("extracting image is interrupted", e);
+            }
+        }
+    }
+
+    @WorkerThread
+    private boolean isUpdateNeeded() {
+        Path[] pathsToCheck = {DEST_DIR, HASH_FILE};
+        for (Path p : pathsToCheck) {
+            if (Files.notExists(p)) {
+                Log.d(TAG, p.toString() + " does not exist.");
+                return true;
+            }
+        }
+
+        try {
+            String installedHash = readAll(new FileInputStream(HASH_FILE.toFile()));
+            String updatedHash = readAll(getAssets().open(ASSET_DIR + "/" + HASH_FILE_NAME));
+            if (installedHash.equals(updatedHash)) {
+                return false;
+            }
+            Log.d(TAG, "Hash mismatch. Installed: " + installedHash + "  Updated: " + updatedHash);
+        } catch (IOException e) {
+            Log.e(TAG, "Error while checking hash: " + e);
+        }
+        return true;
+    }
+
+    private static String readAll(InputStream input) throws IOException {
+        return Streams.readFully(new InputStreamReader(input)).strip();
+    }
+
+    private static void updateFile(InputStream input, Path path) throws IOException {
+        try (input) {
+            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
+        }
+    }
+
+    private void updateStatus(String line) {
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+
+    private ComponentName resolve(PackageManager pm, String action) {
+        Intent intent = new Intent(action);
+        List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
+        if (resolveInfos.size() != 1) {
+            Log.w(
+                    TAG,
+                    "Failed to resolve activity, action=" + action + ", resolved=" + resolveInfos);
+            return null;
+        }
+        ActivityInfo activityInfo = resolveInfos.getFirst().activityInfo;
+        // MainActivityAlias shows in Launcher
+        return new ComponentName(activityInfo.packageName, activityInfo.name + "Alias");
+    }
+}
diff --git a/android/LinuxInstaller/linux_image_builder/commands b/android/LinuxInstaller/linux_image_builder/commands
new file mode 100644
index 0000000..4d27475
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/commands
@@ -0,0 +1,11 @@
+upload init.sh:/root
+upload vsock.py:/usr/local/bin
+upload /tmp/ttyd:/usr/local/bin
+upload ttyd.service:/etc/systemd/system
+upload vsockip.service:/etc/systemd/system
+chmod 0777:/root/init.sh
+firstboot-command "/root/init.sh"
+chmod 0644:/etc/systemd/system/vsockip.service
+chmod 0644:/etc/systemd/system/ttyd.service
+chmod 0777:/usr/local/bin/vsock.py
+chmod 0777:/usr/local/bin/ttyd
diff --git a/android/LinuxInstaller/linux_image_builder/init.sh b/android/LinuxInstaller/linux_image_builder/init.sh
new file mode 100644
index 0000000..bec5ac5
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/init.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+systemctl daemon-reload
+systemctl start ttyd && sudo systemctl enable ttyd
+systemctl start vsockip && sudo systemctl enable vsockip
diff --git a/android/LinuxInstaller/linux_image_builder/setup.sh b/android/LinuxInstaller/linux_image_builder/setup.sh
new file mode 100755
index 0000000..2883e61
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/setup.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+pushd $(dirname $0) > /dev/null
+tempdir=$(mktemp -d)
+echo Get Debian image and dependencies...
+wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-arm64.raw -O ${tempdir}/debian.img
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.aarch64 -O ${tempdir}/ttyd
+
+echo Customize the image...
+virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
+
+asset_dir=../assets/linux
+mkdir -p ${asset_dir}
+
+echo Copy files...
+
+pushd ${tempdir} > /dev/null
+tar czvS -f images.tar.gz debian.img
+popd > /dev/null
+mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
+cp vm_config.json ${asset_dir}
+
+echo Calculating hash...
+hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
+echo ${hash} > ${asset_dir}/hash
+
+popd > /dev/null
+echo Cleaning up...
+rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
new file mode 100755
index 0000000..9748ce2
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+
+pushd $(dirname $0) > /dev/null
+tempdir=$(mktemp -d)
+echo Get Debian image and dependencies...
+wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.raw -O ${tempdir}/debian.img
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.tyd.x86_64 -O ${tempdir}/ttyd
+
+echo Customize the image...
+virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
+
+asset_dir=../assets/linux
+mkdir -p ${asset_dir}
+
+echo Copy files...
+
+pushd ${tempdir} > /dev/null
+tar czvS -f images.tar.gz debian.img
+popd > /dev/null
+mv ${tempdir}/images.tar.gz ${asset_dir}/images.tar.gz
+cp vm_config.json ${asset_dir}
+
+echo Calculating hash...
+hash=$(cat ${asset_dir}/images.tar.gz ${asset_dir}/vm_config.json | sha1sum | cut -d' ' -f 1)
+echo ${hash} > ${asset_dir}/hash
+
+popd > /dev/null
+echo Cleaning up...
+rm -rf ${tempdir}
\ No newline at end of file
diff --git a/android/LinuxInstaller/linux_image_builder/ttyd.service b/android/LinuxInstaller/linux_image_builder/ttyd.service
new file mode 100644
index 0000000..3a8f181
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/ttyd.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=TTYD
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/local/bin/ttyd -W login
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/linux_image_builder/vm_config.json b/android/LinuxInstaller/linux_image_builder/vm_config.json
new file mode 100644
index 0000000..21462b8
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vm_config.json
@@ -0,0 +1,19 @@
+{
+    "name": "debian",
+    "disks": [
+        {
+            "image": "/data/local/tmp/debian.img",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 4096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "ttyS0",
+    "network": true
+}
diff --git a/android/LinuxInstaller/linux_image_builder/vsock.py b/android/LinuxInstaller/linux_image_builder/vsock.py
new file mode 100644
index 0000000..292d953
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsock.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import socket
+
+# Constants for vsock (from linux/vm_sockets.h)
+AF_VSOCK = 40
+SOCK_STREAM = 1
+VMADDR_CID_ANY = -1
+
+def get_local_ip():
+    """Retrieves the first IPv4 address found on the system.
+
+    Returns:
+        str: The local IPv4 address, or '127.0.0.1' if no IPv4 address is found.
+    """
+
+    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    try:
+        s.connect(('8.8.8.8', 80))
+        ip = s.getsockname()[0]
+    except Exception:
+        ip = '127.0.0.1'
+    finally:
+        s.close()
+    return ip
+
+def main():
+    PORT = 1024
+
+    # Create a vsock socket
+    server_socket = socket.socket(AF_VSOCK, SOCK_STREAM)
+
+    # Bind the socket to the server address
+    server_address = (VMADDR_CID_ANY, PORT)
+    server_socket.bind(server_address)
+
+    # Listen for incoming connections
+    server_socket.listen(1)
+    print(f"VSOCK server listening on port {PORT}...")
+
+    while True:
+        # Accept a connection
+        connection, client_address = server_socket.accept()
+        print(f"Connection from: {client_address}")
+
+        try:
+            # Get the local IP address
+            local_ip = get_local_ip()
+
+            # Send the IP address to the client
+            connection.sendall(local_ip.encode())
+        finally:
+            # Close the connection
+            connection.close()
+
+if __name__ == "__main__":
+    main()
diff --git a/android/LinuxInstaller/linux_image_builder/vsockip.service b/android/LinuxInstaller/linux_image_builder/vsockip.service
new file mode 100644
index 0000000..a29020b
--- /dev/null
+++ b/android/LinuxInstaller/linux_image_builder/vsockip.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=vsock ip service
+After=syslog.target
+After=network.target
+[Service]
+ExecStart=/usr/bin/python3 /usr/local/bin/vsock.py
+Type=simple
+Restart=always
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
new file mode 100644
index 0000000..e46ec97
--- /dev/null
+++ b/android/LinuxInstaller/privapp-permissions-linuxinstaller.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<permissions>
+    <privapp-permissions package="com.android.virtualization.linuxinstaller">
+        <permission name="android.permission.CHANGE_COMPONENT_ENABLED_STATE"/>
+    </privapp-permissions>
+</permissions>
\ No newline at end of file
diff --git a/ferrochrome_app/res/layout/activity_ferrochrome.xml b/android/LinuxInstaller/res/layout/activity_main.xml
similarity index 100%
copy from ferrochrome_app/res/layout/activity_ferrochrome.xml
copy to android/LinuxInstaller/res/layout/activity_main.xml
diff --git a/demo/Android.bp b/android/MicrodroidDemoApp/Android.bp
similarity index 100%
rename from demo/Android.bp
rename to android/MicrodroidDemoApp/Android.bp
diff --git a/demo/AndroidManifest.xml b/android/MicrodroidDemoApp/AndroidManifest.xml
similarity index 100%
rename from demo/AndroidManifest.xml
rename to android/MicrodroidDemoApp/AndroidManifest.xml
diff --git a/demo/README.md b/android/MicrodroidDemoApp/README.md
similarity index 100%
rename from demo/README.md
rename to android/MicrodroidDemoApp/README.md
diff --git a/demo/java/com/android/microdroid/demo/MainActivity.java b/android/MicrodroidDemoApp/java/com/android/microdroid/demo/MainActivity.java
similarity index 98%
rename from demo/java/com/android/microdroid/demo/MainActivity.java
rename to android/MicrodroidDemoApp/java/com/android/microdroid/demo/MainActivity.java
index 906d18e..cbea5b9 100644
--- a/demo/java/com/android/microdroid/demo/MainActivity.java
+++ b/android/MicrodroidDemoApp/java/com/android/microdroid/demo/MainActivity.java
@@ -112,9 +112,7 @@
                             logView.append(line + "\n");
                             scrollLogView.fullScroll(View.FOCUS_DOWN);
                         });
-        model.getPayloadOutput()
-                .observeForever(
-                        line -> payloadView.append(line + "\n"));
+        model.getPayloadOutput().observeForever(line -> payloadView.append(line + "\n"));
     }
 
     /** Reads data from an input stream and posts it to the output data */
diff --git a/demo/res/layout/activity_main.xml b/android/MicrodroidDemoApp/res/layout/activity_main.xml
similarity index 100%
rename from demo/res/layout/activity_main.xml
rename to android/MicrodroidDemoApp/res/layout/activity_main.xml
diff --git a/demo/res/values/colors.xml b/android/MicrodroidDemoApp/res/values/colors.xml
similarity index 100%
rename from demo/res/values/colors.xml
rename to android/MicrodroidDemoApp/res/values/colors.xml
diff --git a/demo/res/values/themes.xml b/android/MicrodroidDemoApp/res/values/themes.xml
similarity index 100%
rename from demo/res/values/themes.xml
rename to android/MicrodroidDemoApp/res/values/themes.xml
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
new file mode 100644
index 0000000..3ae014e
--- /dev/null
+++ b/android/TerminalApp/Android.bp
@@ -0,0 +1,19 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "VmTerminalApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    static_libs: [
+        "vm_launcher_lib",
+    ],
+    sdk_version: "system_current",
+    optimize: {
+        shrink_resources: true,
+    },
+    apex_available: [
+        "com.android.virt",
+    ],
+}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
new file mode 100644
index 0000000..d92aa8b
--- /dev/null
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.terminal" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"/>
+
+    <application
+        android:label="VmTerminalApp"
+        android:usesCleartextTraffic="true">
+        <activity android:name=".MainActivity"
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode|screenLayout|smallestScreenSize"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.virtualization.VM_TERMINAL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity-alias
+            android:name=".MainActivityAlias"
+            android:targetActivity="com.android.virtualization.terminal.MainActivity"
+            android:exported="true"
+            android:enabled="false" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity-alias>
+    </application>
+
+</manifest>
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
new file mode 100644
index 0000000..a6723fb
--- /dev/null
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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.virtualization.terminal;
+
+import android.app.Activity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.webkit.WebChromeClient;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.virtualization.vmlauncher.VmLauncherServices;
+
+public class MainActivity extends Activity implements VmLauncherServices.VmLauncherServiceCallback {
+    private static final String TAG = "VmTerminalApp";
+    private String mVmIpAddr;
+    private WebView mWebView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Toast.makeText(this, R.string.vm_creation_message, Toast.LENGTH_SHORT).show();
+        VmLauncherServices.startVmLauncherService(this, this);
+
+        setContentView(R.layout.activity_headless);
+        mWebView = (WebView) findViewById(R.id.webview);
+        mWebView.getSettings().setDatabaseEnabled(true);
+        mWebView.getSettings().setDomStorageEnabled(true);
+        mWebView.getSettings().setJavaScriptEnabled(true);
+        mWebView.setWebChromeClient(new WebChromeClient());
+        mWebView.setWebViewClient(
+                new WebViewClient() {
+                    @Override
+                    public boolean shouldOverrideUrlLoading(WebView view, String url) {
+                        view.loadUrl(url);
+                        return true;
+                    }
+                });
+    }
+
+    @Override
+    protected void onDestroy() {
+        VmLauncherServices.stopVmLauncherService(this);
+        super.onDestroy();
+    }
+
+    private void gotoURL(String url) {
+        runOnUiThread(() -> mWebView.loadUrl(url));
+    }
+
+    public void onVmStart() {
+        Log.i(TAG, "onVmStart()");
+    }
+
+    public void onVmStop() {
+        Toast.makeText(this, R.string.vm_stop_message, Toast.LENGTH_SHORT).show();
+        Log.i(TAG, "onVmStop()");
+        finish();
+    }
+
+    public void onVmError() {
+        Toast.makeText(this, R.string.vm_error_message, Toast.LENGTH_SHORT).show();
+        Log.i(TAG, "onVmError()");
+        finish();
+    }
+
+    public void onIpAddrAvailable(String ipAddr) {
+        mVmIpAddr = ipAddr;
+        ((TextView) findViewById(R.id.ip_addr_textview)).setText(mVmIpAddr);
+
+        // TODO(b/359523803): Use AVF API to be notified when shell is ready instead of using dealy
+        new Handler(Looper.getMainLooper())
+                .postDelayed(() -> gotoURL("http://" + mVmIpAddr + ":7681"), 2000);
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.main_menu, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onMenuItemSelected(int featureId, MenuItem item) {
+        int id = item.getItemId();
+        if (id == R.id.copy_ip_addr) {
+            // TODO(b/340126051): remove this menu item when port forwarding is supported.
+            getSystemService(ClipboardManager.class)
+                    .setPrimaryClip(ClipData.newPlainText("A VM's IP address", mVmIpAddr));
+            return true;
+        } else if (id == R.id.stop_vm) {
+            VmLauncherServices.stopVmLauncherService(this);
+            return true;
+        }
+        return super.onMenuItemSelected(featureId, item);
+    }
+}
diff --git a/android/TerminalApp/res/layout/activity_headless.xml b/android/TerminalApp/res/layout/activity_headless.xml
new file mode 100644
index 0000000..3fe5271
--- /dev/null
+++ b/android/TerminalApp/res/layout/activity_headless.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:fitsSystemWindows="true"
+    tools:context=".MainActivity">
+    <TextView
+        android:id="@+id/ip_addr_textview"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content" />
+  <WebView
+      android:id="@+id/webview"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:layout_marginBottom="5dp" />
+
+</LinearLayout>
diff --git a/android/TerminalApp/res/menu/main_menu.xml b/android/TerminalApp/res/menu/main_menu.xml
new file mode 100644
index 0000000..cc34cda
--- /dev/null
+++ b/android/TerminalApp/res/menu/main_menu.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/copy_ip_addr"
+        android:title="Copy the IP address"/>
+    <item android:id="@+id/stop_vm"
+        android:title="Stop the existing VM instance"/>
+</menu>
\ No newline at end of file
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
new file mode 100644
index 0000000..eb6476d
--- /dev/null
+++ b/android/TerminalApp/res/values/strings.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--  Copyright (C) 2024 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.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="vm_creation_message">A VM instance is being created.</string>
+    <string name="vm_stop_message">A VM instance is stopped, finish the app.</string>
+    <string name="vm_error_message">A VM instance is crashed, finish the app.</string>
+</resources>
\ No newline at end of file
diff --git a/service_vm/demo_apk/Android.bp b/android/VmAttestationDemoApp/Android.bp
similarity index 100%
rename from service_vm/demo_apk/Android.bp
rename to android/VmAttestationDemoApp/Android.bp
diff --git a/service_vm/demo_apk/AndroidManifest.xml b/android/VmAttestationDemoApp/AndroidManifest.xml
similarity index 100%
rename from service_vm/demo_apk/AndroidManifest.xml
rename to android/VmAttestationDemoApp/AndroidManifest.xml
diff --git a/service_vm/demo_apk/README.md b/android/VmAttestationDemoApp/README.md
similarity index 100%
rename from service_vm/demo_apk/README.md
rename to android/VmAttestationDemoApp/README.md
diff --git a/service_vm/demo_apk/assets/config.json b/android/VmAttestationDemoApp/assets/config.json
similarity index 100%
rename from service_vm/demo_apk/assets/config.json
rename to android/VmAttestationDemoApp/assets/config.json
diff --git a/service_vm/demo_apk/src/main.rs b/android/VmAttestationDemoApp/src/main.rs
similarity index 100%
rename from service_vm/demo_apk/src/main.rs
rename to android/VmAttestationDemoApp/src/main.rs
diff --git a/vmlauncher_app/Android.bp b/android/VmLauncherApp/Android.bp
similarity index 91%
rename from vmlauncher_app/Android.bp
rename to android/VmLauncherApp/Android.bp
index 7103d53..7dd2473 100644
--- a/vmlauncher_app/Android.bp
+++ b/android/VmLauncherApp/Android.bp
@@ -11,6 +11,7 @@
         "android.system.virtualizationservice_internal-java",
         // TODO(b/331708504): will be removed when AVF framework handles surface
         "libcrosvm_android_display_service-java",
+        "gson",
     ],
     libs: [
         "framework-virtualization.impl",
@@ -22,7 +23,7 @@
         "com.android.virt",
     ],
     optimize: {
-        optimize: true,
+        proguard_flags_files: ["proguard.flags"],
         shrink_resources: true,
     },
 }
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
new file mode 100644
index 0000000..583fce7
--- /dev/null
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.vmlauncher" >
+
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
+    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+
+    <permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+        android:protectionLevel="signature|preinstalled"/>
+
+    <application
+        android:label="VmLauncherApp">
+        <activity android:name=".MainActivity"
+                  android:screenOrientation="landscape"
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+                  android:theme="@style/MyTheme"
+                  android:resizeableActivity="false"
+                  android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.virtualization.VM_LAUNCHER" />
+                <action android:name="android.virtualization.VM_OPEN_URL" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <service
+            android:name=".VmLauncherService"
+            android:enabled="true"
+            android:exported="true"
+            android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+            android:foregroundServiceType="specialUse">
+            <property
+                android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
+                android:value="Run VM instances" />
+            <intent-filter>
+                <action android:name="android.virtualization.START_VM_LAUNCHER_SERVICE" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </service>
+
+    </application>
+
+</manifest>
diff --git a/vmlauncher_app/README.md b/android/VmLauncherApp/README.md
similarity index 100%
rename from vmlauncher_app/README.md
rename to android/VmLauncherApp/README.md
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
new file mode 100644
index 0000000..def464e
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ClipboardHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+
+/** Provide methods to synchronize clipboard across Android and VM. */
+class ClipboardHandler {
+    private static final String TAG = MainActivity.TAG;
+    private final ClipboardManager mClipboardManager;
+    private final VmAgent mVmAgent;
+
+    ClipboardHandler(Context context, VmAgent vmAgent) {
+        mClipboardManager = context.getSystemService(ClipboardManager.class);
+        mVmAgent = vmAgent;
+    }
+
+    private VmAgent.Connection getConnection() throws InterruptedException {
+        return mVmAgent.connect();
+    }
+
+    /** Read a text clip from Android's clipboard and send it to VM. */
+    void writeClipboardToVm() {
+        if (!mClipboardManager.hasPrimaryClip()) {
+            return;
+        }
+
+        ClipData clip = mClipboardManager.getPrimaryClip();
+        String text = clip.getItemAt(0).getText().toString();
+        // TODO: remove this trailing null character. The size is already encoded in the header.
+        text = text + '\0';
+        // TODO: use UTF-8 encoding
+        byte[] data = text.getBytes();
+
+        try {
+            getConnection().sendData(VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, data);
+        } catch (InterruptedException | RuntimeException e) {
+            Log.e(TAG, "Failed to write clipboard data to VM", e);
+        }
+    }
+
+    /** Read a text clip from VM and paste it to Android's clipboard. */
+    void readClipboardFromVm() {
+        VmAgent.Data data;
+        try {
+            data = getConnection().sendAndReceive(VmAgent.READ_CLIPBOARD_FROM_VM, null);
+        } catch (InterruptedException | RuntimeException e) {
+            Log.e(TAG, "Failed to read clipboard data from VM", e);
+            return;
+        }
+
+        switch (data.type) {
+            case VmAgent.WRITE_CLIPBOARD_TYPE_EMPTY:
+                Log.d(TAG, "clipboard data from VM is empty");
+                break;
+            case VmAgent.WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
+                String text = new String(data.data, StandardCharsets.UTF_8);
+                ClipData clip = ClipData.newPlainText(null, text);
+                mClipboardManager.setPrimaryClip(clip);
+                break;
+            default:
+                Log.e(TAG, "Unknown clipboard response type: " + data.type);
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
new file mode 100644
index 0000000..6d39b46
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Disk;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.Partition;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+import android.view.WindowMetrics;
+
+import com.google.gson.Gson;
+import com.google.gson.annotations.SerializedName;
+
+import java.io.FileReader;
+import java.util.Arrays;
+
+/** This class and its inner classes model vm_config.json. */
+class ConfigJson {
+    private static final boolean DEBUG = true;
+
+    private ConfigJson() {}
+
+    @SerializedName("protected")
+    private boolean isProtected;
+
+    private String name;
+    private String cpu_topology;
+    private String platform_version;
+    private int memory_mib = 1024;
+    private String console_input_device;
+    private String bootloader;
+    private String kernel;
+    private String initrd;
+    private String params;
+    private boolean debuggable;
+    private boolean console_out;
+    private boolean connect_console;
+    private boolean network;
+    private InputJson input;
+    private AudioJson audio;
+    private DiskJson[] disks;
+    private DisplayJson display;
+    private GpuJson gpu;
+
+    /** Parses JSON file at jsonPath */
+    static ConfigJson from(String jsonPath) {
+        try (FileReader r = new FileReader(jsonPath)) {
+            return new Gson().fromJson(r, ConfigJson.class);
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to parse " + jsonPath, e);
+        }
+    }
+
+    private int getCpuTopology() {
+        switch (cpu_topology) {
+            case "one_cpu":
+                return VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+            case "match_host":
+                return VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+            default:
+                throw new RuntimeException("invalid cpu topology: " + cpu_topology);
+        }
+    }
+
+    private int getDebugLevel() {
+        return debuggable
+                ? VirtualMachineConfig.DEBUG_LEVEL_FULL
+                : VirtualMachineConfig.DEBUG_LEVEL_NONE;
+    }
+
+    /** Converts this parsed JSON into VirtualMachieConfig */
+    VirtualMachineConfig toConfig(Context context) {
+        return new VirtualMachineConfig.Builder(context)
+                .setProtectedVm(isProtected)
+                .setMemoryBytes((long) memory_mib * 1024 * 1024)
+                .setConsoleInputDevice(console_input_device)
+                .setCpuTopology(getCpuTopology())
+                .setCustomImageConfig(toCustomImageConfig(context))
+                .setDebugLevel(getDebugLevel())
+                .setVmOutputCaptured(console_out)
+                .setConnectVmConsole(connect_console)
+                .build();
+    }
+
+    private VirtualMachineCustomImageConfig toCustomImageConfig(Context context) {
+        VirtualMachineCustomImageConfig.Builder builder =
+                new VirtualMachineCustomImageConfig.Builder();
+
+        builder.setName(name)
+                .setBootloaderPath(bootloader)
+                .setKernelPath(kernel)
+                .setInitrdPath(initrd)
+                .useNetwork(network);
+
+        if (input != null) {
+            builder.useTouch(input.touchscreen)
+                    .useKeyboard(input.keyboard)
+                    .useMouse(input.mouse)
+                    .useTrackpad(input.trackpad)
+                    .useSwitches(input.switches);
+        }
+
+        if (audio != null) {
+            builder.setAudioConfig(audio.toConfig());
+        }
+
+        if (display != null) {
+            builder.setDisplayConfig(display.toConfig(context));
+        }
+
+        if (gpu != null) {
+            builder.setGpuConfig(gpu.toConfig());
+        }
+
+        if (params != null) {
+            Arrays.stream(params.split(" ")).forEach(builder::addParam);
+        }
+
+        if (disks != null) {
+            Arrays.stream(disks).map(d -> d.toConfig()).forEach(builder::addDisk);
+        }
+
+        return builder.build();
+    }
+
+    private static class InputJson {
+        private InputJson() {}
+
+        private boolean touchscreen;
+        private boolean keyboard;
+        private boolean mouse;
+        private boolean switches;
+        private boolean trackpad;
+    }
+
+    private static class AudioJson {
+        private AudioJson() {}
+
+        private boolean microphone;
+        private boolean speaker;
+
+        private AudioConfig toConfig() {
+            return new AudioConfig.Builder()
+                    .setUseMicrophone(microphone)
+                    .setUseSpeaker(speaker)
+                    .build();
+        }
+    }
+
+    private static class DiskJson {
+        private DiskJson() {}
+
+        private boolean writable;
+        private String image;
+        private PartitionJson[] partitions;
+
+        private Disk toConfig() {
+            Disk d = writable ? Disk.RWDisk(image) : Disk.RODisk(image);
+            for (PartitionJson pj : partitions) {
+                boolean writable = this.writable && pj.writable;
+                d.addPartition(new Partition(pj.label, pj.path, writable, pj.guid));
+            }
+            return d;
+        }
+    }
+
+    private static class PartitionJson {
+        private PartitionJson() {}
+
+        private boolean writable;
+        private String label;
+        private String path;
+        private String guid;
+    }
+
+    private static class DisplayJson {
+        private DisplayJson() {}
+
+        private float scale;
+        private int refresh_rate;
+        private int width_pixels;
+        private int height_pixels;
+
+        private DisplayConfig toConfig(Context context) {
+            WindowManager wm = context.getSystemService(WindowManager.class);
+            WindowMetrics metrics = wm.getCurrentWindowMetrics();
+            Rect dispBounds = metrics.getBounds();
+
+            int width = width_pixels > 0 ? width_pixels : dispBounds.right;
+            int height = height_pixels > 0 ? height_pixels : dispBounds.bottom;
+
+            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * metrics.getDensity());
+            if (scale > 0.0f) {
+                dpi = (int) (dpi * scale);
+            }
+
+            int refreshRate = (int) context.getDisplay().getRefreshRate();
+            if (this.refresh_rate != 0) {
+                refreshRate = this.refresh_rate;
+            }
+
+            return new DisplayConfig.Builder()
+                    .setWidth(width)
+                    .setHeight(height)
+                    .setHorizontalDpi(dpi)
+                    .setVerticalDpi(dpi)
+                    .setRefreshRate(refreshRate)
+                    .build();
+        }
+    }
+
+    private static class GpuJson {
+        private GpuJson() {}
+
+        private String backend;
+        private String pci_address;
+        private String renderer_features;
+        private boolean renderer_use_egl = true;
+        private boolean renderer_use_gles = true;
+        private boolean renderer_use_glx = false;
+        private boolean renderer_use_surfaceless = true;
+        private boolean renderer_use_vulkan = false;
+        private String[] context_types;
+
+        private GpuConfig toConfig() {
+            return new GpuConfig.Builder()
+                    .setBackend(backend)
+                    .setPciAddress(pci_address)
+                    .setRendererFeatures(renderer_features)
+                    .setRendererUseEgl(renderer_use_egl)
+                    .setRendererUseGles(renderer_use_gles)
+                    .setRendererUseGlx(renderer_use_glx)
+                    .setRendererUseSurfaceless(renderer_use_surfaceless)
+                    .setRendererUseVulkan(renderer_use_vulkan)
+                    .setContextTypes(context_types)
+                    .build();
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java
new file mode 100644
index 0000000..6eba709
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/DisplayProvider.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.crosvm.ICrosvmAndroidDisplayService;
+import android.graphics.PixelFormat;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
+import android.util.Log;
+import android.view.SurfaceControl;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import libcore.io.IoBridge;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/** Presents Android-side surface where VM can use as a display */
+class DisplayProvider {
+    private static final String TAG = MainActivity.TAG;
+    private final SurfaceView mMainView;
+    private final SurfaceView mCursorView;
+    private final IVirtualizationServiceInternal mVirtService;
+    private CursorHandler mCursorHandler;
+
+    DisplayProvider(SurfaceView mainView, SurfaceView cursorView) {
+        mMainView = mainView;
+        mCursorView = cursorView;
+
+        mMainView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
+        mMainView.getHolder().addCallback(new Callback(Callback.SurfaceKind.MAIN));
+
+        mCursorView.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
+        mCursorView.getHolder().addCallback(new Callback(Callback.SurfaceKind.CURSOR));
+        mCursorView.getHolder().setFormat(PixelFormat.RGBA_8888);
+        // TODO: do we need this z-order?
+        mCursorView.setZOrderMediaOverlay(true);
+
+        IBinder b = ServiceManager.waitForService("android.system.virtualizationservice");
+        mVirtService = IVirtualizationServiceInternal.Stub.asInterface(b);
+        try {
+            // To ensure that the previous display service is removed.
+            mVirtService.clearDisplayService();
+        } catch (RemoteException e) {
+            throw new RuntimeException("Failed to clear prior display service", e);
+        }
+    }
+
+    void notifyDisplayIsGoingToInvisible() {
+        // When the display is going to be invisible (by putting in the background), save the frame
+        // of the main surface so that we can re-draw it next time the display becomes visible. This
+        // is to save the duration of time where nothing is drawn by VM.
+        try {
+            getDisplayService().saveFrameForSurface(false /* forCursor */);
+        } catch (RemoteException e) {
+            throw new RuntimeException("Failed to save frame for the main surface", e);
+        }
+    }
+
+    private synchronized ICrosvmAndroidDisplayService getDisplayService() {
+        try {
+            IBinder b = mVirtService.waitDisplayService();
+            return ICrosvmAndroidDisplayService.Stub.asInterface(b);
+        } catch (Exception e) {
+            throw new RuntimeException("Error while getting display service", e);
+        }
+    }
+
+    private class Callback implements SurfaceHolder.Callback {
+        enum SurfaceKind {
+            MAIN,
+            CURSOR
+        }
+
+        private final SurfaceKind mSurfaceKind;
+
+        Callback(SurfaceKind kind) {
+            mSurfaceKind = kind;
+        }
+
+        private boolean isForCursor() {
+            return mSurfaceKind == SurfaceKind.CURSOR;
+        }
+
+        @Override
+        public void surfaceCreated(SurfaceHolder holder) {
+            try {
+                getDisplayService().setSurface(holder.getSurface(), isForCursor());
+            } catch (Exception e) {
+                // TODO: don't consume this exception silently. For some unknown reason, setSurface
+                // call above throws IllegalArgumentException and that fails the surface
+                // configuration.
+                Log.e(TAG, "Failed to present surface " + mSurfaceKind + " to VM", e);
+            }
+
+            try {
+                switch (mSurfaceKind) {
+                    case MAIN:
+                        getDisplayService().drawSavedFrameForSurface(isForCursor());
+                        break;
+                    case CURSOR:
+                        ParcelFileDescriptor stream = createNewCursorStream();
+                        getDisplayService().setCursorStream(stream);
+                        break;
+                }
+            } catch (Exception e) {
+                // TODO: don't consume exceptions here too
+                Log.e(TAG, "Failed to configure surface " + mSurfaceKind, e);
+            }
+        }
+
+        @Override
+        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+            // TODO: support resizeable display. We could actually change the display size that the
+            // VM sees, or keep the size and render it by fitting it in the new surface.
+        }
+
+        @Override
+        public void surfaceDestroyed(SurfaceHolder holder) {
+            try {
+                getDisplayService().removeSurface(isForCursor());
+            } catch (RemoteException e) {
+                throw new RuntimeException("Error while destroying surface for " + mSurfaceKind, e);
+            }
+        }
+    }
+
+    private ParcelFileDescriptor createNewCursorStream() {
+        if (mCursorHandler != null) {
+            mCursorHandler.interrupt();
+        }
+        ParcelFileDescriptor[] pfds;
+        try {
+            pfds = ParcelFileDescriptor.createSocketPair();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to create socketpair for cursor stream", e);
+        }
+        mCursorHandler = new CursorHandler(pfds[0]);
+        mCursorHandler.start();
+        return pfds[1];
+    }
+
+    /**
+     * Thread reading cursor coordinate from a stream, and updating the position of the cursor
+     * surface accordingly.
+     */
+    private class CursorHandler extends Thread {
+        private final ParcelFileDescriptor mStream;
+        private final SurfaceControl mCursor;
+        private final SurfaceControl.Transaction mTransaction;
+
+        CursorHandler(ParcelFileDescriptor stream) {
+            mStream = stream;
+            mCursor = DisplayProvider.this.mCursorView.getSurfaceControl();
+            mTransaction = new SurfaceControl.Transaction();
+
+            SurfaceControl main = DisplayProvider.this.mMainView.getSurfaceControl();
+            mTransaction.reparent(mCursor, main).apply();
+        }
+
+        @Override
+        public void run() {
+            try {
+                ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
+                byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
+                while (true) {
+                    if (Thread.interrupted()) {
+                        Log.d(TAG, "CursorHandler thread interrupted!");
+                        return;
+                    }
+                    byteBuffer.clear();
+                    int bytes =
+                            IoBridge.read(
+                                    mStream.getFileDescriptor(),
+                                    byteBuffer.array(),
+                                    0,
+                                    byteBuffer.array().length);
+                    if (bytes == -1) {
+                        Log.e(TAG, "cannot read from cursor stream, stop the handler");
+                        return;
+                    }
+                    float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
+                    mTransaction.setPosition(mCursor, x, y).apply();
+                }
+            } catch (IOException e) {
+                Log.e(TAG, "failed to run CursorHandler", e);
+            }
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java
new file mode 100644
index 0000000..1be362b
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/InputForwarder.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.hardware.input.InputManager;
+import android.os.Handler;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.View;
+
+/** Forwards input events (touch, mouse, ...) from Android to VM */
+class InputForwarder {
+    private static final String TAG = MainActivity.TAG;
+    private final Context mContext;
+    private final VirtualMachine mVirtualMachine;
+    private InputManager.InputDeviceListener mInputDeviceListener;
+
+    private boolean isTabletMode = false;
+
+    InputForwarder(
+            Context context,
+            VirtualMachine vm,
+            View touchReceiver,
+            View mouseReceiver,
+            View keyReceiver) {
+        mContext = context;
+        mVirtualMachine = vm;
+
+        VirtualMachineCustomImageConfig config = vm.getConfig().getCustomImageConfig();
+        if (config.useTouch()) {
+            setupTouchReceiver(touchReceiver);
+        }
+        if (config.useMouse() || config.useTrackpad()) {
+            setupMouseReceiver(mouseReceiver);
+        }
+        if (config.useKeyboard()) {
+            setupKeyReceiver(keyReceiver);
+        }
+        if (config.useSwitches()) {
+            // Any view's handler is fine.
+            setupTabletModeHandler(touchReceiver.getHandler());
+        }
+    }
+
+    void cleanUp() {
+        if (mInputDeviceListener != null) {
+            InputManager im = mContext.getSystemService(InputManager.class);
+            im.unregisterInputDeviceListener(mInputDeviceListener);
+            mInputDeviceListener = null;
+        }
+    }
+
+    private void setupTouchReceiver(View receiver) {
+        receiver.setOnTouchListener(
+                (v, event) -> {
+                    return mVirtualMachine.sendMultiTouchEvent(event);
+                });
+    }
+
+    private void setupMouseReceiver(View receiver) {
+        receiver.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
+        receiver.setOnCapturedPointerListener(
+                (v, event) -> {
+                    int eventSource = event.getSource();
+                    if ((eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0) {
+                        return mVirtualMachine.sendTrackpadEvent(event);
+                    }
+                    return mVirtualMachine.sendMouseEvent(event);
+                });
+    }
+
+    private void setupKeyReceiver(View receiver) {
+        receiver.setOnKeyListener(
+                (v, code, event) -> {
+                    // TODO: this is guest-os specific. It shouldn't be handled here.
+                    if (isVolumeKey(code)) {
+                        return false;
+                    }
+                    return mVirtualMachine.sendKeyEvent(event);
+                });
+    }
+
+    private static boolean isVolumeKey(int keyCode) {
+        return keyCode == KeyEvent.KEYCODE_VOLUME_UP
+                || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE;
+    }
+
+    private void setupTabletModeHandler(Handler handler) {
+        InputManager im = mContext.getSystemService(InputManager.class);
+        mInputDeviceListener =
+                new InputManager.InputDeviceListener() {
+                    @Override
+                    public void onInputDeviceAdded(int deviceId) {
+                        setTabletModeConditionally();
+                    }
+
+                    @Override
+                    public void onInputDeviceRemoved(int deviceId) {
+                        setTabletModeConditionally();
+                    }
+
+                    @Override
+                    public void onInputDeviceChanged(int deviceId) {
+                        setTabletModeConditionally();
+                    }
+                };
+        im.registerInputDeviceListener(mInputDeviceListener, handler);
+    }
+
+    private static boolean hasPhysicalKeyboard() {
+        for (int id : InputDevice.getDeviceIds()) {
+            InputDevice d = InputDevice.getDevice(id);
+            if (!d.isVirtual() && d.isEnabled() && d.isFullKeyboard()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void setTabletModeConditionally() {
+        boolean tabletModeNeeded = !hasPhysicalKeyboard();
+        if (tabletModeNeeded != isTabletMode) {
+            String mode = tabletModeNeeded ? "tablet mode" : "desktop mode";
+            Log.d(TAG, "switching to " + mode);
+            isTabletMode = tabletModeNeeded;
+            mVirtualMachine.sendTabletModeEvent(tabletModeNeeded);
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
new file mode 100644
index 0000000..e1cb285
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Logger.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Forwards VM's console output to a file on the Android side, and VM's log output to Android logd.
+ */
+class Logger {
+    private Logger() {}
+
+    static void setup(VirtualMachine vm, Path path, ExecutorService executor) {
+        if (vm.getConfig().getDebugLevel() != VirtualMachineConfig.DEBUG_LEVEL_FULL) {
+            return;
+        }
+
+        try {
+            InputStream console = vm.getConsoleOutput();
+            OutputStream file = Files.newOutputStream(path, StandardOpenOption.CREATE);
+            executor.submit(() -> Streams.copy(console, new LineBufferedOutputStream(file)));
+
+            InputStream log = vm.getLogOutput();
+            executor.submit(() -> writeToLogd(log, vm.getName()));
+        } catch (VirtualMachineException | IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static boolean writeToLogd(InputStream input, String vmName) throws IOException {
+        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
+        String line;
+        while ((line = reader.readLine()) != null && !Thread.interrupted()) {
+            Log.d(vmName, line);
+        }
+        // TODO: find out why javac complains when the return type of this method is void. It
+        // (incorrectly?) thinks that IOException should be caught inside the lambda.
+        return true;
+    }
+
+    private static class LineBufferedOutputStream extends BufferedOutputStream {
+        LineBufferedOutputStream(OutputStream out) {
+            super(out);
+        }
+
+        @Override
+        public void write(byte[] buf, int off, int len) throws IOException {
+            super.write(buf, off, len);
+            for (int i = 0; i < len; ++i) {
+                if (buf[off + i] == '\n') {
+                    flush();
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
new file mode 100644
index 0000000..fb75533
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends Activity {
+    static final String TAG = "VmLauncherApp";
+    // TODO: this path should be from outside of this activity
+    private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
+
+    private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
+
+    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
+    private static final String ACTION_VM_OPEN_URL = "android.virtualization.VM_OPEN_URL";
+
+    private ExecutorService mExecutorService;
+    private VirtualMachine mVirtualMachine;
+    private InputForwarder mInputForwarder;
+    private DisplayProvider mDisplayProvider;
+    private VmAgent mVmAgent;
+    private ClipboardHandler mClipboardHandler;
+    private OpenUrlHandler mOpenUrlHandler;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.d(TAG, "onCreate intent: " + getIntent());
+        checkAndRequestRecordAudioPermission();
+        mExecutorService = Executors.newCachedThreadPool();
+
+        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        VirtualMachineConfig config = json.toConfig(this);
+
+        Runner runner;
+        try {
+            runner = Runner.create(this, config);
+        } catch (VirtualMachineException e) {
+            throw new RuntimeException(e);
+        }
+        mVirtualMachine = runner.getVm();
+        runner.getExitStatus()
+                .thenAcceptAsync(
+                        success -> {
+                            setResult(success ? RESULT_OK : RESULT_CANCELED);
+                            finish();
+                        });
+
+        // Setup UI
+        setContentView(R.layout.activity_main);
+        SurfaceView mainView = findViewById(R.id.surface_view);
+        SurfaceView cursorView = findViewById(R.id.cursor_surface_view);
+        View touchView = findViewById(R.id.background_touch_view);
+        makeFullscreen();
+
+        // Connect the views to the VM
+        mInputForwarder = new InputForwarder(this, mVirtualMachine, touchView, mainView, mainView);
+        mDisplayProvider = new DisplayProvider(mainView, cursorView);
+
+        Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+        Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+        mVmAgent = new VmAgent(mVirtualMachine);
+        mClipboardHandler = new ClipboardHandler(this, mVmAgent);
+        mOpenUrlHandler = new OpenUrlHandler(mVmAgent);
+        handleIntent(getIntent());
+    }
+
+    private void makeFullscreen() {
+        Window w = getWindow();
+        w.setDecorFitsSystemWindows(false);
+        WindowInsetsController insetsCtrl = w.getInsetsController();
+        insetsCtrl.hide(WindowInsets.Type.systemBars());
+        insetsCtrl.setSystemBarsBehavior(
+                WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        mInputForwarder.setTabletModeConditionally();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+        mDisplayProvider.notifyDisplayIsGoingToInvisible();
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        try {
+            mVirtualMachine.suspend();
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "Failed to suspend VM" + e);
+        }
+    }
+
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        try {
+            mVirtualMachine.resume();
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "Failed to resume VM" + e);
+        }
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        mExecutorService.shutdownNow();
+        mInputForwarder.cleanUp();
+        mOpenUrlHandler.shutdown();
+        Log.d(TAG, "destroyed");
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+
+        // TODO: explain why we have to do this on every focus change
+        if (hasFocus) {
+            SurfaceView mainView = findViewById(R.id.surface_view);
+            mainView.requestPointerCapture();
+        }
+
+        // TODO: remove executor here. Let clipboard handler handle this.
+        mExecutorService.execute(
+                () -> {
+                    if (hasFocus) {
+                        mClipboardHandler.writeClipboardToVm();
+                    } else {
+                        mClipboardHandler.readClipboardFromVm();
+                    }
+                });
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        Log.d(TAG, "onNewIntent intent: " + intent);
+        handleIntent(intent);
+    }
+
+    private void handleIntent(Intent intent) {
+        if (ACTION_VM_OPEN_URL.equals(intent.getAction())) {
+            String url = intent.getStringExtra(Intent.EXTRA_TEXT);
+            if (url != null) {
+                mOpenUrlHandler.sendUrlToVm(url);
+            }
+        }
+    }
+
+    private void checkAndRequestRecordAudioPermission() {
+        if (getApplicationContext().checkSelfPermission(permission.RECORD_AUDIO)
+                != PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[] {permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION_REQUEST_CODE);
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
new file mode 100644
index 0000000..fb0c6bf
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/OpenUrlHandler.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.util.Log;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+class OpenUrlHandler {
+    private static final String TAG = MainActivity.TAG;
+
+    private final VmAgent mVmAgent;
+    private final ExecutorService mExecutorService;
+
+    OpenUrlHandler(VmAgent vmAgent) {
+        mVmAgent = vmAgent;
+        mExecutorService = Executors.newSingleThreadExecutor();
+    }
+
+    void shutdown() {
+        mExecutorService.shutdownNow();
+    }
+
+    void sendUrlToVm(String url) {
+        mExecutorService.execute(
+                () -> {
+                    try {
+                        mVmAgent.connect().sendData(VmAgent.OPEN_URL, url.getBytes());
+                        Log.d(TAG, "Successfully sent URL to the VM");
+                    } catch (InterruptedException | RuntimeException e) {
+                        Log.e(TAG, "Failed to send URL to the VM", e);
+                    }
+                });
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
new file mode 100644
index 0000000..a5f58fe
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/Runner.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineCallback;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.system.virtualmachine.VirtualMachineManager;
+import android.util.Log;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ForkJoinPool;
+
+/** Utility class for creating a VM and waiting for it to finish. */
+class Runner {
+    private static final String TAG = MainActivity.TAG;
+    private final VirtualMachine mVirtualMachine;
+    private final Callback mCallback;
+
+    private Runner(VirtualMachine vm, Callback cb) {
+        mVirtualMachine = vm;
+        mCallback = cb;
+    }
+
+    /** Create a virtual machine of the given config, under the given context. */
+    static Runner create(Context context, VirtualMachineConfig config)
+            throws VirtualMachineException {
+        // context may already be the app context, but calling this again is not harmful.
+        // See b/359439878 on why vmm should be obtained from the app context.
+        Context appContext = context.getApplicationContext();
+        VirtualMachineManager vmm = appContext.getSystemService(VirtualMachineManager.class);
+        VirtualMachineCustomImageConfig customConfig = config.getCustomImageConfig();
+        if (customConfig == null) {
+            throw new RuntimeException("CustomImageConfig is missing");
+        }
+
+        String name = customConfig.getName();
+        if (name == null || name.isEmpty()) {
+            throw new RuntimeException("Virtual machine's name is missing in the config");
+        }
+
+        VirtualMachine vm = vmm.getOrCreate(name, config);
+        try {
+            vm.setConfig(config);
+        } catch (VirtualMachineException e) {
+            vmm.delete(name);
+            vm = vmm.create(name, config);
+            Log.w(TAG, "Re-creating virtual machine (" + name + ")", e);
+        }
+
+        Callback cb = new Callback();
+        vm.setCallback(ForkJoinPool.commonPool(), cb);
+        vm.run();
+        return new Runner(vm, cb);
+    }
+
+    /** Give access to the underlying VirtualMachine object. */
+    VirtualMachine getVm() {
+        return mVirtualMachine;
+    }
+
+    /** Get future about VM's exit status. */
+    CompletableFuture<Boolean> getExitStatus() {
+        return mCallback.mFinishedSuccessfully;
+    }
+
+    private static class Callback implements VirtualMachineCallback {
+        final CompletableFuture<Boolean> mFinishedSuccessfully = new CompletableFuture<>();
+
+        @Override
+        public void onPayloadStarted(VirtualMachine vm) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onPayloadReady(VirtualMachine vm) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onPayloadFinished(VirtualMachine vm, int exitCode) {
+            // This event is only from Microdroid-based VM. Custom VM shouldn't emit this.
+        }
+
+        @Override
+        public void onError(VirtualMachine vm, int errorCode, String message) {
+            Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
+            mFinishedSuccessfully.complete(false);
+        }
+
+        @Override
+        public void onStopped(VirtualMachine vm, int reason) {
+            Log.d(TAG, "VM stopped. Reason: " + reason);
+            mFinishedSuccessfully.complete(true);
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
new file mode 100644
index 0000000..af1d298
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmAgent.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import libcore.io.Streams;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * Agent running in the VM. This class provides connection to the agent and ways to communicate with
+ * it.
+ */
+class VmAgent {
+    private static final String TAG = MainActivity.TAG;
+    private static final int DATA_SHARING_SERVICE_PORT = 3580;
+    private static final int HEADER_SIZE = 8; // size of the header
+    private static final int SIZE_OFFSET = 4; // offset of the size field in the header
+    private static final long RETRY_INTERVAL_MS = 1_000;
+
+    static final byte READ_CLIPBOARD_FROM_VM = 0;
+    static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
+    static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
+    static final byte OPEN_URL = 3;
+
+    private final VirtualMachine mVirtualMachine;
+
+    VmAgent(VirtualMachine vm) {
+        mVirtualMachine = vm;
+    }
+
+    /**
+     * Connects to the agent and returns the established communication channel. This can block.
+     *
+     * @throws InterruptedException If the current thread was interrupted
+     */
+    Connection connect() throws InterruptedException {
+        boolean shouldLog = true;
+        while (true) {
+            if (Thread.interrupted()) {
+                throw new InterruptedException();
+            }
+            try {
+                return new Connection(mVirtualMachine.connectVsock(DATA_SHARING_SERVICE_PORT));
+            } catch (VirtualMachineException e) {
+                if (shouldLog) {
+                    shouldLog = false;
+                    Log.d(TAG, "Still waiting for VM agent to start", e);
+                }
+            }
+            SystemClock.sleep(RETRY_INTERVAL_MS);
+        }
+    }
+
+    static class Data {
+        final int type;
+        final byte[] data;
+
+        Data(int type, byte[] data) {
+            this.type = type;
+            this.data = data;
+        }
+    }
+
+    /** Represents a connection to the agent */
+    class Connection {
+        private final ParcelFileDescriptor mConn;
+
+        private Connection(ParcelFileDescriptor conn) {
+            mConn = conn;
+        }
+
+        /** Send data of a given type. This can block. */
+        void sendData(byte type, byte[] data) {
+            // Byte 0: Data type
+            // Byte 1-3: Padding alignment & Reserved for other use cases in the future
+            // Byte 4-7: Data size of the payload
+            ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+            header.clear();
+            header.order(ByteOrder.LITTLE_ENDIAN);
+            header.put(0, type);
+            int dataSize = data == null ? 0 : data.length;
+            header.putInt(SIZE_OFFSET, dataSize);
+
+            try (OutputStream out = new FileOutputStream(mConn.getFileDescriptor())) {
+                out.write(header.array());
+                if (data != null) {
+                    out.write(data);
+                }
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to send message of type: " + type, e);
+            }
+        }
+
+        /** Read data from agent. This can block. */
+        Data readData() {
+            ByteBuffer header = ByteBuffer.allocate(HEADER_SIZE);
+            header.clear();
+            header.order(ByteOrder.LITTLE_ENDIAN);
+            byte[] data;
+
+            try (InputStream in = new FileInputStream(mConn.getFileDescriptor())) {
+                Streams.readFully(in, header.array());
+                byte type = header.get(0);
+                int dataSize = header.getInt(SIZE_OFFSET);
+                data = new byte[dataSize];
+                Streams.readFully(in, data);
+                return new Data(type, data);
+            } catch (IOException e) {
+                throw new RuntimeException("Failed to read data", e);
+            }
+        }
+
+        /** Convenient method for sending data and then reading response for it. This can block. */
+        Data sendAndReceive(byte type, byte[] data) {
+            sendData(type, data);
+            return readData();
+        }
+    }
+}
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
new file mode 100644
index 0000000..5e78f99
--- /dev/null
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.os.ResultReceiver;
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineException;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class VmLauncherService extends Service {
+    private static final String TAG = "VmLauncherService";
+    // TODO: this path should be from outside of this service
+    private static final String VM_CONFIG_PATH = "/data/local/tmp/vm_config.json";
+
+    private static final int RESULT_START = 0;
+    private static final int RESULT_STOP = 1;
+    private static final int RESULT_ERROR = 2;
+    private static final int RESULT_IPADDR = 3;
+    private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+    private ExecutorService mExecutorService;
+    private VirtualMachine mVirtualMachine;
+    private ResultReceiver mResultReceiver;
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return null;
+    }
+
+    private void startForeground() {
+        NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        NotificationChannel notificationChannel =
+                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+        notificationManager.createNotificationChannel(notificationChannel);
+        startForeground(
+                this.hashCode(),
+                new Notification.Builder(this, TAG)
+                        .setChannelId(TAG)
+                        .setSmallIcon(android.R.drawable.ic_dialog_info)
+                        .setContentText("A VM " + mVirtualMachine.getName() + " is running")
+                        .build());
+    }
+
+    @Override
+    public int onStartCommand(Intent intent, int flags, int startId) {
+        if (isVmRunning()) {
+            Log.d(TAG, "there is already the running VM instance");
+            return START_NOT_STICKY;
+        }
+        mExecutorService = Executors.newCachedThreadPool();
+
+        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        VirtualMachineConfig config = json.toConfig(this);
+
+        Runner runner;
+        try {
+            runner = Runner.create(this, config);
+        } catch (VirtualMachineException e) {
+            Log.e(TAG, "cannot create runner", e);
+            stopSelf();
+            return START_NOT_STICKY;
+        }
+        mVirtualMachine = runner.getVm();
+        mResultReceiver =
+                intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver.class);
+
+        runner.getExitStatus()
+                .thenAcceptAsync(
+                        success -> {
+                            if (mResultReceiver != null) {
+                                mResultReceiver.send(success ? RESULT_STOP : RESULT_ERROR, null);
+                            }
+                            if (!success) {
+                                stopSelf();
+                            }
+                        });
+        Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
+        Logger.setup(mVirtualMachine, logPath, mExecutorService);
+
+        startForeground();
+
+        mResultReceiver.send(RESULT_START, null);
+        if (config.getCustomImageConfig().useNetwork()) {
+            Handler handler = new Handler(Looper.getMainLooper());
+            gatherIpAddrFromVm(handler);
+        }
+        return START_NOT_STICKY;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        if (isVmRunning()) {
+            try {
+                mVirtualMachine.stop();
+                stopForeground(STOP_FOREGROUND_REMOVE);
+            } catch (VirtualMachineException e) {
+                Log.e(TAG, "failed to stop a VM instance", e);
+            }
+            mExecutorService.shutdownNow();
+            mExecutorService = null;
+            mVirtualMachine = null;
+        }
+    }
+
+    private boolean isVmRunning() {
+        return mVirtualMachine != null
+                && mVirtualMachine.getStatus() == VirtualMachine.STATUS_RUNNING;
+    }
+
+    // TODO(b/359523803): Use AVF API to get ip addr when it exists
+    private void gatherIpAddrFromVm(Handler handler) {
+        handler.postDelayed(
+                () -> {
+                    if (!isVmRunning()) {
+                        Log.d(TAG, "A virtual machine instance isn't running");
+                        return;
+                    }
+                    int INTERNAL_VSOCK_SERVER_PORT = 1024;
+                    try (ParcelFileDescriptor pfd =
+                            mVirtualMachine.connectVsock(INTERNAL_VSOCK_SERVER_PORT)) {
+                        try (BufferedReader input =
+                                new BufferedReader(
+                                        new InputStreamReader(
+                                                new FileInputStream(pfd.getFileDescriptor())))) {
+                            String vmIpAddr = input.readLine().strip();
+                            Bundle b = new Bundle();
+                            b.putString(KEY_VM_IP_ADDR, vmIpAddr);
+                            mResultReceiver.send(RESULT_IPADDR, b);
+                            return;
+                        } catch (IOException e) {
+                            Log.e(TAG, e.toString());
+                        }
+                    } catch (Exception e) {
+                        Log.e(TAG, e.toString());
+                    }
+                    gatherIpAddrFromVm(handler);
+                },
+                1000);
+    }
+}
diff --git a/android/VmLauncherApp/proguard.flags b/android/VmLauncherApp/proguard.flags
new file mode 100644
index 0000000..13ec24e
--- /dev/null
+++ b/android/VmLauncherApp/proguard.flags
@@ -0,0 +1,7 @@
+# Keep the no-args constructor of the deserialized class
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson {
+  <init>();
+}
+-keepclassmembers class com.android.virtualization.vmlauncher.ConfigJson$* {
+  <init>();
+}
diff --git a/vmlauncher_app/res/layout/activity_main.xml b/android/VmLauncherApp/res/layout/activity_main.xml
similarity index 100%
rename from vmlauncher_app/res/layout/activity_main.xml
rename to android/VmLauncherApp/res/layout/activity_main.xml
diff --git a/vmlauncher_app/res/values/themes.xml b/android/VmLauncherApp/res/values/themes.xml
similarity index 100%
rename from vmlauncher_app/res/values/themes.xml
rename to android/VmLauncherApp/res/values/themes.xml
diff --git a/java/Android.bp b/android/android.system.virtualmachine.res/Android.bp
similarity index 100%
rename from java/Android.bp
rename to android/android.system.virtualmachine.res/Android.bp
diff --git a/java/AndroidManifest.xml b/android/android.system.virtualmachine.res/AndroidManifest.xml
similarity index 100%
rename from java/AndroidManifest.xml
rename to android/android.system.virtualmachine.res/AndroidManifest.xml
diff --git a/java/AndroidManifestNext.xml b/android/android.system.virtualmachine.res/AndroidManifestNext.xml
similarity index 100%
rename from java/AndroidManifestNext.xml
rename to android/android.system.virtualmachine.res/AndroidManifestNext.xml
diff --git a/compos/verify/Android.bp b/android/compos_verify/Android.bp
similarity index 100%
rename from compos/verify/Android.bp
rename to android/compos_verify/Android.bp
diff --git a/compos/verify/native/Android.bp b/android/compos_verify/native/Android.bp
similarity index 100%
rename from compos/verify/native/Android.bp
rename to android/compos_verify/native/Android.bp
diff --git a/compos/verify/native/lib.rs b/android/compos_verify/native/lib.rs
similarity index 100%
rename from compos/verify/native/lib.rs
rename to android/compos_verify/native/lib.rs
diff --git a/compos/verify/native/verify_native.cpp b/android/compos_verify/native/verify_native.cpp
similarity index 100%
rename from compos/verify/native/verify_native.cpp
rename to android/compos_verify/native/verify_native.cpp
diff --git a/compos/verify/native/verify_native.h b/android/compos_verify/native/verify_native.h
similarity index 100%
rename from compos/verify/native/verify_native.h
rename to android/compos_verify/native/verify_native.h
diff --git a/compos/verify/verify.rs b/android/compos_verify/verify.rs
similarity index 100%
rename from compos/verify/verify.rs
rename to android/compos_verify/verify.rs
diff --git a/compos/composd/Android.bp b/android/composd/Android.bp
similarity index 100%
rename from compos/composd/Android.bp
rename to android/composd/Android.bp
diff --git a/compos/composd/aidl/Android.bp b/android/composd/aidl/Android.bp
similarity index 100%
rename from compos/composd/aidl/Android.bp
rename to android/composd/aidl/Android.bp
diff --git a/compos/composd/aidl/android/system/composd/ICompilationTask.aidl b/android/composd/aidl/android/system/composd/ICompilationTask.aidl
similarity index 100%
rename from compos/composd/aidl/android/system/composd/ICompilationTask.aidl
rename to android/composd/aidl/android/system/composd/ICompilationTask.aidl
diff --git a/compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl b/android/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
similarity index 100%
rename from compos/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
rename to android/composd/aidl/android/system/composd/ICompilationTaskCallback.aidl
diff --git a/compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl b/android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
similarity index 100%
rename from compos/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
rename to android/composd/aidl/android/system/composd/IIsolatedCompilationService.aidl
diff --git a/compos/composd/native/Android.bp b/android/composd/native/Android.bp
similarity index 100%
rename from compos/composd/native/Android.bp
rename to android/composd/native/Android.bp
diff --git a/compos/composd/native/lib.rs b/android/composd/native/lib.rs
similarity index 100%
rename from compos/composd/native/lib.rs
rename to android/composd/native/lib.rs
diff --git a/compos/composd/src/composd_main.rs b/android/composd/src/composd_main.rs
similarity index 100%
rename from compos/composd/src/composd_main.rs
rename to android/composd/src/composd_main.rs
diff --git a/compos/composd/src/fd_server_helper.rs b/android/composd/src/fd_server_helper.rs
similarity index 100%
rename from compos/composd/src/fd_server_helper.rs
rename to android/composd/src/fd_server_helper.rs
diff --git a/compos/composd/src/instance_manager.rs b/android/composd/src/instance_manager.rs
similarity index 100%
rename from compos/composd/src/instance_manager.rs
rename to android/composd/src/instance_manager.rs
diff --git a/compos/composd/src/instance_starter.rs b/android/composd/src/instance_starter.rs
similarity index 100%
rename from compos/composd/src/instance_starter.rs
rename to android/composd/src/instance_starter.rs
diff --git a/compos/composd/src/odrefresh_task.rs b/android/composd/src/odrefresh_task.rs
similarity index 100%
rename from compos/composd/src/odrefresh_task.rs
rename to android/composd/src/odrefresh_task.rs
diff --git a/compos/composd/src/service.rs b/android/composd/src/service.rs
similarity index 100%
rename from compos/composd/src/service.rs
rename to android/composd/src/service.rs
diff --git a/compos/composd_cmd/Android.bp b/android/composd_cmd/Android.bp
similarity index 100%
rename from compos/composd_cmd/Android.bp
rename to android/composd_cmd/Android.bp
diff --git a/compos/composd_cmd/composd_cmd.rs b/android/composd_cmd/composd_cmd.rs
similarity index 100%
rename from compos/composd_cmd/composd_cmd.rs
rename to android/composd_cmd/composd_cmd.rs
diff --git a/authfs/fd_server/Android.bp b/android/fd_server/Android.bp
similarity index 94%
rename from authfs/fd_server/Android.bp
rename to android/fd_server/Android.bp
index b02c104..32a8fec 100644
--- a/authfs/fd_server/Android.bp
+++ b/android/fd_server/Android.bp
@@ -18,6 +18,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "libsafe_ownedfd",
     ],
     prefer_rlib: true,
     apex_available: ["com.android.virt"],
@@ -39,6 +40,7 @@
         "liblog_rust",
         "libnix",
         "librpcbinder_rs",
+        "libsafe_ownedfd",
     ],
     prefer_rlib: true,
     test_suites: ["general-tests"],
diff --git a/authfs/TEST_MAPPING b/android/fd_server/TEST_MAPPING
similarity index 100%
copy from authfs/TEST_MAPPING
copy to android/fd_server/TEST_MAPPING
diff --git a/authfs/fd_server/src/aidl.rs b/android/fd_server/src/aidl.rs
similarity index 94%
rename from authfs/fd_server/src/aidl.rs
rename to android/fd_server/src/aidl.rs
index 5f91987..2f3697c 100644
--- a/authfs/fd_server/src/aidl.rs
+++ b/android/fd_server/src/aidl.rs
@@ -14,20 +14,21 @@
  * limitations under the License.
  */
 
-use anyhow::Result;
+use anyhow::{Context, Result};
 use log::error;
 use nix::{
     errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::fchmod, sys::stat::mkdirat,
     sys::stat::mode_t, sys::stat::Mode, sys::statvfs::statvfs, sys::statvfs::Statvfs,
     unistd::unlinkat, unistd::UnlinkatFlags,
 };
+use safe_ownedfd::take_fd_ownership;
 use std::cmp::min;
 use std::collections::{btree_map, BTreeMap};
 use std::convert::TryInto;
 use std::fs::File;
 use std::io;
 use std::os::unix::fs::FileExt;
-use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
 use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
 use std::sync::{Arc, RwLock};
 
@@ -38,7 +39,8 @@
     get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
 };
 use binder::{
-    BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
+    BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Status,
+    StatusCode, Strong,
 };
 
 /// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
@@ -299,9 +301,11 @@
                     mode,
                 )
                 .map_err(new_errno_error)?;
-                // SAFETY: new_fd is just created and not an error.
-                let new_file = unsafe { File::from_raw_fd(new_fd) };
-                Ok((new_fd, FdConfig::ReadWrite(new_file)))
+                let new_fd = take_fd_ownership(new_fd)
+                    .context("Failed to take ownership of fd for file")
+                    .or_service_specific_exception(-1)?;
+                let new_file = File::from(new_fd);
+                Ok((new_file.as_raw_fd(), FdConfig::ReadWrite(new_file)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -327,9 +331,10 @@
                     Mode::empty(),
                 )
                 .map_err(new_errno_error)?;
-                // SAFETY: new_dir_fd is just created and not an error.
-                let fd_owner = unsafe { OwnedFd::from_raw_fd(new_dir_fd) };
-                Ok((new_dir_fd, FdConfig::OutputDir(fd_owner)))
+                let fd_owner = take_fd_ownership(new_dir_fd)
+                    .context("Failed to take ownership of the fd for directory")
+                    .or_service_specific_exception(-1)?;
+                Ok((fd_owner.as_raw_fd(), FdConfig::OutputDir(fd_owner)))
             }
             _ => Err(new_errno_error(Errno::ENOTDIR)),
         })
@@ -408,9 +413,11 @@
 
 fn open_readonly_at(dir_fd: BorrowedFd, path: &Path) -> nix::Result<File> {
     let new_fd = openat(Some(dir_fd.as_raw_fd()), path, OFlag::O_RDONLY, Mode::empty())?;
-    // SAFETY: new_fd is just created successfully and not owned.
-    let new_file = unsafe { File::from_raw_fd(new_fd) };
-    Ok(new_file)
+    let new_fd = take_fd_ownership(new_fd).map_err(|e| match e {
+        safe_ownedfd::Error::Errno(e) => e,
+        _ => Errno::UnknownErrno,
+    })?;
+    Ok(File::from(new_fd))
 }
 
 fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
diff --git a/authfs/fd_server/src/main.rs b/android/fd_server/src/main.rs
similarity index 100%
rename from authfs/fd_server/src/main.rs
rename to android/fd_server/src/main.rs
diff --git a/virtualizationmanager/Android.bp b/android/virtmgr/Android.bp
similarity index 93%
rename from virtualizationmanager/Android.bp
rename to android/virtmgr/Android.bp
index ae85934..4828057 100644
--- a/virtualizationmanager/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -55,6 +55,7 @@
         "libregex",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libsemver",
         "libselinux_bindgen",
         "libserde",
@@ -87,6 +88,14 @@
     apex_available: ["com.android.virt"],
 }
 
+rust_binary {
+    name: "early_virtmgr",
+    defaults: ["virtualizationmanager_defaults"],
+    srcs: ["src/main.rs"],
+    cfgs: ["early"],
+    apex_available: ["com.android.virt"],
+}
+
 rust_test {
     name: "virtualizationmanager_device_test",
     srcs: ["src/main.rs"],
diff --git a/virtualizationmanager/TEST_MAPPING b/android/virtmgr/TEST_MAPPING
similarity index 100%
rename from virtualizationmanager/TEST_MAPPING
rename to android/virtmgr/TEST_MAPPING
diff --git a/virtualizationmanager/fsfdt/Android.bp b/android/virtmgr/fsfdt/Android.bp
similarity index 100%
rename from virtualizationmanager/fsfdt/Android.bp
rename to android/virtmgr/fsfdt/Android.bp
diff --git a/virtualizationmanager/fsfdt/src/lib.rs b/android/virtmgr/fsfdt/src/lib.rs
similarity index 98%
rename from virtualizationmanager/fsfdt/src/lib.rs
rename to android/virtmgr/fsfdt/src/lib.rs
index e176b7b..ff15efa 100644
--- a/virtualizationmanager/fsfdt/src/lib.rs
+++ b/android/virtmgr/fsfdt/src/lib.rs
@@ -76,7 +76,7 @@
                     stack.push(entry.path());
                     subnode_names.push(name);
                 } else if entry_type.is_file() {
-                    let value = fs::read(&entry.path())?;
+                    let value = fs::read(entry.path())?;
 
                     node.setprop(&name, &value)
                         .map_err(|e| anyhow!("Failed to set FDT property, {e:?}"))?;
diff --git a/virtualizationmanager/fsfdt/src/main.rs b/android/virtmgr/fsfdt/src/main.rs
similarity index 100%
rename from virtualizationmanager/fsfdt/src/main.rs
rename to android/virtmgr/fsfdt/src/main.rs
diff --git a/virtualizationmanager/fsfdt/testdata/fs/avf/reference/oem/stub b/android/virtmgr/fsfdt/testdata/fs/avf/reference/oem/stub
similarity index 100%
rename from virtualizationmanager/fsfdt/testdata/fs/avf/reference/oem/stub
rename to android/virtmgr/fsfdt/testdata/fs/avf/reference/oem/stub
Binary files differ
diff --git a/virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor/empty b/android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor/empty
similarity index 100%
rename from virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor/empty
rename to android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor/empty
diff --git a/virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor/vendor_extra_node/flag b/android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor/vendor_extra_node/flag
similarity index 100%
rename from virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor/vendor_extra_node/flag
rename to android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor/vendor_extra_node/flag
Binary files differ
diff --git a/virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor_hashtree_descriptor_root_digest b/android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor_hashtree_descriptor_root_digest
similarity index 100%
rename from virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor_hashtree_descriptor_root_digest
rename to android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor_hashtree_descriptor_root_digest
Binary files differ
diff --git a/virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor_image_key b/android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor_image_key
similarity index 100%
rename from virtualizationmanager/fsfdt/testdata/fs/avf/reference/vendor_image_key
rename to android/virtmgr/fsfdt/testdata/fs/avf/reference/vendor_image_key
Binary files differ
diff --git a/virtualizationmanager/fuzzer.cpp b/android/virtmgr/fuzzer.cpp
similarity index 100%
rename from virtualizationmanager/fuzzer.cpp
rename to android/virtmgr/fuzzer.cpp
diff --git a/virtualizationmanager/src/aidl.rs b/android/virtmgr/src/aidl.rs
similarity index 98%
rename from virtualizationmanager/src/aidl.rs
rename to android/virtmgr/src/aidl.rs
index f1bfd8c..7a357f3 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -73,6 +73,7 @@
 use nix::unistd::pipe;
 use rpcbinder::RpcServer;
 use rustutils::system_properties;
+use safe_ownedfd::take_fd_ownership;
 use semver::VersionReq;
 use std::collections::HashSet;
 use std::convert::TryInto;
@@ -82,7 +83,7 @@
 use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
 use std::iter;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
+use std::os::unix::io::{AsRawFd, IntoRawFd};
 use std::os::unix::raw::pid_t;
 use std::path::{Path, PathBuf};
 use std::sync::{Arc, Mutex, Weak};
@@ -411,9 +412,9 @@
 
         let state = &mut *self.state.lock().unwrap();
         let console_out_fd =
-            clone_or_prepare_logger_fd(&debug_config, console_out_fd, format!("Console({})", cid))?;
+            clone_or_prepare_logger_fd(console_out_fd, format!("Console({})", cid))?;
         let console_in_fd = console_in_fd.map(clone_file).transpose()?;
-        let log_fd = clone_or_prepare_logger_fd(&debug_config, log_fd, format!("Log({})", cid))?;
+        let log_fd = clone_or_prepare_logger_fd(log_fd, format!("Log({})", cid))?;
 
         // Counter to generate unique IDs for temporary image files.
         let mut next_temporary_image_id = 0;
@@ -1274,7 +1275,7 @@
         let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
             .context("Failed to connect")
             .or_service_specific_exception(-1)?;
-        Ok(vsock_stream_to_pfd(stream))
+        vsock_stream_to_pfd(stream)
     }
 
     fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
@@ -1433,10 +1434,12 @@
 }
 
 /// Converts a `VsockStream` to a `ParcelFileDescriptor`.
-fn vsock_stream_to_pfd(stream: VsockStream) -> ParcelFileDescriptor {
-    // SAFETY: ownership is transferred from stream to f
-    let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
-    ParcelFileDescriptor::new(f)
+fn vsock_stream_to_pfd(stream: VsockStream) -> binder::Result<ParcelFileDescriptor> {
+    let owned_fd = take_fd_ownership(stream.into_raw_fd())
+        .context("Failed to take ownership of the vsock stream")
+        .with_log()
+        .or_service_specific_exception(-1)?;
+    Ok(ParcelFileDescriptor::new(owned_fd))
 }
 
 /// Parses the platform version requirement string.
@@ -1563,7 +1566,6 @@
 }
 
 fn clone_or_prepare_logger_fd(
-    debug_config: &DebugConfig,
     fd: Option<&ParcelFileDescriptor>,
     tag: String,
 ) -> Result<Option<File>, Status> {
@@ -1571,10 +1573,6 @@
         return Ok(Some(clone_file(fd)?));
     }
 
-    if !debug_config.should_prepare_console_output() {
-        return Ok(None);
-    };
-
     let (read_fd, write_fd) =
         pipe().context("Failed to create pipe").or_service_specific_exception(-1)?;
 
diff --git a/virtualizationmanager/src/atom.rs b/android/virtmgr/src/atom.rs
similarity index 100%
rename from virtualizationmanager/src/atom.rs
rename to android/virtmgr/src/atom.rs
diff --git a/virtualizationmanager/src/composite.rs b/android/virtmgr/src/composite.rs
similarity index 100%
rename from virtualizationmanager/src/composite.rs
rename to android/virtmgr/src/composite.rs
diff --git a/virtualizationmanager/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
similarity index 95%
rename from virtualizationmanager/src/crosvm.rs
rename to android/virtmgr/src/crosvm.rs
index 4d19670..08a9e47 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -35,7 +35,7 @@
 use std::io::{self, Read};
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{AsRawFd, OwnedFd};
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -57,7 +57,6 @@
 use rpcbinder::RpcServer;
 
 /// external/crosvm
-use base::AsRawDescriptor;
 use base::UnixSeqpacketListener;
 use vm_control::{BalloonControlCommand, VmRequest, VmResponse};
 
@@ -369,6 +368,7 @@
     /// Callbacks to clients of the VM.
     pub callbacks: VirtualMachineCallbacks,
     /// VirtualMachineService binder object for the VM.
+    #[allow(dead_code)]
     pub vm_service: Mutex<Option<Strong<dyn IVirtualMachineService>>>,
     /// Recorded metrics of VM such as timestamp or cpu / memory usage.
     pub vm_metric: Mutex<VmMetric>,
@@ -871,26 +871,6 @@
     }
 }
 
-fn append_platform_devices(
-    command: &mut Command,
-    preserved_fds: &mut Vec<RawFd>,
-    config: &CrosvmConfig,
-) -> Result<(), Error> {
-    if config.vfio_devices.is_empty() {
-        return Ok(());
-    }
-
-    let Some(dtbo) = &config.dtbo else {
-        bail!("VFIO devices assigned but no DTBO available");
-    };
-    command.arg(format!("--device-tree-overlay={},filter", add_preserved_fd(preserved_fds, dtbo)));
-
-    for device in &config.vfio_devices {
-        command.arg(vfio_argument_for_platform_device(device)?);
-    }
-    Ok(())
-}
-
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -985,7 +965,7 @@
     }
 
     // Keep track of what file descriptors should be mapped to the crosvm process.
-    let mut preserved_fds = config.indirect_files.iter().map(|file| file.as_raw_fd()).collect();
+    let mut preserved_fds = config.indirect_files.into_iter().map(|f| f.into()).collect();
 
     // Setup the serial devices.
     // 1. uart device: used as the output device by bootloaders and as early console by linux
@@ -996,15 +976,14 @@
     //
     // When [console|log]_fd is not specified, the devices are attached to sink, which means what's
     // written there is discarded.
-    let console_out_arg = format_serial_out_arg(&mut preserved_fds, &config.console_out_fd);
+    let console_out_arg = format_serial_out_arg(&mut preserved_fds, config.console_out_fd);
     let console_in_arg = config
         .console_in_fd
-        .as_ref()
         .map(|fd| format!(",input={}", add_preserved_fd(&mut preserved_fds, fd)))
         .unwrap_or_default();
-    let log_arg = format_serial_out_arg(&mut preserved_fds, &config.log_fd);
-    let failure_serial_path = add_preserved_fd(&mut preserved_fds, &failure_pipe_write);
-    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, &config.ramdump);
+    let log_arg = format_serial_out_arg(&mut preserved_fds, config.log_fd);
+    let failure_serial_path = add_preserved_fd(&mut preserved_fds, failure_pipe_write);
+    let ramdump_arg = format_serial_out_arg(&mut preserved_fds, config.ramdump);
     let console_input_device = config.console_input_device.as_deref().unwrap_or(CONSOLE_HVC0);
     match console_input_device {
         CONSOLE_HVC0 | CONSOLE_TTYS0 => {}
@@ -1034,11 +1013,11 @@
     // /dev/hvc2
     command.arg(format!("--serial={},hardware=virtio-console,num=3", &log_arg));
 
-    if let Some(bootloader) = &config.bootloader {
+    if let Some(bootloader) = config.bootloader {
         command.arg("--bios").arg(add_preserved_fd(&mut preserved_fds, bootloader));
     }
 
-    if let Some(initrd) = &config.initrd {
+    if let Some(initrd) = config.initrd {
         command.arg("--initrd").arg(add_preserved_fd(&mut preserved_fds, initrd));
     }
 
@@ -1046,25 +1025,23 @@
         command.arg("--params").arg(params);
     }
 
-    for disk in &config.disks {
+    for disk in config.disks {
         command.arg("--block").arg(format!(
             "path={},ro={}",
-            add_preserved_fd(&mut preserved_fds, &disk.image),
+            add_preserved_fd(&mut preserved_fds, disk.image),
             !disk.writable,
         ));
     }
 
-    if let Some(kernel) = &config.kernel {
+    if let Some(kernel) = config.kernel {
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
-    let control_server_socket = UnixSeqpacketListener::bind(crosvm_control_socket_path)
+    let control_sock = UnixSeqpacketListener::bind(crosvm_control_socket_path)
         .context("failed to create control server")?;
-    command
-        .arg("--socket")
-        .arg(add_preserved_fd(&mut preserved_fds, &control_server_socket.as_raw_descriptor()));
+    command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
 
-    if let Some(dt_overlay) = &config.device_tree_overlay {
+    if let Some(dt_overlay) = config.device_tree_overlay {
         command.arg("--device-tree-overlay").arg(add_preserved_fd(&mut preserved_fds, dt_overlay));
     }
 
@@ -1115,15 +1092,15 @@
     }
 
     if cfg!(network) {
-        if let Some(tap) = &config.tap {
-            let tap_fd = tap.as_raw_fd();
-            preserved_fds.push(tap_fd);
-            command.arg("--net").arg(format!("tap-fd={}", tap_fd));
+        if let Some(tap) = config.tap {
+            add_preserved_fd(&mut preserved_fds, tap);
+            let tap_fd = preserved_fds.last().unwrap().as_raw_fd();
+            command.arg("--net").arg(format!("tap-fd={tap_fd}"));
         }
     }
 
     if cfg!(paravirtualized_devices) {
-        for input_device_option in config.input_device_options.iter() {
+        for input_device_option in config.input_device_options.into_iter() {
             command.arg("--input");
             command.arg(match input_device_option {
                 InputDeviceOption::EvDev(file) => {
@@ -1171,7 +1148,19 @@
         command.arg("--boost-uclamp");
     }
 
-    append_platform_devices(&mut command, &mut preserved_fds, &config)?;
+    if !config.vfio_devices.is_empty() {
+        if let Some(dtbo) = config.dtbo {
+            command.arg(format!(
+                "--device-tree-overlay={},filter",
+                add_preserved_fd(&mut preserved_fds, dtbo)
+            ));
+        } else {
+            bail!("VFIO devices assigned but no DTBO available");
+        }
+    };
+    for device in config.vfio_devices {
+        command.arg(vfio_argument_for_platform_device(&device)?);
+    }
 
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
@@ -1241,15 +1230,16 @@
 
 /// Adds the file descriptor for `file` to `preserved_fds`, and returns a string of the form
 /// "/proc/self/fd/N" where N is the file descriptor.
-fn add_preserved_fd(preserved_fds: &mut Vec<RawFd>, file: &dyn AsRawFd) -> String {
-    let fd = file.as_raw_fd();
+fn add_preserved_fd<F: Into<OwnedFd>>(preserved_fds: &mut Vec<OwnedFd>, file: F) -> String {
+    let fd = file.into();
+    let raw_fd = fd.as_raw_fd();
     preserved_fds.push(fd);
-    format!("/proc/self/fd/{}", fd)
+    format!("/proc/self/fd/{}", raw_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_out_arg(preserved_fds: &mut Vec<RawFd>, file: &Option<File>) -> String {
+fn format_serial_out_arg(preserved_fds: &mut Vec<OwnedFd>, file: Option<File>) -> String {
     if let Some(file) = file {
         format!("type=file,path={}", add_preserved_fd(preserved_fds, file))
     } else {
diff --git a/virtualizationmanager/src/debug_config.rs b/android/virtmgr/src/debug_config.rs
similarity index 100%
rename from virtualizationmanager/src/debug_config.rs
rename to android/virtmgr/src/debug_config.rs
diff --git a/virtualizationmanager/src/dt_overlay.rs b/android/virtmgr/src/dt_overlay.rs
similarity index 100%
rename from virtualizationmanager/src/dt_overlay.rs
rename to android/virtmgr/src/dt_overlay.rs
diff --git a/virtualizationmanager/src/main.rs b/android/virtmgr/src/main.rs
similarity index 68%
rename from virtualizationmanager/src/main.rs
rename to android/virtmgr/src/main.rs
index 4e88507..7d29685 100644
--- a/virtualizationmanager/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -25,16 +25,16 @@
 
 use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, Result};
 use binder::{BinderFeatures, ProcessState};
 use lazy_static::lazy_static;
 use log::{info, LevelFilter};
 use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{AsFd, RawFd};
 use clap::Parser;
-use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
 use nix::unistd::{write, Pid, Uid};
 use std::os::unix::raw::{pid_t, uid_t};
+use safe_ownedfd::take_fd_ownership;
 
 const LOG_TAG: &str = "virtmgr";
 
@@ -73,32 +73,6 @@
     ready_fd: RawFd,
 }
 
-fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
-    // Basic check that the integer value does correspond to a file descriptor.
-    fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
-
-    // The file descriptor had CLOEXEC disabled to be inherited from the parent.
-    // Re-enable it to make sure it is not accidentally inherited further.
-    fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
-        .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-
-    // Reject RawFds that already have a corresponding OwnedFd.
-    if owned_fds.contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} already owned");
-    }
-    owned_fds.push(raw_fd);
-
-    // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
-    // We checked that the integer value corresponds to a valid FD and that this
-    // is the first argument to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
-}
-
 fn check_vm_support() -> Result<()> {
     if hypervisor_props::is_any_vm_supported()? {
         Ok(())
@@ -122,16 +96,18 @@
 
     let args = Args::parse();
 
-    let mut owned_fds = vec![];
-    let rpc_server_fd = take_fd_ownership(args.rpc_server_fd, &mut owned_fds)
-        .expect("Failed to take ownership of rpc_server_fd");
-    let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
-        .expect("Failed to take ownership of ready_fd");
+    let rpc_server_fd =
+        take_fd_ownership(args.rpc_server_fd).expect("Failed to take ownership of rpc_server_fd");
+    let ready_fd = take_fd_ownership(args.ready_fd).expect("Failed to take ownership of ready_fd");
 
     // Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
     ProcessState::start_thread_pool();
 
-    GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
+    if cfg!(early) {
+        panic!("Early VM not implemented");
+    } else {
+        GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
+    }
 
     let service = VirtualizationService::init();
     let service =
diff --git a/virtualizationmanager/src/payload.rs b/android/virtmgr/src/payload.rs
similarity index 100%
rename from virtualizationmanager/src/payload.rs
rename to android/virtmgr/src/payload.rs
diff --git a/virtualizationmanager/src/selinux.rs b/android/virtmgr/src/selinux.rs
similarity index 100%
rename from virtualizationmanager/src/selinux.rs
rename to android/virtmgr/src/selinux.rs
diff --git a/virtualizationservice/Android.bp b/android/virtualizationservice/Android.bp
similarity index 100%
rename from virtualizationservice/Android.bp
rename to android/virtualizationservice/Android.bp
diff --git a/virtualizationservice/TEST_MAPPING b/android/virtualizationservice/TEST_MAPPING
similarity index 100%
rename from virtualizationservice/TEST_MAPPING
rename to android/virtualizationservice/TEST_MAPPING
diff --git a/virtualizationservice/aidl/Android.bp b/android/virtualizationservice/aidl/Android.bp
similarity index 97%
rename from virtualizationservice/aidl/Android.bp
rename to android/virtualizationservice/aidl/Android.bp
index bca4512..c1bff5e 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/android/virtualizationservice/aidl/Android.bp
@@ -31,6 +31,7 @@
             apex_available: [
                 "com.android.virt",
                 "com.android.compos",
+                "com.android.microfuchsia",
             ],
         },
     },
@@ -150,6 +151,7 @@
             apex_available: [
                 "com.android.virt",
                 "com.android.compos",
+                "com.android.microfuchsia",
             ],
         },
     },
diff --git a/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl b/android/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationcommon/Certificate.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationcommon/DeathReason.aidl b/android/virtualizationservice/aidl/android/system/virtualizationcommon/DeathReason.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationcommon/DeathReason.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationcommon/DeathReason.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationcommon/ErrorCode.aidl b/android/virtualizationservice/aidl/android/system/virtualizationcommon/ErrorCode.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationcommon/ErrorCode.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationcommon/ErrorCode.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl b/android/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl b/android/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/AssignableDevice.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/AudioConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/AudioConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/AudioConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/AudioConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/DiskImage.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/DisplayConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/GpuConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachine.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualMachineCallback.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/InputDevice.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/Partition.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/PartitionType.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/PartitionType.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/PartitionType.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/PartitionType.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineDebugInfo.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachinePayloadConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineState.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmBooted.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmBooted.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmBooted.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmBooted.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmCreationRequested.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmCreationRequested.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmCreationRequested.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmCreationRequested.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmExited.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmExited.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmExited.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/AtomVmExited.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IBoundDevice.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IBoundDevice.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/IBoundDevice.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IBoundDevice.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IGlobalVmContext.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVfioHandler.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
rename to android/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVmnic.aidl
diff --git a/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl b/android/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
rename to android/virtualizationservice/aidl/android/system/virtualmachineservice/IVirtualMachineService.aidl
diff --git a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl b/android/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
similarity index 100%
rename from virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
rename to android/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
diff --git a/virtualizationservice/assignable_devices.xsd b/android/virtualizationservice/assignable_devices.xsd
similarity index 100%
rename from virtualizationservice/assignable_devices.xsd
rename to android/virtualizationservice/assignable_devices.xsd
diff --git a/virtualizationservice/schema/current.txt b/android/virtualizationservice/schema/current.txt
similarity index 100%
rename from virtualizationservice/schema/current.txt
rename to android/virtualizationservice/schema/current.txt
diff --git a/virtualizationservice/schema/last_current.txt b/android/virtualizationservice/schema/last_current.txt
similarity index 100%
rename from virtualizationservice/schema/last_current.txt
rename to android/virtualizationservice/schema/last_current.txt
diff --git a/virtualizationservice/schema/last_removed.txt b/android/virtualizationservice/schema/last_removed.txt
similarity index 100%
rename from virtualizationservice/schema/last_removed.txt
rename to android/virtualizationservice/schema/last_removed.txt
diff --git a/virtualizationservice/schema/removed.txt b/android/virtualizationservice/schema/removed.txt
similarity index 100%
rename from virtualizationservice/schema/removed.txt
rename to android/virtualizationservice/schema/removed.txt
diff --git a/virtualizationservice/src/aidl.rs b/android/virtualizationservice/src/aidl.rs
similarity index 100%
rename from virtualizationservice/src/aidl.rs
rename to android/virtualizationservice/src/aidl.rs
diff --git a/virtualizationservice/src/atom.rs b/android/virtualizationservice/src/atom.rs
similarity index 100%
rename from virtualizationservice/src/atom.rs
rename to android/virtualizationservice/src/atom.rs
diff --git a/virtualizationservice/src/main.rs b/android/virtualizationservice/src/main.rs
similarity index 100%
rename from virtualizationservice/src/main.rs
rename to android/virtualizationservice/src/main.rs
diff --git a/virtualizationservice/src/maintenance.rs b/android/virtualizationservice/src/maintenance.rs
similarity index 100%
rename from virtualizationservice/src/maintenance.rs
rename to android/virtualizationservice/src/maintenance.rs
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/android/virtualizationservice/src/maintenance/vmdb.rs
similarity index 100%
rename from virtualizationservice/src/maintenance/vmdb.rs
rename to android/virtualizationservice/src/maintenance/vmdb.rs
diff --git a/virtualizationservice/src/remote_provisioning.rs b/android/virtualizationservice/src/remote_provisioning.rs
similarity index 100%
rename from virtualizationservice/src/remote_provisioning.rs
rename to android/virtualizationservice/src/remote_provisioning.rs
diff --git a/virtualizationservice/src/rkpvm.rs b/android/virtualizationservice/src/rkpvm.rs
similarity index 100%
rename from virtualizationservice/src/rkpvm.rs
rename to android/virtualizationservice/src/rkpvm.rs
diff --git a/virtualizationservice/testdata/rkp_cert_chain.der b/android/virtualizationservice/testdata/rkp_cert_chain.der
similarity index 100%
rename from virtualizationservice/testdata/rkp_cert_chain.der
rename to android/virtualizationservice/testdata/rkp_cert_chain.der
Binary files differ
diff --git a/virtualizationservice/vfio_handler/Android.bp b/android/virtualizationservice/vfio_handler/Android.bp
similarity index 100%
rename from virtualizationservice/vfio_handler/Android.bp
rename to android/virtualizationservice/vfio_handler/Android.bp
diff --git a/virtualizationservice/vfio_handler/src/aidl.rs b/android/virtualizationservice/vfio_handler/src/aidl.rs
similarity index 100%
rename from virtualizationservice/vfio_handler/src/aidl.rs
rename to android/virtualizationservice/vfio_handler/src/aidl.rs
diff --git a/virtualizationservice/vfio_handler/src/main.rs b/android/virtualizationservice/vfio_handler/src/main.rs
similarity index 100%
rename from virtualizationservice/vfio_handler/src/main.rs
rename to android/virtualizationservice/vfio_handler/src/main.rs
diff --git a/virtualizationservice/vmnic/Android.bp b/android/virtualizationservice/vmnic/Android.bp
similarity index 100%
rename from virtualizationservice/vmnic/Android.bp
rename to android/virtualizationservice/vmnic/Android.bp
diff --git a/virtualizationservice/vmnic/src/aidl.rs b/android/virtualizationservice/vmnic/src/aidl.rs
similarity index 100%
rename from virtualizationservice/vmnic/src/aidl.rs
rename to android/virtualizationservice/vmnic/src/aidl.rs
diff --git a/virtualizationservice/vmnic/src/main.rs b/android/virtualizationservice/vmnic/src/main.rs
similarity index 100%
rename from virtualizationservice/vmnic/src/main.rs
rename to android/virtualizationservice/vmnic/src/main.rs
diff --git a/vm/Android.bp b/android/vm/Android.bp
similarity index 100%
rename from vm/Android.bp
rename to android/vm/Android.bp
diff --git a/vm/TEST_MAPPING b/android/vm/TEST_MAPPING
similarity index 100%
rename from vm/TEST_MAPPING
rename to android/vm/TEST_MAPPING
diff --git a/vm/src/create_idsig.rs b/android/vm/src/create_idsig.rs
similarity index 100%
rename from vm/src/create_idsig.rs
rename to android/vm/src/create_idsig.rs
diff --git a/vm/src/create_partition.rs b/android/vm/src/create_partition.rs
similarity index 100%
rename from vm/src/create_partition.rs
rename to android/vm/src/create_partition.rs
diff --git a/vm/src/main.rs b/android/vm/src/main.rs
similarity index 100%
rename from vm/src/main.rs
rename to android/vm/src/main.rs
diff --git a/vm/src/run.rs b/android/vm/src/run.rs
similarity index 95%
rename from vm/src/run.rs
rename to android/vm/src/run.rs
index cb15802..b3743ae 100644
--- a/vm/src/run.rs
+++ b/android/vm/src/run.rs
@@ -36,7 +36,7 @@
 use std::fs::File;
 use std::io;
 use std::io::{Read, Write};
-use std::os::unix::io::{AsRawFd, FromRawFd};
+use std::os::fd::AsFd;
 use std::path::{Path, PathBuf};
 use vmclient::{ErrorCode, VmInstance};
 use vmconfig::{get_debug_level, open_parcel_file, VmConfig};
@@ -365,16 +365,6 @@
 }
 
 /// Safely duplicate the file descriptor.
-fn duplicate_fd<T: AsRawFd>(file: T) -> io::Result<File> {
-    let fd = file.as_raw_fd();
-    // SAFETY: This just duplicates a file descriptor which we know to be valid, and we check for an
-    // an error.
-    let dup_fd = unsafe { libc::dup(fd) };
-    if dup_fd < 0 {
-        Err(io::Error::last_os_error())
-    } else {
-        // SAFETY: We have just duplicated the file descriptor so we own it, and `from_raw_fd` takes
-        // ownership of it.
-        Ok(unsafe { File::from_raw_fd(dup_fd) })
-    }
+fn duplicate_fd<T: AsFd>(file: T) -> io::Result<File> {
+    Ok(file.as_fd().try_clone_to_owned()?.into())
 }
diff --git a/vm/vm_shell.sh b/android/vm/vm_shell.sh
similarity index 100%
rename from vm/vm_shell.sh
rename to android/vm/vm_shell.sh
diff --git a/demo_native/Android.bp b/android/vm_demo_native/Android.bp
similarity index 100%
rename from demo_native/Android.bp
rename to android/vm_demo_native/Android.bp
diff --git a/demo_native/README.md b/android/vm_demo_native/README.md
similarity index 97%
rename from demo_native/README.md
rename to android/vm_demo_native/README.md
index 700ca83..4bbbc28 100644
--- a/demo_native/README.md
+++ b/android/vm_demo_native/README.md
@@ -8,7 +8,7 @@
 like creating and using a VM from a HAL process.
 
 For non-system-level VMs, you must use the Java APIs from an Android app. See the [Java demo
-app](../demo/README.md).
+app](../MicrodroidDemoApp/README.md).
 
 ## Building
 
diff --git a/demo_native/main.cpp b/android/vm_demo_native/main.cpp
similarity index 100%
rename from demo_native/main.cpp
rename to android/vm_demo_native/main.cpp
diff --git a/apex b/apex
new file mode 120000
index 0000000..db9c327
--- /dev/null
+++ b/apex
@@ -0,0 +1 @@
+build/apex
\ No newline at end of file
diff --git a/build/Android.bp b/build/Android.bp
new file mode 100644
index 0000000..66cc626
--- /dev/null
+++ b/build/Android.bp
@@ -0,0 +1,88 @@
+//
+// Copyright (C) 2021 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 {
+    default_team: "trendy_team_virtualization",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "avf_build_flags_rust",
+    cfgs: select(release_flag("RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT"), {
+        true: ["device_assignment"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_DICE_CHANGES"), {
+        true: ["dice_changes"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_LLPVM_CHANGES"), {
+        true: ["llpvm_changes"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM"), {
+        true: ["multi_tenant"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_NETWORK"), {
+        true: ["network"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_REMOTE_ATTESTATION"), {
+        true: ["remote_attestation"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_VENDOR_MODULES"), {
+        true: ["vendor_modules"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_VIRT_CPUFREQ"), {
+        true: ["virt_cpufreq"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
+        true: ["paravirtualized_devices"],
+        default: [],
+    }),
+}
+
+cc_defaults {
+    name: "avf_build_flags_cc",
+    cflags: select(release_flag("RELEASE_AVF_ENABLE_DICE_CHANGES"), {
+        true: ["-DAVF_OPEN_DICE_CHANGES=1"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_VENDOR_MODULES"), {
+        true: ["-DAVF_ENABLE_VENDOR_MODULES=1"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_VIRT_CPUFREQ"), {
+        true: ["-DAVF_ENABLE_VIRT_CPUFREQ=1"],
+        default: [],
+    }),
+}
+
+genrule_defaults {
+    name: "dts_to_dtb",
+    tools: ["dtc"],
+    cmd: "FILES=($(in)) && $(location dtc) -@ -I dts -O dtb $${FILES[-1]} -o $(out)",
+}
+
+// This is a temporary workaround until b/309090563 is implemented.
+aconfig_declarations {
+    name: "avf_aconfig_flags",
+    package: "com.android.system.virtualmachine.flags",
+    container: "com.android.virt",
+    srcs: [
+        "avf_flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "avf_aconfig_flags_java",
+    aconfig_declarations: "avf_aconfig_flags",
+    sdk_version: "module_current",
+    apex_available: ["com.android.virt"],
+}
diff --git a/apex/Android.bp b/build/apex/Android.bp
similarity index 62%
rename from apex/Android.bp
rename to build/apex/Android.bp
index 5e74aca..f493202 100644
--- a/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -16,7 +16,6 @@
     ],
     properties: [
         "defaults",
-        "prebuilts",
     ],
 }
 
@@ -32,41 +31,7 @@
     },
 }
 
-soong_config_string_variable {
-    name: "avf_microdroid_guest_gki_version",
-    values: [
-        "android15_66",
-    ],
-}
-
-soong_config_module_type {
-    name: "avf_flag_aware_apex_defaults",
-    module_type: "apex_defaults",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_device_assignment",
-        "release_avf_enable_llpvm_changes",
-        "release_avf_enable_network",
-        "avf_remote_attestation_enabled",
-        "release_avf_enable_vendor_modules",
-        "release_avf_enable_virt_cpufreq",
-        "release_avf_support_custom_vm_with_paravirtualized_devices",
-    ],
-    variables: [
-        "avf_microdroid_guest_gki_version",
-    ],
-    properties: [
-        "androidManifest",
-        "arch",
-        "canned_fs_config",
-        "prebuilts",
-        "systemserverclasspath_fragments",
-        "vintf_fragments",
-        "apps",
-    ],
-}
-
-avf_flag_aware_apex_defaults {
+apex_defaults {
     name: "com.android.virt_common",
     // TODO(jiyong): make it updatable
     updatable: false,
@@ -80,7 +45,13 @@
 
     apps: [
         "android.system.virtualmachine.res",
-    ],
+    ] + select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
+        true: [
+            "VmLauncherApp",
+            "VmTerminalApp",
+        ],
+        default: [],
+    }),
 
     file_contexts: ":com.android.virt-file_contexts",
 
@@ -97,27 +68,19 @@
         "libsso",
         "libutils",
     ],
-    soong_config_variables: {
-        release_avf_enable_llpvm_changes: {
-            systemserverclasspath_fragments: [
-                "com.android.virt-systemserver-fragment",
-            ],
-        },
-        release_avf_enable_virt_cpufreq: {
-            canned_fs_config: "canned_fs_config_sys_nice",
-            conditions_default: {
-                canned_fs_config: "canned_fs_config",
-            },
-        },
-        release_avf_support_custom_vm_with_paravirtualized_devices: {
-            apps: [
-                "VmLauncherApp",
-            ],
-        },
-    },
+
+    systemserverclasspath_fragments: select(release_flag("RELEASE_AVF_ENABLE_LLPVM_CHANGES"), {
+        true: ["com.android.virt-systemserver-fragment"],
+        default: [],
+    }),
+
+    canned_fs_config: select(release_flag("RELEASE_AVF_ENABLE_VIRT_CPUFREQ"), {
+        true: "canned_fs_config_sys_nice",
+        default: "canned_fs_config",
+    }),
 }
 
-avf_flag_aware_apex_defaults {
+apex_defaults {
     name: "com.android.virt_avf_enabled",
 
     defaults: ["com.android.virt_common"],
@@ -131,7 +94,16 @@
                 "crosvm",
                 "virtmgr",
                 "virtualizationservice",
-            ],
+            ] + select(release_flag("RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT"), {
+                true: ["vfio_handler"],
+                default: [],
+            }) + select(release_flag("RELEASE_AVF_ENABLE_NETWORK"), {
+                true: ["vmnic"],
+                default: [],
+            }) + select(release_flag("RELEASE_AVF_ENABLE_EARLY_VM"), {
+                true: ["early_virtmgr"],
+                default: [],
+            }),
             filesystems: microdroid_filesystem_images,
             prebuilts: [
                 "rialto_bin",
@@ -142,7 +114,16 @@
                 "crosvm",
                 "virtmgr",
                 "virtualizationservice",
-            ],
+            ] + select(release_flag("RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT"), {
+                true: ["vfio_handler"],
+                default: [],
+            }) + select(release_flag("RELEASE_AVF_ENABLE_NETWORK"), {
+                true: ["vmnic"],
+                default: [],
+            }) + select(release_flag("RELEASE_AVF_ENABLE_EARLY_VM"), {
+                true: ["early_virtmgr"],
+                default: [],
+            }),
             filesystems: microdroid_filesystem_images,
         },
     },
@@ -158,59 +139,35 @@
         "microdroid_kernel",
         "com.android.virt.init.rc",
         "android_bootloader_crosvm_aarch64",
-    ],
+    ] + select(soong_config_variable("ANDROID", "avf_microdroid_guest_gki_version"), {
+        "android15_66": [
+            "microdroid_gki-android15-6.6_initrd_debuggable",
+            "microdroid_gki-android15-6.6_initrd_normal",
+            "microdroid_gki-android15-6.6_kernel",
+            "microdroid_gki-android15-6.6.json",
+        ],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_DEVICE_ASSIGNMENT"), {
+        true: ["com.android.virt.vfio_handler.rc"],
+        default: [],
+    }) + select(release_flag("RELEASE_AVF_ENABLE_NETWORK"), {
+        true: ["com.android.virt.vmnic.rc"],
+        default: [],
+    }),
     host_required: [
         "vm_shell",
     ],
     apps: [
         "EmptyPayloadApp",
     ],
-    soong_config_variables: {
-        avf_microdroid_guest_gki_version: {
-            android15_66: {
-                prebuilts: [
-                    "microdroid_gki-android15-6.6_initrd_debuggable",
-                    "microdroid_gki-android15-6.6_initrd_normal",
-                    "microdroid_gki-android15-6.6_kernel",
-                    "microdroid_gki-android15-6.6.json",
-                ],
-            },
-        },
-        release_avf_enable_device_assignment: {
-            prebuilts: [
-                "com.android.virt.vfio_handler.rc",
-            ],
-            arch: {
-                arm64: {
-                    binaries: ["vfio_handler"],
-                },
-                x86_64: {
-                    binaries: ["vfio_handler"],
-                },
-            },
-        },
-        release_avf_enable_llpvm_changes: {
-            androidManifest: "AndroidManifest.xml",
-        },
-        release_avf_enable_network: {
-            prebuilts: [
-                "com.android.virt.vmnic.rc",
-            ],
-            arch: {
-                arm64: {
-                    binaries: ["vmnic"],
-                },
-                x86_64: {
-                    binaries: ["vmnic"],
-                },
-            },
-        },
-        avf_remote_attestation_enabled: {
-            vintf_fragments: [
-                "virtualizationservice.xml",
-            ],
-        },
-    },
+    androidManifest: select(release_flag("RELEASE_AVF_ENABLE_LLPVM_CHANGES"), {
+        true: "AndroidManifest.xml",
+        default: unset,
+    }),
+    vintf_fragments: select(soong_config_variable("ANDROID", "avf_remote_attestation_enabled"), {
+        "true": ["virtualizationservice.xml"],
+        default: unset,
+    }),
 }
 
 apex_defaults {
@@ -230,28 +187,17 @@
     certificate: "com.android.virt",
 }
 
-soong_config_module_type {
-    name: "avf_flag_aware_genrule",
-    module_type: "genrule",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_llpvm_changes",
-        "avf_remote_attestation_enabled",
-    ],
-    properties: ["srcs"],
-}
-
-avf_flag_aware_genrule {
+genrule {
     name: "virtualizationservice_rc_combined",
-    srcs: ["virtualizationservice.rc.base"],
-    soong_config_variables: {
-        release_avf_enable_llpvm_changes: {
-            srcs: ["virtualizationservice.rc.llpvm"],
-        },
-        avf_remote_attestation_enabled: {
-            srcs: ["virtualizationservice.rc.ra"],
-        },
-    },
+    srcs: [
+        "virtualizationservice.rc.base",
+    ] + select(release_flag("RELEASE_AVF_ENABLE_LLPVM_CHANGES"), {
+        true: ["virtualizationservice.rc.llpvm"],
+        default: [],
+    }) + select(soong_config_variable("ANDROID", "avf_remote_attestation_enabled"), {
+        "true": ["virtualizationservice.rc.ra"],
+        default: [],
+    }),
     out: ["virtualizationservice.rc"],
     cmd: "cat $(in) > $(out)",
 }
@@ -401,28 +347,14 @@
     },
 }
 
-soong_config_module_type {
-    name: "avf_flag_aware_systemserverclasspath_fragment",
-    module_type: "systemserverclasspath_fragment",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_llpvm_changes",
-    ],
-    properties: [
-        "enabled",
-    ],
-}
-
-avf_flag_aware_systemserverclasspath_fragment {
+systemserverclasspath_fragment {
     name: "com.android.virt-systemserver-fragment",
     contents: [
         "service-virtualization",
     ],
     apex_available: ["com.android.virt"],
-    enabled: false,
-    soong_config_variables: {
-        release_avf_enable_llpvm_changes: {
-            enabled: true,
-        },
-    },
+    enabled: select(release_flag("RELEASE_AVF_ENABLE_LLPVM_CHANGES"), {
+        true: true,
+        default: false,
+    }),
 }
diff --git a/apex/AndroidManifest.xml b/build/apex/AndroidManifest.xml
similarity index 100%
rename from apex/AndroidManifest.xml
rename to build/apex/AndroidManifest.xml
diff --git a/apex/canned_fs_config b/build/apex/canned_fs_config
similarity index 100%
rename from apex/canned_fs_config
rename to build/apex/canned_fs_config
diff --git a/apex/canned_fs_config_sys_nice b/build/apex/canned_fs_config_sys_nice
similarity index 100%
rename from apex/canned_fs_config_sys_nice
rename to build/apex/canned_fs_config_sys_nice
diff --git a/apex/com.android.virt.avbpubkey b/build/apex/com.android.virt.avbpubkey
similarity index 100%
rename from apex/com.android.virt.avbpubkey
rename to build/apex/com.android.virt.avbpubkey
Binary files differ
diff --git a/apex/com.android.virt.pem b/build/apex/com.android.virt.pem
similarity index 100%
rename from apex/com.android.virt.pem
rename to build/apex/com.android.virt.pem
diff --git a/apex/com.android.virt.pk8 b/build/apex/com.android.virt.pk8
similarity index 100%
rename from apex/com.android.virt.pk8
rename to build/apex/com.android.virt.pk8
Binary files differ
diff --git a/apex/com.android.virt.x509.pem b/build/apex/com.android.virt.x509.pem
similarity index 100%
rename from apex/com.android.virt.x509.pem
rename to build/apex/com.android.virt.x509.pem
diff --git a/apex/manifest.json b/build/apex/manifest.json
similarity index 100%
rename from apex/manifest.json
rename to build/apex/manifest.json
diff --git a/apex/permissions/Android.bp b/build/apex/permissions/Android.bp
similarity index 100%
rename from apex/permissions/Android.bp
rename to build/apex/permissions/Android.bp
diff --git a/apex/permissions/features_com.android.virt.xml b/build/apex/permissions/features_com.android.virt.xml
similarity index 100%
rename from apex/permissions/features_com.android.virt.xml
rename to build/apex/permissions/features_com.android.virt.xml
diff --git a/apex/product_packages.mk b/build/apex/product_packages.mk
similarity index 79%
rename from apex/product_packages.mk
rename to build/apex/product_packages.mk
index 486334c..a024192 100644
--- a/apex/product_packages.mk
+++ b/build/apex/product_packages.mk
@@ -17,7 +17,7 @@
 # TODO: Remove this once the APEX is included in base system.
 
 # To include the APEX in your build, insert this in your device.mk:
-#   $(call inherit-product, packages/modules/Virtualization/apex/product_packages.mk)
+#   $(call inherit-product, packages/modules/Virtualization/build/apex/product_packages.mk)
 
 # If devices supports AVF it implies that it uses non-flattened APEXes.
 $(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk)
@@ -62,3 +62,15 @@
     $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
   endif
 endif
+
+ifdef RELEASE_AVF_ENABLE_EARLY_VM
+  # We can't query TARGET_RELEASE from here, so we use RELEASE_AIDL_USE_UNFROZEN as a proxy value of
+  # whether we are building -next release.
+  ifneq ($(RELEASE_AIDL_USE_UNFROZEN),true)
+    $(error RELEASE_AVF_ENABLE_EARLY_VM can only be enabled in trunk_staging until b/357025924 is fixed)
+  endif
+endif
+
+ifdef RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES
+  PRODUCT_PACKAGES += LinuxInstallerAppStub
+endif
diff --git a/apex/replace_bytes.py b/build/apex/replace_bytes.py
similarity index 100%
rename from apex/replace_bytes.py
rename to build/apex/replace_bytes.py
diff --git a/apex/sign_virt_apex.py b/build/apex/sign_virt_apex.py
similarity index 100%
rename from apex/sign_virt_apex.py
rename to build/apex/sign_virt_apex.py
diff --git a/apex/sign_virt_apex_test.sh b/build/apex/sign_virt_apex_test.sh
similarity index 100%
rename from apex/sign_virt_apex_test.sh
rename to build/apex/sign_virt_apex_test.sh
diff --git a/apex/sign_virt_apex_test.xml b/build/apex/sign_virt_apex_test.xml
similarity index 100%
rename from apex/sign_virt_apex_test.xml
rename to build/apex/sign_virt_apex_test.xml
diff --git a/apex/test.com.android.virt.pem b/build/apex/test.com.android.virt.pem
similarity index 100%
rename from apex/test.com.android.virt.pem
rename to build/apex/test.com.android.virt.pem
diff --git a/apex/test2.com.android.virt.pem b/build/apex/test2.com.android.virt.pem
similarity index 100%
rename from apex/test2.com.android.virt.pem
rename to build/apex/test2.com.android.virt.pem
diff --git a/apex/vfio_handler.rc b/build/apex/vfio_handler.rc
similarity index 100%
rename from apex/vfio_handler.rc
rename to build/apex/vfio_handler.rc
diff --git a/apex/virtualizationservice.rc.base b/build/apex/virtualizationservice.rc.base
similarity index 100%
rename from apex/virtualizationservice.rc.base
rename to build/apex/virtualizationservice.rc.base
diff --git a/apex/virtualizationservice.rc.llpvm b/build/apex/virtualizationservice.rc.llpvm
similarity index 100%
rename from apex/virtualizationservice.rc.llpvm
rename to build/apex/virtualizationservice.rc.llpvm
diff --git a/apex/virtualizationservice.rc.ra b/build/apex/virtualizationservice.rc.ra
similarity index 100%
rename from apex/virtualizationservice.rc.ra
rename to build/apex/virtualizationservice.rc.ra
diff --git a/apex/virtualizationservice.xml b/build/apex/virtualizationservice.xml
similarity index 100%
rename from apex/virtualizationservice.xml
rename to build/apex/virtualizationservice.xml
diff --git a/apex/vmnic.rc b/build/apex/vmnic.rc
similarity index 100%
rename from apex/vmnic.rc
rename to build/apex/vmnic.rc
diff --git a/avf_flags.aconfig b/build/avf_flags.aconfig
similarity index 100%
rename from avf_flags.aconfig
rename to build/avf_flags.aconfig
diff --git a/compos/apex/Android.bp b/build/compos/Android.bp
similarity index 100%
rename from compos/apex/Android.bp
rename to build/compos/Android.bp
diff --git a/compos/apk/Android.bp b/build/compos/CompOSPayloadApp/Android.bp
similarity index 100%
rename from compos/apk/Android.bp
rename to build/compos/CompOSPayloadApp/Android.bp
diff --git a/compos/apk/AndroidManifest.xml b/build/compos/CompOSPayloadApp/AndroidManifest.xml
similarity index 100%
rename from compos/apk/AndroidManifest.xml
rename to build/compos/CompOSPayloadApp/AndroidManifest.xml
diff --git a/compos/apk/assets/vm_config.json b/build/compos/CompOSPayloadApp/assets/vm_config.json
similarity index 100%
rename from compos/apk/assets/vm_config.json
rename to build/compos/CompOSPayloadApp/assets/vm_config.json
diff --git a/compos/apk/assets/vm_config_staged.json b/build/compos/CompOSPayloadApp/assets/vm_config_staged.json
similarity index 100%
rename from compos/apk/assets/vm_config_staged.json
rename to build/compos/CompOSPayloadApp/assets/vm_config_staged.json
diff --git a/compos/apk/assets/vm_config_system_ext.json b/build/compos/CompOSPayloadApp/assets/vm_config_system_ext.json
similarity index 100%
rename from compos/apk/assets/vm_config_system_ext.json
rename to build/compos/CompOSPayloadApp/assets/vm_config_system_ext.json
diff --git a/compos/apk/assets/vm_config_system_ext_staged.json b/build/compos/CompOSPayloadApp/assets/vm_config_system_ext_staged.json
similarity index 100%
rename from compos/apk/assets/vm_config_system_ext_staged.json
rename to build/compos/CompOSPayloadApp/assets/vm_config_system_ext_staged.json
diff --git a/compos/apex/com.android.compos.avbpubkey b/build/compos/com.android.compos.avbpubkey
similarity index 100%
rename from compos/apex/com.android.compos.avbpubkey
rename to build/compos/com.android.compos.avbpubkey
Binary files differ
diff --git a/compos/apex/com.android.compos.pem b/build/compos/com.android.compos.pem
similarity index 100%
rename from compos/apex/com.android.compos.pem
rename to build/compos/com.android.compos.pem
diff --git a/compos/apex/com.android.compos.pk8 b/build/compos/com.android.compos.pk8
similarity index 100%
rename from compos/apex/com.android.compos.pk8
rename to build/compos/com.android.compos.pk8
Binary files differ
diff --git a/compos/apex/com.android.compos.x509.pem b/build/compos/com.android.compos.x509.pem
similarity index 100%
rename from compos/apex/com.android.compos.x509.pem
rename to build/compos/com.android.compos.x509.pem
diff --git a/compos/apex/composd.rc b/build/compos/composd.rc
similarity index 100%
rename from compos/apex/composd.rc
rename to build/compos/composd.rc
diff --git a/compos/apex/manifest.json b/build/compos/manifest.json
similarity index 100%
rename from compos/apex/manifest.json
rename to build/compos/manifest.json
diff --git a/microdroid/Android.bp b/build/microdroid/Android.bp
similarity index 83%
rename from microdroid/Android.bp
rename to build/microdroid/Android.bp
index 289bc82..27d0246 100644
--- a/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -44,22 +44,7 @@
     },
 ]
 
-soong_config_module_type {
-    name: "flag_aware_microdroid_system_image",
-    module_type: "android_system_image",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_dice_changes",
-        "release_avf_enable_multi_tenant_microdroid_vm",
-    ],
-    properties: [
-        "deps",
-        "dirs",
-        "multilib",
-    ],
-}
-
-flag_aware_microdroid_system_image {
+android_system_image {
     name: "microdroid",
     use_avb: true,
     avb_private_key: ":microdroid_sign_key",
@@ -108,7 +93,13 @@
         "traced",
         "traced_probes",
         "perfetto",
-    ] + microdroid_shell_and_utilities,
+    ] + select(release_flag("RELEASE_AVF_ENABLE_MULTI_TENANT_MICRODROID_VM"), {
+        true: [
+            "microdroid_etc_passwd",
+            "microdroid_etc_group",
+        ],
+        default: [],
+    }) + microdroid_shell_and_utilities,
     multilib: {
         common: {
             deps: [
@@ -128,7 +119,10 @@
                 "microdroid_kexec",
                 "microdroid_manager",
                 "zipfuse",
-            ],
+            ] + select(release_flag("RELEASE_AVF_ENABLE_DICE_CHANGES"), {
+                true: ["derive_microdroid_vendor_dice_node"],
+                default: [],
+            }),
         },
     },
     arch: {
@@ -147,33 +141,16 @@
     },
     linker_config_src: "linker.config.json",
     base_dir: "system",
-    dirs: microdroid_rootdirs,
+    dirs: microdroid_rootdirs + select(release_flag("RELEASE_AVF_ENABLE_DICE_CHANGES"), {
+        true: ["microdroid_resources"],
+        default: [],
+    }),
     symlinks: microdroid_symlinks,
     file_contexts: ":microdroid_file_contexts.gen",
     // For deterministic output, use fake_timestamp, hard-coded uuid
     fake_timestamp: "1611569676",
     // python -c "import uuid; print(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com/avf/microdroid/system'))"
     uuid: "5fe079c6-f01a-52be-87d3-d415231a72ad",
-
-    // Below are dependencies that are conditionally enabled depending on value of build flags.
-    soong_config_variables: {
-        release_avf_enable_dice_changes: {
-            multilib: {
-                lib64: {
-                    deps: ["derive_microdroid_vendor_dice_node"],
-                },
-            },
-            dirs: [
-                "microdroid_resources",
-            ],
-        },
-        release_avf_enable_multi_tenant_microdroid_vm: {
-            deps: [
-                "microdroid_etc_passwd",
-                "microdroid_etc_group",
-            ],
-        },
-    },
 }
 
 prebuilt_etc {
@@ -224,15 +201,15 @@
     name: "microdroid_build_prop_gen_x86_64",
     srcs: [
         "build.prop",
-        ":buildinfo.prop",
+        ":system-build.prop",
     ],
     out: ["build.prop.out"],
-    cmd: "(echo '# build properties from buildinfo.prop module' && " +
-        "grep ro\\.build\\.version\\.codename= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.release= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.sdk= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.security_patch= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.known_codenames= $(location :buildinfo.prop) && " +
+    cmd: "(echo '# build properties from system/build.prop' && " +
+        "grep ro\\.build\\.version\\.codename= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.release= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.sdk= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.security_patch= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.known_codenames= $(location :system-build.prop) && " +
         "cat $(location build.prop) && " +
         "echo ro.product.cpu.abilist=x86_64 && " +
         "echo ro.product.cpu.abi=x86_64) > $(out)",
@@ -242,15 +219,15 @@
     name: "microdroid_build_prop_gen_arm64",
     srcs: [
         "build.prop",
-        ":buildinfo.prop",
+        ":system-build.prop",
     ],
     out: ["build.prop.out"],
-    cmd: "(echo '# build properties from buildinfo.prop module' && " +
-        "grep ro\\.build\\.version\\.codename= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.release= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.sdk= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.security_patch= $(location :buildinfo.prop) && " +
-        "grep ro\\.build\\.version\\.known_codenames= $(location :buildinfo.prop) && " +
+    cmd: "(echo '# build properties from system/build.prop' && " +
+        "grep ro\\.build\\.version\\.codename= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.release= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.sdk= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.security_patch= $(location :system-build.prop) && " +
+        "grep ro\\.build\\.version\\.known_codenames= $(location :system-build.prop) && " +
         "cat $(location build.prop) && " +
         "echo ro.product.cpu.abilist=arm64-v8a && " +
         "echo ro.product.cpu.abi=arm64-v8a) > $(out)",
@@ -308,19 +285,7 @@
     },
 }
 
-soong_config_module_type {
-    name: "flag_aware_microdroid_filesystem",
-    module_type: "android_filesystem",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_dice_changes",
-    ],
-    properties: [
-        "dirs",
-    ],
-}
-
-flag_aware_microdroid_filesystem {
+android_filesystem {
     name: "microdroid_ramdisk",
     deps: [
         "init_first_stage.microdroid",
@@ -333,17 +298,11 @@
         "mnt",
         "debug_ramdisk",
         "second_stage_resources",
-    ],
+    ] + select(release_flag("RELEASE_AVF_ENABLE_DICE_CHANGES"), {
+        true: ["microdroid_resources"],
+        default: [],
+    }),
     type: "compressed_cpio",
-
-    // Below are dependencies that are conditionally enabled depending on value of build flags.
-    soong_config_variables: {
-        release_avf_enable_dice_changes: {
-            dirs: [
-                "microdroid_resources",
-            ],
-        },
-    },
 }
 
 android_filesystem {
@@ -400,32 +359,16 @@
     srcs: [":pvmfw_embedded_key"],
 }
 
-soong_config_module_type {
-    name: "flag_aware_microdroid_vbmeta",
-    module_type: "vbmeta",
-    config_namespace: "ANDROID",
-    bool_variables: [
-        "release_avf_enable_vendor_modules",
-    ],
-    properties: [
-        "partitions",
-    ],
-}
-
-flag_aware_microdroid_vbmeta {
+vbmeta {
     name: "microdroid_vbmeta",
     partition_name: "vbmeta",
     private_key: ":microdroid_sign_key",
     partitions: [
         "microdroid",
-    ],
-    soong_config_variables: {
-        release_avf_enable_vendor_modules: {
-            conditions_default: {
-                partitions: ["microdroid_vendor"],
-            },
-        },
-    },
+    ] + select(release_flag("RELEASE_AVF_ENABLE_VENDOR_MODULES"), {
+        true: [],
+        default: ["microdroid_vendor"],
+    }),
 }
 
 prebuilt_etc {
diff --git a/microdroid/README.md b/build/microdroid/README.md
similarity index 94%
rename from microdroid/README.md
rename to build/microdroid/README.md
index c0cba97..197eec7 100644
--- a/microdroid/README.md
+++ b/build/microdroid/README.md
@@ -14,7 +14,7 @@
 pre-installed. To do this, add the following line in your product makefile.
 
 ```make
-$(call inherit-product, packages/modules/Virtualization/apex/product_packages.mk)
+$(call inherit-product, packages/modules/Virtualization/build/apex/product_packages.mk)
 ```
 
 Build the target product after adding the line, and flash it. This step needs
@@ -41,7 +41,7 @@
 ## Building an app
 
 A [vm
-payload](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/vm_payload/)
+payload](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/libs/libvm_payload/)
 is a shared library file that gets executed in microdroid. It is packaged as
 part of an Android application.  The library should have an entry point
 `AVmPayload_main` as shown below:
@@ -132,12 +132,12 @@
 ### Using the APIs
 
 Use the [Android Virtualization Framework Java
-APIs](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/java/framework/README.md)
+APIs](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/libs/framework-virtualization/README.md)
 in your app to create a microdroid VM and run payload in it. The APIs are currently
 @SystemApi, and only available to preinstalled apps.
 
 If you are looking for an example usage of the APIs, you may refer to the [demo
-app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/demo/).
+app](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/android/MicrodroidDemoApp/).
 
 
 ## Running Microdroid with vendor image
@@ -182,7 +182,7 @@
 [fstab.microdroid]: fstab.microdroid
 [dm-verity]: https://source.android.com/docs/security/features/verifiedboot/dm-verity
 [DTBO]: https://android.googlesource.com/platform/external/dtc/+/refs/heads/main/Documentation/dt-object-internal.txt
-[pvmfw config data]: ../pvmfw/README.md#configuration-data-format
+[pvmfw config data]: ../guest/pvmfw/README.md#configuration-data-format
 [bootloader]: https://source.android.com/docs/core/architecture/bootloader
 [Makefile]: https://cs.android.com/android/platform/superproject/main/+/main:build/make/core/Makefile
 
diff --git a/microdroid/bootconfig.arm64 b/build/microdroid/bootconfig.arm64
similarity index 100%
rename from microdroid/bootconfig.arm64
rename to build/microdroid/bootconfig.arm64
diff --git a/microdroid/bootconfig.common b/build/microdroid/bootconfig.common
similarity index 100%
rename from microdroid/bootconfig.common
rename to build/microdroid/bootconfig.common
diff --git a/microdroid/bootconfig.debuggable b/build/microdroid/bootconfig.debuggable
similarity index 100%
rename from microdroid/bootconfig.debuggable
rename to build/microdroid/bootconfig.debuggable
diff --git a/microdroid/bootconfig.normal b/build/microdroid/bootconfig.normal
similarity index 100%
rename from microdroid/bootconfig.normal
rename to build/microdroid/bootconfig.normal
diff --git a/microdroid/bootconfig.x86_64 b/build/microdroid/bootconfig.x86_64
similarity index 100%
rename from microdroid/bootconfig.x86_64
rename to build/microdroid/bootconfig.x86_64
diff --git a/microdroid/build.prop b/build/microdroid/build.prop
similarity index 100%
rename from microdroid/build.prop
rename to build/microdroid/build.prop
diff --git a/microdroid/dummy_dtb.img b/build/microdroid/dummy_dtb.img
similarity index 100%
rename from microdroid/dummy_dtb.img
rename to build/microdroid/dummy_dtb.img
diff --git a/microdroid/extract_microdroid_kernel_hashes.py b/build/microdroid/extract_microdroid_kernel_hashes.py
similarity index 100%
rename from microdroid/extract_microdroid_kernel_hashes.py
rename to build/microdroid/extract_microdroid_kernel_hashes.py
diff --git a/microdroid/fstab.microdroid b/build/microdroid/fstab.microdroid
similarity index 100%
rename from microdroid/fstab.microdroid
rename to build/microdroid/fstab.microdroid
diff --git a/microdroid/init.rc b/build/microdroid/init.rc
similarity index 100%
rename from microdroid/init.rc
rename to build/microdroid/init.rc
diff --git a/microdroid/init_debug_policy/Android.bp b/build/microdroid/init_debug_policy/Android.bp
similarity index 100%
rename from microdroid/init_debug_policy/Android.bp
rename to build/microdroid/init_debug_policy/Android.bp
diff --git a/microdroid/init_debug_policy/src/init_debug_policy.rs b/build/microdroid/init_debug_policy/src/init_debug_policy.rs
similarity index 100%
rename from microdroid/init_debug_policy/src/init_debug_policy.rs
rename to build/microdroid/init_debug_policy/src/init_debug_policy.rs
diff --git a/microdroid/initrd/Android.bp b/build/microdroid/initrd/Android.bp
similarity index 100%
rename from microdroid/initrd/Android.bp
rename to build/microdroid/initrd/Android.bp
diff --git a/microdroid/initrd/gen_vbmeta_bootconfig.py b/build/microdroid/initrd/gen_vbmeta_bootconfig.py
similarity index 100%
rename from microdroid/initrd/gen_vbmeta_bootconfig.py
rename to build/microdroid/initrd/gen_vbmeta_bootconfig.py
diff --git a/microdroid/initrd/src/main.rs b/build/microdroid/initrd/src/main.rs
similarity index 100%
rename from microdroid/initrd/src/main.rs
rename to build/microdroid/initrd/src/main.rs
diff --git a/microdroid/linker.config.json b/build/microdroid/linker.config.json
similarity index 100%
rename from microdroid/linker.config.json
rename to build/microdroid/linker.config.json
diff --git a/microdroid/microdroid.json b/build/microdroid/microdroid.json
similarity index 100%
rename from microdroid/microdroid.json
rename to build/microdroid/microdroid.json
diff --git a/microdroid/microdroid_event-log-tags b/build/microdroid/microdroid_event-log-tags
similarity index 100%
rename from microdroid/microdroid_event-log-tags
rename to build/microdroid/microdroid_event-log-tags
diff --git a/microdroid/microdroid_gki-android15-6.6.json b/build/microdroid/microdroid_gki-android15-6.6.json
similarity index 100%
rename from microdroid/microdroid_gki-android15-6.6.json
rename to build/microdroid/microdroid_gki-android15-6.6.json
diff --git a/microdroid/microdroid_group b/build/microdroid/microdroid_group
similarity index 100%
rename from microdroid/microdroid_group
rename to build/microdroid/microdroid_group
diff --git a/microdroid/microdroid_manifest.xml b/build/microdroid/microdroid_manifest.xml
similarity index 100%
rename from microdroid/microdroid_manifest.xml
rename to build/microdroid/microdroid_manifest.xml
diff --git a/microdroid/microdroid_passwd b/build/microdroid/microdroid_passwd
similarity index 100%
rename from microdroid/microdroid_passwd
rename to build/microdroid/microdroid_passwd
diff --git a/microdroid/uboot-env.txt b/build/microdroid/uboot-env.txt
similarity index 100%
rename from microdroid/uboot-env.txt
rename to build/microdroid/uboot-env.txt
diff --git a/microdroid/ueventd.rc b/build/microdroid/ueventd.rc
similarity index 100%
rename from microdroid/ueventd.rc
rename to build/microdroid/ueventd.rc
diff --git a/docs/abl.md b/docs/abl.md
index 7139d26..c10f1bf 100644
--- a/docs/abl.md
+++ b/docs/abl.md
@@ -14,7 +14,7 @@
 * loading it into memory, and
 * describing the region where pvmfw is loaded using DT and passing it to hypervisor.
 
-See [ABL Support](../pvmfw/README.md#android-bootloader-abl_support) for more detail.
+See [ABL Support](../guest/pvmfw/README.md#android-bootloader-abl_support) for more detail.
 
 ABL is also responsible for constructing the pvmfw configuration data. The data consists of the
 following info:
@@ -26,7 +26,7 @@
     * Hashtree digest of the
       [microdroid-vendor.img](microdroid_vendor_modules.md#changes-in-abl).
 
-See [Configuration Data](../pvmfw/README.md#configuration-data) for more detail.
+See [Configuration Data](../guest/pvmfw/README.md#configuration-data) for more detail.
 
 ## Android
 
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 6c51795..b02fbf7 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -25,19 +25,35 @@
 The `vm` command also has other subcommands for debugging; run
 `/apex/com.android.virt/bin/vm help` for details.
 
-### Running Debian with u-boot
-1. Prepare u-boot binary from `u-boot_crosvm_aarch64` in https://ci.android.com/builds/branches/aosp_u-boot-mainline/grid
-or build it by https://source.android.com/docs/devices/cuttlefish/bootloader-dev#develop-bootloader
-2. Prepare Debian image from https://cloud.debian.org/images/cloud/ (We tested nocloud image)
-3. Copy `u-boot.bin`, Debian image file(like `debian-12-nocloud-arm64.raw`) and `vm_config.json` to `/data/local/tmp`
+### Running Debian
+1. Download an ARM64 image from https://cloud.debian.org/images/cloud/ (We tested nocloud image)
+
+2. Resize the image
+```shell
+truncate -s 20G debian.img
+virt-resize --expand /dev/sda1 <download_image_file> debian.img
+```
+
+3. Copy the image file
+```shell
+tar cfS debian.img.tar debian.img
+adb push debian.img.tar /data/local/tmp/
+adb shell tar xf /data/local/tmp/debian.img.tar -C /data/local/tmp/
+adb shell rm /data/local/tmp/debian.img.tar
+adb shell chmod a+w /data/local/tmp/debian.img
+rm debian.img.tar
+```
+
+Note: we tar and untar to keep the image file sparse.
+
+4. Make the VM config file
 ```shell
 cat > vm_config.json <<EOF
 {
     "name": "debian",
-    "bootloader": "/data/local/tmp/u-boot.bin",
     "disks": [
         {
-            "image": "/data/local/tmp/debian-12-nocloud-arm64.raw",
+            "image": "/data/local/tmp/debian.img",
             "partitions": [],
             "writable": true
         }
@@ -45,24 +61,60 @@
     "protected": false,
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
-    "memory_mib" : 8096
+    "memory_mib": 8096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "ttyS0",
+    "network": true,
+    "input": {
+        "touchscreen": true,
+        "keyboard": true,
+        "mouse": true,
+        "trackpad": true,
+        "switches": true
+    },
+    "audio": {
+        "speaker": true,
+         "microphone": true
+    },
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
+    "display": {
+        "refresh_rate": "30"
+    }
 }
 EOF
-adb push `u-boot.bin` /data/local/tmp
-adb push `debian-12-nocloud-arm64.raw` /data/local/tmp
-adb push vm_config.json /data/local/tmp/vm_config.json
+adb push vm_config.json /data/local/tmp/
 ```
-4. Launch VmLauncherApp(the detail will be explain below)
+
+5. Launch VmLauncherApp(the detail will be explain below)
+
+6. For console, we can refer to `Debugging` section below. (id: root)
+
+7. For graphical shell, you need to install xfce(for now, only xfce is tested)
+```
+apt install task-xfce-desktop
+dpkg --configure -a (if necessary)
+systemctl set-default graphical.target
+
+# need non-root user for graphical shell
+adduser linux
+# optional
+adduser linux sudo
+reboot
+```
 
 ## Graphical VMs
 
 To run OSes with graphics support, simply
-`packages/modules/Virtualization/tests/ferrochrome/ferrochrome.sh`. It prepares
-and launches the ChromiumOS, which is the only officially supported guest
-payload. We will be adding more OSes in the future.
+`packages/modules/Virtualization/tests/ferrochrome/ferrochrome.sh --forever`.
+It prepares and launches the ChromiumOS, which is the only officially supported
+guest payload. We will be adding more OSes in the future.
 
-If you want to do so by yourself (e.g. boot with your build), follow the
-instruction below.
+If you want to do so by yourself, follow the instruction below.
 
 ### Prepare a guest image
 
@@ -192,43 +244,50 @@
             "writable": true
         }
     ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 8096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "hvc0",
+    "network": true,
+    "input": {
+        "touchscreen": true,
+        "keyboard": true,
+        "mouse": true,
+        "trackpad": true,
+        "switches": true
+    },
+    "audio": {
+        "speaker": true,
+        "microphone": true
+    },
     "gpu": {
         "backend": "virglrenderer",
         "context_types": ["virgl2"]
     },
-    "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,
-    "console_input_device": "ttyS0"
+    "display": {
+        "scale": "0.77",
+        "refresh_rate": "30"
+    }
 }
 ```
 
 ### Running the VM
 
-First, enable the `VmLauncherApp` app. This needs to be done only once. In the
-future, this step won't be necesssary.
+1. Grant permission to the `VmLauncherApp` if the virt apex is Google-signed.
+    ```shell
+    $ adb shell su root pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+    ```
 
-```
-$ adb root
-$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivityAlias
-$ adb unroot
-```
+2. Ensure your device is connected to the Internet.
 
-If virt apex is Google-signed, you need to enable the app and grant the
-permission to the app.
-```
-$ adb root
-$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivityAlias
-$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
-$ adb unroot
-```
-
-Second, ensure your device is connected to the Internet.
-
-Finally, tap the VmLauncherApp app from the launcher UI. You will see
-Ferrochrome booting!
+3. Launch the app with adb.
+    ```shell
+    $ adb shell su root am start-activity -a android.virtualization.VM_LAUNCHER
+    ```
 
 If it doesn’t work well, try
 
@@ -246,15 +305,12 @@
 ```
 
 To see console logs only, check
-`/data/data/com{,.google}.android.virtualization.vmlauncher/files/console.log`
-
-For HSUM enabled devices,
-`/data/user/${current_user_id}/com{,.google}.android.virtualization.vmlauncher/files/console.log`
+`/data/user/${current_user_id}/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log`
 
 You can monitor console out as follows
 
 ```shell
-$ adb shell 'su root tail +0 -F /data/user/$(am get-current-user)/com{,.google}.android.virtualization.vmlauncher/files/console.log'
+$ adb shell 'su root tail +0 -F /data/user/$(am get-current-user)/com{,.google}.android.virtualization.vmlauncher/files/${vm_name}.log'
 ```
 
 For ChromiumOS, you can enter to the console via SSH connection. Check your IP
diff --git a/docs/debug/README.md b/docs/debug/README.md
index 1e5f096..4b42531 100644
--- a/docs/debug/README.md
+++ b/docs/debug/README.md
@@ -96,7 +96,7 @@
 
 1. Bootloader loads it from the `dpm` partition and verifies it.
 1. Bootloader appends the loaded debug policy as the [configuration
-   data](../../pvmfw/README.md#configuration-data) of the pvmfw.
+   data](../../guest/pvmfw/README.md#configuration-data) of the pvmfw.
 1. When a pVM is started, pvmfw [overlays][apply_debug_policy] the debug policy to the baseline
    device tree from crosvm.
 1. OS payload (e.g. Microdroid) [reads][read_debug_policy] the device tree and enables specific
diff --git a/docs/debug/gdb_kernel.md b/docs/debug/gdb_kernel.md
index 316faad..ced6d73 100644
--- a/docs/debug/gdb_kernel.md
+++ b/docs/debug/gdb_kernel.md
@@ -33,14 +33,14 @@
 ## Obtaining vmlinux for Microdroid kernels
 
 If you are debugging Microdroid kernel that you have built [locally](
-../../microdroid/kernel/README.md), then look for `out/dist/vmlinux` in your
+../../build/microdroid/kernel/README.md), then look for `out/dist/vmlinux` in your
 kernel repository.
 
 If you are debugging Microdroid kernel bundled with the `com.android.virt` APEX,
 then you need to obtain the build ID of this kernel. You can do this by
 checking the prebuilt-info.txt file in the
-`packages/modules/Virtualization/microdroid/kernel/arm64` or
-`packages/modules/Virtualization/microdroid/kernel/x86_64` directories.
+`packages/modules/Virtualization/build/microdroid/kernel/arm64` or
+`packages/modules/Virtualization/build/microdroid/kernel/x86_64` directories.
 
 Using that build ID you can download the vmlinux from the build server via:
 https://ci.android.com/builds/submitted/${BUILD_ID}/kernel_microdroid_aarch64/latest/vmlinux
diff --git a/docs/device_assignment.md b/docs/device_assignment.md
index 1166cdf..4b2296c 100644
--- a/docs/device_assignment.md
+++ b/docs/device_assignment.md
@@ -221,7 +221,7 @@
 incoming device tree with the VM DTBO.
 
 For more detail about providing VM DTBO in pvmfw,
-see: [pvmfw/README.md](../pvmfw/README.md#configuration-data-format)
+see: [pvmfw/README.md](../guest/pvmfw/README.md#configuration-data-format)
 
 
 ## Launch VM with device assignment
@@ -236,4 +236,4 @@
 
 ```sh
 adb shell /apex/com.android.virt/bin/vm run-microdroid --devices /sys/bus/platform/devices/16d00000.light
-```
\ No newline at end of file
+```
diff --git a/docs/device_trees.md b/docs/device_trees.md
new file mode 100644
index 0000000..003e7be
--- /dev/null
+++ b/docs/device_trees.md
@@ -0,0 +1,211 @@
+# Device Trees in AVF
+
+This document aims to provide a centralized overview of the way the Android
+Virtualization Framework (AVF) composes and validates the device tree (DT)
+received by protected guest kernels, such as [Microdroid].
+
+[Microdroid]: ../guest/microdroid/README.md
+
+## Context
+
+As of Android 15, AVF only supports protected virtual machines (pVMs) on
+AArch64. On this architecture, the Linux kernel and many other embedded projects
+have adopted the [device tree format][dtspec] as the way to describe the
+platform to the software. This includes so-called "[platform devices]" (which are
+non-discoverable MMIO-based devices), CPUs (number, characteristics, ...),
+memory (address and size), and more.
+
+With virtualization, it is common for the virtual machine manager (VMM, e.g.
+crosvm or QEMU), typically a host userspace process, to generate the DT as it
+configures the virtual platform. In the case of AVF, the threat model prevents
+the guest from trusting the host and therefore the DT must be validated by a
+trusted entity. To avoid adding extra logic in the highly-privileged hypervisor,
+AVF relies on [pvmfw], a small piece of code that runs in the context of the
+guest (but before the guest kernel), loaded by the hypervisor, which validates
+the untrusted device tree. If any anomaly is detected, pvmfw aborts the boot of
+the guest. As a result, the guest kernel can trust the DT it receives.
+
+The DT sanitized by pvmfw is received by guests following the [Linux boot
+protocol][booting.txt] and includes both virtual and physical devices, which are
+hardly distinguishable from the guest's perspective (although the context could
+provide information helping to identify the nature of the device e.g. a
+virtio-blk device is likely to be virtual while a platform accelerator would be
+physical). The guest is not expected to treat physical devices differently from
+virtual devices and this distinction is therefore not relevant.
+
+```
+┌────────┐               ┌───────┐ valid              ┌───────┐
+│ crosvm ├──{input DT}──►│ pvmfw ├───────{guest DT}──►│ guest │
+└────────┘               └───┬───┘                    └───────┘
+                             │   invalid
+                             └───────────► SYSTEM RESET
+```
+
+[dtspec]: https://www.devicetree.org/specifications
+[platform devices]: https://docs.kernel.org/driver-api/driver-model/platform.html
+[pvmfw]: ../guest/pvmfw/README.md
+[booting.txt]: https://www.kernel.org/doc/Documentation/arm64/booting.txt
+
+## Device Tree Generation (Host-side)
+
+crosvm describes the virtual platform to the guest by generating a DT
+enumerating the memory region, virtual CPUs, virtual devices, and other
+properties (e.g. ramdisk, cmdline, ...). For physical devices (assigned using
+VFIO), it generates simple nodes describing the fundamental properties it
+configures for the devices i.e. `<reg>`, `<interrupts>`, `<iommus>`
+(respectively referring to IPA ranges, vIRQs, and pvIOMMUs).
+
+It is possible for the caller of crosvm to pass more DT properties or nodes to
+the guest by providing device tree overlays (DTBO) to crosvm. These overlays get
+applied after the DT describing the configured platform has been generated, the
+final result getting passed to the guest.
+
+For physical devices, crosvm supports applying a "filtered" subset of the DTBO
+received, where subnodes are only kept if they have a label corresponding to an
+assigned VFIO device. This allows the caller to always pass the same overlay,
+irrespective of which physical devices are being assigned, greatly simplifying
+the logic of the caller. This makes it possible for crosvm to support complex
+nodes for physical devices without including device-specific logic as any extra
+property (e.g. `<compatible>`) will be passed through the overlay and added to
+the final DT in a generic way. This _vm DTBO_ is read from an AVB-verified
+partition (see `ro.boot.hypervisor.vm_dtbo_idx`).
+
+Otherwise, if the `filter` option is not used, crosvm applies the overlay fully.
+This can be used to supplement the guest DT with nodes and properties which are
+not tied to particular assigned physical devices or emulated virtual devices. In
+particular, `virtualizationservice` currently makes use of it to pass
+AVF-specific properties.
+
+```
+            ┌─►{DTBO,filter}─┐
+┌─────────┐ │                │  ┌────────┐
+│ virtmgr ├─┼────►{DTBO}─────┼─►│ crosvm ├───►{guest DT}───► ...
+└─────────┘ │                │  └────────┘
+            └─►{VFIO sysfs}──┘
+```
+
+## Device Tree Sanitization
+
+pvmfw intercepts the boot sequence of the guest and locates the DT generated by
+the VMM through the VMM-guest ABI. A design goal of pvmfw is to have as little
+side-effect as possible on the guest so that the VMM can keep the illusion that
+it configured and booted the guest directly and the guest does not need to rely
+or expect pvmfw to have performed any noticeable work (a noteworthy exception
+being the memory region describing the [DICE chain]). As a result, both VMM and
+guest can mostly use the same logic between protected and non-protected VMs
+(where pvmfw does not run) and keep the simpler VMM-guest execution model they
+are used to. In the context of pvmfw and DT validation, the final DT passed by
+crosvm to the guest is typically referred to as the _input DT_.
+
+```
+┌────────┐                  ┌───────┐                  ┌───────┐
+│ crosvm ├───►{input DT}───►│ pvmfw │───►{guest DT}───►│ guest │
+└────────┘                  └───────┘                  └───────┘
+                              ▲   ▲
+   ┌─────┐  ┌─►{VM DTBO}──────┘   │
+   │ ABL ├──┤                     │
+   └─────┘  └─►{ref. DT}──────────┘
+```
+
+[DICE chain]: ../guest/pvmfw/README.md#virtual-platform-dice-chain-handover
+
+### Virtual Platform
+
+The DT sanitization policy in pvmfw matches the virtual platform defined by
+crosvm and its implementation is therefore tightly coupled with it (this is one
+reason why AVF expects pvmfw and the VMM to be updated in sync). It covers
+fundamental properties of the platform (e.g.  location of main memory,
+properties of CPUs, layout of the interrupt controller, ...) and the properties
+of (sometimes optional) virtual devices supported by crosvm and used by AVF
+guests.
+
+### Physical Devices
+
+To support device assignment, pvmfw needs to be able to validate physical
+platform-specific device properties. To achieve this in a platform-agnostic way,
+pvmfw receives a DT overlay (called the _VM DTBO_) from the Android Bootloader
+(ABL), containing a description of all the assignable devices. By detecting
+which devices have been assigned using platform-specific reserved DT labels, it
+can validate the properties of the physical devices through [generic logic].
+pvmfw also verifies with the hypervisor that the guest addresses from the DT
+have been properly mapped to the expected physical addresses of the devices; see
+[_Getting started with device assignment_][da.md].
+
+Note that, as pvmfw runs within the context of an individual pVM, it cannot
+detect abuses by the host of device assignment across guests (e.g.
+simultaneously assigning the same device to multiple guests), and it is the
+responsibility of the hypervisor to enforce this isolation. AVF also relies on
+the hypervisor to clear the state of the device on donation and (most
+importantly) on return to the host so that pvmfw does not need to access the
+assigned devices.
+
+[generic logic]: ../guest/pvmfw/src/device_assignment.rs
+[da.md]: ../docs/device_assignment.md
+
+### Extra Properties (Security-Sensitive)
+
+Some AVF use-cases require passing platform-specific inputs to protected guests.
+If these are security-sensitive, they must also be validated before being used
+by the guest. In most cases, the DT property is platform-agnostic (and supported
+by the generic guest) but its value is platform-specific. The _reference DT_ is
+an [input of pvmfw][pvmfw-config] (received from the loader) and used to
+validate DT entries which are:
+
+- security-sensitive: the host should not be able to tamper with these values
+- not confidential: the property is visible to the host (as it generates it)
+- Same across VMs: the property (if present) must be same across all instances
+- possibly optional: pvmfw does not abort the boot if the entry is missing
+
+[pvmfw-config]: ../guest/pvmfw/README.md#configuration-data-format
+
+### Extra Properties (Host-Generated)
+
+Finally, to allow the host to generate values that vary between guests (and
+which therefore can't be described using one the previous mechanisms), pvmfw
+treats the subtree of the input DT at path `/avf/untrusted` differently: it only
+performs minimal sanitization on it, allowing the host to pass arbitrary,
+unsanitized DT entries. Therefore, this subtree must be used with extra
+validation by guests e.g. only accessed by path (where the name, "`untrusted`",
+acts as a reminder), with no assumptions about the presence or correctness of
+nodes or properties, without expecting properties to be well-formed, ...
+
+In particular, pvmfw prevents other nodes from linking to this subtree
+(`<phandle>` is rejected) and limits the risk of guests unexpectedly parsing it
+other than by path (`<compatible>` is also rejected) but guests must not support
+non-standard ways of binding against nodes by property as they would then be
+vulnerable to attacks from a malicious host.
+
+### Implementation details
+
+DT sanitization is currently implemented in pvmfw by parsing the input DT into
+temporary data structures and pruning a built-in device tree (called the
+_platform DT_; see [platform.dts]) accordingly. For device assignment, it prunes
+the received VM DTBO to only keep the devices that have actually been assigned
+(as the overlay contains all assignable devices of the platform).
+
+[platform.dts]: ../guest/pvmfw/platform.dts
+
+## DT for guests
+
+### AVF-specific properties and nodes
+
+For Microdroid and other AVF guests, some special DT entries are defined:
+
+- the `/chosen/avf,new-instance` flag, set when pvmfw triggered the generation
+  of a new set of CDIs (see DICE) _i.e._ the pVM instance was booted for the
+  first time. This should be used by the next stages to synchronise the
+  generation of new CDIs and detect a malicious host attempting to force only
+  one stage to do so. This property becomes obsolete (and might not be set) when
+  [deferred rollback protection] is used by the guest kernel;
+
+- the `/chosen/avf,strict-boot` flag, always set for protected VMs and can be
+  used by guests to enable extra validation;
+
+- the `/avf/untrusted/defer-rollback-protection` flag controls [deferred
+  rollback protection] on devices and for guests which support it;
+
+- the host-allocated `/avf/untrusted/instance-id` is used to assign a unique
+  identifier to the VM instance & is used for differentiating VM secrets as well
+  as by guest OS to index external storage such as Secretkeeper.
+
+[deferred rollback protection]: ../docs/updatable_vm.md#deferring-rollback-protection
diff --git a/docs/getting_started.md b/docs/getting_started.md
index 74f2012..0a7cca6 100644
--- a/docs/getting_started.md
+++ b/docs/getting_started.md
@@ -94,7 +94,7 @@
 
 ## Step 4: Run a Microdroid VM
 
-[Microdroid](../../microdroid/README.md) is a lightweight version of Android
+[Microdroid](../../build/microdroid/README.md) is a lightweight version of Android
 that is intended to run on pVM. You can run a Microdroid-based VM with an empty
 payload using the following command:
 
diff --git a/docs/microdroid_vendor_modules.md b/docs/microdroid_vendor_modules.md
index ef55225..6ead195 100644
--- a/docs/microdroid_vendor_modules.md
+++ b/docs/microdroid_vendor_modules.md
@@ -111,7 +111,7 @@
 
 The execution flow is very similar to the non-protected case above, however
 there is one important addition. The `pvmfw` binary will use the
-[VM reference DT blob](#../pvmfw/README.md#pvmfw-data-v1-2) passed from the
+[VM reference DT blob](#../guest/pvmfw/README.md#pvmfw-data-v1-2) passed from the
 Android Bootloader (ABL), to validate the guest DT overlay passed from the host.
 
 See [Changes in Android Bootloader](#changes-in-abl) section below for more
@@ -146,7 +146,7 @@
 the host's `vendor.img`.
 
 The Android Bootloader can read that property when construction the
-[VM reference DT blob](#../pvmfw/README.md#pvmfw-data-v1-2) passed to pvmfw.
+[VM reference DT blob](#../guest/pvmfw/README.md#pvmfw-data-v1-2) passed to pvmfw.
 
 ## GKI as Microdroid guest kernel
 
diff --git a/service_vm/README.md b/docs/service_vm.md
similarity index 94%
rename from service_vm/README.md
rename to docs/service_vm.md
index b45b3ae..735c14d 100644
--- a/service_vm/README.md
+++ b/docs/service_vm.md
@@ -18,7 +18,7 @@
 
 [Rialto](../rialto) is used as the bare-metal kernel for the Service VM. It
 shares some low-level setup, such as memory management and virtio device
-parsing, with pvmfw. The common setup code is grouped in [vmbase/](../vmbase).
+parsing, with pvmfw. The common setup code is grouped in [vmbase/](../libs/libvmbase).
 
 ## Functionality
 
@@ -30,7 +30,7 @@
 -   [./requests](./requests) contains the library that processes the requests.
 -   [./manager](./manager) manages the Service VM session, ensuring that only
     one Service VM is active at any given time. The
-    [virtualizationservice](../virtualizationservice) process owns and manages
+    [virtualizationservice](../android/virtualizationservice) process owns and manages
     the Service VM instance.
 
 ### RKP VM (Remote Key Provisioning Virtual Machine)
diff --git a/docs/vm_remote_attestation.md b/docs/vm_remote_attestation.md
index 195804f..79f44b9 100644
--- a/docs/vm_remote_attestation.md
+++ b/docs/vm_remote_attestation.md
@@ -27,7 +27,7 @@
 1.  Attesting the RKP VM against the RKP server.
 2.  Attesting the pVM against the RKP VM.
 
-[rkpvm]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/service_vm/README.md
+[rkpvm]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/docs/service_vm.md
 
 ### RKP VM attestation
 
@@ -67,8 +67,8 @@
 For detailed information and usage examples, please refer to the
 [demo app][demo].
 
-[api]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/vm_payload/README.md
-[demo]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/service_vm/demo_apk
+[api]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/libs/libvm_payload/README.md
+[demo]: https://android.googlesource.com/platform/packages/modules/Virtualization/+/main/android/VmAttestationDemoApp
 
 ## Output
 
@@ -105,7 +105,11 @@
     set to true only when all the DICE certificates in the pVM DICE chain are in
     normal mode.
 -   The `vmComponents` field contains a list of all the APKs and apexes loaded
-    by the pVM.
+    by the pVM. These components are extracted from the config descriptor of the
+    last DiceChainEntry of the pVM DICE chain. Refer to
+    [dice_for_avf_guest.cddl][dice_for_avf_guest_cddl] for more information.
+
+[dice_for_avf_guest_cddl]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/dice_for_avf_guest.cddl
 
 ## To Support It
 
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
deleted file mode 100644
index a5480ff..0000000
--- a/ferrochrome_app/custom_vm_setup.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/system/bin/sh
-
-function round_up() {
-  num=$1
-  div=$2
-  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
-}
-
-function install() {
-  user=$(cmd user get-main-user)
-  src_dir=/data/media/${user}/ferrochrome/
-  dst_dir=/data/local/tmp/
-
-  cat $(find ${src_dir} -name "images.tar.gz*" | sort) | tar xz -C ${dst_dir}
-  cp -u ${src_dir}vm_config.json ${dst_dir}
-  chmod 666 ${dst_dir}*
-
-  # increase the size of state.img to the multiple of 4096
-  num_blocks=$(du -b -K ${dst_dir}state.img | cut -f 1)
-  required_num_blocks=$(round_up ${num_blocks} 4)
-  additional_blocks=$((( ${required_num_blocks} - ${num_blocks} )))
-  dd if=/dev/zero bs=512 count=${additional_blocks} >> ${dst_dir}state.img
-
-  rm ${src_dir}images.tar.gz*
-  rm ${src_dir}vm_config.json
-}
-
-setprop debug.custom_vm_setup.done false
-install
-setprop debug.custom_vm_setup.start false
-setprop debug.custom_vm_setup.done true
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
deleted file mode 100644
index 5006413..0000000
--- a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright (C) 2024 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.virtualization.ferrochrome;
-
-import android.app.Activity;
-import android.app.ActivityManager;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.Bundle;
-import android.os.Environment;
-import android.os.SystemProperties;
-import android.util.Log;
-import android.view.WindowManager;
-import android.widget.TextView;
-
-import java.io.BufferedReader;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.List;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class FerrochromeActivity extends Activity {
-    private static final String TAG = FerrochromeActivity.class.getName();
-    private static final String ACTION_VM_LAUNCHER = "android.virtualization.VM_LAUNCHER";
-
-    private static final Path DEST_DIR =
-            Path.of(Environment.getExternalStorageDirectory().getPath(), "ferrochrome");
-    private static final Path VERSION_FILE = Path.of(DEST_DIR.toString(), "version");
-
-    private static final int REQUEST_CODE_VMLAUNCHER = 1;
-
-    ExecutorService executorService = Executors.newSingleThreadExecutor();
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_ferrochrome);
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
-        // Find VM Launcher
-        Intent intent = new Intent(ACTION_VM_LAUNCHER);
-        PackageManager pm = getPackageManager();
-        List<ResolveInfo> resolveInfos =
-                pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
-        if (resolveInfos == null || resolveInfos.size() != 1) {
-            updateStatus("Failed to resolve VM Launcher");
-            return;
-        }
-
-        // Clean up the existing vm launcher process if there is
-        ActivityManager am = getSystemService(ActivityManager.class);
-        am.killBackgroundProcesses(resolveInfos.get(0).activityInfo.packageName);
-
-        executorService.execute(
-                () -> {
-                    if (updateImageIfNeeded()) {
-                        updateStatus("Starting Ferrochrome...");
-                        runOnUiThread(
-                                () -> startActivityForResult(intent, REQUEST_CODE_VMLAUNCHER));
-                    }
-                });
-    }
-
-    @Override
-    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == REQUEST_CODE_VMLAUNCHER) {
-            finishAndRemoveTask();
-        }
-    }
-
-    private boolean updateImageIfNeeded() {
-        if (!isUpdateNeeded()) {
-            Log.d(TAG, "No update needed.");
-            return true;
-        }
-
-        try {
-            if (Files.notExists(DEST_DIR)) {
-                Files.createDirectory(DEST_DIR);
-            }
-
-            String[] files = getAssets().list("ferrochrome");
-            if (files == null || files.length == 0) {
-                updateStatus("ChromeOS image not found. Please go/try-ferrochrome");
-                return false;
-            }
-
-            updateStatus("Copying images...");
-            for (String file : files) {
-                updateStatus(file);
-                Path dst = Path.of(DEST_DIR.toString(), file);
-                updateFile(getAssets().open("ferrochrome/" + file), dst);
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "Error while updating image: " + e);
-            updateStatus("Failed.");
-            return false;
-        }
-        updateStatus("Done.");
-
-        updateStatus("Extracting images...");
-        SystemProperties.set("debug.custom_vm_setup.done", "false");
-        SystemProperties.set("debug.custom_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
-            try {
-                Thread.sleep(1000);
-            } catch (Exception e) {
-                Log.e(TAG, "Error while extracting image: " + e);
-                updateStatus("Failed.");
-                return false;
-            }
-        }
-
-        updateStatus("Done.");
-        return true;
-    }
-
-    private boolean isUpdateNeeded() {
-        Path[] pathsToCheck = {DEST_DIR, VERSION_FILE};
-        for (Path p : pathsToCheck) {
-            if (Files.notExists(p)) {
-                Log.d(TAG, p.toString() + " does not exist.");
-                return true;
-            }
-        }
-
-        try {
-            String installedVer = readLine(new FileInputStream(VERSION_FILE.toFile()));
-            String updatedVer = readLine(getAssets().open("ferrochrome/version"));
-            if (installedVer.equals(updatedVer)) {
-                return false;
-            }
-            Log.d(TAG, "Version mismatch. Installed: " + installedVer + "  Updated: " + updatedVer);
-        } catch (IOException e) {
-            Log.e(TAG, "Error while checking version: " + e);
-        }
-        return true;
-    }
-
-    private static String readLine(InputStream input) throws IOException {
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(input))) {
-            return reader.readLine();
-        } catch (IOException e) {
-            throw e;
-        }
-    }
-
-    private static void updateFile(InputStream input, Path path) throws IOException {
-        try {
-            Files.copy(input, path, StandardCopyOption.REPLACE_EXISTING);
-        } finally {
-            input.close();
-        }
-    }
-
-    private void updateStatus(String line) {
-        runOnUiThread(
-                () -> {
-                    TextView statusView = findViewById(R.id.status_txt_view);
-                    statusView.append(line + "\n");
-                });
-    }
-}
diff --git a/apkdmverity/.cargo/config.toml b/guest/apkdmverity/.cargo/config.toml
similarity index 100%
rename from apkdmverity/.cargo/config.toml
rename to guest/apkdmverity/.cargo/config.toml
diff --git a/apkdmverity/Android.bp b/guest/apkdmverity/Android.bp
similarity index 100%
rename from apkdmverity/Android.bp
rename to guest/apkdmverity/Android.bp
diff --git a/apkdmverity/AndroidTest.xml b/guest/apkdmverity/AndroidTest.xml
similarity index 100%
rename from apkdmverity/AndroidTest.xml
rename to guest/apkdmverity/AndroidTest.xml
diff --git a/apkdmverity/Cargo.toml b/guest/apkdmverity/Cargo.toml
similarity index 100%
rename from apkdmverity/Cargo.toml
rename to guest/apkdmverity/Cargo.toml
diff --git a/apkdmverity/TEST_MAPPING b/guest/apkdmverity/TEST_MAPPING
similarity index 100%
rename from apkdmverity/TEST_MAPPING
rename to guest/apkdmverity/TEST_MAPPING
diff --git a/apkdmverity/src/main.rs b/guest/apkdmverity/src/main.rs
similarity index 100%
rename from apkdmverity/src/main.rs
rename to guest/apkdmverity/src/main.rs
diff --git a/apkdmverity/testdata/README b/guest/apkdmverity/testdata/README
similarity index 100%
rename from apkdmverity/testdata/README
rename to guest/apkdmverity/testdata/README
diff --git a/apkdmverity/testdata/keystore b/guest/apkdmverity/testdata/keystore
similarity index 100%
rename from apkdmverity/testdata/keystore
rename to guest/apkdmverity/testdata/keystore
Binary files differ
diff --git a/apkdmverity/testdata/test.apk b/guest/apkdmverity/testdata/test.apk
similarity index 100%
rename from apkdmverity/testdata/test.apk
rename to guest/apkdmverity/testdata/test.apk
Binary files differ
diff --git a/apkdmverity/testdata/test.apk.idsig b/guest/apkdmverity/testdata/test.apk.idsig
similarity index 100%
rename from apkdmverity/testdata/test.apk.idsig
rename to guest/apkdmverity/testdata/test.apk.idsig
Binary files differ
diff --git a/guest/authfs/Android.bp b/guest/authfs/Android.bp
new file mode 100644
index 0000000..b11da3d
--- /dev/null
+++ b/guest/authfs/Android.bp
@@ -0,0 +1,52 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "authfs_defaults",
+    crate_name: "authfs",
+    edition: "2021",
+    srcs: [":authfs_src"],
+    rustlibs: [
+        "authfs_aidl_interface-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libauthfs_fsverity_metadata",
+        "libbinder_rs",
+        "libcfg_if",
+        "libclap",
+        "libfsverity_digests_proto_rust",
+        "libfuse_rust",
+        "libhex",
+        "liblibc",
+        "liblog_rust",
+        "libnix",
+        "libopenssl",
+        "libprotobuf",
+        "librpcbinder_rs",
+        "libthiserror",
+    ],
+    prefer_rlib: true,
+    target: {
+        darwin: {
+            enabled: false,
+        },
+    },
+    defaults: [
+        "crosvm_defaults",
+        "avf_build_flags_rust",
+    ],
+}
+
+filegroup {
+    name: "authfs_src",
+    srcs: [
+        "src/main.rs",
+    ],
+}
+
+rust_binary {
+    name: "authfs",
+    defaults: ["authfs_defaults"],
+    apex_available: ["com.android.virt"],
+}
diff --git a/authfs/TEST_MAPPING b/guest/authfs/TEST_MAPPING
similarity index 100%
rename from authfs/TEST_MAPPING
rename to guest/authfs/TEST_MAPPING
diff --git a/authfs/src/common.rs b/guest/authfs/src/common.rs
similarity index 100%
rename from authfs/src/common.rs
rename to guest/authfs/src/common.rs
diff --git a/authfs/src/file.rs b/guest/authfs/src/file.rs
similarity index 100%
rename from authfs/src/file.rs
rename to guest/authfs/src/file.rs
diff --git a/authfs/src/file/attr.rs b/guest/authfs/src/file/attr.rs
similarity index 100%
rename from authfs/src/file/attr.rs
rename to guest/authfs/src/file/attr.rs
diff --git a/authfs/src/file/dir.rs b/guest/authfs/src/file/dir.rs
similarity index 100%
rename from authfs/src/file/dir.rs
rename to guest/authfs/src/file/dir.rs
diff --git a/authfs/src/file/remote_file.rs b/guest/authfs/src/file/remote_file.rs
similarity index 100%
rename from authfs/src/file/remote_file.rs
rename to guest/authfs/src/file/remote_file.rs
diff --git a/authfs/src/fsstat.rs b/guest/authfs/src/fsstat.rs
similarity index 100%
rename from authfs/src/fsstat.rs
rename to guest/authfs/src/fsstat.rs
diff --git a/authfs/src/fsverity.rs b/guest/authfs/src/fsverity.rs
similarity index 100%
rename from authfs/src/fsverity.rs
rename to guest/authfs/src/fsverity.rs
diff --git a/authfs/src/fsverity/builder.rs b/guest/authfs/src/fsverity/builder.rs
similarity index 100%
rename from authfs/src/fsverity/builder.rs
rename to guest/authfs/src/fsverity/builder.rs
diff --git a/authfs/src/fsverity/common.rs b/guest/authfs/src/fsverity/common.rs
similarity index 100%
rename from authfs/src/fsverity/common.rs
rename to guest/authfs/src/fsverity/common.rs
diff --git a/authfs/src/fsverity/editor.rs b/guest/authfs/src/fsverity/editor.rs
similarity index 100%
rename from authfs/src/fsverity/editor.rs
rename to guest/authfs/src/fsverity/editor.rs
diff --git a/authfs/src/fsverity/metadata/Android.bp b/guest/authfs/src/fsverity/metadata/Android.bp
similarity index 100%
rename from authfs/src/fsverity/metadata/Android.bp
rename to guest/authfs/src/fsverity/metadata/Android.bp
diff --git a/authfs/src/fsverity/metadata/metadata.hpp b/guest/authfs/src/fsverity/metadata/metadata.hpp
similarity index 100%
rename from authfs/src/fsverity/metadata/metadata.hpp
rename to guest/authfs/src/fsverity/metadata/metadata.hpp
diff --git a/authfs/src/fsverity/metadata/metadata.rs b/guest/authfs/src/fsverity/metadata/metadata.rs
similarity index 100%
rename from authfs/src/fsverity/metadata/metadata.rs
rename to guest/authfs/src/fsverity/metadata/metadata.rs
diff --git a/authfs/src/fsverity/sys.rs b/guest/authfs/src/fsverity/sys.rs
similarity index 100%
rename from authfs/src/fsverity/sys.rs
rename to guest/authfs/src/fsverity/sys.rs
diff --git a/authfs/src/fsverity/verifier.rs b/guest/authfs/src/fsverity/verifier.rs
similarity index 100%
rename from authfs/src/fsverity/verifier.rs
rename to guest/authfs/src/fsverity/verifier.rs
diff --git a/authfs/src/fusefs.rs b/guest/authfs/src/fusefs.rs
similarity index 98%
rename from authfs/src/fusefs.rs
rename to guest/authfs/src/fusefs.rs
index ab75dac..618b8ac 100644
--- a/authfs/src/fusefs.rs
+++ b/guest/authfs/src/fusefs.rs
@@ -99,9 +99,9 @@
     /// Number of `Handle`s (i.e. file descriptors) that are currently referring to the this inode.
     ///
     /// Technically, this does not matter to readonly entries, since they live forever. The
-    /// reference count is only needed for manageing lifetime of writable entries like `VerifiedNew`
-    /// and `VerifiedNewDirectory`. That is, when an entry is deleted, the actual entry needs to
-    /// stay alive until the reference count reaches zero.
+    /// reference count is only needed for manageing lifetime of writable entries like
+    /// `VerifiedNew` and `VerifiedNewDirectory`. That is, when an entry is deleted, the actual
+    /// entry needs to stay alive until the reference count reaches zero.
     ///
     /// Note: This is not to be confused with hardlinks, which AuthFS doesn't currently implement.
     handle_ref_count: AtomicU64,
@@ -192,9 +192,9 @@
     /// The next available inode number.
     next_inode: AtomicU64,
 
-    /// Table for `Handle` to `Arc<DirEntriesSnapshot>` lookup. On `opendir`, a new directory handle
-    /// is created and the snapshot of the current directory is created. This is not super
-    /// efficient, but is the simplest way to be compliant to the FUSE contract (see
+    /// Table for `Handle` to `Arc<DirEntriesSnapshot>` lookup. On `opendir`, a new directory
+    /// handle is created and the snapshot of the current directory is created. This is not
+    /// super efficient, but is the simplest way to be compliant to the FUSE contract (see
     /// `fuse::filesystem::readdir`).
     ///
     /// Currently, no code locks `dir_handle_table` and `inode_table` at the same time to avoid
@@ -822,9 +822,9 @@
         self.handle_inode(&inode, |config| {
             match config {
                 AuthFsEntry::VerifiedNew { editor, .. } => {
-                    // FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a kernel
-                    // change (see b/196635431). Until it's possible, use xattr to expose what we need
-                    // as an authfs specific API.
+                    // FUSE ioctl is limited, thus we can't implement fs-verity ioctls without a
+                    // kernel change (see b/196635431). Until it's possible, use
+                    // xattr to expose what we need as an authfs specific API.
                     if name != CStr::from_bytes_with_nul(b"authfs.fsverity.digest\0").unwrap() {
                         return Err(io::Error::from_raw_os_error(libc::ENODATA));
                     }
diff --git a/authfs/src/fusefs/file.rs b/guest/authfs/src/fusefs/file.rs
similarity index 100%
rename from authfs/src/fusefs/file.rs
rename to guest/authfs/src/fusefs/file.rs
diff --git a/authfs/src/fusefs/mount.rs b/guest/authfs/src/fusefs/mount.rs
similarity index 100%
rename from authfs/src/fusefs/mount.rs
rename to guest/authfs/src/fusefs/mount.rs
diff --git a/authfs/src/main.rs b/guest/authfs/src/main.rs
similarity index 100%
rename from authfs/src/main.rs
rename to guest/authfs/src/main.rs
diff --git a/authfs/service/Android.bp b/guest/authfs_service/Android.bp
similarity index 94%
rename from authfs/service/Android.bp
rename to guest/authfs_service/Android.bp
index 2101a36..e508c17 100644
--- a/authfs/service/Android.bp
+++ b/guest/authfs_service/Android.bp
@@ -18,6 +18,7 @@
         "libnix",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libshared_child",
     ],
     prefer_rlib: true,
diff --git a/authfs/TEST_MAPPING b/guest/authfs_service/TEST_MAPPING
similarity index 100%
copy from authfs/TEST_MAPPING
copy to guest/authfs_service/TEST_MAPPING
diff --git a/authfs/service/authfs_service.rc b/guest/authfs_service/authfs_service.rc
similarity index 100%
rename from authfs/service/authfs_service.rc
rename to guest/authfs_service/authfs_service.rc
diff --git a/authfs/service/src/authfs.rs b/guest/authfs_service/src/authfs.rs
similarity index 100%
rename from authfs/service/src/authfs.rs
rename to guest/authfs_service/src/authfs.rs
diff --git a/authfs/service/src/main.rs b/guest/authfs_service/src/main.rs
similarity index 83%
rename from authfs/service/src/main.rs
rename to guest/authfs_service/src/main.rs
index 97e684d..ff2f770 100644
--- a/authfs/service/src/main.rs
+++ b/guest/authfs_service/src/main.rs
@@ -26,9 +26,10 @@
 use log::*;
 use rpcbinder::RpcServer;
 use rustutils::sockets::android_get_control_socket;
+use safe_ownedfd::take_fd_ownership;
 use std::ffi::OsString;
 use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 use std::sync::atomic::{AtomicUsize, Ordering};
 
 use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
@@ -109,22 +110,9 @@
 }
 
 /// Prepares a socket file descriptor for the authfs service.
-///
-/// # Safety requirement
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_authfs_service_socket() -> Result<OwnedFd> {
+fn prepare_authfs_service_socket() -> Result<OwnedFd> {
     let raw_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+    Ok(take_fd_ownership(raw_fd)?)
 }
 
 #[allow(clippy::eq_op)]
@@ -137,8 +125,7 @@
 
     clean_up_working_directory()?;
 
-    // SAFETY: This is the only place we take the ownership of the fd of the authfs service.
-    let socket_fd = unsafe { prepare_authfs_service_socket()? };
+    let socket_fd = prepare_authfs_service_socket()?;
     let service = AuthFsService::new_binder(debuggable).as_binder();
     debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
     let server = RpcServer::new_bound_socket(service, socket_fd)?;
diff --git a/compos/compos_key_helper/Android.bp b/guest/compos_key_helper/Android.bp
similarity index 100%
rename from compos/compos_key_helper/Android.bp
rename to guest/compos_key_helper/Android.bp
diff --git a/compos/compos_key_helper/compos_key.cpp b/guest/compos_key_helper/compos_key.cpp
similarity index 100%
rename from compos/compos_key_helper/compos_key.cpp
rename to guest/compos_key_helper/compos_key.cpp
diff --git a/compos/compos_key_helper/compos_key.h b/guest/compos_key_helper/compos_key.h
similarity index 100%
rename from compos/compos_key_helper/compos_key.h
rename to guest/compos_key_helper/compos_key.h
diff --git a/compos/compos_key_helper/compos_key_main.cpp b/guest/compos_key_helper/compos_key_main.cpp
similarity index 100%
rename from compos/compos_key_helper/compos_key_main.cpp
rename to guest/compos_key_helper/compos_key_main.cpp
diff --git a/compos/compos_key_helper/compos_key_test.cpp b/guest/compos_key_helper/compos_key_test.cpp
similarity index 100%
rename from compos/compos_key_helper/compos_key_test.cpp
rename to guest/compos_key_helper/compos_key_test.cpp
diff --git a/compos/compos_key_helper/tests/AndroidTest.xml b/guest/compos_key_helper/tests/AndroidTest.xml
similarity index 100%
rename from compos/compos_key_helper/tests/AndroidTest.xml
rename to guest/compos_key_helper/tests/AndroidTest.xml
diff --git a/compos/Android.bp b/guest/compsvc/Android.bp
similarity index 100%
rename from compos/Android.bp
rename to guest/compsvc/Android.bp
diff --git a/compos/src/artifact_signer.rs b/guest/compsvc/src/artifact_signer.rs
similarity index 100%
rename from compos/src/artifact_signer.rs
rename to guest/compsvc/src/artifact_signer.rs
diff --git a/compos/src/compilation.rs b/guest/compsvc/src/compilation.rs
similarity index 100%
rename from compos/src/compilation.rs
rename to guest/compsvc/src/compilation.rs
diff --git a/compos/src/compos_key.rs b/guest/compsvc/src/compos_key.rs
similarity index 100%
rename from compos/src/compos_key.rs
rename to guest/compsvc/src/compos_key.rs
diff --git a/compos/src/compsvc.rs b/guest/compsvc/src/compsvc.rs
similarity index 100%
rename from compos/src/compsvc.rs
rename to guest/compsvc/src/compsvc.rs
diff --git a/compos/src/compsvc_main.rs b/guest/compsvc/src/compsvc_main.rs
similarity index 100%
rename from compos/src/compsvc_main.rs
rename to guest/compsvc/src/compsvc_main.rs
diff --git a/compos/src/fsverity.rs b/guest/compsvc/src/fsverity.rs
similarity index 100%
rename from compos/src/fsverity.rs
rename to guest/compsvc/src/fsverity.rs
diff --git a/microdroid/derive_microdroid_vendor_dice_node/Android.bp b/guest/derive_microdroid_vendor_dice_node/Android.bp
similarity index 100%
rename from microdroid/derive_microdroid_vendor_dice_node/Android.bp
rename to guest/derive_microdroid_vendor_dice_node/Android.bp
diff --git a/microdroid/derive_microdroid_vendor_dice_node/src/main.rs b/guest/derive_microdroid_vendor_dice_node/src/main.rs
similarity index 100%
rename from microdroid/derive_microdroid_vendor_dice_node/src/main.rs
rename to guest/derive_microdroid_vendor_dice_node/src/main.rs
diff --git a/encryptedstore/Android.bp b/guest/encryptedstore/Android.bp
similarity index 100%
rename from encryptedstore/Android.bp
rename to guest/encryptedstore/Android.bp
diff --git a/encryptedstore/README.md b/guest/encryptedstore/README.md
similarity index 90%
rename from encryptedstore/README.md
rename to guest/encryptedstore/README.md
index 3d55d85..5627984 100644
--- a/encryptedstore/README.md
+++ b/guest/encryptedstore/README.md
@@ -26,6 +26,6 @@
 even after OTA/updates of boot images and apks. This requires chipsets to support [Secretkeeper HAL][sk_hal].
 
 
-[vm_payload_api]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/include/vm_payload.h;l=2?q=vm_payload%2Finclude%2Fvm_payload.h&ss=android%2Fplatform%2Fsuperproject%2Fmain
+[vm_payload_api]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/libvm_payload/include/vm_payload.h;l=2?q=vm_payload%2Finclude%2Fvm_payload.h&ss=android%2Fplatform%2Fsuperproject%2Fmain
 [updatable_vm]: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/docs/updatable_vm.md
 [sk_hal]: https://cs.android.com/android/platform/superproject/main/+/main:system/secretkeeper/README.md
diff --git a/encryptedstore/TEST_MAPPING b/guest/encryptedstore/TEST_MAPPING
similarity index 100%
rename from encryptedstore/TEST_MAPPING
rename to guest/encryptedstore/TEST_MAPPING
diff --git a/encryptedstore/src/main.rs b/guest/encryptedstore/src/main.rs
similarity index 100%
rename from encryptedstore/src/main.rs
rename to guest/encryptedstore/src/main.rs
diff --git a/microdroid/kdump/Android.bp b/guest/kdump/Android.bp
similarity index 100%
rename from microdroid/kdump/Android.bp
rename to guest/kdump/Android.bp
diff --git a/microdroid/kdump/crashdump.c b/guest/kdump/crashdump.c
similarity index 100%
rename from microdroid/kdump/crashdump.c
rename to guest/kdump/crashdump.c
diff --git a/microdroid/kdump/kernel/Android.bp b/guest/kdump/kernel/Android.bp
similarity index 100%
rename from microdroid/kdump/kernel/Android.bp
rename to guest/kdump/kernel/Android.bp
diff --git a/microdroid/kdump/kernel/arm64/kernel-5.15 b/guest/kdump/kernel/arm64/kernel-5.15
similarity index 100%
rename from microdroid/kdump/kernel/arm64/kernel-5.15
rename to guest/kdump/kernel/arm64/kernel-5.15
Binary files differ
diff --git a/microdroid/kdump/kernel/empty b/guest/kdump/kernel/empty
similarity index 100%
rename from microdroid/kdump/kernel/empty
rename to guest/kdump/kernel/empty
diff --git a/microdroid/kdump/kernel/x86_64/kernel-5.15 b/guest/kdump/kernel/x86_64/kernel-5.15
similarity index 100%
rename from microdroid/kdump/kernel/x86_64/kernel-5.15
rename to guest/kdump/kernel/x86_64/kernel-5.15
Binary files differ
diff --git a/microdroid/kdump/kexec.c b/guest/kdump/kexec.c
similarity index 97%
rename from microdroid/kdump/kexec.c
rename to guest/kdump/kexec.c
index d3e8e02..fcec996 100644
--- a/microdroid/kdump/kexec.c
+++ b/guest/kdump/kexec.c
@@ -39,7 +39,7 @@
 static const char *CMDLINE = "1 panic=-1 rdinit=/bin/crashdump nr_cpus=1 reset_devices "
                              "console=hvc0 " EARLYCON;
 
-static int open_checked(const char* path) {
+static int open_checked(const char *path) {
     int fd = open(path, O_RDONLY);
     if (fd == -1) {
         fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
diff --git a/guest/kernel/Android.bp b/guest/kernel/Android.bp
new file mode 100644
index 0000000..19cdc49
--- /dev/null
+++ b/guest/kernel/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 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 {
+    default_applicable_licenses: ["microdroid_kernel_prebuilts_license"],
+}
+
+// See: http://go/android-license-faq
+license {
+    name: "microdroid_kernel_prebuilts_license",
+    visibility: [":__subpackages__"],
+    license_kinds: [
+        "SPDX-license-identifier-GPL-2.0-only",
+    ],
+    // large-scale-change unable to identify any license_text files
+}
+
+filegroup {
+    name: "microdroid_kernel_prebuilt-arm64",
+    // Below are properties that are conditionally set depending on value of build flags.
+    srcs: select(release_flag("RELEASE_AVF_MICRODROID_KERNEL_VERSION"), {
+        "android14_61": ["android14-6.1/arm64/kernel-6.1"],
+        "android15_66": ["android15-6.6/arm64/kernel-6.6"],
+        // In case release configuration doesn't specify value of the
+        // RELEASE_AVF_MICRODROID_KERNEL_VERSION fallback to the kernel we
+        // already released.
+        // TODO(b/298011555): remove this once we set the flag in all release configs.
+        default: ["android14-6.1/arm64/kernel-6.1"],
+    }),
+}
+
+filegroup {
+    name: "microdroid_kernel_prebuilt-x86_64",
+    // Below are properties that are conditionally set depending on value of build flags.
+    srcs: select(release_flag("RELEASE_AVF_MICRODROID_KERNEL_VERSION"), {
+        "android14_61": ["android14-6.1/x86_64/kernel-6.1"],
+        "android15_66": ["android15-6.6/x86_64/kernel-6.6"],
+        // In case release configuration doesn't specify value of the
+        // RELEASE_AVF_MICRODROID_KERNEL_VERSION fallback to the kernel we
+        // already released.
+        // TODO(b/298011555): remove this once we set the flag in all release configs.
+        default: ["android14-6.1/x86_64/kernel-6.1"],
+    }),
+}
diff --git a/microdroid/kernel/README.md b/guest/kernel/README.md
similarity index 94%
rename from microdroid/kernel/README.md
rename to guest/kernel/README.md
index 52df333..c4ffb81 100644
--- a/microdroid/kernel/README.md
+++ b/guest/kernel/README.md
@@ -10,7 +10,7 @@
 ### Checkout the GKI source code.
 
 ```bash
-repo init -u https://android.googlesource.com/kernel/manifest -b common-android14-6.1
+repo init -u https://android.googlesource.com/kernel/manifest -b common-android15-6.6
 repo sync
 ```
 
@@ -55,12 +55,12 @@
 
 For ARM64,
 ```bash
-cp out/dist/Image <android_checkout>/packages/modules/Virtualization/microdroid/kernel/arm64/kernel-6.1
+cp out/dist/Image <android_checkout>/packages/modules/Virtualization/guest/kernel/android15-6.6/arm64/kernel-6.6
 ```
 
 For x86\_64,
 ```bash
-cp out/dist/bzImage <android_checkout>/packages/modules/Virtualization/microdroid/kernel/x86_64/kernel-6.1
+cp out/dist/bzImage <android_checkout>/packages/modules/Virtualization/guest/kernel/android15-6.6/x86_64/kernel-6.6
 ```
 
 ### For official updates
diff --git a/microdroid/kernel/android14-6.1/arm64/System.map b/guest/kernel/android14-6.1/arm64/System.map
similarity index 100%
rename from microdroid/kernel/android14-6.1/arm64/System.map
rename to guest/kernel/android14-6.1/arm64/System.map
diff --git a/microdroid/kernel/android14-6.1/arm64/kernel-6.1 b/guest/kernel/android14-6.1/arm64/kernel-6.1
similarity index 100%
rename from microdroid/kernel/android14-6.1/arm64/kernel-6.1
rename to guest/kernel/android14-6.1/arm64/kernel-6.1
Binary files differ
diff --git a/microdroid/kernel/android14-6.1/arm64/kernel-6.1-gz b/guest/kernel/android14-6.1/arm64/kernel-6.1-gz
similarity index 100%
rename from microdroid/kernel/android14-6.1/arm64/kernel-6.1-gz
rename to guest/kernel/android14-6.1/arm64/kernel-6.1-gz
Binary files differ
diff --git a/microdroid/kernel/android14-6.1/arm64/kernel-6.1-lz4 b/guest/kernel/android14-6.1/arm64/kernel-6.1-lz4
similarity index 100%
rename from microdroid/kernel/android14-6.1/arm64/kernel-6.1-lz4
rename to guest/kernel/android14-6.1/arm64/kernel-6.1-lz4
Binary files differ
diff --git a/microdroid/kernel/android14-6.1/arm64/prebuilt-info.txt b/guest/kernel/android14-6.1/arm64/prebuilt-info.txt
similarity index 100%
rename from microdroid/kernel/android14-6.1/arm64/prebuilt-info.txt
rename to guest/kernel/android14-6.1/arm64/prebuilt-info.txt
diff --git a/microdroid/kernel/android14-6.1/x86_64/System.map b/guest/kernel/android14-6.1/x86_64/System.map
similarity index 100%
rename from microdroid/kernel/android14-6.1/x86_64/System.map
rename to guest/kernel/android14-6.1/x86_64/System.map
diff --git a/microdroid/kernel/android14-6.1/x86_64/kernel-6.1 b/guest/kernel/android14-6.1/x86_64/kernel-6.1
similarity index 100%
rename from microdroid/kernel/android14-6.1/x86_64/kernel-6.1
rename to guest/kernel/android14-6.1/x86_64/kernel-6.1
Binary files differ
diff --git a/microdroid/kernel/android14-6.1/x86_64/prebuilt-info.txt b/guest/kernel/android14-6.1/x86_64/prebuilt-info.txt
similarity index 100%
rename from microdroid/kernel/android14-6.1/x86_64/prebuilt-info.txt
rename to guest/kernel/android14-6.1/x86_64/prebuilt-info.txt
diff --git a/microdroid/kernel/android15-6.6/arm64/System.map b/guest/kernel/android15-6.6/arm64/System.map
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/System.map
rename to guest/kernel/android15-6.6/arm64/System.map
diff --git a/microdroid/kernel/android15-6.6/arm64/kernel-6.6 b/guest/kernel/android15-6.6/arm64/kernel-6.6
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/kernel-6.6
rename to guest/kernel/android15-6.6/arm64/kernel-6.6
Binary files differ
diff --git a/microdroid/kernel/android15-6.6/arm64/kernel-6.6-gz b/guest/kernel/android15-6.6/arm64/kernel-6.6-gz
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/kernel-6.6-gz
rename to guest/kernel/android15-6.6/arm64/kernel-6.6-gz
Binary files differ
diff --git a/microdroid/kernel/android15-6.6/arm64/kernel-6.6-lz4 b/guest/kernel/android15-6.6/arm64/kernel-6.6-lz4
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/kernel-6.6-lz4
rename to guest/kernel/android15-6.6/arm64/kernel-6.6-lz4
Binary files differ
diff --git a/microdroid/kernel/android15-6.6/arm64/kernel_version.mk b/guest/kernel/android15-6.6/arm64/kernel_version.mk
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/kernel_version.mk
rename to guest/kernel/android15-6.6/arm64/kernel_version.mk
diff --git a/microdroid/kernel/android15-6.6/arm64/prebuilt-info.txt b/guest/kernel/android15-6.6/arm64/prebuilt-info.txt
similarity index 100%
rename from microdroid/kernel/android15-6.6/arm64/prebuilt-info.txt
rename to guest/kernel/android15-6.6/arm64/prebuilt-info.txt
diff --git a/microdroid/kernel/android15-6.6/x86_64/System.map b/guest/kernel/android15-6.6/x86_64/System.map
similarity index 100%
rename from microdroid/kernel/android15-6.6/x86_64/System.map
rename to guest/kernel/android15-6.6/x86_64/System.map
diff --git a/microdroid/kernel/android15-6.6/x86_64/kernel-6.6 b/guest/kernel/android15-6.6/x86_64/kernel-6.6
similarity index 100%
rename from microdroid/kernel/android15-6.6/x86_64/kernel-6.6
rename to guest/kernel/android15-6.6/x86_64/kernel-6.6
Binary files differ
diff --git a/microdroid/kernel/android15-6.6/x86_64/kernel_version.mk b/guest/kernel/android15-6.6/x86_64/kernel_version.mk
similarity index 100%
rename from microdroid/kernel/android15-6.6/x86_64/kernel_version.mk
rename to guest/kernel/android15-6.6/x86_64/kernel_version.mk
diff --git a/microdroid/kernel/android15-6.6/x86_64/prebuilt-info.txt b/guest/kernel/android15-6.6/x86_64/prebuilt-info.txt
similarity index 100%
rename from microdroid/kernel/android15-6.6/x86_64/prebuilt-info.txt
rename to guest/kernel/android15-6.6/x86_64/prebuilt-info.txt
diff --git a/launcher/Android.bp b/guest/microdroid_launcher/Android.bp
similarity index 100%
rename from launcher/Android.bp
rename to guest/microdroid_launcher/Android.bp
diff --git a/launcher/main.cpp b/guest/microdroid_launcher/main.cpp
similarity index 100%
rename from launcher/main.cpp
rename to guest/microdroid_launcher/main.cpp
diff --git a/microdroid_manager/Android.bp b/guest/microdroid_manager/Android.bp
similarity index 97%
rename from microdroid_manager/Android.bp
rename to guest/microdroid_manager/Android.bp
index 9c9a3d0..82e26b7 100644
--- a/microdroid_manager/Android.bp
+++ b/guest/microdroid_manager/Android.bp
@@ -48,6 +48,7 @@
         "libprotobuf",
         "librpcbinder_rs",
         "librustutils",
+        "libsafe_ownedfd",
         "libsecretkeeper_client",
         "libsecretkeeper_comm_nostd",
         "libscopeguard",
@@ -59,6 +60,7 @@
         "libvsock",
         "librand",
         "libzeroize",
+        "libsafe_ownedfd",
     ],
     init_rc: ["microdroid_manager.rc"],
     multilib: {
diff --git a/microdroid_manager/TEST_MAPPING b/guest/microdroid_manager/TEST_MAPPING
similarity index 100%
rename from microdroid_manager/TEST_MAPPING
rename to guest/microdroid_manager/TEST_MAPPING
diff --git a/microdroid_manager/aidl/Android.bp b/guest/microdroid_manager/aidl/Android.bp
similarity index 100%
rename from microdroid_manager/aidl/Android.bp
rename to guest/microdroid_manager/aidl/Android.bp
diff --git a/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl b/guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
similarity index 100%
rename from microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
rename to guest/microdroid_manager/aidl/android/system/virtualization/payload/IVmPayloadService.aidl
diff --git a/microdroid_manager/microdroid_manager.rc b/guest/microdroid_manager/microdroid_manager.rc
similarity index 100%
rename from microdroid_manager/microdroid_manager.rc
rename to guest/microdroid_manager/microdroid_manager.rc
diff --git a/microdroid_manager/src/dice.rs b/guest/microdroid_manager/src/dice.rs
similarity index 100%
rename from microdroid_manager/src/dice.rs
rename to guest/microdroid_manager/src/dice.rs
diff --git a/microdroid_manager/src/instance.rs b/guest/microdroid_manager/src/instance.rs
similarity index 100%
rename from microdroid_manager/src/instance.rs
rename to guest/microdroid_manager/src/instance.rs
diff --git a/microdroid_manager/src/ioutil.rs b/guest/microdroid_manager/src/ioutil.rs
similarity index 100%
rename from microdroid_manager/src/ioutil.rs
rename to guest/microdroid_manager/src/ioutil.rs
diff --git a/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
similarity index 95%
rename from microdroid_manager/src/main.rs
rename to guest/microdroid_manager/src/main.rs
index 990d27a..8b676b8 100644
--- a/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -50,13 +50,14 @@
 use rustutils::sockets::android_get_control_socket;
 use rustutils::system_properties;
 use rustutils::system_properties::PropertyWatcher;
+use safe_ownedfd::take_fd_ownership;
 use secretkeeper_comm::data_types::ID_SIZE;
 use std::borrow::Cow::{Borrowed, Owned};
 use std::env;
 use std::ffi::CString;
 use std::fs::{self, create_dir, File, OpenOptions};
 use std::io::{Read, Write};
-use std::os::unix::io::{FromRawFd, OwnedFd};
+use std::os::unix::io::OwnedFd;
 use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::Path;
@@ -199,13 +200,7 @@
     );
     info!("started.");
 
-    // SAFETY: This is the only place we take the ownership of the fd of the vm payload service.
-    //
-    // To ensure that the CLOEXEC flag is set on the file descriptor as early as possible,
-    // it is necessary to fetch the socket corresponding to vm_payload_service at the
-    // very beginning, as android_get_control_socket() sets the CLOEXEC flag on the file
-    // descriptor.
-    let vm_payload_service_fd = unsafe { prepare_vm_payload_service_socket()? };
+    let vm_payload_service_fd = prepare_vm_payload_service_socket()?;
 
     load_crashkernel_if_supported().context("Failed to load crashkernel")?;
 
@@ -487,22 +482,9 @@
 }
 
 /// Prepares a socket file descriptor for the vm payload service.
-///
-/// # Safety
-///
-/// The caller must ensure that this function is the only place that claims ownership
-/// of the file descriptor and it is called only once.
-unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
+fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
     let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
-
-    // Creating OwnedFd for stdio FDs is not safe.
-    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
-        bail!("File descriptor {raw_fd} is standard I/O descriptor");
-    }
-    // SAFETY: Initializing OwnedFd for a RawFd created by the init.
-    // We checked that the integer value corresponds to a valid FD and that the caller
-    // ensures that this is the only place to claim its ownership.
-    Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+    Ok(take_fd_ownership(raw_fd)?)
 }
 
 fn is_strict_boot() -> bool {
diff --git a/microdroid_manager/src/payload.rs b/guest/microdroid_manager/src/payload.rs
similarity index 100%
rename from microdroid_manager/src/payload.rs
rename to guest/microdroid_manager/src/payload.rs
diff --git a/microdroid_manager/src/swap.rs b/guest/microdroid_manager/src/swap.rs
similarity index 100%
rename from microdroid_manager/src/swap.rs
rename to guest/microdroid_manager/src/swap.rs
diff --git a/microdroid_manager/src/verify.rs b/guest/microdroid_manager/src/verify.rs
similarity index 99%
rename from microdroid_manager/src/verify.rs
rename to guest/microdroid_manager/src/verify.rs
index 84feb68..90671a6 100644
--- a/microdroid_manager/src/verify.rs
+++ b/guest/microdroid_manager/src/verify.rs
@@ -272,7 +272,7 @@
     for argument in args {
         cmd.arg("--apk").arg(argument.apk).arg(argument.idsig).arg(argument.name);
         if let Some(root_hash) = argument.saved_root_hash {
-            cmd.arg(&hex::encode(root_hash));
+            cmd.arg(hex::encode(root_hash));
         } else {
             cmd.arg("none");
         }
diff --git a/microdroid_manager/src/vm_payload_service.rs b/guest/microdroid_manager/src/vm_payload_service.rs
similarity index 100%
rename from microdroid_manager/src/vm_payload_service.rs
rename to guest/microdroid_manager/src/vm_payload_service.rs
diff --git a/microdroid_manager/src/vm_secret.rs b/guest/microdroid_manager/src/vm_secret.rs
similarity index 100%
rename from microdroid_manager/src/vm_secret.rs
rename to guest/microdroid_manager/src/vm_secret.rs
diff --git a/pvmfw/Android.bp b/guest/pvmfw/Android.bp
similarity index 100%
rename from pvmfw/Android.bp
rename to guest/pvmfw/Android.bp
diff --git a/pvmfw/README.md b/guest/pvmfw/README.md
similarity index 93%
rename from pvmfw/README.md
rename to guest/pvmfw/README.md
index 7a03f0b..4712d77 100644
--- a/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -291,6 +291,19 @@
 `/reserved-memory` device tree node marked as
 [`compatible=”google,open-dice”`][dice-dt].
 
+#### Testing
+
+To verify that the DICE handover is successful in pvmfw and eventually the pVM
+has a valid DICE chain, you can run the VSR test
+`MicrodroidTests#protectedVmHasValidDiceChain`. The test retrieves the DICE
+chain from within a Microdroid VM in protected mode and checks the following
+properties using the [hwtrust][hwtrust] library:
+
+1. All the fields in the DICE chain conform to
+   [Android Profile for DICE][android-open-dice].
+2. The DICE chain is a valid certificate chain, where the subject public key in
+   each certificate can be used to verify the signature of the next certificate.
+
 [AVB]: https://source.android.com/docs/security/features/verifiedboot/boot-flow
 [AndroidDiceHandover]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#212
 [DiceAndroidHandoverMainFlow]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#221
@@ -299,6 +312,8 @@
 [dice-dt]: https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/google%2Copen-dice.yaml
 [Layering]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#layering-details
 [Trusty-BCC]: https://android.googlesource.com/trusty/lib/+/1696be0a8f3a7103/lib/hwbcc/common/swbcc.c#554
+[hwtrust]: https://cs.android.com/android/platform/superproject/main/+/main:tools/security/remote_provisioning/hwtrust/
+[android-open-dice]: https://android.googlesource.com/platform/external/open-dice/+/refs/heads/main/docs/android.md
 
 ### Platform Requirements
 
@@ -390,32 +405,25 @@
 ### Handover ABI
 
 After verifying the guest kernel, pvmfw boots it using the Linux ABI described
-above. It uses the device tree to pass the following:
+above. It uses the device tree to pass [AVF-specific properties][dt.md] and the
+DICE chain:
 
-- a reserved memory node containing the produced DICE chain:
-
-    ```
-    / {
-        reserved-memory {
-            #address-cells = <0x02>;
-            #size-cells = <0x02>;
-            ranges;
-            dice {
-                compatible = "google,open-dice";
-                no-map;
-                reg = <0x0 0x7fe0000>, <0x0 0x1000>;
-            };
+```
+/ {
+    reserved-memory {
+        #address-cells = <0x02>;
+        #size-cells = <0x02>;
+        ranges;
+        dice {
+            compatible = "google,open-dice";
+            no-map;
+            reg = <0x0 0x7fe0000>, <0x0 0x1000>;
         };
     };
-    ```
+};
+```
 
-- the `/chosen/avf,new-instance` flag, set when pvmfw generated a new secret
-  (_i.e._ the pVM instance was booted for the first time). This should be used
-  by the next stages to ensure that an attacker isn't trying to force new
-  secrets to be generated by one stage, in isolation;
-
-- the `/chosen/avf,strict-boot` flag, always set and can be used by guests to
-  enable extra validation
+[dt.md]: ../docs/device_trees.md#avf_specific-properties-and-nodes
 
 ### Guest Image Signing
 
diff --git a/pvmfw/TEST_MAPPING b/guest/pvmfw/TEST_MAPPING
similarity index 100%
rename from pvmfw/TEST_MAPPING
rename to guest/pvmfw/TEST_MAPPING
diff --git a/pvmfw/avb/Android.bp b/guest/pvmfw/avb/Android.bp
similarity index 100%
rename from pvmfw/avb/Android.bp
rename to guest/pvmfw/avb/Android.bp
diff --git a/pvmfw/avb/fuzz/Android.bp b/guest/pvmfw/avb/fuzz/Android.bp
similarity index 100%
rename from pvmfw/avb/fuzz/Android.bp
rename to guest/pvmfw/avb/fuzz/Android.bp
diff --git a/pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs b/guest/pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs
similarity index 96%
rename from pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs
rename to guest/pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs
index 6eed15b..141361e 100644
--- a/pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs
+++ b/guest/pvmfw/avb/fuzz/with_footer_verify_fuzzer.rs
@@ -55,5 +55,5 @@
         .copy_from_slice(&AVB_MAGIC[..(AVB_MAGIC_LEN as usize)]);
     modified_kernel[kernel_and_vbmeta.len()..].copy_from_slice(&avb_footer);
 
-    let _ = verify_payload(&modified_kernel, /*initrd=*/ None, &[0u8; 64]);
+    let _ = verify_payload(&modified_kernel, /* initrd= */ None, &[0u8; 64]);
 });
diff --git a/pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs b/guest/pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs
similarity index 93%
rename from pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs
rename to guest/pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs
index fc8fa85..d57f96f 100644
--- a/pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs
+++ b/guest/pvmfw/avb/fuzz/without_footer_verify_fuzzer.rs
@@ -24,5 +24,5 @@
     // kernel can pass the kernel verification, so the value of `initrd`
     // is not so important as we won't reach initrd verification with
     // this fuzzer.
-    let _ = verify_payload(kernel, /*initrd=*/ None, &[0u8; 64]);
+    let _ = verify_payload(kernel, /* initrd= */ None, &[0u8; 64]);
 });
diff --git a/pvmfw/avb/src/error.rs b/guest/pvmfw/avb/src/error.rs
similarity index 100%
rename from pvmfw/avb/src/error.rs
rename to guest/pvmfw/avb/src/error.rs
diff --git a/pvmfw/avb/src/lib.rs b/guest/pvmfw/avb/src/lib.rs
similarity index 100%
rename from pvmfw/avb/src/lib.rs
rename to guest/pvmfw/avb/src/lib.rs
diff --git a/pvmfw/avb/src/ops.rs b/guest/pvmfw/avb/src/ops.rs
similarity index 100%
rename from pvmfw/avb/src/ops.rs
rename to guest/pvmfw/avb/src/ops.rs
diff --git a/pvmfw/avb/src/partition.rs b/guest/pvmfw/avb/src/partition.rs
similarity index 100%
rename from pvmfw/avb/src/partition.rs
rename to guest/pvmfw/avb/src/partition.rs
diff --git a/pvmfw/avb/src/verify.rs b/guest/pvmfw/avb/src/verify.rs
similarity index 100%
rename from pvmfw/avb/src/verify.rs
rename to guest/pvmfw/avb/src/verify.rs
diff --git a/pvmfw/avb/tests/api_test.rs b/guest/pvmfw/avb/tests/api_test.rs
similarity index 100%
rename from pvmfw/avb/tests/api_test.rs
rename to guest/pvmfw/avb/tests/api_test.rs
diff --git a/pvmfw/avb/tests/utils.rs b/guest/pvmfw/avb/tests/utils.rs
similarity index 100%
rename from pvmfw/avb/tests/utils.rs
rename to guest/pvmfw/avb/tests/utils.rs
diff --git a/pvmfw/idmap.S b/guest/pvmfw/idmap.S
similarity index 100%
rename from pvmfw/idmap.S
rename to guest/pvmfw/idmap.S
diff --git a/pvmfw/image.ld b/guest/pvmfw/image.ld
similarity index 93%
rename from pvmfw/image.ld
rename to guest/pvmfw/image.ld
index 18bb3ba..fb26806 100644
--- a/pvmfw/image.ld
+++ b/guest/pvmfw/image.ld
@@ -18,5 +18,4 @@
 {
 	image		: ORIGIN = 0x7fc00000, LENGTH = 2M
 	writable_data	: ORIGIN = 0x7fe00000, LENGTH = 2M
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
 }
diff --git a/pvmfw/platform.dts b/guest/pvmfw/platform.dts
similarity index 100%
rename from pvmfw/platform.dts
rename to guest/pvmfw/platform.dts
diff --git a/pvmfw/src/bcc.rs b/guest/pvmfw/src/bcc.rs
similarity index 100%
rename from pvmfw/src/bcc.rs
rename to guest/pvmfw/src/bcc.rs
diff --git a/pvmfw/src/bootargs.rs b/guest/pvmfw/src/bootargs.rs
similarity index 100%
rename from pvmfw/src/bootargs.rs
rename to guest/pvmfw/src/bootargs.rs
diff --git a/pvmfw/src/config.rs b/guest/pvmfw/src/config.rs
similarity index 100%
rename from pvmfw/src/config.rs
rename to guest/pvmfw/src/config.rs
diff --git a/pvmfw/src/device_assignment.rs b/guest/pvmfw/src/device_assignment.rs
similarity index 100%
rename from pvmfw/src/device_assignment.rs
rename to guest/pvmfw/src/device_assignment.rs
diff --git a/pvmfw/src/dice.rs b/guest/pvmfw/src/dice.rs
similarity index 99%
rename from pvmfw/src/dice.rs
rename to guest/pvmfw/src/dice.rs
index 8be73a4..470711f 100644
--- a/pvmfw/src/dice.rs
+++ b/guest/pvmfw/src/dice.rs
@@ -36,8 +36,10 @@
 #[derive(Debug)]
 pub enum Error {
     /// Error in CBOR operations
+    #[allow(dead_code)]
     CborError(ciborium::value::Error),
     /// Error in DICE operations
+    #[allow(dead_code)]
     DiceError(diced_open_dice::DiceError),
 }
 
diff --git a/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
similarity index 97%
rename from pvmfw/src/entry.rs
rename to guest/pvmfw/src/entry.rs
index 0ff7270..ce04317 100644
--- a/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -256,7 +256,7 @@
     )?;
 
     // This wrapper allows main() to be blissfully ignorant of platform details.
-    let next_bcc = crate::main(
+    let (next_bcc, debuggable_payload) = crate::main(
         slices.fdt,
         slices.kernel,
         slices.ramdisk,
@@ -274,11 +274,15 @@
     })?;
     // Call unshare_all_memory here (instead of relying on the dtor) while UART is still mapped.
     MEMORY.lock().as_mut().unwrap().unshare_all_memory();
+
     if let Some(mmio_guard) = get_mmio_guard() {
-        mmio_guard.unmap(UART_PAGE_ADDR).map_err(|e| {
-            error!("Failed to unshare the UART: {e}");
-            RebootReason::InternalError
-        })?;
+        // Keep UART MMIO_GUARD-ed for debuggable payloads, to enable earlycon.
+        if !debuggable_payload {
+            mmio_guard.unmap(UART_PAGE_ADDR).map_err(|e| {
+                error!("Failed to unshare the UART: {e}");
+                RebootReason::InternalError
+            })?;
+        }
     }
 
     // Drop MemoryTracker and deactivate page table.
diff --git a/pvmfw/src/exceptions.rs b/guest/pvmfw/src/exceptions.rs
similarity index 100%
rename from pvmfw/src/exceptions.rs
rename to guest/pvmfw/src/exceptions.rs
diff --git a/pvmfw/src/fdt.rs b/guest/pvmfw/src/fdt.rs
similarity index 99%
rename from pvmfw/src/fdt.rs
rename to guest/pvmfw/src/fdt.rs
index 939a4ea..953fdae 100644
--- a/pvmfw/src/fdt.rs
+++ b/guest/pvmfw/src/fdt.rs
@@ -593,7 +593,6 @@
 ) -> Result<(), RebootReason> {
     let mem_flags = PciMemoryFlags(range.addr.0);
     let range_type = mem_flags.range_type();
-    let prefetchable = mem_flags.prefetchable();
     let bus_addr = range.addr.1;
     let cpu_addr = range.parent_addr;
     let size = range.size;
@@ -602,10 +601,6 @@
         error!("Invalid range type {:?} for bus address {:#x} in PCI node", range_type, bus_addr);
         return Err(RebootReason::InvalidFdt);
     }
-    if prefetchable {
-        error!("PCI bus address {:#x} in PCI node is prefetchable", bus_addr);
-        return Err(RebootReason::InvalidFdt);
-    }
     // Enforce ID bus-to-cpu mappings, as used by crosvm.
     if bus_addr != cpu_addr {
         error!("PCI bus address: {:#x} is different from CPU address: {:#x}", bus_addr, cpu_addr);
diff --git a/pvmfw/src/gpt.rs b/guest/pvmfw/src/gpt.rs
similarity index 100%
rename from pvmfw/src/gpt.rs
rename to guest/pvmfw/src/gpt.rs
diff --git a/pvmfw/src/helpers.rs b/guest/pvmfw/src/helpers.rs
similarity index 100%
rename from pvmfw/src/helpers.rs
rename to guest/pvmfw/src/helpers.rs
diff --git a/pvmfw/src/instance.rs b/guest/pvmfw/src/instance.rs
similarity index 98%
rename from pvmfw/src/instance.rs
rename to guest/pvmfw/src/instance.rs
index 43c7442..0ef57dc 100644
--- a/pvmfw/src/instance.rs
+++ b/guest/pvmfw/src/instance.rs
@@ -237,7 +237,7 @@
 
 /// Marks the start of an instance.img entry.
 ///
-/// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition".
+/// Note: Virtualization/guest/microdroid_manager/src/instance.rs uses the name "partition".
 #[derive(AsBytes, FromZeroes, FromBytes)]
 #[repr(C, packed)]
 struct EntryHeader {
diff --git a/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
similarity index 98%
rename from pvmfw/src/main.rs
rename to guest/pvmfw/src/main.rs
index 247aa6a..10f8549 100644
--- a/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -67,7 +67,7 @@
     ramdisk: Option<&[u8]>,
     current_bcc_handover: &[u8],
     mut debug_policy: Option<&[u8]>,
-) -> Result<Range<usize>, RebootReason> {
+) -> Result<(Range<usize>, bool), RebootReason> {
     info!("pVM firmware");
     debug!("FDT: {:?}", fdt.as_ptr());
     debug!("Signed kernel: {:?} ({:#x} bytes)", signed_kernel.as_ptr(), signed_kernel.len());
@@ -240,7 +240,7 @@
         (r.start as usize)..(r.end as usize)
     };
 
-    Ok(bcc_range)
+    Ok((bcc_range, debuggable))
 }
 
 fn check_dice_measurements_match_entry(
diff --git a/pvmfw/src/memory.rs b/guest/pvmfw/src/memory.rs
similarity index 100%
rename from pvmfw/src/memory.rs
rename to guest/pvmfw/src/memory.rs
diff --git a/pvmfw/testdata/expected_dt_with_dependency.dts b/guest/pvmfw/testdata/expected_dt_with_dependency.dts
similarity index 100%
rename from pvmfw/testdata/expected_dt_with_dependency.dts
rename to guest/pvmfw/testdata/expected_dt_with_dependency.dts
diff --git a/pvmfw/testdata/expected_dt_with_dependency_loop.dts b/guest/pvmfw/testdata/expected_dt_with_dependency_loop.dts
similarity index 100%
rename from pvmfw/testdata/expected_dt_with_dependency_loop.dts
rename to guest/pvmfw/testdata/expected_dt_with_dependency_loop.dts
diff --git a/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts b/guest/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts
similarity index 100%
rename from pvmfw/testdata/expected_dt_with_multiple_dependencies.dts
rename to guest/pvmfw/testdata/expected_dt_with_multiple_dependencies.dts
diff --git a/pvmfw/testdata/test_crosvm_dt_base.dtsi b/guest/pvmfw/testdata/test_crosvm_dt_base.dtsi
similarity index 100%
rename from pvmfw/testdata/test_crosvm_dt_base.dtsi
rename to guest/pvmfw/testdata/test_crosvm_dt_base.dtsi
diff --git a/pvmfw/testdata/test_pvmfw_devices_overlapping_pvmfw.dts b/guest/pvmfw/testdata/test_pvmfw_devices_overlapping_pvmfw.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_overlapping_pvmfw.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_overlapping_pvmfw.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts b/guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts b/guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_dependencies.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dts b/guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_with_duplicated_iommus.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts b/guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_vm_dtbo_without_symbols.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_dependency.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_dependency.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_dependency_loop.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_duplicated_pviommus.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_duplicated_pviommus.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_duplicated_pviommus.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_duplicated_pviommus.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_iommu_id_conflict.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_iommu_id_conflict.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_iommu_id_conflict.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_iommu_id_conflict.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_iommu_sharing.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_iommu_sharing.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_iommu_sharing.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_iommu_sharing.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_dependencies.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_multiple_devices_iommus.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_devices_iommus.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_multiple_devices_iommus.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_devices_iommus.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_multiple_reg_iommus.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_reg_iommus.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_multiple_reg_iommus.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_multiple_reg_iommus.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_with_rng.dts b/guest/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_with_rng.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_with_rng.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_without_device.dts b/guest/pvmfw/testdata/test_pvmfw_devices_without_device.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_without_device.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_without_device.dts
diff --git a/pvmfw/testdata/test_pvmfw_devices_without_iommus.dts b/guest/pvmfw/testdata/test_pvmfw_devices_without_iommus.dts
similarity index 100%
rename from pvmfw/testdata/test_pvmfw_devices_without_iommus.dts
rename to guest/pvmfw/testdata/test_pvmfw_devices_without_iommus.dts
diff --git a/rialto/Android.bp b/guest/rialto/Android.bp
similarity index 100%
rename from rialto/Android.bp
rename to guest/rialto/Android.bp
diff --git a/rialto/AndroidTest.xml b/guest/rialto/AndroidTest.xml
similarity index 100%
rename from rialto/AndroidTest.xml
rename to guest/rialto/AndroidTest.xml
diff --git a/rialto/TEST_MAPPING b/guest/rialto/TEST_MAPPING
similarity index 100%
rename from rialto/TEST_MAPPING
rename to guest/rialto/TEST_MAPPING
diff --git a/rialto/idmap.S b/guest/rialto/idmap.S
similarity index 91%
rename from rialto/idmap.S
rename to guest/rialto/idmap.S
index 7281d9b..eb4d823 100644
--- a/rialto/idmap.S
+++ b/guest/rialto/idmap.S
@@ -28,9 +28,8 @@
 .set .PAGE_SIZE, .SZ_4K
 
 .set .ORIGIN_ADDR, 2 * .SZ_1G
-.set .DTB_ADDR, .ORIGIN_ADDR + (0 * .SZ_2M)
-.set .TEXT_ADDR, .ORIGIN_ADDR + (1 * .SZ_2M)
-.set .DATA_ADDR, .ORIGIN_ADDR + (2 * .SZ_2M)
+.set .TEXT_ADDR, .ORIGIN_ADDR + (0 * .SZ_2M)
+.set .DATA_ADDR, .ORIGIN_ADDR + (1 * .SZ_2M)
 
 .set .L_TT_TYPE_BLOCK, 0x1
 .set .L_TT_TYPE_PAGE,  0x3
@@ -60,7 +59,7 @@
 	.balign .PAGE_SIZE, 0				// unmapped
 
 	/* level 2 */
-0:	.quad		.L_BLOCK_RO  | .DTB_ADDR	// DT provided by VMM
+0:
 	.quad		.L_BLOCK_MEM_XIP | .TEXT_ADDR	// 2 MiB of DRAM containing image
 	.quad		.L_BLOCK_MEM | .DATA_ADDR	// 2 MiB of writable DRAM
 	.balign .PAGE_SIZE, 0				// unmapped
diff --git a/rialto/image.ld b/guest/rialto/image.ld
similarity index 81%
rename from rialto/image.ld
rename to guest/rialto/image.ld
index 368acbb..3bf910c 100644
--- a/rialto/image.ld
+++ b/guest/rialto/image.ld
@@ -16,7 +16,6 @@
 
 MEMORY
 {
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
-	image		: ORIGIN = 0x80200000, LENGTH = 2M
-	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
+	image		: ORIGIN = 0x80000000, LENGTH = 2M
+	writable_data	: ORIGIN = 0x80200000, LENGTH = 2M
 }
diff --git a/rialto/src/communication.rs b/guest/rialto/src/communication.rs
similarity index 100%
rename from rialto/src/communication.rs
rename to guest/rialto/src/communication.rs
diff --git a/rialto/src/error.rs b/guest/rialto/src/error.rs
similarity index 100%
rename from rialto/src/error.rs
rename to guest/rialto/src/error.rs
diff --git a/rialto/src/exceptions.rs b/guest/rialto/src/exceptions.rs
similarity index 100%
rename from rialto/src/exceptions.rs
rename to guest/rialto/src/exceptions.rs
diff --git a/rialto/src/fdt.rs b/guest/rialto/src/fdt.rs
similarity index 85%
rename from rialto/src/fdt.rs
rename to guest/rialto/src/fdt.rs
index b220f41..e97a262 100644
--- a/rialto/src/fdt.rs
+++ b/guest/rialto/src/fdt.rs
@@ -29,3 +29,10 @@
     let node = fdt.node(cstr!("/avf"))?.ok_or(FdtError::NotFound)?;
     node.getprop(cstr!("vendor_hashtree_descriptor_root_digest"))
 }
+
+pub(crate) fn read_is_strict_boot(fdt: &Fdt) -> libfdt::Result<bool> {
+    match fdt.chosen()? {
+        Some(node) => Ok(node.getprop(cstr!("avf,strict-boot"))?.is_some()),
+        None => Ok(false),
+    }
+}
diff --git a/rialto/src/main.rs b/guest/rialto/src/main.rs
similarity index 93%
rename from rialto/src/main.rs
rename to guest/rialto/src/main.rs
index 701a287..a98ec25 100644
--- a/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -26,7 +26,7 @@
 
 use crate::communication::VsockStream;
 use crate::error::{Error, Result};
-use crate::fdt::{read_dice_range_from, read_vendor_hashtree_root_digest};
+use crate::fdt::{read_dice_range_from, read_is_strict_boot, read_vendor_hashtree_root_digest};
 use alloc::boxed::Box;
 use bssl_sys::CRYPTO_library_init;
 use ciborium_io::Write;
@@ -47,6 +47,7 @@
 use vmbase::{
     configure_heap,
     fdt::SwiotlbInfo,
+    generate_image_header,
     hyp::{get_mem_sharer, get_mmio_guard},
     layout::{self, crosvm, UART_PAGE_ADDR},
     main,
@@ -58,16 +59,15 @@
     },
 };
 
-fn host_addr() -> VsockAddr {
-    VsockAddr { cid: VMADDR_CID_HOST, port: vm_type().port() }
+fn host_addr(fdt: &libfdt::Fdt) -> Result<VsockAddr> {
+    Ok(VsockAddr { cid: VMADDR_CID_HOST, port: vm_type(fdt)?.port() })
 }
 
-fn vm_type() -> VmType {
-    // Use MMIO support to determine whether the VM is protected.
-    if get_mmio_guard().is_some() {
-        VmType::ProtectedVm
+fn vm_type(fdt: &libfdt::Fdt) -> Result<VmType> {
+    if read_is_strict_boot(fdt)? {
+        Ok(VmType::ProtectedVm)
     } else {
-        VmType::NonProtectedVm
+        Ok(VmType::NonProtectedVm)
     }
 }
 
@@ -143,7 +143,7 @@
     unsafe {
         CRYPTO_library_init();
     }
-    let bcc_handover: Box<dyn DiceArtifacts> = match vm_type() {
+    let bcc_handover: Box<dyn DiceArtifacts> = match vm_type(fdt)? {
         VmType::ProtectedVm => {
             let dice_range = read_dice_range_from(fdt)?;
             info!("DICE range: {dice_range:#x?}");
@@ -178,7 +178,7 @@
     let request_context =
         RequestContext { dice_artifacts: bcc_handover.as_ref(), vendor_hashtree_root_digest };
 
-    let mut vsock_stream = VsockStream::new(socket_device, host_addr())?;
+    let mut vsock_stream = VsockStream::new(socket_device, host_addr(fdt)?)?;
     while let ServiceVmRequest::Process(req) = vsock_stream.read_request()? {
         info!("Received request: {}", req.name());
         let response = process_request(req, &request_context);
@@ -233,5 +233,6 @@
     }
 }
 
+generate_image_header!();
 main!(main);
 configure_heap!(SIZE_128KB * 2);
diff --git a/rialto/tests/test.rs b/guest/rialto/tests/test.rs
similarity index 91%
rename from rialto/tests/test.rs
rename to guest/rialto/tests/test.rs
index cf5630f..582b69e 100644
--- a/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -34,7 +34,7 @@
 use service_vm_fake_chain::client_vm::{
     fake_client_vm_dice_artifacts, fake_sub_components, SubComponent,
 };
-use service_vm_manager::ServiceVm;
+use service_vm_manager::{ServiceVm, VM_MEMORY_MB};
 use std::fs;
 use std::fs::File;
 use std::panic;
@@ -59,7 +59,7 @@
         // The test is skipped if the feature flag |dice_changes| is not enabled, because when
         // the flag is off, the DICE chain is truncated in the pvmfw, and the service VM cannot
         // verify the chain due to the missing entries in the chain.
-        check_processing_requests(VmType::ProtectedVm)
+        check_processing_requests(VmType::ProtectedVm, None)
     } else {
         warn!("pVMs are not supported on device, skipping test");
         Ok(())
@@ -68,11 +68,13 @@
 
 #[test]
 fn process_requests_in_non_protected_vm() -> Result<()> {
-    check_processing_requests(VmType::NonProtectedVm)
+    const MEMORY_MB: i32 = 300;
+    check_processing_requests(VmType::NonProtectedVm, Some(MEMORY_MB))?;
+    check_processing_requests(VmType::NonProtectedVm, None)
 }
 
-fn check_processing_requests(vm_type: VmType) -> Result<()> {
-    let mut vm = start_service_vm(vm_type)?;
+fn check_processing_requests(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<()> {
+    let mut vm = start_service_vm(vm_type, vm_memory_mb)?;
 
     check_processing_reverse_request(&mut vm)?;
     let key_pair = check_processing_generating_key_pair_request(&mut vm)?;
@@ -281,11 +283,13 @@
 }
 
 fn check_csr(csr: Vec<u8>) -> Result<()> {
-    let _csr = rkp::Csr::from_cbor(&Session::default(), &csr[..]).context("Failed to parse CSR")?;
+    let mut session = Session::default();
+    session.set_allow_any_mode(true);
+    let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
     Ok(())
 }
 
-fn start_service_vm(vm_type: VmType) -> Result<ServiceVm> {
+fn start_service_vm(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<ServiceVm> {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("rialto")
@@ -297,19 +301,20 @@
     }));
     // We need to start the thread pool for Binder to work properly, especially link_to_death.
     ProcessState::start_thread_pool();
-    ServiceVm::start_vm(vm_instance(vm_type)?, vm_type)
+    ServiceVm::start_vm(vm_instance(vm_type, vm_memory_mb)?, vm_type)
 }
 
-fn vm_instance(vm_type: VmType) -> Result<VmInstance> {
+fn vm_instance(vm_type: VmType, vm_memory_mb: Option<i32>) -> Result<VmInstance> {
     match vm_type {
         VmType::ProtectedVm => {
+            assert!(vm_memory_mb.is_none());
             service_vm_manager::protected_vm_instance(PathBuf::from(INSTANCE_IMG_PATH))
         }
-        VmType::NonProtectedVm => nonprotected_vm_instance(),
+        VmType::NonProtectedVm => nonprotected_vm_instance(vm_memory_mb.unwrap_or(VM_MEMORY_MB)),
     }
 }
 
-fn nonprotected_vm_instance() -> Result<VmInstance> {
+fn nonprotected_vm_instance(memory_mib: i32) -> Result<VmInstance> {
     let rialto = File::open(UNSIGNED_RIALTO_PATH).context("Failed to open Rialto kernel binary")?;
     // Do not use `#allocateInstanceId` to generate the instance ID because the method
     // also adds an instance ID to the database it manages.
@@ -317,10 +322,10 @@
     let mut instance_id = [0u8; 64];
     rand_bytes(&mut instance_id).unwrap();
     let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
-        name: String::from("Non protected rialto"),
-        bootloader: Some(ParcelFileDescriptor::new(rialto)),
+        name: format!("Non protected rialto ({memory_mib}MiB)"),
+        kernel: Some(ParcelFileDescriptor::new(rialto)),
         protectedVm: false,
-        memoryMib: 300,
+        memoryMib: memory_mib,
         platformVersion: "~1.0".to_string(),
         instanceId: instance_id,
         ..Default::default()
diff --git a/guest/vmbase_example/Android.bp b/guest/vmbase_example/Android.bp
new file mode 100644
index 0000000..ff7bd83
--- /dev/null
+++ b/guest/vmbase_example/Android.bp
@@ -0,0 +1,114 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_ffi_static {
+    name: "libvmbase_example",
+    defaults: ["vmbase_ffi_defaults"],
+    crate_name: "vmbase_example",
+    srcs: ["src/main.rs"],
+    rustlibs: [
+        "libaarch64_paging",
+        "libcstr",
+        "libdiced_open_dice_nostd",
+        "libfdtpci",
+        "liblibfdt",
+        "liblog_rust_nostd",
+        "libvirtio_drivers",
+        "libvmbase",
+    ],
+}
+
+genrule {
+    name: "vmbase_image.ld.S.mm",
+    // Soong won't let us use cc_object to preprocess *.ld.S files because it
+    // can't resist feeding any and all *.S files to the assembler, which fails
+    // because linker scripts typically aren't valid assembly. Also, cc_object
+    // rejects inputs that don't end in one of .{s,S,c,cpp,cc,cxx,mm}. So keep
+    // the proper extension (.ld.S) for the file in VCS and use this convoluted
+    // extra step to please Soong by pretending that our linker script is in
+    // fact some Object C++ code, which fortunately it doesn't try to compile.
+    srcs: ["image.ld.S"],
+    out: ["image.ld.S.mm"],
+    cmd: "cp $(in) $(out)",
+    visibility: ["//visibility:private"],
+}
+
+cc_defaults {
+    name: "vmbase_example_ld_defaults",
+    defaults: ["vmbase_cc_defaults"],
+    cflags: [
+        "-E",
+        "-P",
+        "-xassembler-with-cpp", // allow C preprocessor directives
+    ],
+    srcs: [":vmbase_image.ld.S.mm"],
+    visibility: ["//visibility:private"],
+}
+
+cc_object {
+    name: "vmbase_example_bios.ld",
+    defaults: ["vmbase_example_ld_defaults"],
+    cflags: ["-DVMBASE_EXAMPLE_IS_BIOS"],
+}
+
+cc_object {
+    name: "vmbase_example_kernel.ld",
+    defaults: ["vmbase_example_ld_defaults"],
+    cflags: ["-DVMBASE_EXAMPLE_IS_KERNEL"],
+}
+
+cc_defaults {
+    name: "vmbase_example_elf_defaults",
+    defaults: ["vmbase_elf_defaults"],
+    srcs: [
+        "idmap.S",
+    ],
+    static_libs: [
+        "libvmbase_example",
+    ],
+}
+
+cc_binary {
+    name: "vmbase_example_bios",
+    defaults: ["vmbase_example_elf_defaults"],
+    asflags: ["-DVMBASE_EXAMPLE_IS_BIOS"],
+    linker_scripts: [
+        ":vmbase_example_bios.ld",
+        ":vmbase_sections",
+    ],
+}
+
+cc_binary {
+    name: "vmbase_example_kernel",
+    defaults: ["vmbase_example_elf_defaults"],
+    asflags: ["-DVMBASE_EXAMPLE_IS_KERNEL"],
+    linker_scripts: [
+        ":vmbase_example_kernel.ld",
+        ":vmbase_sections",
+    ],
+}
+
+raw_binary {
+    name: "vmbase_example_bios_bin",
+    stem: "vmbase_example_bios.bin",
+    src: ":vmbase_example_bios",
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
+
+raw_binary {
+    name: "vmbase_example_kernel_bin",
+    stem: "vmbase_example_kernel.bin",
+    src: ":vmbase_example_kernel",
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
diff --git a/vmbase/example/idmap.S b/guest/vmbase_example/idmap.S
similarity index 82%
rename from vmbase/example/idmap.S
rename to guest/vmbase_example/idmap.S
index 71a6ade..881850c 100644
--- a/vmbase/example/idmap.S
+++ b/guest/vmbase_example/idmap.S
@@ -43,8 +43,16 @@
 	.quad		.L_TT_TYPE_TABLE + 0f		// up to 1 GiB of DRAM
 	.fill		509, 8, 0x0			// 509 GiB of remaining VA space
 
-	/* level 2 */
-0:	.quad		.L_BLOCK_MEM | 0x80000000	// DT provided by VMM
+0:	/* level 2 */
+#if defined(VMBASE_EXAMPLE_IS_BIOS)
+	.quad		0				// 2 MiB not mapped (DT)
 	.quad		.L_BLOCK_MEM_XIP | 0x80200000	// 2 MiB of DRAM containing image
 	.quad		.L_BLOCK_MEM | 0x80400000	// 2 MiB of writable DRAM
 	.fill		509, 8, 0x0
+#elif defined(VMBASE_EXAMPLE_IS_KERNEL)
+	.quad		.L_BLOCK_MEM_XIP | 0x80000000	// 2 MiB of DRAM containing image
+	.quad		.L_BLOCK_MEM | 0x80200000	// 2 MiB of writable DRAM
+	.fill		510, 8, 0x0
+#else
+#error "Unexpected vmbase_example mode: failed to generate idmap"
+#endif
diff --git a/rialto/image.ld b/guest/vmbase_example/image.ld.S
similarity index 73%
copy from rialto/image.ld
copy to guest/vmbase_example/image.ld.S
index 368acbb..a5cd965 100644
--- a/rialto/image.ld
+++ b/guest/vmbase_example/image.ld.S
@@ -16,7 +16,13 @@
 
 MEMORY
 {
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
+#if defined(VMBASE_EXAMPLE_IS_BIOS)
 	image		: ORIGIN = 0x80200000, LENGTH = 2M
 	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
+#elif defined(VMBASE_EXAMPLE_IS_KERNEL)
+	image		: ORIGIN = 0x80000000, LENGTH = 2M
+	writable_data	: ORIGIN = 0x80200000, LENGTH = 2M
+#else
+#error "Unexpected vmbase_example mode: failed to generate image layout"
+#endif
 }
diff --git a/vmbase/example/src/exceptions.rs b/guest/vmbase_example/src/exceptions.rs
similarity index 100%
rename from vmbase/example/src/exceptions.rs
rename to guest/vmbase_example/src/exceptions.rs
diff --git a/vmbase/example/src/layout.rs b/guest/vmbase_example/src/layout.rs
similarity index 90%
rename from vmbase/example/src/layout.rs
rename to guest/vmbase_example/src/layout.rs
index fc578bc..50ecb7e 100644
--- a/vmbase/example/src/layout.rs
+++ b/guest/vmbase_example/src/layout.rs
@@ -17,20 +17,17 @@
 use aarch64_paging::paging::{MemoryRegion, VirtualAddress};
 use core::ops::Range;
 use log::info;
-use vmbase::layout;
+use vmbase::{layout, memory::PAGE_SIZE};
 
 /// The first 1 GiB of memory are used for MMIO.
 pub const DEVICE_REGION: MemoryRegion = MemoryRegion::new(0, 0x40000000);
 
 /// Writable data region for the stack.
 pub fn boot_stack_range() -> Range<VirtualAddress> {
-    const PAGE_SIZE: usize = 4 << 10;
     layout::stack_range(40 * PAGE_SIZE)
 }
 
 pub fn print_addresses() {
-    let dtb = layout::dtb_range();
-    info!("dtb:        {}..{} ({} bytes)", dtb.start, dtb.end, dtb.end - dtb.start);
     let text = layout::text_range();
     info!("text:       {}..{} ({} bytes)", text.start, text.end, text.end - text.start);
     let rodata = layout::rodata_range();
diff --git a/vmbase/example/src/main.rs b/guest/vmbase_example/src/main.rs
similarity index 87%
rename from vmbase/example/src/main.rs
rename to guest/vmbase_example/src/main.rs
index da82b17..7a3f427 100644
--- a/vmbase/example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -25,38 +25,37 @@
 
 use crate::layout::{boot_stack_range, print_addresses, DEVICE_REGION};
 use crate::pci::{check_pci, get_bar_region};
-use aarch64_paging::paging::MemoryRegion;
+use aarch64_paging::paging::VirtualAddress;
 use aarch64_paging::MapError;
 use alloc::{vec, vec::Vec};
+use core::mem;
 use core::ptr::addr_of_mut;
 use cstr::cstr;
 use fdtpci::PciInfo;
 use libfdt::Fdt;
 use log::{debug, error, info, trace, warn, LevelFilter};
 use vmbase::{
-    bionic, configure_heap,
-    layout::{dtb_range, rodata_range, scratch_range, text_range},
+    bionic, configure_heap, generate_image_header,
+    layout::{crosvm::FDT_MAX_SIZE, rodata_range, scratch_range, text_range},
     linker, logger, main,
     memory::{PageTable, SIZE_64KB},
+    util::RangeExt as _,
 };
 
 static INITIALISED_DATA: [u32; 4] = [1, 2, 3, 4];
 static mut ZEROED_DATA: [u32; 10] = [0; 10];
 static mut MUTABLE_DATA: [u32; 4] = [1, 2, 3, 4];
 
+generate_image_header!();
 main!(main);
 configure_heap!(SIZE_64KB);
 
-fn init_page_table(pci_bar_range: &MemoryRegion) -> Result<(), MapError> {
-    let mut page_table = PageTable::default();
-
+fn init_page_table(page_table: &mut PageTable) -> Result<(), MapError> {
     page_table.map_device(&DEVICE_REGION)?;
     page_table.map_code(&text_range().into())?;
     page_table.map_rodata(&rodata_range().into())?;
     page_table.map_data(&scratch_range().into())?;
     page_table.map_data(&boot_stack_range().into())?;
-    page_table.map_rodata(&dtb_range().into())?;
-    page_table.map_device(pci_bar_range)?;
 
     info!("Activating IdMap...");
     // SAFETY: page_table duplicates the static mappings for everything that the Rust code is
@@ -76,15 +75,18 @@
     info!("Hello world");
     info!("x0={:#018x}, x1={:#018x}, x2={:#018x}, x3={:#018x}", arg0, arg1, arg2, arg3);
     print_addresses();
-    assert_eq!(arg0, dtb_range().start.0 as u64);
     check_data();
     check_stack_guard();
 
+    let mut page_table = PageTable::default();
+    init_page_table(&mut page_table).unwrap();
+
     info!("Checking FDT...");
-    let fdt = dtb_range();
-    let fdt_size = fdt.end.0 - fdt.start.0;
+    let fdt_addr = usize::try_from(arg0).unwrap();
     // SAFETY: The DTB range is valid, writable memory, and we don't construct any aliases to it.
-    let fdt = unsafe { core::slice::from_raw_parts_mut(fdt.start.0 as *mut u8, fdt_size) };
+    let fdt = unsafe { core::slice::from_raw_parts_mut(fdt_addr as *mut u8, FDT_MAX_SIZE) };
+    let fdt_region = (VirtualAddress(fdt_addr)..VirtualAddress(fdt_addr + fdt.len())).into();
+    page_table.map_data(&fdt_region).unwrap();
     let fdt = Fdt::from_mut_slice(fdt).unwrap();
     info!("FDT passed verification.");
     check_fdt(fdt);
@@ -96,7 +98,13 @@
 
     check_alloc();
 
-    init_page_table(&get_bar_region(&pci_info)).unwrap();
+    let bar_region = get_bar_region(&pci_info);
+    if bar_region.is_within(&DEVICE_REGION) {
+        // Avoid a MapError::BreakBeforeMakeViolation.
+        info!("BAR region is within already mapped device region: skipping page table ops.");
+    } else {
+        page_table.map_device(&bar_region).unwrap();
+    }
 
     check_data();
     check_dice();
@@ -106,6 +114,10 @@
     check_pci(&mut pci_root);
 
     emit_suppressed_log();
+
+    info!("De-activating IdMap...");
+    mem::drop(page_table); // Release PageTable and switch back to idmap.S
+    info!("De-activated.");
 }
 
 fn check_stack_guard() {
diff --git a/vmbase/example/src/pci.rs b/guest/vmbase_example/src/pci.rs
similarity index 100%
rename from vmbase/example/src/pci.rs
rename to guest/vmbase_example/src/pci.rs
diff --git a/zipfuse/.cargo/config.toml b/guest/zipfuse/.cargo/config.toml
similarity index 100%
rename from zipfuse/.cargo/config.toml
rename to guest/zipfuse/.cargo/config.toml
diff --git a/zipfuse/Android.bp b/guest/zipfuse/Android.bp
similarity index 100%
rename from zipfuse/Android.bp
rename to guest/zipfuse/Android.bp
diff --git a/zipfuse/AndroidTest.xml b/guest/zipfuse/AndroidTest.xml
similarity index 100%
rename from zipfuse/AndroidTest.xml
rename to guest/zipfuse/AndroidTest.xml
diff --git a/zipfuse/Cargo.toml b/guest/zipfuse/Cargo.toml
similarity index 100%
rename from zipfuse/Cargo.toml
rename to guest/zipfuse/Cargo.toml
diff --git a/zipfuse/TEST_MAPPING b/guest/zipfuse/TEST_MAPPING
similarity index 100%
rename from zipfuse/TEST_MAPPING
rename to guest/zipfuse/TEST_MAPPING
diff --git a/zipfuse/src/inode.rs b/guest/zipfuse/src/inode.rs
similarity index 100%
rename from zipfuse/src/inode.rs
rename to guest/zipfuse/src/inode.rs
diff --git a/zipfuse/src/main.rs b/guest/zipfuse/src/main.rs
similarity index 100%
rename from zipfuse/src/main.rs
rename to guest/zipfuse/src/main.rs
diff --git a/zipfuse/testdata/README b/guest/zipfuse/testdata/README
similarity index 100%
rename from zipfuse/testdata/README
rename to guest/zipfuse/testdata/README
diff --git a/zipfuse/testdata/dir/file1 b/guest/zipfuse/testdata/dir/file1
similarity index 100%
rename from zipfuse/testdata/dir/file1
rename to guest/zipfuse/testdata/dir/file1
diff --git a/zipfuse/testdata/dir/file2 b/guest/zipfuse/testdata/dir/file2
similarity index 100%
rename from zipfuse/testdata/dir/file2
rename to guest/zipfuse/testdata/dir/file2
diff --git a/zipfuse/testdata/test.zip b/guest/zipfuse/testdata/test.zip
similarity index 100%
rename from zipfuse/testdata/test.zip
rename to guest/zipfuse/testdata/test.zip
Binary files differ
diff --git a/libs/android_display_backend/Android.bp b/libs/android_display_backend/Android.bp
index 32587dd..f682627 100644
--- a/libs/android_display_backend/Android.bp
+++ b/libs/android_display_backend/Android.bp
@@ -46,6 +46,9 @@
         "android.system.virtualizationcommon-ndk",
         "android.system.virtualizationservice-ndk",
     ],
+    static_libs: [
+        "libbase",
+    ],
     shared_libs: [
         "libbinder_ndk",
         "libnativewindow",
diff --git a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
index e42cdd1..77e3a8c 100644
--- a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -27,4 +27,6 @@
     void setSurface(inout Surface surface, boolean forCursor);
     void setCursorStream(in ParcelFileDescriptor stream);
     void removeSurface(boolean forCursor);
+    void saveFrameForSurface(boolean forCursor);
+    void drawSavedFrameForSurface(boolean forCursor);
 }
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 6e4a793..3802a69 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -16,6 +16,7 @@
 
 #include <aidl/android/crosvm/BnCrosvmAndroidDisplayService.h>
 #include <aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.h>
+#include <android-base/result.h>
 #include <android/binder_manager.h>
 #include <android/binder_process.h>
 #include <system/graphics.h> // for HAL_PIXEL_FORMAT_*
@@ -28,16 +29,16 @@
 using aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal;
 using aidl::android::view::Surface;
 
+using android::base::Error;
+using android::base::Result;
+
 namespace {
 
 class SinkANativeWindow_Buffer {
 public:
-    SinkANativeWindow_Buffer() = default;
-    virtual ~SinkANativeWindow_Buffer() = default;
-
-    bool configure(uint32_t width, uint32_t height, int format) {
+    Result<void> configure(uint32_t width, uint32_t height, int format) {
         if (format != HAL_PIXEL_FORMAT_BGRA_8888) {
-            return false;
+            return Error() << "Pixel format " << format << " is not BGRA_8888.";
         }
 
         mBufferBits.resize(width * height * 4);
@@ -48,7 +49,7 @@
                 .format = format,
                 .bits = mBufferBits.data(),
         };
-        return true;
+        return {};
     }
 
     operator ANativeWindow_Buffer&() { return mBuffer; }
@@ -58,16 +59,28 @@
     std::vector<uint8_t> mBufferBits;
 };
 
-// Wrapper which contains the latest available Surface/ANativeWindow
-// from the DisplayService, if available. A Surface/ANativeWindow may
-// not always be available if, for example, the VmLauncherApp on the
-// other end of the DisplayService is not in the foreground / is paused.
+static Result<void> copyBuffer(ANativeWindow_Buffer& from, ANativeWindow_Buffer& to) {
+    if (from.width != to.width || from.height != to.height) {
+        return Error() << "dimension mismatch. from=(" << from.width << ", " << from.height << ") "
+                       << "to=(" << to.width << ", " << to.height << ")";
+    }
+    uint32_t* dst = reinterpret_cast<uint32_t*>(to.bits);
+    uint32_t* src = reinterpret_cast<uint32_t*>(from.bits);
+    size_t bytes_on_line = to.width * 4; // 4 bytes per pixel
+    for (int32_t h = 0; h < to.height; h++) {
+        memcpy(dst + (h * to.stride), src + (h * from.stride), bytes_on_line);
+    }
+    return {};
+}
+
+// Wrapper which contains the latest available Surface/ANativeWindow from the DisplayService, if
+// available. A Surface/ANativeWindow may not always be available if, for example, the VmLauncherApp
+// on the other end of the DisplayService is not in the foreground / is paused.
 class AndroidDisplaySurface {
 public:
-    AndroidDisplaySurface() = default;
-    virtual ~AndroidDisplaySurface() = default;
+    AndroidDisplaySurface(const std::string& name) : mName(name) {}
 
-    void setSurface(Surface* surface) {
+    void setNativeSurface(Surface* surface) {
         {
             std::lock_guard lk(mSurfaceMutex);
             mNativeSurface = std::make_unique<Surface>(surface->release());
@@ -90,7 +103,7 @@
         return mNativeSurface.get();
     }
 
-    void configure(uint32_t width, uint32_t height) {
+    Result<void> configure(uint32_t width, uint32_t height) {
         std::unique_lock lk(mSurfaceMutex);
 
         mRequestedSurfaceDimensions = Rect{
@@ -98,7 +111,13 @@
                 .height = height,
         };
 
-        mSinkBuffer.configure(width, height, kFormat);
+        if (auto ret = mSinkBuffer.configure(width, height, kFormat); !ret.ok()) {
+            return Error() << "Failed to configure sink buffer: " << ret.error();
+        }
+        if (auto ret = mSavedFrameBuffer.configure(width, height, kFormat); !ret.ok()) {
+            return Error() << "Failed to configure saved frame buffer: " << ret.error();
+        }
+        return {};
     }
 
     void waitForNativeSurface() {
@@ -106,7 +125,7 @@
         mNativeSurfaceReady.wait(lk, [this] { return mNativeSurface != nullptr; });
     }
 
-    int lock(ANativeWindow_Buffer* out_buffer) {
+    Result<void> lock(ANativeWindow_Buffer* out_buffer) {
         std::unique_lock lk(mSurfaceMutex);
 
         Surface* surface = mNativeSurface.get();
@@ -114,62 +133,137 @@
             // Surface not currently available but not necessarily an error
             // if, for example, the VmLauncherApp is not in the foreground.
             *out_buffer = mSinkBuffer;
-            return 0;
+            return {};
         }
 
         ANativeWindow* anw = surface->get();
         if (anw == nullptr) {
-            return -1;
+            return Error() << "Failed to get ANativeWindow";
         }
 
         if (mNativeSurfaceNeedsConfiguring) {
             if (!mRequestedSurfaceDimensions) {
-                return -1;
+                return Error() << "Surface dimension is not configured yet!";
             }
             const auto& dims = *mRequestedSurfaceDimensions;
 
             // Ensure locked buffers have our desired format.
             if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
-                return -1;
+                return Error() << "Failed to set buffer geometry.";
             }
 
             mNativeSurfaceNeedsConfiguring = false;
         }
 
-        return ANativeWindow_lock(anw, out_buffer, nullptr);
+        if (ANativeWindow_lock(anw, out_buffer, nullptr) != 0) {
+            return Error() << "Failed to lock window";
+        }
+        mLastBuffer = *out_buffer;
+        return {};
     }
 
-    int unlockAndPost() {
+    Result<void> unlockAndPost() {
         std::unique_lock lk(mSurfaceMutex);
 
         Surface* surface = mNativeSurface.get();
         if (surface == nullptr) {
             // Surface not currently available but not necessarily an error
             // if, for example, the VmLauncherApp is not in the foreground.
-            return 0;
+            return {};
         }
 
         ANativeWindow* anw = surface->get();
         if (anw == nullptr) {
-            return -1;
+            return Error() << "Failed to get ANativeWindow";
         }
 
-        return ANativeWindow_unlockAndPost(anw);
+        if (ANativeWindow_unlockAndPost(anw) != 0) {
+            return Error() << "Failed to unlock and post window";
+        }
+        return {};
     }
 
+    // Saves the last frame drawn
+    Result<void> saveFrame() {
+        std::unique_lock lk(mSurfaceMutex);
+        if (auto ret = copyBuffer(mLastBuffer, mSavedFrameBuffer); !ret.ok()) {
+            return Error() << "Failed to copy frame: " << ret.error();
+        }
+        return {};
+    }
+
+    // Draws the saved frame
+    Result<void> drawSavedFrame() {
+        std::unique_lock lk(mSurfaceMutex);
+        Surface* surface = mNativeSurface.get();
+        if (surface == nullptr) {
+            return Error() << "Surface not ready";
+        }
+
+        ANativeWindow* anw = surface->get();
+        if (anw == nullptr) {
+            return Error() << "Failed to get ANativeWindow";
+        }
+
+        // TODO: dedup this and the one in lock(...)
+        if (mNativeSurfaceNeedsConfiguring) {
+            if (!mRequestedSurfaceDimensions) {
+                return Error() << "Surface dimension is not configured yet!";
+            }
+            const auto& dims = *mRequestedSurfaceDimensions;
+
+            // Ensure locked buffers have our desired format.
+            if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
+                return Error() << "Failed to set buffer geometry.";
+            }
+
+            mNativeSurfaceNeedsConfiguring = false;
+        }
+
+        ANativeWindow_Buffer buf;
+        if (ANativeWindow_lock(anw, &buf, nullptr) != 0) {
+            return Error() << "Failed to lock window";
+        }
+
+        if (auto ret = copyBuffer(mSavedFrameBuffer, buf); !ret.ok()) {
+            return Error() << "Failed to copy frame: " << ret.error();
+        }
+
+        if (ANativeWindow_unlockAndPost(anw) != 0) {
+            return Error() << "Failed to unlock and post window";
+        }
+        return {};
+    }
+
+    const std::string& name() const { return mName; }
+
 private:
     // Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in
     // crosvm where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope
     // that we will need alpha blending for the cursor surface.
     static constexpr const int kFormat = HAL_PIXEL_FORMAT_BGRA_8888;
 
+    std::string mName;
+
     std::mutex mSurfaceMutex;
     std::unique_ptr<Surface> mNativeSurface;
     std::condition_variable mNativeSurfaceReady;
     bool mNativeSurfaceNeedsConfiguring = true;
 
+    // Buffer which crosvm uses when in background. This is just to not fail crosvm even when
+    // Android-side Surface doesn't exist. The content drawn here is never displayed on the physical
+    // screen.
     SinkANativeWindow_Buffer mSinkBuffer;
 
+    // Buffer which is currently allocated for crosvm to draw onto. This holds the last frame. This
+    // is what gets displayed on the physical screen.
+    ANativeWindow_Buffer mLastBuffer;
+
+    // Copy of mLastBuffer made by the call saveFrameForSurface. This holds the last good (i.e.
+    // non-blank) frame before the VM goes background. When the VM is brought up to foreground,
+    // this is drawn to the physical screen until the VM starts to emit actual frames.
+    SinkANativeWindow_Buffer mSavedFrameBuffer;
+
     struct Rect {
         uint32_t width = 0;
         uint32_t height = 0;
@@ -183,35 +277,50 @@
     virtual ~DisplayService() = default;
 
     ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
-        if (forCursor) {
-            mCursor.setSurface(surface);
-        } else {
-            mScanout.setSurface(surface);
-        }
+        getSurface(forCursor).setNativeSurface(surface);
         return ::ndk::ScopedAStatus::ok();
     }
 
     ndk::ScopedAStatus removeSurface(bool forCursor) override {
-        if (forCursor) {
-            mCursor.removeSurface();
-        } else {
-            mScanout.removeSurface();
-        }
+        getSurface(forCursor).removeSurface();
         return ::ndk::ScopedAStatus::ok();
     }
 
-    AndroidDisplaySurface* getCursorSurface() { return &mCursor; }
-    AndroidDisplaySurface* getScanoutSurface() { return &mScanout; }
-
     ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
     ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
         mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
         return ::ndk::ScopedAStatus::ok();
     }
 
+    ndk::ScopedAStatus saveFrameForSurface(bool forCursor) override {
+        if (auto ret = getSurface(forCursor).saveFrame(); !ret.ok()) {
+            std::string msg = std::format("Failed to save frame: {}", ret.error().message());
+            return ::ndk::ScopedAStatus(
+                    AStatus_fromServiceSpecificErrorWithMessage(-1, msg.c_str()));
+        }
+        return ::ndk::ScopedAStatus::ok();
+    }
+
+    ndk::ScopedAStatus drawSavedFrameForSurface(bool forCursor) override {
+        if (auto ret = getSurface(forCursor).drawSavedFrame(); !ret.ok()) {
+            std::string msg = std::format("Failed to draw saved frame: {}", ret.error().message());
+            return ::ndk::ScopedAStatus(
+                    AStatus_fromServiceSpecificErrorWithMessage(-1, msg.c_str()));
+        }
+        return ::ndk::ScopedAStatus::ok();
+    }
+
+    AndroidDisplaySurface& getSurface(bool forCursor) {
+        if (forCursor) {
+            return mCursor;
+        } else {
+            return mScanout;
+        }
+    }
+
 private:
-    AndroidDisplaySurface mScanout;
-    AndroidDisplaySurface mCursor;
+    AndroidDisplaySurface mScanout{"scanout"};
+    AndroidDisplaySurface mCursor{"cursor"};
     ndk::ScopedFileDescriptor mCursorStream;
 };
 
@@ -287,21 +396,18 @@
         return nullptr;
     }
 
-    AndroidDisplaySurface* displaySurface = forCursor ? ctx->disp_service->getCursorSurface()
-                                                      : ctx->disp_service->getScanoutSurface();
-    if (displaySurface == nullptr) {
-        ctx->errorf("AndroidDisplaySurface was not created");
-        return nullptr;
+    AndroidDisplaySurface& surface = ctx->disp_service->getSurface(forCursor);
+    if (auto ret = surface.configure(width, height); !ret.ok()) {
+        ctx->errorf("Failed to configure surface %s: %s", surface.name().c_str(),
+                    ret.error().message().c_str());
     }
 
-    displaySurface->configure(width, height);
-
-    displaySurface->waitForNativeSurface(); // this can block
+    surface.waitForNativeSurface(); // this can block
 
     // TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
     // consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
     // crosvm doesn't hold a dangling pointer.
-    return displaySurface;
+    return &surface;
 }
 
 extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
@@ -321,8 +427,10 @@
         return false;
     }
 
-    if (surface->lock(out_buffer) != 0) {
-        ctx->errorf("Failed to lock buffer");
+    auto ret = surface->lock(out_buffer);
+    if (!ret.ok()) {
+        ctx->errorf("Failed to lock surface %s: %s", surface->name().c_str(),
+                    ret.error().message().c_str());
         return false;
     }
 
@@ -351,8 +459,10 @@
         return;
     }
 
-    if (surface->unlockAndPost() != 0) {
-        ctx->errorf("Failed to unlock and post AndroidDisplaySurface.");
-        return;
+    auto ret = surface->unlockAndPost();
+    if (!ret.ok()) {
+        ctx->errorf("Failed to unlock and post for surface %s: %s", surface->name().c_str(),
+                    ret.error().message().c_str());
     }
+    return;
 }
diff --git a/libs/apkverify/src/sigutil.rs b/libs/apkverify/src/sigutil.rs
index 7d03bb2..a47b4c5 100644
--- a/libs/apkverify/src/sigutil.rs
+++ b/libs/apkverify/src/sigutil.rs
@@ -79,6 +79,7 @@
     /// 2. The top-level digest is computed over the concatenation of byte 0x5a, the number of
     ///    chunks (little-endian uint32), and the concatenation of digests of the chunks in the
     ///    order the chunks appear in the APK.
+    ///
     /// (see https://source.android.com/security/apksigning/v2#integrity-protected-contents)
     pub(crate) fn compute_digest(
         &mut self,
diff --git a/authfs/aidl/Android.bp b/libs/authfs_aidl_interface/Android.bp
similarity index 100%
rename from authfs/aidl/Android.bp
rename to libs/authfs_aidl_interface/Android.bp
diff --git a/authfs/TEST_MAPPING b/libs/authfs_aidl_interface/TEST_MAPPING
similarity index 100%
copy from authfs/TEST_MAPPING
copy to libs/authfs_aidl_interface/TEST_MAPPING
diff --git a/authfs/aidl/com/android/virt/fs/AuthFsConfig.aidl b/libs/authfs_aidl_interface/com/android/virt/fs/AuthFsConfig.aidl
similarity index 100%
rename from authfs/aidl/com/android/virt/fs/AuthFsConfig.aidl
rename to libs/authfs_aidl_interface/com/android/virt/fs/AuthFsConfig.aidl
diff --git a/authfs/aidl/com/android/virt/fs/IAuthFs.aidl b/libs/authfs_aidl_interface/com/android/virt/fs/IAuthFs.aidl
similarity index 100%
rename from authfs/aidl/com/android/virt/fs/IAuthFs.aidl
rename to libs/authfs_aidl_interface/com/android/virt/fs/IAuthFs.aidl
diff --git a/authfs/aidl/com/android/virt/fs/IAuthFsService.aidl b/libs/authfs_aidl_interface/com/android/virt/fs/IAuthFsService.aidl
similarity index 100%
rename from authfs/aidl/com/android/virt/fs/IAuthFsService.aidl
rename to libs/authfs_aidl_interface/com/android/virt/fs/IAuthFsService.aidl
diff --git a/authfs/aidl/com/android/virt/fs/IVirtFdService.aidl b/libs/authfs_aidl_interface/com/android/virt/fs/IVirtFdService.aidl
similarity index 100%
rename from authfs/aidl/com/android/virt/fs/IVirtFdService.aidl
rename to libs/authfs_aidl_interface/com/android/virt/fs/IVirtFdService.aidl
diff --git a/compos/aidl/Android.bp b/libs/compos_aidl_interface/Android.bp
similarity index 100%
rename from compos/aidl/Android.bp
rename to libs/compos_aidl_interface/Android.bp
diff --git a/compos/aidl/com/android/compos/ICompOsService.aidl b/libs/compos_aidl_interface/com/android/compos/ICompOsService.aidl
similarity index 100%
rename from compos/aidl/com/android/compos/ICompOsService.aidl
rename to libs/compos_aidl_interface/com/android/compos/ICompOsService.aidl
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index 62504d1..efe350f 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -92,6 +92,9 @@
         "--ctypes-prefix=core::ffi",
         "--raw-line=#![no_std]",
     ],
+    dylib: {
+        enabled: false,
+    },
     no_stdlibs: true,
     prefer_rlib: true,
     stdlibs: [
diff --git a/libs/fdtpci/src/lib.rs b/libs/fdtpci/src/lib.rs
index 602f736..bdd904f 100644
--- a/libs/fdtpci/src/lib.rs
+++ b/libs/fdtpci/src/lib.rs
@@ -206,7 +206,7 @@
 impl PciMemoryFlags {
     /// Returns whether this PCI range is prefetchable
     pub fn prefetchable(self) -> bool {
-        self.0 & 0x80000000 != 0
+        self.0 & 0x40000000 != 0
     }
 
     /// Returns the type of this PCI range
diff --git a/java/framework/Android.bp b/libs/framework-virtualization/Android.bp
similarity index 68%
rename from java/framework/Android.bp
rename to libs/framework-virtualization/Android.bp
index 7c0240d..d02eec6 100644
--- a/java/framework/Android.bp
+++ b/libs/framework-virtualization/Android.bp
@@ -9,7 +9,10 @@
 
     jarjar_rules: "jarjar-rules.txt",
 
-    srcs: ["src/**/*.java"],
+    srcs: [
+        "src/**/*.java",
+        ":avf-build-flags-java-gen",
+    ],
     static_libs: [
         "android.system.virtualizationservice-java",
         "avf_aconfig_flags_java",
@@ -47,8 +50,21 @@
         "avf_aconfig_flags",
     ],
     lint: {
+        baseline_filename: "lint-baseline.xml",
         warning_checks: [
             "FlaggedApi",
         ],
     },
 }
+
+gensrcs {
+    name: "avf-build-flags-java-gen",
+    srcs: ["src/**/BuildFlags.java_template"],
+    output_extension: "java",
+    cmd: "cp $(in) $(genDir)/tmp.java && " +
+        select(release_flag("RELEASE_AVF_ENABLE_VENDOR_MODULES"), {
+            true: "sed -ie 's/@vendor_modules_enabled_placeholder/true/g'",
+            default: "sed -ie 's/@vendor_modules_enabled_placeholder/false/g'",
+        }) + " $(genDir)/tmp.java && " +
+        " cp $(genDir)/tmp.java $(out)",
+}
diff --git a/java/framework/README.md b/libs/framework-virtualization/README.md
similarity index 95%
rename from java/framework/README.md
rename to libs/framework-virtualization/README.md
index 61ba096..0dd7e64 100644
--- a/java/framework/README.md
+++ b/libs/framework-virtualization/README.md
@@ -1,12 +1,12 @@
 # Android Virtualization Framework API
 
 These Java APIs allow an app to configure and run a Virtual Machine running
-[Microdroid](../microdroid/README.md) and execute native code from the app (the
+[Microdroid](../build/microdroid/README.md) and execute native code from the app (the
 payload) within it.
 
 There is more information on AVF [here](../README.md). To see how to package the
 payload code that is to run inside a VM, and the native API available to it, see
-the [VM Payload API](../vm_payload/README.md)
+the [VM Payload API](../libs/libvm_payload/README.md)
 
 The API classes are all in the
 [`android.system.virtualmachine`](src/android/system/virtualmachine) package.
@@ -137,10 +137,10 @@
 - `onPayloadStarted()`: The VM payload is about to be run.
 - `onPayloadReady()`: The VM payload is running and ready to accept
   connections. (This notification is triggered by the payload code, using the
-  [`AVmPayload_notifyPayloadReady()`](../vm_payload/include/vm_payload.h)
+  [`AVmPayload_notifyPayloadReady()`](../libs/libvm_payload/include/vm_payload.h)
   function.)
 - `onPayloadFinished()`: The VM payload has exited normally. The exit code of
-  the VM (the value returned by [`AVmPayload_main()`](../vm_payload/README.md))
+  the VM (the value returned by [`AVmPayload_main()`](../libs/libvm_payload/README.md))
   is supplied as a parameter.
 - `onError()`: The VM failed; something went wrong. An error code and
   human-readable message are provided which may help diagnosing the problem.
@@ -195,7 +195,7 @@
 The payload code is not given direct access to the VM secret, but an API is
 provided to allow deterministically deriving further secrets from it,
 e.g. encryption or signing keys. See
-[`AVmPayload_getVmInstanceSecret()`](../vm_payload/include/vm_payload.h).
+[`AVmPayload_getVmInstanceSecret()`](../libs/libvm_payload/include/vm_payload.h).
 
 Some VM configuration changes are allowed that don’t affect the identity -
 e.g. changing the number of CPUs or the amount of memory allocated to the
@@ -304,7 +304,7 @@
 the `onStopped()` callback).
 
 Then you can arrange for your VM payload code to exit when it has finished its
-task (by returning from [`AVmPayload_main()`](../vm_payload/README.md), or
+task (by returning from [`AVmPayload_main()`](../libs/libvm_payload/README.md), or
 calling `exit()`). Alternatively you could exit when you receive a request to do
 so from the app, e.g. via binder.
 
@@ -330,7 +330,7 @@
 builder.
 
 Inside the VM this storage is mounted at a path that can be retrieved via the
-[`AVmPayload_getEncryptedStoragePath()`](../vm_payload/include/vm_payload.h)
+[`AVmPayload_getEncryptedStoragePath()`](../libs/libvm_payload/include/vm_payload.h)
 function. The VM can create sub-directories and read and write files here. Any
 data written is persisted and should be available next time the VM is run. (An
 automatic sync is done when the payload exits normally.)
@@ -343,7 +343,7 @@
 powerful attacker could delete it, wholly or partially roll it back to an
 earlier version, or modify it, corrupting the data.
 
-For more info see [README](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/java/framework/README.md)
+For more info see [README](https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/framework-virtualization/README.md)
 
 # Transferring a VM
 
diff --git a/java/framework/api/current.txt b/libs/framework-virtualization/api/current.txt
similarity index 100%
rename from java/framework/api/current.txt
rename to libs/framework-virtualization/api/current.txt
diff --git a/java/framework/api/module-lib-current.txt b/libs/framework-virtualization/api/module-lib-current.txt
similarity index 100%
rename from java/framework/api/module-lib-current.txt
rename to libs/framework-virtualization/api/module-lib-current.txt
diff --git a/java/framework/api/module-lib-removed.txt b/libs/framework-virtualization/api/module-lib-removed.txt
similarity index 100%
rename from java/framework/api/module-lib-removed.txt
rename to libs/framework-virtualization/api/module-lib-removed.txt
diff --git a/java/framework/api/removed.txt b/libs/framework-virtualization/api/removed.txt
similarity index 100%
rename from java/framework/api/removed.txt
rename to libs/framework-virtualization/api/removed.txt
diff --git a/java/framework/api/system-current.txt b/libs/framework-virtualization/api/system-current.txt
similarity index 100%
rename from java/framework/api/system-current.txt
rename to libs/framework-virtualization/api/system-current.txt
diff --git a/java/framework/api/system-removed.txt b/libs/framework-virtualization/api/system-removed.txt
similarity index 100%
rename from java/framework/api/system-removed.txt
rename to libs/framework-virtualization/api/system-removed.txt
diff --git a/java/framework/api/test-current.txt b/libs/framework-virtualization/api/test-current.txt
similarity index 100%
rename from java/framework/api/test-current.txt
rename to libs/framework-virtualization/api/test-current.txt
diff --git a/java/framework/api/test-removed.txt b/libs/framework-virtualization/api/test-removed.txt
similarity index 100%
rename from java/framework/api/test-removed.txt
rename to libs/framework-virtualization/api/test-removed.txt
diff --git a/java/framework/jarjar-rules.txt b/libs/framework-virtualization/jarjar-rules.txt
similarity index 100%
rename from java/framework/jarjar-rules.txt
rename to libs/framework-virtualization/jarjar-rules.txt
diff --git a/libs/framework-virtualization/lint-baseline.xml b/libs/framework-virtualization/lint-baseline.xml
new file mode 100644
index 0000000..a77a80b
--- /dev/null
+++ b/libs/framework-virtualization/lint-baseline.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getExtraApks()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `createVirtualMachineConfigForAppFrom` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        if (!vmConfig.getExtraApks().isEmpty()) {"
+        errorLine2="             ~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java"
+            line="1105"
+            column="14"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `getExtraApks()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `setupExtraApks` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        List&lt;String> extraApks = config.getExtraApks();"
+        errorLine2="                                 ~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java"
+            line="1730"
+            column="34"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setVendorDiskImage()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="            builder.setVendorDiskImage(new File(vendorDiskImagePath));"
+        errorLine2="            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="367"
+            column="13"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `setOs()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        builder.setOs(b.getString(KEY_OS));"
+        errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="370"
+            column="9"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Method `addExtraApk()` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `fromPersistableBundle` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="                builder.addExtraApk(extraApk);"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="375"
+            column="17"/>
+    </issue>
+
+    <issue
+        id="FlaggedApi"
+        message="Field `MICRODROID` is a flagged API and should be inside an `if (Flags.avfVTestApis())` check (or annotate the surrounding method `?` with `@FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS) to transfer requirement to caller`)"
+        errorLine1="        @OsName private final String DEFAULT_OS = MICRODROID;"
+        errorLine2="                                                  ~~~~~~~~~~">
+        <location
+		file="packages/modules/Virtualization/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java"
+            line="855"
+            column="51"/>
+    </issue>
+
+</issues>
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/BuildFlags.java_template b/libs/framework-virtualization/src/android/system/virtualmachine/BuildFlags.java_template
new file mode 100644
index 0000000..12b249c
--- /dev/null
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/BuildFlags.java_template
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 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 android.system.virtualmachine;
+
+/**
+ * Exposes AVF build flags (RELEASE_AVF_*) to java.
+ *
+ * @hide
+ */
+public final class BuildFlags {
+
+    /**
+     * Value of the {@code RELEASE_AVF_ENABLE_VENDOR_MODULES} build flag.
+     */
+    public static boolean VENDOR_MODULES_ENABLED = @vendor_modules_enabled_placeholder;
+
+    private BuildFlags() {};
+}
+
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
similarity index 96%
rename from java/framework/src/android/system/virtualmachine/VirtualMachine.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
index b6de9bb..3b16a8a 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachine.java
@@ -132,7 +132,7 @@
  * #connectToVsockServer} or {@link #connectVsock}.
  *
  * <p>The payload code running inside the VM has access to a set of native APIs; see the <a
- * href="https://cs.android.com/android/platform/superproject/+/master:packages/modules/Virtualization/vm_payload/README.md">README
+ * href="https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/libvm_payload/README.md">README
  * file</a> for details.
  *
  * <p>Each VM has a unique secret, computed from the APK that contains the code running in it, the
@@ -193,11 +193,9 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = "STATUS_", value = {
-            STATUS_STOPPED,
-            STATUS_RUNNING,
-            STATUS_DELETED
-    })
+    @IntDef(
+            prefix = "STATUS_",
+            value = {STATUS_STOPPED, STATUS_RUNNING, STATUS_DELETED})
     public @interface Status {}
 
     /** The virtual machine has just been created, or {@link #stop} was called on it. */
@@ -245,9 +243,7 @@
     /** Name of this VM within the package. The name should be unique in the package. */
     @NonNull private final String mName;
 
-    /**
-     * Path to the directory containing all the files related to this VM.
-     */
+    /** Path to the directory containing all the files related to this VM. */
     @NonNull private final File mVmRootPath;
 
     /**
@@ -691,8 +687,7 @@
             try {
                 byte[] instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
                 service.claimVmInstance(instanceId);
-            }
-            catch (IOException e) {
+            } catch (IOException e) {
                 throw new VirtualMachineException("failed to read instance_id", e);
             } catch (RemoteException e) {
                 throw e.rethrowAsRuntimeException();
@@ -998,7 +993,8 @@
     /** @hide */
     public boolean sendMouseEvent(MotionEvent event) {
         try {
-            mInputEventQueue.add(Pair.create(InputEventType.MOUSE, event));
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.MOUSE, MotionEvent.obtainNoHistory(event)));
             return true;
         } catch (Exception e) {
             Log.e(TAG, e.toString());
@@ -1097,7 +1093,8 @@
     /** @hide */
     public boolean sendMultiTouchEvent(MotionEvent event) {
         try {
-            mInputEventQueue.add(Pair.create(InputEventType.TOUCH, event));
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.TOUCH, MotionEvent.obtainNoHistory(event)));
             return true;
         } catch (Exception e) {
             Log.e(TAG, e.toString());
@@ -1192,9 +1189,29 @@
     }
 
     /** @hide */
+    public boolean sendTabletModeEvent(boolean tabletMode) {
+        if (mSwitchesSock == null) {
+            Log.d(TAG, "mSwitcheSock == null");
+            return false;
+        }
+
+        // from include/uapi/linux/input-event-codes.h in the kernel.
+        short EV_SYN = 0x00;
+        short EV_SW = 0x05;
+        short SW_TABLET_MODE = 0x01;
+        short SYN_REPORT = 0x00;
+        return writeEventsToSock(
+                mSwitchesSock,
+                Arrays.asList(
+                        new InputEvent(EV_SW, SW_TABLET_MODE, tabletMode ? 1 : 0),
+                        new InputEvent(EV_SYN, SYN_REPORT, 0)));
+    }
+
+    /** @hide */
     public boolean sendTrackpadEvent(MotionEvent event) {
         try {
-            mInputEventQueue.add(Pair.create(InputEventType.TRACKPAD, event));
+            mInputEventQueue.add(
+                    Pair.create(InputEventType.TRACKPAD, MotionEvent.obtainNoHistory(event)));
             return true;
         } catch (Exception e) {
             Log.e(TAG, e.toString());
@@ -1541,6 +1558,7 @@
                                             sendMouseEventInternal(event.second);
                                             break;
                                     }
+                                    event.second.recycle();
                                 } catch (Exception e) {
                                     Log.e(TAG, e.toString());
                                 }
@@ -1750,7 +1768,6 @@
         }
     }
 
-
     /**
      * Returns the stream object representing the log output from the virtual machine. The log
      * output is only available if the VirtualMachineConfig specifies that it should be {@linkplain
@@ -1884,22 +1901,26 @@
     private static void deleteRecursively(File dir) throws IOException {
         // Note: This doesn't follow symlinks, which is important. Instead they are just deleted
         // (and Files.delete deletes the link not the target).
-        Files.walkFileTree(dir.toPath(), new SimpleFileVisitor<>() {
-            @Override
-            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
-                    throws IOException {
-                Files.delete(file);
-                return FileVisitResult.CONTINUE;
-            }
+        Files.walkFileTree(
+                dir.toPath(),
+                new SimpleFileVisitor<>() {
+                    @Override
+                    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+                            throws IOException {
+                        Files.delete(file);
+                        return FileVisitResult.CONTINUE;
+                    }
 
-            @Override
-            public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException {
-                // Directory is deleted after we've visited (deleted) all its contents, so it
-                // should be empty by now.
-                Files.delete(dir);
-                return FileVisitResult.CONTINUE;
-            }
-        });
+                    @Override
+                    public FileVisitResult postVisitDirectory(Path dir, IOException e)
+                            throws IOException {
+                        // Directory is deleted after we've visited (deleted) all its contents, so
+                        // it
+                        // should be empty by now.
+                        Files.delete(dir);
+                        return FileVisitResult.CONTINUE;
+                    }
+                });
     }
 
     /**
@@ -2087,21 +2108,14 @@
         String payloadBinaryName = config.getPayloadBinaryName();
 
         StringBuilder result = new StringBuilder();
-        result.append("VirtualMachine(")
-                .append("name:")
-                .append(getName())
-                .append(", ");
+        result.append("VirtualMachine(").append("name:").append(getName()).append(", ");
         if (payloadBinaryName != null) {
             result.append("payload:").append(payloadBinaryName).append(", ");
         }
         if (payloadConfigPath != null) {
-            result.append("config:")
-                    .append(payloadConfigPath)
-                    .append(", ");
+            result.append("config:").append(payloadConfigPath).append(", ");
         }
-        result.append("package: ")
-                .append(mPackageName)
-                .append(")");
+        result.append("package: ").append(mPackageName).append(")");
         return result.toString();
     }
 
@@ -2148,7 +2162,7 @@
     private static List<String> parseExtraApkListFromPayloadConfig(JsonReader reader)
             throws VirtualMachineException {
         /*
-         * JSON schema from packages/modules/Virtualization/microdroid/payload/config/src/lib.rs:
+         * JSON schema from packages/modules/Virtualization/microdroid/libs/libmicrodroid_payload_metadata/config/src/lib.rs:
          *
          * <p>{ "extra_apks": [ { "path": "/system/app/foo.apk", }, ... ], ... }
          */
@@ -2209,7 +2223,7 @@
             throws VirtualMachineException {
         try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
                 FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
-            idOutput.transferFrom(idInput, /*position=*/ 0, idInput.size());
+            idOutput.transferFrom(idInput, /* position= */ 0, idInput.size());
         } catch (IOException e) {
             throw new VirtualMachineException("failed to copy instance_id", e);
         }
@@ -2219,7 +2233,7 @@
             throws VirtualMachineException {
         try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
                 FileChannel instanceInput = new AutoCloseInputStream(instanceFd).getChannel()) {
-            instance.transferFrom(instanceInput, /*position=*/ 0, instanceInput.size());
+            instance.transferFrom(instanceInput, /* position= */ 0, instanceInput.size());
         } catch (IOException e) {
             throw new VirtualMachineException("failed to transfer instance image", e);
         }
@@ -2229,7 +2243,7 @@
             throws VirtualMachineException {
         try (FileChannel storeOutput = new FileOutputStream(mEncryptedStoreFilePath).getChannel();
                 FileChannel storeInput = new AutoCloseInputStream(encryptedStoreFd).getChannel()) {
-            storeOutput.transferFrom(storeInput, /*position=*/ 0, storeInput.size());
+            storeOutput.transferFrom(storeInput, /* position= */ 0, storeInput.size());
         } catch (IOException e) {
             throw new VirtualMachineException("failed to transfer encryptedstore image", e);
         }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java
similarity index 95%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java
index d72ba14..b077826 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCallback.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCallback.java
@@ -35,23 +35,28 @@
 public interface VirtualMachineCallback {
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = "ERROR_", value = {
-        ERROR_UNKNOWN,
-        ERROR_PAYLOAD_VERIFICATION_FAILED,
-        ERROR_PAYLOAD_CHANGED,
-        ERROR_PAYLOAD_INVALID_CONFIG
-    })
+    @IntDef(
+            prefix = "ERROR_",
+            value = {
+                ERROR_UNKNOWN,
+                ERROR_PAYLOAD_VERIFICATION_FAILED,
+                ERROR_PAYLOAD_CHANGED,
+                ERROR_PAYLOAD_INVALID_CONFIG
+            })
     @interface ErrorCode {}
 
     /** Error code for all other errors not listed below. */
     int ERROR_UNKNOWN = 0;
+
     /**
      * Error code indicating that the payload can't be verified due to various reasons (e.g invalid
      * merkle tree, invalid formats, etc).
      */
     int ERROR_PAYLOAD_VERIFICATION_FAILED = 1;
+
     /** Error code indicating that the payload is verified, but has changed since the last boot. */
     int ERROR_PAYLOAD_CHANGED = 2;
+
     /** Error code indicating that the payload config is invalid. */
     int ERROR_PAYLOAD_INVALID_CONFIG = 3;
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
similarity index 98%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index a2d8ee6..7ae4a55 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -105,10 +105,9 @@
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = "DEBUG_LEVEL_", value = {
-            DEBUG_LEVEL_NONE,
-            DEBUG_LEVEL_FULL
-    })
+    @IntDef(
+            prefix = "DEBUG_LEVEL_",
+            value = {DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL})
     public @interface DebugLevel {}
 
     /**
@@ -164,9 +163,7 @@
 
     @DebugLevel private final int mDebugLevel;
 
-    /**
-     * Whether to run the VM in protected mode, so the host can't access its memory.
-     */
+    /** Whether to run the VM in protected mode, so the host can't access its memory. */
     private final boolean mProtectedVm;
 
     /**
@@ -181,9 +178,7 @@
     /** The serial device for VM console input. */
     @Nullable private final String mConsoleInputDevice;
 
-    /**
-     * Path within the APK to the payload config file that defines software aspects of the VM.
-     */
+    /** Path within the APK to the payload config file that defines software aspects of the VM. */
     @Nullable private final String mPayloadConfigPath;
 
     /** Name of the payload binary file within the APK that will be executed within the VM. */
@@ -754,11 +749,9 @@
             VirtualMachinePayloadConfig payloadConfig = new VirtualMachinePayloadConfig();
             payloadConfig.payloadBinaryName = mPayloadBinaryName;
             payloadConfig.extraApks = Collections.emptyList();
-            vsConfig.payload =
-                    VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
+            vsConfig.payload = VirtualMachineAppConfig.Payload.payloadConfig(payloadConfig);
         } else {
-            vsConfig.payload =
-                    VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
+            vsConfig.payload = VirtualMachineAppConfig.Payload.configPath(mPayloadConfigPath);
         }
         vsConfig.osName = mOs;
         switch (mDebugLevel) {
@@ -1013,7 +1006,8 @@
         /**
          * Sets the path within the APK to the payload config file that defines software aspects of
          * the VM. The file is a JSON file; see
-         * packages/modules/Virtualization/microdroid/payload/config/src/lib.rs for the format.
+         * packages/modules/Virtualization/libs/libmicrodroid_payload_metadata/config/src/lib.rs for
+         * the format.
          *
          * @hide
          */
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
similarity index 99%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 2da83a0..37dc8fa 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -211,8 +211,6 @@
         return builder.build();
     }
 
-
-
     PersistableBundle toPersistableBundle() {
         PersistableBundle pb = new PersistableBundle();
         pb.putString(KEY_NAME, this.name);
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineDescriptor.java
similarity index 100%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineDescriptor.java
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineException.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineException.java
similarity index 100%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineException.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineException.java
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java
similarity index 97%
rename from java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java
index abb2c81..9295c6c 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -89,10 +89,10 @@
      * @hide
      */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef(prefix = "CAPABILITY_", flag = true, value = {
-            CAPABILITY_PROTECTED_VM,
-            CAPABILITY_NON_PROTECTED_VM
-    })
+    @IntDef(
+            prefix = "CAPABILITY_",
+            flag = true,
+            value = {CAPABILITY_PROTECTED_VM, CAPABILITY_NON_PROTECTED_VM})
     public @interface Capability {}
 
     /**
@@ -371,10 +371,6 @@
     private static final List<String> SUPPORTED_OS_LIST_FROM_CFG =
             extractSupportedOSListFromConfig();
 
-    private boolean isVendorModuleEnabled() {
-        return VirtualizationService.nativeIsVendorModulesFlagEnabled();
-    }
-
     private static List<String> extractSupportedOSListFromConfig() {
         List<String> supportedOsList = new ArrayList<>();
         File directory = new File("/apex/com.android.virt/etc");
@@ -400,7 +396,7 @@
     @FlaggedApi(Flags.FLAG_AVF_V_TEST_APIS)
     @NonNull
     public List<String> getSupportedOSList() throws VirtualMachineException {
-        if (isVendorModuleEnabled()) {
+        if (BuildFlags.VENDOR_MODULES_ENABLED) {
             return SUPPORTED_OS_LIST_FROM_CFG;
         } else {
             return Arrays.asList("microdroid");
diff --git a/java/framework/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
similarity index 100%
rename from java/framework/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationFrameworkInitializer.java
diff --git a/java/framework/src/android/system/virtualmachine/VirtualizationService.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java
similarity index 94%
rename from java/framework/src/android/system/virtualmachine/VirtualizationService.java
rename to libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java
index 83b64ee..57990a9 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualizationService.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualizationService.java
@@ -51,12 +51,6 @@
     private native boolean nativeIsOk(int clientFd);
 
     /*
-     * Retrieve boolean value whether RELEASE_AVF_ENABLE_VENDOR_MODULES build flag is enabled or
-     * not.
-     */
-    static native boolean nativeIsVendorModulesFlagEnabled();
-
-    /*
      * Spawns a new virtmgr subprocess that will host a VirtualizationService
      * AIDL service.
      */
diff --git a/flags/cpp/Android.bp b/libs/libavf_cc_flags/Android.bp
similarity index 100%
rename from flags/cpp/Android.bp
rename to libs/libavf_cc_flags/Android.bp
diff --git a/flags/cpp/include/android/avf_cc_flags.h b/libs/libavf_cc_flags/include/android/avf_cc_flags.h
similarity index 100%
rename from flags/cpp/include/android/avf_cc_flags.h
rename to libs/libavf_cc_flags/include/android/avf_cc_flags.h
diff --git a/service_vm/client_vm_csr/Android.bp b/libs/libclient_vm_csr/Android.bp
similarity index 100%
rename from service_vm/client_vm_csr/Android.bp
rename to libs/libclient_vm_csr/Android.bp
diff --git a/service_vm/client_vm_csr/TEST_MAPPING b/libs/libclient_vm_csr/TEST_MAPPING
similarity index 100%
rename from service_vm/client_vm_csr/TEST_MAPPING
rename to libs/libclient_vm_csr/TEST_MAPPING
diff --git a/service_vm/client_vm_csr/src/lib.rs b/libs/libclient_vm_csr/src/lib.rs
similarity index 98%
rename from service_vm/client_vm_csr/src/lib.rs
rename to libs/libclient_vm_csr/src/lib.rs
index 70152cb..77cfea9 100644
--- a/service_vm/client_vm_csr/src/lib.rs
+++ b/libs/libclient_vm_csr/src/lib.rs
@@ -36,7 +36,7 @@
 
 /// Key parameters for the attestation key.
 ///
-/// See service_vm/comm/client_vm_csr.cddl for more information about the attestation key.
+/// See libs/libservice_vm_comm/client_vm_csr.cddl for more information about the attestation key.
 const ATTESTATION_KEY_NID: Nid = Nid::X9_62_PRIME256V1; // NIST P-256 curve
 const ATTESTATION_KEY_ALGO: iana::Algorithm = iana::Algorithm::ES256;
 const ATTESTATION_KEY_CURVE: iana::EllipticCurve = iana::EllipticCurve::P_256;
diff --git a/compos/common/Android.bp b/libs/libcompos_common/Android.bp
similarity index 100%
rename from compos/common/Android.bp
rename to libs/libcompos_common/Android.bp
diff --git a/compos/common/binder.rs b/libs/libcompos_common/binder.rs
similarity index 100%
rename from compos/common/binder.rs
rename to libs/libcompos_common/binder.rs
diff --git a/compos/common/compos_client.rs b/libs/libcompos_common/compos_client.rs
similarity index 100%
rename from compos/common/compos_client.rs
rename to libs/libcompos_common/compos_client.rs
diff --git a/compos/common/lib.rs b/libs/libcompos_common/lib.rs
similarity index 100%
rename from compos/common/lib.rs
rename to libs/libcompos_common/lib.rs
diff --git a/compos/common/odrefresh.rs b/libs/libcompos_common/odrefresh.rs
similarity index 100%
rename from compos/common/odrefresh.rs
rename to libs/libcompos_common/odrefresh.rs
diff --git a/compos/common/timeouts.rs b/libs/libcompos_common/timeouts.rs
similarity index 100%
rename from compos/common/timeouts.rs
rename to libs/libcompos_common/timeouts.rs
diff --git a/libs/libfdt/Android.bp b/libs/libfdt/Android.bp
index 7dc9e64..b2e7b2b 100644
--- a/libs/libfdt/Android.bp
+++ b/libs/libfdt/Android.bp
@@ -16,6 +16,9 @@
         "--raw-line=#![no_std]",
         "--ctypes-prefix=core::ffi",
     ],
+    dylib: {
+        enabled: false,
+    },
     static_libs: [
         "libfdt",
     ],
diff --git a/microdroid/payload/Android.bp b/libs/libmicrodroid_payload_metadata/Android.bp
similarity index 100%
rename from microdroid/payload/Android.bp
rename to libs/libmicrodroid_payload_metadata/Android.bp
diff --git a/microdroid/payload/README.md b/libs/libmicrodroid_payload_metadata/README.md
similarity index 92%
rename from microdroid/payload/README.md
rename to libs/libmicrodroid_payload_metadata/README.md
index b1eb63f..98ace61 100644
--- a/microdroid/payload/README.md
+++ b/libs/libmicrodroid_payload_metadata/README.md
@@ -3,7 +3,7 @@
 Payload disk is a composite disk image referencing host APEXes and an APK so that microdroid
 mounts/activates APK/APEXes and executes a binary within the APK.
 
-Payload disk is created by [VirtualizationService](../../virtualizationservice) Service when
+Payload disk is created by [VirtualizationService](../../android/virtualizationservice) Service when
 starting a VM.
 
 ## Partitions
diff --git a/microdroid/payload/config/Android.bp b/libs/libmicrodroid_payload_metadata/config/Android.bp
similarity index 100%
rename from microdroid/payload/config/Android.bp
rename to libs/libmicrodroid_payload_metadata/config/Android.bp
diff --git a/microdroid/payload/config/src/lib.rs b/libs/libmicrodroid_payload_metadata/config/src/lib.rs
similarity index 100%
rename from microdroid/payload/config/src/lib.rs
rename to libs/libmicrodroid_payload_metadata/config/src/lib.rs
diff --git a/microdroid/payload/include/microdroid/metadata.h b/libs/libmicrodroid_payload_metadata/include/microdroid/metadata.h
similarity index 100%
rename from microdroid/payload/include/microdroid/metadata.h
rename to libs/libmicrodroid_payload_metadata/include/microdroid/metadata.h
diff --git a/microdroid/payload/metadata.cc b/libs/libmicrodroid_payload_metadata/metadata.cc
similarity index 100%
rename from microdroid/payload/metadata.cc
rename to libs/libmicrodroid_payload_metadata/metadata.cc
diff --git a/microdroid/payload/metadata.proto b/libs/libmicrodroid_payload_metadata/metadata.proto
similarity index 100%
rename from microdroid/payload/metadata.proto
rename to libs/libmicrodroid_payload_metadata/metadata.proto
diff --git a/microdroid/payload/metadata/Android.bp b/libs/libmicrodroid_payload_metadata/metadata/Android.bp
similarity index 100%
rename from microdroid/payload/metadata/Android.bp
rename to libs/libmicrodroid_payload_metadata/metadata/Android.bp
diff --git a/microdroid/payload/metadata/src/lib.rs b/libs/libmicrodroid_payload_metadata/metadata/src/lib.rs
similarity index 100%
rename from microdroid/payload/metadata/src/lib.rs
rename to libs/libmicrodroid_payload_metadata/metadata/src/lib.rs
diff --git a/microdroid/payload/src/com/android/virt/PayloadMetadata.java b/libs/libmicrodroid_payload_metadata/src/com/android/virt/PayloadMetadata.java
similarity index 100%
rename from microdroid/payload/src/com/android/virt/PayloadMetadata.java
rename to libs/libmicrodroid_payload_metadata/src/com/android/virt/PayloadMetadata.java
diff --git a/libs/libsafe_ownedfd/Android.bp b/libs/libsafe_ownedfd/Android.bp
new file mode 100644
index 0000000..53e14dc
--- /dev/null
+++ b/libs/libsafe_ownedfd/Android.bp
@@ -0,0 +1,38 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+    name: "libsafe_ownedfd.defaults",
+    crate_name: "safe_ownedfd",
+    srcs: ["src/lib.rs"],
+    edition: "2021",
+    rustlibs: [
+        "libnix",
+        "libthiserror",
+    ],
+}
+
+rust_library {
+    name: "libsafe_ownedfd",
+    defaults: ["libsafe_ownedfd.defaults"],
+    apex_available: [
+        "com.android.compos",
+        "com.android.microfuchsia",
+        "com.android.virt",
+    ],
+}
+
+rust_test {
+    name: "libsafe_ownedfd.test",
+    defaults: ["libsafe_ownedfd.defaults"],
+    rustlibs: [
+        "libanyhow",
+        "libtempfile",
+    ],
+    host_supported: true,
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/libs/libsafe_ownedfd/src/lib.rs b/libs/libsafe_ownedfd/src/lib.rs
new file mode 100644
index 0000000..52ae180
--- /dev/null
+++ b/libs/libsafe_ownedfd/src/lib.rs
@@ -0,0 +1,127 @@
+// Copyright 2024, 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.
+
+//! Library for a safer conversion from `RawFd` to `OwnedFd`
+
+use nix::fcntl::{fcntl, FdFlag, F_DUPFD, F_GETFD, F_SETFD};
+use nix::libc;
+use nix::unistd::close;
+use std::os::fd::FromRawFd;
+use std::os::fd::OwnedFd;
+use std::os::fd::RawFd;
+use std::sync::Mutex;
+use thiserror::Error;
+
+/// Errors that can occur while taking an ownership of `RawFd`
+#[derive(Debug, PartialEq, Error)]
+pub enum Error {
+    /// RawFd is not a valid file descriptor
+    #[error("{0} is not a file descriptor")]
+    Invalid(RawFd),
+
+    /// RawFd is either stdio, stdout, or stderr
+    #[error("standard IO descriptors cannot be owned")]
+    StdioNotAllowed,
+
+    /// Generic UNIX error
+    #[error("UNIX error")]
+    Errno(#[from] nix::errno::Errno),
+}
+
+static LOCK: Mutex<()> = Mutex::new(());
+
+/// Takes the ownership of `RawFd` and converts it to `OwnedFd`. It is important to know that
+/// `RawFd` is closed when this function successfully returns. The raw file descriptor of the
+/// returned `OwnedFd` is different from `RawFd`. The returned file descriptor is CLOEXEC set.
+pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
+    fcntl(raw_fd, F_GETFD).map_err(|_| Error::Invalid(raw_fd))?;
+
+    if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+        return Err(Error::StdioNotAllowed);
+    }
+
+    // sync is needed otherwise we can create multiple OwnedFds out of the same RawFd
+    let lock = LOCK.lock().unwrap();
+    let new_fd = fcntl(raw_fd, F_DUPFD(raw_fd))?;
+    close(raw_fd)?;
+    drop(lock);
+
+    // This is not essential, but let's follow the common practice in the Rust ecosystem
+    fcntl(new_fd, F_SETFD(FdFlag::FD_CLOEXEC)).map_err(Error::Errno)?;
+
+    // SAFETY: In this function, we have checked that RawFd is actually an open file descriptor and
+    // this is the first time to claim its ownership because we just created it by duping.
+    Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use anyhow::Result;
+    use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
+    use std::os::fd::AsRawFd;
+    use std::os::fd::IntoRawFd;
+    use tempfile::tempfile;
+
+    #[test]
+    fn good_fd() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+        assert!(take_fd_ownership(raw_fd).is_ok());
+        Ok(())
+    }
+
+    #[test]
+    fn invalid_fd() -> Result<()> {
+        let raw_fd = 12345; // randomly chosen
+        assert_eq!(take_fd_ownership(raw_fd).unwrap_err(), Error::Invalid(raw_fd));
+        Ok(())
+    }
+
+    #[test]
+    fn original_fd_closed() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+        let owned_fd = take_fd_ownership(raw_fd)?;
+        assert_ne!(raw_fd, owned_fd.as_raw_fd());
+        assert!(fcntl(raw_fd, F_GETFD).is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn cannot_use_same_rawfd_multiple_times() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+
+        let owned_fd = take_fd_ownership(raw_fd); // once
+        let owned_fd2 = take_fd_ownership(raw_fd); // twice
+
+        assert!(owned_fd.is_ok());
+        assert!(owned_fd2.is_err());
+        Ok(())
+    }
+
+    #[test]
+    fn cloexec() -> Result<()> {
+        let raw_fd = tempfile()?.into_raw_fd();
+
+        // intentionally clear cloexec to see if it is set by take_fd_ownership
+        fcntl(raw_fd, F_SETFD(FdFlag::empty()))?;
+        let flags = fcntl(raw_fd, F_GETFD)?;
+        assert_eq!(flags, FdFlag::empty().bits());
+
+        let owned_fd = take_fd_ownership(raw_fd)?;
+        let flags = fcntl(owned_fd.as_raw_fd(), F_GETFD)?;
+        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
+        drop(owned_fd);
+        Ok(())
+    }
+}
diff --git a/service_vm/comm/Android.bp b/libs/libservice_vm_comm/Android.bp
similarity index 100%
rename from service_vm/comm/Android.bp
rename to libs/libservice_vm_comm/Android.bp
diff --git a/service_vm/comm/TEST_MAPPING b/libs/libservice_vm_comm/TEST_MAPPING
similarity index 100%
rename from service_vm/comm/TEST_MAPPING
rename to libs/libservice_vm_comm/TEST_MAPPING
diff --git a/service_vm/comm/src/client_vm_csr.cddl b/libs/libservice_vm_comm/src/client_vm_csr.cddl
similarity index 100%
rename from service_vm/comm/src/client_vm_csr.cddl
rename to libs/libservice_vm_comm/src/client_vm_csr.cddl
diff --git a/service_vm/comm/src/csr.rs b/libs/libservice_vm_comm/src/csr.rs
similarity index 100%
rename from service_vm/comm/src/csr.rs
rename to libs/libservice_vm_comm/src/csr.rs
diff --git a/service_vm/comm/src/lib.rs b/libs/libservice_vm_comm/src/lib.rs
similarity index 100%
rename from service_vm/comm/src/lib.rs
rename to libs/libservice_vm_comm/src/lib.rs
diff --git a/service_vm/comm/src/message.rs b/libs/libservice_vm_comm/src/message.rs
similarity index 100%
rename from service_vm/comm/src/message.rs
rename to libs/libservice_vm_comm/src/message.rs
diff --git a/service_vm/comm/src/vsock.rs b/libs/libservice_vm_comm/src/vsock.rs
similarity index 100%
rename from service_vm/comm/src/vsock.rs
rename to libs/libservice_vm_comm/src/vsock.rs
diff --git a/service_vm/comm/tests/api_test.rs b/libs/libservice_vm_comm/tests/api_test.rs
similarity index 100%
rename from service_vm/comm/tests/api_test.rs
rename to libs/libservice_vm_comm/tests/api_test.rs
diff --git a/service_vm/fake_chain/Android.bp b/libs/libservice_vm_fake_chain/Android.bp
similarity index 94%
rename from service_vm/fake_chain/Android.bp
rename to libs/libservice_vm_fake_chain/Android.bp
index 2bc7b4e..39f36eb 100644
--- a/service_vm/fake_chain/Android.bp
+++ b/libs/libservice_vm_fake_chain/Android.bp
@@ -22,7 +22,7 @@
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
     visibility: [
-        "//packages/modules/Virtualization/rialto:__subpackages__",
+        "//packages/modules/Virtualization/guest/rialto:__subpackages__",
     ],
     prefer_rlib: true,
     rustlibs: [
diff --git a/service_vm/fake_chain/src/client_vm.rs b/libs/libservice_vm_fake_chain/src/client_vm.rs
similarity index 100%
rename from service_vm/fake_chain/src/client_vm.rs
rename to libs/libservice_vm_fake_chain/src/client_vm.rs
diff --git a/service_vm/fake_chain/src/lib.rs b/libs/libservice_vm_fake_chain/src/lib.rs
similarity index 100%
rename from service_vm/fake_chain/src/lib.rs
rename to libs/libservice_vm_fake_chain/src/lib.rs
diff --git a/service_vm/fake_chain/src/service_vm.rs b/libs/libservice_vm_fake_chain/src/service_vm.rs
similarity index 100%
rename from service_vm/fake_chain/src/service_vm.rs
rename to libs/libservice_vm_fake_chain/src/service_vm.rs
diff --git a/service_vm/manager/Android.bp b/libs/libservice_vm_manager/Android.bp
similarity index 100%
rename from service_vm/manager/Android.bp
rename to libs/libservice_vm_manager/Android.bp
diff --git a/service_vm/manager/src/lib.rs b/libs/libservice_vm_manager/src/lib.rs
similarity index 98%
rename from service_vm/manager/src/lib.rs
rename to libs/libservice_vm_manager/src/lib.rs
index 78ed85b..d3d86e9 100644
--- a/service_vm/manager/src/lib.rs
+++ b/libs/libservice_vm_manager/src/lib.rs
@@ -37,12 +37,14 @@
 use vmclient::{DeathReason, VmInstance};
 use vsock::{VsockListener, VsockStream, VMADDR_CID_HOST};
 
+/// Size of virtual memory allocated to the Service VM.
+pub const VM_MEMORY_MB: i32 = 6;
+
 const VIRT_DATA_DIR: &str = "/data/misc/apexdata/com.android.virt";
 const RIALTO_PATH: &str = "/apex/com.android.virt/etc/rialto.bin";
 const INSTANCE_IMG_NAME: &str = "service_vm_instance.img";
 const INSTANCE_ID_FILENAME: &str = "service_vm_instance_id";
 const INSTANCE_IMG_SIZE_BYTES: i64 = 1 << 20; // 1MB
-const MEMORY_MB: i32 = 300;
 const WRITE_BUFFER_CAPACITY: usize = 512;
 const READ_TIMEOUT: Duration = Duration::from_secs(10);
 const WRITE_TIMEOUT: Duration = Duration::from_secs(10);
@@ -227,11 +229,11 @@
     let instance_id = get_or_allocate_instance_id(service.as_ref(), instance_id_file)?;
     let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
         name: String::from("Service VM"),
-        bootloader: Some(ParcelFileDescriptor::new(rialto)),
+        kernel: Some(ParcelFileDescriptor::new(rialto)),
         disks: vec![DiskImage { image: None, partitions: writable_partitions, writable: true }],
         instanceId: instance_id,
         protectedVm: true,
-        memoryMib: MEMORY_MB,
+        memoryMib: VM_MEMORY_MB,
         cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         gdbPort: 0, // No gdb
diff --git a/service_vm/requests/Android.bp b/libs/libservice_vm_requests/Android.bp
similarity index 100%
rename from service_vm/requests/Android.bp
rename to libs/libservice_vm_requests/Android.bp
diff --git a/service_vm/requests/TEST_MAPPING b/libs/libservice_vm_requests/TEST_MAPPING
similarity index 100%
rename from service_vm/requests/TEST_MAPPING
rename to libs/libservice_vm_requests/TEST_MAPPING
diff --git a/service_vm/requests/src/api.rs b/libs/libservice_vm_requests/src/api.rs
similarity index 100%
rename from service_vm/requests/src/api.rs
rename to libs/libservice_vm_requests/src/api.rs
diff --git a/service_vm/requests/src/cert.rs b/libs/libservice_vm_requests/src/cert.rs
similarity index 100%
rename from service_vm/requests/src/cert.rs
rename to libs/libservice_vm_requests/src/cert.rs
diff --git a/service_vm/requests/src/client_vm.rs b/libs/libservice_vm_requests/src/client_vm.rs
similarity index 98%
rename from service_vm/requests/src/client_vm.rs
rename to libs/libservice_vm_requests/src/client_vm.rs
index 2aa7113..4e54510 100644
--- a/service_vm/requests/src/client_vm.rs
+++ b/libs/libservice_vm_requests/src/client_vm.rs
@@ -55,7 +55,7 @@
         vendor_hashtree_root_digest_from_dt,
     )?;
 
-    // AAD is empty as defined in service_vm/comm/client_vm_csr.cddl.
+    // AAD is empty as defined in libs/libservice_vm_comm/client_vm_csr.cddl.
     let aad = &[];
 
     // Verifies the first signature with the leaf private key in the DICE chain.
diff --git a/service_vm/requests/src/dice.rs b/libs/libservice_vm_requests/src/dice.rs
similarity index 99%
rename from service_vm/requests/src/dice.rs
rename to libs/libservice_vm_requests/src/dice.rs
index 247c34e..ef9d894 100644
--- a/service_vm/requests/src/dice.rs
+++ b/libs/libservice_vm_requests/src/dice.rs
@@ -76,7 +76,7 @@
     ///
     /// - The first entry of the `client_vm_dice_chain` must be signed with the root public key.
     /// - After the first entry, each entry of the `client_vm_dice_chain` must be signed with the
-    ///  subject public key of the previous entry.
+    ///   subject public key of the previous entry.
     ///
     /// Returns a partially decoded client VM's DICE chain if the verification succeeds.
     pub(crate) fn validate_signatures_and_parse_dice_chain(
diff --git a/service_vm/requests/src/keyblob.rs b/libs/libservice_vm_requests/src/keyblob.rs
similarity index 100%
rename from service_vm/requests/src/keyblob.rs
rename to libs/libservice_vm_requests/src/keyblob.rs
diff --git a/service_vm/requests/src/lib.rs b/libs/libservice_vm_requests/src/lib.rs
similarity index 100%
rename from service_vm/requests/src/lib.rs
rename to libs/libservice_vm_requests/src/lib.rs
diff --git a/service_vm/requests/src/pub_key.rs b/libs/libservice_vm_requests/src/pub_key.rs
similarity index 100%
rename from service_vm/requests/src/pub_key.rs
rename to libs/libservice_vm_requests/src/pub_key.rs
diff --git a/service_vm/requests/src/rkp.rs b/libs/libservice_vm_requests/src/rkp.rs
similarity index 98%
rename from service_vm/requests/src/rkp.rs
rename to libs/libservice_vm_requests/src/rkp.rs
index c62a36b..e2be11b 100644
--- a/service_vm/requests/src/rkp.rs
+++ b/libs/libservice_vm_requests/src/rkp.rs
@@ -180,8 +180,8 @@
     /// order as per RFC8949.
     /// The CBOR ordering rules are:
     /// 1. If two keys have different lengths, the shorter one sorts earlier;
-    /// 2. If two keys have the same length, the one with the lower value in
-    ///  (bytewise) lexical order sorts earlier.
+    /// 2. If two keys have the same length, the one with the lower value in (bytewise) lexical
+    ///    order sorts earlier.
     #[test]
     fn device_info_is_in_length_first_deterministic_order() {
         let device_info = cbor!(device_info()).unwrap();
diff --git a/java/jni/Android.bp b/libs/libvirtualization_jni/Android.bp
similarity index 91%
rename from java/jni/Android.bp
rename to libs/libvirtualization_jni/Android.bp
index 4a569d4..9dc86b0 100644
--- a/java/jni/Android.bp
+++ b/libs/libvirtualization_jni/Android.bp
@@ -16,7 +16,10 @@
         "liblog",
         "libnativehelper",
     ],
-    static_libs: ["libavf_cc_flags"],
+    static_libs: [
+        "libavf_cc_flags",
+        "libvmclient.ffi",
+    ],
 }
 
 cc_library_shared {
diff --git a/java/jni/android_system_virtualmachine_VirtualMachine.cpp b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
similarity index 96%
rename from java/jni/android_system_virtualmachine_VirtualMachine.cpp
rename to libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
index ed102bf..3f1d8a0 100644
--- a/java/jni/android_system_virtualmachine_VirtualMachine.cpp
+++ b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualMachine.cpp
@@ -49,7 +49,7 @@
 
 extern "C" JNIEXPORT jobject JNICALL
 Java_android_system_virtualmachine_VirtualMachine_nativeConnectToVsockServer(
-        JNIEnv* env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
+        JNIEnv *env, [[maybe_unused]] jclass clazz, jobject vmBinder, jint port) {
     using aidl::android::system::virtualizationservice::IVirtualMachine;
     using ndk::ScopedFileDescriptor;
     using ndk::SpAIBinder;
@@ -59,8 +59,8 @@
     std::tuple args{env, vm.get(), port};
     using Args = decltype(args);
 
-    auto requestFunc = [](void* param) {
-        auto [env, vm, port] = *static_cast<Args*>(param);
+    auto requestFunc = [](void *param) {
+        auto [env, vm, port] = *static_cast<Args *>(param);
 
         ScopedFileDescriptor fd;
         if (auto status = vm->connectVsock(port, &fd); !status.isOk()) {
diff --git a/java/jni/android_system_virtualmachine_VirtualizationService.cpp b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
similarity index 61%
rename from java/jni/android_system_virtualmachine_VirtualizationService.cpp
rename to libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
index ced2079..f0c9b4f 100644
--- a/java/jni/android_system_virtualmachine_VirtualizationService.cpp
+++ b/libs/libvirtualization_jni/android_system_virtualmachine_VirtualizationService.cpp
@@ -19,6 +19,7 @@
 #include <android-base/unique_fd.h>
 #include <android/avf_cc_flags.h>
 #include <android/binder_ibinder_jni.h>
+#include <errno.h>
 #include <jni.h>
 #include <log/log.h>
 #include <poll.h>
@@ -29,57 +30,25 @@
 
 using namespace android::base;
 
-static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr";
 static constexpr size_t VIRTMGR_THREADS = 2;
 
+void error_callback(int code, const char* msg, void* ctx) {
+    JNIEnv* env = reinterpret_cast<JNIEnv*>(ctx);
+    if (code == EPERM || code == EACCES) {
+        env->ThrowNew(env->FindClass("java/lang/SecurityException"),
+                      "Virtmgr didn't send any data through pipe. Please consider checking if "
+                      "android.permission.MANAGE_VIRTUAL_MACHINE permission is granted");
+        return;
+    }
+    env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"), msg);
+}
+
+extern "C" int get_virtualization_service(decltype(error_callback)*, void*);
+
 extern "C" JNIEXPORT jint JNICALL
 Java_android_system_virtualmachine_VirtualizationService_nativeSpawn(
         JNIEnv* env, [[maybe_unused]] jclass clazz) {
-    unique_fd serverFd, clientFd;
-    if (!Socketpair(SOCK_STREAM, &serverFd, &clientFd)) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      ("Failed to create socketpair: " + std::string(strerror(errno))).c_str());
-        return -1;
-    }
-
-    unique_fd waitFd, readyFd;
-    if (!Pipe(&waitFd, &readyFd, 0)) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      ("Failed to create pipe: " + std::string(strerror(errno))).c_str());
-        return -1;
-    }
-
-    if (fork() == 0) {
-        // Close client's FDs.
-        clientFd.reset();
-        waitFd.reset();
-
-        auto strServerFd = std::to_string(serverFd.get());
-        auto strReadyFd = std::to_string(readyFd.get());
-
-        execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", strServerFd.c_str(), "--ready-fd",
-              strReadyFd.c_str(), NULL);
-    }
-
-    // Close virtmgr's FDs.
-    serverFd.reset();
-    readyFd.reset();
-
-    // Wait for the server to signal its readiness by closing its end of the pipe.
-    char buf;
-    int ret = read(waitFd.get(), &buf, sizeof(buf));
-    if (ret < 0) {
-        env->ThrowNew(env->FindClass("android/system/virtualmachine/VirtualMachineException"),
-                      "Failed to wait for VirtualizationService to be ready");
-        return -1;
-    } else if (ret < 1) {
-        env->ThrowNew(env->FindClass("java/lang/SecurityException"),
-                      "Virtmgr didn't send any data through pipe. Please consider checking if "
-                      "android.permission.MANAGE_VIRTUAL_MACHINE permission is granted");
-        return -1;
-    }
-
-    return clientFd.release();
+    return get_virtualization_service(error_callback, env);
 }
 
 extern "C" JNIEXPORT jobject JNICALL
@@ -108,9 +77,3 @@
     }
     return pfds[0].revents == 0;
 }
-
-extern "C" JNIEXPORT jboolean JNICALL
-Java_android_system_virtualmachine_VirtualizationService_nativeIsVendorModulesFlagEnabled(
-        [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
-    return android::virtualization::IsVendorModulesFlagEnabled();
-}
diff --git a/java/jni/common.h b/libs/libvirtualization_jni/common.h
similarity index 100%
rename from java/jni/common.h
rename to libs/libvirtualization_jni/common.h
diff --git a/vm_payload/Android.bp b/libs/libvm_payload/Android.bp
similarity index 100%
rename from vm_payload/Android.bp
rename to libs/libvm_payload/Android.bp
diff --git a/vm_payload/README.md b/libs/libvm_payload/README.md
similarity index 93%
rename from vm_payload/README.md
rename to libs/libvm_payload/README.md
index 66fd532..8ef1bac 100644
--- a/vm_payload/README.md
+++ b/libs/libvm_payload/README.md
@@ -2,14 +2,14 @@
 
 This directory contains the definition of the VM Payload API. This is a native
 API, exposed as a set of C functions, available to payload code running inside a
-[Microdroid](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/microdroid/README.md)
+[Microdroid](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/build/microdroid/README.md)
 VM.
 
 Note that only native code is supported in Microdroid, so no Java APIs are
 available in the VM, and only 64 bit code is supported.
 
 To create a VM and run the payload from Android see the [AVF Java
-APIs](../java/framework/README.md).
+APIs](../libs/framework-virtualization/README.md).
 
 ## Entry point
 
@@ -17,7 +17,7 @@
 under the `lib/<ABI>` directory, like other JNI code.
 
 The primary .so, which is specified as part of the VM configuration via
-[VirtualMachineConfig.Builder#setPayloadBinaryPath](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java),
+[VirtualMachineConfig.Builder#setPayloadBinaryPath](https://android.googlesource.com/platform/packages/modules/Virtualization/+/refs/heads/main/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java),
 must define the entry point for the payload.
 
 This entry point is a C function called `AVmPayload_main()`, as declared in
diff --git a/vm_payload/include-restricted/vm_payload_restricted.h b/libs/libvm_payload/include-restricted/vm_payload_restricted.h
similarity index 100%
rename from vm_payload/include-restricted/vm_payload_restricted.h
rename to libs/libvm_payload/include-restricted/vm_payload_restricted.h
diff --git a/vm_payload/include/vm_main.h b/libs/libvm_payload/include/vm_main.h
similarity index 100%
rename from vm_payload/include/vm_main.h
rename to libs/libvm_payload/include/vm_main.h
diff --git a/vm_payload/include/vm_payload.h b/libs/libvm_payload/include/vm_payload.h
similarity index 100%
rename from vm_payload/include/vm_payload.h
rename to libs/libvm_payload/include/vm_payload.h
diff --git a/vm_payload/libvm_payload.map.txt b/libs/libvm_payload/libvm_payload.map.txt
similarity index 100%
rename from vm_payload/libvm_payload.map.txt
rename to libs/libvm_payload/libvm_payload.map.txt
diff --git a/vm_payload/src/lib.rs b/libs/libvm_payload/src/lib.rs
similarity index 98%
rename from vm_payload/src/lib.rs
rename to libs/libvm_payload/src/lib.rs
index 5cc4431..13c6e76 100644
--- a/vm_payload/src/lib.rs
+++ b/libs/libvm_payload/src/lib.rs
@@ -401,8 +401,8 @@
 /// Behavior is undefined if any of the following conditions are violated:
 ///
 /// * `data` must be [valid] for writes of `size` bytes, if size > 0.
-/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
-///  region of memory `res` points to.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the region of
+///   memory `res` points to.
 ///
 /// [valid]: ptr#safety
 /// [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
@@ -439,8 +439,8 @@
 ///
 /// * `message` must be [valid] for reads of `message_size` bytes.
 /// * `data` must be [valid] for writes of `size` bytes, if size > 0.
-/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
-///  region of memory `res` or `message` point to.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the region of
+///   memory `res` or `message` point to.
 ///
 ///
 /// [valid]: ptr#safety
@@ -507,8 +507,8 @@
 /// * `data` must be [valid] for writes of `size` bytes, if size > 0.
 /// * `index` must be within the range of [0, number of certificates). The number of certificates
 ///   can be obtained with `AVmAttestationResult_getCertificateCount`.
-/// * The region of memory beginning at `data` with `size` bytes must not overlap with the
-///  region of memory `res` points to.
+/// * The region of memory beginning at `data` with `size` bytes must not overlap with the region of
+///   memory `res` points to.
 ///
 /// [valid]: ptr#safety
 #[no_mangle]
diff --git a/vm_payload/stub/readme.txt b/libs/libvm_payload/stub/readme.txt
similarity index 85%
rename from vm_payload/stub/readme.txt
rename to libs/libvm_payload/stub/readme.txt
index 47679b5..15b8743 100644
--- a/vm_payload/stub/readme.txt
+++ b/libs/libvm_payload/stub/readme.txt
@@ -14,4 +14,4 @@
 lunch aosp_arm64-eng
 m MicrodroidTestNativeLib
 
-The generated stub file can then be found at out/soong/.intermediates/packages/modules/Virtualization/vm_payload/libvm_payload/android_arm64_armv8-a_shared_current/gen/stub.c
+The generated stub file can then be found at out/soong/.intermediates/packages/modules/Virtualization/libs/libvm_payload/libvm_payload/android_arm64_armv8-a_shared_current/gen/stub.c
diff --git a/vm_payload/stub/stub.c b/libs/libvm_payload/stub/stub.c
similarity index 100%
rename from vm_payload/stub/stub.c
rename to libs/libvm_payload/stub/stub.c
diff --git a/vm_payload/wrapper/attestation.rs b/libs/libvm_payload/wrapper/attestation.rs
similarity index 100%
rename from vm_payload/wrapper/attestation.rs
rename to libs/libvm_payload/wrapper/attestation.rs
diff --git a/vm_payload/wrapper/lib.rs b/libs/libvm_payload/wrapper/lib.rs
similarity index 98%
rename from vm_payload/wrapper/lib.rs
rename to libs/libvm_payload/wrapper/lib.rs
index d3f03d7..b9ce6c8 100644
--- a/vm_payload/wrapper/lib.rs
+++ b/libs/libvm_payload/wrapper/lib.rs
@@ -17,7 +17,7 @@
 //! Rust wrapper for the VM Payload API, allowing virtual machine payload code to be written in
 //! Rust. This wraps the raw C API, accessed via bindgen, into a more idiomatic Rust interface.
 //!
-//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/README.md`
+//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/libs/libvm_payload/README.md`
 //! for more information on the VM Payload API.
 
 mod attestation;
diff --git a/vmbase/Android.bp b/libs/libvmbase/Android.bp
similarity index 100%
rename from vmbase/Android.bp
rename to libs/libvmbase/Android.bp
diff --git a/vmbase/README.md b/libs/libvmbase/README.md
similarity index 100%
rename from vmbase/README.md
rename to libs/libvmbase/README.md
diff --git a/vmbase/TEST_MAPPING b/libs/libvmbase/TEST_MAPPING
similarity index 100%
rename from vmbase/TEST_MAPPING
rename to libs/libvmbase/TEST_MAPPING
diff --git a/vmbase/common.h b/libs/libvmbase/common.h
similarity index 100%
rename from vmbase/common.h
rename to libs/libvmbase/common.h
diff --git a/vmbase/entry.S b/libs/libvmbase/entry.S
similarity index 100%
rename from vmbase/entry.S
rename to libs/libvmbase/entry.S
diff --git a/vmbase/exceptions.S b/libs/libvmbase/exceptions.S
similarity index 100%
rename from vmbase/exceptions.S
rename to libs/libvmbase/exceptions.S
diff --git a/vmbase/exceptions_panic.S b/libs/libvmbase/exceptions_panic.S
similarity index 100%
rename from vmbase/exceptions_panic.S
rename to libs/libvmbase/exceptions_panic.S
diff --git a/vmbase/sections.ld b/libs/libvmbase/sections.ld
similarity index 96%
rename from vmbase/sections.ld
rename to libs/libvmbase/sections.ld
index c7ef0ec..7d464bc 100644
--- a/vmbase/sections.ld
+++ b/libs/libvmbase/sections.ld
@@ -29,17 +29,13 @@
 
 SECTIONS
 {
-	.dtb (NOLOAD) : {
-		dtb_begin = .;
-		. += LENGTH(dtb_region);
-		dtb_end = .;
-	} >dtb_region
-
 	/*
 	 * Collect together the code. This is page aligned so it can be mapped
 	 * as executable-only.
 	 */
 	.text : ALIGN(4096) {
+		KEEP(*(.init.head));
+		*(.init.head)
 		text_begin = .;
 		*(.init.entry)
 		*(.init.*)
diff --git a/vmbase/src/arch.rs b/libs/libvmbase/src/arch.rs
similarity index 100%
rename from vmbase/src/arch.rs
rename to libs/libvmbase/src/arch.rs
diff --git a/vmbase/src/bionic.rs b/libs/libvmbase/src/bionic.rs
similarity index 100%
rename from vmbase/src/bionic.rs
rename to libs/libvmbase/src/bionic.rs
diff --git a/vmbase/src/console.rs b/libs/libvmbase/src/console.rs
similarity index 78%
rename from vmbase/src/console.rs
rename to libs/libvmbase/src/console.rs
index bbbcb07..7b01bb6 100644
--- a/vmbase/src/console.rs
+++ b/libs/libvmbase/src/console.rs
@@ -15,25 +15,18 @@
 //! Console driver for 8250 UART.
 
 use crate::uart::Uart;
-use core::{
-    cell::OnceCell,
-    fmt::{write, Arguments, Write},
-};
-use spin::mutex::SpinMutex;
+use core::fmt::{write, Arguments, Write};
+use spin::{mutex::SpinMutex, Once};
 
 // Arbitrary limit on the number of consoles that can be registered.
 //
 // Matches the UART count in crosvm.
 const MAX_CONSOLES: usize = 4;
 
-static CONSOLES: [SpinMutex<Option<Uart>>; MAX_CONSOLES] =
-    [SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None)];
-static ADDRESSES: [SpinMutex<OnceCell<usize>>; MAX_CONSOLES] = [
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-    SpinMutex::new(OnceCell::new()),
-];
+static CONSOLES: [Once<SpinMutex<Uart>>; MAX_CONSOLES] =
+    [Once::new(), Once::new(), Once::new(), Once::new()];
+static ADDRESSES: [Once<usize>; MAX_CONSOLES] =
+    [Once::new(), Once::new(), Once::new(), Once::new()];
 
 /// Index of the console used by default for logging.
 pub const DEFAULT_CONSOLE_INDEX: usize = 0;
@@ -52,13 +45,13 @@
 pub unsafe fn init(base_addresses: &[usize]) {
     for (i, &base_address) in base_addresses.iter().enumerate() {
         // Remember the valid address, for emergency console accesses.
-        ADDRESSES[i].lock().set(base_address).expect("console::init() called more than once");
+        ADDRESSES[i].call_once(|| base_address);
 
         // Initialize the console driver, for normal console accesses.
-        let mut console = CONSOLES[i].lock();
-        assert!(console.is_none(), "console::init() called more than once");
-        // SAFETY: base_address must be the base of a mapped UART.
-        console.replace(unsafe { Uart::new(base_address) });
+        assert!(!CONSOLES[i].is_completed(), "console::init() called more than once");
+        // SAFETY: The caller promised that base_address is the base of a mapped UART with no
+        // aliases.
+        CONSOLES[i].call_once(|| SpinMutex::new(unsafe { Uart::new(base_address) }));
     }
 }
 
@@ -66,8 +59,7 @@
 ///
 /// Panics if the n-th console was not initialized by calling [`init`] first.
 pub fn writeln(n: usize, format_args: Arguments) {
-    let mut guard = CONSOLES[n].lock();
-    let uart = guard.as_mut().unwrap();
+    let uart = &mut *CONSOLES[n].get().unwrap().lock();
 
     write(uart, format_args).unwrap();
     let _ = uart.write_str("\n");
@@ -78,8 +70,7 @@
 /// This is intended for use in situations where the UART may be in an unknown state or the global
 /// instance may be locked, such as in an exception handler or panic handler.
 pub fn ewriteln(n: usize, format_args: Arguments) {
-    let Some(cell) = ADDRESSES[n].try_lock() else { return };
-    let Some(addr) = cell.get() else { return };
+    let Some(addr) = ADDRESSES[n].get() else { return };
 
     // SAFETY: addr contains the base of a mapped UART, passed in init().
     let mut uart = unsafe { Uart::new(*addr) };
diff --git a/vmbase/src/entry.rs b/libs/libvmbase/src/entry.rs
similarity index 76%
rename from vmbase/src/entry.rs
rename to libs/libvmbase/src/entry.rs
index ad633ed..99f28fc 100644
--- a/vmbase/src/entry.rs
+++ b/libs/libvmbase/src/entry.rs
@@ -18,7 +18,7 @@
     bionic, console, heap, hyp,
     layout::{UART_ADDRESSES, UART_PAGE_ADDR},
     logger,
-    memory::{SIZE_16KB, SIZE_4KB},
+    memory::{PAGE_SIZE, SIZE_16KB, SIZE_4KB},
     power::{reboot, shutdown},
     rand,
 };
@@ -129,3 +129,37 @@
         }
     };
 }
+
+/// Prepends a Linux kernel header to the generated binary image.
+///
+/// See https://docs.kernel.org/arch/arm64/booting.html
+/// ```
+#[macro_export]
+macro_rules! generate_image_header {
+    () => {
+        #[cfg(not(target_endian = "little"))]
+        compile_error!("Image header uses wrong endianness: bootloaders expect LE!");
+
+        core::arch::global_asm!(
+            // This section gets linked at the start of the image.
+            ".section .init.head, \"ax\"",
+            // This prevents the macro from being called more than once.
+            ".global image_header",
+            "image_header:",
+            // Linux uses a special NOP to be ELF-compatible; we're not.
+            "nop",                          // code0
+            "b entry",                      // code1
+            ".quad 0",                      // text_offset
+            ".quad bin_end - image_header", // image_size
+            ".quad (1 << 1)",               // flags (PAGE_SIZE=4KiB)
+            ".quad 0",                      // res2
+            ".quad 0",                      // res3
+            ".quad 0",                      // res4
+            ".ascii \"ARM\x64\"",           // magic
+            ".long 0",                      // res5
+        );
+    };
+}
+
+// If this fails, the image header flags are out-of-sync with PAGE_SIZE!
+static_assertions::const_assert_eq!(PAGE_SIZE, SIZE_4KB);
diff --git a/vmbase/src/exceptions.rs b/libs/libvmbase/src/exceptions.rs
similarity index 100%
rename from vmbase/src/exceptions.rs
rename to libs/libvmbase/src/exceptions.rs
diff --git a/vmbase/src/fdt.rs b/libs/libvmbase/src/fdt.rs
similarity index 100%
rename from vmbase/src/fdt.rs
rename to libs/libvmbase/src/fdt.rs
diff --git a/vmbase/src/heap.rs b/libs/libvmbase/src/heap.rs
similarity index 100%
rename from vmbase/src/heap.rs
rename to libs/libvmbase/src/heap.rs
diff --git a/vmbase/src/hvc.rs b/libs/libvmbase/src/hvc.rs
similarity index 100%
rename from vmbase/src/hvc.rs
rename to libs/libvmbase/src/hvc.rs
diff --git a/vmbase/src/hvc/trng.rs b/libs/libvmbase/src/hvc/trng.rs
similarity index 100%
rename from vmbase/src/hvc/trng.rs
rename to libs/libvmbase/src/hvc/trng.rs
diff --git a/vmbase/src/hyp.rs b/libs/libvmbase/src/hyp.rs
similarity index 100%
rename from vmbase/src/hyp.rs
rename to libs/libvmbase/src/hyp.rs
diff --git a/vmbase/src/hyp/error.rs b/libs/libvmbase/src/hyp/error.rs
similarity index 100%
rename from vmbase/src/hyp/error.rs
rename to libs/libvmbase/src/hyp/error.rs
diff --git a/vmbase/src/hyp/hypervisor.rs b/libs/libvmbase/src/hyp/hypervisor.rs
similarity index 100%
rename from vmbase/src/hyp/hypervisor.rs
rename to libs/libvmbase/src/hyp/hypervisor.rs
diff --git a/vmbase/src/hyp/hypervisor/common.rs b/libs/libvmbase/src/hyp/hypervisor/common.rs
similarity index 100%
rename from vmbase/src/hyp/hypervisor/common.rs
rename to libs/libvmbase/src/hyp/hypervisor/common.rs
diff --git a/vmbase/src/hyp/hypervisor/geniezone.rs b/libs/libvmbase/src/hyp/hypervisor/geniezone.rs
similarity index 100%
rename from vmbase/src/hyp/hypervisor/geniezone.rs
rename to libs/libvmbase/src/hyp/hypervisor/geniezone.rs
diff --git a/vmbase/src/hyp/hypervisor/gunyah.rs b/libs/libvmbase/src/hyp/hypervisor/gunyah.rs
similarity index 100%
rename from vmbase/src/hyp/hypervisor/gunyah.rs
rename to libs/libvmbase/src/hyp/hypervisor/gunyah.rs
diff --git a/vmbase/src/hyp/hypervisor/kvm.rs b/libs/libvmbase/src/hyp/hypervisor/kvm.rs
similarity index 98%
rename from vmbase/src/hyp/hypervisor/kvm.rs
rename to libs/libvmbase/src/hyp/hypervisor/kvm.rs
index 8450bed..e496f09 100644
--- a/vmbase/src/hyp/hypervisor/kvm.rs
+++ b/libs/libvmbase/src/hyp/hypervisor/kvm.rs
@@ -75,7 +75,7 @@
 const VENDOR_HYP_KVM_MMIO_GUARD_UNMAP_FUNC_ID: u32 = 0xc6000008;
 
 const VENDOR_HYP_KVM_DEV_REQ_MMIO_FUNC_ID: u32 = 0xc6000012;
-const VENDOR_HYP_KVM_DEV_REQ_DMA_FUNC_ID: u32 = 0xc6000013;
+const VENDOR_HYP_KVM_DEV_REQ_DMA_FUNC_ID: u32 = 0xc600001b;
 
 pub(super) struct RegularKvmHypervisor;
 
diff --git a/vmbase/src/layout.rs b/libs/libvmbase/src/layout.rs
similarity index 96%
rename from vmbase/src/layout.rs
rename to libs/libvmbase/src/layout.rs
index 5ac435f..adcb2fa 100644
--- a/vmbase/src/layout.rs
+++ b/libs/libvmbase/src/layout.rs
@@ -60,11 +60,6 @@
     }};
 }
 
-/// Memory reserved for the DTB.
-pub fn dtb_range() -> Range<VirtualAddress> {
-    linker_region!(dtb_begin, dtb_end)
-}
-
 /// Executable code.
 pub fn text_range() -> Range<VirtualAddress> {
     linker_region!(text_begin, text_end)
diff --git a/vmbase/src/layout/crosvm.rs b/libs/libvmbase/src/layout/crosvm.rs
similarity index 100%
rename from vmbase/src/layout/crosvm.rs
rename to libs/libvmbase/src/layout/crosvm.rs
diff --git a/vmbase/src/lib.rs b/libs/libvmbase/src/lib.rs
similarity index 100%
rename from vmbase/src/lib.rs
rename to libs/libvmbase/src/lib.rs
diff --git a/vmbase/src/linker.rs b/libs/libvmbase/src/linker.rs
similarity index 100%
rename from vmbase/src/linker.rs
rename to libs/libvmbase/src/linker.rs
diff --git a/vmbase/src/logger.rs b/libs/libvmbase/src/logger.rs
similarity index 100%
rename from vmbase/src/logger.rs
rename to libs/libvmbase/src/logger.rs
diff --git a/vmbase/src/memory.rs b/libs/libvmbase/src/memory.rs
similarity index 100%
rename from vmbase/src/memory.rs
rename to libs/libvmbase/src/memory.rs
diff --git a/vmbase/src/memory/dbm.rs b/libs/libvmbase/src/memory/dbm.rs
similarity index 100%
rename from vmbase/src/memory/dbm.rs
rename to libs/libvmbase/src/memory/dbm.rs
diff --git a/vmbase/src/memory/error.rs b/libs/libvmbase/src/memory/error.rs
similarity index 100%
rename from vmbase/src/memory/error.rs
rename to libs/libvmbase/src/memory/error.rs
diff --git a/vmbase/src/memory/page_table.rs b/libs/libvmbase/src/memory/page_table.rs
similarity index 87%
rename from vmbase/src/memory/page_table.rs
rename to libs/libvmbase/src/memory/page_table.rs
index dc346e7..62b52ae 100644
--- a/vmbase/src/memory/page_table.rs
+++ b/libs/libvmbase/src/memory/page_table.rs
@@ -16,23 +16,29 @@
 
 use crate::read_sysreg;
 use aarch64_paging::idmap::IdMap;
-use aarch64_paging::paging::{Attributes, Constraints, Descriptor, MemoryRegion};
+use aarch64_paging::paging::{
+    Attributes, Constraints, Descriptor, MemoryRegion, TranslationRegime,
+};
 use aarch64_paging::MapError;
 use core::result;
 
 /// Software bit used to indicate a device that should be lazily mapped.
 pub(super) const MMIO_LAZY_MAP_FLAG: Attributes = Attributes::SWFLAG_0;
 
-// We assume that:
-// - MAIR_EL1.Attr0 = "Device-nGnRE memory" (0b0000_0100)
-// - MAIR_EL1.Attr1 = "Normal memory, Outer & Inner WB Non-transient, R/W-Allocate" (0b1111_1111)
+/// We assume that MAIR_EL1.Attr0 = "Device-nGnRE memory" (0b0000_0100)
+const DEVICE_NGNRE: Attributes = Attributes::ATTRIBUTE_INDEX_0;
+
+/// We assume that MAIR_EL1.Attr1 = "Normal memory, Outer & Inner WB Non-transient, R/W-Allocate"
+/// (0b1111_1111)
+const NORMAL: Attributes = Attributes::ATTRIBUTE_INDEX_1.union(Attributes::INNER_SHAREABLE);
+
 const MEMORY: Attributes =
-    Attributes::VALID.union(Attributes::NORMAL).union(Attributes::NON_GLOBAL);
+    Attributes::VALID.union(NORMAL).union(Attributes::NON_GLOBAL).union(Attributes::ACCESSED);
 const DEVICE_LAZY: Attributes =
-    MMIO_LAZY_MAP_FLAG.union(Attributes::DEVICE_NGNRE).union(Attributes::EXECUTE_NEVER);
+    MMIO_LAZY_MAP_FLAG.union(DEVICE_NGNRE).union(Attributes::UXN).union(Attributes::ACCESSED);
 const DEVICE: Attributes = DEVICE_LAZY.union(Attributes::VALID);
 const CODE: Attributes = MEMORY.union(Attributes::READ_ONLY);
-const DATA: Attributes = MEMORY.union(Attributes::EXECUTE_NEVER);
+const DATA: Attributes = MEMORY.union(Attributes::UXN);
 const RODATA: Attributes = DATA.union(Attributes::READ_ONLY);
 const DATA_DBM: Attributes = RODATA.union(Attributes::DBM);
 
@@ -64,7 +70,7 @@
         assert_eq!((tcr_el1 >> TCR_EL1_TG0_SHIFT) & TCR_EL1_TG0_MASK, TCR_EL1_TG0_SIZE_4KB);
         assert_eq!((tcr_el1 >> TCR_EL1_T0SZ_SHIFT) & TCR_EL1_T0SZ_MASK, TCR_EL1_T0SZ_39_VA_BITS);
 
-        IdMap::new(Self::ASID, Self::ROOT_LEVEL).into()
+        IdMap::new(Self::ASID, Self::ROOT_LEVEL, TranslationRegime::El1And0).into()
     }
 }
 
diff --git a/vmbase/src/memory/shared.rs b/libs/libvmbase/src/memory/shared.rs
similarity index 100%
rename from vmbase/src/memory/shared.rs
rename to libs/libvmbase/src/memory/shared.rs
diff --git a/vmbase/src/memory/util.rs b/libs/libvmbase/src/memory/util.rs
similarity index 100%
rename from vmbase/src/memory/util.rs
rename to libs/libvmbase/src/memory/util.rs
diff --git a/vmbase/src/power.rs b/libs/libvmbase/src/power.rs
similarity index 100%
rename from vmbase/src/power.rs
rename to libs/libvmbase/src/power.rs
diff --git a/vmbase/src/rand.rs b/libs/libvmbase/src/rand.rs
similarity index 100%
rename from vmbase/src/rand.rs
rename to libs/libvmbase/src/rand.rs
diff --git a/vmbase/src/uart.rs b/libs/libvmbase/src/uart.rs
similarity index 100%
rename from vmbase/src/uart.rs
rename to libs/libvmbase/src/uart.rs
diff --git a/vmbase/src/util.rs b/libs/libvmbase/src/util.rs
similarity index 90%
rename from vmbase/src/util.rs
rename to libs/libvmbase/src/util.rs
index 8c230a1..e52ac8e 100644
--- a/vmbase/src/util.rs
+++ b/libs/libvmbase/src/util.rs
@@ -14,6 +14,7 @@
 
 //! Utility functions.
 
+use aarch64_paging::paging::MemoryRegion;
 use core::ops::Range;
 
 /// Flatten [[T; N]] into &[T]
@@ -91,3 +92,13 @@
         self.start < other.end && other.start < self.end
     }
 }
+
+impl RangeExt for MemoryRegion {
+    fn is_within(&self, other: &Self) -> bool {
+        self.start() >= other.start() && self.end() <= other.end()
+    }
+
+    fn overlaps(&self, other: &Self) -> bool {
+        self.start() < other.end() && other.start() < self.end()
+    }
+}
diff --git a/vmbase/src/virtio.rs b/libs/libvmbase/src/virtio.rs
similarity index 100%
rename from vmbase/src/virtio.rs
rename to libs/libvmbase/src/virtio.rs
diff --git a/vmbase/src/virtio/hal.rs b/libs/libvmbase/src/virtio/hal.rs
similarity index 100%
rename from vmbase/src/virtio/hal.rs
rename to libs/libvmbase/src/virtio/hal.rs
diff --git a/vmbase/src/virtio/pci.rs b/libs/libvmbase/src/virtio/pci.rs
similarity index 100%
rename from vmbase/src/virtio/pci.rs
rename to libs/libvmbase/src/virtio/pci.rs
diff --git a/vmclient/Android.bp b/libs/libvmclient/Android.bp
similarity index 73%
rename from vmclient/Android.bp
rename to libs/libvmclient/Android.bp
index 96fe667..5bd59da 100644
--- a/vmclient/Android.bp
+++ b/libs/libvmclient/Android.bp
@@ -2,8 +2,8 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-rust_library {
-    name: "libvmclient",
+rust_defaults {
+    name: "libvmclient.default",
     crate_name: "vmclient",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
@@ -21,6 +21,17 @@
     ],
     apex_available: [
         "com.android.compos",
+        "com.android.microfuchsia",
         "com.android.virt",
     ],
 }
+
+rust_library {
+    name: "libvmclient",
+    defaults: ["libvmclient.default"],
+}
+
+rust_ffi_static {
+    name: "libvmclient.ffi",
+    defaults: ["libvmclient.default"],
+}
diff --git a/vmclient/src/death_reason.rs b/libs/libvmclient/src/death_reason.rs
similarity index 100%
rename from vmclient/src/death_reason.rs
rename to libs/libvmclient/src/death_reason.rs
diff --git a/vmclient/src/error_code.rs b/libs/libvmclient/src/error_code.rs
similarity index 100%
rename from vmclient/src/error_code.rs
rename to libs/libvmclient/src/error_code.rs
diff --git a/vmclient/src/errors.rs b/libs/libvmclient/src/errors.rs
similarity index 100%
rename from vmclient/src/errors.rs
rename to libs/libvmclient/src/errors.rs
diff --git a/vmclient/src/lib.rs b/libs/libvmclient/src/lib.rs
similarity index 87%
rename from vmclient/src/lib.rs
rename to libs/libvmclient/src/lib.rs
index 88072a7..bc9d683 100644
--- a/vmclient/src/lib.rs
+++ b/libs/libvmclient/src/lib.rs
@@ -43,7 +43,9 @@
 use log::warn;
 use rpcbinder::{FileDescriptorTransportMode, RpcSession};
 use shared_child::SharedChild;
+use std::ffi::{c_char, c_int, c_void, CString};
 use std::io::{self, Read};
+use std::os::fd::RawFd;
 use std::process::Command;
 use std::{
     fmt::{self, Debug, Formatter},
@@ -74,6 +76,40 @@
     Ok(socketpair(AddressFamily::Unix, SockType::Stream, None, SockFlag::SOCK_CLOEXEC)?)
 }
 
+/// Error handling function for `get_virtualization_service`.
+///
+/// # Safety
+/// `message` shouldn't be used outside of the lifetime of the function. Management of `ctx` is
+/// entirely up to the function.
+pub type ErrorCallback =
+    unsafe extern "C" fn(code: c_int, message: *const c_char, ctx: *mut c_void);
+
+/// Spawns a new instance of virtmgr and rerturns a file descriptor for the socket connection to
+/// the service. When error occurs, it is reported via the ErrorCallback function along with the
+/// error message and any context that is set by the client.
+///
+/// # Safety
+/// `cb` should be null or a valid function pointer of type `ErrorCallback`
+#[no_mangle]
+pub unsafe extern "C" fn get_virtualization_service(
+    cb: Option<ErrorCallback>,
+    ctx: *mut c_void,
+) -> RawFd {
+    match VirtualizationService::new() {
+        Ok(vs) => vs.client_fd.into_raw_fd(),
+        Err(e) => {
+            if let Some(cb) = cb {
+                let code = e.raw_os_error().unwrap_or(-1);
+                let msg = CString::new(e.to_string()).unwrap();
+                // SAFETY: `cb` doesn't use `msg` outside of the lifetime of the function.
+                // msg's lifetime is longer than `cb` as it is bound to a local variable.
+                unsafe { cb(code, msg.as_ptr(), ctx) };
+            }
+            -1
+        }
+    }
+}
+
 /// A running instance of virtmgr which is hosting a VirtualizationService
 /// RpcBinder server.
 pub struct VirtualizationService {
@@ -90,20 +126,18 @@
         let (client_fd, server_fd) = posix_socketpair()?;
 
         let mut command = Command::new(VIRTMGR_PATH);
+        // Can't use BorrowedFd as it doesn't implement Display
         command.arg("--rpc-server-fd").arg(format!("{}", server_fd.as_raw_fd()));
         command.arg("--ready-fd").arg(format!("{}", ready_fd.as_raw_fd()));
-        command.preserved_fds(vec![server_fd.as_raw_fd(), ready_fd.as_raw_fd()]);
+        command.preserved_fds(vec![server_fd, ready_fd]);
 
         SharedChild::spawn(&mut command)?;
 
-        // Drop FDs that belong to virtmgr.
-        drop(server_fd);
-        drop(ready_fd);
-
-        // Wait for the child to signal that the RpcBinder server is ready
-        // by closing its end of the pipe.
-        let _ignored = File::from(wait_fd).read(&mut [0]);
-
+        // Wait for the child to signal that the RpcBinder server is read by closing its end of the
+        // pipe. Failing to read (especially EACCESS or EPERM) can happen if the client lacks the
+        // MANAGE_VIRTUAL_MACHINE permission. Therefore, such errors are propagated instead of
+        // being ignored.
+        let _ = File::from(wait_fd).read(&mut [0])?;
         Ok(VirtualizationService { client_fd })
     }
 
diff --git a/vmclient/src/sync.rs b/libs/libvmclient/src/sync.rs
similarity index 100%
rename from vmclient/src/sync.rs
rename to libs/libvmclient/src/sync.rs
diff --git a/compos/service/Android.bp b/libs/service-compos/Android.bp
similarity index 100%
rename from compos/service/Android.bp
rename to libs/service-compos/Android.bp
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
similarity index 89%
rename from compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
rename to libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
index 933ac7a..adc0300 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationJobService.java
+++ b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationJobService.java
@@ -52,14 +52,16 @@
         ComponentName serviceName =
                 new ComponentName("android", IsolatedCompilationJobService.class.getName());
 
-        int result = scheduler.schedule(new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName)
-                // Wait in case more APEXes are staged
-                .setMinimumLatency(TimeUnit.MINUTES.toMillis(60))
-                // We consume CPU, power, and storage
-                .setRequiresDeviceIdle(true)
-                .setRequiresCharging(true)
-                .setRequiresStorageNotLow(true)
-                .build());
+        int result =
+                scheduler.schedule(
+                        new JobInfo.Builder(STAGED_APEX_JOB_ID, serviceName)
+                                // Wait in case more APEXes are staged
+                                .setMinimumLatency(TimeUnit.MINUTES.toMillis(60))
+                                // We consume CPU, power, and storage
+                                .setRequiresDeviceIdle(true)
+                                .setRequiresCharging(true)
+                                .setRequiresStorageNotLow(true)
+                                .build());
         if (result == JobScheduler.RESULT_SUCCESS) {
             IsolatedCompilationMetrics.onCompilationScheduled(
                     IsolatedCompilationMetrics.SCHEDULING_SUCCESS);
@@ -86,13 +88,14 @@
         if (oldJob != null) {
             // We're already running a job, give up on this one
             Log.w(TAG, "Another job is in progress, skipping");
-            return false;  // Already finished
+            return false; // Already finished
         }
 
         IsolatedCompilationMetrics metrics = new IsolatedCompilationMetrics();
 
-        CompilationJob newJob = new CompilationJob(IsolatedCompilationJobService.this::onCompletion,
-                params, metrics);
+        CompilationJob newJob =
+                new CompilationJob(
+                        IsolatedCompilationJobService.this::onCompletion, params, metrics);
         mCurrentJob.set(newJob);
 
         // This can take some time - we need to start up a VM - so we do it on a separate
@@ -108,7 +111,7 @@
                     metrics.onCompilationEnded(IsolatedCompilationMetrics.RESULT_FAILED_TO_START);
                     mCurrentJob.set(null);
                     newJob.stop(); // Just in case it managed to start before failure
-                    jobFinished(params, /*wantReschedule=*/ false);
+                    jobFinished(params, /* wantReschedule= */ false);
                 }
             }
         }.start();
@@ -137,7 +140,7 @@
         // On success we don't need to reschedule.
         // On failure we could reschedule, but that could just use a lot of resources and still
         // fail; instead we just let odsign do compilation on reboot if necessary.
-        jobFinished(params, /*wantReschedule=*/ false);
+        jobFinished(params, /* wantReschedule= */ false);
     }
 
     interface CompilationCallback {
@@ -152,7 +155,9 @@
         private final JobParameters mParams;
         private volatile boolean mStopRequested = false;
 
-        CompilationJob(CompilationCallback callback, JobParameters params,
+        CompilationJob(
+                CompilationCallback callback,
+                JobParameters params,
                 IsolatedCompilationMetrics metrics) {
             mCallback = requireNonNull(callback);
             mParams = params;
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationMetrics.java
similarity index 77%
rename from compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
rename to libs/service-compos/java/com/android/server/compos/IsolatedCompilationMetrics.java
index f7799a4..0419a55 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationMetrics.java
+++ b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationMetrics.java
@@ -61,10 +61,11 @@
             ArtStatsLog.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_FAILED_TO_START;
     public static final int RESULT_JOB_CANCELED =
             ArtStatsLog.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_JOB_CANCELED;
-    public static final int RESULT_COMPILATION_FAILED = ArtStatsLog
-            .ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_COMPILATION_FAILED;
-    public static final int RESULT_UNEXPECTED_COMPILATION_RESULT = ArtStatsLog
-            .ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_UNEXPECTED_COMPILATION_RESULT;
+    public static final int RESULT_COMPILATION_FAILED =
+            ArtStatsLog.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_COMPILATION_FAILED;
+    public static final int RESULT_UNEXPECTED_COMPILATION_RESULT =
+            ArtStatsLog
+                    .ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_UNEXPECTED_COMPILATION_RESULT;
     public static final int RESULT_COMPOSD_DIED =
             ArtStatsLog.ISOLATED_COMPILATION_ENDED__COMPILATION_RESULT__RESULT_COMPOSD_DIED;
     public static final int RESULT_FAILED_TO_ENABLE_FSVERITY =
@@ -78,8 +79,9 @@
     // Keep this in sync with Result enum in IsolatedCompilationScheduled in
     // frameworks/proto_logging/stats/atoms.proto
 
-    public static final int SCHEDULING_RESULT_UNKNOWN = ArtStatsLog
-            .ISOLATED_COMPILATION_SCHEDULED__SCHEDULING_RESULT__SCHEDULING_RESULT_UNKNOWN;
+    public static final int SCHEDULING_RESULT_UNKNOWN =
+            ArtStatsLog
+                    .ISOLATED_COMPILATION_SCHEDULED__SCHEDULING_RESULT__SCHEDULING_RESULT_UNKNOWN;
     public static final int SCHEDULING_FAILURE =
             ArtStatsLog.ISOLATED_COMPILATION_SCHEDULED__SCHEDULING_RESULT__SCHEDULING_FAILURE;
     public static final int SCHEDULING_SUCCESS =
@@ -104,16 +106,24 @@
         statsLogPostCompilation(result, JobParameters.STOP_REASON_UNDEFINED);
     }
 
-    private void statsLogPostCompilation(@CompilationResult int result,
-                @JobParameters.StopReason int jobStopReason) {
+    private void statsLogPostCompilation(
+            @CompilationResult int result, @JobParameters.StopReason int jobStopReason) {
 
-        long compilationTime = mCompilationStartTimeMs == 0 ? -1
-                : SystemClock.elapsedRealtime() - mCompilationStartTimeMs;
+        long compilationTime =
+                mCompilationStartTimeMs == 0
+                        ? -1
+                        : SystemClock.elapsedRealtime() - mCompilationStartTimeMs;
         mCompilationStartTimeMs = 0;
 
-        ArtStatsLog.write(ArtStatsLog.ISOLATED_COMPILATION_ENDED, compilationTime,
-                result, jobStopReason);
-        Log.i(TAG, "ISOLATED_COMPILATION_ENDED: " + result + ", " + compilationTime
-                + ", " + jobStopReason);
+        ArtStatsLog.write(
+                ArtStatsLog.ISOLATED_COMPILATION_ENDED, compilationTime, result, jobStopReason);
+        Log.i(
+                TAG,
+                "ISOLATED_COMPILATION_ENDED: "
+                        + result
+                        + ", "
+                        + compilationTime
+                        + ", "
+                        + jobStopReason);
     }
 }
diff --git a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
similarity index 94%
rename from compos/service/java/com/android/server/compos/IsolatedCompilationService.java
rename to libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
index b2fcbe0..95e365d 100644
--- a/compos/service/java/com/android/server/compos/IsolatedCompilationService.java
+++ b/libs/service-compos/java/com/android/server/compos/IsolatedCompilationService.java
@@ -60,7 +60,6 @@
             return;
         }
 
-
         JobScheduler scheduler = getContext().getSystemService(JobScheduler.class);
         if (scheduler == null) {
             Log.e(TAG, "No scheduler");
@@ -92,8 +91,9 @@
         private final IPackageManagerNative mPackageNative;
 
         static void registerForStagedApexUpdates(JobScheduler scheduler) {
-            final IPackageManagerNative packageNative = IPackageManagerNative.Stub.asInterface(
-                    ServiceManager.getService("package_native"));
+            final IPackageManagerNative packageNative =
+                    IPackageManagerNative.Stub.asInterface(
+                            ServiceManager.getService("package_native"));
             if (packageNative == null) {
                 Log.e(TAG, "No IPackageManagerNative");
                 return;
@@ -110,8 +110,7 @@
             }
         }
 
-        private StagedApexObserver(JobScheduler scheduler,
-                IPackageManagerNative packageNative) {
+        private StagedApexObserver(JobScheduler scheduler, IPackageManagerNative packageNative) {
             mScheduler = scheduler;
             mPackageNative = packageNative;
         }
diff --git a/java/service/Android.bp b/libs/service-virtualization/Android.bp
similarity index 100%
rename from java/service/Android.bp
rename to libs/service-virtualization/Android.bp
diff --git a/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java b/libs/service-virtualization/src/com/android/system/virtualmachine/SecretkeeperJobService.java
similarity index 98%
rename from java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
rename to libs/service-virtualization/src/com/android/system/virtualmachine/SecretkeeperJobService.java
index 473fbfb..4899dcb 100644
--- a/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
+++ b/libs/service-virtualization/src/com/android/system/virtualmachine/SecretkeeperJobService.java
@@ -95,7 +95,7 @@
                 }
                 sJob.set(null);
                 // We don't reschedule on error, we will try again the next day anyway.
-                jobFinished(params, /*wantReschedule=*/ false);
+                jobFinished(params, /* wantReschedule= */ false);
             }
         }.start();
 
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java
similarity index 100%
rename from java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
rename to libs/service-virtualization/src/com/android/system/virtualmachine/VirtualizationSystemService.java
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
new file mode 100644
index 0000000..8591c8d
--- /dev/null
+++ b/libs/vm_launcher_lib/Android.bp
@@ -0,0 +1,13 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "vm_launcher_lib",
+    srcs: ["java/**/*.java"],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.virt",
+    ],
+    sdk_version: "system_current",
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
new file mode 100644
index 0000000..565b793
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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.virtualization.vmlauncher;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import java.util.List;
+
+public class VmLauncherServices {
+    private static final String TAG = "VmLauncherServices";
+
+    private static final String ACTION_START_VM_LAUNCHER_SERVICE =
+            "android.virtualization.START_VM_LAUNCHER_SERVICE";
+
+    private static final int RESULT_START = 0;
+    private static final int RESULT_STOP = 1;
+    private static final int RESULT_ERROR = 2;
+    private static final int RESULT_IPADDR = 3;
+    private static final String KEY_VM_IP_ADDR = "ip_addr";
+
+    private static Intent buildVmLauncherServiceIntent(Context context) {
+        Intent i = new Intent();
+        i.setAction(ACTION_START_VM_LAUNCHER_SERVICE);
+
+        Intent intent = new Intent(ACTION_START_VM_LAUNCHER_SERVICE);
+        PackageManager pm = context.getPackageManager();
+        List<ResolveInfo> resolveInfos =
+                pm.queryIntentServices(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        if (resolveInfos == null || resolveInfos.size() != 1) {
+            Log.e(TAG, "cannot find a service to handle ACTION_START_VM_LAUNCHER_SERVICE");
+            return null;
+        }
+        String packageName = resolveInfos.get(0).serviceInfo.packageName;
+
+        i.setPackage(packageName);
+        return i;
+    }
+
+    public static void stopVmLauncherService(Context context) {
+        Intent i = buildVmLauncherServiceIntent(context);
+        context.stopService(i);
+    }
+
+    public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
+        Intent i = buildVmLauncherServiceIntent(context);
+        if (i == null) {
+            return;
+        }
+        ResultReceiver resultReceiver =
+                new ResultReceiver(new Handler(Looper.myLooper())) {
+                    @Override
+                    protected void onReceiveResult(int resultCode, Bundle resultData) {
+                        if (callback == null) {
+                            return;
+                        }
+                        switch (resultCode) {
+                            case RESULT_START:
+                                callback.onVmStart();
+                                return;
+                            case RESULT_STOP:
+                                callback.onVmStop();
+                                return;
+                            case RESULT_ERROR:
+                                callback.onVmError();
+                                return;
+                            case RESULT_IPADDR:
+                                callback.onIpAddrAvailable(resultData.getString(KEY_VM_IP_ADDR));
+                                return;
+                        }
+                    }
+                };
+        i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+        context.startForegroundService(i);
+    }
+
+    public interface VmLauncherServiceCallback {
+        void onVmStart();
+
+        void onVmStop();
+
+        void onVmError();
+
+        void onIpAddrAvailable(String ipAddr);
+    }
+
+    private static ResultReceiver getResultReceiverForIntent(ResultReceiver r) {
+        Parcel parcel = Parcel.obtain();
+        r.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        r = ResultReceiver.CREATOR.createFromParcel(parcel);
+        parcel.recycle();
+        return r;
+    }
+}
diff --git a/microdroid/kernel/Android.bp b/microdroid/kernel/Android.bp
deleted file mode 100644
index 7a8af90..0000000
--- a/microdroid/kernel/Android.bp
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2024 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 {
-    default_applicable_licenses: ["microdroid_kernel_prebuilts_license"],
-}
-
-// See: http://go/android-license-faq
-license {
-    name: "microdroid_kernel_prebuilts_license",
-    visibility: [":__subpackages__"],
-    license_kinds: [
-        "SPDX-license-identifier-GPL-2.0-only",
-    ],
-    // large-scale-change unable to identify any license_text files
-}
-
-soong_config_module_type {
-    name: "avf_microdroid_kernel_prebuilt",
-    module_type: "filegroup",
-    config_namespace: "ANDROID",
-    variables: [
-        "release_avf_microdroid_kernel_version",
-    ],
-    properties: [
-        "srcs",
-    ],
-}
-
-soong_config_string_variable {
-    name: "release_avf_microdroid_kernel_version",
-    values: [
-        "android14_61",
-        "android15_66",
-    ],
-}
-
-avf_microdroid_kernel_prebuilt {
-    name: "microdroid_kernel_prebuilt-arm64",
-    // Below are properties that are conditionally set depending on value of build flags.
-    soong_config_variables: {
-        release_avf_microdroid_kernel_version: {
-            android14_61: {
-                srcs: ["android14-6.1/arm64/kernel-6.1"],
-            },
-            android15_66: {
-                srcs: ["android15-6.6/arm64/kernel-6.6"],
-            },
-            // In case release configuration doesn't specify value of the
-            // RELEASE_AVF_MICRODROID_KERNEL_VERSION fallback to the kernel we
-            // already released.
-            // TODO(b/298011555): remove this once we set the flag in all release configs.
-            conditions_default: {
-                srcs: ["android14-6.1/arm64/kernel-6.1"],
-            },
-        },
-    },
-}
-
-avf_microdroid_kernel_prebuilt {
-    name: "microdroid_kernel_prebuilt-x86_64",
-    // Below are properties that are conditionally set depending on value of build flags.
-    soong_config_variables: {
-        release_avf_microdroid_kernel_version: {
-            android14_61: {
-                srcs: ["android14-6.1/x86_64/kernel-6.1"],
-            },
-            android15_66: {
-                srcs: ["android15-6.6/x86_64/kernel-6.6"],
-            },
-            // In case release configuration doesn't specify value of the
-            // RELEASE_AVF_MICRODROID_KERNEL_VERSION fallback to the kernel we
-            // already released.
-            // TODO(b/298011555): remove this once we set the flag in all release configs.
-            conditions_default: {
-                srcs: ["android14-6.1/x86_64/kernel-6.1"],
-            },
-        },
-    },
-}
diff --git a/microfuchsia/OWNERS b/microfuchsia/OWNERS
new file mode 100644
index 0000000..8092be7
--- /dev/null
+++ b/microfuchsia/OWNERS
@@ -0,0 +1,2 @@
+awolter@google.com
+jamesr@google.com
diff --git a/microfuchsia/README.md b/microfuchsia/README.md
new file mode 100644
index 0000000..82de725
--- /dev/null
+++ b/microfuchsia/README.md
@@ -0,0 +1,30 @@
+# Microfuchsia
+
+Microfuchsia is an experimental solution for running trusted applications on
+pkvm using the Android Virtualization Framework (AVF).
+
+# How to use
+
+Add the `com.android.microfuchsia` apex to your product.
+
+```
+PRODUCT_PACKAGES += com.android.microfuchsia
+```
+
+Define and add a `com.android.microfuchsia.images` apex to hold the images.
+
+```
+PRODUCT_PACKAGES += com.android.microfuchsia.images
+```
+
+This apex must have a prebuilt `fuchsia.zbi` in `/etc/fuchsia.zbi` and a boot
+shim in `/etc/linux-arm64-boot-shim.bin`.
+
+# Using the console
+
+This command will open the console for the first VM running in AVF, and can be
+used to connect to the microfuchsia console.
+
+```
+adb shell -t /apex/com.android.virt/bin/vm console
+```
diff --git a/microfuchsia/apex/Android.bp b/microfuchsia/apex/Android.bp
new file mode 100644
index 0000000..eddda9f
--- /dev/null
+++ b/microfuchsia/apex/Android.bp
@@ -0,0 +1,55 @@
+// Copyright (C) 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+apex {
+    name: "com.android.microfuchsia",
+    manifest: "manifest.json",
+    key: "com.android.microfuchsia.key",
+
+    // Allows us to specify a file_contexts in our own repository.
+    system_ext_specific: true,
+    file_contexts: "com.android.microfuchsia-file_contexts",
+
+    updatable: false,
+    future_updatable: false,
+    platform_apis: true,
+
+    binaries: [
+        // A daemon that starts on bootup that launches microfuchsia in AVF.
+        "microfuchsiad",
+    ],
+
+    prebuilts: [
+        // An init script to launch the microfuchsiad daemon on bootup which
+        // launches the microfuchsia VM in AVF.
+        "com.android.microfuchsia.init.rc",
+    ],
+}
+
+apex_key {
+    name: "com.android.microfuchsia.key",
+    public_key: "com.android.microfuchsia.avbpubkey",
+    private_key: "com.android.microfuchsia.pem",
+}
+
+prebuilt_etc {
+    name: "com.android.microfuchsia.init.rc",
+    src: "microfuchsia.rc",
+    filename: "init.rc",
+    installable: false,
+}
diff --git a/microfuchsia/apex/com.android.microfuchsia-file_contexts b/microfuchsia/apex/com.android.microfuchsia-file_contexts
new file mode 100644
index 0000000..13d7286
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia-file_contexts
@@ -0,0 +1,2 @@
+(/.*)?                   u:object_r:system_file:s0
+/bin/microfuchsiad       u:object_r:microfuchsiad_exec:s0
diff --git a/microfuchsia/apex/com.android.microfuchsia.avbpubkey b/microfuchsia/apex/com.android.microfuchsia.avbpubkey
new file mode 100644
index 0000000..10d4b88
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia.avbpubkey
Binary files differ
diff --git a/microfuchsia/apex/com.android.microfuchsia.pem b/microfuchsia/apex/com.android.microfuchsia.pem
new file mode 100644
index 0000000..541fa80
--- /dev/null
+++ b/microfuchsia/apex/com.android.microfuchsia.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQAIBADANBgkqhkiG9w0BAQEFAASCCSowggkmAgEAAoICAQCr8qQ+s57kXmB6
+m51lEcX7edl3l5jc1gQxmgopb4ddY0fXm4f0xj9El/Ye4J9lNpf9e1sTJuaytQZR
+lz/I/Kyla6erb37zw18kI1OyTY7PWoeNyNUehLEHqIoeDaj1S3xvb3BiRcncBpLt
+KT/Lunxu4C9sL8kAg9egH/zhOPvm37dqWxJq7CJC/TxSC4sizH6pxjx8AigVCDP3
+i4rwtgUxEdO4CKKm0bK+izUIGRXta3AToL6PKeki0r8E3HhpjNbcpTMpC57TtIgt
+39VSsk5azqSFeEUuBqZdI2Sqgsdxyh3CC4n7MzRduKtrlYAM94Mf2VNQINQ2dG/i
+AhH6Azd8WizGv5MHUeBqn/wHXQ699q19rQj5fFy1vFpw2ptSmkDP3xDsKZsfpYQl
+2FzYoEKIPli7uKOXu5Wa8N+a32SVF8nKbbvPCojklVmOC2IWOxolxI5BlvuMy8aJ
++Ly743dRHu6hEKIHZLRcVCHiixwjlZ8Wqweq5WaxMAKAlvQ4FY0xMoRMeij9WpJ/
+rBYE7qQE2GRm7h9D16nqoJvTeoucsQ50sg5U5aR00aH4xQacK4v6UnkQ5yU8ssPV
+oCIcLbAZ+i0ZRULSom7Lmeu+Lb4kb0+GhP31M3UjGMmyTZYtxbaHwkMK+W4ja6/X
+M4O5+cruvEAxkNQhRUTVBNDKo7YKewIDAQABAoIB/2taktvoSXagy0ZsN1i4QA6X
+hQRQd0q+/t9OeAm8GEe2NKSTS88HTM5cEiOKb/pBRk58izWUlB9UkR1f0UiAeUoj
+wgtxu/wgKXE78oWK5smPPBLJ0PBnkspf79vTq37QImDGCDn9rd+G5d+BttL7xl9z
+Q33IV+ElGlBe/a5LEFCVB27fwsqpo2Uvtk7YkNtT0cEt2OrpGHKz1xOMNrMS8dWG
+dn6a5ZzsT9enZ598CgoG33K3FEKjaBYrKMK1jnhX9njMAPp0xt+8AfSiS2MrmsAX
+REtl9nXwsO3LAI7KGBEd9SEHE0mYLpmqiAbOJaSdjsB+b1sXzrww9lRP9pP3GNcC
+dLF+MOZMFiT+mltSNOmVgPM5nV8njFruqcGOssyq8UJVl/aoIc5CNTsRgiudxOjy
+1kS2VPw4zeoQqyt3lFoZQR/PfrJEXsOJJqJngS8cUmuAAKEWZb0ZjtMFcUrXfFH1
+IXyOl1eQysvQQQynnVc4Xsg67FkqO4OEfxO2Ia9WzGmBV1DfCAK52iLbh2dNxPxg
+5SwkOuzmsztDNHAXMZZZJgwQJ7j4mc1ftfilaNUJn6PDguakclpMKVzP72Hg62TY
+ieQzSo1aKmd4fGMmVe0vCcAur2VnbmKjrblxigg4Gf7S794WJccVsZyGEcasEryA
+OP6M+jHA8EaZQT7DGxUCggEBAO/gqOobZV1b5WyX2WLi+v+Hyd8ZaCpCEeW+NPHd
+Bhh+LffoSrQ4LT4qLfHOaplarA8qcf/Tws4PUgB0yAd/OkwjCBsKSnaa/5368elv
+MOVFhZlg+jn7NXfNh3KvyZ7c/Usg/Hh6w6IleY8mvCj25A8aqb4xqEEHIh6AgYu7
+1bcqmKvEh3zVgkVCNFqDMQvA2F86qo4kW4QCeH4uCH749ynbwO6xungHJdEvEYLv
+hr9r7KXYD6m+redF8UQZE2y35o+MHgzmX1u7ak427D11Uq7OkP3U1xxyPgZ5hURX
+nHKJStGQ1xKZvBQ7aZGKPFTE+7GZJBuwO7NGhFAtOGWWOwcCggEBALeBLjMVTVo+
+8OqnJ2zbCYHTbcP0fBFdXFQLg+XhOxpVCQjDP59pJZC0vyH4BkCpnrSGTJRYuZz6
+MA4uptjU07P9bRBM3mK0c6pb71S2bMIzV5PxiwXvRKVzIAcXY4f2KgIQM6STRaT6
+r50gNTYak+CsdqQqPTqIpii3O9ddp9JEB1sZNys36GKuNm2a86dZO7gV5n5NBPJJ
+AHnSYIhPF3JD9EqlSeAmWOtW2vDc7Kogkf4SdaYFIX2FYIffFEOOUjlaIL5Xgf7P
+iFF8/Tu9WiExyA+sD8yLG2pNdS66eBXVEdCBC44uDDVU4awYgpi34ZJTgay1yj0o
+tloYeexpM+0CggEAHA8Zcxj1SHBha8xvX0PRvGYz1Obx6k+ELG2NX+VMuzy3P9Jq
+Op5/nE/uw+QzT/DtQ3DhmN06YkQkgW0noMjfFtzaK9+OSkVjNSWPepDJFWiGciSH
+4JRj8rmV6HJrkSukbU9UePtTOvpLN9V+GQSYNLQXuumwFrsw4ISDosa7/wr6hM0e
+VBndfSB7Y0MJT6ilJq6EGNBj7BMl6QyVbdTNhJXyAXnEqBmd8NQipkBCcM29BsE5
+Q8/MI8top2CPhx4T2CK5uSSRbveDPdbq112L6Gq9RxPIfclXPAam8hGVeUhZ+h2J
+KuHUwEEa3i1fVUMdde7F7H823IeZHo/LkwZ5rQKCAQA4qfYnJgPNwzPHcbg13+ku
+oqf5Y2xQPGD/PtMK0CLc/bcdcpUZ13EXHwkKJzlfDEGKgxHwmPkv5P2j03oH6Kg6
+ox3jc6kUF57D00GzCeXJjesULvj76ydqY4NXTTyZxkSwgGpB/ov55sMFpOVpgIl7
+TiYQiU6A3aNZXUNoPG5O+ly/H6kuekQS/LKn47orSd2r+W9EPuoxGqO/+lt+m9Wk
+niE4T5PhWFYKzbYrvDyESCxspSyZCGqQBPiK3DK4raDsPs1vmTv2AAWbDBpyMQU8
+zM93L21tfuMHT0XJGSFttG6c0MxNqiBw83YAG01wdQ99jLW1LCl3+zNb3MUBYHb9
+AoIBAGWTZQOQLMVDH5ljzty/HnW3J9ZPPhF+x3B5L98eiYD96tJ6UVsU9Cok6WKu
+V7q7SdwI4pI3mdiuD7ljHMHXiSmF8zPmpG1TpZ1yFNKBQyhIkA/Pffe2wc3ua6Kj
+baXi9jWfLDCQoa8fZ/dzlaUuqN23YuCSwUrLpJ/3o/xgTG085vD3ycbcYvw715PK
+B/9YspIMDQkf2yvOuDwXCjI3IFIGwBGLHoHt+Giqz3z68z54z5qaFi092yNeAewQ
+hhUl1mh6VVanYiERqAgvYUxHuEyD211UYGwMxRHUdiqbtALexZjOB1hLxLnWRtdS
+wa28hvmts5NyMy819GfPGqdRa14=
+-----END PRIVATE KEY-----
diff --git a/microfuchsia/apex/manifest.json b/microfuchsia/apex/manifest.json
new file mode 100644
index 0000000..b7ea23b
--- /dev/null
+++ b/microfuchsia/apex/manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.microfuchsia",
+  "version": 1
+}
diff --git a/microfuchsia/apex/microfuchsia.rc b/microfuchsia/apex/microfuchsia.rc
new file mode 100644
index 0000000..2b19ed3
--- /dev/null
+++ b/microfuchsia/apex/microfuchsia.rc
@@ -0,0 +1,22 @@
+# Copyright (C) 2024 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.
+
+service microfuchsiad /apex/com.android.microfuchsia/bin/microfuchsiad
+    class main
+    user root
+    group system
+    # We need SYS_NICE in order to allow the crosvm child process to use it.
+    # (b/322197421). composd itself never uses it (and isn't allowed to by
+    # SELinux).
+    capabilities SYS_NICE
diff --git a/microfuchsia/microfuchsiad/Android.bp b/microfuchsia/microfuchsiad/Android.bp
new file mode 100644
index 0000000..ab3f865
--- /dev/null
+++ b/microfuchsia/microfuchsiad/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// A daemon that launches microfuchsia in AVF.
+rust_binary {
+    name: "microfuchsiad",
+    srcs: ["src/main.rs"],
+    edition: "2021",
+    prefer_rlib: true,
+    defaults: ["avf_build_flags_rust"],
+    rustlibs: [
+        "android.system.microfuchsiad-rust",
+        "android.system.virtualizationservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libbinder_rs",
+        "liblibc",
+        "liblog_rust",
+        "libsafe_ownedfd",
+        "libvmclient",
+    ],
+    apex_available: [
+        "com.android.microfuchsia",
+    ],
+}
diff --git a/microfuchsia/microfuchsiad/aidl/Android.bp b/microfuchsia/microfuchsiad/aidl/Android.bp
new file mode 100644
index 0000000..02bb7c6
--- /dev/null
+++ b/microfuchsia/microfuchsiad/aidl/Android.bp
@@ -0,0 +1,24 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "android.system.microfuchsiad",
+    srcs: ["android/system/microfuchsiad/*.aidl"],
+    // TODO: Make this stable when the APEX becomes updatable.
+    unstable: true,
+    backend: {
+        java: {
+            enabled: false,
+        },
+        ndk: {
+            enabled: false,
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.microfuchsia",
+            ],
+        },
+    },
+}
diff --git a/rialto/image.ld b/microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl
similarity index 64%
copy from rialto/image.ld
copy to microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl
index 368acbb..a04ae2b 100644
--- a/rialto/image.ld
+++ b/microfuchsia/microfuchsiad/aidl/android/system/microfuchsiad/IMicrofuchsiaService.aidl
@@ -1,11 +1,11 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 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
  *
- *     https://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -13,10 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+package android.system.microfuchsiad;
 
-MEMORY
-{
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
-	image		: ORIGIN = 0x80200000, LENGTH = 2M
-	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
+// This service exists as a placeholder in case we want to communicate with the
+// daemon in the future.
+interface IMicrofuchsiaService {
 }
diff --git a/microfuchsia/microfuchsiad/src/instance_manager.rs b/microfuchsia/microfuchsiad/src/instance_manager.rs
new file mode 100644
index 0000000..5082e50
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/instance_manager.rs
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+//! Manages running instances of the Microfuchsia VM.
+//! At most one instance should be running at a time.
+
+use crate::instance_starter::{InstanceStarter, MicrofuchsiaInstance};
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
+use anyhow::{bail, Result};
+use binder::Strong;
+use virtualizationservice::IVirtualizationService::IVirtualizationService;
+
+pub struct InstanceManager {
+    service: Strong<dyn IVirtualizationService>,
+    started: bool,
+}
+
+impl InstanceManager {
+    pub fn new(service: Strong<dyn IVirtualizationService>) -> Self {
+        Self { service, started: false }
+    }
+
+    pub fn start_instance(&mut self) -> Result<MicrofuchsiaInstance> {
+        if self.started {
+            bail!("Cannot start multiple microfuchsia instances");
+        }
+
+        let instance_starter = InstanceStarter::new("Microfuchsia", 0);
+        let instance = instance_starter.start_new_instance(&*self.service);
+
+        if instance.is_ok() {
+            self.started = true;
+        }
+        instance
+    }
+}
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
new file mode 100644
index 0000000..6688447
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+//! Responsible for starting an instance of the Microfuchsia VM.
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
+    VirtualMachineConfig::VirtualMachineConfig, VirtualMachineRawConfig::VirtualMachineRawConfig,
+};
+use anyhow::{ensure, Context, Result};
+use binder::{LazyServiceGuard, ParcelFileDescriptor};
+use log::info;
+use safe_ownedfd::take_fd_ownership;
+use std::ffi::CStr;
+use std::fs::File;
+use std::os::fd::AsRawFd;
+use vmclient::VmInstance;
+
+pub struct MicrofuchsiaInstance {
+    _vm_instance: VmInstance,
+    _lazy_service_guard: LazyServiceGuard,
+    _pty: Pty,
+}
+
+pub struct InstanceStarter {
+    instance_name: String,
+    instance_id: u8,
+}
+
+impl InstanceStarter {
+    pub fn new(instance_name: &str, instance_id: u8) -> Self {
+        Self { instance_name: instance_name.to_owned(), instance_id }
+    }
+
+    pub fn start_new_instance(
+        &self,
+        virtualization_service: &dyn IVirtualizationService,
+    ) -> Result<MicrofuchsiaInstance> {
+        info!("Creating {} instance", self.instance_name);
+
+        // Always use instance id 0, because we will only ever have one instance.
+        let mut instance_id = [0u8; 64];
+        instance_id[0] = self.instance_id;
+
+        // Open the kernel and initrd files from the microfuchsia.images apex.
+        let kernel_fd =
+            File::open("/apex/com.android.microfuchsia.images/etc/linux-arm64-boot-shim.bin")
+                .context("Failed to open the boot-shim")?;
+        let initrd_fd = File::open("/apex/com.android.microfuchsia.images/etc/fuchsia.zbi")
+            .context("Failed to open the fuchsia ZBI")?;
+        let kernel = Some(ParcelFileDescriptor::new(kernel_fd));
+        let initrd = Some(ParcelFileDescriptor::new(initrd_fd));
+
+        // Prepare a pty for console input/output.
+        let pty = openpty()?;
+        let console_in = Some(pty.leader.try_clone().context("cloning pty")?);
+        let console_out = Some(pty.leader.try_clone().context("cloning pty")?);
+
+        let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
+            name: "Microfuchsia".into(),
+            instanceId: instance_id,
+            kernel,
+            initrd,
+            params: None,
+            bootloader: None,
+            disks: vec![],
+            protectedVm: false,
+            memoryMib: 256,
+            cpuTopology: CpuTopology::ONE_CPU,
+            platformVersion: "1.0.0".into(),
+            // Fuchsia uses serial for console by default.
+            consoleInputDevice: Some("ttyS0".into()),
+            ..Default::default()
+        });
+        let vm_instance = VmInstance::create(
+            virtualization_service,
+            &config,
+            console_out,
+            console_in,
+            /* log= */ None,
+            None,
+        )
+        .context("Failed to create VM")?;
+        vm_instance
+            .vm
+            .setHostConsoleName(&pty.follower_name)
+            .context("Setting host console name")?;
+        vm_instance.start().context("Starting VM")?;
+
+        Ok(MicrofuchsiaInstance {
+            _vm_instance: vm_instance,
+            _lazy_service_guard: Default::default(),
+            _pty: pty,
+        })
+    }
+}
+
+struct Pty {
+    leader: File,
+    follower_name: String,
+}
+
+/// Opens a pseudoterminal (pty), configures it to be a raw terminal, and returns the file pair.
+fn openpty() -> Result<Pty> {
+    // Create a pty pair.
+    let mut leader: libc::c_int = -1;
+    let mut _follower: libc::c_int = -1;
+    let mut follower_name: Vec<libc::c_char> = vec![0; 32];
+
+    // SAFETY: calling openpty with valid+initialized variables is safe.
+    // The two null pointers are valid inputs for openpty.
+    unsafe {
+        ensure!(
+            libc::openpty(
+                &mut leader,
+                &mut _follower,
+                follower_name.as_mut_ptr(),
+                std::ptr::null_mut(),
+                std::ptr::null_mut(),
+            ) == 0,
+            "failed to openpty"
+        );
+    }
+    let leader = take_fd_ownership(leader)?;
+
+    // SAFETY: calling these libc functions with valid+initialized variables is safe.
+    unsafe {
+        // Fetch the termios attributes.
+        let mut attr = libc::termios {
+            c_iflag: 0,
+            c_oflag: 0,
+            c_cflag: 0,
+            c_lflag: 0,
+            c_line: 0,
+            c_cc: [0u8; 19],
+        };
+        ensure!(
+            libc::tcgetattr(leader.as_raw_fd(), &mut attr) == 0,
+            "failed to get termios attributes"
+        );
+
+        // Force it to be a raw pty and re-set it.
+        libc::cfmakeraw(&mut attr);
+        ensure!(
+            libc::tcsetattr(leader.as_raw_fd(), libc::TCSANOW, &attr) == 0,
+            "failed to set termios attributes"
+        );
+    }
+
+    // Construct the return value.
+    let follower_name: Vec<u8> = follower_name.iter_mut().map(|x| *x as _).collect();
+    let follower_name = CStr::from_bytes_until_nul(&follower_name)
+        .context("pty filename missing NUL")?
+        .to_str()
+        .context("pty filename invalid utf8")?
+        .to_string();
+    Ok(Pty { leader: File::from(leader), follower_name })
+}
diff --git a/microfuchsia/microfuchsiad/src/main.rs b/microfuchsia/microfuchsiad/src/main.rs
new file mode 100644
index 0000000..ec290cc
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/main.rs
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+//! A daemon that can be launched on bootup that runs microfuchsia in AVF.
+//! An on-demand binder service is also prepared in case we want to communicate with the daemon in
+//! the future.
+
+mod instance_manager;
+mod instance_starter;
+mod service;
+
+use crate::instance_manager::InstanceManager;
+use anyhow::{Context, Result};
+use binder::{register_lazy_service, ProcessState};
+use log::{error, info};
+
+#[allow(clippy::eq_op)]
+fn try_main() -> Result<()> {
+    let debuggable = env!("TARGET_BUILD_VARIANT") != "user";
+    let log_level = if debuggable { log::LevelFilter::Debug } else { log::LevelFilter::Info };
+    android_logger::init_once(
+        android_logger::Config::default().with_tag("microfuchsiad").with_max_level(log_level),
+    );
+
+    ProcessState::start_thread_pool();
+
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    let virtualization_service =
+        virtmgr.connect().context("Failed to connect to VirtualizationService")?;
+
+    let instance_manager = InstanceManager::new(virtualization_service);
+    let service = service::new_binder(instance_manager);
+    register_lazy_service("android.system.microfuchsiad", service.as_binder())
+        .context("Registering microfuchsiad service")?;
+
+    info!("Registered services, joining threadpool");
+    ProcessState::join_thread_pool();
+
+    info!("Exiting");
+    Ok(())
+}
+
+fn main() {
+    if let Err(e) = try_main() {
+        error!("{:?}", e);
+        std::process::exit(1)
+    }
+}
diff --git a/microfuchsia/microfuchsiad/src/service.rs b/microfuchsia/microfuchsiad/src/service.rs
new file mode 100644
index 0000000..a2112b1
--- /dev/null
+++ b/microfuchsia/microfuchsiad/src/service.rs
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+//! Implementation of IMicrofuchsiaService that runs microfuchsia in AVF when
+//! created.
+
+use crate::instance_manager::InstanceManager;
+use crate::instance_starter::MicrofuchsiaInstance;
+use android_system_microfuchsiad::aidl::android::system::microfuchsiad::IMicrofuchsiaService::{
+    BnMicrofuchsiaService, IMicrofuchsiaService,
+};
+use anyhow::Context;
+use binder::{self, BinderFeatures, Interface, Strong};
+
+#[allow(unused)]
+pub struct MicrofuchsiaService {
+    instance_manager: InstanceManager,
+    microfuchsia: MicrofuchsiaInstance,
+}
+
+pub fn new_binder(mut instance_manager: InstanceManager) -> Strong<dyn IMicrofuchsiaService> {
+    let microfuchsia = instance_manager.start_instance().context("Starting Microfuchsia").unwrap();
+    let service = MicrofuchsiaService { instance_manager, microfuchsia };
+    BnMicrofuchsiaService::new_binder(service, BinderFeatures::default())
+}
+
+impl Interface for MicrofuchsiaService {}
+
+impl IMicrofuchsiaService for MicrofuchsiaService {}
diff --git a/compos/benchmark/Android.bp b/tests/ComposBenchmarkApp/Android.bp
similarity index 100%
rename from compos/benchmark/Android.bp
rename to tests/ComposBenchmarkApp/Android.bp
diff --git a/compos/benchmark/AndroidManifest.xml b/tests/ComposBenchmarkApp/AndroidManifest.xml
similarity index 100%
rename from compos/benchmark/AndroidManifest.xml
rename to tests/ComposBenchmarkApp/AndroidManifest.xml
diff --git a/compos/benchmark/AndroidTest.xml b/tests/ComposBenchmarkApp/AndroidTest.xml
similarity index 100%
rename from compos/benchmark/AndroidTest.xml
rename to tests/ComposBenchmarkApp/AndroidTest.xml
diff --git a/compos/benchmark/src/java/com/android/compos/benchmark/ComposBenchmark.java b/tests/ComposBenchmarkApp/src/java/com/android/compos/benchmark/ComposBenchmark.java
similarity index 100%
rename from compos/benchmark/src/java/com/android/compos/benchmark/ComposBenchmark.java
rename to tests/ComposBenchmarkApp/src/java/com/android/compos/benchmark/ComposBenchmark.java
diff --git a/compos/tests/Android.bp b/tests/ComposHostTestCases/Android.bp
similarity index 100%
rename from compos/tests/Android.bp
rename to tests/ComposHostTestCases/Android.bp
diff --git a/compos/tests/AndroidTest.xml b/tests/ComposHostTestCases/AndroidTest.xml
similarity index 100%
rename from compos/tests/AndroidTest.xml
rename to tests/ComposHostTestCases/AndroidTest.xml
diff --git a/compos/tests/java/android/compos/test/ComposTestCase.java b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
similarity index 89%
rename from compos/tests/java/android/compos/test/ComposTestCase.java
rename to tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
index b31f4f3..6e583c0 100644
--- a/compos/tests/java/android/compos/test/ComposTestCase.java
+++ b/tests/ComposHostTestCases/java/android/compos/test/ComposTestCase.java
@@ -54,8 +54,7 @@
     // Binaries used in test. (These paths are valid both in host and Microdroid.)
     private static final String ODREFRESH_BIN = "/apex/com.android.art/bin/odrefresh";
     private static final String COMPOSD_CMD_BIN = "/apex/com.android.compos/bin/composd_cmd";
-    private static final String COMPOS_VERIFY_BIN =
-            "/apex/com.android.compos/bin/compos_verify";
+    private static final String COMPOS_VERIFY_BIN = "/apex/com.android.compos/bin/compos_verify";
 
     private static final String COMPOS_APEXDATA_DIR = "/data/misc/apexdata/com.android.compos";
 
@@ -111,10 +110,13 @@
         android.tryRun("rm", "-rf", ODREFRESH_OUTPUT_DIR);
 
         if (mBackupSystemServerCompilerFilter != null) {
-            CLog.d("Restore dalvik.vm.systemservercompilerfilter to "
-                    + mBackupSystemServerCompilerFilter);
-            getDevice().setProperty(SYSTEM_SERVER_COMPILER_FILTER_PROP_NAME,
-                    mBackupSystemServerCompilerFilter);
+            CLog.d(
+                    "Restore dalvik.vm.systemservercompilerfilter to "
+                            + mBackupSystemServerCompilerFilter);
+            getDevice()
+                    .setProperty(
+                            SYSTEM_SERVER_COMPILER_FILTER_PROP_NAME,
+                            mBackupSystemServerCompilerFilter);
         }
     }
 
@@ -143,8 +145,8 @@
         }
 
         // Save the expected checksum for the output directory.
-        String expectedChecksumSnapshot = checksumDirectoryContentPartial(android,
-                ODREFRESH_OUTPUT_DIR);
+        String expectedChecksumSnapshot =
+                checksumDirectoryContentPartial(android, ODREFRESH_OUTPUT_DIR);
 
         // --check may delete the output.
         CommandResult result = runOdrefresh(android, "--check");
@@ -166,8 +168,8 @@
         assertVmBccIsValid();
 
         // Save the actual checksum for the output directory.
-        String actualChecksumSnapshot = checksumDirectoryContentPartial(android,
-                ODREFRESH_OUTPUT_DIR);
+        String actualChecksumSnapshot =
+                checksumDirectoryContentPartial(android, ODREFRESH_OUTPUT_DIR);
 
         // Expect the output of Comp OS to be the same as compiled on Android.
         assertThat(actualChecksumSnapshot).isEqualTo(expectedChecksumSnapshot);
@@ -185,11 +187,12 @@
         assertThat(bcc_file).isNotNull();
 
         // Add the BCC to test artifacts, in case it is ill-formed or otherwise interesting.
-        mTestLogs.addTestLog(bcc_file.getPath(), LogDataType.UNKNOWN,
-                new FileInputStreamSource(bcc_file));
+        mTestLogs.addTestLog(
+                bcc_file.getPath(), LogDataType.UNKNOWN, new FileInputStreamSource(bcc_file));
 
         // Find the validator binary - note that it's specified as a dependency in our Android.bp.
-        File validator = getTestInformation().getDependencyFile("hwtrust", /*targetFirst=*/ false);
+        File validator =
+                getTestInformation().getDependencyFile("hwtrust", /* targetFirst= */ false);
 
         CommandResult result =
                 new RunUtil()
@@ -197,6 +200,7 @@
                                 10000,
                                 validator.getAbsolutePath(),
                                 "dice-chain",
+                                "--allow-any-mode",
                                 bcc_file.getAbsolutePath());
         assertWithMessage("hwtrust failed").about(command_results()).that(result).isSuccess();
     }
diff --git a/authfs/Android.bp b/tests/authfs/Android.bp
similarity index 60%
rename from authfs/Android.bp
rename to tests/authfs/Android.bp
index e04d5e1..aa814eb 100644
--- a/authfs/Android.bp
+++ b/tests/authfs/Android.bp
@@ -1,51 +1,3 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
-    name: "authfs_defaults",
-    crate_name: "authfs",
-    srcs: [
-        "src/main.rs",
-    ],
-    edition: "2021",
-    rustlibs: [
-        "authfs_aidl_interface-rust",
-        "libandroid_logger",
-        "libanyhow",
-        "libauthfs_fsverity_metadata",
-        "libbinder_rs",
-        "libcfg_if",
-        "libclap",
-        "libfsverity_digests_proto_rust",
-        "libfuse_rust",
-        "libhex",
-        "liblibc",
-        "liblog_rust",
-        "libnix",
-        "libopenssl",
-        "libprotobuf",
-        "librpcbinder_rs",
-        "libthiserror",
-    ],
-    prefer_rlib: true,
-    target: {
-        darwin: {
-            enabled: false,
-        },
-    },
-    defaults: [
-        "crosvm_defaults",
-        "avf_build_flags_rust",
-    ],
-}
-
-rust_binary {
-    name: "authfs",
-    defaults: ["authfs_defaults"],
-    apex_available: ["com.android.virt"],
-}
-
 rust_test {
     name: "authfs_device_test_src_lib",
     defaults: ["authfs_defaults"],
diff --git a/authfs/TEST_MAPPING b/tests/authfs/TEST_MAPPING
similarity index 100%
copy from authfs/TEST_MAPPING
copy to tests/authfs/TEST_MAPPING
diff --git a/authfs/tests/benchmarks/Android.bp b/tests/authfs/benchmarks/Android.bp
similarity index 100%
rename from authfs/tests/benchmarks/Android.bp
rename to tests/authfs/benchmarks/Android.bp
diff --git a/authfs/tests/benchmarks/AndroidTest.xml b/tests/authfs/benchmarks/AndroidTest.xml
similarity index 100%
rename from authfs/tests/benchmarks/AndroidTest.xml
rename to tests/authfs/benchmarks/AndroidTest.xml
diff --git a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java b/tests/authfs/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
similarity index 99%
rename from authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
rename to tests/authfs/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
index 085d06e..abda3e3 100644
--- a/authfs/tests/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
+++ b/tests/authfs/benchmarks/src/java/com/android/fs/benchmarks/AuthFsBenchmarks.java
@@ -18,7 +18,6 @@
 
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
 
-
 import static org.junit.Assume.assumeFalse;
 import static org.junit.Assume.assumeTrue;
 
diff --git a/authfs/tests/benchmarks/src/measure_io.cpp b/tests/authfs/benchmarks/src/measure_io.cpp
similarity index 100%
rename from authfs/tests/benchmarks/src/measure_io.cpp
rename to tests/authfs/benchmarks/src/measure_io.cpp
diff --git a/authfs/tests/common/Android.bp b/tests/authfs/common/Android.bp
similarity index 100%
rename from authfs/tests/common/Android.bp
rename to tests/authfs/common/Android.bp
diff --git a/authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java b/tests/authfs/common/src/java/com/android/fs/common/AuthFsTestRule.java
similarity index 100%
rename from authfs/tests/common/src/java/com/android/fs/common/AuthFsTestRule.java
rename to tests/authfs/common/src/java/com/android/fs/common/AuthFsTestRule.java
diff --git a/authfs/tests/common/src/open_then_run.rs b/tests/authfs/common/src/open_then_run.rs
similarity index 92%
rename from authfs/tests/common/src/open_then_run.rs
rename to tests/authfs/common/src/open_then_run.rs
index e5e33eb..a9004b0 100644
--- a/authfs/tests/common/src/open_then_run.rs
+++ b/tests/authfs/common/src/open_then_run.rs
@@ -24,7 +24,7 @@
 use log::{debug, error};
 use std::fs::OpenOptions;
 use std::os::unix::fs::OpenOptionsExt;
-use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
+use std::os::unix::io::{OwnedFd, RawFd};
 use std::process::Command;
 
 // `PseudoRawFd` is just an integer and not necessarily backed by a real FD. It is used to denote
@@ -38,8 +38,8 @@
 }
 
 impl OwnedFdMapping {
-    fn as_fd_mapping(&self) -> FdMapping {
-        FdMapping { parent_fd: self.owned_fd.as_raw_fd(), child_fd: self.target_fd }
+    fn into_fd_mapping(self) -> FdMapping {
+        FdMapping { parent_fd: self.owned_fd, child_fd: self.target_fd }
     }
 }
 
@@ -148,9 +148,9 @@
 
     // Set up FD mappings in the child process.
     let mut fd_mappings = Vec::new();
-    fd_mappings.extend(args.ro_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
-    fd_mappings.extend(args.rw_file_fds.iter().map(OwnedFdMapping::as_fd_mapping));
-    fd_mappings.extend(args.dir_fds.iter().map(OwnedFdMapping::as_fd_mapping));
+    fd_mappings.extend(args.ro_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+    fd_mappings.extend(args.rw_file_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
+    fd_mappings.extend(args.dir_fds.into_iter().map(OwnedFdMapping::into_fd_mapping));
     command.fd_mappings(fd_mappings)?;
 
     debug!("Spawning {:?}", command);
diff --git a/authfs/tests/hosttests/Android.bp b/tests/authfs/hosttests/Android.bp
similarity index 100%
rename from authfs/tests/hosttests/Android.bp
rename to tests/authfs/hosttests/Android.bp
diff --git a/authfs/tests/hosttests/AndroidTest.xml b/tests/authfs/hosttests/AndroidTest.xml
similarity index 100%
rename from authfs/tests/hosttests/AndroidTest.xml
rename to tests/authfs/hosttests/AndroidTest.xml
diff --git a/authfs/tests/hosttests/java/src/com/android/fs/AuthFsHostTest.java b/tests/authfs/hosttests/java/src/com/android/fs/AuthFsHostTest.java
similarity index 87%
rename from authfs/tests/hosttests/java/src/com/android/fs/AuthFsHostTest.java
rename to tests/authfs/hosttests/java/src/com/android/fs/AuthFsHostTest.java
index d0a7c66..0a27506 100644
--- a/authfs/tests/hosttests/java/src/com/android/fs/AuthFsHostTest.java
+++ b/tests/authfs/hosttests/java/src/com/android/fs/AuthFsHostTest.java
@@ -75,8 +75,8 @@
     @BeforeClassWithInfo
     public static void beforeClassWithDevice(TestInformation testInfo) throws Exception {
         AuthFsTestRule.setUpAndroid(testInfo);
-        assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(/*protectedVm=*/ true));
-        AuthFsTestRule.startMicrodroid(/*protectedVm=*/ true);
+        assumeTrue(AuthFsTestRule.getDevice().supportsMicrodroid(/* protectedVm= */ true));
+        AuthFsTestRule.startMicrodroid(/* protectedVm= */ true);
         sAndroid = AuthFsTestRule.getAndroid();
         sMicrodroid = AuthFsTestRule.getMicrodroid();
     }
@@ -114,7 +114,7 @@
         // Setup
         runFdServerOnAndroid(
                 "--open-ro 3:input.4k --open-ro 4:input.4k.fsv_meta --open-ro"
-                    + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta",
+                        + " 6:input.4k1 --open-ro 7:input.4k1.fsv_meta",
                 "--ro-fds 3:4 --ro-fds 6:7");
         runAuthFsOnMicrodroid(
                 "--remote-ro-file 3:" + DIGEST_4K + " --remote-ro-file 6:" + DIGEST_4K1);
@@ -135,8 +135,7 @@
     public void testReadWithFsverityVerification_TamperedMerkleTree() throws Exception {
         // Setup
         runFdServerOnAndroid(
-                "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle",
-                "--ro-fds 3:4");
+                "--open-ro 3:input.4m --open-ro 4:input.4m.fsv_meta.bad_merkle", "--ro-fds 3:4");
         runAuthFsOnMicrodroid("--remote-ro-file 3:" + DIGEST_4M);
 
         // Verify
@@ -190,8 +189,12 @@
         // Action
         // Tampering with the first 2 4K-blocks of the backing file.
         assertThat(
-                writeZerosAtFileOffset(sAndroid, backendPath,
-                        /* offset */ 0, /* number */ 8192, /* writeThrough */ false))
+                        writeZerosAtFileOffset(
+                                sAndroid,
+                                backendPath,
+                                /* offset */ 0, /* number */
+                                8192, /* writeThrough */
+                                false))
                 .isSuccess();
 
         // Verify
@@ -199,21 +202,33 @@
         // when the content is inconsistent to the known hash. Use direct I/O to avoid simply
         // writing to the filesystem cache.
         assertThat(
-                writeZerosAtFileOffset(sMicrodroid, destPath,
-                        /* offset */ 0, /* number */ 1024, /* writeThrough */ true))
+                        writeZerosAtFileOffset(
+                                sMicrodroid,
+                                destPath,
+                                /* offset */ 0, /* number */
+                                1024, /* writeThrough */
+                                true))
                 .isFailed();
 
         // A full 4K write does not require to read back, so write can succeed even if the backing
         // block has already been tampered.
         assertThat(
-                writeZerosAtFileOffset(sMicrodroid, destPath,
-                        /* offset */ 4096, /* number */ 4096, /* writeThrough */ false))
+                        writeZerosAtFileOffset(
+                                sMicrodroid,
+                                destPath,
+                                /* offset */ 4096, /* number */
+                                4096, /* writeThrough */
+                                false))
                 .isSuccess();
 
         // Otherwise, a partial write with correct backing file should still succeed.
         assertThat(
-                writeZerosAtFileOffset(sMicrodroid, destPath,
-                        /* offset */ 8192, /* number */ 1024, /* writeThrough */ false))
+                        writeZerosAtFileOffset(
+                                sMicrodroid,
+                                destPath,
+                                /* offset */ 8192, /* number */
+                                1024, /* writeThrough */
+                                false))
                 .isSuccess();
     }
 
@@ -231,8 +246,12 @@
         // Action
         // Tampering with the first 4K-block of the backing file.
         assertThat(
-                writeZerosAtFileOffset(sAndroid, backendPath,
-                        /* offset */ 0, /* number */ 4096, /* writeThrough */ false))
+                        writeZerosAtFileOffset(
+                                sAndroid,
+                                backendPath,
+                                /* offset */ 0, /* number */
+                                4096, /* writeThrough */
+                                false))
                 .isSuccess();
 
         // Verify
@@ -258,8 +277,12 @@
         // Action
         // Tampering with the last 4K-block of the backing file.
         assertThat(
-                writeZerosAtFileOffset(sAndroid, backendPath,
-                        /* offset */ 4096, /* number */ 1, /* writeThrough */ false))
+                        writeZerosAtFileOffset(
+                                sAndroid,
+                                backendPath,
+                                /* offset */ 4096, /* number */
+                                1, /* writeThrough */
+                                false))
                 .isSuccess();
 
         // Verify
@@ -470,15 +493,22 @@
         sAndroid.run("test -f " + androidOutputPath);
 
         // Action
-        String output = sMicrodroid.run(
-                // Open the file for append and read
-                "exec 4>>" + outputPath + " 5<" + outputPath + "; "
-                // Delete the file from the directory
-                + "rm " + outputPath + "; "
-                // Append more data to the file descriptor
-                + "echo -n 456 >&4; "
-                // Print the whole file from the file descriptor
-                + "cat <&5");
+        String output =
+                sMicrodroid.run(
+                        // Open the file for append and read
+                        "exec 4>>"
+                                + outputPath
+                                + " 5<"
+                                + outputPath
+                                + "; "
+                                // Delete the file from the directory
+                                + "rm "
+                                + outputPath
+                                + "; "
+                                // Append more data to the file descriptor
+                                + "echo -n 456 >&4; "
+                                // Print the whole file from the file descriptor
+                                + "cat <&5");
 
         // Verify
         // Output contains all written data, while the files are deleted.
@@ -531,27 +561,33 @@
 
         // Verify
         String[] actual = sMicrodroid.run("cd " + authfsOutputDir + "; find |sort").split("\n");
-        String[] expected = new String[] {
-                ".",
-                "./dir",
-                "./dir/dir2",
-                "./dir/dir2/dir3",
-                "./dir/dir2/dir3/file1",
-                "./dir/dir2/dir3/file2",
-                "./dir/dir2/dir3/file3",
-                "./file"};
+        String[] expected =
+                new String[] {
+                    ".",
+                    "./dir",
+                    "./dir/dir2",
+                    "./dir/dir2/dir3",
+                    "./dir/dir2/dir3/file1",
+                    "./dir/dir2/dir3/file2",
+                    "./dir/dir2/dir3/file3",
+                    "./file"
+                };
         assertEquals(expected, actual);
 
         // Add more entries.
         sMicrodroid.run("mkdir -p " + authfsOutputDir + "/dir2");
         sMicrodroid.run("touch " + authfsOutputDir + "/file2");
         // Check new entries. Also check that the types are correct.
-        actual = sMicrodroid.run(
-                "cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort").split("\n");
+        actual =
+                sMicrodroid
+                        .run("cd " + authfsOutputDir + "; find -maxdepth 1 -type f |sort")
+                        .split("\n");
         expected = new String[] {"./file", "./file2"};
         assertEquals(expected, actual);
-        actual = sMicrodroid.run(
-                "cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort").split("\n");
+        actual =
+                sMicrodroid
+                        .run("cd " + authfsOutputDir + "; find -maxdepth 1 -type d |sort")
+                        .split("\n");
         expected = new String[] {".", "./dir", "./dir2"};
         assertEquals(expected, actual);
     }
@@ -687,8 +723,9 @@
                 "yes $'\\x01' | tr -d '\\n' | dd bs=1 count=" + numberOfOnes + " of=" + filePath);
     }
 
-    private static CommandResult checkReadAt(CommandRunner runner, String filePath, long offset,
-            long size) throws DeviceNotAvailableException {
+    private static CommandResult checkReadAt(
+            CommandRunner runner, String filePath, long offset, long size)
+            throws DeviceNotAvailableException {
         String cmd = "dd if=" + filePath + " of=/dev/null bs=1 count=" + size;
         if (offset > 0) {
             cmd += " skip=" + offset;
@@ -696,10 +733,15 @@
         return runner.runForResult(cmd);
     }
 
-    private CommandResult writeZerosAtFileOffset(CommandRunner runner, String filePath, long offset,
-            long numberOfZeros, boolean writeThrough) throws DeviceNotAvailableException {
-        String cmd = "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros
-                + " conv=notrunc";
+    private CommandResult writeZerosAtFileOffset(
+            CommandRunner runner,
+            String filePath,
+            long offset,
+            long numberOfZeros,
+            boolean writeThrough)
+            throws DeviceNotAvailableException {
+        String cmd =
+                "dd if=/dev/zero of=" + filePath + " bs=1 count=" + numberOfZeros + " conv=notrunc";
         if (offset > 0) {
             cmd += " seek=" + offset;
         }
diff --git a/authfs/testdata/README.md b/tests/authfs/testdata/README.md
similarity index 100%
rename from authfs/testdata/README.md
rename to tests/authfs/testdata/README.md
diff --git a/authfs/testdata/cert.der b/tests/authfs/testdata/cert.der
similarity index 100%
rename from authfs/testdata/cert.der
rename to tests/authfs/testdata/cert.der
Binary files differ
diff --git a/authfs/testdata/cert.pem b/tests/authfs/testdata/cert.pem
similarity index 100%
rename from authfs/testdata/cert.pem
rename to tests/authfs/testdata/cert.pem
diff --git a/authfs/testdata/input.4k b/tests/authfs/testdata/input.4k
similarity index 100%
rename from authfs/testdata/input.4k
rename to tests/authfs/testdata/input.4k
Binary files differ
diff --git a/authfs/testdata/input.4k.fsv_meta b/tests/authfs/testdata/input.4k.fsv_meta
similarity index 100%
rename from authfs/testdata/input.4k.fsv_meta
rename to tests/authfs/testdata/input.4k.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4k1 b/tests/authfs/testdata/input.4k1
similarity index 100%
rename from authfs/testdata/input.4k1
rename to tests/authfs/testdata/input.4k1
Binary files differ
diff --git a/authfs/testdata/input.4k1.fsv_meta b/tests/authfs/testdata/input.4k1.fsv_meta
similarity index 100%
rename from authfs/testdata/input.4k1.fsv_meta
rename to tests/authfs/testdata/input.4k1.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4m b/tests/authfs/testdata/input.4m
similarity index 100%
rename from authfs/testdata/input.4m
rename to tests/authfs/testdata/input.4m
Binary files differ
diff --git a/authfs/testdata/input.4m.fsv_meta b/tests/authfs/testdata/input.4m.fsv_meta
similarity index 100%
rename from authfs/testdata/input.4m.fsv_meta
rename to tests/authfs/testdata/input.4m.fsv_meta
Binary files differ
diff --git a/authfs/testdata/input.4m.fsv_meta.bad_merkle b/tests/authfs/testdata/input.4m.fsv_meta.bad_merkle
similarity index 100%
rename from authfs/testdata/input.4m.fsv_meta.bad_merkle
rename to tests/authfs/testdata/input.4m.fsv_meta.bad_merkle
Binary files differ
diff --git a/authfs/testdata/key.pem b/tests/authfs/testdata/key.pem
similarity index 100%
rename from authfs/testdata/key.pem
rename to tests/authfs/testdata/key.pem
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index ec1a553..0e59a01 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -268,6 +268,7 @@
                 /* fullDebug */ false,
                 (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
     }
+
     @Test
     public void testMicrodroidHostCpuTopologyBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
@@ -280,10 +281,7 @@
     @Test
     public void testMicrodroidDebugBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
-        runBootTimeTest(
-                "test_vm_boot_time_debug",
-                /* fullDebug */ true,
-                (builder) -> builder);
+        runBootTimeTest("test_vm_boot_time_debug", /* fullDebug */ true, (builder) -> builder);
     }
 
     private void testMicrodroidDebugBootTime_withVendorBase(File vendorDiskImage) throws Exception {
@@ -366,12 +364,12 @@
 
     @Test
     public void testVirtioBlkSeqReadRate() throws Exception {
-        testVirtioBlkReadRate(/*isRand=*/ false);
+        testVirtioBlkReadRate(/* isRand= */ false);
     }
 
     @Test
     public void testVirtioBlkRandReadRate() throws Exception {
-        testVirtioBlkReadRate(/*isRand=*/ true);
+        testVirtioBlkReadRate(/* isRand= */ true);
     }
 
     private void testVirtioBlkReadRate(boolean isRand) throws Exception {
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index 4a61016..e2956f2 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -69,6 +69,7 @@
 
     /** Boot time test related variables */
     private static final int REINSTALL_APEX_RETRY_INTERVAL_MS = 5 * 1000;
+
     private static final int REINSTALL_APEX_TIMEOUT_SEC = 15;
     private static final int COMPILE_STAGED_APEX_RETRY_INTERVAL_MS = 10 * 1000;
     private static final int COMPILE_STAGED_APEX_TIMEOUT_SEC = 540;
@@ -122,17 +123,18 @@
 
     @Test
     public void testNoLongHypSections() throws Exception {
-        String[] hypEvents = {
-            "hyp_enter", "hyp_exit"
-        };
+        String[] hypEvents = {"hyp_enter", "hyp_exit"};
 
-        assumeTrue("Skip without hypervisor tracing",
-            KvmHypTracer.isSupported(getDevice(), hypEvents));
+        assumeTrue(
+                "Skip without hypervisor tracing",
+                KvmHypTracer.isSupported(getDevice(), hypEvents));
 
         KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
         String result = tracer.run(COMPOSD_CMD_BIN + " test-compile");
         assertWithMessage("Failed to test compilation VM.")
-                .that(result).ignoringCase().contains("all ok");
+                .that(result)
+                .ignoringCase()
+                .contains("all ok");
 
         SimpleStats stats = tracer.getDurationStats();
         reportMetric(stats.getData(), "hyp_sections", "s");
@@ -141,32 +143,37 @@
 
     @Test
     public void testPsciMemProtect() throws Exception {
-        String[] hypEvents = {
-            "psci_mem_protect"
-        };
+        String[] hypEvents = {"psci_mem_protect"};
 
-        assumeTrue("Skip without hypervisor tracing",
-            KvmHypTracer.isSupported(getDevice(), hypEvents));
+        assumeTrue(
+                "Skip without hypervisor tracing",
+                KvmHypTracer.isSupported(getDevice(), hypEvents));
         KvmHypTracer tracer = new KvmHypTracer(getDevice(), hypEvents);
 
         /* We need to wait for crosvm to die so all the VM pages are reclaimed */
         String result = tracer.run(COMPOSD_CMD_BIN + " test-compile && killall -w crosvm || true");
         assertWithMessage("Failed to test compilation VM.")
-                .that(result).ignoringCase().contains("all ok");
+                .that(result)
+                .ignoringCase()
+                .contains("all ok");
 
         List<Integer> values = tracer.getPsciMemProtect();
 
         assertWithMessage("PSCI MEM_PROTECT events not recorded")
-            .that(values.size()).isGreaterThan(2);
+                .that(values.size())
+                .isGreaterThan(2);
 
         assertWithMessage("PSCI MEM_PROTECT counter not starting from 0")
-            .that(values.get(0)).isEqualTo(0);
+                .that(values.get(0))
+                .isEqualTo(0);
 
         assertWithMessage("PSCI MEM_PROTECT counter not ending with 0")
-            .that(values.get(values.size() - 1)).isEqualTo(0);
+                .that(values.get(values.size() - 1))
+                .isEqualTo(0);
 
         assertWithMessage("PSCI MEM_PROTECT counter didn't increment")
-            .that(Collections.max(values)).isGreaterThan(0);
+                .that(Collections.max(values))
+                .isGreaterThan(0);
     }
 
     @Test
@@ -182,9 +189,7 @@
 
     @Test
     public void testSettingsAppStartupTime() throws Exception {
-        String[] launchIntentPackages = {
-            "com.android.settings"
-        };
+        String[] launchIntentPackages = {"com.android.settings"};
         String launchIntentPackage = findSupportedPackage(launchIntentPackages);
         assume().withMessage("No supported settings package").that(launchIntentPackage).isNotNull();
         appStartupHelper(launchIntentPackage);
@@ -193,28 +198,34 @@
     private void appStartupHelper(String launchIntentPackage) throws Exception {
         assumeTrue(
                 "Skip on non-protected VMs",
-                ((TestDevice) getDevice()).supportsMicrodroid(/*protectedVm=*/ true));
+                ((TestDevice) getDevice()).supportsMicrodroid(/* protectedVm= */ true));
 
         StartupTimeMetricCollection mCollection =
                 new StartupTimeMetricCollection(getPackageName(launchIntentPackage), ROUND_COUNT);
         getAppStartupTime(launchIntentPackage, mCollection);
 
-        reportMetric(mCollection.mAppBeforeVmRunTotalTime,
+        reportMetric(
+                mCollection.mAppBeforeVmRunTotalTime,
                 "app_startup/" + mCollection.getPkgName() + "/total_time/before_vm",
                 "ms");
-        reportMetric(mCollection.mAppBeforeVmRunWaitTime,
+        reportMetric(
+                mCollection.mAppBeforeVmRunWaitTime,
                 "app_startup/" + mCollection.getPkgName() + "/wait_time/before_vm",
                 "ms");
-        reportMetric(mCollection.mAppDuringVmRunTotalTime,
+        reportMetric(
+                mCollection.mAppDuringVmRunTotalTime,
                 "app_startup/" + mCollection.getPkgName() + "/total_time/during_vm",
                 "ms");
-        reportMetric(mCollection.mAppDuringVmRunWaitTime,
+        reportMetric(
+                mCollection.mAppDuringVmRunWaitTime,
                 "app_startup/" + mCollection.getPkgName() + "/wait_time/during_vm",
                 "ms");
-        reportMetric(mCollection.mAppAfterVmRunTotalTime,
+        reportMetric(
+                mCollection.mAppAfterVmRunTotalTime,
                 "app_startup/" + mCollection.getPkgName() + "/total_time/after_vm",
                 "ms");
-        reportMetric(mCollection.mAppAfterVmRunWaitTime,
+        reportMetric(
+                mCollection.mAppAfterVmRunWaitTime,
                 "app_startup/" + mCollection.getPkgName() + "/wait_time/after_vm",
                 "ms");
     }
@@ -234,8 +245,9 @@
 
         for (String pkgName : pkgNameList) {
             String appPkg = getPackageName(pkgName);
-            String hasPackage = android.run("pm list package | grep -w " + appPkg + " 1> /dev/null"
-                    + "; echo $?");
+            String hasPackage =
+                    android.run(
+                            "pm list package | grep -w " + appPkg + " 1> /dev/null" + "; echo $?");
             assertNotNull(hasPackage);
 
             if (hasPackage.equals("0")) {
@@ -390,8 +402,8 @@
         }
     }
 
-    private int getFreeMemoryInfoMb(CommandRunner android) throws DeviceNotAvailableException,
-            IllegalArgumentException {
+    private int getFreeMemoryInfoMb(CommandRunner android)
+            throws DeviceNotAvailableException, IllegalArgumentException {
         int freeMemory = 0;
         String content = android.runForResult("cat /proc/meminfo").getStdout().trim();
         String[] lines = content.split("[\r\n]+");
@@ -410,8 +422,8 @@
             throws DeviceNotAvailableException, InterruptedException {
         android.run("input keyevent", "KEYCODE_WAKEUP");
         Thread.sleep(500);
-        final String ret = android.runForResult("dumpsys nfc | grep 'mScreenState='")
-                .getStdout().trim();
+        final String ret =
+                android.runForResult("dumpsys nfc | grep 'mScreenState='").getStdout().trim();
         if (ret != null && ret.contains("ON_LOCKED")) {
             android.run("input keyevent", "KEYCODE_MENU");
         }
@@ -429,8 +441,9 @@
                 String[] bootKeyVal = bootLoaderPhase.split(":");
                 String key = String.format("%s%s", BOOTLOADER_PREFIX, bootKeyVal[0]);
 
-                bootloaderTime.computeIfAbsent(key,
-                        k -> new ArrayList<>()).add(Double.parseDouble(bootKeyVal[1]));
+                bootloaderTime
+                        .computeIfAbsent(key, k -> new ArrayList<>())
+                        .add(Double.parseDouble(bootKeyVal[1]));
                 // SW is the time spent on the warning screen. So ignore it in
                 // final boot time calculation.
                 if (BOOTLOADER_PHASE_SW.equalsIgnoreCase(bootKeyVal[0])) {
@@ -438,8 +451,9 @@
                 }
                 bootLoaderTotalTime += Double.parseDouble(bootKeyVal[1]);
             }
-            bootloaderTime.computeIfAbsent(BOOTLOADER_TIME,
-                    k -> new ArrayList<>()).add(bootLoaderTotalTime);
+            bootloaderTime
+                    .computeIfAbsent(BOOTLOADER_TIME, k -> new ArrayList<>())
+                    .add(bootLoaderTotalTime);
         }
     }
 
@@ -518,7 +532,9 @@
                         android.runWithTimeout(
                                 3 * 60 * 1000, COMPOSD_CMD_BIN + " staged-apex-compile");
                 assertWithMessage("Failed to compile staged APEX. Reason: " + result)
-                    .that(result).ignoringCase().contains("all ok");
+                        .that(result)
+                        .ignoringCase()
+                        .contains("all ok");
 
                 CLog.i("Success to compile staged APEX. Result: " + result);
 
@@ -546,22 +562,23 @@
             try {
                 CommandRunner android = new CommandRunner(getDevice());
 
-                String packagesOutput =
-                        android.run("pm list packages -f --apex-only");
+                String packagesOutput = android.run("pm list packages -f --apex-only");
 
-                Pattern p = Pattern.compile(
-                        "package:(.*)=(com(?:\\.google)?\\.android\\.art)$", Pattern.MULTILINE);
+                Pattern p =
+                        Pattern.compile(
+                                "package:(.*)=(com(?:\\.google)?\\.android\\.art)$",
+                                Pattern.MULTILINE);
                 Matcher m = p.matcher(packagesOutput);
                 assertWithMessage("ART module not found. Packages are:\n" + packagesOutput)
-                    .that(m.find())
-                    .isTrue();
+                        .that(m.find())
+                        .isTrue();
 
                 String artApexPath = m.group(1);
 
-                CommandResult result = android.runForResult(
-                        "pm install --apex " + artApexPath);
+                CommandResult result = android.runForResult("pm install --apex " + artApexPath);
                 assertWithMessage("Failed to install APEX. Reason: " + result)
-                    .that(result.getExitCode()).isEqualTo(0);
+                        .that(result.getExitCode())
+                        .isEqualTo(0);
 
                 CLog.i("Success to install APEX. Result: " + result);
 
diff --git a/tests/ferrochrome/Android.bp b/tests/ferrochrome/Android.bp
index f165b8f..f1b7f27 100644
--- a/tests/ferrochrome/Android.bp
+++ b/tests/ferrochrome/Android.bp
@@ -19,8 +19,7 @@
     name: "ferrochrome-tests.sh",
     srcs: ["ferrochrome.sh"],
     out: ["ferrochrome-tests"],
-    // This breaks shebang, but test will execute the script with bash
-    cmd: "echo \"set -x\" > $(out); cat $(in) >> $(out)",
+    cmd: "sed '2 i set -x' $(in) > $(out)",
 }
 
 sh_binary_host {
diff --git a/tests/ferrochrome/AndroidTest.xml b/tests/ferrochrome/AndroidTest.xml
index 8053674..6c975be 100644
--- a/tests/ferrochrome/AndroidTest.xml
+++ b/tests/ferrochrome/AndroidTest.xml
@@ -34,14 +34,20 @@
         <option name="throw-if-cmd-fail" value="false" />
         <option name="run-command" value="mkdir /data/local/tmp" />
         <option name="teardown-command" value="pkill vmlauncher" />
-        <option name="teardown-command" value="rm /data/local/tmp/chromiumos_test_image.bin" />
+        <option name="teardown-command" value="rm /data/local/tmp/chromiumos_base_image.bin" />
+        <option name="teardown-command" value="rm -rf /data/local/tmp/ferrochrome_screenshots" />
     </target_preparer>
 
-    <test class="com.android.tradefed.testtype.binary.ExecutableHostTest" >
+    <test class="com.android.tradefed.testtype.binary.ExecutableHostTest">
         <option name="binary" value="ferrochrome-tests" />
         <option name="relative-path-execution" value="true" />
         <option name="runtime-hint" value="10m" />
         <option name="per-binary-timeout" value="20m" />
     </test>
+
+    <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+        <option name="directory-keys" value="/data/local/tmp/ferrochrome_screenshots" />
+        <option name="collect-on-run-ended-only" value="true" />
+    </metrics_collector>
 </configuration>
 
diff --git a/tests/ferrochrome/assets/vm_config.json b/tests/ferrochrome/assets/vm_config.json
index 358df95..53e3b72 100644
--- a/tests/ferrochrome/assets/vm_config.json
+++ b/tests/ferrochrome/assets/vm_config.json
@@ -2,19 +2,37 @@
     "name": "cros",
     "disks": [
         {
-            "image": "/data/local/tmp/chromiumos_test_image.bin",
+            "image": "/data/local/tmp/chromiumos_base_image.bin",
             "partitions": [],
             "writable": true
         }
     ],
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 8096,
+    "debuggable": true,
+    "console_out": true,
+    "connect_console": true,
+    "console_input_device": "hvc0",
+    "network": true,
+    "input": {
+        "touchscreen": true,
+        "keyboard": true,
+        "mouse": true,
+        "trackpad": true,
+        "switches": true
+    },
+    "audio": {
+        "speaker": true,
+        "microphone": true
+    },
     "gpu": {
         "backend": "virglrenderer",
         "context_types": ["virgl2"]
     },
-    "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,
-    "console_input_device": "ttyS0"
+    "display": {
+        "scale": "0.77",
+        "refresh_rate": "30"
+    }
 }
diff --git a/tests/ferrochrome/ferrochrome.sh b/tests/ferrochrome/ferrochrome.sh
index 210548a..03630dd 100755
--- a/tests/ferrochrome/ferrochrome.sh
+++ b/tests/ferrochrome/ferrochrome.sh
@@ -20,18 +20,29 @@
 set -e
 
 FECR_GS_URL="https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public"
-FECR_DEFAULT_VERSION="R127-15916.0.0"
+FECR_DEFAULT_VERSION="R128-15958.0.0"
+FECR_DEFAULT_SCREENSHOT_DIR="/data/local/tmp/ferrochrome_screenshots"  # Hardcoded at AndroidTest.xml
+FECR_TEST_IMAGE="chromiumos_test_image"
+FECR_BASE_IMAGE="chromiumos_base_image"
 FECR_DEVICE_DIR="/data/local/tmp"
+FECR_IMAGE_VM_CONFIG_JSON="chromiumos_base_image.bin"  # hardcoded at vm_config.json
 FECR_CONFIG_PATH="/data/local/tmp/vm_config.json"  # hardcoded at VmLauncherApp
-FECR_CONSOLE_LOG_PATH="/data/data/\${pkg_name}/files/console.log"
-FECR_BOOT_COMPLETED_LOG="Have fun and send patches!"
+FECR_CONSOLE_LOG_PATH="files/cros.log" # log file name is ${vm_name}.log
+FECR_TEST_IMAGE_BOOT_COMPLETED_LOG="Have fun and send patches!"
+FECR_BASE_IMAGE_BOOT_COMPLETED_LOG="Chrome started, our work is done, exiting"
 FECR_BOOT_TIMEOUT="300" # 5 minutes (300 seconds)
 ACTION_NAME="android.virtualization.VM_LAUNCHER"
-TRY_UNLOCK_MAX=10
+
+# Match this with AndroidTest.xml and assets/vm_config.json
+FECR_DEFAULT_IMAGE="${FECR_BASE_IMAGE}"
+FECR_DEFAULT_BOOT_COMPLETED_LOG="${FECR_BASE_IMAGE_BOOT_COMPLETED_LOG}"
 
 fecr_clean_up() {
   trap - INT
 
+  # Reset screen always on
+  adb shell svc power stayon false
+
   if [[ -d ${fecr_dir} && -z ${fecr_keep} ]]; then
     rm -rf ${fecr_dir}
   fi
@@ -40,7 +51,7 @@
 print_usage() {
   echo "ferochrome: Launches ferrochrome image"
   echo ""
-  echo "By default, this downloads ferrochrome image with version ${FECR_DEFAULT_VERSION},"
+  echo "By default, this downloads ${FECR_DEFAULT_VERSION} with version ${FECR_DEFAULT_VERSION},"
   echo "launches, and waits for boot completed."
   echo "When done, removes downloaded image on host while keeping pushed image on device."
   echo ""
@@ -53,14 +64,20 @@
   echo "  --skip: Skipping downloading and/or pushing images"
   echo "  --version \${version}: ferrochrome version to be downloaded"
   echo "  --keep: Keep downloaded ferrochrome image"
+  echo "  --test: Download test image instead"
+  echo "  --forever: Keep ferrochrome running forever. Used for manual test"
 }
 
-fecr_version=""
+fecr_version="${FECR_DEFAULT_VERSION}"
 fecr_dir=""
 fecr_keep=""
 fecr_skip=""
 fecr_script_path=$(dirname ${0})
 fecr_verbose=""
+fecr_image="${FECR_DEFAULT_IMAGE}"
+fecr_boot_completed_log="${FECR_DEFAULT_BOOT_COMPLETED_LOG}"
+fecr_screenshot_dir="${FECR_DEFAULT_SCREENSHOT_DIR}"
+fecr_forever=""
 
 # Parse parameters
 while (( "${#}" )); do
@@ -83,6 +100,13 @@
     --skip)
       fecr_skip="true"
       ;;
+    --test)
+      fecr_image="${FECR_TEST_IMAGE}"
+      fecr_boot_completed_log="${FECR_TEST_IMAGE_BOOT_COMPLETED_LOG}"
+      ;;
+    --forever)
+      fecr_forever="true"
+      ;;
     -h|--help)
       print_usage
       exit 0
@@ -112,9 +136,12 @@
 fi
 
 pkg_name=$(dirname ${resolved_activities})
+current_user=$(adb shell am get-current-user)
 
-adb shell pm grant ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
-adb shell pm clear ${pkg_name} > /dev/null
+echo "Reset app & granting permission"
+adb shell pm clear --user ${current_user} ${pkg_name} > /dev/null
+adb shell pm grant --user ${current_user} ${pkg_name} android.permission.RECORD_AUDIO
+adb shell pm grant --user ${current_user} ${pkg_name} android.permission.USE_CUSTOM_VIRTUAL_MACHINE > /dev/null
 
 if [[ -z "${fecr_skip}" ]]; then
   if [[ -z "${fecr_dir}" ]]; then
@@ -123,62 +150,43 @@
     fecr_dir=$(mktemp -d)
 
     echo "Downloading & extracting ferrochrome image to ${fecr_dir}"
-    fecr_version=${fecr_version:-${FECR_DEFAULT_VERSION}}
-    curl ${FECR_GS_URL}/${fecr_version}/chromiumos_test_image.tar.xz | tar xfJ - -C ${fecr_dir}
+    curl ${FECR_GS_URL}/${fecr_version}/${fecr_image}.tar.xz | tar xfJ - -C ${fecr_dir}
   fi
 
   echo "Pushing ferrochrome image to ${FECR_DEVICE_DIR}"
   adb shell mkdir -p ${FECR_DEVICE_DIR} > /dev/null || true
-  adb push ${fecr_dir}/chromiumos_test_image.bin ${FECR_DEVICE_DIR}
+  adb push ${fecr_dir}/${fecr_image}.bin ${FECR_DEVICE_DIR}/${FECR_IMAGE_VM_CONFIG_JSON}
   adb push ${fecr_script_path}/assets/vm_config.json ${FECR_CONFIG_PATH}
 fi
 
 echo "Ensure screen unlocked"
-
-try_unlock=0
-while [[ "${try_unlock}" -le "${TRY_UNLOCK_MAX}" ]]; do
-  screen_state=$(adb shell dumpsys nfc | sed -n 's/^mScreenState=\(.*\)$/\1/p')
-  case "${screen_state}" in
-    "ON_UNLOCKED")
-      break
-      ;;
-    "ON_LOCKED")
-      # Disclaimer: This can unlock phone only if unlock method is swipe (default after FDR)
-      adb shell input keyevent KEYCODE_MENU
-      ;;
-    "OFF_LOCKED"|"OFF_UNLOCKED")
-      adb shell input keyevent KEYCODE_WAKEUP
-      ;;
-    *)
-      echo "Unknown screen state. Continue to boot, but may fail"
-      break
-      ;;
-  esac
-  sleep 1
-  try_unlock=$((try_unlock+1))
-done
-if [[ "${try_unlock}" -gt "${TRY_UNLOCK_MAX}" ]]; then
-  >&2 echo "Failed to unlock screen. Try again after manual unlock"
-  exit 1
-fi
+adb shell svc power stayon true
+adb shell wm dismiss-keyguard
 
 echo "Starting ferrochrome"
 adb shell am start-activity -a ${ACTION_NAME} > /dev/null
 
-if [[ $(adb shell getprop ro.fw.mu.headless_system_user) == "true" ]]; then
-  current_user=$(adb shell am get-current-user)
-  log_path="/data/user/${current_user}/${pkg_name}/files/console.log"
-else
-  log_path="/data/data/${pkg_name}/files/console.log"
-fi
+# HSUM aware log path
+log_path="/data/user/${current_user}/${pkg_name}/${FECR_CONSOLE_LOG_PATH}"
 fecr_start_time=${EPOCHSECONDS}
 
-while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
-  adb shell grep -sF \""${FECR_BOOT_COMPLETED_LOG}"\" "${log_path}" && exit 0
-  sleep 10
-done
+echo "Check ${log_path} on device for console log"
 
->&2 echo "Ferrochrome failed to boot. Dumping console log"
->&2 adb shell cat ${log_path}
+if [[ "${fecr_forever}" == "true" ]]; then
+  echo "Ctrl+C to stop running"
+  echo "To open interactive serial console, use following command:"
+  echo "adb shell -t /apex/com.android.virt/bin/vm console"
+else
+  adb shell mkdir -p "${fecr_screenshot_dir}"
+  while [[ $((EPOCHSECONDS - fecr_start_time)) -lt ${FECR_BOOT_TIMEOUT} ]]; do
+    adb shell screencap -p "${fecr_screenshot_dir}/screenshot-${EPOCHSECONDS}.png"
+    adb shell grep -soF \""${fecr_boot_completed_log}"\" "${log_path}" && exit 0 || true
+    sleep 10
+  done
 
-exit 1
+  >&2 echo "Ferrochrome failed to boot. Dumping console log"
+  >&2 adb shell cat ${log_path}
+
+  exit 1
+fi
+
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
index 69527be..af68eb3 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/DeviceProperties.java
@@ -33,6 +33,8 @@
 
     private static final String CUTTLEFISH_DEVICE_PREFIX = "vsoc_";
     private static final String CUTTLEFISH_ARM64_DEVICE_PREFIX = "vsoc_arm64";
+    private static final String GOLDFISH_DEVICE_PREFIX = "emu64";
+    private static final String GOLDFISH_ARM64_DEVICE_PREFIX = "emu64a";
     private static final String USER_BUILD_TYPE = "user";
     private static final String HWASAN_SUFFIX = "_hwasan";
 
@@ -65,6 +67,23 @@
     }
 
     /**
+     * @return whether the device is a cuttlefish device.
+     */
+    public boolean isGoldfish() {
+        String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+        return vendorDeviceName != null && vendorDeviceName.startsWith(GOLDFISH_DEVICE_PREFIX);
+    }
+
+    /**
+     * @return whether the device is a cuttlefish device running on 64 bit Arm.
+     */
+    public boolean isGoldfishArm64() {
+        String vendorDeviceName = getProperty(KEY_VENDOR_DEVICE);
+        return vendorDeviceName != null
+                && vendorDeviceName.startsWith(GOLDFISH_ARM64_DEVICE_PREFIX);
+    }
+
+    /**
      * @return whether the build is HWASAN.
      */
     public boolean isHwasan() {
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
index dd68d6a..8f93d1e 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/MetricsProcessor.java
@@ -28,8 +28,8 @@
 
     public static String getMetricPrefix(String debugTag) {
         return "avf_perf"
-            + ((debugTag != null && !debugTag.isEmpty()) ? "[" + debugTag + "]" : "")
-            + "/";
+                + ((debugTag != null && !debugTag.isEmpty()) ? "[" + debugTag + "]" : "")
+                + "/";
     }
 
     public MetricsProcessor(String prefix) {
@@ -41,8 +41,8 @@
      * a {@link Map} with the corresponding keys equal to [mPrefix + name +
      * _[min|max|average|stdev]_ + unit].
      */
-    public Map<String, Double> computeStats(List<? extends Number> metrics, String name,
-            String unit) {
+    public Map<String, Double> computeStats(
+            List<? extends Number> metrics, String name, String unit) {
         List<Double> values = new ArrayList<>(metrics.size());
         for (Number metric : metrics) {
             values.add(metric.doubleValue());
diff --git a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
index e058674..c4aba81 100644
--- a/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
+++ b/tests/helper/src/java/com/android/microdroid/test/common/ProcessUtil.java
@@ -69,13 +69,13 @@
     }
 
     /** Gets global memory metrics key and values mapping */
-    public static Map<String, Long> getProcessMemoryMap(
-            Function<String, String> shellExecutor) throws IOException {
+    public static Map<String, Long> getProcessMemoryMap(Function<String, String> shellExecutor)
+            throws IOException {
         // The input file of parseMemoryInfo need a header string as the key of output entries.
         // /proc/meminfo doesn't have this line so add one as the key.
         String header = "device memory info\n";
-        List<SMapEntry> entries = parseMemoryInfo(header
-                + shellExecutor.apply("cat /proc/meminfo"));
+        List<SMapEntry> entries =
+                parseMemoryInfo(header + shellExecutor.apply("cat /proc/meminfo"));
         if (entries.size() != 1) {
             throw new RuntimeException(
                     "expected one entry in /proc/meminfo, got " + entries.size());
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 91ff4d8..135d947 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -81,6 +81,14 @@
         return getDeviceProperties().isCuttlefishArm64();
     }
 
+    public static boolean isGoldfish() {
+        return getDeviceProperties().isGoldfish();
+    }
+
+    private static boolean isGoldfishArm64() {
+        return getDeviceProperties().isGoldfishArm64();
+    }
+
     public static boolean isHwasan() {
         return getDeviceProperties().isHwasan();
     }
@@ -100,15 +108,15 @@
     protected final void grantPermission(String permission) {
         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         UiAutomation uiAutomation = instrumentation.getUiAutomation();
-        uiAutomation.grantRuntimePermission(instrumentation.getContext().getPackageName(),
-                permission);
+        uiAutomation.grantRuntimePermission(
+                instrumentation.getContext().getPackageName(), permission);
     }
 
     protected final void revokePermission(String permission) {
         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
         UiAutomation uiAutomation = instrumentation.getUiAutomation();
-        uiAutomation.revokeRuntimePermission(instrumentation.getContext().getPackageName(),
-                permission);
+        uiAutomation.revokeRuntimePermission(
+                instrumentation.getContext().getPackageName(), permission);
     }
 
     protected final void setMaxPerformanceTaskProfile() throws IOException {
@@ -225,12 +233,11 @@
     }
 
     protected void assumeVsrCompliant() {
-        boolean featureCheck = mCtx.getPackageManager().hasSystemFeature(FEATURE_WATCH) ||
-                               mCtx.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE) ||
-                               mCtx.getPackageManager().hasSystemFeature(FEATURE_LEANBACK);
-        assume().withMessage("This device is not VSR compliant")
-                .that(featureCheck)
-                .isFalse();
+        boolean featureCheck =
+                mCtx.getPackageManager().hasSystemFeature(FEATURE_WATCH)
+                        || mCtx.getPackageManager().hasSystemFeature(FEATURE_AUTOMOTIVE)
+                        || mCtx.getPackageManager().hasSystemFeature(FEATURE_LEANBACK);
+        assume().withMessage("This device is not VSR compliant").that(featureCheck).isFalse();
     }
 
     protected boolean isGsi() {
@@ -246,10 +253,12 @@
                 .that(KERNEL_VERSION)
                 .isNotEqualTo("5.4");
 
-        // Cuttlefish on Arm 64 doesn't and cannot support any form of virtualization, so there's
-        // no point running any of these tests.
-        assume().withMessage("Virtualization not supported on Arm64 Cuttlefish. b/341889915")
-                .that(isCuttlefishArm64())
+        // Cuttlefish/Goldfish on Arm 64 doesn't and cannot support any form of virtualization,
+        // so there's no point running any of these tests.
+        assume().withMessage(
+                        "Virtualization not supported on Arm64 Cuttlefish/Goldfish."
+                                + " b/341889915")
+                .that(isCuttlefishArm64() || isGoldfishArm64())
                 .isFalse();
     }
 
@@ -279,7 +288,8 @@
             if (log.contains("Run /init as init process") && !mInitStartedNanoTime.isPresent()) {
                 mInitStartedNanoTime = OptionalLong.of(System.nanoTime());
             }
-            if (log.contains("microdroid_manager") && log.contains("executing main task")
+            if (log.contains("microdroid_manager")
+                    && log.contains("executing main task")
                     && !mPayloadStartedNanoTime.isPresent()) {
                 mPayloadStartedNanoTime = OptionalLong.of(System.nanoTime());
             }
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java b/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
index 2e9d078..1d292eb 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/CommandResultSubject.java
@@ -25,9 +25,7 @@
 import com.google.common.truth.StringSubject;
 import com.google.common.truth.Subject;
 
-/**
- * A <a href="https://github.com/google/truth">Truth</a> subject for {@link CommandResult}.
- */
+/** A <a href="https://github.com/google/truth">Truth</a> subject for {@link CommandResult}. */
 public class CommandResultSubject extends Subject {
     private final CommandResult mActual;
 
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
index 5c72358..3814cdd 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/KvmHypTracer.java
@@ -17,22 +17,23 @@
 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.BufferedReader;
 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.List;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+
 import javax.annotation.Nonnull;
 
 class KvmHypEvent {
@@ -42,16 +43,14 @@
     public final String args;
     public final boolean valid;
 
-    private static final Pattern LOST_EVENT_PATTERN = Pattern.compile(
-            "^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
+    private static final Pattern LOST_EVENT_PATTERN =
+            Pattern.compile("^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
 
     public KvmHypEvent(String str) {
         Matcher matcher = LOST_EVENT_PATTERN.matcher(str);
-        if (matcher.find())
-            throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
+        if (matcher.find()) throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
 
-        Pattern pattern = Pattern.compile(
-                "^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (\\S+) (.*)");
+        Pattern pattern = Pattern.compile("^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (\\S+) (.*)");
 
         matcher = pattern.matcher(str);
         if (!matcher.find()) {
@@ -72,8 +71,7 @@
     }
 
     public String toString() {
-        return String.format(
-                "[%03d]\t%f: %s %s", cpu, timestamp, name, args);
+        return String.format("[%03d]\t%f: %s %s", cpu, timestamp, name, args);
     }
 }
 
@@ -99,16 +97,16 @@
     }
 
     public static boolean isSupported(ITestDevice device, String[] events) throws Exception {
-        for (String event: events) {
-            if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable"))
-                return false;
+        for (String event : events) {
+            if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable")) return false;
         }
         return true;
     }
 
     public KvmHypTracer(@Nonnull ITestDevice device, String[] events) throws Exception {
         assertWithMessage("Hypervisor events " + String.join(",", events) + " not supported")
-            .that(isSupported(device, events)).isTrue();
+                .that(isSupported(device, events))
+                .isTrue();
 
         mDevice = device;
         mRunner = new CommandRunner(mDevice);
@@ -123,8 +121,7 @@
         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: mHypEvents)
-            setNode(eventDir(event) + "/enable", 1);
+        for (String event : mHypEvents) setNode(eventDir(event) + "/enable", 1);
         setNode("trace", 0);
 
         /* Cat each per-cpu trace_pipe in its own tmp file in the background */
@@ -147,8 +144,10 @@
 
         /* Wait for cat to finish reading the pipe interface before killing it */
         for (int i = 0; i < mNrCpus; i++) {
-            cmd += "while $(test '$(ps -o S -p $CPU" + i
-                + "_TRACE_PIPE_PID | tail -n 1)' = 'R'); do sleep 1; done;";
+            cmd +=
+                    "while $(test '$(ps -o S -p $CPU"
+                            + i
+                            + "_TRACE_PIPE_PID | tail -n 1)' = 'R'); do sleep 1; done;";
             cmd += "kill -9 $CPU" + i + "_TRACE_PIPE_PID;";
         }
         cmd += "wait";
@@ -164,7 +163,7 @@
 
         mRunner.run("rm -f " + cmd_script);
 
-        for (String t: trace_pipes) {
+        for (String t : trace_pipes) {
             File trace = mDevice.pullFile(t);
             assertNotNull(trace);
             mTraces.add(trace);
@@ -190,12 +189,10 @@
         KvmHypEvent event;
         String l;
 
-        if ((l = br.readLine()) == null)
-            return null;
+        if ((l = br.readLine()) == null) return null;
 
         event = new KvmHypEvent(l);
-        if (!event.valid)
-            return null;
+        if (!event.valid) return null;
 
         return event;
     }
@@ -205,9 +202,10 @@
         SimpleStats stats = new SimpleStats();
 
         assertWithMessage("KvmHypTracer() is missing events " + String.join(",", reqEvents))
-            .that(hasEvents(reqEvents)).isTrue();
+                .that(hasEvents(reqEvents))
+                .isTrue();
 
-        for (File trace: mTraces) {
+        for (File trace : mTraces) {
             BufferedReader br = new BufferedReader(new FileReader(trace));
             double last = 0.0, hyp_enter = 0.0;
             String prev_event = "";
@@ -219,20 +217,18 @@
                     throw new ParseException("Incorrect CPU number: " + cpu, 0);
 
                 double cur = hypEvent.timestamp;
-                if (cur < last)
-                    throw new ParseException("Time must not go backward: " + cur, 0);
+                if (cur < last) throw new ParseException("Time must not go backward: " + cur, 0);
                 last = cur;
 
                 String event = hypEvent.name;
                 if (event.equals(prev_event)) {
-                    throw new ParseException("Hyp event found twice in a row: " +
-                                             trace + " - " + hypEvent, 0);
+                    throw new ParseException(
+                            "Hyp event found twice in a row: " + trace + " - " + hypEvent, 0);
                 }
 
                 switch (event) {
                     case "hyp_exit":
-                        if (prev_event.equals("hyp_enter"))
-                            stats.add(cur - hyp_enter);
+                        if (prev_event.equals("hyp_enter")) stats.add(cur - hyp_enter);
                         break;
                     case "hyp_enter":
                         hyp_enter = cur;
@@ -252,7 +248,8 @@
         List<Integer> psciMemProtect = new ArrayList<>();
 
         assertWithMessage("KvmHypTracer() is missing events " + String.join(",", reqEvents))
-            .that(hasEvents(reqEvents)).isTrue();
+                .that(hasEvents(reqEvents))
+                .isTrue();
 
         BufferedReader[] brs = new BufferedReader[mTraces.size()];
         KvmHypEvent[] next = new KvmHypEvent[mTraces.size()];
@@ -266,22 +263,20 @@
             double oldest = Double.MAX_VALUE;
             int oldestIdx = -1;
 
-            for (int i = 0; i < mTraces.size(); i ++) {
+            for (int i = 0; i < mTraces.size(); i++) {
                 if ((next[i] != null) && (next[i].timestamp < oldest)) {
                     oldest = next[i].timestamp;
                     oldestIdx = i;
                 }
             }
 
-            if (oldestIdx < 0)
-                break;
+            if (oldestIdx < 0) break;
 
-            Pattern pattern = Pattern.compile(
-                "count=([0-9]*) was=([0-9]*)");
+            Pattern pattern = Pattern.compile("count=([0-9]*) was=([0-9]*)");
             Matcher matcher = pattern.matcher(next[oldestIdx].args);
             if (!matcher.find()) {
-                throw new ParseException("Unexpected psci_mem_protect event: " +
-                                         next[oldestIdx], 0);
+                throw new ParseException(
+                        "Unexpected psci_mem_protect event: " + next[oldestIdx], 0);
             }
 
             int count = Integer.parseInt(matcher.group(1));
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java b/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
index 96ab543..ed753d0 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/LogArchiver.java
@@ -27,15 +27,17 @@
 
 /** A helper class for archiving device log files to the host's tradefed output directory. */
 public abstract class LogArchiver {
-    /** Copy device log (then delete) to a tradefed output directory on the host.
+    /**
+     * Copy device log (then delete) to a tradefed output directory on the host.
      *
      * @param logs A {@link TestLogData} that needs to be owned by the actual test case.
      * @param device The device to pull the log file from.
      * @param remotePath The path on the device.
      * @param localName Local file name to be copied to.
      */
-    public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
-            String localName) throws DeviceNotAvailableException {
+    public static void archiveLogThenDelete(
+            TestLogData logs, ITestDevice device, String remotePath, String localName)
+            throws DeviceNotAvailableException {
         File logFile = device.pullFile(remotePath);
         if (logFile != null) {
             logs.addTestLog(localName, LogDataType.TEXT, new FileInputStreamSource(logFile));
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index cd90fbe..974a58c 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -67,8 +67,11 @@
     protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
     private static final long MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS = 500;
     protected static final int MICRODROID_ADB_CONNECT_MAX_ATTEMPTS =
-            (int) (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000
-                / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
+            (int)
+                    (MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES
+                            * 60
+                            * 1000
+                            / MICRODROID_COMMAND_RETRY_INTERVAL_MILLIS);
 
     protected static final Set<String> SUPPORTED_GKI_VERSIONS =
             Collections.unmodifiableSet(new HashSet(Arrays.asList("android15-6.6")));
@@ -148,8 +151,9 @@
                 isGsi && vendorApiLevel < 202404);
     }
 
-    public static void archiveLogThenDelete(TestLogData logs, ITestDevice device, String remotePath,
-            String localName) throws DeviceNotAvailableException {
+    public static void archiveLogThenDelete(
+            TestLogData logs, ITestDevice device, String remotePath, String localName)
+            throws DeviceNotAvailableException {
         LogArchiver.archiveLogThenDelete(logs, device, remotePath, localName);
     }
 
@@ -167,6 +171,7 @@
         CommandResult result = RunUtil.getDefault().runTimedCmd(timeout, cmd);
         return result.getStdout().trim();
     }
+
     private static String join(String... strs) {
         return String.join(" ", Arrays.asList(strs));
     }
@@ -197,8 +202,7 @@
         throw new AssertionError("Failed to find test file " + name + " for module " + moduleName);
     }
 
-    public String getPathForPackage(String packageName)
-            throws DeviceNotAvailableException {
+    public String getPathForPackage(String packageName) throws DeviceNotAvailableException {
         return getPathForPackage(getDevice(), packageName);
     }
 
@@ -210,7 +214,8 @@
         CommandRunner android = new CommandRunner(device);
         String pathLine = android.run("pm", "path", packageName);
         assertWithMessage("Package " + packageName + " not found")
-                .that(pathLine).startsWith("package:");
+                .that(pathLine)
+                .startsWith("package:");
         return pathLine.substring("package:".length());
     }
 
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 80d1fc6..0f7be20 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -19,7 +19,6 @@
 import static com.android.microdroid.test.host.CommandResultSubject.command_results;
 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
-import com.android.tradefed.device.DeviceRuntimeException;
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
@@ -45,6 +44,7 @@
 import com.android.os.AtomsProto;
 import com.android.os.StatsLog;
 import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.DeviceRuntimeException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.device.TestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
@@ -79,13 +79,13 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
-import java.util.Objects;
 
 @RunWith(DeviceJUnit4Parameterized.class)
 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
@@ -490,6 +490,7 @@
                         .cpuTopology("match_host")
                         .protectedVm(true)
                         .gki(mGki)
+                        .name("protected_vm_runs_pvmfw")
                         .build(getAndroidDevice());
 
         // Assert
@@ -785,6 +786,7 @@
                         .cpuTopology("match_host")
                         .protectedVm(mProtectedVm)
                         .gki(mGki)
+                        .name("test_telemetry_pushed_atoms")
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         device.shutdownMicrodroid(microdroid);
@@ -816,7 +818,8 @@
         assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(mProtectedVm);
         assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
         assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
-        assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
+        assertThat(atomVmCreationRequested.getVmIdentifier())
+                .isEqualTo("test_telemetry_pushed_atoms");
         assertThat(atomVmCreationRequested.getConfigType())
                 .isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
         assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
@@ -826,11 +829,11 @@
 
         // Check VmBooted atom
         AtomsProto.VmBooted atomVmBooted = data.get(1).getAtom().getVmBooted();
-        assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("VmRunApp");
+        assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("test_telemetry_pushed_atoms");
 
         // Check VmExited atom
         AtomsProto.VmExited atomVmExited = data.get(2).getAtom().getVmExited();
-        assertThat(atomVmExited.getVmIdentifier()).isEqualTo("VmRunApp");
+        assertThat(atomVmExited.getVmIdentifier()).isEqualTo("test_telemetry_pushed_atoms");
         assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
         assertThat(atomVmExited.getExitSignal()).isEqualTo(9);
         // In CPU & memory related fields, check whether positive values are collected or not.
@@ -927,6 +930,7 @@
                         .memoryMib(minMemorySize())
                         .cpuTopology("match_host")
                         .protectedVm(mProtectedVm)
+                        .name("test_microdroid_boots")
                         .gki(mGki));
     }
 
@@ -940,6 +944,7 @@
                         .cpuTopology("match_host")
                         .protectedVm(mProtectedVm)
                         .gki(mGki)
+                        .name("test_microdroid_ram_usage")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -1205,6 +1210,7 @@
                         .protectedVm(mProtectedVm)
                         .gki(mGki)
                         .hugePages(true)
+                        .name("test_huge_pages")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
 
diff --git a/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
index a77ba40..5ae5186 100644
--- a/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
+++ b/tests/pvmfw/helper/java/com/android/pvmfw/test/host/Pvmfw.java
@@ -27,8 +27,8 @@
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.util.Objects;
 import java.nio.ByteBuffer;
+import java.util.Objects;
 
 /** pvmfw.bin with custom config payloads on host. */
 public final class Pvmfw {
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
index 2a6ab2d..7efbbc7 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -30,8 +30,8 @@
 import com.android.tradefed.device.DeviceRuntimeException;
 import com.android.tradefed.device.ITestDevice;
 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
-import com.android.tradefed.util.CommandStatus;
 import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
index 3b237aa..0cf0606 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/HwTrustJni.java
@@ -25,7 +25,8 @@
      * Validates a DICE chain.
      *
      * @param diceChain The dice chain to validate.
+     * @param allowAnyMode Allow the chain's certificates to have any mode.
      * @return true if the dice chain is valid, false otherwise.
      */
-    public static native boolean validateDiceChain(byte[] diceChain);
+    public static native boolean validateDiceChain(byte[] diceChain, boolean allowAnyMode);
 }
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 9629f37..d38af45 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -335,6 +335,7 @@
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
     }
+
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void autoCloseVm() throws Exception {
@@ -737,7 +738,6 @@
         VirtualMachineConfig.Builder otherOsBuilder =
                 newBaselineBuilder().setOs("microdroid_gki-android14-6.1");
         assertConfigCompatible(microdroidOsConfig, otherOsBuilder).isFalse();
-
     }
 
     private VirtualMachineConfig.Builder newBaselineBuilder() {
@@ -870,11 +870,12 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-1-2",
-            "9.17/C-1-4",
-    })
+    @CddTest(
+            requirements = {
+                "9.17/C-1-1",
+                "9.17/C-1-2",
+                "9.17/C-1-4",
+            })
     public void createVmWithConfigRequiresPermission() throws Exception {
         assumeSupportedDevice();
         revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
@@ -890,14 +891,16 @@
         SecurityException e =
                 assertThrows(
                         SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
-        assertThat(e).hasMessageThat()
+        assertThat(e)
+                .hasMessageThat()
                 .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-    })
+    @CddTest(
+            requirements = {
+                "9.17/C-1-1",
+            })
     public void deleteVm() throws Exception {
         assumeSupportedDevice();
 
@@ -954,9 +957,10 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-    })
+    @CddTest(
+            requirements = {
+                "9.17/C-1-1",
+            })
     public void validApkPathIsAccepted() throws Exception {
         assumeSupportedDevice();
 
@@ -989,10 +993,7 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-1"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"})
     public void extraApk() throws Exception {
         assumeSupportedDevice();
 
@@ -1044,7 +1045,7 @@
 
     @Test
     public void bootFailsWhenLowMem() throws Exception {
-        for (int memMib : new int[]{ 10, 20, 40 }) {
+        for (int memMib : new int[] {10, 20, 40}) {
             VirtualMachineConfig lowMemConfig =
                     newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                             .setMemoryBytes(memMib)
@@ -1061,8 +1062,9 @@
                             onPayloadReadyExecuted.complete(true);
                             super.onPayloadReady(vm);
                         }
+
                         @Override
-                        public void onStopped(VirtualMachine vm,  int reason) {
+                        public void onStopped(VirtualMachine vm, int reason) {
                             onStoppedExecuted.complete(true);
                             super.onStopped(vm, reason);
                         }
@@ -1210,16 +1212,14 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-7"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void instancesOfSameVmHaveDifferentCdis() throws Exception {
         assumeSupportedDevice();
         // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
         // `instance-id` which is input to DICE is contained in DT which is missing in CF.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
 
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig normalConfig =
@@ -1239,10 +1239,7 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-7"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void sameInstanceKeepsSameCdis() throws Exception {
         assumeSupportedDevice();
         assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse();
@@ -1321,7 +1318,7 @@
         grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config.json")
-                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setDebugLevel(DEBUG_LEVEL_NONE)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm_for_vsr", config);
         TestResults testResults =
@@ -1334,14 +1331,15 @@
         testResults.assertNoException();
         byte[] bccBytes = testResults.mBcc;
         assertThat(bccBytes).isNotNull();
-        assertThat(HwTrustJni.validateDiceChain(bccBytes)).isTrue();
+
+        String buildType = SystemProperties.get("ro.build.type");
+        boolean nonUserBuild = !buildType.isEmpty() && buildType != "user";
+
+        assertThat(HwTrustJni.validateDiceChain(bccBytes, nonUserBuild)).isTrue();
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-1-2"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2"})
     public void accessToCdisIsRestricted() throws Exception {
         assumeSupportedDevice();
 
@@ -1398,8 +1396,7 @@
 
     private void assertThatPartitionIsMissing(UUID partitionUuid) throws Exception {
         RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity");
-        assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent())
-                .isFalse();
+        assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent()).isFalse();
     }
 
     // Flips a bit of given partition, and then see if boot fails.
@@ -1419,10 +1416,7 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-7"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception {
         // If Updatable VM is supported => No instance.img required
         assumeNoUpdatableVmSupport();
@@ -1430,10 +1424,7 @@
     }
 
     @Test
-    @CddTest(requirements = {
-            "9.17/C-1-1",
-            "9.17/C-2-7"
-    })
+    @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"})
     public void bootFailsWhenPvmFwDataIsCompromised() throws Exception {
         // If Updatable VM is supported => No instance.img required
         assumeNoUpdatableVmSupport();
@@ -1455,8 +1446,8 @@
 
         BootResult bootResult = tryBootVmWithConfig(config, "test_vm_invalid_config");
         assertThat(bootResult.payloadStarted).isFalse();
-        assertThat(bootResult.deathReason).isEqualTo(
-                VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
+        assertThat(bootResult.deathReason)
+                .isEqualTo(VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG);
     }
 
     @Test
@@ -1693,7 +1684,8 @@
         // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
         // `instance-id` which is input to DICE is contained in DT which is missing in CF.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
 
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
@@ -2141,7 +2133,6 @@
         IVmShareTestService service = connection.waitForService();
         assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull();
 
-
         try {
             ITestService testServiceProxy = transferAndStartVm(service, vmDesc, "vm_to_share");
 
@@ -2401,7 +2392,8 @@
         assumeSupportedDevice();
         // TODO(b/325094712): Boot fails with vendor partition in Cuttlefish.
         assumeFalse(
-                "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
+                "Cuttlefish/Goldfish doesn't support device tree under /proc/device-tree",
+                isCuttlefish() || isGoldfish());
         // TODO(b/317567210): Boot fails with vendor partition in HWASAN enabled microdroid
         // after introducing verification based on DT and fstab in microdroid vendor partition.
         assumeFalse(
@@ -2624,16 +2616,15 @@
     }
 
     private long minMemoryRequired() {
-      assertThat(Build.SUPPORTED_ABIS).isNotEmpty();
-      String primaryAbi = Build.SUPPORTED_ABIS[0];
-      switch (primaryAbi) {
-        case "x86_64":
-          return MIN_MEM_X86_64;
-        case "arm64-v8a":
-        case "arm64-v8a-hwasan":
-          return MIN_MEM_ARM64;
-      }
-      throw new AssertionError("Unsupported ABI: " + primaryAbi);
+        assertThat(Build.SUPPORTED_ABIS).isNotEmpty();
+        String primaryAbi = Build.SUPPORTED_ABIS[0];
+        switch (primaryAbi) {
+            case "x86_64":
+                return MIN_MEM_X86_64;
+            case "arm64-v8a":
+            case "arm64-v8a-hwasan":
+                return MIN_MEM_ARM64;
+        }
+        throw new AssertionError("Unsupported ABI: " + primaryAbi);
     }
-
 }
diff --git a/tests/testapk/src/native/hwtrust_jni.rs b/tests/testapk/src/native/hwtrust_jni.rs
index 3b00364..058b1a6 100644
--- a/tests/testapk/src/native/hwtrust_jni.rs
+++ b/tests/testapk/src/native/hwtrust_jni.rs
@@ -29,6 +29,7 @@
     env: JNIEnv,
     _class: JClass,
     dice_chain: JByteArray,
+    allow_any_mode: jboolean,
 ) -> jboolean {
     android_logger::init_once(
         android_logger::Config::default()
@@ -36,7 +37,7 @@
             .with_max_level(log::LevelFilter::Debug),
     );
     debug!("Starting the DICE chain validation ...");
-    match validate_dice_chain(env, dice_chain) {
+    match validate_dice_chain(env, dice_chain, allow_any_mode) {
         Ok(_) => {
             info!("DICE chain validated successfully");
             true
@@ -49,9 +50,14 @@
     .into()
 }
 
-fn validate_dice_chain(env: JNIEnv, jdice_chain: JByteArray) -> Result<()> {
+fn validate_dice_chain(
+    env: JNIEnv,
+    jdice_chain: JByteArray,
+    allow_any_mode: jboolean,
+) -> Result<()> {
     let dice_chain = env.convert_byte_array(jdice_chain)?;
-    let session = Session::default();
+    let mut session = Session::default();
+    session.set_allow_any_mode(allow_any_mode == jboolean::from(true));
     let _chain = dice::Chain::from_cbor(&session, &dice_chain)?;
     Ok(())
 }
diff --git a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
index 1772e6b..27e26e5 100644
--- a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
+++ b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
@@ -16,18 +16,19 @@
 
 package com.android.microdroid.test;
 
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
 import android.system.virtualmachine.VirtualMachineConfig;
 
 import com.android.compatibility.common.util.CddTest;
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
 
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-
 import org.junit.Before;
-import org.junit.runners.Parameterized;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
 /**
  * Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
diff --git a/tests/vm_accessor/README.md b/tests/vm_accessor/README.md
new file mode 100644
index 0000000..c85cf3c
--- /dev/null
+++ b/tests/vm_accessor/README.md
@@ -0,0 +1,40 @@
+# Demo for serving a service in a VM
+
+You can implement a service in a VM, and let client in the Android can use it
+as if it's in the Android. To do so, implement IAccessor.
+
+IAccessor allows AIDL service in a VM can be accessed via servicemanager.
+To do so, VM owners should also provide IAccessor implementation. servicemanager
+will connect to the IAccessor and get the binder of the service in a VM with it.
+
+com.android.virt.accessor_demo apex contains the minimum setup for IAccessor as
+follows:
+  - accessor_demo: Sample implementation of IAccessor, which is expected to
+      launch VM and returns the Vsock connection of service in the VM.
+  - AccessorVmApp: Sample app that conatins VM payload. Provides the actual
+      implementation of service in a VM.
+
+## Build
+
+You need to do envsetup.sh
+```shell
+m com.android.virt.accessor_demo
+```
+
+## Install (requires userdebug build)
+
+For very first install,
+
+```shell
+adb remount -R || adb wait-for-device  # Remount to push apex to /system_ext
+adb root && adb remount                # Ensure it's rebooted.
+adb push $ANDROID_PRODUCT_OUT/system_ext/apex/com.android.virt.accessor_demo.apex /system_ext/apex
+adb reboot && adb wait-for-device      # Ensure that newly pushed apex at /system_ext is installed
+```
+
+Once it's installed, you can simply use `adb install` for update.
+
+```shell
+adb install $ANDROID_PRODUCT_OUT/system_ext/com.android.virt.accessor_demo.apex
+adb reboot
+```
diff --git a/tests/vm_accessor/accessor/Android.bp b/tests/vm_accessor/accessor/Android.bp
new file mode 100644
index 0000000..7c0ee6d
--- /dev/null
+++ b/tests/vm_accessor/accessor/Android.bp
@@ -0,0 +1,31 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+    name: "accessor_demo",
+    crate_name: "accessor_demo",
+    srcs: ["src/main.rs"],
+    edition: "2021",
+    prefer_rlib: true,
+    defaults: ["avf_build_flags_rust"], // for reading llvm_changes
+    apex_available: [
+        "com.android.virt.accessor_demo",
+    ],
+    rustlibs: [
+        "android.system.virtualizationservice-rust",
+        "android.os.accessor-rust",
+        "libanyhow",
+        "libandroid_logger",
+        "libbinder_rs",
+        "libenv_logger",
+        "libglob",
+        "libhypervisor_props",
+        "liblog_rust",
+        "libmicrodroid_payload_config",
+        "librand",
+        "libvmconfig",
+        "libvmclient",
+        "libnix",
+    ],
+}
diff --git a/tests/vm_accessor/accessor/src/accessor.rs b/tests/vm_accessor/accessor/src/accessor.rs
new file mode 100644
index 0000000..6a9ced6
--- /dev/null
+++ b/tests/vm_accessor/accessor/src/accessor.rs
@@ -0,0 +1,52 @@
+// Copyright 2024, 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.
+
+//! IAcessor implementation.
+//! TODO: Keep this in proper places, so other pVMs can use this.
+//! TODO: Allows to customize VMs for launching. (e.g. port, ...)
+
+use android_os_accessor::aidl::android::os::IAccessor::IAccessor;
+use binder::{self, Interface, ParcelFileDescriptor};
+use log::info;
+use std::time::Duration;
+use vmclient::VmInstance;
+
+// Note: Do not use LazyServiceGuard here, to make this service and VM are quit
+//       when nobody references it.
+// TODO(b/353492849): Do not use IAccessor directly.
+#[derive(Debug)]
+pub struct Accessor {
+    // Note: we can't simply keep reference by specifying lifetime to Accessor,
+    //       because 'trait Interface' requires 'static.
+    vm: VmInstance,
+    port: i32,
+}
+
+impl Accessor {
+    pub fn new(vm: VmInstance, port: i32) -> Self {
+        Self { vm, port }
+    }
+}
+
+impl Interface for Accessor {}
+
+impl IAccessor for Accessor {
+    fn addConnection(&self) -> binder::Result<ParcelFileDescriptor> {
+        self.vm.wait_until_ready(Duration::from_secs(10)).unwrap();
+
+        info!("VM is ready. Connecting to service via port {}", self.port);
+
+        self.vm.vm.connectVsock(self.port)
+    }
+}
diff --git a/tests/vm_accessor/accessor/src/main.rs b/tests/vm_accessor/accessor/src/main.rs
new file mode 100644
index 0000000..27ce415
--- /dev/null
+++ b/tests/vm_accessor/accessor/src/main.rs
@@ -0,0 +1,55 @@
+// Copyright 2024, 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.
+
+//! Android VM control tool.
+
+mod accessor;
+mod run;
+
+use accessor::Accessor;
+use android_os_accessor::aidl::android::os::IAccessor::BnAccessor;
+use anyhow::Error;
+use anyhow::{anyhow, bail};
+use binder::{BinderFeatures, ProcessState};
+use log::info;
+use run::run_vm;
+
+// Private contract between IAccessor impl and VM service.
+const PORT: i32 = 5678;
+
+// MUST match with VINTF and init.rc
+// TODO(b/354632613): Get this from VINTF
+const SERVICE_NAME: &str = "android.os.IAccessor/IAccessorVmService/default";
+
+fn main() -> Result<(), Error> {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("accessor_demo")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+
+    let vm = run_vm()?;
+
+    // If you want to serve multiple services in a VM, then register Accessor impls multiple times.
+    let accessor = Accessor::new(vm, PORT);
+    let accessor_binder = BnAccessor::new_binder(accessor, BinderFeatures::default());
+    binder::register_lazy_service(SERVICE_NAME, accessor_binder.as_binder()).map_err(|e| {
+        anyhow!("Failed to register lazy service, service={SERVICE_NAME}, err={e:?}",)
+    })?;
+    info!("service {SERVICE_NAME} is registered as lazy service");
+
+    ProcessState::join_thread_pool();
+
+    bail!("Thread pool unexpectedly ended")
+}
diff --git a/tests/vm_accessor/accessor/src/run.rs b/tests/vm_accessor/accessor/src/run.rs
new file mode 100644
index 0000000..932baab
--- /dev/null
+++ b/tests/vm_accessor/accessor/src/run.rs
@@ -0,0 +1,181 @@
+// Copyright 2024, 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.
+
+//! Command to run a VM.
+
+use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    IVirtualizationService::IVirtualizationService,
+    PartitionType::PartitionType,
+    VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
+    VirtualMachineConfig::VirtualMachineConfig,
+    VirtualMachinePayloadConfig::VirtualMachinePayloadConfig,
+};
+use anyhow::{bail, Context, Error};
+use binder::{ParcelFileDescriptor, Strong};
+use glob::glob;
+use log::{error, info};
+use rand::{distributions::Alphanumeric, Rng};
+use std::fs::{self, File};
+use std::io::{self, BufRead, BufReader};
+use std::path::PathBuf;
+use std::thread;
+use vmclient::{ErrorCode, VmInstance};
+use vmconfig::open_parcel_file;
+
+// These are private contract between IAccessor impl and VM service.
+const PAYLOAD_BINARY_NAME: &str = "libaccessor_vm_payload.so";
+const VM_OS_NAME: &str = "microdroid";
+
+const INSTANCE_FILE_SIZE: u64 = 10 * 1024 * 1024;
+
+fn get_service() -> Result<Strong<dyn IVirtualizationService>, Error> {
+    let virtmgr =
+        vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
+    virtmgr.connect().context("Failed to connect to VirtualizationService")
+}
+
+fn find_vm_apk_path() -> Result<PathBuf, Error> {
+    const GLOB_PATTERN: &str = "/apex/com.android.virt.accessor_demo/app/**/AccessorVmApp*.apk";
+    let mut entries: Vec<PathBuf> =
+        glob(GLOB_PATTERN).context("failed to glob")?.filter_map(|e| e.ok()).collect();
+    if entries.len() > 1 {
+        bail!("Found more than one apk matching {}", GLOB_PATTERN);
+    }
+    if let Some(path) = entries.pop() {
+        info!("Found accessor apk at {path:?}");
+        Ok(path)
+    } else {
+        bail!("No apks match {}", GLOB_PATTERN)
+    }
+}
+
+fn create_work_dir() -> Result<PathBuf, Error> {
+    let s: String =
+        rand::thread_rng().sample_iter(&Alphanumeric).take(17).map(char::from).collect();
+    let work_dir = PathBuf::from("/data/local/tmp/microdroid").join(s);
+    info!("creating work dir {}", work_dir.display());
+    fs::create_dir_all(&work_dir).context("failed to mkdir")?;
+    Ok(work_dir)
+}
+
+/// Run a VM with Microdroid
+pub fn run_vm() -> Result<VmInstance, Error> {
+    let service = get_service()?;
+
+    let apk = File::open(find_vm_apk_path()?).context("Failed to open APK file")?;
+    let apk_fd = ParcelFileDescriptor::new(apk);
+
+    let work_dir = create_work_dir()?;
+    info!("work dir: {}", work_dir.display());
+
+    let idsig =
+        File::create_new(work_dir.join("apk.idsig")).context("Failed to create idsig file")?;
+    let idsig_fd = ParcelFileDescriptor::new(idsig);
+    service.createOrUpdateIdsigFile(&apk_fd, &idsig_fd)?;
+
+    let instance_img_path = work_dir.join("instance.img");
+    let instance_img =
+        File::create_new(&instance_img_path).context("Failed to create instance.img file")?;
+    service.initializeWritablePartition(
+        &ParcelFileDescriptor::new(instance_img),
+        INSTANCE_FILE_SIZE.try_into()?,
+        PartitionType::ANDROID_VM_INSTANCE,
+    )?;
+    info!("created instance image at: {instance_img_path:?}");
+
+    let instance_id = if cfg!(llpvm_changes) {
+        let id = service.allocateInstanceId().context("Failed to allocate instance_id")?;
+        fs::write(work_dir.join("instance_id"), id)?;
+        id
+    } else {
+        // if llpvm feature flag is disabled, instance_id is not used.
+        [0u8; 64]
+    };
+
+    let payload = Payload::PayloadConfig(VirtualMachinePayloadConfig {
+        payloadBinaryName: PAYLOAD_BINARY_NAME.to_owned(),
+        extraApks: Default::default(),
+    });
+
+    let vm_config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
+        name: String::from("AccessorVm"),
+        apk: apk_fd.into(),
+        idsig: idsig_fd.into(),
+        extraIdsigs: Default::default(),
+        instanceImage: open_parcel_file(&instance_img_path, true /* writable */)?.into(),
+        instanceId: instance_id,
+        payload,
+        osName: VM_OS_NAME.to_owned(),
+        debugLevel: DebugLevel::FULL,
+        ..Default::default()
+    });
+
+    info!("creating VM");
+    let vm = VmInstance::create(
+        service.as_ref(),
+        &vm_config,
+        Some(android_log_fd()?), /* console_out */
+        None,                    /* console_in */
+        Some(android_log_fd()?), /* log */
+        Some(Box::new(Callback {})),
+    )
+    .context("Failed to create VM")?;
+    vm.start().context("Failed to start VM")?;
+
+    info!("started IAccessor VM with CID {}", vm.cid());
+
+    Ok(vm)
+}
+
+struct Callback {}
+
+impl vmclient::VmCallback for Callback {
+    fn on_payload_started(&self, _cid: i32) {
+        info!("payload started");
+    }
+
+    fn on_payload_ready(&self, _cid: i32) {
+        info!("payload is ready");
+    }
+
+    fn on_payload_finished(&self, _cid: i32, exit_code: i32) {
+        info!("payload finished with exit code {}", exit_code);
+    }
+
+    fn on_error(&self, _cid: i32, error_code: ErrorCode, message: &str) {
+        error!("VM encountered an error: code={:?}, message={}", error_code, message);
+    }
+}
+
+/// This function is only exposed for testing.
+/// Production code prefer not expose logs from VM.
+fn android_log_fd() -> io::Result<File> {
+    let (reader_fd, writer_fd) = nix::unistd::pipe()?;
+
+    let reader = File::from(reader_fd);
+    let writer = File::from(writer_fd);
+
+    thread::spawn(|| {
+        for line in BufReader::new(reader).lines() {
+            match line {
+                Ok(l) => info!("{}", l),
+                Err(e) => {
+                    error!("Failed to read line from VM: {e:?}");
+                    break;
+                }
+            }
+        }
+    });
+    Ok(writer)
+}
diff --git a/tests/vm_accessor/accessor_vm/Android.bp b/tests/vm_accessor/accessor_vm/Android.bp
new file mode 100644
index 0000000..bbd15bd
--- /dev/null
+++ b/tests/vm_accessor/accessor_vm/Android.bp
@@ -0,0 +1,30 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "AccessorVmApp",
+    installable: true,
+    jni_libs: ["libaccessor_vm_payload"],
+    jni_uses_platform_apis: true,
+    use_embedded_native_libs: true,
+    sdk_version: "system_current",
+    compile_multilib: "first",
+    apex_available: ["com.android.virt.accessor_demo"],
+}
+
+rust_ffi {
+    name: "libaccessor_vm_payload",
+    crate_name: "accessor_vm_payload",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["src/main.rs"],
+    prefer_rlib: true,
+    rustlibs: [
+        "com.android.virt.accessor_demo.vm_service-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "liblog_rust",
+        "libvm_payload_rs",
+    ],
+    apex_available: ["com.android.virt.accessor_demo"],
+}
diff --git a/tests/vm_accessor/accessor_vm/AndroidManifest.xml b/tests/vm_accessor/accessor_vm/AndroidManifest.xml
new file mode 100644
index 0000000..429e08a
--- /dev/null
+++ b/tests/vm_accessor/accessor_vm/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.virtualization.accessor_demo">
+     <application android:hasCode="false"/>
+</manifest>
diff --git a/tests/vm_accessor/accessor_vm/assets/config.json b/tests/vm_accessor/accessor_vm/assets/config.json
new file mode 100644
index 0000000..f921ec1
--- /dev/null
+++ b/tests/vm_accessor/accessor_vm/assets/config.json
@@ -0,0 +1,10 @@
+{
+    "os": {
+        "name": "microdroid"
+    },
+    "task": {
+        "type": "microdroid_launcher",
+        "command": "accessor_vm.so"
+    },
+    "export_tombstones": true
+}
diff --git a/tests/vm_accessor/accessor_vm/src/main.rs b/tests/vm_accessor/accessor_vm/src/main.rs
new file mode 100644
index 0000000..bd83cc1
--- /dev/null
+++ b/tests/vm_accessor/accessor_vm/src/main.rs
@@ -0,0 +1,64 @@
+// Copyright 2024, 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.
+
+//! VM with the simplest service for IAccessor demo
+
+use anyhow::Result;
+use com_android_virt_accessor_demo_vm_service::{
+    aidl::com::android::virt::accessor_demo::vm_service::IAccessorVmService::{
+        BnAccessorVmService, IAccessorVmService,
+    },
+    binder::{self, BinderFeatures, Interface, Strong},
+};
+use log::{error, info};
+
+// Private contract between IAccessor impl and VM service.
+const PORT: u32 = 5678;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("accessor_vm")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        std::process::exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Starting stub payload for IAccessor demo");
+
+    vm_payload::run_single_vsock_service(AccessorVmService::new_binder(), PORT)
+}
+
+struct AccessorVmService {}
+
+impl Interface for AccessorVmService {}
+
+impl AccessorVmService {
+    fn new_binder() -> Strong<dyn IAccessorVmService> {
+        BnAccessorVmService::new_binder(AccessorVmService {}, BinderFeatures::default())
+    }
+}
+
+impl IAccessorVmService for AccessorVmService {
+    fn add(&self, a: i32, b: i32) -> binder::Result<i32> {
+        Ok(a + b)
+    }
+}
diff --git a/tests/vm_accessor/aidl/Android.bp b/tests/vm_accessor/aidl/Android.bp
new file mode 100644
index 0000000..0078608
--- /dev/null
+++ b/tests/vm_accessor/aidl/Android.bp
@@ -0,0 +1,20 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aidl_interface {
+    name: "com.android.virt.accessor_demo.vm_service",
+    srcs: ["**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            gen_rpc: true,
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt.accessor_demo",
+            ],
+        },
+    },
+}
diff --git a/rialto/image.ld b/tests/vm_accessor/aidl/com/android/virt/accessor_demo/vm_service/IAccessorVmService.aidl
similarity index 62%
copy from rialto/image.ld
copy to tests/vm_accessor/aidl/com/android/virt/accessor_demo/vm_service/IAccessorVmService.aidl
index 368acbb..29bf979 100644
--- a/rialto/image.ld
+++ b/tests/vm_accessor/aidl/com/android/virt/accessor_demo/vm_service/IAccessorVmService.aidl
@@ -1,11 +1,11 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright 2024 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
  *
- *     https://www.apache.org/licenses/LICENSE-2.0
+ *      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,
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-MEMORY
-{
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
-	image		: ORIGIN = 0x80200000, LENGTH = 2M
-	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
+package com.android.virt.accessor_demo.vm_service;
+
+/** {@hide} */
+// TODO(b/349578050): Add more methods that take or return another binder.
+interface IAccessorVmService {
+    int add(int a, int b);
 }
diff --git a/tests/vm_accessor/apex/Android.bp b/tests/vm_accessor/apex/Android.bp
new file mode 100644
index 0000000..e954572
--- /dev/null
+++ b/tests/vm_accessor/apex/Android.bp
@@ -0,0 +1,52 @@
+// Copyright 2024 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// apex_test allows to skips apex_available checks for dependencies.
+// However, real apex should add itself to apex_available for all dependencies.
+apex_test {
+    name: "com.android.virt.accessor_demo",
+    manifest: "manifest.json",
+    file_contexts: "accessor_demo-file_contexts",
+
+    // You probably need your own key
+    key: "com.android.virt.key",
+
+    updatable: false,
+    future_updatable: false,
+    platform_apis: true,
+    system_ext_specific: true,
+
+    binaries: ["accessor_demo"],
+    apps: ["AccessorVmApp"],
+    prebuilts: [
+        "accessor_demo.init.rc",
+        "accessor_demo.xml",
+    ],
+}
+
+prebuilt_etc {
+    name: "accessor_demo.init.rc",
+    src: "accessor_demo.init.rc",
+    installable: false,
+}
+
+prebuilt_etc {
+    name: "accessor_demo.xml",
+    src: "accessor_demo.xml",
+    sub_dir: "vintf",
+    installable: false,
+}
diff --git a/tests/vm_accessor/apex/accessor_demo-file_contexts b/tests/vm_accessor/apex/accessor_demo-file_contexts
new file mode 100644
index 0000000..2007157
--- /dev/null
+++ b/tests/vm_accessor/apex/accessor_demo-file_contexts
@@ -0,0 +1,3 @@
+# TODO: Give proper label
+(/.*)?                         u:object_r:system_file:s0
+/bin/accessor_demo             u:object_r:virtualizationservice_exec:s0
diff --git a/tests/vm_accessor/apex/accessor_demo.init.rc b/tests/vm_accessor/apex/accessor_demo.init.rc
new file mode 100644
index 0000000..1ebb038
--- /dev/null
+++ b/tests/vm_accessor/apex/accessor_demo.init.rc
@@ -0,0 +1,21 @@
+# Copyright 2024 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.
+
+# Register this as lazy service, so IAccessor and its VM is only running while binder is in use.
+service accessor_demo /apex/com.android.virt.accessor_demo/bin/accessor_demo
+    disabled
+    oneshot
+    user root
+    # MUST match with VINTF and accessor/src/main.rs
+    interface aidl android.os.IAccessor/IAccessorVmService/default
diff --git a/tests/vm_accessor/apex/accessor_demo.xml b/tests/vm_accessor/apex/accessor_demo.xml
new file mode 100644
index 0000000..e9df3df
--- /dev/null
+++ b/tests/vm_accessor/apex/accessor_demo.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 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.
+-->
+
+<manifest version="1.0" type="framework">
+    <hal format="aidl">
+        <name>com.android.virt.accessor_demo.vm_service</name>
+        <version>1</version>
+        <fqname>IAccessorVmService/default</fqname>
+        <!-- MUST match with init.rc and accessor/src/main.rs -->
+        <accessor>android.os.IAccessor/IAccessorVmService/default</accessor>
+    </hal>
+</manifest>
\ No newline at end of file
diff --git a/tests/vm_accessor/apex/manifest.json b/tests/vm_accessor/apex/manifest.json
new file mode 100644
index 0000000..a09523a
--- /dev/null
+++ b/tests/vm_accessor/apex/manifest.json
@@ -0,0 +1,4 @@
+{
+  "name": "com.android.virt.accessor_demo",
+  "version": 1
+}
diff --git a/tests/vm_accessor/test/Android.bp b/tests/vm_accessor/test/Android.bp
new file mode 100644
index 0000000..71746c7
--- /dev/null
+++ b/tests/vm_accessor/test/Android.bp
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 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 {
+    default_team: "trendy_team_virtualization",
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_test {
+    name: "vm_accessor_test",
+    srcs: ["src/test.rs"],
+    defaults: [
+        "rdroidtest.defaults",
+    ],
+    test_suites: [
+        "general-tests",
+    ],
+    test_config: "AndroidTest.xml",
+    rustlibs: [
+        "com.android.virt.accessor_demo.vm_service-rust",
+        "libbinder_rs",
+        "liblog_rust",
+    ],
+    data: [":com.android.virt.accessor_demo"],
+    compile_multilib: "first",
+}
diff --git a/tests/vm_accessor/test/AndroidTest.xml b/tests/vm_accessor/test/AndroidTest.xml
new file mode 100644
index 0000000..4cbf9ec
--- /dev/null
+++ b/tests/vm_accessor/test/AndroidTest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2024 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.
+-->
+<configuration description="Accessor demo test">
+  <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+  <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+  <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+  <!-- Install APEX begins -->
+  <!-- Step 0: adb reboot, so PushFilePreparer can remount system if needed -->
+  <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer">
+    <option name="force-root" value="true"/>
+  </target_preparer>
+  <!-- Step 1: Push for the very first install. -->
+  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+    <option name="abort-on-push-failure" value="true" />
+    <!-- Disclaimer: 'remount-system' remounts all partitions (adb remount),
+         but do so after checking the verity of /system partition.
+         This works for now, but may misbehave in the future. -->
+    <option name="remount-system" value="true" />
+    <option name="push-file" key="com.android.virt.accessor_demo.apex" value="/system_ext/apex/com.android.virt.accessor_demo.apex" />
+  </target_preparer>
+  <!-- Step 2: Reboot for pushed APEX to be installed. -->
+  <target_preparer class="com.android.tradefed.targetprep.RebootTargetPreparer" />
+
+  <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+  <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+    <option name="abort-on-push-failure" value="true" />
+    <option name="push-file" key="vm_accessor_test" value="/data/local/tmp/vm_accessor_test" />
+  </target_preparer>
+
+  <!-- TODO(b/346763236): Remove this -->
+  <target_preparer class="com.android.tradefed.targetprep.DisableSELinuxTargetPreparer" />
+
+  <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+    <option name="test-device-path" value="/data/local/tmp" />
+    <option name="module-name" value="vm_accessor_test" />
+  </test>
+</configuration>
diff --git a/tests/vm_accessor/test/src/test.rs b/tests/vm_accessor/test/src/test.rs
new file mode 100644
index 0000000..d521acf
--- /dev/null
+++ b/tests/vm_accessor/test/src/test.rs
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2024 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.
+ */
+
+//! Test end-to-end IAccessor implementation with accessor_demo.
+
+use com_android_virt_accessor_demo_vm_service::aidl::com::android::virt::accessor_demo::vm_service::IAccessorVmService::IAccessorVmService;
+use binder::{Strong, ProcessState};
+use rdroidtest::rdroidtest;
+
+const VM_SERVICE: &str = "com.android.virt.accessor_demo.vm_service.IAccessorVmService/default";
+
+fn init() {
+    ProcessState::set_thread_pool_max_thread_count(5);
+    ProcessState::start_thread_pool();
+}
+
+fn wait_for_interface() -> Strong<dyn IAccessorVmService> {
+    binder::wait_for_interface(VM_SERVICE).unwrap()
+}
+
+fn get_interface() -> Strong<dyn IAccessorVmService> {
+    binder::get_interface(VM_SERVICE).unwrap()
+}
+
+fn check_interface() -> Strong<dyn IAccessorVmService> {
+    binder::check_interface(VM_SERVICE).unwrap()
+}
+
+#[rdroidtest]
+fn test_wait_for_interface() {
+    init();
+
+    let service = wait_for_interface();
+    let sum = service.add(11, 12).unwrap();
+
+    assert_eq!(sum, 23);
+}
+
+#[rdroidtest]
+fn test_wait_for_interface_twice() {
+    init();
+
+    let service1 = wait_for_interface();
+    let service2 = wait_for_interface();
+
+    assert_eq!(service1.add(11, 12).unwrap(), 23);
+    assert_eq!(service2.add(11, 12).unwrap(), 23);
+}
+
+#[rdroidtest]
+fn test_wait_and_get_interface() {
+    init();
+
+    let service1 = wait_for_interface();
+    let service2 = get_interface();
+
+    assert_eq!(service1.add(11, 12).unwrap(), 23);
+    assert_eq!(service2.add(11, 12).unwrap(), 23);
+}
+
+#[rdroidtest]
+fn test_wait_and_check_interface() {
+    init();
+
+    let service1 = wait_for_interface();
+    let service2 = check_interface();
+
+    assert_eq!(service1.add(11, 12).unwrap(), 23);
+    assert_eq!(service2.add(11, 12).unwrap(), 23);
+}
+
+rdroidtest::test_main!();
diff --git a/service_vm/test_apk/Android.bp b/tests/vm_attestation/Android.bp
similarity index 100%
rename from service_vm/test_apk/Android.bp
rename to tests/vm_attestation/Android.bp
diff --git a/service_vm/test_apk/AndroidManifest.rkpd.xml b/tests/vm_attestation/AndroidManifest.rkpd.xml
similarity index 100%
rename from service_vm/test_apk/AndroidManifest.rkpd.xml
rename to tests/vm_attestation/AndroidManifest.rkpd.xml
diff --git a/service_vm/test_apk/AndroidManifest.xml b/tests/vm_attestation/AndroidManifest.xml
similarity index 100%
rename from service_vm/test_apk/AndroidManifest.xml
rename to tests/vm_attestation/AndroidManifest.xml
diff --git a/service_vm/test_apk/AndroidTest.rkpd.xml b/tests/vm_attestation/AndroidTest.rkpd.xml
similarity index 100%
rename from service_vm/test_apk/AndroidTest.rkpd.xml
rename to tests/vm_attestation/AndroidTest.rkpd.xml
diff --git a/service_vm/test_apk/AndroidTest.xml b/tests/vm_attestation/AndroidTest.xml
similarity index 100%
rename from service_vm/test_apk/AndroidTest.xml
rename to tests/vm_attestation/AndroidTest.xml
diff --git a/service_vm/test_apk/aidl/Android.bp b/tests/vm_attestation/aidl/Android.bp
similarity index 100%
rename from service_vm/test_apk/aidl/Android.bp
rename to tests/vm_attestation/aidl/Android.bp
diff --git a/service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl b/tests/vm_attestation/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
similarity index 100%
rename from service_vm/test_apk/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
rename to tests/vm_attestation/aidl/com/android/virt/vm_attestation/testservice/IAttestationService.aidl
diff --git a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java b/tests/vm_attestation/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
similarity index 99%
rename from service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
rename to tests/vm_attestation/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
index f456cb4..b41323f 100644
--- a/service_vm/test_apk/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
+++ b/tests/vm_attestation/src/java/com/android/virt/rkpd/vm_attestation/testapp/RkpdVmAttestationTest.java
@@ -20,17 +20,17 @@
 
 import static com.google.common.truth.TruthJUnit.assume;
 
-import android.net.ConnectivityManager;
-import android.net.NetworkCapabilities;
-import android.net.Network;
 import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineManager;
 
 import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
 import com.android.virt.vm_attestation.testservice.IAttestationService.SigningResult;
 import com.android.virt.vm_attestation.util.X509Utils;
-import android.system.virtualmachine.VirtualMachineManager;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java b/tests/vm_attestation/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
similarity index 99%
rename from service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
rename to tests/vm_attestation/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
index ff760b4..43a4c66 100644
--- a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
+++ b/tests/vm_attestation/src/java/com/android/virt/vm_attestation/testapp/VmAttestationTests.java
@@ -16,27 +16,29 @@
 
 package com.android.virt.vm_attestation.testapp;
 
+import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
+
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineManager;
 
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+import com.android.virt.vm_attestation.testservice.IAttestationService;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
-import com.android.virt.vm_attestation.testservice.IAttestationService;
-
 @RunWith(Parameterized.class)
 public class VmAttestationTests extends MicrodroidDeviceTestBase {
     private static final String TAG = "VmAttestationTest";
diff --git a/service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java b/tests/vm_attestation/src/java/com/android/virt/vm_attestation/util/X509Utils.java
similarity index 100%
rename from service_vm/test_apk/src/java/com/android/virt/vm_attestation/util/X509Utils.java
rename to tests/vm_attestation/src/java/com/android/virt/vm_attestation/util/X509Utils.java
diff --git a/service_vm/test_apk/src/native/main.rs b/tests/vm_attestation/src/native/main.rs
similarity index 100%
rename from service_vm/test_apk/src/native/main.rs
rename to tests/vm_attestation/src/native/main.rs
diff --git a/tests/vmbase_example/Android.bp b/tests/vmbase_example/Android.bp
new file mode 100644
index 0000000..4c1aa30
--- /dev/null
+++ b/tests/vmbase_example/Android.bp
@@ -0,0 +1,31 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_test {
+    name: "vmbase_example.integration_test",
+    crate_name: "vmbase_example_test",
+    srcs: ["src/main.rs"],
+    prefer_rlib: true,
+    edition: "2021",
+    rustlibs: [
+        "android.system.virtualizationservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "liblibc",
+        "liblog_rust",
+        "libnix",
+        "libvmclient",
+    ],
+    data: [
+        ":vmbase_example_bios_bin",
+        ":vmbase_example_kernel_bin",
+    ],
+    test_suites: ["general-tests"],
+    enabled: false,
+    target: {
+        android_arm64: {
+            enabled: true,
+        },
+    },
+}
diff --git a/vmbase/example/tests/test.rs b/tests/vmbase_example/src/main.rs
similarity index 86%
rename from vmbase/example/tests/test.rs
rename to tests/vmbase_example/src/main.rs
index 8f9fafc..e0563b7 100644
--- a/vmbase/example/tests/test.rs
+++ b/tests/vmbase_example/src/main.rs
@@ -31,13 +31,27 @@
 };
 use vmclient::{DeathReason, VmInstance};
 
-const VMBASE_EXAMPLE_PATH: &str = "vmbase_example.bin";
+const VMBASE_EXAMPLE_KERNEL_PATH: &str = "vmbase_example_kernel.bin";
+const VMBASE_EXAMPLE_BIOS_PATH: &str = "vmbase_example_bios.bin";
 const TEST_DISK_IMAGE_PATH: &str = "test_disk.img";
 const EMPTY_DISK_IMAGE_PATH: &str = "empty_disk.img";
 
-/// Runs the vmbase_example VM as an unprotected VM via VirtualizationService.
+/// Runs the vmbase_example VM as an unprotected VM kernel via VirtualizationService.
 #[test]
-fn test_run_example_vm() -> Result<(), Error> {
+fn test_run_example_kernel_vm() -> Result<(), Error> {
+    run_test(Some(open_payload(VMBASE_EXAMPLE_KERNEL_PATH)?), None)
+}
+
+/// Runs the vmbase_example VM as an unprotected VM BIOS via VirtualizationService.
+#[test]
+fn test_run_example_bios_vm() -> Result<(), Error> {
+    run_test(None, Some(open_payload(VMBASE_EXAMPLE_BIOS_PATH)?))
+}
+
+fn run_test(
+    kernel: Option<ParcelFileDescriptor>,
+    bootloader: Option<ParcelFileDescriptor>,
+) -> Result<(), Error> {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("vmbase")
@@ -56,12 +70,6 @@
         vmclient::VirtualizationService::new().context("Failed to spawn VirtualizationService")?;
     let service = virtmgr.connect().context("Failed to connect to VirtualizationService")?;
 
-    // Start example VM.
-    let bootloader = ParcelFileDescriptor::new(
-        File::open(VMBASE_EXAMPLE_PATH)
-            .with_context(|| format!("Failed to open VM image {}", VMBASE_EXAMPLE_PATH))?,
-    );
-
     // Make file for test disk image.
     let mut test_image = File::options()
         .create(true)
@@ -91,10 +99,10 @@
 
     let config = VirtualMachineConfig::RawConfig(VirtualMachineRawConfig {
         name: String::from("VmBaseTest"),
-        kernel: None,
+        kernel,
         initrd: None,
         params: None,
-        bootloader: Some(bootloader),
+        bootloader,
         disks: vec![disk_image, empty_disk_image],
         protectedVm: false,
         memoryMib: 300,
@@ -142,6 +150,11 @@
     Ok((reader_fd.into(), writer_fd.into()))
 }
 
+fn open_payload(path: &str) -> Result<ParcelFileDescriptor, Error> {
+    let file = File::open(path).with_context(|| format!("Failed to open VM image {path}"))?;
+    Ok(ParcelFileDescriptor::new(file))
+}
+
 struct VmLogProcessor {
     reader: Option<File>,
     expected: VecDeque<String>,
diff --git a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
index 109486c..9f606e5 100644
--- a/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
+++ b/tests/vmshareapp/src/java/com/android/microdroid/test/sharevm/VmShareServiceImpl.java
@@ -28,8 +28,8 @@
 import android.util.Log;
 
 import com.android.microdroid.test.vmshare.IVmShareTestService;
-import com.android.microdroid.testservice.ITestService;
 import com.android.microdroid.testservice.IAppCallback;
+import com.android.microdroid.testservice.ITestService;
 
 import java.util.UUID;
 import java.util.concurrent.CountDownLatch;
diff --git a/vmbase/example/Android.bp b/vmbase/example/Android.bp
deleted file mode 100644
index fe9de44..0000000
--- a/vmbase/example/Android.bp
+++ /dev/null
@@ -1,74 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_ffi_static {
-    name: "libvmbase_example",
-    defaults: ["vmbase_ffi_defaults"],
-    crate_name: "vmbase_example",
-    srcs: ["src/main.rs"],
-    rustlibs: [
-        "libaarch64_paging",
-        "libcstr",
-        "libdiced_open_dice_nostd",
-        "libfdtpci",
-        "liblibfdt",
-        "liblog_rust_nostd",
-        "libvirtio_drivers",
-        "libvmbase",
-    ],
-}
-
-cc_binary {
-    name: "vmbase_example",
-    defaults: ["vmbase_elf_defaults"],
-    srcs: [
-        "idmap.S",
-    ],
-    static_libs: [
-        "libvmbase_example",
-    ],
-    linker_scripts: [
-        "image.ld",
-        ":vmbase_sections",
-    ],
-}
-
-raw_binary {
-    name: "vmbase_example_bin",
-    stem: "vmbase_example.bin",
-    src: ":vmbase_example",
-    enabled: false,
-    target: {
-        android_arm64: {
-            enabled: true,
-        },
-    },
-}
-
-rust_test {
-    name: "vmbase_example.integration_test",
-    crate_name: "vmbase_example_test",
-    srcs: ["tests/test.rs"],
-    prefer_rlib: true,
-    edition: "2021",
-    rustlibs: [
-        "android.system.virtualizationservice-rust",
-        "libandroid_logger",
-        "libanyhow",
-        "liblibc",
-        "liblog_rust",
-        "libnix",
-        "libvmclient",
-    ],
-    data: [
-        ":vmbase_example_bin",
-    ],
-    test_suites: ["general-tests"],
-    enabled: false,
-    target: {
-        android_arm64: {
-            enabled: true,
-        },
-    },
-}
diff --git a/vmbase/example/image.ld b/vmbase/example/image.ld
deleted file mode 100644
index 368acbb..0000000
--- a/vmbase/example/image.ld
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright 2022 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
- *
- *     https://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.
- */
-
-MEMORY
-{
-	dtb_region	: ORIGIN = 0x80000000, LENGTH = 2M
-	image		: ORIGIN = 0x80200000, LENGTH = 2M
-	writable_data	: ORIGIN = 0x80400000, LENGTH = 2M
-}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
deleted file mode 100644
index 32724f8..0000000
--- a/vmlauncher_app/AndroidManifest.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.virtualization.vmlauncher" >
-
-    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
-    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
-    <uses-permission android:name="android.permission.INTERNET" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
-    <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
-    <application
-        android:label="VmLauncherApp">
-        <activity android:name=".MainActivity"
-                  android:screenOrientation="landscape"
-                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
-                  android:theme="@style/MyTheme"
-                  android:exported="true">
-            <intent-filter>
-                <action android:name="android.virtualization.VM_LAUNCHER" />
-                <category android:name="android.intent.category.DEFAULT" />
-	    </intent-filter>
-        </activity>
-    </application>
-
-</manifest>
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
deleted file mode 100644
index 8bb0fd4..0000000
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ /dev/null
@@ -1,773 +0,0 @@
-/*
- * Copyright (C) 2024 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.virtualization.vmlauncher;
-
-import static android.content.pm.PackageManager.PERMISSION_GRANTED;
-import static android.os.ParcelFileDescriptor.AutoCloseInputStream;
-import static android.os.ParcelFileDescriptor.AutoCloseOutputStream;
-import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
-
-import android.Manifest.permission;
-import android.app.Activity;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.crosvm.ICrosvmAndroidDisplayService;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.os.Bundle;
-import android.os.ParcelFileDescriptor;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-import android.system.virtualizationservice_internal.IVirtualizationServiceInternal;
-import android.system.virtualmachine.VirtualMachine;
-import android.system.virtualmachine.VirtualMachineCallback;
-import android.system.virtualmachine.VirtualMachineConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
-import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
-import android.system.virtualmachine.VirtualMachineException;
-import android.system.virtualmachine.VirtualMachineManager;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.view.Display;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.SurfaceHolder;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.WindowInsetsController;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-
-import libcore.io.IoBridge;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedOutputStream;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-public class MainActivity extends Activity {
-    private static final String TAG = "VmLauncherApp";
-    private static final String VM_NAME = "my_custom_vm";
-    private static final boolean DEBUG = true;
-    private ExecutorService mExecutorService;
-    private VirtualMachine mVirtualMachine;
-    private CursorHandler mCursorHandler;
-    private ClipboardManager mClipboardManager;
-    private static final int RECORD_AUDIO_PERMISSION_REQUEST_CODE = 101;
-
-    private VirtualMachineConfig createVirtualMachineConfig(String jsonPath) {
-        VirtualMachineConfig.Builder configBuilder =
-                new VirtualMachineConfig.Builder(getApplication());
-        configBuilder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST);
-
-        configBuilder.setProtectedVm(false);
-        if (DEBUG) {
-            configBuilder.setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_FULL);
-            configBuilder.setVmOutputCaptured(true);
-            configBuilder.setConnectVmConsole(true);
-        }
-        VirtualMachineCustomImageConfig.Builder customImageConfigBuilder =
-                new VirtualMachineCustomImageConfig.Builder();
-        try {
-            String rawJson = new String(Files.readAllBytes(Path.of(jsonPath)));
-            JSONObject json = new JSONObject(rawJson);
-            customImageConfigBuilder.setName(json.optString("name", ""));
-            if (json.has("kernel")) {
-                customImageConfigBuilder.setKernelPath(json.getString("kernel"));
-            }
-            if (json.has("initrd")) {
-                customImageConfigBuilder.setInitrdPath(json.getString("initrd"));
-            }
-            if (json.has("params")) {
-                Arrays.stream(json.getString("params").split(" "))
-                        .forEach(customImageConfigBuilder::addParam);
-            }
-            if (json.has("bootloader")) {
-                customImageConfigBuilder.setBootloaderPath(json.getString("bootloader"));
-            }
-            if (json.has("disks")) {
-                JSONArray diskArr = json.getJSONArray("disks");
-                for (int i = 0; i < diskArr.length(); i++) {
-                    JSONObject item = diskArr.getJSONObject(i);
-                    if (item.has("image")) {
-                        if (item.optBoolean("writable", false)) {
-                            customImageConfigBuilder.addDisk(
-                                    VirtualMachineCustomImageConfig.Disk.RWDisk(
-                                            item.getString("image")));
-                        } else {
-                            customImageConfigBuilder.addDisk(
-                                    VirtualMachineCustomImageConfig.Disk.RODisk(
-                                            item.getString("image")));
-                        }
-                    } else if (item.has("partitions")) {
-                        boolean diskWritable = item.optBoolean("writable", false);
-                        VirtualMachineCustomImageConfig.Disk disk =
-                                diskWritable
-                                        ? VirtualMachineCustomImageConfig.Disk.RWDisk(null)
-                                        : VirtualMachineCustomImageConfig.Disk.RODisk(null);
-                        JSONArray partitions = item.getJSONArray("partitions");
-                        for (int j = 0; j < partitions.length(); j++) {
-                            JSONObject partition = partitions.getJSONObject(j);
-                            String label = partition.getString("label");
-                            String path = partition.getString("path");
-                            boolean partitionWritable =
-                                    diskWritable && partition.optBoolean("writable", false);
-                            String guid = partition.optString("guid");
-                            VirtualMachineCustomImageConfig.Partition p =
-                                    new VirtualMachineCustomImageConfig.Partition(
-                                            label, path, partitionWritable, guid);
-                            disk.addPartition(p);
-                        }
-                        customImageConfigBuilder.addDisk(disk);
-                    }
-                }
-            }
-            if (json.has("console_input_device")) {
-                configBuilder.setConsoleInputDevice(json.getString("console_input_device"));
-            }
-            if (json.has("gpu")) {
-                JSONObject gpuJson = json.getJSONObject("gpu");
-
-                GpuConfig.Builder gpuConfigBuilder = new GpuConfig.Builder();
-
-                if (gpuJson.has("backend")) {
-                    gpuConfigBuilder.setBackend(gpuJson.getString("backend"));
-                }
-                if (gpuJson.has("context_types")) {
-                    ArrayList<String> contextTypes = new ArrayList<String>();
-                    JSONArray contextTypesJson = gpuJson.getJSONArray("context_types");
-                    for (int i = 0; i < contextTypesJson.length(); i++) {
-                        contextTypes.add(contextTypesJson.getString(i));
-                    }
-                    gpuConfigBuilder.setContextTypes(contextTypes.toArray(new String[0]));
-                }
-                if (gpuJson.has("pci_address")) {
-                    gpuConfigBuilder.setPciAddress(gpuJson.getString("pci_address"));
-                }
-                if (gpuJson.has("renderer_features")) {
-                    gpuConfigBuilder.setRendererFeatures(gpuJson.getString("renderer_features"));
-                }
-                if (gpuJson.has("renderer_use_egl")) {
-                    gpuConfigBuilder.setRendererUseEgl(gpuJson.getBoolean("renderer_use_egl"));
-                }
-                if (gpuJson.has("renderer_use_gles")) {
-                    gpuConfigBuilder.setRendererUseGles(gpuJson.getBoolean("renderer_use_gles"));
-                }
-                if (gpuJson.has("renderer_use_glx")) {
-                    gpuConfigBuilder.setRendererUseGlx(gpuJson.getBoolean("renderer_use_glx"));
-                }
-                if (gpuJson.has("renderer_use_surfaceless")) {
-                    gpuConfigBuilder.setRendererUseSurfaceless(
-                            gpuJson.getBoolean("renderer_use_surfaceless"));
-                }
-                if (gpuJson.has("renderer_use_vulkan")) {
-                    gpuConfigBuilder.setRendererUseVulkan(
-                            gpuJson.getBoolean("renderer_use_vulkan"));
-                }
-                customImageConfigBuilder.setGpuConfig(gpuConfigBuilder.build());
-            }
-
-            configBuilder.setMemoryBytes(8L * 1024 * 1024 * 1024 /* 8 GB */);
-            WindowMetrics windowMetrics = getWindowManager().getCurrentWindowMetrics();
-            Rect windowSize = windowMetrics.getBounds();
-            int dpi = (int) (DisplayMetrics.DENSITY_DEFAULT * windowMetrics.getDensity());
-            DisplayConfig.Builder displayConfigBuilder = new DisplayConfig.Builder();
-            displayConfigBuilder.setWidth(windowSize.right);
-            displayConfigBuilder.setHeight(windowSize.bottom);
-            displayConfigBuilder.setHorizontalDpi(dpi);
-            displayConfigBuilder.setVerticalDpi(dpi);
-
-            Display display = getDisplay();
-            if (display != null) {
-                displayConfigBuilder.setRefreshRate((int) display.getRefreshRate());
-            }
-
-            customImageConfigBuilder.setDisplayConfig(displayConfigBuilder.build());
-            customImageConfigBuilder.useTouch(true);
-            customImageConfigBuilder.useKeyboard(true);
-            customImageConfigBuilder.useMouse(true);
-            customImageConfigBuilder.useSwitches(true);
-            customImageConfigBuilder.useTrackpad(true);
-            customImageConfigBuilder.useNetwork(true);
-
-            AudioConfig.Builder audioConfigBuilder = new AudioConfig.Builder();
-            audioConfigBuilder.setUseMicrophone(true);
-            audioConfigBuilder.setUseSpeaker(true);
-            customImageConfigBuilder.setAudioConfig(audioConfigBuilder.build());
-            configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
-
-        } catch (JSONException | IOException e) {
-            throw new IllegalStateException("malformed input", e);
-        }
-        return configBuilder.build();
-    }
-
-    private static boolean isVolumeKey(int keyCode) {
-        return keyCode == KeyEvent.KEYCODE_VOLUME_UP
-                || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
-                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE;
-    }
-
-    @Override
-    public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (mVirtualMachine == null) {
-            return false;
-        }
-        return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
-    }
-
-    @Override
-    public boolean onKeyUp(int keyCode, KeyEvent event) {
-        if (mVirtualMachine == null) {
-            return false;
-        }
-        return !isVolumeKey(keyCode) && mVirtualMachine.sendKeyEvent(event);
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        checkAndRequestRecordAudioPermission();
-        mExecutorService = Executors.newCachedThreadPool();
-        try {
-            // To ensure that the previous display service is removed.
-            IVirtualizationServiceInternal.Stub.asInterface(
-                            ServiceManager.waitForService("android.system.virtualizationservice"))
-                    .clearDisplayService();
-        } catch (RemoteException e) {
-            Log.d(TAG, "failed to clearDisplayService");
-        }
-        getWindow().setDecorFitsSystemWindows(false);
-        setContentView(R.layout.activity_main);
-        VirtualMachineCallback callback =
-                new VirtualMachineCallback() {
-                    // store reference to ExecutorService to avoid race condition
-                    private final ExecutorService mService = mExecutorService;
-
-                    @Override
-                    public void onPayloadStarted(VirtualMachine vm) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
-
-                    @Override
-                    public void onPayloadReady(VirtualMachine vm) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
-
-                    @Override
-                    public void onPayloadFinished(VirtualMachine vm, int exitCode) {
-                        // This event is only from Microdroid-based VM. Custom VM shouldn't emit
-                        // this.
-                    }
-
-                    @Override
-                    public void onError(VirtualMachine vm, int errorCode, String message) {
-                        Log.e(TAG, "Error from VM. code: " + errorCode + " (" + message + ")");
-                        setResult(RESULT_CANCELED);
-                        finish();
-                    }
-
-                    @Override
-                    public void onStopped(VirtualMachine vm, int reason) {
-                        Log.d(TAG, "VM stopped. Reason: " + reason);
-                        setResult(RESULT_OK);
-                        finish();
-                    }
-                };
-
-        try {
-            VirtualMachineConfig config =
-                    createVirtualMachineConfig("/data/local/tmp/vm_config.json");
-            VirtualMachineManager vmm =
-                    getApplication().getSystemService(VirtualMachineManager.class);
-            if (vmm == null) {
-                Log.e(TAG, "vmm is null");
-                return;
-            }
-            mVirtualMachine = vmm.getOrCreate(VM_NAME, config);
-            try {
-                mVirtualMachine.setConfig(config);
-            } catch (VirtualMachineException e) {
-                vmm.delete(VM_NAME);
-                mVirtualMachine = vmm.create(VM_NAME, config);
-                Log.e(TAG, "error for setting VM config", e);
-            }
-
-            Log.d(TAG, "vm start");
-            mVirtualMachine.run();
-            mVirtualMachine.setCallback(Executors.newSingleThreadExecutor(), callback);
-            if (DEBUG) {
-                InputStream console = mVirtualMachine.getConsoleOutput();
-                InputStream log = mVirtualMachine.getLogOutput();
-                OutputStream consoleLogFile =
-                        new LineBufferedOutputStream(
-                                getApplicationContext().openFileOutput("console.log", 0));
-                mExecutorService.execute(new CopyStreamTask("console", console, consoleLogFile));
-                mExecutorService.execute(new Reader("log", log));
-            }
-        } catch (VirtualMachineException | IOException e) {
-            throw new RuntimeException(e);
-        }
-
-        SurfaceView surfaceView = findViewById(R.id.surface_view);
-        SurfaceView cursorSurfaceView = findViewById(R.id.cursor_surface_view);
-        cursorSurfaceView.setZOrderMediaOverlay(true);
-        View backgroundTouchView = findViewById(R.id.background_touch_view);
-        backgroundTouchView.setOnTouchListener(
-                (v, event) -> {
-                    if (mVirtualMachine == null) {
-                        return false;
-                    }
-                    return mVirtualMachine.sendMultiTouchEvent(event);
-                });
-        surfaceView.requestUnbufferedDispatch(InputDevice.SOURCE_ANY);
-        surfaceView.setOnCapturedPointerListener(
-                (v, event) -> {
-                    if (mVirtualMachine == null) {
-                        return false;
-                    }
-                    int eventSource = event.getSource();
-                    if ((eventSource & InputDevice.SOURCE_CLASS_POSITION) != 0) {
-                        return mVirtualMachine.sendTrackpadEvent(event);
-                    }
-                    return mVirtualMachine.sendMouseEvent(event);
-                });
-        surfaceView
-                .getHolder()
-                .addCallback(
-                        // TODO(b/331708504): it should be handled in AVF framework.
-                        new SurfaceHolder.Callback() {
-                            @Override
-                            public void surfaceCreated(SurfaceHolder holder) {
-                                Log.d(
-                                        TAG,
-                                        "surface size: "
-                                                + holder.getSurfaceFrame().flattenToString());
-                                Log.d(
-                                        TAG,
-                                        "ICrosvmAndroidDisplayService.setSurface("
-                                                + holder.getSurface()
-                                                + ")");
-                                runWithDisplayService(
-                                        (service) ->
-                                                service.setSurface(
-                                                        holder.getSurface(),
-                                                        false /* forCursor */));
-                            }
-
-                            @Override
-                            public void surfaceChanged(
-                                    SurfaceHolder holder, int format, int width, int height) {
-                                Log.d(
-                                        TAG,
-                                        "surface changed, width: " + width + ", height: " + height);
-                            }
-
-                            @Override
-                            public void surfaceDestroyed(SurfaceHolder holder) {
-                                Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
-                                runWithDisplayService(
-                                        (service) -> service.removeSurface(false /* forCursor */));
-                            }
-                        });
-        cursorSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
-        cursorSurfaceView
-                .getHolder()
-                .addCallback(
-                        new SurfaceHolder.Callback() {
-                            @Override
-                            public void surfaceCreated(SurfaceHolder holder) {
-                                try {
-                                    ParcelFileDescriptor[] pfds =
-                                            ParcelFileDescriptor.createSocketPair();
-                                    if (mCursorHandler != null) {
-                                        mCursorHandler.interrupt();
-                                    }
-                                    mCursorHandler = new CursorHandler(cursorSurfaceView, pfds[0]);
-                                    mCursorHandler.start();
-                                    runWithDisplayService(
-                                            (service) -> service.setCursorStream(pfds[1]));
-                                } catch (Exception e) {
-                                    Log.d(TAG, "failed to run cursor stream handler", e);
-                                }
-                                Log.d(
-                                        TAG,
-                                        "ICrosvmAndroidDisplayService.setSurface("
-                                                + holder.getSurface()
-                                                + ")");
-                                runWithDisplayService(
-                                        (service) ->
-                                                service.setSurface(
-                                                        holder.getSurface(), true /* forCursor */));
-                            }
-
-                            @Override
-                            public void surfaceChanged(
-                                    SurfaceHolder holder, int format, int width, int height) {
-                                Log.d(
-                                        TAG,
-                                        "cursor surface changed, width: "
-                                                + width
-                                                + ", height: "
-                                                + height);
-                            }
-
-                            @Override
-                            public void surfaceDestroyed(SurfaceHolder holder) {
-                                Log.d(TAG, "ICrosvmAndroidDisplayService.removeSurface()");
-                                runWithDisplayService(
-                                        (service) -> service.removeSurface(true /* forCursor */));
-                            }
-                        });
-        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
-        // Fullscreen:
-        WindowInsetsController windowInsetsController = surfaceView.getWindowInsetsController();
-        windowInsetsController.setSystemBarsBehavior(
-                WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
-        windowInsetsController.hide(WindowInsets.Type.systemBars());
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-        if (mVirtualMachine != null) {
-            try {
-                mVirtualMachine.sendLidEvent(/* close */ true);
-                mVirtualMachine.suspend();
-            } catch (VirtualMachineException e) {
-                Log.e(TAG, "Failed to suspend VM" + e);
-            }
-        }
-    }
-
-    @Override
-    protected void onRestart() {
-        super.onRestart();
-        if (mVirtualMachine != null) {
-            try {
-                mVirtualMachine.resume();
-                mVirtualMachine.sendLidEvent(/* close */ false);
-            } catch (VirtualMachineException e) {
-                Log.e(TAG, "Failed to resume VM" + e);
-            }
-        }
-    }
-
-    @Override
-    protected void onDestroy() {
-        super.onDestroy();
-        if (mExecutorService != null) {
-            mExecutorService.shutdownNow();
-        }
-        Log.d(TAG, "destroyed");
-    }
-
-    private static final int CLIPBOARD_SHARING_SERVER_PORT = 3580;
-    private static final byte READ_CLIPBOARD_FROM_VM = 0;
-    private static final byte WRITE_CLIPBOARD_TYPE_EMPTY = 1;
-    private static final byte WRITE_CLIPBOARD_TYPE_TEXT_PLAIN = 2;
-
-    private ClipboardManager getClipboardManager() {
-        if (mClipboardManager == null) {
-            mClipboardManager = getSystemService(ClipboardManager.class);
-        }
-        return mClipboardManager;
-    }
-
-    // Construct header for the clipboard data.
-    // Byte 0: Data type
-    // Byte 1-3: Padding alignment & Reserved for other use cases in the future
-    // Byte 4-7: Data size of the payload
-    private byte[] constructClipboardHeader(byte type, int dataSize) {
-        ByteBuffer header = ByteBuffer.allocate(8);
-        header.clear();
-        header.order(ByteOrder.LITTLE_ENDIAN);
-        header.put(0, type);
-        header.putInt(4, dataSize);
-        return header.array();
-    }
-
-    private ParcelFileDescriptor connectClipboardSharingServer() {
-        ParcelFileDescriptor pfd;
-        try {
-            // TODO(349702313): Consider when clipboard sharing server is started to run in VM.
-            pfd = mVirtualMachine.connectVsock(CLIPBOARD_SHARING_SERVER_PORT);
-        } catch (VirtualMachineException e) {
-            Log.d(TAG, "cannot connect to the clipboard sharing server", e);
-            return null;
-        }
-        return pfd;
-    }
-
-    private boolean writeClipboardToVm() {
-        ClipboardManager clipboardManager = getClipboardManager();
-        if (!clipboardManager.hasPrimaryClip()) {
-            Log.d(TAG, "host device has no clipboard data");
-            return true;
-        }
-        ClipData clip = clipboardManager.getPrimaryClip();
-        String text = clip.getItemAt(0).getText().toString();
-        byte[] header =
-                constructClipboardHeader(
-                        WRITE_CLIPBOARD_TYPE_TEXT_PLAIN, text.getBytes().length + 1);
-        ParcelFileDescriptor pfd = connectClipboardSharingServer();
-        if (pfd == null) {
-            Log.d(TAG, "file descriptor of ClipboardSharingServer is null");
-            return false;
-        }
-        try (OutputStream stream = new AutoCloseOutputStream(pfd)) {
-            stream.write(header);
-            stream.write(text.getBytes());
-            stream.write('\0');
-            stream.flush();
-            Log.d(TAG, "successfully wrote clipboard data to the VM");
-            return true;
-        } catch (IOException e) {
-            Log.e(TAG, "failed to write clipboard data to the VM", e);
-            return false;
-        }
-    }
-
-    private boolean readClipboardFromVm() {
-        byte[] request = constructClipboardHeader(READ_CLIPBOARD_FROM_VM, 0);
-        ParcelFileDescriptor pfd = connectClipboardSharingServer();
-        if (pfd == null) {
-            Log.d(TAG, "file descriptor of ClipboardSharingServer is null");
-            return false;
-        }
-        try (OutputStream output = new AutoCloseOutputStream(pfd.dup())) {
-            output.write(request);
-            output.flush();
-            Log.d(TAG, "successfully send request to the VM for reading clipboard");
-        } catch (IOException e) {
-            Log.e(TAG, "failed to send request to the VM for read clipboard", e);
-            try {
-                pfd.close();
-            } catch (IOException err) {
-                Log.e(TAG, "failed to close file descriptor", err);
-            }
-            return false;
-        }
-
-        try (InputStream input = new AutoCloseInputStream(pfd)) {
-            ByteBuffer header = ByteBuffer.wrap(input.readNBytes(8));
-            header.order(ByteOrder.LITTLE_ENDIAN);
-            switch (header.get(0)) {
-                case WRITE_CLIPBOARD_TYPE_EMPTY:
-                    Log.d(TAG, "clipboard data in VM is empty");
-                    return true;
-                case WRITE_CLIPBOARD_TYPE_TEXT_PLAIN:
-                    int dataSize = header.getInt(4);
-                    String text_data =
-                            new String(input.readNBytes(dataSize), StandardCharsets.UTF_8);
-                    getClipboardManager().setPrimaryClip(ClipData.newPlainText(null, text_data));
-                    Log.d(TAG, "successfully received clipboard data from VM");
-                    return true;
-                default:
-                    Log.e(TAG, "unknown clipboard response type");
-                    return false;
-            }
-        } catch (IOException e) {
-            Log.e(TAG, "failed to receive clipboard content from the VM", e);
-            return false;
-        }
-    }
-
-    @Override
-    public void onWindowFocusChanged(boolean hasFocus) {
-        super.onWindowFocusChanged(hasFocus);
-        if (hasFocus) {
-            SurfaceView surfaceView = findViewById(R.id.surface_view);
-            Log.d(TAG, "requestPointerCapture()");
-            surfaceView.requestPointerCapture();
-        }
-        if (mVirtualMachine != null) {
-            if (hasFocus) {
-                Log.d(TAG, "writing clipboard of host device into VM");
-                writeClipboardToVm();
-            } else {
-                Log.d(TAG, "reading clipboard of VM");
-                readClipboardFromVm();
-            }
-        }
-    }
-
-    @FunctionalInterface
-    public interface RemoteExceptionCheckedFunction<T> {
-        void apply(T t) throws RemoteException;
-    }
-
-    private void runWithDisplayService(
-            RemoteExceptionCheckedFunction<ICrosvmAndroidDisplayService> func) {
-        IVirtualizationServiceInternal vs =
-                IVirtualizationServiceInternal.Stub.asInterface(
-                        ServiceManager.waitForService("android.system.virtualizationservice"));
-        try {
-            Log.d(TAG, "wait for the display service");
-            ICrosvmAndroidDisplayService service =
-                    ICrosvmAndroidDisplayService.Stub.asInterface(vs.waitDisplayService());
-            assert service != null;
-            func.apply(service);
-            Log.d(TAG, "display service runs successfully");
-        } catch (Exception e) {
-            Log.d(TAG, "error on running display service", e);
-        }
-    }
-
-    static class CursorHandler extends Thread {
-        private final SurfaceView mSurfaceView;
-        private final ParcelFileDescriptor mStream;
-
-        CursorHandler(SurfaceView s, ParcelFileDescriptor stream) {
-            mSurfaceView = s;
-            mStream = stream;
-        }
-
-        @Override
-        public void run() {
-            Log.d(TAG, "running CursorHandler");
-            try {
-                ByteBuffer byteBuffer = ByteBuffer.allocate(8 /* (x: u32, y: u32) */);
-                byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
-                while (true) {
-                    if (Thread.interrupted()) {
-                        Log.d(TAG, "interrupted: exiting CursorHandler");
-                        return;
-                    }
-                    byteBuffer.clear();
-                    int bytes =
-                            IoBridge.read(
-                                    mStream.getFileDescriptor(),
-                                    byteBuffer.array(),
-                                    0,
-                                    byteBuffer.array().length);
-                    if (bytes == -1) {
-                        Log.e(TAG, "cannot read from cursor stream, stop the handler");
-                        return;
-                    }
-                    float x = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
-                    float y = (float) (byteBuffer.getInt() & 0xFFFFFFFF);
-                    mSurfaceView.post(
-                            () -> {
-                                mSurfaceView.setTranslationX(x);
-                                mSurfaceView.setTranslationY(y);
-                            });
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "failed to run CursorHandler", e);
-            }
-        }
-    }
-
-    private void checkAndRequestRecordAudioPermission() {
-        if (getApplicationContext().checkSelfPermission(permission.RECORD_AUDIO)
-                != PERMISSION_GRANTED) {
-            requestPermissions(
-                    new String[] {permission.RECORD_AUDIO}, RECORD_AUDIO_PERMISSION_REQUEST_CODE);
-        }
-    }
-
-    /** Reads data from an input stream and posts it to the output data */
-    static class Reader implements Runnable {
-        private final String mName;
-        private final InputStream mStream;
-
-        Reader(String name, InputStream stream) {
-            mName = name;
-            mStream = stream;
-        }
-
-        @Override
-        public void run() {
-            try {
-                BufferedReader reader = new BufferedReader(new InputStreamReader(mStream));
-                String line;
-                while ((line = reader.readLine()) != null && !Thread.interrupted()) {
-                    Log.d(TAG, mName + ": " + line);
-                }
-            } catch (IOException e) {
-                Log.e(TAG, "Exception while posting " + mName + " output: " + e.getMessage());
-            }
-        }
-    }
-
-    private static class CopyStreamTask implements Runnable {
-        private final String mName;
-        private final InputStream mIn;
-        private final OutputStream mOut;
-
-        CopyStreamTask(String name, InputStream in, OutputStream out) {
-            mName = name;
-            mIn = in;
-            mOut = out;
-        }
-
-        @Override
-        public void run() {
-            try {
-                byte[] buffer = new byte[2048];
-                while (!Thread.interrupted()) {
-                    int len = mIn.read(buffer);
-                    if (len < 0) {
-                        break;
-                    }
-                    mOut.write(buffer, 0, len);
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "Exception while posting " + mName, e);
-            }
-        }
-    }
-
-    private static class LineBufferedOutputStream extends BufferedOutputStream {
-        LineBufferedOutputStream(OutputStream out) {
-            super(out);
-        }
-
-        @Override
-        public void write(byte[] buf, int off, int len) throws IOException {
-            super.write(buf, off, len);
-            for (int i = 0; i < len; ++i) {
-                if (buf[off + i] == '\n') {
-                    flush();
-                    break;
-                }
-            }
-        }
-    }
-}