Merge "virtmgr: migrate from vm_control to crosvm_control" into main
diff --git a/android/TerminalApp/Android.bp b/android/TerminalApp/Android.bp
index e5e8b0a..932ca76 100644
--- a/android/TerminalApp/Android.bp
+++ b/android/TerminalApp/Android.bp
@@ -26,15 +26,3 @@
         "com.android.virt",
     ],
 }
-
-filegroup {
-    name: "linux_vm_setup.rc",
-    srcs: ["linux_vm_setup.rc"],
-}
-
-sh_binary {
-    name: "linux_vm_setup",
-    src: "linux_vm_setup.sh",
-    init_rc: [":linux_vm_setup.rc"],
-    host_supported: false,
-}
diff --git a/android/TerminalApp/AndroidManifest.xml b/android/TerminalApp/AndroidManifest.xml
index 105e454..28b5436 100644
--- a/android/TerminalApp/AndroidManifest.xml
+++ b/android/TerminalApp/AndroidManifest.xml
@@ -9,6 +9,7 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
 
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
 
diff --git a/android/TerminalApp/generate_assets.sh b/android/TerminalApp/generate_assets.sh
deleted file mode 100755
index 4001bfd..0000000
--- a/android/TerminalApp/generate_assets.sh
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/bin/bash
-set -e
-
-if [ "$#" -ne 1 ]; then
-    echo "$0 <image.raw path>"
-    echo "image.raw can be built with packages/modules/Virtualization/build/debian/build.sh"
-    exit 1
-fi
-image_raw_path=$(realpath $1)
-pushd $(dirname $0) > /dev/null
-tempdir=$(mktemp -d)
-asset_dir=./assets/linux
-mkdir -p ${asset_dir}
-echo Copy files...
-pushd ${tempdir} > /dev/null
-cp "${image_raw_path}" ${tempdir}
-tar czvS -f images.tar.gz $(basename ${image_raw_path})
-popd > /dev/null
-cp vm_config.json ${asset_dir}
-mv ${tempdir}/images.tar.gz ${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}
-
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
index 1c739e2..a49ea72 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/InstallerActivity.java
@@ -16,37 +16,20 @@
 
 package com.android.virtualization.terminal;
 
-import android.annotation.WorkerThread;
 import android.app.Activity;
+import android.os.Build;
 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 com.android.virtualization.vmlauncher.InstallUtils;
 
