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 {