Merge changes from topic "revert-3326198-revert-3323102-ZENOTQPEYR-LPYTEZBETE" into main

* changes:
  Reland "Update kernel to builds 12570979"
  Reland "vm tool: rename --gki arg to more generic --os one"
  Reland "Generalize our parameterized tests"
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
index af41c85..e278165 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/MainActivity.java
@@ -26,8 +26,11 @@
 import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.graphics.fonts.FontStyle;
+import android.net.Uri;
 import android.net.http.SslError;
 import android.os.Bundle;
+import android.os.Environment;
+import android.provider.Settings;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.Log;
@@ -44,6 +47,10 @@
 import android.webkit.WebViewClient;
 import android.widget.Toast;
 
+import androidx.activity.result.ActivityResult;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+
 import com.android.virtualization.vmlauncher.InstallUtils;
 import com.android.virtualization.vmlauncher.VmLauncherService;
 import com.android.virtualization.vmlauncher.VmLauncherServices;
@@ -81,6 +88,7 @@
     private WebView mWebView;
     private AccessibilityManager mAccessibilityManager;
     private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 101;
+    private ActivityResultLauncher<Intent> manageExternalStorageActivityResultLauncher;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -111,12 +119,41 @@
         connectToTerminalService();
         readClientCertificate();
 
+        manageExternalStorageActivityResultLauncher =
+                registerForActivityResult(
+                        new ActivityResultContracts.StartActivityForResult(),
+                        (ActivityResult result) -> {
+                            if (Environment.isExternalStorageManager()) {
+                                Toast.makeText(this, "Storage permission set!", Toast.LENGTH_SHORT)
+                                        .show();
+                            } else {
+                                Toast.makeText(
+                                                this,
+                                                "Storage permission not set",
+                                                Toast.LENGTH_SHORT)
+                                        .show();
+                            }
+                            startVm();
+                        });
+
         // if installer is launched, it will be handled in onActivityResult
         if (!launchInstaller) {
-            startVm();
+            if (!Environment.isExternalStorageManager()) {
+                requestStoragePermissions(this, manageExternalStorageActivityResultLauncher);
+            } else {
+                startVm();
+            }
         }
     }
 
+    private void requestStoragePermissions(
+            Context context, ActivityResultLauncher<Intent> activityResultLauncher) {
+        Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
+        Uri uri = Uri.fromParts("package", context.getPackageName(), null);
+        intent.setData(uri);
+        activityResultLauncher.launch(intent);
+    }
+
     private URL getTerminalServiceUrl() {
         Configuration config = getResources().getConfiguration();
 
@@ -406,7 +443,11 @@
                 Log.e(TAG, "Failed to start VM. Installer returned error.");
                 finish();
             }
-            startVm();
+            if (!Environment.isExternalStorageManager()) {
+                requestStoragePermissions(this, manageExternalStorageActivityResultLauncher);
+            } else {
+                startVm();
+            }
         }
     }
 