-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.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 public class InstallerActivity extends Activity {
     private static final String TAG = "LinuxInstaller";
 
-    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
@@ -60,115 +43,24 @@
     }
 
     private void installLinuxImage() {
-        if (!hasLocalAssets()) {
-            updateStatus("No local assets");
-            setResult(RESULT_CANCELED, null);
-            return;
+        Log.d(TAG, "installLinuxImage");
+        // Installing from sdcard is supported only in debuggable build.
+        if (Build.isDebuggable()) {
+            updateStatus("try /sdcard/linux/images.tar.gz");
+            if (InstallUtils.installImageFromExternalStorage(this)) {
+                Log.d(TAG, "success / sdcard");
+                updateStatus("image is installed from /sdcard/linux/images.tar.gz");
+                setResult(RESULT_OK);
+                finish();
+                return;
+            }
+            Log.d(TAG, "fail / sdcard");
+            updateStatus("There is no /sdcard/linux/images.tar.gz");
         }
-        try {
-            updateImageIfNeeded();
-        } catch (IOException e) {
-            Log.e(TAG, "failed to update image", e);
-            return;
-        }
-        updateStatus("Done.");
-        setResult(RESULT_OK);
+        setResult(RESULT_CANCELED, null);
         finish();
     }
 
-    @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.linux_vm_setup.path", destDir);
-        SystemProperties.set("debug.linux_vm_setup.done", "false");
-        SystemProperties.set("debug.linux_vm_setup.start", "true");
-        while (!SystemProperties.getBoolean("debug.linux_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(
                 () -> {
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index 2a87348..0a750e3 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -15,9 +15,19 @@
  */
 package com.android.virtualization.terminal;
 
+import android.Manifest;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.drawable.Icon;
+import android.graphics.fonts.FontStyle;
 import android.net.http.SslError;
+import android.os.Build;
 import android.os.Bundle;
 import android.system.ErrnoException;
 import android.system.Os;
@@ -37,6 +47,7 @@
 
 import androidx.appcompat.app.AppCompatActivity;
 
+import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
 
 import com.google.android.material.appbar.MaterialToolbar;
@@ -65,17 +76,19 @@
     private static final String VM_ADDR = "192.168.0.2";
     private static final int TTYD_PORT = 7681;
     private static final int REQUEST_CODE_INSTALLER = 0x33;
+    private static final int FONT_SIZE_DEFAULT = 12;
 
     private X509Certificate[] mCertificates;
     private PrivateKey mPrivateKey;
     private WebView mWebView;
     private AccessibilityManager mAccessibilityManager;
+    private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        checkForUpdate();
+        boolean launchInstaller = installIfNecessary();
         try {
             // No resize for now.
             long newSizeInBytes = 0;
@@ -86,6 +99,14 @@
                     .show();
         }
 
+        checkAndRequestPostNotificationsPermission();
+
+        NotificationManager notificationManager = getSystemService(NotificationManager.class);
+        NotificationChannel notificationChannel =
+                new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_LOW);
+        assert notificationManager != null;
+        notificationManager.createNotificationChannel(notificationChannel);
+
         setContentView(R.layout.activity_headless);
 
         MaterialToolbar toolbar = (MaterialToolbar) findViewById(R.id.toolbar);
@@ -101,15 +122,28 @@
 
         connectToTerminalService();
         readClientCertificate();
+
+        // if installer is launched, it will be handled in onActivityResult
+        if (!launchInstaller) {
+            startVm();
+        }
     }
 
     private URL getTerminalServiceUrl() {
-        boolean needsAccessibility = mAccessibilityManager.isTouchExplorationEnabled();
-        String file = "/";
-        String query = needsAccessibility ? "?screenReaderMode=true" : "";
+        Configuration config = getResources().getConfiguration();
+
+        String query =
+                "?fontSize="
+                        + (int) (config.fontScale * FONT_SIZE_DEFAULT)
+                        + "&fontWeight="
+                        + (FontStyle.FONT_WEIGHT_NORMAL + config.fontWeightAdjustment)
+                        + "&fontWeightBold="
+                        + (FontStyle.FONT_WEIGHT_BOLD + config.fontWeightAdjustment)
+                        + "&screenReaderMode="
+                        + mAccessibilityManager.isTouchExplorationEnabled();
 
         try {
-            return new URL("https", VM_ADDR, TTYD_PORT, file + query);
+            return new URL("https", VM_ADDR, TTYD_PORT, "/" + query);
         } catch (MalformedURLException e) {
             // this cannot happen
             return null;
@@ -304,6 +338,15 @@
         return;
     }
 
+    private void checkAndRequestPostNotificationsPermission() {
+        if (getApplicationContext().checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
+                != PackageManager.PERMISSION_GRANTED) {
+            requestPermissions(
+                    new String[]{Manifest.permission.POST_NOTIFICATIONS},
+                    POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE);
+        }
+    }
+
     @Override
     protected void onDestroy() {
         getSystemService(AccessibilityManager.class).removeTouchExplorationStateChangeListener(this);
@@ -370,13 +413,45 @@
         }
     }
 
-    private void checkForUpdate() {
-        Intent intent = new Intent(this, InstallerActivity.class);
-        startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+    private boolean installIfNecessary() {
+        // If payload from external storage exists(only for debuggable build) or there is no
+        // installed image, launch installer activity.
+        if ((Build.isDebuggable() && InstallUtils.payloadFromExternalStorageExists())
+                || !InstallUtils.isImageInstalled(this)) {
+            Intent intent = new Intent(this, InstallerActivity.class);
+            startActivityForResult(intent, REQUEST_CODE_INSTALLER);
+            return true;
+        }
+        return false;
     }
 
     private void startVm() {
+        if (!InstallUtils.isImageInstalled(this)) {
+            return;
+        }
+        // TODO: implement intent for setting, close and tap to the notification
+        // Currently mock a PendingIntent for notification.
+        Intent intent = new Intent();
+        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+
+        Icon icon = Icon.createWithResource(getResources(), R.drawable.ic_launcher_foreground);
+        Notification notification = new Notification.Builder(this, TAG)
+                .setChannelId(TAG)
+                .setSmallIcon(R.drawable.ic_launcher_foreground)
+                .setContentTitle(getResources().getString(R.string.service_notification_title))
+                .setContentText(getResources().getString(R.string.service_notification_content))
+                .setContentIntent(pendingIntent)
+                .setOngoing(true)
+                .addAction(new Notification.Action.Builder(icon,
+                        getResources().getString(R.string.service_notification_settings),
+                        pendingIntent).build())
+                .addAction(new Notification.Action.Builder(icon,
+                        getResources().getString(R.string.service_notification_quit_action),
+                        pendingIntent).build())
+                .build();
+
         android.os.Trace.beginAsyncSection("executeTerminal", 0);
-        VmLauncherServices.startVmLauncherService(this, this);
+        VmLauncherServices.startVmLauncherService(this, this, notification);
     }
 }
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
index 6c36cc8..7119225 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/SettingsPortForwardingActivity.kt
@@ -15,12 +15,21 @@
  */
 package com.android.virtualization.terminal
 
+import android.Manifest
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Icon
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
 
 class SettingsPortForwardingActivity : AppCompatActivity() {
+    val TAG: String = "VmTerminalApp"
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.settings_port_forwarding)
@@ -37,5 +46,43 @@
         val recyclerView: RecyclerView = findViewById(R.id.settings_port_forwarding_recycler_view)
         recyclerView.layoutManager = LinearLayoutManager(this)
         recyclerView.adapter = settingsPortForwardingAdapter
+
+        // TODO: implement intent for accept, deny and tap to the notification
+        // Currently show a mock notification of a port opening
+        val terminalIntent = Intent()
+        val pendingIntent = PendingIntent.getActivity(
+            this, 0, terminalIntent,
+            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+        )
+        val notification =
+            Notification.Builder(this, TAG)
+                .setChannelId(TAG)
+                .setSmallIcon(R.drawable.ic_launcher_foreground)
+                .setContentTitle(resources.getString(R.string.settings_port_forwarding_notification_title))
+                .setContentText(resources.getString(R.string.settings_port_forwarding_notification_content, settingsPortForwardingItems[0].port))
+                .addAction(
+                    Notification.Action.Builder(
+                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
+                        resources.getString(R.string.settings_port_forwarding_notification_accept),
+                        pendingIntent
+                    ).build()
+                )
+                .addAction(
+                    Notification.Action.Builder(
+                        Icon.createWithResource(resources, R.drawable.ic_launcher_foreground),
+                        resources.getString(R.string.settings_port_forwarding_notification_deny),
+                        pendingIntent
+                    ).build()
+                )
+                .build()
+
+        with(NotificationManager.from(this)) {
+            if (ActivityCompat.checkSelfPermission(
+                    this@SettingsPortForwardingActivity, Manifest.permission.POST_NOTIFICATIONS
+                ) == PackageManager.PERMISSION_GRANTED
+            ) {
+                notify(0, notification)
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/android/TerminalApp/linux_vm_setup.rc b/android/TerminalApp/linux_vm_setup.rc
deleted file mode 100644
index ac91532..0000000
--- a/android/TerminalApp/linux_vm_setup.rc
+++ /dev/null
@@ -1,23 +0,0 @@
-# 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.
-
-service linux_vm_setup /system/bin/linux_vm_setup
-    user shell
-    group shell media_rw
-    disabled
-    oneshot
-    seclabel u:r:shell:s0
-
-on property:debug.linux_vm_setup.start=true
-    start linux_vm_setup
diff --git a/android/TerminalApp/linux_vm_setup.sh b/android/TerminalApp/linux_vm_setup.sh
deleted file mode 100644
index 6a93f6f..0000000
--- a/android/TerminalApp/linux_vm_setup.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/system/bin/sh
-
-function round_up() {
-  num=$1
-  div=$2
-  echo $((( (( ${num} / ${div} ) + 1) * ${div} )))
-}
-
-function install() {
-  src_dir=$(getprop debug.linux_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.linux_vm_setup.done false
-install
-setprop debug.linux_vm_setup.start false
-setprop debug.linux_vm_setup.done true
diff --git a/android/TerminalApp/res/values/strings.xml b/android/TerminalApp/res/values/strings.xml
index 1cbaee8..0cdb939 100644
--- a/android/TerminalApp/res/values/strings.xml
+++ b/android/TerminalApp/res/values/strings.xml
@@ -49,6 +49,14 @@
     <string name="settings_port_forwarding_title">Port Forwarding</string>
     <!-- Settings menu subtitle for 'port forwarding' [CHAR LIMIT=none] -->
     <string name="settings_port_forwarding_sub_title">Configure port forwarding</string>
+    <!-- Notification title for new port forwarding [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_title">Terminal is trying to open a new port</string>
+    <!-- Notification content for new port forwarding [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_content">Port requested to be open: <xliff:g id="port_number" example="8080">%d</xliff:g></string>
+    <!-- Notification action accept [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_accept">Accept</string>
+    <!-- Notification action deny [CHAR LIMIT=none] -->
+    <string name="settings_port_forwarding_notification_deny">Deny</string>
 
     <!-- Settings menu title for recoverying image [CHAR LIMIT=none] -->
     <string name="settings_recovery_title">Recovery</string>
@@ -60,4 +68,13 @@
     <string name="settings_recovery_reset_sub_title">Remove all</string>
     <!-- Toast message for reset is completed [CHAR LIMIT=none] -->
     <string name="settings_recovery_reset_message">VM reset</string>
+
+    <!-- Notification action button for settings [CHAR LIMIT=none] -->
+    <string name="service_notification_settings">Settings</string>
+    <!-- Notification title for foreground service notification [CHAR LIMIT=none] -->
+    <string name="service_notification_title">Terminal is running</string>
+    <!-- Notification content for foreground service notification [CHAR LIMIT=none] -->
+    <string name="service_notification_content">Click to open the terminal.</string>
+    <!-- Notification action button for closing the virtual machine [CHAR LIMIT=none] -->
+    <string name="service_notification_quit_action">Close</string>
 </resources>
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index 23652d2..5dac07f 100644
--- a/android/virtmgr/src/aidl.rs
+++ b/android/virtmgr/src/aidl.rs
@@ -17,7 +17,7 @@
 use crate::{get_calling_pid, get_calling_uid, get_this_pid};
 use crate::atom::{write_vm_booted_stats, write_vm_creation_stats};
 use crate::composite::make_composite_image;
-use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, UsbConfig, VmContext, VmInstance, VmState};
+use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, SharedPathConfig, DisplayConfig, GpuConfig, InputDeviceOption, PayloadState, UsbConfig, VmContext, VmInstance, VmState};
 use crate::debug_config::DebugConfig;
 use crate::dt_overlay::{create_device_tree_overlay, VM_DT_OVERLAY_MAX_SIZE, VM_DT_OVERLAY_PATH};
 use crate::payload::{add_microdroid_payload_images, add_microdroid_system_images, add_microdroid_vendor_image};
@@ -32,6 +32,7 @@
     AssignableDevice::AssignableDevice,
     CpuTopology::CpuTopology,
     DiskImage::DiskImage,
+    SharedPath::SharedPath,
     InputDevice::InputDevice,
     IVirtualMachine::{self, BnVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
@@ -67,6 +68,7 @@
 };
 use cstr::cstr;
 use glob::glob;
+use libc::{AF_VSOCK, sa_family_t, sockaddr_vm};
 use log::{debug, error, info, warn};
 use microdroid_payload_config::{ApkConfig, Task, TaskType, VmPayloadConfig};
 use nix::unistd::pipe;
@@ -89,7 +91,7 @@
 use std::sync::{Arc, Mutex, Weak, LazyLock};
 use vbmeta::VbMetaImage;
 use vmconfig::{VmConfig, get_debug_level};
-use vsock::{VsockAddr, VsockStream};
+use vsock::VsockStream;
 use zip::ZipArchive;
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
@@ -612,6 +614,8 @@
             })
             .collect::<Result<Vec<DiskFile>, _>>()?;
 
+        let shared_paths = assemble_shared_paths(&config.sharedPaths, &temporary_directory)?;
+
         let (cpus, host_cpu_topology) = match config.cpuTopology {
             CpuTopology::MATCH_HOST => (None, true),
             CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
@@ -718,6 +722,7 @@
             kernel,
             initrd,
             disks,
+            shared_paths,
             params: config.params.to_owned(),
             protected: *is_protected,
             debug_config,
@@ -955,6 +960,32 @@
         },
     })
 }
+
+fn assemble_shared_paths(
+    shared_paths: &[SharedPath],
+    temporary_directory: &Path,
+) -> Result<Vec<SharedPathConfig>, Status> {
+    if shared_paths.is_empty() {
+        return Ok(Vec::new()); // Return an empty vector if shared_paths is empty
+    }
+
+    shared_paths
+        .iter()
+        .map(|path| {
+            Ok(SharedPathConfig {
+                path: path.sharedPath.clone(),
+                host_uid: path.hostUid,
+                host_gid: path.hostGid,
+                guest_uid: path.guestUid,
+                guest_gid: path.guestGid,
+                mask: path.mask,
+                tag: path.tag.clone(),
+                socket_path: temporary_directory.join(&path.socket).to_string_lossy().to_string(),
+            })
+        })
+        .collect()
+}
+
 /// Given the configuration for a disk image, assembles the `DiskFile` to pass to crosvm.
 ///
 /// This may involve assembling a composite disk from a set of partition images.
@@ -1434,8 +1465,14 @@
             ));
         }
         let cid = self.instance.cid;
-        let get_connection_info =
-            move |_instance: &str| Some(ConnectionInfo::Vsock(VsockAddr::new(cid, port)));
+        let addr = sockaddr_vm {
+            svm_family: AF_VSOCK as sa_family_t,
+            svm_reserved1: 0,
+            svm_port: port,
+            svm_cid: cid,
+            svm_zero: [0u8; 4],
+        };
+        let get_connection_info = move |_instance: &str| Some(ConnectionInfo::Vsock(addr));
         let accessor = Accessor::new(name, get_connection_info);
         accessor
             .as_binder()
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 4532ef9..86b3571 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -36,6 +36,7 @@
 use std::mem;
 use std::num::{NonZeroU16, NonZeroU32};
 use std::os::unix::io::{AsRawFd, OwnedFd};
+use std::os::unix::process::CommandExt;
 use std::os::unix::process::ExitStatusExt;
 use std::path::{Path, PathBuf};
 use std::process::{Command, ExitStatus};
@@ -106,6 +107,7 @@
     pub kernel: Option<File>,
     pub initrd: Option<File>,
     pub disks: Vec<DiskFile>,
+    pub shared_paths: Vec<SharedPathConfig>,
     pub params: Option<String>,
     pub protected: bool,
     pub debug_config: DebugConfig,
@@ -222,6 +224,19 @@
     pub writable: bool,
 }
 
+/// Shared path between host and guest VM.
+#[derive(Debug)]
+pub struct SharedPathConfig {
+    pub path: String,
+    pub host_uid: i32,
+    pub host_gid: i32,
+    pub guest_uid: i32,
+    pub guest_gid: i32,
+    pub mask: i32,
+    pub tag: String,
+    pub socket_path: String,
+}
+
 /// virtio-input device configuration from `external/crosvm/src/crosvm/config.rs`
 #[derive(Debug)]
 #[allow(dead_code)]
@@ -304,6 +319,8 @@
             let tap =
                 if let Some(tap_file) = &config.tap { Some(tap_file.try_clone()?) } else { None };
 
+            run_virtiofs(&config)?;
+
             // If this fails and returns an error, `self` will be left in the `Failed` state.
             let child =
                 Arc::new(run_vm(config, &instance.crosvm_control_socket_path, failure_pipe_write)?);
@@ -881,6 +898,39 @@
     }
 }
 
+fn run_virtiofs(config: &CrosvmConfig) -> io::Result<()> {
+    for shared_path in &config.shared_paths {
+        let ugid_map_value = format!(
+            "{} {} {} {} {} /",
+            shared_path.guest_uid,
+            shared_path.guest_gid,
+            shared_path.host_uid,
+            shared_path.host_gid,
+            shared_path.mask,
+        );
+
+        let cfg_arg = format!("writeback=true,cache_policy=always,ugid_map='{}'", ugid_map_value);
+
+        let mut command = Command::new(CROSVM_PATH);
+        command
+            .arg("device")
+            .arg("fs")
+            .arg(format!("--socket={}", &shared_path.socket_path))
+            .arg(format!("--tag={}", &shared_path.tag))
+            .arg(format!("--shared-dir={}", &shared_path.path))
+            .arg("--cfg")
+            .arg(cfg_arg.as_str())
+            .arg("--disable-sandbox");
+
+        print_crosvm_args(&command);
+
+        let result = SharedChild::spawn(&mut command)?;
+        info!("Spawned virtiofs crosvm({})", result.id());
+    }
+
+    Ok(())
+}
+
 /// Starts an instance of `crosvm` to manage a new VM.
 fn run_vm(
     config: CrosvmConfig,
@@ -890,6 +940,9 @@
     validate_config(&config)?;
 
     let mut command = Command::new(CROSVM_PATH);
+
+    let vm_name = "crosvm_".to_owned() + &config.name;
+    command.arg0(vm_name.clone());
     // TODO(qwandor): Remove --disable-sandbox.
     command
         .arg("--extended-status")
@@ -898,6 +951,8 @@
         .arg("--log-level")
         .arg("info,disk=warn")
         .arg("run")
+        .arg("--name")
+        .arg(vm_name)
         .arg("--disable-sandbox")
         .arg("--cid")
         .arg(config.cid.to_string());
@@ -1059,6 +1114,9 @@
         command.arg(add_preserved_fd(&mut preserved_fds, kernel));
     }
 
+    #[cfg(target_arch = "aarch64")]
+    command.arg("--no-pmu");
+
     let control_sock = create_crosvm_control_listener(crosvm_control_socket_path)
         .context("failed to create control listener")?;
     command.arg("--socket").arg(add_preserved_fd(&mut preserved_fds, control_sock));
@@ -1184,6 +1242,12 @@
         command.arg(vfio_argument_for_platform_device(&device)?);
     }
 
+    for shared_path in &config.shared_paths {
+        command
+            .arg("--vhost-user-fs")
+            .arg(format!("{},tag={}", &shared_path.socket_path, &shared_path.tag));
+    }
+
     debug!("Preserving FDs {:?}", preserved_fds);
     command.preserved_fds(preserved_fds);
 
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl
new file mode 100644
index 0000000..7be7a5f
--- /dev/null
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/SharedPath.aidl
@@ -0,0 +1,43 @@
+/*
+ * 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 android.system.virtualizationservice;
+
+/** Shared directory path between host and guest */
+parcelable SharedPath {
+    /** Shared path between host and guest */
+    String sharedPath;
+
+    /** UID of the path on the host */
+    int hostUid;
+
+    /** GID of the path on the host */
+    int hostGid;
+
+    /** UID of the path on the guest */
+    int guestUid;
+
+    /** GID of the path on the guest */
+    int guestGid;
+
+    /** umask settings for the path */
+    int mask;
+
+    /** virtiofs unique tag per path */
+    String tag;
+
+    /** socket name for vhost-user-fs */
+    String socket;
+}
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index f559a71..9f2a23e 100644
--- a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -21,6 +21,7 @@
 import android.system.virtualizationservice.DisplayConfig;
 import android.system.virtualizationservice.GpuConfig;
 import android.system.virtualizationservice.InputDevice;
+import android.system.virtualizationservice.SharedPath;
 import android.system.virtualizationservice.UsbConfig;
 
 /** Raw configuration for running a VM. */
@@ -52,6 +53,9 @@
     /** Disk images to be made available to the VM. */
     DiskImage[] disks;
 
+    /** Shared paths between host and guest */
+    SharedPath[] sharedPaths;
+
     /** Whether the VM should be a protected VM. */
     boolean protectedVm;
 
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 4b69660..e485aa7 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -175,10 +175,6 @@
         "true": ["virtualizationservice.xml"],
         default: unset,
     }),