diff --git a/android/forwarder_host/Android.bp b/android/forwarder_host/Android.bp
new file mode 100644
index 0000000..35c478e
--- /dev/null
+++ b/android/forwarder_host/Android.bp
@@ -0,0 +1,21 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_binary {
+    name: "forwarder_host",
+    edition: "2021",
+    srcs: ["src/main.rs"],
+    rustlibs: [
+        "libforwarder",
+        "liblog_rust",
+        "libnix",
+        "libvmm_sys_util",
+        "libvsock",
+    ],
+    proc_macros: [
+        "libpoll_token_derive",
+        "libremain",
+    ],
+    static_executable: true,
+}
diff --git a/android/forwarder_host/src/main.rs b/android/forwarder_host/src/main.rs
new file mode 100644
index 0000000..b95b2cc
--- /dev/null
+++ b/android/forwarder_host/src/main.rs
@@ -0,0 +1,352 @@
+// 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.
+
+// Copied from ChromiumOS with relicensing:
+// src/platform2/vm_tools/chunnel/src/bin/chunneld.rs
+
+//! Host-side stream socket forwarder
+
+use std::collections::btree_map::Entry as BTreeMapEntry;
+use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
+use std::fmt;
+use std::io;
+use std::net::{Ipv4Addr, Ipv6Addr, TcpListener};
+use std::os::unix::io::AsRawFd;
+use std::result;
+use std::sync::{Arc, Mutex};
+use std::time::Duration;
+
+use forwarder::forwarder::ForwarderSession;
+use log::{error, warn};
+use nix::sys::eventfd::EventFd;
+use poll_token_derive::PollToken;
+use vmm_sys_util::poll::{PollContext, PollToken};
+use vsock::VsockListener;
+use vsock::VMADDR_CID_ANY;
+
+const CHUNNEL_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
+
+const VMADDR_PORT_ANY: u32 = u32::MAX;
+
+#[remain::sorted]
+#[derive(Debug)]
+enum Error {
+    BindVsock(io::Error),
+    EventFdNew(nix::Error),
+    IncorrectCid(u32),
+    NoListenerForPort(u16),
+    NoSessionForTag(SessionTag),
+    PollContextAdd(vmm_sys_util::errno::Error),
+    PollContextDelete(vmm_sys_util::errno::Error),
+    PollContextNew(vmm_sys_util::errno::Error),
+    PollWait(vmm_sys_util::errno::Error),
+    SetVsockNonblocking(io::Error),
+    TcpAccept(io::Error),
+    UpdateEventRead(nix::Error),
+    VsockAccept(io::Error),
+    VsockAcceptTimeout,
+}
+
+type Result<T> = result::Result<T, Error>;
+
+impl fmt::Display for Error {
+    #[remain::check]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::Error::*;
+
+        #[remain::sorted]
+        match self {
+            BindVsock(e) => write!(f, "failed to bind vsock: {}", e),
+            EventFdNew(e) => write!(f, "failed to create eventfd: {}", e),
+            IncorrectCid(cid) => write!(f, "chunnel connection from unexpected cid {}", cid),
+            NoListenerForPort(port) => write!(f, "could not find listener for port: {}", port),
+            NoSessionForTag(tag) => write!(f, "could not find session for tag: {:x}", tag),
+            PollContextAdd(e) => write!(f, "failed to add fd to poll context: {}", e),
+            PollContextDelete(e) => write!(f, "failed to delete fd from poll context: {}", e),
+            PollContextNew(e) => write!(f, "failed to create poll context: {}", e),
+            PollWait(e) => write!(f, "failed to wait for poll: {}", e),
+            SetVsockNonblocking(e) => write!(f, "failed to set vsock to nonblocking: {}", e),
+            TcpAccept(e) => write!(f, "failed to accept tcp: {}", e),
+            UpdateEventRead(e) => write!(f, "failed to read update eventfd: {}", e),
+            VsockAccept(e) => write!(f, "failed to accept vsock: {}", e),
+            VsockAcceptTimeout => write!(f, "timed out waiting for vsock connection"),
+        }
+    }
+}
+
+/// A TCP forwarding target. Uniquely identifies a listening port in a given container.
+struct TcpForwardTarget {
+    pub port: u16,
+    pub vsock_cid: u32,
+}
+
+/// A tag that uniquely identifies a particular forwarding session. This has arbitrarily been
+/// chosen as the fd of the local (TCP) socket.
+type SessionTag = u32;
+
+/// Implements PollToken for chunneld's main poll loop.
+#[derive(Clone, Copy, PollToken)]
+enum Token {
+    UpdatePorts,
+    Ipv4Listener(u16),
+    Ipv6Listener(u16),
+    LocalSocket(SessionTag),
+    RemoteSocket(SessionTag),
+}
+
+/// PortListeners includes all listeners (IPv4 and IPv6) for a given port, and the target
+/// container.
+struct PortListeners {
+    tcp4_listener: TcpListener,
+    tcp6_listener: TcpListener,
+    forward_target: TcpForwardTarget,
+}
+
+/// SocketFamily specifies whether a socket uses IPv4 or IPv6.
+enum SocketFamily {
+    Ipv4,
+    Ipv6,
+}
+
+/// ForwarderSessions encapsulates all forwarding state for chunneld.
+struct ForwarderSessions {
+    listening_ports: BTreeMap<u16, PortListeners>,
+    tcp4_forwarders: HashMap<SessionTag, ForwarderSession>,
+    update_evt: EventFd,
+    update_queue: Arc<Mutex<VecDeque<TcpForwardTarget>>>,
+}
+
+impl ForwarderSessions {
+    /// Creates a new instance of ForwarderSessions.
+    fn new(
+        update_evt: EventFd,
+        update_queue: Arc<Mutex<VecDeque<TcpForwardTarget>>>,
+    ) -> Result<Self> {
+        Ok(ForwarderSessions {
+            listening_ports: BTreeMap::new(),
+            tcp4_forwarders: HashMap::new(),
+            update_evt,
+            update_queue,
+        })
+    }
+
+    /// Adds or removes listeners based on the latest listening ports from the D-Bus thread.
+    fn process_update_queue(&mut self, poll_ctx: &PollContext<Token>) -> Result<()> {
+        // Unwrap of LockResult is customary.
+        let mut update_queue = self.update_queue.lock().unwrap();
+        let mut active_ports: BTreeSet<u16> = BTreeSet::new();
+
+        // Add any new listeners first.
+        while let Some(target) = update_queue.pop_front() {
+            let port = target.port;
+            // Ignore privileged ports.
+            if port < 1024 {
+                continue;
+            }
+            if let BTreeMapEntry::Vacant(o) = self.listening_ports.entry(port) {
+                // Failing to bind a port is not fatal, but we should log it.
+                // Both IPv4 and IPv6 localhost must be bound since the host may resolve
+                // "localhost" to either.
+                let tcp4_listener = match TcpListener::bind((Ipv4Addr::LOCALHOST, port)) {
+                    Ok(listener) => listener,
+                    Err(e) => {
+                        warn!("failed to bind TCPv4 port: {}", e);
+                        continue;
+                    }
+                };
+                let tcp6_listener = match TcpListener::bind((Ipv6Addr::LOCALHOST, port)) {
+                    Ok(listener) => listener,
+                    Err(e) => {
+                        warn!("failed to bind TCPv6 port: {}", e);
+                        continue;
+                    }
+                };
+                poll_ctx
+                    .add(&tcp4_listener, Token::Ipv4Listener(port))
+                    .map_err(Error::PollContextAdd)?;
+                poll_ctx
+                    .add(&tcp6_listener, Token::Ipv6Listener(port))
+                    .map_err(Error::PollContextAdd)?;
+                o.insert(PortListeners { tcp4_listener, tcp6_listener, forward_target: target });
+            }
+            active_ports.insert(port);
+        }
+
+        // Iterate over the existing listeners; if the port is no longer in the
+        // listener list, remove it.
+        let old_ports: Vec<u16> = self.listening_ports.keys().cloned().collect();
+        for port in old_ports.iter() {
+            if !active_ports.contains(port) {
+                // Remove the PortListeners struct first - on error we want to drop it and the
+                // fds it contains.
+                let _listening_port = self.listening_ports.remove(port);
+            }
+        }
+
+        // Consume the eventfd.
+        self.update_evt.read().map_err(Error::UpdateEventRead)?;
+
+        Ok(())
+    }
+
+    fn accept_connection(
+        &mut self,
+        poll_ctx: &PollContext<Token>,
+        port: u16,
+        sock_family: SocketFamily,
+    ) -> Result<()> {
+        let port_listeners =
+            self.listening_ports.get(&port).ok_or(Error::NoListenerForPort(port))?;
+
+        let listener = match sock_family {
+            SocketFamily::Ipv4 => &port_listeners.tcp4_listener,
+            SocketFamily::Ipv6 => &port_listeners.tcp6_listener,
+        };
+
+        // This session should be dropped if any of the PollContext setup fails. Since the only
+        // extant fds for the underlying sockets will be closed, they will be unregistered from
+        // epoll set automatically.
+        let session = create_forwarder_session(listener, &port_listeners.forward_target)?;
+
+        let tag = session.local_stream().as_raw_fd() as u32;
+
+        poll_ctx
+            .add(session.local_stream(), Token::LocalSocket(tag))
+            .map_err(Error::PollContextAdd)?;
+        poll_ctx
+            .add(session.remote_stream(), Token::RemoteSocket(tag))
+            .map_err(Error::PollContextAdd)?;
+
+        self.tcp4_forwarders.insert(tag, session);
+
+        Ok(())
+    }
+
+    fn forward_from_local(&mut self, poll_ctx: &PollContext<Token>, tag: SessionTag) -> Result<()> {
+        let session = self.tcp4_forwarders.get_mut(&tag).ok_or(Error::NoSessionForTag(tag))?;
+        let shutdown = session.forward_from_local().unwrap_or(true);
+        if shutdown {
+            poll_ctx.delete(session.local_stream()).map_err(Error::PollContextDelete)?;
+            if session.is_shut_down() {
+                self.tcp4_forwarders.remove(&tag);
+            }
+        }
+
+        Ok(())
+    }
+
+    fn forward_from_remote(
+        &mut self,
+        poll_ctx: &PollContext<Token>,
+        tag: SessionTag,
+    ) -> Result<()> {
+        let session = self.tcp4_forwarders.get_mut(&tag).ok_or(Error::NoSessionForTag(tag))?;
+        let shutdown = session.forward_from_remote().unwrap_or(true);
+        if shutdown {
+            poll_ctx.delete(session.remote_stream()).map_err(Error::PollContextDelete)?;
+            if session.is_shut_down() {
+                self.tcp4_forwarders.remove(&tag);
+            }
+        }
+
+        Ok(())
+    }
+
+    fn run(&mut self) -> Result<()> {
+        let poll_ctx: PollContext<Token> = PollContext::new().map_err(Error::PollContextNew)?;
+        poll_ctx.add(&self.update_evt, Token::UpdatePorts).map_err(Error::PollContextAdd)?;
+
+        loop {
+            let events = poll_ctx.wait().map_err(Error::PollWait)?;
+
+            for event in events.iter_readable() {
+                match event.token() {
+                    Token::UpdatePorts => {
+                        if let Err(e) = self.process_update_queue(&poll_ctx) {
+                            error!("error updating listening ports: {}", e);
+                        }
+                    }
+                    Token::Ipv4Listener(port) => {
+                        if let Err(e) = self.accept_connection(&poll_ctx, port, SocketFamily::Ipv4)
+                        {
+                            error!("error accepting connection: {}", e);
+                        }
+                    }
+                    Token::Ipv6Listener(port) => {
+                        if let Err(e) = self.accept_connection(&poll_ctx, port, SocketFamily::Ipv6)
+                        {
+                            error!("error accepting connection: {}", e);
+                        }
+                    }
+                    Token::LocalSocket(tag) => {
+                        if let Err(e) = self.forward_from_local(&poll_ctx, tag) {
+                            error!("error forwarding local traffic: {}", e);
+                        }
+                    }
+                    Token::RemoteSocket(tag) => {
+                        if let Err(e) = self.forward_from_remote(&poll_ctx, tag) {
+                            error!("error forwarding remote traffic: {}", e);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Creates a forwarder session from a `listener` that has a pending connection to accept.
+fn create_forwarder_session(
+    listener: &TcpListener,
+    target: &TcpForwardTarget,
+) -> Result<ForwarderSession> {
+    let (tcp_stream, _) = listener.accept().map_err(Error::TcpAccept)?;
+    // Bind a vsock port, tell the guest to connect, and accept the connection.
+    let vsock_listener = VsockListener::bind_with_cid_port(VMADDR_CID_ANY, VMADDR_PORT_ANY)
+        .map_err(Error::BindVsock)?;
+    vsock_listener.set_nonblocking(true).map_err(Error::SetVsockNonblocking)?;
+
+    #[derive(PollToken)]
+    enum Token {
+        VsockAccept,
+    }
+
+    let poll_ctx: PollContext<Token> = PollContext::new().map_err(Error::PollContextNew)?;
+    poll_ctx.add(&vsock_listener, Token::VsockAccept).map_err(Error::PollContextAdd)?;
+
+    // Wait a few seconds for the guest to connect.
+    let events = poll_ctx.wait_timeout(CHUNNEL_CONNECT_TIMEOUT).map_err(Error::PollWait)?;
+
+    match events.iter_readable().next() {
+        Some(_) => {
+            let (vsock_stream, sockaddr) = vsock_listener.accept().map_err(Error::VsockAccept)?;
+
+            if sockaddr.cid() != target.vsock_cid {
+                Err(Error::IncorrectCid(sockaddr.cid()))
+            } else {
+                Ok(ForwarderSession::new(tcp_stream.into(), vsock_stream.into()))
+            }
+        }
+        None => Err(Error::VsockAcceptTimeout),
+    }
+}
+
+// TODO(b/340126051): Host can receive opened ports from the guest.
+// TODO(b/340126051): Host can order executing chunnel on the guest.
+fn main() -> Result<()> {
+    let update_evt = EventFd::new().map_err(Error::EventFdNew)?;
+    let update_queue = Arc::new(Mutex::new(VecDeque::new()));
+
+    let mut sessions = ForwarderSessions::new(update_evt, update_queue)?;
+    sessions.run()
+}
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index b2283d0..b28834a 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -1244,6 +1244,9 @@
     }
 
     for shared_path in &config.shared_paths {
+        if let Err(e) = wait_for_file(&shared_path.socket_path, 5) {
+            bail!("Error waiting for file: {}", e);
+        }
         command
             .arg("--vhost-user-fs")
             .arg(format!("{},tag={}", &shared_path.socket_path, &shared_path.tag));
@@ -1269,6 +1272,23 @@
     Ok(result)
 }
 
+fn wait_for_file(path: &str, timeout_secs: u64) -> Result<(), std::io::Error> {
+    let start_time = std::time::Instant::now();
+    let timeout = Duration::from_secs(timeout_secs);
+
+    while start_time.elapsed() < timeout {
+        if std::fs::metadata(path).is_ok() {
+            return Ok(()); // File exists
+        }
+        thread::sleep(Duration::from_millis(100));
+    }
+
+    Err(std::io::Error::new(
+        std::io::ErrorKind::NotFound,
+        format!("File not found within {} seconds: {}", timeout_secs, path),
+    ))
+}
+
 /// Ensure that the configuration has a valid combination of fields set, or return an error if not.
 fn validate_config(config: &CrosvmConfig) -> Result<(), Error> {
     if config.bootloader.is_none() && config.kernel.is_none() {
diff --git a/build/debian/fai_config/files/etc/systemd/system/virtiofs.service/AVF b/build/debian/fai_config/files/etc/systemd/system/virtiofs.service/AVF
new file mode 100644
index 0000000..31ed059
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/virtiofs.service/AVF
@@ -0,0 +1,13 @@
+[Unit]
+Description=Mount virtiofs emulated file path
+After=network.target
+
+[Service]
+Type=oneshot
+User=root
+Group=root
+ExecStart=/bin/bash -c 'mkdir -p /mnt/shared; chown 1000:100 /mnt/shared; mount -t virtiofs android /mnt/shared'
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index 6a106c6..0886f72 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -6,3 +6,4 @@
 chmod +x $target/usr/local/bin/ttyd
 ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
 ln -s /etc/systemd/system/ip_addr_reporter.service $target/etc/systemd/system/multi-user.target.wants/ip_addr_reporter.service
+ln -s /etc/systemd/system/virtiofs.service $target/etc/systemd/system/multi-user.target.wants/virtiofs.service
diff --git a/build/debian/vm_config.json.aarch64 b/build/debian/vm_config.json.aarch64
index 9f9295c..5b7489e 100644
--- a/build/debian/vm_config.json.aarch64
+++ b/build/debian/vm_config.json.aarch64
@@ -7,6 +7,14 @@
             "writable": true
         }
     ],
+    "sharedPath": [
+        {
+            "sharedPath": "/storage/emulated"
+        },
+        {
+            "sharedPath": "/data/data/com.google.android.virtualization.terminal/files"
+        }
+    ],
     "protected": false,
     "cpu_topology": "match_host",
     "platform_version": "~1.0",
diff --git a/build/debian/vm_config.json.x86_64 b/build/debian/vm_config.json.x86_64
index 2fb9faa..8a491e4 100644
--- a/build/debian/vm_config.json.x86_64
+++ b/build/debian/vm_config.json.x86_64
@@ -7,6 +7,14 @@
             "writable": true
         }
     ],
+    "sharedPath": [
+        {
+            "sharedPath": "/storage/emulated"
+        },
+        {
+            "sharedPath": "/data/data/com.google.android.virtualization.terminal/files"
+        }
+    ],
     "kernel": "$PAYLOAD_DIR/vmlinuz",
     "initrd": "$PAYLOAD_DIR/initrd.img",
     "params": "root=/dev/vda1",
diff --git a/docs/img/pvm-dice-built-during-boot.png b/docs/img/pvm-dice-built-during-boot.png
new file mode 100644
index 0000000..6abd49a
--- /dev/null
+++ b/docs/img/pvm-dice-built-during-boot.png
Binary files differ
diff --git a/docs/img/pvm-dice-handover.png b/docs/img/pvm-dice-handover.png
new file mode 100644
index 0000000..8b3b592
--- /dev/null
+++ b/docs/img/pvm-dice-handover.png
Binary files differ
diff --git a/docs/img/pvm-dice.png b/docs/img/pvm-dice.png
deleted file mode 100644
index 5b26038..0000000
--- a/docs/img/pvm-dice.png
+++ /dev/null
Binary files differ
diff --git a/docs/pvm_dice_chain.md b/docs/pvm_dice_chain.md
index 67d1f28..68a67ab 100644
--- a/docs/pvm_dice_chain.md
+++ b/docs/pvm_dice_chain.md
@@ -1,25 +1,44 @@
 # pVM DICE Chain
 
-Unlike KeyMint, which only needs a vendor DICE chain, the pVM DICE
-chain combines the vendor's DICE chain with additional pVM DICE nodes
-describing the protected VM's environment.
+A VM [DICE][open-dice] chain is a cryptographically linked
+[certificates chain][cert-chain] that captures measurements of the VM's
+entire execution environment.
 
-![][pvm-dice-chain-img]
+This chain should be rooted in the device's ROM and encompass all components
+involved in the VM's loading and boot process. To achieve this, we typically
+extract measurements of all the components after verified boot at each stage
+of the boot process. These measurements are then used to derive a new DICE
+certificate describing the next boot stage.
 
-The full [RKP VM DICE chain][rkpvm-dice-chain], starting from `UDS_Pub`
-rooted in ROM, is sent to the RKP server during
-[pVM remote attestation][vm-attestation].
+![][pvm-dice-chain-built-img]
 
-[vm-attestation]: vm_remote_attestation.md
-[pvm-dice-chain-img]: img/pvm-dice.png
-[rkpvm-dice-chain]: vm_remote_attestation.md#rkp-vm-marker
+[pvm-dice-chain-built-img]: img/pvm-dice-built-during-boot.png
+[cert-chain]: https://en.wikipedia.org/wiki/Chain_of_trust
 
-## Key derivation
+## Vendor responsibility
+
+Vendors are responsible for constructing the first portion of the DICE chain,
+from ROM to the pvmfw loader (e.g., ABL). This portion describes the VM's
+loading environment. The final certificate in the vendor's chain must include
+measurements of pvmfw, the hypervisor, and any other code relevant to pvmfw's
+secure execution.
+
+## pVM DICE handover
+
+Vendors then pass this DICE chain, along with its corresponding
+[CDI values][dice-cdi], in a handover to pvmfw. The pVM takes over this
+handover and extends it with additional nodes describing its own execution
+environment.
+
+[dice-cdi]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/specification.md#cdi-values
+![][pvm-dice-handover-img]
+
+### Key derivation
 
 Key derivation is a critical step in the DICE handover process within
 [pvmfw][pvmfw]. Vendors need to ensure that both pvmfw and their final DICE
 node use the same method to derive a key pair from `CDI_Attest` in order to
-maintain a valid certificate chain. Pvmfw use [open-dice][open-dice] with the
+maintain a valid certificate chain. Pvmfw uses [open-dice][open-dice] with the
 following formula:
 
 ```
@@ -35,7 +54,17 @@
 compatibility and chain integrity.
 
 [pvmfw]: ../guest/pvmfw
-[open-dice]: https://cs.android.com/android/platform/superproject/main/+/main:external/open-dice/
+[pvm-dice-handover-img]: img/pvm-dice-handover.png
+[open-dice]: https://android.googlesource.com/platform/external/open-dice/+/main/docs/specification.md
+
+## Validation
+
+While pvmfw and the Microdroid OS extend the VM DICE chain, they don't
+perform comprehensive validation of the chain's structure or its ROM-rooted
+origin. The [VM Remote Attestation][vm-attestation] feature is specifically
+designed to ensure the validity and ROM-rooted nature of a VM DICE chain.
+
+[vm-attestation]: vm_remote_attestation.md
 
 ## Testing
 
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 5d6b13f..a259fe2 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
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Rect;
+import android.os.Environment;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig;
 import android.system.virtualmachine.VirtualMachineCustomImageConfig.AudioConfig;
@@ -157,22 +158,40 @@
     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 static final int GUEST_UID = 1000;
+        private static final int GUEST_GID = 100;
 
         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");
+                int terminalUid = getTerminalUid(context);
+                if (sharedPath.contains("emulated")) {
+                    if (Environment.isExternalStorageManager()) {
+                        int currentUserId = context.getUserId();
+                        String path = sharedPath + "/" + currentUserId + "/Download";
+                        return new SharedPath(
+                                path,
+                                terminalUid,
+                                terminalUid,
+                                GUEST_UID,
+                                GUEST_GID,
+                                0007,
+                                "android",
+                                "android");
+                    }
+                    return null;
+                }
+                return new SharedPath(
+                        sharedPath, terminalUid, terminalUid, 0, 0, 0007, "internal", "internal");
             } catch (NameNotFoundException e) {
                 return null;
             }
         }
+
+        private int getTerminalUid(Context context) throws NameNotFoundException {
+            return context.getPackageManager()
+                    .getPackageUidAsUser(context.getPackageName(), context.getUserId());
+        }
     }
 
     private static class InputJson {