-    required: select(release_flag("RELEASE_AVF_SUPPORT_CUSTOM_VM_WITH_PARAVIRTUALIZED_DEVICES"), {
-        true: ["linux_vm_setup"],
-        default: [],
-    }),
 }
 
 apex_defaults {
@@ -289,9 +285,11 @@
         "libz",
     ],
     data: [
-        ":com.android.virt",
         ":test.com.android.virt.pem",
     ],
+    device_common_data: [
+        ":com.android.virt",
+    ],
     test_suites: ["general-tests"],
 }
 
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 71a9d3b..b4e8b2f 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -82,6 +82,13 @@
 			qemu-system
 		)
 	fi
+
+	# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
+	if [[ "$arch" == "x86_64" ]]; then
+		packages+=(
+			libguestfs-tools
+		)
+	fi
 	DEBIAN_FRONTEND=noninteractive \
 	apt install --no-install-recommends --assume-yes "${packages[@]}"
 
@@ -160,3 +167,18 @@
 copy_android_config
 run_fai
 fdisk -l image.raw
+images=(image.raw)
+# TODO(b/365955006): remove these lines when uboot supports x86_64 EFI application
+if [[ "$arch" == "x86_64" ]]; then
+	virt-get-kernel -a image.raw
+	mv vmlinuz* vmlinuz
+	mv initrd.img* initrd.img
+	images+=(
+		vmlinuz
+		initrd.img
+	)
+fi
+
+cp $(dirname $0)/vm_config.json.${arch} vm_config.json
+# --sparse option isn't supported in apache-commons-compress
+tar czv -f images.tar.gz ${images[@]} vm_config.json
\ No newline at end of file
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
index c1524dd..7a1523a 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/aarch64/build.sh
@@ -6,7 +6,7 @@
 sudo losetup -D
 grep vmx /proc/cpuinfo || true
 sudo ./build.sh
-tar czvS -f ${KOKORO_ARTIFACTS_DIR}/images.tar.gz image.raw
+sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
 
 mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
 sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
index ab675f0..66e3d64 100644
--- a/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
+++ b/build/debian/kokoro/gcp_ubuntu_docker/x86_64/build.sh
@@ -6,7 +6,7 @@
 sudo losetup -D
 grep vmx /proc/cpuinfo || true
 sudo ./build.sh -a x86_64
-tar czvS -f ${KOKORO_ARTIFACTS_DIR}/images.tar.gz image.raw
+sudo mv images.tar.gz ${KOKORO_ARTIFACTS_DIR} || true
 
 mkdir -p ${KOKORO_ARTIFACTS_DIR}/logs
 sudo cp -r /var/log/fai/* ${KOKORO_ARTIFACTS_DIR}/logs || true
diff --git a/android/TerminalApp/vm_config.json b/build/debian/vm_config.json.aarch64
similarity index 86%
rename from android/TerminalApp/vm_config.json
rename to build/debian/vm_config.json.aarch64
index 474e9c3..9f9295c 100644
--- a/android/TerminalApp/vm_config.json
+++ b/build/debian/vm_config.json.aarch64
@@ -1,9 +1,8 @@
-
 {
     "name": "debian",
     "disks": [
         {
-            "image": "/data/local/tmp/image.raw",
+            "image": "$PAYLOAD_DIR/image.raw",
             "partitions": [],
             "writable": true
         }
@@ -18,4 +17,3 @@
     "console_input_device": "ttyS0",
     "network": true
 }
-
diff --git a/android/TerminalApp/vm_config.json b/build/debian/vm_config.json.x86_64
similarity index 69%
copy from android/TerminalApp/vm_config.json
copy to build/debian/vm_config.json.x86_64
index 474e9c3..2fb9faa 100644
--- a/android/TerminalApp/vm_config.json
+++ b/build/debian/vm_config.json.x86_64
@@ -1,13 +1,15 @@
-
 {
     "name": "debian",
     "disks": [
         {
-            "image": "/data/local/tmp/image.raw",
+            "image": "$PAYLOAD_DIR/image.raw",
             "partitions": [],
             "writable": true
         }
     ],
+    "kernel": "$PAYLOAD_DIR/vmlinuz",
+    "initrd": "$PAYLOAD_DIR/initrd.img",
+    "params": "root=/dev/vda1",
     "protected": false,
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
@@ -18,4 +20,3 @@
     "console_input_device": "ttyS0",
     "network": true
 }
-
diff --git a/build/microdroid/Android.bp b/build/microdroid/Android.bp
index 27d0246..abb97da 100644
--- a/build/microdroid/Android.bp
+++ b/build/microdroid/Android.bp
@@ -197,7 +197,7 @@
     no_full_install: true,
 }
 
-genrule {
+java_genrule {
     name: "microdroid_build_prop_gen_x86_64",
     srcs: [
         "build.prop",
@@ -215,7 +215,7 @@
         "echo ro.product.cpu.abi=x86_64) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_build_prop_gen_arm64",
     srcs: [
         "build.prop",
@@ -597,6 +597,7 @@
 // HACK: use cc_genrule for arch-specific properties
 cc_genrule {
     name: "microdroid_kernel_hashes_rs",
+    compile_multilib: "first",
     srcs: [":microdroid_kernel"],
     arch: {
         arm64: {
@@ -621,6 +622,7 @@
 
 rust_library_rlib {
     name: "libmicrodroid_kernel_hashes",
+    compile_multilib: "first",
     srcs: [":microdroid_kernel_hashes_rs"],
     crate_name: "microdroid_kernel_hashes",
     prefer_rlib: true,
diff --git a/build/microdroid/initrd/Android.bp b/build/microdroid/initrd/Android.bp
index 9904511..6d45417 100644
--- a/build/microdroid/initrd/Android.bp
+++ b/build/microdroid/initrd/Android.bp
@@ -30,7 +30,7 @@
     srcs: ["gen_vbmeta_bootconfig.py"],
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_gen",
     srcs: [
         ":microdroid_ramdisk",
@@ -40,7 +40,7 @@
     cmd: "cat $(in) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_gen_arm64",
     srcs: [
         ":microdroid_ramdisk",
@@ -51,7 +51,7 @@
     cmd: "cat $(in) > $(out)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_gen_x86_64",
     srcs: [
         ":microdroid_ramdisk",
@@ -63,7 +63,7 @@
 }
 
 // This contains vbmeta hashes & related (boot)configs which are passed to kernel/init
-genrule {
+java_genrule {
     name: "microdroid_vbmeta_bootconfig_gen",
     srcs: [":microdroid_vbmeta"],
     out: ["bootconfig_microdroid_vbmeta"],
@@ -84,7 +84,7 @@
     ":microdroid_vbmeta_bootconfig_gen",
 ]
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_debuggable_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -95,7 +95,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_debuggable_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -106,7 +106,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_debuggable_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -117,7 +117,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_debuggable_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -128,7 +128,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_normal_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -139,7 +139,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_normal_arm64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -150,7 +150,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_initrd_normal_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
@@ -161,7 +161,7 @@
     cmd: "$(location initrd_bootconfig) attach --output $(out) $(in)",
 }
 
-genrule {
+java_genrule {
     name: "microdroid_gki-android15-6.6_initrd_normal_x86_64",
     tools: ["initrd_bootconfig"],
     srcs: [
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index bcd3e42..4586cca 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -18,7 +18,6 @@
         "libciborium_io_nostd",
         "libcstr",
         "libdiced_open_dice_nostd",
-        "libfdtpci",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libpvmfw_avb_nostd",
diff --git a/guest/pvmfw/src/fdt.rs b/guest/pvmfw/src/fdt.rs
index 953fdae..0d934a6 100644
--- a/guest/pvmfw/src/fdt.rs
+++ b/guest/pvmfw/src/fdt.rs
@@ -30,8 +30,6 @@
 use core::mem::size_of;
 use core::ops::Range;
 use cstr::cstr;
-use fdtpci::PciMemoryFlags;
-use fdtpci::PciRangeType;
 use libfdt::AddressRange;
 use libfdt::CellIterator;
 use libfdt::Fdt;
@@ -45,6 +43,8 @@
 use log::warn;
 use static_assertions::const_assert;
 use tinyvec::ArrayVec;
+use vmbase::fdt::pci::PciMemoryFlags;
+use vmbase::fdt::pci::PciRangeType;
 use vmbase::fdt::SwiotlbInfo;
 use vmbase::hyp;
 use vmbase::layout::{crosvm::MEM_START, MAX_VIRT_ADDR};
diff --git a/guest/pvmfw/src/main.rs b/guest/pvmfw/src/main.rs
index 10f8549..1e88c4b 100644
--- a/guest/pvmfw/src/main.rs
+++ b/guest/pvmfw/src/main.rs
@@ -46,13 +46,13 @@
 use core::ops::Range;
 use cstr::cstr;
 use diced_open_dice::{bcc_handover_parse, DiceArtifacts, Hidden};
-use fdtpci::{PciError, PciInfo};
 use libfdt::{Fdt, FdtNode};
 use log::{debug, error, info, trace, warn};
 use pvmfw_avb::verify_payload;
 use pvmfw_avb::Capability;
 use pvmfw_avb::DebugLevel;
 use pvmfw_embedded_key::PUBLIC_KEY;
+use vmbase::fdt::pci::{PciError, PciInfo};
 use vmbase::heap;
 use vmbase::memory::flush;
 use vmbase::memory::MEMORY;
diff --git a/guest/rialto/Android.bp b/guest/rialto/Android.bp
index eeb5b2d..7bcfd54 100644
--- a/guest/rialto/Android.bp
+++ b/guest/rialto/Android.bp
@@ -14,7 +14,6 @@
         "libciborium_nostd",
         "libcstr",
         "libdiced_open_dice_nostd",
-        "libfdtpci",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libservice_vm_comm_nostd",
diff --git a/guest/rialto/src/error.rs b/guest/rialto/src/error.rs
index 033159b..ba5f4b0 100644
--- a/guest/rialto/src/error.rs
+++ b/guest/rialto/src/error.rs
@@ -17,10 +17,11 @@
 use aarch64_paging::MapError;
 use core::{fmt, result};
 use diced_open_dice::DiceError;
-use fdtpci::PciError;
 use libfdt::FdtError;
 use service_vm_comm::RequestProcessingError;
-use vmbase::{hyp::Error as HypervisorError, memory::MemoryTrackerError, virtio::pci};
+use vmbase::{
+    fdt::pci::PciError, hyp::Error as HypervisorError, memory::MemoryTrackerError, virtio::pci,
+};
 
 pub type Result<T> = result::Result<T, Error>;
 
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index 9265775..f09cbd2 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -32,7 +32,6 @@
 use core::num::NonZeroUsize;
 use core::slice;
 use diced_open_dice::{bcc_handover_parse, DiceArtifacts};
-use fdtpci::PciInfo;
 use libfdt::FdtError;
 use log::{debug, error, info};
 use service_vm_comm::{ServiceVmRequest, VmType};
@@ -45,6 +44,7 @@
 };
 use vmbase::{
     configure_heap,
+    fdt::pci::PciInfo,
     fdt::SwiotlbInfo,
     generate_image_header,
     hyp::{get_mem_sharer, get_mmio_guard},
diff --git a/guest/rialto/tests/test.rs b/guest/rialto/tests/test.rs
index 7ec5647..09f9cc3 100644
--- a/guest/rialto/tests/test.rs
+++ b/guest/rialto/tests/test.rs
@@ -25,7 +25,10 @@
 use bssl_avf::{rand_bytes, sha256, EcKey, PKey};
 use client_vm_csr::generate_attestation_key_and_csr;
 use coset::{CborSerializable, CoseMac0, CoseSign};
-use hwtrust::{rkp, session::Session};
+use hwtrust::{
+    rkp,
+    session::{RkpInstance, Session},
+};
 use log::{info, warn};
 use service_vm_comm::{
     ClientVmAttestationParams, Csr, CsrPayload, EcdsaP256KeyPair, GenerateCertificateRequestParams,
@@ -206,7 +209,8 @@
     let vm_component = vm_component.decode_as::<asn1::SequenceOf<asn1::Any, 4>>().unwrap();
     assert_eq!(4, vm_component.len());
     let name = vm_component.get(0).unwrap().decode_as::<asn1::Utf8StringRef>().unwrap();
-    assert_eq!(expected_component.name, name.as_ref());
+    let name_str: &str = name.as_ref();
+    assert_eq!(expected_component.name, name_str);
     let version = vm_component.get(1).unwrap().decode_as::<u64>().unwrap();
     assert_eq!(expected_component.version, version);
     let code_hash = vm_component.get(2).unwrap().decode_as::<asn1::OctetString>().unwrap();
@@ -285,6 +289,7 @@
 fn check_csr(csr: Vec<u8>) -> Result<()> {
     let mut session = Session::default();
     session.set_allow_any_mode(true);
+    session.set_rkp_instance(RkpInstance::Avf);
     let _csr = rkp::Csr::from_cbor(&session, &csr[..]).context("Failed to parse CSR")?;
     Ok(())
 }
diff --git a/guest/vmbase_example/Android.bp b/guest/vmbase_example/Android.bp
index 49a6d69..09bd77c 100644
--- a/guest/vmbase_example/Android.bp
+++ b/guest/vmbase_example/Android.bp
@@ -11,7 +11,6 @@
         "libaarch64_paging",
         "libcstr",
         "libdiced_open_dice_nostd",
-        "libfdtpci",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libvirtio_drivers",
diff --git a/guest/vmbase_example/src/main.rs b/guest/vmbase_example/src/main.rs
index 7a3f427..1466d1e 100644
--- a/guest/vmbase_example/src/main.rs
+++ b/guest/vmbase_example/src/main.rs
@@ -31,11 +31,12 @@
 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, generate_image_header,
+    bionic, configure_heap,
+    fdt::pci::PciInfo,
+    generate_image_header,
     layout::{crosvm::FDT_MAX_SIZE, rodata_range, scratch_range, text_range},
     linker, logger, main,
     memory::{PageTable, SIZE_64KB},
diff --git a/guest/vmbase_example/src/pci.rs b/guest/vmbase_example/src/pci.rs
index b838539..563f24a 100644
--- a/guest/vmbase_example/src/pci.rs
+++ b/guest/vmbase_example/src/pci.rs
@@ -17,7 +17,6 @@
 use aarch64_paging::paging::MemoryRegion;
 use alloc::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout};
 use core::{mem::size_of, ptr::NonNull};
-use fdtpci::PciInfo;
 use log::{debug, info};
 use virtio_drivers::{
     device::console::VirtIOConsole,
@@ -27,7 +26,10 @@
     },
     BufferDirection, Error, Hal, PhysAddr, PAGE_SIZE,
 };
-use vmbase::virtio::pci::{self, PciTransportIterator};
+use vmbase::{
+    fdt::pci::PciInfo,
+    virtio::pci::{self, PciTransportIterator},
+};
 
 /// The standard sector size of a VirtIO block device, in bytes.
 const SECTOR_SIZE_BYTES: usize = 512;
diff --git a/libs/fdtpci/Android.bp b/libs/fdtpci/Android.bp
deleted file mode 100644
index d7a5da3..0000000
--- a/libs/fdtpci/Android.bp
+++ /dev/null
@@ -1,19 +0,0 @@
-package {
-    default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_library_rlib {
-    name: "libfdtpci",
-    edition: "2021",
-    no_stdlibs: true,
-    host_supported: false,
-    crate_name: "fdtpci",
-    defaults: ["avf_build_flags_rust"],
-    srcs: ["src/lib.rs"],
-    rustlibs: [
-        "liblibfdt_nostd",
-        "liblog_rust_nostd",
-        "libvirtio_drivers",
-    ],
-    apex_available: ["com.android.virt"],
-}
diff --git a/libs/fdtpci/TEST_MAPPING b/libs/fdtpci/TEST_MAPPING
deleted file mode 100644
index 192a7c6..0000000
--- a/libs/fdtpci/TEST_MAPPING
+++ /dev/null
@@ -1,9 +0,0 @@
-// When adding or removing tests here, don't forget to amend _all_modules list in
-// wireless/android/busytown/ath_config/configs/prod/avf/tests.gcl
-{
-  "avf-presubmit": [
-    {
-      "name": "vmbase_example.integration_test"
-    }
-  ]
-}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index de1b081..2bcb40b 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -40,6 +40,7 @@
 import android.sysprop.HypervisorProperties;
 import android.system.virtualizationservice.DiskImage;
 import android.system.virtualizationservice.Partition;
+import android.system.virtualizationservice.SharedPath;
 import android.system.virtualizationservice.UsbConfig;
 import android.system.virtualizationservice.VirtualMachineAppConfig;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
@@ -712,6 +713,15 @@
             config.disks[i].partitions = partitions.toArray(new Partition[0]);
         }
 
+        config.sharedPaths =
+                new SharedPath
+                        [Optional.ofNullable(customImageConfig.getSharedPaths())
+                                .map(arr -> arr.length)
+                                .orElse(0)];
+        for (int i = 0; i < config.sharedPaths.length; i++) {
+            config.sharedPaths[i] = customImageConfig.getSharedPaths()[i].toParcelable();
+        }
+
         config.displayConfig =
                 Optional.ofNullable(customImageConfig.getDisplayConfig())
                         .map(dc -> dc.toParcelable())
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index 9774585..9b0709d 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -54,6 +54,7 @@
     @Nullable private final String bootloaderPath;
     @Nullable private final String[] params;
     @Nullable private final Disk[] disks;
+    @Nullable private final SharedPath[] sharedPaths;
     @Nullable private final DisplayConfig displayConfig;
     @Nullable private final AudioConfig audioConfig;
     private final boolean touch;
@@ -96,6 +97,11 @@
         return params;
     }
 
+    @Nullable
+    public SharedPath[] getSharedPaths() {
+        return sharedPaths;
+    }
+
     public boolean useTouch() {
         return touch;
     }
@@ -132,6 +138,7 @@
             String bootloaderPath,
             String[] params,
             Disk[] disks,
+            SharedPath[] sharedPaths,
             DisplayConfig displayConfig,
             boolean touch,
             boolean keyboard,
@@ -149,6 +156,7 @@
         this.bootloaderPath = bootloaderPath;
         this.params = params;
         this.disks = disks;
+        this.sharedPaths = sharedPaths;
         this.displayConfig = displayConfig;
         this.touch = touch;
         this.keyboard = keyboard;
@@ -300,6 +308,91 @@
     }
 
     /** @hide */
+    public static final class SharedPath {
+        private final String path;
+        private final int hostUid;
+        private final int hostGid;
+        private final int guestUid;
+        private final int guestGid;
+        private final int mask;
+        private final String tag;
+        private final String socket;
+
+        public SharedPath(
+                String path,
+                int hostUid,
+                int hostGid,
+                int guestUid,
+                int guestGid,
+                int mask,
+                String tag,
+                String socket) {
+            this.path = path;
+            this.hostUid = hostUid;
+            this.hostGid = hostGid;
+            this.guestUid = guestUid;
+            this.guestGid = guestGid;
+            this.mask = mask;
+            this.tag = tag;
+            this.socket = socket;
+        }
+
+        android.system.virtualizationservice.SharedPath toParcelable() {
+            android.system.virtualizationservice.SharedPath parcelable =
+                    new android.system.virtualizationservice.SharedPath();
+            parcelable.sharedPath = this.path;
+            parcelable.hostUid = this.hostUid;
+            parcelable.hostGid = this.hostGid;
+            parcelable.guestUid = this.guestUid;
+            parcelable.guestGid = this.guestGid;
+            parcelable.mask = this.mask;
+            parcelable.tag = this.tag;
+            parcelable.socket = this.socket;
+            return parcelable;
+        }
+
+        /** @hide */
+        public String getSharedPath() {
+            return path;
+        }
+
+        /** @hide */
+        public int getHostUid() {
+            return hostUid;
+        }
+
+        /** @hide */
+        public int getHostGid() {
+            return hostGid;
+        }
+
+        /** @hide */
+        public int getGuestUid() {
+            return guestUid;
+        }
+
+        /** @hide */
+        public int getGuestGid() {
+            return guestGid;
+        }
+
+        /** @hide */
+        public int getMask() {
+            return mask;
+        }
+
+        /** @hide */
+        public String getTag() {
+            return tag;
+        }
+
+        /** @hide */
+        public String getSocket() {
+            return socket;
+        }
+    }
+
+    /** @hide */
     public static final class Disk {
         private final boolean writable;
         private final String imagePath;
@@ -366,6 +459,7 @@
         private String bootloaderPath;
         private List<String> params = new ArrayList<>();
         private List<Disk> disks = new ArrayList<>();
+        private List<SharedPath> sharedPaths = new ArrayList<>();
         private AudioConfig audioConfig;
         private DisplayConfig displayConfig;
         private boolean touch;
@@ -413,6 +507,12 @@
         }
 
         /** @hide */
+        public Builder addSharedPath(SharedPath path) {
+            this.sharedPaths.add(path);
+            return this;
+        }
+
+        /** @hide */
         public Builder addParam(String param) {
             this.params.add(param);
             return this;
@@ -493,6 +593,7 @@
                     this.bootloaderPath,
                     this.params.toArray(new String[0]),
                     this.disks.toArray(new Disk[0]),
+                    this.sharedPaths.toArray(new SharedPath[0]),
                     displayConfig,
                     touch,
                     keyboard,
diff --git a/libs/libservice_vm_fake_chain/Android.bp b/libs/libservice_vm_fake_chain/Android.bp
index 39f36eb..56fb22a 100644
--- a/libs/libservice_vm_fake_chain/Android.bp
+++ b/libs/libservice_vm_fake_chain/Android.bp
@@ -18,6 +18,7 @@
 
 rust_defaults {
     name: "libservice_vm_fake_chain_defaults",
+    compile_multilib: "first",
     crate_name: "service_vm_fake_chain",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
diff --git a/libs/libservice_vm_fake_chain/src/service_vm.rs b/libs/libservice_vm_fake_chain/src/service_vm.rs
index 9bd831d..86fd3ea 100644
--- a/libs/libservice_vm_fake_chain/src/service_vm.rs
+++ b/libs/libservice_vm_fake_chain/src/service_vm.rs
@@ -116,6 +116,7 @@
         component_name: Some(cstr!("Protected VM firmware")),
         component_version: Some(1),
         resettable: true,
+        rkp_vm_marker: true,
         ..Default::default()
     };
     let config_descriptor = retry_bcc_format_config_descriptor(&config_values)?;
@@ -158,6 +159,7 @@
         component_name: Some(cstr!("vm_entry")),
         component_version: Some(12),
         resettable: true,
+        rkp_vm_marker: true,
         ..Default::default()
     };
     let config_descriptor = retry_bcc_format_config_descriptor(&config_values)?;
diff --git a/libs/libservice_vm_requests/Android.bp b/libs/libservice_vm_requests/Android.bp
index 57da012..d87b087 100644
--- a/libs/libservice_vm_requests/Android.bp
+++ b/libs/libservice_vm_requests/Android.bp
@@ -4,6 +4,7 @@
 
 rust_defaults {
     name: "libservice_vm_requests_nostd_defaults",
+    compile_multilib: "first",
     crate_name: "service_vm_requests",
     defaults: ["avf_build_flags_rust"],
     srcs: ["src/lib.rs"],
diff --git a/libs/libvmbase/Android.bp b/libs/libvmbase/Android.bp
index e634c18..206c4cb 100644
--- a/libs/libvmbase/Android.bp
+++ b/libs/libvmbase/Android.bp
@@ -80,7 +80,6 @@
         "libaarch64_paging",
         "libbuddy_system_allocator",
         "libcstr",
-        "libfdtpci",
         "liblibfdt_nostd",
         "liblog_rust_nostd",
         "libonce_cell_nostd",
diff --git a/libs/libvmbase/src/arch.rs b/libs/libvmbase/src/arch.rs
index d8bb8b2..992ab27 100644
--- a/libs/libvmbase/src/arch.rs
+++ b/libs/libvmbase/src/arch.rs
@@ -91,3 +91,22 @@
         }
     }};
 }
+
+/// Write with well-defined compiled behavior.
+///
+/// See https://github.com/rust-lang/rust/issues/131894
+///
+/// # Safety
+///
+/// `dst` must be valid for writes.
+pub unsafe fn write_volatile_u8(dst: *mut u8, src: u8) {
+    // SAFETY: strb only modifies *dst, which must be valid for writes.
+    unsafe {
+        core::arch::asm!(
+            "strb {value:w}, [{ptr}]",
+            value = in(reg) src,
+            ptr = in(reg) dst,
+            options(preserves_flags),
+        );
+    }
+}
diff --git a/libs/libvmbase/src/fdt.rs b/libs/libvmbase/src/fdt.rs
index 4101f7e..ff0eaf0 100644
--- a/libs/libvmbase/src/fdt.rs
+++ b/libs/libvmbase/src/fdt.rs
@@ -14,6 +14,8 @@
 
 //! High-level FDT functions.
 
+pub mod pci;
+
 use core::ops::Range;
 use cstr::cstr;
 use libfdt::{self, Fdt, FdtError};
diff --git a/libs/fdtpci/src/lib.rs b/libs/libvmbase/src/fdt/pci.rs
similarity index 99%
rename from libs/fdtpci/src/lib.rs
rename to libs/libvmbase/src/fdt/pci.rs
index bdd904f..ebaa671 100644
--- a/libs/fdtpci/src/lib.rs
+++ b/libs/libvmbase/src/fdt/pci.rs
@@ -14,8 +14,6 @@
 
 //! Library for working with (VirtIO) PCI devices discovered from a device tree.
 
-#![no_std]
-
 use core::{
     ffi::CStr,
     fmt::{self, Display, Formatter},
diff --git a/libs/libvmbase/src/uart.rs b/libs/libvmbase/src/uart.rs
index e35555d..427499b 100644
--- a/libs/libvmbase/src/uart.rs
+++ b/libs/libvmbase/src/uart.rs
@@ -15,6 +15,7 @@
 //! Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
 //! provided by crosvm, and won't work with real hardware.
 
+use crate::arch::write_volatile_u8;
 use core::fmt::{self, Write};
 
 /// Minimal driver for an 8250 UART. This only implements enough to work with the emulated 8250
@@ -39,13 +40,7 @@
     pub fn write_byte(&self, byte: u8) {
         // SAFETY: We know that the base address points to the control registers of a UART device
         // which is appropriately mapped.
-        unsafe {
-            core::arch::asm!(
-                "strb {value:w}, [{ptr}]",
-                value = in(reg) byte,
-                ptr = in(reg) self.base_address,
-            );
-        }
+        unsafe { write_volatile_u8(self.base_address, byte) }
     }
 }
 
diff --git a/libs/libvmbase/src/virtio/pci.rs b/libs/libvmbase/src/virtio/pci.rs
index 1d05c18..72e648b 100644
--- a/libs/libvmbase/src/virtio/pci.rs
+++ b/libs/libvmbase/src/virtio/pci.rs
@@ -14,11 +14,13 @@
 
 //! Functions to scan the PCI bus for VirtIO devices.
 
-use crate::memory::{MemoryTracker, MemoryTrackerError};
+use crate::{
+    fdt::pci::PciInfo,
+    memory::{MemoryTracker, MemoryTrackerError},
+};
 use alloc::boxed::Box;
 use core::fmt;
 use core::marker::PhantomData;
-use fdtpci::PciInfo;
 use log::debug;
 use once_cell::race::OnceBox;
 use virtio_drivers::{
diff --git a/libs/vbmeta/Android.bp b/libs/vbmeta/Android.bp
index 9a7375d..15c7b4a 100644
--- a/libs/vbmeta/Android.bp
+++ b/libs/vbmeta/Android.bp
@@ -31,7 +31,7 @@
         "libanyhow",
         "libtempfile",
     ],
-    data: [
+    device_common_data: [
         ":avb_testkey_rsa2048",
         ":avb_testkey_rsa4096",
         ":avb_testkey_rsa8192",
diff --git a/libs/vm_launcher_lib/Android.bp b/libs/vm_launcher_lib/Android.bp
index 5267508..f47f6b6 100644
--- a/libs/vm_launcher_lib/Android.bp
+++ b/libs/vm_launcher_lib/Android.bp
@@ -13,6 +13,7 @@
     static_libs: [
         "gson",
         "debian-service-grpclib-lite",
+        "apache-commons-compress",
     ],
     libs: [
         "framework-virtualization.impl",
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
index 6d39b46..5d6b13f 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/ConfigJson.java
@@ -17,6 +17,7 @@
 package com.android.virtualization.vmlauncher;
 
 import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Rect;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig;
@@ -25,6 +26,7 @@
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.DisplayConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.GpuConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.Partition;
+import android.system.virtualmachine.VirtualMachineCustomImageConfig.SharedPath;
 import android.util.DisplayMetrics;
 import android.view.WindowManager;
 import android.view.WindowMetrics;
@@ -34,6 +36,7 @@
 
 import java.io.FileReader;
 import java.util.Arrays;
+import java.util.Objects;
 
 /** This class and its inner classes model vm_config.json. */
 class ConfigJson {
@@ -60,6 +63,7 @@
     private InputJson input;
     private AudioJson audio;
     private DiskJson[] disks;
+    private SharedPathJson[] sharedPath;
     private DisplayJson display;
     private GpuJson gpu;
 
@@ -141,9 +145,36 @@
             Arrays.stream(disks).map(d -> d.toConfig()).forEach(builder::addDisk);
         }
 
+        if (sharedPath != null) {
+            Arrays.stream(sharedPath)
+                    .map(d -> d.toConfig(context))
+                    .filter(Objects::nonNull)
+                    .forEach(builder::addSharedPath);
+        }
         return builder.build();
     }
 
+    private static class SharedPathJson {
+        private SharedPathJson() {}
+
+        // Package ID of Terminal app.
+        private static final String TERMINAL_PACKAGE_ID =
+                "com.google.android.virtualization.terminal";
+        private String sharedPath;
+
+        private SharedPath toConfig(Context context) {
+            try {
+                int uid =
+                        context.getPackageManager()
+                                .getPackageUidAsUser(TERMINAL_PACKAGE_ID, context.getUserId());
+
+                return new SharedPath(sharedPath, uid, uid, 0, 0, 0007, "android", "android");
+            } catch (NameNotFoundException e) {
+                return null;
+            }
+        }
+    }
+
     private static class InputJson {
         private InputJson() {}
 
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
new file mode 100644
index 0000000..eb6dd77
--- /dev/null
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/InstallUtils.java
@@ -0,0 +1,141 @@
+/*
+ * 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.os.Environment;
+import android.util.Log;
+
+import org.apache.commons.compress.archivers.ArchiveEntry;
+import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
+import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+public class InstallUtils {
+    private static final String TAG = InstallUtils.class.getSimpleName();
+
+    private static final String VM_CONFIG_FILENAME = "vm_config.json";
+    private static final String COMPRESSED_PAYLOAD_FILENAME = "images.tar.gz";
+    private static final String PAYLOAD_DIR = "linux";
+
+    public static String getVmConfigPath(Context context) {
+        return new File(context.getFilesDir(), PAYLOAD_DIR)
+                .toPath()
+                .resolve(VM_CONFIG_FILENAME)
+                .toString();
+    }
+
+    public static boolean isImageInstalled(Context context) {
+        return Files.exists(Path.of(getVmConfigPath(context)));
+    }
+
+    private static Path getPayloadPath() {
+        File payloadDir = Environment.getExternalStoragePublicDirectory(PAYLOAD_DIR);
+        if (payloadDir == null) {
+            Log.d(TAG, "no payload dir: " + payloadDir);
+            return null;
+        }
+        Path payloadPath = payloadDir.toPath().resolve(COMPRESSED_PAYLOAD_FILENAME);
+        return payloadPath;
+    }
+
+    public static boolean payloadFromExternalStorageExists() {
+        return Files.exists(getPayloadPath());
+    }
+
+    public static boolean installImageFromExternalStorage(Context context) {
+        if (!payloadFromExternalStorageExists()) {
+            Log.d(TAG, "no artifact file from external storage");
+        }
+        Path payloadPath = getPayloadPath();
+        try (BufferedInputStream inputStream =
+                        new BufferedInputStream(Files.newInputStream(payloadPath));
+                TarArchiveInputStream tar =
+                        new TarArchiveInputStream(new GzipCompressorInputStream(inputStream))) {
+            ArchiveEntry entry;
+            Path baseDir = new File(context.getFilesDir(), PAYLOAD_DIR).toPath();
+            Files.createDirectories(baseDir);
+            while ((entry = tar.getNextEntry()) != null) {
+                Path extractTo = baseDir.resolve(entry.getName());
+                if (entry.isDirectory()) {
+                    Files.createDirectories(extractTo);
+                } else {
+                    Files.copy(tar, extractTo, StandardCopyOption.REPLACE_EXISTING);
+                }
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "installation failed", e);
+            return false;
+        }
+        if (!isImageInstalled(context)) {
+            return false;
+        }
+
+        if (!resolvePathInVmConfig(context)) {
+            Log.d(TAG, "resolving path failed");
+            try {
+                Files.deleteIfExists(Path.of(getVmConfigPath(context)));
+            } catch (IOException e) {
+                return false;
+            }
+            return false;
+        }
+
+        // remove payload if installation is done.
+        try {
+            Files.deleteIfExists(payloadPath);
+        } catch (IOException e) {
+            Log.d(TAG, "failed to remove installed payload", e);
+        }
+
+        return true;
+    }
+
+    private static Function<String, String> getReplacer(Context context) {
+        Map<String, String> rules = new HashMap<>();
+        rules.put("\\$PAYLOAD_DIR", new File(context.getFilesDir(), PAYLOAD_DIR).toString());
+        return (s) -> {
+            for (Map.Entry<String, String> rule : rules.entrySet()) {
+                s = s.replaceAll(rule.getKey(), rule.getValue());
+            }
+            return s;
+        };
+    }
+
+    private static boolean resolvePathInVmConfig(Context context) {
+        try {
+            String replacedVmConfig =
+                    String.join(
+                            "\n",
+                            Files.readAllLines(Path.of(getVmConfigPath(context))).stream()
+                                    .map(getReplacer(context))
+                                    .toList());
+            Files.write(Path.of(getVmConfigPath(context)), replacedVmConfig.getBytes());
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+}
diff --git a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
index 3d5c345..849cc24 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherService.java
@@ -17,8 +17,6 @@
 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;
@@ -39,9 +37,8 @@
 import java.util.concurrent.Executors;
 
 public class VmLauncherService extends Service implements DebianServiceImpl.DebianServiceCallback {
+    public static final String EXTRA_NOTIFICATION = "EXTRA_NOTIFICATION";
     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;
@@ -59,18 +56,8 @@
         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());
+    private void startForeground(Notification notification) {
+        startForeground(this.hashCode(), notification);
     }
 
     @Override
@@ -81,7 +68,7 @@
         }
         mExecutorService = Executors.newCachedThreadPool();
 
-        ConfigJson json = ConfigJson.from(VM_CONFIG_PATH);
+        ConfigJson json = ConfigJson.from(InstallUtils.getVmConfigPath(this));
         VirtualMachineConfig config = json.toConfig(this);
 
         Runner runner;
@@ -112,7 +99,10 @@
         Path logPath = getFileStreamPath(mVirtualMachine.getName() + ".log").toPath();
         Logger.setup(mVirtualMachine, logPath, mExecutorService);
 
-        startForeground();
+        Notification notification = intent.getParcelableExtra(EXTRA_NOTIFICATION,
+                Notification.class);
+
+        startForeground(notification);
 
         mResultReceiver.send(RESULT_START, null);
 
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
index 565b793..2fa0b32 100644
--- a/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
+++ b/libs/vm_launcher_lib/java/com/android/virtualization/vmlauncher/VmLauncherServices.java
@@ -16,6 +16,7 @@
 
 package com.android.virtualization.vmlauncher;
 
+import android.app.Notification;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -64,7 +65,8 @@
         context.stopService(i);
     }
 
-    public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback) {
+    public static void startVmLauncherService(Context context, VmLauncherServiceCallback callback,
+            Notification notification) {
         Intent i = buildVmLauncherServiceIntent(context);
         if (i == null) {
             return;
@@ -93,6 +95,7 @@
                     }
                 };
         i.putExtra(Intent.EXTRA_RESULT_RECEIVER, getResultReceiverForIntent(resultReceiver));
+        i.putExtra(VmLauncherService.EXTRA_NOTIFICATION, notification);
         context.startForegroundService(i);
     }
 
diff --git a/tests/authfs/benchmarks/Android.bp b/tests/authfs/benchmarks/Android.bp
index 27a6af1..aad8d59 100644
--- a/tests/authfs/benchmarks/Android.bp
+++ b/tests/authfs/benchmarks/Android.bp
@@ -19,7 +19,7 @@
         "open_then_run",
     ],
     per_testcase_directory: true,
-    data: [
+    device_common_data: [
         ":authfs_test_files",
         ":MicrodroidTestApp",
     ],
@@ -43,7 +43,7 @@
 java_genrule {
     name: "measure_io_as_jar",
     out: ["measure_io.jar"],
-    srcs: [
+    device_first_srcs: [
         ":measure_io",
     ],
     tools: ["soong_zip"],
diff --git a/tests/authfs/hosttests/Android.bp b/tests/authfs/hosttests/Android.bp
index 83ef853..50dbc05 100644
--- a/tests/authfs/hosttests/Android.bp
+++ b/tests/authfs/hosttests/Android.bp
@@ -20,6 +20,8 @@
     per_testcase_directory: true,
     data: [
         ":authfs_test_files",
+    ],
+    device_common_data: [
         ":MicrodroidTestApp",
     ],
 }
diff --git a/tests/benchmark_hostside/Android.bp b/tests/benchmark_hostside/Android.bp
index b613a8a..e91ac8f 100644
--- a/tests/benchmark_hostside/Android.bp
+++ b/tests/benchmark_hostside/Android.bp
@@ -18,7 +18,7 @@
     test_suites: [
         "general-tests",
     ],
-    data: [
+    device_common_data: [
         ":MicrodroidTestApp",
     ],
 }
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index d0838a6..0f2fe58 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -20,7 +20,7 @@
         "microdroid_payload_metadata",
     ],
     per_testcase_directory: true,
-    data: [
+    device_common_data: [
         ":MicrodroidTestApp",
         ":MicrodroidTestAppUpdated",
         ":microdroid_general_sepolicy.conf",
diff --git a/tests/pvmfw/Android.bp b/tests/pvmfw/Android.bp
index 0483066..e124e55 100644
--- a/tests/pvmfw/Android.bp
+++ b/tests/pvmfw/Android.bp
@@ -45,13 +45,17 @@
     ],
     per_testcase_directory: true,
     data: [
+        "assets/bcc.dat",
+    ],
+    device_common_data: [
         ":MicrodroidTestApp",
-        ":pvmfw_test",
         ":test_avf_debug_policy_with_ramdump",
         ":test_avf_debug_policy_without_ramdump",
         ":test_avf_debug_policy_with_adb",
         ":test_avf_debug_policy_without_adb",
-        "assets/bcc.dat",
+    ],
+    device_first_data: [
+        ":pvmfw_test",
     ],
     data_device_bins_first: ["dtc_static"],
 }
diff --git a/tests/vendor_images/Android.bp b/tests/vendor_images/Android.bp
index 66f0219..0430eaa 100644
--- a/tests/vendor_images/Android.bp
+++ b/tests/vendor_images/Android.bp
@@ -2,19 +2,13 @@
     default_applicable_licenses: ["Android-Apache-2.0"],
 }
 
-prebuilt_etc {
-    name: "vendor_sign_key",
-    src: ":avb_testkey_rsa4096",
-    installable: false,
-}
-
 android_filesystem {
     name: "test_microdroid_vendor_image",
     partition_name: "microdroid-vendor",
     type: "ext4",
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
-    avb_private_key: ":vendor_sign_key",
+    avb_private_key: ":avb_testkey_rsa4096",
     rollback_index: 5,
 }
 
@@ -24,7 +18,7 @@
     type: "ext4",
     file_contexts: ":microdroid_vendor_file_contexts.gen",
     use_avb: true,
-    avb_private_key: ":vendor_sign_key",
+    avb_private_key: ":avb_testkey_rsa4096",
 }
 
 android_filesystem {