Merge "vm: DRY *Config structs w.r.t. aflags using cfg_if" into main
diff --git a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
index 9748ce2..c543b2a 100755
--- a/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
+++ b/android/LinuxInstaller/linux_image_builder/setup_x86_64.sh
@@ -4,7 +4,7 @@
tempdir=$(mktemp -d)
echo Get Debian image and dependencies...
wget https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-nocloud-amd64.raw -O ${tempdir}/debian.img
-wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.tyd.x86_64 -O ${tempdir}/ttyd
+wget https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 -O ${tempdir}/ttyd
echo Customize the image...
virt-customize --commands-from-file <(sed "s|/tmp|$tempdir|g" commands) -a ${tempdir}/debian.img
diff --git a/android/fd_server/Android.bp b/android/fd_server/Android.bp
index 32a8fec..b02c104 100644
--- a/android/fd_server/Android.bp
+++ b/android/fd_server/Android.bp
@@ -18,7 +18,6 @@
"liblog_rust",
"libnix",
"librpcbinder_rs",
- "libsafe_ownedfd",
],
prefer_rlib: true,
apex_available: ["com.android.virt"],
@@ -40,7 +39,6 @@
"liblog_rust",
"libnix",
"librpcbinder_rs",
- "libsafe_ownedfd",
],
prefer_rlib: true,
test_suites: ["general-tests"],
diff --git a/android/fd_server/src/aidl.rs b/android/fd_server/src/aidl.rs
index 2f3697c..5f91987 100644
--- a/android/fd_server/src/aidl.rs
+++ b/android/fd_server/src/aidl.rs
@@ -14,21 +14,20 @@
* limitations under the License.
*/
-use anyhow::{Context, Result};
+use anyhow::Result;
use log::error;
use nix::{
errno::Errno, fcntl::openat, fcntl::OFlag, sys::stat::fchmod, sys::stat::mkdirat,
sys::stat::mode_t, sys::stat::Mode, sys::statvfs::statvfs, sys::statvfs::Statvfs,
unistd::unlinkat, unistd::UnlinkatFlags,
};
-use safe_ownedfd::take_fd_ownership;
use std::cmp::min;
use std::collections::{btree_map, BTreeMap};
use std::convert::TryInto;
use std::fs::File;
use std::io;
use std::os::unix::fs::FileExt;
-use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd};
+use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::path::{Component, Path, PathBuf, MAIN_SEPARATOR};
use std::sync::{Arc, RwLock};
@@ -39,8 +38,7 @@
get_fsverity_metadata_path, parse_fsverity_metadata, FSVerityMetadata,
};
use binder::{
- BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Result as BinderResult, Status,
- StatusCode, Strong,
+ BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, StatusCode, Strong,
};
/// Bitflags of forbidden file mode, e.g. setuid, setgid and sticky bit.
@@ -301,11 +299,9 @@
mode,
)
.map_err(new_errno_error)?;
- let new_fd = take_fd_ownership(new_fd)
- .context("Failed to take ownership of fd for file")
- .or_service_specific_exception(-1)?;
- let new_file = File::from(new_fd);
- Ok((new_file.as_raw_fd(), FdConfig::ReadWrite(new_file)))
+ // SAFETY: new_fd is just created and not an error.
+ let new_file = unsafe { File::from_raw_fd(new_fd) };
+ Ok((new_fd, FdConfig::ReadWrite(new_file)))
}
_ => Err(new_errno_error(Errno::ENOTDIR)),
})
@@ -331,10 +327,9 @@
Mode::empty(),
)
.map_err(new_errno_error)?;
- let fd_owner = take_fd_ownership(new_dir_fd)
- .context("Failed to take ownership of the fd for directory")
- .or_service_specific_exception(-1)?;
- Ok((fd_owner.as_raw_fd(), FdConfig::OutputDir(fd_owner)))
+ // SAFETY: new_dir_fd is just created and not an error.
+ let fd_owner = unsafe { OwnedFd::from_raw_fd(new_dir_fd) };
+ Ok((new_dir_fd, FdConfig::OutputDir(fd_owner)))
}
_ => Err(new_errno_error(Errno::ENOTDIR)),
})
@@ -413,11 +408,9 @@
fn open_readonly_at(dir_fd: BorrowedFd, path: &Path) -> nix::Result<File> {
let new_fd = openat(Some(dir_fd.as_raw_fd()), path, OFlag::O_RDONLY, Mode::empty())?;
- let new_fd = take_fd_ownership(new_fd).map_err(|e| match e {
- safe_ownedfd::Error::Errno(e) => e,
- _ => Errno::UnknownErrno,
- })?;
- Ok(File::from(new_fd))
+ // SAFETY: new_fd is just created successfully and not owned.
+ let new_file = unsafe { File::from_raw_fd(new_fd) };
+ Ok(new_file)
}
fn validate_and_cast_offset(offset: i64) -> Result<u64, Status> {
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index 62ff8d8..f0b6881 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -54,7 +54,6 @@
"libregex",
"librpcbinder_rs",
"librustutils",
- "libsafe_ownedfd",
"libsemver",
"libselinux_bindgen",
"libserde",
@@ -95,6 +94,13 @@
apex_available: ["com.android.virt"],
}
+xsd_config {
+ name: "early_vms",
+ srcs: ["early_vms.xsd"],
+ api_dir: "schema",
+ package_name: "android.system.virtualizationservice",
+}
+
rust_test {
name: "virtualizationmanager_device_test",
srcs: ["src/main.rs"],
diff --git a/android/virtmgr/early_vms.xsd b/android/virtmgr/early_vms.xsd
new file mode 100644
index 0000000..14dbf7b
--- /dev/null
+++ b/android/virtmgr/early_vms.xsd
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- KEEP IN SYNC WITH aidl.rs -->
+<xs:schema version="2.0"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:element name="early_vms">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element name="early_vm" type="early_vm" minOccurs="0" maxOccurs="unbounded"/>
+ </xs:sequence>
+ </xs:complexType>
+ </xs:element>
+ <xs:complexType name="early_vm">
+ <!-- Name of the VM, which will be passed to VirtualMachineConfig. -->
+ <xs:attribute name="name" type="xs:string"/>
+ <!-- CID of the VM. Available ranges:
+ * system: 100 ~ 199
+ * system_ext / product: 200 ~ 299
+ * vendor / odm: 300 ~ 399
+ -->
+ <xs:attribute name="cid" type="xs:int"/>
+ <!-- Absolute file path of the client executable running the VM. -->
+ <xs:attribute name="path" type="xs:string"/>
+ </xs:complexType>
+</xs:schema>
diff --git a/android/virtmgr/schema/current.txt b/android/virtmgr/schema/current.txt
new file mode 100644
index 0000000..b21c909
--- /dev/null
+++ b/android/virtmgr/schema/current.txt
@@ -0,0 +1,27 @@
+// Signature format: 2.0
+package android.system.virtualizationservice {
+
+ public class EarlyVm {
+ ctor public EarlyVm();
+ method public int getCid();
+ method public String getName();
+ method public String getPath();
+ method public void setCid(int);
+ method public void setName(String);
+ method public void setPath(String);
+ }
+
+ public class EarlyVms {
+ ctor public EarlyVms();
+ method public java.util.List<android.system.virtualizationservice.EarlyVm> getEarly_vm();
+ }
+
+ public class XmlParser {
+ ctor public XmlParser();
+ method public static android.system.virtualizationservice.EarlyVms read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ }
+
+}
+
diff --git a/android/virtmgr/schema/last_current.txt b/android/virtmgr/schema/last_current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/virtmgr/schema/last_current.txt
diff --git a/android/virtmgr/schema/last_removed.txt b/android/virtmgr/schema/last_removed.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/android/virtmgr/schema/last_removed.txt
diff --git a/android/virtmgr/schema/removed.txt b/android/virtmgr/schema/removed.txt
new file mode 100644
index 0000000..d802177
--- /dev/null
+++ b/android/virtmgr/schema/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/android/virtmgr/src/aidl.rs b/android/virtmgr/src/aidl.rs
index fb3d353..87fb611 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, VmContext, VmInstance, VmState};
+use crate::crosvm::{AudioConfig, CrosvmConfig, DiskFile, 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};
@@ -45,6 +45,7 @@
VirtualMachineRawConfig::VirtualMachineRawConfig,
VirtualMachineState::VirtualMachineState,
};
+use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal::IVirtualizationServiceInternal;
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::{
BnVirtualMachineService, IVirtualMachineService,
@@ -72,17 +73,18 @@
use nix::unistd::pipe;
use rpcbinder::RpcServer;
use rustutils::system_properties;
-use safe_ownedfd::take_fd_ownership;
use semver::VersionReq;
+use serde::Deserialize;
use std::collections::HashSet;
use std::convert::TryInto;
use std::fs;
use std::ffi::CStr;
-use std::fs::{canonicalize, read_dir, remove_file, File, OpenOptions};
+use std::fs::{canonicalize, create_dir_all, read_dir, remove_dir_all, remove_file, File, OpenOptions};
use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
use std::iter;
use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{AsRawFd, IntoRawFd};
+use std::ops::Range;
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd};
use std::os::unix::raw::pid_t;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, Weak, LazyLock};
@@ -120,8 +122,12 @@
pub static GLOBAL_SERVICE: LazyLock<Strong<dyn IVirtualizationServiceInternal>> =
LazyLock::new(|| {
- wait_for_interface(BINDER_SERVICE_IDENTIFIER)
- .expect("Could not connect to VirtualizationServiceInternal")
+ if cfg!(early) {
+ panic!("Early virtmgr must not connect to VirtualizatinoServiceInternal")
+ } else {
+ wait_for_interface(BINDER_SERVICE_IDENTIFIER)
+ .expect("Could not connect to VirtualizationServiceInternal")
+ }
});
static SUPPORTED_OS_NAMES: LazyLock<HashSet<String>> =
LazyLock::new(|| get_supported_os_names().expect("Failed to get list of supported os names"));
@@ -340,11 +346,110 @@
}
}
+/// Implementation of the AIDL `IGlobalVmContext` interface for early VMs.
+#[derive(Debug, Default)]
+struct EarlyVmContext {
+ /// The unique CID assigned to the VM for vsock communication.
+ cid: Cid,
+ /// Temporary directory for this VM instance.
+ temp_dir: PathBuf,
+}
+
+impl EarlyVmContext {
+ fn new(cid: Cid, temp_dir: PathBuf) -> Result<Self> {
+ // Remove the entire directory before creating a VM. Early VMs use predefined CIDs and AVF
+ // should trust clients, e.g. they won't run two VMs at the same time
+ let _ = remove_dir_all(&temp_dir);
+ create_dir_all(&temp_dir).context(format!("can't create '{}'", temp_dir.display()))?;
+
+ Ok(Self { cid, temp_dir })
+ }
+}
+
+impl Interface for EarlyVmContext {}
+
+impl Drop for EarlyVmContext {
+ fn drop(&mut self) {
+ if let Err(e) = remove_dir_all(&self.temp_dir) {
+ error!("Cannot remove {} upon dropping: {e}", self.temp_dir.display());
+ }
+ }
+}
+
+impl IGlobalVmContext for EarlyVmContext {
+ fn getCid(&self) -> binder::Result<i32> {
+ Ok(self.cid as i32)
+ }
+
+ fn getTemporaryDirectory(&self) -> binder::Result<String> {
+ Ok(self.temp_dir.to_string_lossy().to_string())
+ }
+
+ fn setHostConsoleName(&self, _pathname: &str) -> binder::Result<()> {
+ Err(Status::new_exception_str(
+ ExceptionCode::UNSUPPORTED_OPERATION,
+ Some("Early VM doesn't support setting host console name"),
+ ))
+ }
+}
+
+fn find_partition(path: &Path) -> binder::Result<String> {
+ match path.components().nth(1) {
+ Some(std::path::Component::Normal(partition)) => {
+ Ok(partition.to_string_lossy().into_owned())
+ }
+ _ => Err(anyhow!("Can't find partition in '{}'", path.display()))
+ .or_service_specific_exception(-1),
+ }
+}
+
impl VirtualizationService {
pub fn init() -> VirtualizationService {
VirtualizationService::default()
}
+ fn create_early_vm_context(
+ &self,
+ config: &VirtualMachineConfig,
+ ) -> binder::Result<(VmContext, Cid, PathBuf)> {
+ let calling_exe_path = format!("/proc/{}/exe", get_calling_pid());
+ let link = fs::read_link(&calling_exe_path)
+ .context(format!("can't read_link '{calling_exe_path}'"))
+ .or_service_specific_exception(-1)?;
+ let partition = find_partition(&link)?;
+
+ let name = match config {
+ VirtualMachineConfig::RawConfig(config) => &config.name,
+ VirtualMachineConfig::AppConfig(config) => &config.name,
+ };
+ let early_vm =
+ find_early_vm_for_partition(&partition, name).or_service_specific_exception(-1)?;
+ if Path::new(&early_vm.path) != link {
+ return Err(anyhow!(
+ "VM '{name}' in partition '{partition}' must be created with '{}', not '{}'",
+ &early_vm.path,
+ link.display()
+ ))
+ .or_service_specific_exception(-1);
+ }
+
+ let cid = early_vm.cid as Cid;
+ let temp_dir = PathBuf::from(format!("/mnt/vm/early/{cid}"));
+
+ let context = EarlyVmContext::new(cid, temp_dir.clone())
+ .context(format!("Can't create early vm contexts for {cid}"))
+ .or_service_specific_exception(-1)?;
+ let service = VirtualMachineService::new_binder(self.state.clone(), cid).as_binder();
+
+ // Start VM service listening for connections from the new CID on port=CID.
+ let port = cid;
+ let vm_server = RpcServer::new_vsock(service, cid, port)
+ .context(format!("Could not start RpcServer on port {port}"))
+ .or_service_specific_exception(-1)?;
+ vm_server.start();
+ Ok((VmContext::new(Strong::new(Box::new(context)), vm_server), cid, temp_dir))
+ }
+
fn create_vm_context(
&self,
requester_debug_pid: pid_t,
@@ -386,8 +491,16 @@
check_config_features(config)?;
+ if cfg!(early) {
+ check_config_allowed_for_early_vms(config)?;
+ }
+
// Allocating VM context checks the MANAGE_VIRTUAL_MACHINE permission.
- let (vm_context, cid, temporary_directory) = self.create_vm_context(requester_debug_pid)?;
+ let (vm_context, cid, temporary_directory) = if cfg!(early) {
+ self.create_early_vm_context(config)?
+ } else {
+ self.create_vm_context(requester_debug_pid)?
+ };
if is_custom_config(config) {
check_use_custom_virtual_machine()?;
@@ -584,6 +697,13 @@
None
};
+ let usb_config = config
+ .usbConfig
+ .as_ref()
+ .map(UsbConfig::new)
+ .unwrap_or(Ok(UsbConfig { controller: false }))
+ .or_binder_exception(ExceptionCode::BAD_PARCELABLE)?;
+
// Actually start the VM.
let crosvm_config = CrosvmConfig {
cid,
@@ -623,6 +743,7 @@
gpu_config,
audio_config,
no_balloon: config.noBalloon,
+ usb_config,
};
let instance = Arc::new(
VmInstance::new(
@@ -1110,6 +1231,10 @@
/// Checks whether the caller has a specific permission
fn check_permission(perm: &str) -> binder::Result<()> {
+ if cfg!(early) {
+ // Skip permission check for early VMs, in favor of SELinux
+ return Ok(());
+ }
let calling_pid = get_calling_pid();
let calling_uid = get_calling_uid();
// Root can do anything
@@ -1279,7 +1404,7 @@
let stream = VsockStream::connect_with_cid_port(self.instance.cid, port)
.context("Failed to connect")
.or_service_specific_exception(-1)?;
- vsock_stream_to_pfd(stream)
+ Ok(vsock_stream_to_pfd(stream))
}
fn setHostConsoleName(&self, ptsname: &str) -> binder::Result<()> {
@@ -1438,12 +1563,10 @@
}
/// Converts a `VsockStream` to a `ParcelFileDescriptor`.
-fn vsock_stream_to_pfd(stream: VsockStream) -> binder::Result<ParcelFileDescriptor> {
- let owned_fd = take_fd_ownership(stream.into_raw_fd())
- .context("Failed to take ownership of the vsock stream")
- .with_log()
- .or_service_specific_exception(-1)?;
- Ok(ParcelFileDescriptor::new(owned_fd))
+fn vsock_stream_to_pfd(stream: VsockStream) -> ParcelFileDescriptor {
+ // SAFETY: ownership is transferred from stream to f
+ let f = unsafe { File::from_raw_fd(stream.into_raw_fd()) };
+ ParcelFileDescriptor::new(f)
}
/// Parses the platform version requirement string.
@@ -1583,6 +1706,13 @@
Ok(())
}
+fn check_config_allowed_for_early_vms(config: &VirtualMachineConfig) -> binder::Result<()> {
+ check_no_vendor_modules(config)?;
+ check_no_devices(config)?;
+
+ Ok(())
+}
+
fn clone_or_prepare_logger_fd(
fd: Option<&ParcelFileDescriptor>,
tag: String,
@@ -1796,6 +1926,74 @@
}
}
+// KEEP IN SYNC WITH early_vms.xsd
+#[derive(Debug, Deserialize, PartialEq)]
+struct EarlyVm {
+ #[allow(dead_code)]
+ name: String,
+ #[allow(dead_code)]
+ cid: i32,
+ #[allow(dead_code)]
+ path: String,
+}
+
+#[derive(Debug, Default, Deserialize)]
+struct EarlyVms {
+ #[allow(dead_code)]
+ early_vm: Vec<EarlyVm>,
+}
+
+fn range_for_partition(partition: &str) -> Result<Range<Cid>> {
+ match partition {
+ "system" => Ok(100..200),
+ "system_ext" | "product" => Ok(200..300),
+ _ => Err(anyhow!("Early VMs are not supported for {partition}")),
+ }
+}
+
+fn find_early_vm(xml_path: &Path, cid_range: &Range<Cid>, name: &str) -> Result<EarlyVm> {
+ if !xml_path.exists() {
+ bail!("{} doesn't exist", xml_path.display());
+ }
+
+ let xml =
+ fs::read(xml_path).with_context(|| format!("Failed to read {}", xml_path.display()))?;
+ let xml = String::from_utf8(xml)
+ .with_context(|| format!("{} is not a valid UTF-8 file", xml_path.display()))?;
+ let early_vms: EarlyVms = serde_xml_rs::from_str(&xml)
+ .with_context(|| format!("Can't parse {}", xml_path.display()))?;
+
+ let mut found_vm: Option<EarlyVm> = None;
+
+ for early_vm in early_vms.early_vm {
+ if early_vm.name != name {
+ continue;
+ }
+
+ let cid = early_vm
+ .cid
+ .try_into()
+ .with_context(|| format!("Invalid CID value {}", early_vm.cid))?;
+
+ if !cid_range.contains(&cid) {
+ bail!("VM '{}' uses CID {cid} which is out of range. Available CIDs for '{}': {cid_range:?}", xml_path.display(), early_vm.name);
+ }
+
+ if found_vm.is_some() {
+ bail!("Multiple VMs named {name} are found in {}", xml_path.display());
+ }
+
+ found_vm = Some(early_vm);
+ }
+
+ found_vm.ok_or_else(|| anyhow!("Can't find {name} in {}", xml_path.display()))
+}
+
+fn find_early_vm_for_partition(partition: &str, name: &str) -> Result<EarlyVm> {
+ let cid_range = range_for_partition(partition)?;
+ find_early_vm(Path::new(&format!("/{partition}/etc/avf/early_vms.xml")), &cid_range, name)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -2011,4 +2209,69 @@
}
Ok(())
}
+
+ #[test]
+ fn test_find_early_vms_from_xml() -> Result<()> {
+ let tmp_dir = tempfile::TempDir::new()?;
+ let tmp_dir_path = tmp_dir.path().to_owned();
+ let xml_path = tmp_dir_path.join("early_vms.xml");
+
+ std::fs::write(
+ &xml_path,
+ br#"<?xml version="1.0" encoding="utf-8"?>
+ <early_vms>
+ <early_vm>
+ <name>vm_demo_native_early</name>
+ <cid>123</cid>
+ <path>/system/bin/vm_demo_native_early</path>
+ </early_vm>
+ <early_vm>
+ <name>vm_demo_duplicated_name</name>
+ <cid>456</cid>
+ <path>/system/bin/vm_demo_duplicated_name_1</path>
+ </early_vm>
+ <early_vm>
+ <name>vm_demo_duplicated_name</name>
+ <cid>789</cid>
+ <path>/system/bin/vm_demo_duplicated_name_2</path>
+ </early_vm>
+ <early_vm>
+ <name>vm_demo_invalid_cid_1</name>
+ <cid>-1</cid>
+ <path>/system/bin/vm_demo_invalid_cid_1</path>
+ </early_vm>
+ <early_vm>
+ <name>vm_demo_invalid_cid_2</name>
+ <cid>999999</cid>
+ <path>/system/bin/vm_demo_invalid_cid_2</path>
+ </early_vm>
+ </early_vms>
+ "#,
+ )?;
+
+ let cid_range = 100..1000;
+
+ let result = find_early_vm(&xml_path, &cid_range, "vm_demo_native_early")?;
+ let expected = EarlyVm {
+ name: "vm_demo_native_early".to_owned(),
+ cid: 123,
+ path: "/system/bin/vm_demo_native_early".to_owned(),
+ };
+ assert_eq!(result, expected);
+
+ assert!(
+ find_early_vm(&xml_path, &cid_range, "vm_demo_duplicated_name").is_err(),
+ "should fail"
+ );
+ assert!(
+ find_early_vm(&xml_path, &cid_range, "vm_demo_invalid_cid_1").is_err(),
+ "should fail"
+ );
+ assert!(
+ find_early_vm(&xml_path, &cid_range, "vm_demo_invalid_cid_2").is_err(),
+ "should fail"
+ );
+
+ Ok(())
+ }
}
diff --git a/android/virtmgr/src/atom.rs b/android/virtmgr/src/atom.rs
index 1d2d191..45b020e 100644
--- a/android/virtmgr/src/atom.rs
+++ b/android/virtmgr/src/atom.rs
@@ -99,6 +99,10 @@
is_protected: bool,
ret: &binder::Result<Strong<dyn IVirtualMachine>>,
) {
+ if cfg!(early) {
+ info!("Writing VmCreationRequested atom for early VMs is not implemented; skipping");
+ return;
+ }
let creation_succeeded;
let binder_exception_code;
match ret {
@@ -165,6 +169,11 @@
vm_identifier: &str,
vm_start_timestamp: Option<SystemTime>,
) {
+ if cfg!(early) {
+ info!("Writing VmCreationRequested atom for early VMs is not implemented; skipping");
+ return;
+ }
+
let vm_identifier = vm_identifier.to_owned();
let duration = get_duration(vm_start_timestamp);
@@ -190,6 +199,10 @@
exit_signal: Option<i32>,
vm_metric: &VmMetric,
) {
+ if cfg!(early) {
+ info!("Writing VmExited atom for early VMs is not implemented; skipping");
+ return;
+ }
let vm_identifier = vm_identifier.to_owned();
let elapsed_time_millis = get_duration(vm_metric.start_timestamp).as_millis() as i64;
let guest_time_millis = vm_metric.cpu_guest_time.unwrap_or_default();
diff --git a/android/virtmgr/src/crosvm.rs b/android/virtmgr/src/crosvm.rs
index 9d688a2..5886535 100644
--- a/android/virtmgr/src/crosvm.rs
+++ b/android/virtmgr/src/crosvm.rs
@@ -47,6 +47,7 @@
AudioConfig::AudioConfig as AudioConfigParcelable,
DisplayConfig::DisplayConfig as DisplayConfigParcelable,
GpuConfig::GpuConfig as GpuConfigParcelable,
+ UsbConfig::UsbConfig as UsbConfigParcelable,
};
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IGlobalVmContext::IGlobalVmContext;
use android_system_virtualizationservice_internal::aidl::android::system::virtualizationservice_internal::IBoundDevice::IBoundDevice;
@@ -134,6 +135,7 @@
pub gpu_config: Option<GpuConfig>,
pub audio_config: Option<AudioConfig>,
pub no_balloon: bool,
+ pub usb_config: UsbConfig,
}
#[derive(Debug)]
@@ -149,6 +151,17 @@
}
#[derive(Debug)]
+pub struct UsbConfig {
+ pub controller: bool,
+}
+
+impl UsbConfig {
+ pub fn new(raw_config: &UsbConfigParcelable) -> Result<UsbConfig> {
+ Ok(UsbConfig { controller: raw_config.controller })
+ }
+}
+
+#[derive(Debug)]
pub struct DisplayConfig {
pub width: NonZeroU32,
pub height: NonZeroU32,
@@ -900,6 +913,10 @@
command.arg("--no-balloon");
}
+ if !config.usb_config.controller {
+ command.arg("--no-usb");
+ }
+
let mut memory_mib = config.memory_mib;
if config.protected {
diff --git a/android/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index a4e75a7..67e7282 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -25,16 +25,16 @@
use crate::aidl::{GLOBAL_SERVICE, VirtualizationService};
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::IVirtualizationService::BnVirtualizationService;
-use anyhow::{bail, Result};
+use anyhow::{bail, Context, Result};
use binder::{BinderFeatures, ProcessState};
use log::{info, LevelFilter};
use rpcbinder::{FileDescriptorTransportMode, RpcServer};
-use std::os::unix::io::{AsFd, RawFd};
+use std::os::unix::io::{AsFd, FromRawFd, OwnedFd, RawFd};
use std::sync::LazyLock;
use clap::Parser;
+use nix::fcntl::{fcntl, F_GETFD, F_SETFD, FdFlag};
use nix::unistd::{write, Pid, Uid};
use std::os::unix::raw::{pid_t, uid_t};
-use safe_ownedfd::take_fd_ownership;
const LOG_TAG: &str = "virtmgr";
@@ -71,6 +71,32 @@
ready_fd: RawFd,
}
+fn take_fd_ownership(raw_fd: RawFd, owned_fds: &mut Vec<RawFd>) -> Result<OwnedFd, anyhow::Error> {
+ // Basic check that the integer value does correspond to a file descriptor.
+ fcntl(raw_fd, F_GETFD).with_context(|| format!("Invalid file descriptor {raw_fd}"))?;
+
+ // The file descriptor had CLOEXEC disabled to be inherited from the parent.
+ // Re-enable it to make sure it is not accidentally inherited further.
+ fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC))
+ .with_context(|| format!("Could not set CLOEXEC on file descriptor {raw_fd}"))?;
+
+ // Creating OwnedFd for stdio FDs is not safe.
+ if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+ bail!("File descriptor {raw_fd} is standard I/O descriptor");
+ }
+
+ // Reject RawFds that already have a corresponding OwnedFd.
+ if owned_fds.contains(&raw_fd) {
+ bail!("File descriptor {raw_fd} already owned");
+ }
+ owned_fds.push(raw_fd);
+
+ // SAFETY: Initializing OwnedFd for a RawFd provided in cmdline arguments.
+ // We checked that the integer value corresponds to a valid FD and that this
+ // is the first argument to claim its ownership.
+ Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
+}
+
fn check_vm_support() -> Result<()> {
if hypervisor_props::is_any_vm_supported()? {
Ok(())
@@ -94,15 +120,27 @@
let args = Args::parse();
- let rpc_server_fd =
- take_fd_ownership(args.rpc_server_fd).expect("Failed to take ownership of rpc_server_fd");
- let ready_fd = take_fd_ownership(args.ready_fd).expect("Failed to take ownership of ready_fd");
+ let mut owned_fds = vec![];
+ let rpc_server_fd = take_fd_ownership(args.rpc_server_fd, &mut owned_fds)
+ .expect("Failed to take ownership of rpc_server_fd");
+ let ready_fd = take_fd_ownership(args.ready_fd, &mut owned_fds)
+ .expect("Failed to take ownership of ready_fd");
// Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
ProcessState::start_thread_pool();
if cfg!(early) {
- panic!("Early VM not implemented");
+ // we can't access VirtualizationServiceInternal, so directly call rlimit
+ let pid = i32::from(*PID_CURRENT);
+ let lim = libc::rlimit { rlim_cur: libc::RLIM_INFINITY, rlim_max: libc::RLIM_INFINITY };
+
+ // SAFETY: borrowing the new limit struct only. prlimit doesn't use lim outside its lifetime
+ let ret = unsafe { libc::prlimit(pid, libc::RLIMIT_MEMLOCK, &lim, std::ptr::null_mut()) };
+ if ret == -1 {
+ panic!("rlimit error: {}", std::io::Error::last_os_error());
+ } else if ret != 0 {
+ panic!("Unexpected return value from prlimit(): {ret}");
+ }
} else {
GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
}
diff --git a/android/virtmgr/src/payload.rs b/android/virtmgr/src/payload.rs
index 82d9ba0..81e02b7 100644
--- a/android/virtmgr/src/payload.rs
+++ b/android/virtmgr/src/payload.rs
@@ -35,6 +35,7 @@
use serde::Deserialize;
use serde_xml_rs::from_reader;
use std::collections::HashSet;
+use std::ffi::OsStr;
use std::fs::{metadata, File, OpenOptions};
use std::path::{Path, PathBuf};
use std::process::Command;
@@ -94,11 +95,13 @@
// For active APEXes, we run derive_classpath and parse its output to see if it
// contributes to the classpath(s). (This allows us to handle any new classpath env
// vars seamlessly.)
- let classpath_vars = run_derive_classpath()?;
- let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
+ if !cfg!(early) {
+ let classpath_vars = run_derive_classpath()?;
+ let classpath_apexes = find_apex_names_in_classpath(&classpath_vars)?;
- for apex_info in apex_info_list.list.iter_mut() {
- apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
+ for apex_info in apex_info_list.list.iter_mut() {
+ apex_info.has_classpath_jar = classpath_apexes.contains(&apex_info.name);
+ }
}
Ok(apex_info_list)
@@ -169,6 +172,9 @@
let mut list = self.apex_info_list.clone();
// When prefer_staged, we override ApexInfo by consulting "package_native"
if prefer_staged {
+ if cfg!(early) {
+ return Err(anyhow!("Can't turn on prefer_staged on early boot VMs"));
+ }
let pm =
wait_for_interface::<dyn IPackageManagerNative>(PACKAGE_MANAGER_NATIVE_SERVICE)
.context("Failed to get service when prefer_staged is set.")?;
@@ -293,7 +299,16 @@
}];
for (i, apex_info) in apex_infos.iter().enumerate() {
- let apex_file = open_parcel_file(&apex_info.path, false)?;
+ let path = if cfg!(early) {
+ let path = &apex_info.preinstalled_path;
+ if path.extension().and_then(OsStr::to_str).unwrap_or("") != "apex" {
+ bail!("compressed APEX {} not supported", path.display());
+ }
+ path
+ } else {
+ &apex_info.path
+ };
+ let apex_file = open_parcel_file(path, false)?;
partitions.push(Partition {
label: format!("microdroid-apex-{}", i),
image: Some(apex_file),
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl
new file mode 100644
index 0000000..1889d2c
--- /dev/null
+++ b/android/virtualizationservice/aidl/android/system/virtualizationservice/UsbConfig.aidl
@@ -0,0 +1,22 @@
+/*
+ * 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;
+
+parcelable UsbConfig {
+ /** Enable the USB controller */
+ boolean controller;
+}
diff --git a/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/android/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 4ac401d..f559a71 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.UsbConfig;
/** Raw configuration for running a VM. */
parcelable VirtualMachineRawConfig {
@@ -102,4 +103,7 @@
@nullable AudioConfig audioConfig;
boolean noBalloon;
+
+ /** Enable or disable USB passthrough support */
+ @nullable UsbConfig usbConfig;
}
diff --git a/docs/early_vm.md b/docs/early_vm.md
new file mode 100644
index 0000000..44b71ff
--- /dev/null
+++ b/docs/early_vm.md
@@ -0,0 +1,52 @@
+# Early VM
+
+Early VMs are specialized virtual machines that can run even before the `/data`
+partition is mounted, unlike regular VMs. `early_virtmgr` is a binary that
+serves as the interface for early VMs, functioning similarly to `virtmgr`,
+which provides the [`IVirtualizationService`](../android/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl)
+aidl interface.
+
+To run an early VM, clients must follow these steps.
+
+1) Early VMs need to be defined in `{partition}/etc/avf/early_vms.xml`. The
+schema for this file is defined in [`early_vms.xsd`](../android/virtmgr/early_vms.xsd).
+
+```early_vms.xml
+<early_vms>
+ <early_vm>
+ <name>vm_demo_native_early</name>
+ <cid>123</cid>
+ <path>/system/bin/vm_demo_native_early</path>
+ </early_vm>
+</early_vms>
+```
+
+In this example, the binary `/system/bin/vm_demo_native_early` can establish a
+connection with `early_virtmgr` and create a VM named `vm_demo_native_early`,
+which will be assigned the static CID 123.
+
+2) The client must have the following three or four capabilities.
+
+* `IPC_LOCK`
+* `NET_BIND_SERVICE`
+* `SYS_NICE` (required if `RELEASE_AVF_ENABLE_VIRT_CPUFREQ` is enabled)
+* `SYS_RESOURCES`
+
+Typically, the client is defined as a service in an init script, where
+capabilities can also be specified.
+
+```vm_demo_native_early.rc
+service vm_demo_native_early /system/bin/vm_demo_native_early
+ user system
+ group system virtualmachine
+ capabilities IPC_LOCK NET_BIND_SERVICE SYS_RESOURCE SYS_NICE
+ oneshot
+ stdio_to_kmsg
+ class early_hal
+```
+
+3) The client forks `early_virtmgr` instead of `virtmgr`.
+
+The remaining steps are identical to those for regular VMs: connect to
+`early_virtmgr`, obtain the `IVirtualizationService` interface, then create and
+run the VM.
diff --git a/docs/img/pvm-dice.png b/docs/img/pvm-dice.png
new file mode 100644
index 0000000..5b26038
--- /dev/null
+++ b/docs/img/pvm-dice.png
Binary files differ
diff --git a/docs/pvm_dice_chain.md b/docs/pvm_dice_chain.md
new file mode 100644
index 0000000..11cdb6f
--- /dev/null
+++ b/docs/pvm_dice_chain.md
@@ -0,0 +1,52 @@
+# 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.
+
+![][pvm-dice-chain-img]
+
+The full RKP VM DICE chain, starting from `UDS_Pub` rooted in ROM, is
+sent to the RKP server during [pVM remote attestation][vm-attestation].
+
+[vm-attestation]: vm_remote_attestation.md
+[pvm-dice-chain-img]: img/pvm-dice.png
+
+## 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
+following formula:
+
+```
+CDI_Attest_pub, CDI_Attest_priv = KDF_ASYM(KDF(CDI_Attest))
+```
+
+Where KDF = HKDF-SHA-512 (RFC 5869).
+
+Currently, KDF_ASYM = Ed25519, but EC p-384 and p-256 (RFC 6979) support is
+coming soon.
+
+Vendors must use a supported algorithm for the last DICE node to ensure
+compatibility and chain integrity.
+
+[pvmfw]: ../guest/pvmfw
+[open-dice]: https://cs.android.com/android/platform/superproject/main/+/main:external/open-dice/
+
+## Testing
+
+To verify that the DICE handover is successful in pvmfw and eventually the pVM
+has a valid DICE chain, you can run the VSR test
+`MicrodroidTests#protectedVmHasValidDiceChain`. The test retrieves the DICE
+chain from within a Microdroid VM in protected mode and checks the following
+properties using the [hwtrust][hwtrust] library:
+
+1. All the fields in the DICE chain conform to
+ [Android Profile for DICE][android-open-dice].
+2. The DICE chain is a valid certificate chain, where the subject public key in
+ each certificate can be used to verify the signature of the next certificate.
+
+[hwtrust]: https://cs.android.com/android/platform/superproject/main/+/main:tools/security/remote_provisioning/hwtrust/
+[android-open-dice]: https://android.googlesource.com/platform/external/open-dice/+/refs/heads/main/docs/android.md
diff --git a/guest/authfs_service/Android.bp b/guest/authfs_service/Android.bp
index e508c17..2101a36 100644
--- a/guest/authfs_service/Android.bp
+++ b/guest/authfs_service/Android.bp
@@ -18,7 +18,6 @@
"libnix",
"librpcbinder_rs",
"librustutils",
- "libsafe_ownedfd",
"libshared_child",
],
prefer_rlib: true,
diff --git a/guest/authfs_service/src/authfs.rs b/guest/authfs_service/src/authfs.rs
index cfd5766..f2638c2 100644
--- a/guest/authfs_service/src/authfs.rs
+++ b/guest/authfs_service/src/authfs.rs
@@ -89,12 +89,9 @@
&config.outputDirFdAnnotations,
debuggable,
)?;
- wait_until_authfs_ready(&child, &mountpoint).map_err(|e| {
- match child.wait() {
- Ok(status) => debug!("Wait for authfs: {}", status),
- Err(e) => warn!("Failed to wait for child: {}", e),
- }
- e
+ wait_until_authfs_ready(&child, &mountpoint).inspect_err(|_| match child.wait() {
+ Ok(status) => debug!("Wait for authfs: {}", status),
+ Err(e) => warn!("Failed to wait for child: {}", e),
})?;
let authfs = AuthFs { mountpoint, process: child };
diff --git a/guest/authfs_service/src/main.rs b/guest/authfs_service/src/main.rs
index ff2f770..97e684d 100644
--- a/guest/authfs_service/src/main.rs
+++ b/guest/authfs_service/src/main.rs
@@ -26,10 +26,9 @@
use log::*;
use rpcbinder::RpcServer;
use rustutils::sockets::android_get_control_socket;
-use safe_ownedfd::take_fd_ownership;
use std::ffi::OsString;
use std::fs::{create_dir, read_dir, remove_dir_all, remove_file};
-use std::os::unix::io::OwnedFd;
+use std::os::unix::io::{FromRawFd, OwnedFd};
use std::sync::atomic::{AtomicUsize, Ordering};
use authfs_aidl_interface::aidl::com::android::virt::fs::AuthFsConfig::AuthFsConfig;
@@ -110,9 +109,22 @@
}
/// Prepares a socket file descriptor for the authfs service.
-fn prepare_authfs_service_socket() -> Result<OwnedFd> {
+///
+/// # Safety requirement
+///
+/// The caller must ensure that this function is the only place that claims ownership
+/// of the file descriptor and it is called only once.
+unsafe fn prepare_authfs_service_socket() -> Result<OwnedFd> {
let raw_fd = android_get_control_socket(AUTHFS_SERVICE_SOCKET_NAME)?;
- Ok(take_fd_ownership(raw_fd)?)
+
+ // Creating OwnedFd for stdio FDs is not safe.
+ if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+ bail!("File descriptor {raw_fd} is standard I/O descriptor");
+ }
+ // SAFETY: Initializing OwnedFd for a RawFd created by the init.
+ // We checked that the integer value corresponds to a valid FD and that the caller
+ // ensures that this is the only place to claim its ownership.
+ Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
}
#[allow(clippy::eq_op)]
@@ -125,7 +137,8 @@
clean_up_working_directory()?;
- let socket_fd = prepare_authfs_service_socket()?;
+ // SAFETY: This is the only place we take the ownership of the fd of the authfs service.
+ let socket_fd = unsafe { prepare_authfs_service_socket()? };
let service = AuthFsService::new_binder(debuggable).as_binder();
debug!("{} is starting as a rpc service.", AUTHFS_SERVICE_SOCKET_NAME);
let server = RpcServer::new_bound_socket(service, socket_fd)?;
diff --git a/guest/microdroid_manager/Android.bp b/guest/microdroid_manager/Android.bp
index 82e26b7..9c9a3d0 100644
--- a/guest/microdroid_manager/Android.bp
+++ b/guest/microdroid_manager/Android.bp
@@ -48,7 +48,6 @@
"libprotobuf",
"librpcbinder_rs",
"librustutils",
- "libsafe_ownedfd",
"libsecretkeeper_client",
"libsecretkeeper_comm_nostd",
"libscopeguard",
@@ -60,7 +59,6 @@
"libvsock",
"librand",
"libzeroize",
- "libsafe_ownedfd",
],
init_rc: ["microdroid_manager.rc"],
multilib: {
diff --git a/guest/microdroid_manager/src/main.rs b/guest/microdroid_manager/src/main.rs
index 8b676b8..7352a2c 100644
--- a/guest/microdroid_manager/src/main.rs
+++ b/guest/microdroid_manager/src/main.rs
@@ -50,14 +50,13 @@
use rustutils::sockets::android_get_control_socket;
use rustutils::system_properties;
use rustutils::system_properties::PropertyWatcher;
-use safe_ownedfd::take_fd_ownership;
use secretkeeper_comm::data_types::ID_SIZE;
use std::borrow::Cow::{Borrowed, Owned};
use std::env;
use std::ffi::CString;
use std::fs::{self, create_dir, File, OpenOptions};
use std::io::{Read, Write};
-use std::os::unix::io::OwnedFd;
+use std::os::unix::io::{FromRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
@@ -200,7 +199,13 @@
);
info!("started.");
- let vm_payload_service_fd = prepare_vm_payload_service_socket()?;
+ // SAFETY: This is the only place we take the ownership of the fd of the vm payload service.
+ //
+ // To ensure that the CLOEXEC flag is set on the file descriptor as early as possible,
+ // it is necessary to fetch the socket corresponding to vm_payload_service at the
+ // very beginning, as android_get_control_socket() sets the CLOEXEC flag on the file
+ // descriptor.
+ let vm_payload_service_fd = unsafe { prepare_vm_payload_service_socket()? };
load_crashkernel_if_supported().context("Failed to load crashkernel")?;
@@ -265,7 +270,7 @@
// Verify the payload before using it.
let extracted_data = verify_payload(metadata, saved_data.as_ref())
.context("Payload verification failed")
- .map_err(|e| MicrodroidError::PayloadVerificationFailed(e.to_string()))?;
+ .map_err(|e| MicrodroidError::PayloadVerificationFailed(format!("{:?}", e)))?;
// In case identity is ignored (by debug policy), we should reuse existing payload data, even
// when the payload is changed. This is to keep the derived secret same as before.
@@ -482,9 +487,22 @@
}
/// Prepares a socket file descriptor for the vm payload service.
-fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
+///
+/// # Safety
+///
+/// The caller must ensure that this function is the only place that claims ownership
+/// of the file descriptor and it is called only once.
+unsafe fn prepare_vm_payload_service_socket() -> Result<OwnedFd> {
let raw_fd = android_get_control_socket(VM_PAYLOAD_SERVICE_SOCKET_NAME)?;
- Ok(take_fd_ownership(raw_fd)?)
+
+ // Creating OwnedFd for stdio FDs is not safe.
+ if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+ bail!("File descriptor {raw_fd} is standard I/O descriptor");
+ }
+ // SAFETY: Initializing OwnedFd for a RawFd created by the init.
+ // We checked that the integer value corresponds to a valid FD and that the caller
+ // ensures that this is the only place to claim its ownership.
+ Ok(unsafe { OwnedFd::from_raw_fd(raw_fd) })
}
fn is_strict_boot() -> bool {
diff --git a/guest/microdroid_manager/src/payload.rs b/guest/microdroid_manager/src/payload.rs
index 98fe24b..8cb0f4e 100644
--- a/guest/microdroid_manager/src/payload.rs
+++ b/guest/microdroid_manager/src/payload.rs
@@ -16,7 +16,7 @@
use crate::instance::ApexData;
use crate::ioutil::wait_for_file;
-use anyhow::Result;
+use anyhow::{Context, Result};
use log::{info, warn};
use microdroid_metadata::{read_metadata, ApexPayload, Metadata};
use std::time::Duration;
@@ -38,7 +38,8 @@
.iter()
.map(|apex| {
let apex_path = format!("/dev/block/by-name/{}", apex.partition_name);
- let extracted = apexutil::verify(&apex_path)?;
+ let extracted =
+ apexutil::verify(&apex_path).context(format!("Failed to parse {}", &apex_path))?;
if let Some(manifest_name) = &extracted.name {
if &apex.name != manifest_name {
warn!("Apex named {} is named {} in its manifest", apex.name, manifest_name);
diff --git a/guest/pvmfw/Android.bp b/guest/pvmfw/Android.bp
index 144e81e..b502af6 100644
--- a/guest/pvmfw/Android.bp
+++ b/guest/pvmfw/Android.bp
@@ -13,7 +13,6 @@
rustlibs: [
"libaarch64_paging",
"libbssl_avf_nostd",
- "libbssl_sys_nostd",
"libcbor_util_nostd",
"libciborium_nostd",
"libciborium_io_nostd",
diff --git a/guest/pvmfw/README.md b/guest/pvmfw/README.md
index 4712d77..58ba10c 100644
--- a/guest/pvmfw/README.md
+++ b/guest/pvmfw/README.md
@@ -251,10 +251,13 @@
}
```
-and contains the _Compound Device Identifiers_ ("CDIs"), used to derive the
-next-stage secret, and a certificate chain, intended for pVM attestation. Note
-that it differs from the `AndroidDiceHandover` defined by the specification in
-that its `DiceCertChain` field is mandatory (while optional in the original).
+It contains the _Compound Device Identifiers_ (CDIs), used for deriving the
+next-stage secret, and a certificate chain, necessary for building the full
+[pVM DICE chain][pvm-dice-chain] required by features like
+[pVM remote attestation][vm-attestation].
+
+Note that it differs from the `AndroidDiceHandover` defined by the specification
+in that its `DiceCertChain` field is mandatory (while optional in the original).
Devices that fully implement DICE should provide a certificate rooted at the
Unique Device Secret (UDS) in a boot stage preceding the pvmfw loader (typically
@@ -262,16 +265,6 @@
can be passed to [`DiceAndroidHandoverMainFlow`][DiceAndroidHandoverMainFlow] along with
the inputs described below.
-Otherwise, as an intermediate step towards supporting DICE throughout the
-software stack of the device, incomplete implementations may root the DICE chain
-at the pvmfw loader, using an arbitrary constant as initial CDI. The pvmfw
-loader can easily do so by:
-
-1. Building an "empty" `AndroidDiceHandover` using CBOR operations only
- containing constant CDIs ([example][Trusty-BCC])
-1. Passing the resulting `AndroidDiceHandover` to `DiceAndroidHandoverMainFlow`
- as described above
-
The recommended DICE inputs at this stage are:
- **Code**: hash of the pvmfw image, hypervisor (`boot.img`), and other target
@@ -291,19 +284,6 @@
`/reserved-memory` device tree node marked as
[`compatible=”google,open-dice”`][dice-dt].
-#### Testing
-
-To verify that the DICE handover is successful in pvmfw and eventually the pVM
-has a valid DICE chain, you can run the VSR test
-`MicrodroidTests#protectedVmHasValidDiceChain`. The test retrieves the DICE
-chain from within a Microdroid VM in protected mode and checks the following
-properties using the [hwtrust][hwtrust] library:
-
-1. All the fields in the DICE chain conform to
- [Android Profile for DICE][android-open-dice].
-2. The DICE chain is a valid certificate chain, where the subject public key in
- each certificate can be used to verify the signature of the next certificate.
-
[AVB]: https://source.android.com/docs/security/features/verifiedboot/boot-flow
[AndroidDiceHandover]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#212
[DiceAndroidHandoverMainFlow]: https://pigweed.googlesource.com/open-dice/+/42ae7760023/src/android.c#221
@@ -311,9 +291,8 @@
[dice-mode]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#Mode-Value-Details
[dice-dt]: https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/google%2Copen-dice.yaml
[Layering]: https://pigweed.googlesource.com/open-dice/+/refs/heads/main/docs/specification.md#layering-details
-[Trusty-BCC]: https://android.googlesource.com/trusty/lib/+/1696be0a8f3a7103/lib/hwbcc/common/swbcc.c#554
-[hwtrust]: https://cs.android.com/android/platform/superproject/main/+/main:tools/security/remote_provisioning/hwtrust/
-[android-open-dice]: https://android.googlesource.com/platform/external/open-dice/+/refs/heads/main/docs/android.md
+[pvm-dice-chain]: ../../docs/pvm_dice_chain.md
+[vm-attestation]: ../../docs/vm_remote_attestation.md
### Platform Requirements
diff --git a/guest/pvmfw/src/entry.rs b/guest/pvmfw/src/entry.rs
index 8f9340b..945ad6a 100644
--- a/guest/pvmfw/src/entry.rs
+++ b/guest/pvmfw/src/entry.rs
@@ -17,7 +17,6 @@
use crate::config;
use crate::fdt;
use crate::memory;
-use bssl_sys::CRYPTO_library_init;
use core::arch::asm;
use core::mem::{drop, size_of};
use core::num::NonZeroUsize;
@@ -216,12 +215,6 @@
// - only access non-pvmfw memory once (and while) it has been mapped
log::set_max_level(LevelFilter::Info);
- // TODO(https://crbug.com/boringssl/35): Remove this init when BoringSSL can handle this
- // internally.
- // SAFETY: Configures the internal state of the library - may be called multiple times.
- unsafe {
- CRYPTO_library_init();
- }
let page_table = memory::init_page_table().map_err(|e| {
error!("Failed to set up the dynamic page tables: {e}");
diff --git a/guest/rialto/Android.bp b/guest/rialto/Android.bp
index b26a1c4..4c18bf9 100644
--- a/guest/rialto/Android.bp
+++ b/guest/rialto/Android.bp
@@ -10,7 +10,6 @@
rustlibs: [
"libaarch64_paging",
"libbssl_avf_nostd",
- "libbssl_sys_nostd",
"libciborium_io_nostd",
"libciborium_nostd",
"libcstr",
diff --git a/guest/rialto/src/main.rs b/guest/rialto/src/main.rs
index a98ec25..9265775 100644
--- a/guest/rialto/src/main.rs
+++ b/guest/rialto/src/main.rs
@@ -28,7 +28,6 @@
use crate::error::{Error, Result};
use crate::fdt::{read_dice_range_from, read_is_strict_boot, read_vendor_hashtree_root_digest};
use alloc::boxed::Box;
-use bssl_sys::CRYPTO_library_init;
use ciborium_io::Write;
use core::num::NonZeroUsize;
use core::slice;
@@ -109,40 +108,30 @@
let fdt = libfdt::Fdt::from_slice(fdt)?;
let memory_range = fdt.first_memory_range()?;
- MEMORY.lock().as_mut().unwrap().shrink(&memory_range).map_err(|e| {
+ MEMORY.lock().as_mut().unwrap().shrink(&memory_range).inspect_err(|_| {
error!("Failed to use memory range value from DT: {memory_range:#x?}");
- e
})?;
if let Some(mem_sharer) = get_mem_sharer() {
let granule = mem_sharer.granule()?;
- MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).map_err(|e| {
+ MEMORY.lock().as_mut().unwrap().init_dynamic_shared_pool(granule).inspect_err(|_| {
error!("Failed to initialize dynamically shared pool.");
- e
})?;
} else if let Ok(swiotlb_info) = SwiotlbInfo::new_from_fdt(fdt) {
let range = swiotlb_info.fixed_range().ok_or_else(|| {
error!("Pre-shared pool range not specified in swiotlb node");
Error::from(FdtError::BadValue)
})?;
- MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).map_err(|e| {
+ MEMORY.lock().as_mut().unwrap().init_static_shared_pool(range).inspect_err(|_| {
error!("Failed to initialize pre-shared pool.");
- e
})?;
} else {
info!("No MEM_SHARE capability detected or swiotlb found: allocating buffers from heap.");
- MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().map_err(|e| {
+ MEMORY.lock().as_mut().unwrap().init_heap_shared_pool().inspect_err(|_| {
error!("Failed to initialize heap-based pseudo-shared pool.");
- e
})?;
}
- // Initializes the crypto library before any crypto operations and after the heap is
- // initialized.
- // SAFETY: It is safe to call this function multiple times and concurrently.
- unsafe {
- CRYPTO_library_init();
- }
let bcc_handover: Box<dyn DiceArtifacts> = match vm_type(fdt)? {
VmType::ProtectedVm => {
let dice_range = read_dice_range_from(fdt)?;
@@ -153,9 +142,8 @@
let res = unsafe {
MEMORY.lock().as_mut().unwrap().alloc_range_outside_main_memory(&dice_range)
};
- res.map_err(|e| {
+ res.inspect_err(|_| {
error!("Failed to use DICE range from DT: {dice_range:#x?}");
- e
})?;
let dice_start = dice_range.start as *const u8;
// SAFETY: There's no memory overlap and the region is mapped as read-only data.
@@ -233,6 +221,28 @@
}
}
+/// Flushes data caches over the provided address range.
+///
+/// # Safety
+///
+/// The provided address and size must be to an address range that is valid for read and write
+/// (typically on the stack, .bss, .data, or provided BCC) from a single allocation
+/// (e.g. stack array).
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(
+ _ctx: *mut core::ffi::c_void,
+ size: usize,
+ addr: *mut core::ffi::c_void,
+) {
+ use core::slice;
+ use vmbase::memory::flushed_zeroize;
+
+ // SAFETY: We require our caller to provide a valid range within a single object. The open-dice
+ // always calls this on individual stack-allocated arrays which ensures that.
+ let region = unsafe { slice::from_raw_parts_mut(addr as *mut u8, size) };
+ flushed_zeroize(region)
+}
+
generate_image_header!();
main!(main);
configure_heap!(SIZE_128KB * 2);
diff --git a/libs/dice/open_dice/Android.bp b/libs/dice/open_dice/Android.bp
index efe350f..d1129fb 100644
--- a/libs/dice/open_dice/Android.bp
+++ b/libs/dice/open_dice/Android.bp
@@ -22,7 +22,6 @@
"alloc",
],
whole_static_libs: [
- "libopen_dice_cbor",
"libcrypto_baremetal",
],
visibility: [
@@ -55,6 +54,7 @@
"//packages/modules/Virtualization:__subpackages__",
"//system/authgraph/tests:__subpackages__",
"//system/secretkeeper/client:__subpackages__",
+ "//system/software_defined_vehicle:__subpackages__",
],
apex_available: [
"//apex_available:platform",
diff --git a/libs/dice/sample_inputs/tests/api_test.rs b/libs/dice/sample_inputs/tests/api_test.rs
index 0823f16..d713168 100644
--- a/libs/dice/sample_inputs/tests/api_test.rs
+++ b/libs/dice/sample_inputs/tests/api_test.rs
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+#![cfg_attr(not(feature = "std"), no_std)]
+
use anyhow::Result;
use diced_open_dice::{derive_cdi_leaf_priv, sign, DiceArtifacts};
use diced_sample_inputs::make_sample_bcc_and_cdis;
@@ -144,3 +146,21 @@
let public_key = chain.leaf().subject_public_key();
public_key.verify(&signature, MESSAGE)
}
+
+/// Flushes data caches over the provided address range in open-dice.
+///
+/// # Safety
+///
+/// The provided address and size must be to an address range that is valid for read and write
+/// (typically on the stack, .bss, .data, or provided BCC) from a single allocation
+/// (e.g. stack array).
+#[cfg(not(feature = "std"))]
+#[no_mangle]
+unsafe extern "C" fn DiceClearMemory(
+ _ctx: *mut core::ffi::c_void,
+ size: usize,
+ addr: *mut core::ffi::c_void,
+) {
+ // SAFETY: The caller ensures that the address and size are valid for write.
+ unsafe { core::ptr::write_bytes(addr as *mut u8, 0, size) };
+}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineConfig.java
index 644a85a..cb21ccf 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.UsbConfig;
import android.system.virtualizationservice.VirtualMachineAppConfig;
import android.system.virtualizationservice.VirtualMachinePayloadConfig;
import android.system.virtualizationservice.VirtualMachineRawConfig;
@@ -725,6 +726,15 @@
.map(ac -> ac.toParcelable())
.orElse(null);
config.noBalloon = !customImageConfig.useAutoMemoryBalloon();
+ config.usbConfig =
+ Optional.ofNullable(customImageConfig.getUsbConfig())
+ .map(
+ uc -> {
+ UsbConfig usbConfig = new UsbConfig();
+ usbConfig.controller = uc.getUsbController();
+ return usbConfig;
+ })
+ .orElse(null);
return config;
}
diff --git a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index a38ee7f..9774585 100644
--- a/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/libs/framework-virtualization/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -46,6 +46,7 @@
private static final String KEY_AUDIO_CONFIG = "audio_config";
private static final String KEY_TRACKPAD = "trackpad";
private static final String KEY_AUTO_MEMORY_BALLOON = "auto_memory_balloon";
+ private static final String KEY_USB_CONFIG = "usb_config";
@Nullable private final String name;
@Nullable private final String kernelPath;
@@ -63,6 +64,7 @@
@Nullable private final GpuConfig gpuConfig;
private final boolean trackpad;
private final boolean autoMemoryBalloon;
+ @Nullable private final UsbConfig usbConfig;
@Nullable
public Disk[] getDisks() {
@@ -139,7 +141,8 @@
GpuConfig gpuConfig,
AudioConfig audioConfig,
boolean trackpad,
- boolean autoMemoryBalloon) {
+ boolean autoMemoryBalloon,
+ UsbConfig usbConfig) {
this.name = name;
this.kernelPath = kernelPath;
this.initrdPath = initrdPath;
@@ -156,6 +159,7 @@
this.audioConfig = audioConfig;
this.trackpad = trackpad;
this.autoMemoryBalloon = autoMemoryBalloon;
+ this.usbConfig = usbConfig;
}
static VirtualMachineCustomImageConfig from(PersistableBundle customImageConfigBundle) {
@@ -208,6 +212,9 @@
builder.setAudioConfig(AudioConfig.from(audioConfigPb));
builder.useTrackpad(customImageConfigBundle.getBoolean(KEY_TRACKPAD));
builder.useAutoMemoryBalloon(customImageConfigBundle.getBoolean(KEY_AUTO_MEMORY_BALLOON));
+ PersistableBundle usbConfigPb =
+ customImageConfigBundle.getPersistableBundle(KEY_USB_CONFIG);
+ builder.setUsbConfig(UsbConfig.from(usbConfigPb));
return builder.build();
}
@@ -266,6 +273,9 @@
Optional.ofNullable(audioConfig).map(ac -> ac.toPersistableBundle()).orElse(null));
pb.putBoolean(KEY_TRACKPAD, trackpad);
pb.putBoolean(KEY_AUTO_MEMORY_BALLOON, autoMemoryBalloon);
+ pb.putPersistableBundle(
+ KEY_USB_CONFIG,
+ Optional.ofNullable(usbConfig).map(uc -> uc.toPersistableBundle()).orElse(null));
return pb;
}
@@ -284,6 +294,11 @@
return gpuConfig;
}
+ @Nullable
+ public UsbConfig getUsbConfig() {
+ return usbConfig;
+ }
+
/** @hide */
public static final class Disk {
private final boolean writable;
@@ -362,6 +377,7 @@
private boolean trackpad;
// TODO(b/363985291): balloon breaks Linux VM behavior
private boolean autoMemoryBalloon = false;
+ private UsbConfig usbConfig;
/** @hide */
public Builder() {}
@@ -463,6 +479,12 @@
}
/** @hide */
+ public Builder setUsbConfig(UsbConfig usbConfig) {
+ this.usbConfig = usbConfig;
+ return this;
+ }
+
+ /** @hide */
public VirtualMachineCustomImageConfig build() {
return new VirtualMachineCustomImageConfig(
this.name,
@@ -480,7 +502,63 @@
gpuConfig,
audioConfig,
trackpad,
- autoMemoryBalloon);
+ autoMemoryBalloon,
+ usbConfig);
+ }
+ }
+
+ /** @hide */
+ public static final class UsbConfig {
+ private static final String KEY_USE_CONTROLLER = "use_controller";
+ public final boolean controller;
+
+ public UsbConfig(boolean controller) {
+ this.controller = controller;
+ }
+
+ public boolean getUsbController() {
+ return this.controller;
+ }
+
+ android.system.virtualizationservice.UsbConfig toParceclable() {
+ android.system.virtualizationservice.UsbConfig parcelable =
+ new android.system.virtualizationservice.UsbConfig();
+ parcelable.controller = this.controller;
+ return parcelable;
+ }
+
+ private static UsbConfig from(PersistableBundle pb) {
+ if (pb == null) {
+ return null;
+ }
+ Builder builder = new Builder();
+ builder.setController(pb.getBoolean(KEY_USE_CONTROLLER));
+ return builder.build();
+ }
+
+ private PersistableBundle toPersistableBundle() {
+ PersistableBundle pb = new PersistableBundle();
+ pb.putBoolean(KEY_USE_CONTROLLER, this.controller);
+ return pb;
+ }
+
+ /** @hide */
+ public static class Builder {
+ private boolean useController = false;
+
+ /** @hide */
+ public Builder() {}
+
+ /** @hide */
+ public Builder setController(boolean useController) {
+ this.useController = useController;
+ return this;
+ }
+
+ /** @hide */
+ public UsbConfig build() {
+ return new UsbConfig(useController);
+ }
}
}
diff --git a/libs/libfdt/src/libfdt.rs b/libs/libfdt/src/libfdt.rs
index b2250f5..6869af6 100644
--- a/libs/libfdt/src/libfdt.rs
+++ b/libs/libfdt/src/libfdt.rs
@@ -292,7 +292,7 @@
// SAFETY: Accesses (read-only) are constrained to the DT totalsize.
let ret = unsafe { libfdt_bindgen::fdt_find_max_phandle(fdt, &mut phandle) };
- FdtRawResult::from(ret).try_into()?;
+ () = FdtRawResult::from(ret).try_into()?;
phandle.try_into()
}
@@ -390,7 +390,7 @@
// SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
unsafe { libfdt_bindgen::fdt_setprop_placeholder(fdt, node, name, len, &mut data) };
- FdtRawResult::from(ret).try_into()?;
+ () = FdtRawResult::from(ret).try_into()?;
get_mut_slice_at_ptr(self.as_fdt_slice_mut(), data.cast(), size).ok_or(FdtError::Internal)
}
diff --git a/libs/libinherited_fd/Android.bp b/libs/libinherited_fd/Android.bp
new file mode 100644
index 0000000..28ec2e5
--- /dev/null
+++ b/libs/libinherited_fd/Android.bp
@@ -0,0 +1,44 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+rust_defaults {
+ name: "libinherited_fd.defaults",
+ crate_name: "inherited_fd",
+ srcs: ["src/lib.rs"],
+ edition: "2021",
+ rustlibs: [
+ "libnix",
+ "libonce_cell",
+ "libthiserror",
+ ],
+}
+
+rust_library {
+ name: "libinherited_fd",
+ defaults: ["libinherited_fd.defaults"],
+ apex_available: [
+ "com.android.compos",
+ "com.android.virt",
+ ],
+}
+
+rust_test {
+ name: "libinherited_fd.test",
+ defaults: ["libinherited_fd.defaults"],
+ rustlibs: [
+ "libanyhow",
+ "libtempfile",
+ ],
+ host_supported: true,
+ test_suites: ["general-tests"],
+ test_options: {
+ unit_test: true,
+ },
+ // this is to run each test function in a separate process.
+ // note that they still run in parallel.
+ flags: [
+ "-C panic=abort",
+ "-Z panic_abort_tests",
+ ],
+}
diff --git a/libs/libinherited_fd/src/lib.rs b/libs/libinherited_fd/src/lib.rs
new file mode 100644
index 0000000..f5e2d6b
--- /dev/null
+++ b/libs/libinherited_fd/src/lib.rs
@@ -0,0 +1,270 @@
+// Copyright 2024, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Library for safely obtaining `OwnedFd` for inherited file descriptors.
+
+use nix::fcntl::{fcntl, FdFlag, F_SETFD};
+use nix::libc;
+use std::collections::HashMap;
+use std::fs::canonicalize;
+use std::fs::read_dir;
+use std::os::fd::FromRawFd;
+use std::os::fd::OwnedFd;
+use std::os::fd::RawFd;
+use std::sync::Mutex;
+use std::sync::OnceLock;
+use thiserror::Error;
+
+/// Errors that can occur while taking an ownership of `RawFd`
+#[derive(Debug, PartialEq, Error)]
+pub enum Error {
+ /// init_once() not called
+ #[error("init_once() not called")]
+ NotInitialized,
+
+ /// Ownership already taken
+ #[error("Ownership of FD {0} is already taken")]
+ OwnershipTaken(RawFd),
+
+ /// Not an inherited file descriptor
+ #[error("FD {0} is either invalid file descriptor or not an inherited one")]
+ FileDescriptorNotInherited(RawFd),
+
+ /// Failed to set CLOEXEC
+ #[error("Failed to set CLOEXEC on FD {0}")]
+ FailCloseOnExec(RawFd),
+}
+
+static INHERITED_FDS: OnceLock<Mutex<HashMap<RawFd, Option<OwnedFd>>>> = OnceLock::new();
+
+/// Take ownership of all open file descriptors in this process, which later can be obtained by
+/// calling `take_fd_ownership`.
+///
+/// # Safety
+/// This function has to be called very early in the program before the ownership of any file
+/// descriptors (except stdin/out/err) is taken.
+pub unsafe fn init_once() -> Result<(), std::io::Error> {
+ let mut fds = HashMap::new();
+
+ let fd_path = canonicalize("/proc/self/fd")?;
+
+ for entry in read_dir(&fd_path)? {
+ let entry = entry?;
+
+ // Files in /prod/self/fd are guaranteed to be numbers. So parsing is always successful.
+ let file_name = entry.file_name();
+ let raw_fd = file_name.to_str().unwrap().parse::<RawFd>().unwrap();
+
+ // We don't take ownership of the stdio FDs as the Rust runtime owns them.
+ if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
+ continue;
+ }
+
+ // Exceptional case: /proc/self/fd/* may be a dir fd created by read_dir just above. Since
+ // the file descriptor is owned by read_dir (and thus closed by it), we shouldn't take
+ // ownership to it.
+ if entry.path().read_link()? == fd_path {
+ continue;
+ }
+
+ // SAFETY: /proc/self/fd/* are file descriptors that are open. If `init_once()` was called
+ // at the very beginning of the program execution (as requested by the safety requirement
+ // of this function), this is the first time to claim the ownership of these file
+ // descriptors.
+ let owned_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
+ fds.insert(raw_fd, Some(owned_fd));
+ }
+
+ INHERITED_FDS
+ .set(Mutex::new(fds))
+ .or(Err(std::io::Error::other("Inherited fds were already initialized")))
+}
+
+/// Take the ownership of the given `RawFd` and returns `OwnedFd` for it. The returned FD is set
+/// CLOEXEC. `Error` is returned when the ownership was already taken (by a prior call to this
+/// function with the same `RawFd`) or `RawFd` is not an inherited file descriptor.
+pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
+ let mut fds = INHERITED_FDS.get().ok_or(Error::NotInitialized)?.lock().unwrap();
+
+ if let Some(value) = fds.get_mut(&raw_fd) {
+ if let Some(owned_fd) = value.take() {
+ fcntl(raw_fd, F_SETFD(FdFlag::FD_CLOEXEC)).or(Err(Error::FailCloseOnExec(raw_fd)))?;
+ Ok(owned_fd)
+ } else {
+ Err(Error::OwnershipTaken(raw_fd))
+ }
+ } else {
+ Err(Error::FileDescriptorNotInherited(raw_fd))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use anyhow::Result;
+ use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
+ use nix::unistd::close;
+ use std::os::fd::{AsRawFd, IntoRawFd};
+ use tempfile::tempfile;
+
+ struct Fixture {
+ fds: Vec<RawFd>,
+ }
+
+ impl Fixture {
+ fn setup(num_fds: usize) -> Result<Self> {
+ let mut fds = Vec::new();
+ for _ in 0..num_fds {
+ fds.push(tempfile()?.into_raw_fd());
+ }
+ Ok(Fixture { fds })
+ }
+
+ fn open_new_file(&mut self) -> Result<RawFd> {
+ let raw_fd = tempfile()?.into_raw_fd();
+ self.fds.push(raw_fd);
+ Ok(raw_fd)
+ }
+ }
+
+ impl Drop for Fixture {
+ fn drop(&mut self) {
+ self.fds.iter().for_each(|fd| {
+ let _ = close(*fd);
+ });
+ }
+ }
+
+ fn is_fd_opened(raw_fd: RawFd) -> bool {
+ fcntl(raw_fd, F_GETFD).is_ok()
+ }
+
+ #[test]
+ fn happy_case() -> Result<()> {
+ let fixture = Fixture::setup(2)?;
+ let f0 = fixture.fds[0];
+ let f1 = fixture.fds[1];
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ let f0_owned = take_fd_ownership(f0)?;
+ let f1_owned = take_fd_ownership(f1)?;
+ assert_eq!(f0, f0_owned.as_raw_fd());
+ assert_eq!(f1, f1_owned.as_raw_fd());
+
+ drop(f0_owned);
+ drop(f1_owned);
+ assert!(!is_fd_opened(f0));
+ assert!(!is_fd_opened(f1));
+ Ok(())
+ }
+
+ #[test]
+ fn access_non_inherited_fd() -> Result<()> {
+ let mut fixture = Fixture::setup(2)?;
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ let f = fixture.open_new_file()?;
+ assert_eq!(Some(Error::FileDescriptorNotInherited(f)), take_fd_ownership(f).err());
+ Ok(())
+ }
+
+ #[test]
+ fn call_init_once_multiple_times() -> Result<()> {
+ let _ = Fixture::setup(2)?;
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ // SAFETY: for testing
+ let res = unsafe { init_once() };
+ assert!(res.is_err());
+ Ok(())
+ }
+
+ #[test]
+ fn access_without_init_once() -> Result<()> {
+ let fixture = Fixture::setup(2)?;
+
+ let f = fixture.fds[0];
+ assert_eq!(Some(Error::NotInitialized), take_fd_ownership(f).err());
+ Ok(())
+ }
+
+ #[test]
+ fn double_ownership() -> Result<()> {
+ let fixture = Fixture::setup(2)?;
+ let f = fixture.fds[0];
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ let f_owned = take_fd_ownership(f)?;
+ let f_double_owned = take_fd_ownership(f);
+ assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
+
+ // just to highlight that f_owned is kept alive when the second call to take_fd_ownership
+ // is made.
+ drop(f_owned);
+ Ok(())
+ }
+
+ #[test]
+ fn take_drop_retake() -> Result<()> {
+ let fixture = Fixture::setup(2)?;
+ let f = fixture.fds[0];
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ let f_owned = take_fd_ownership(f)?;
+ drop(f_owned);
+
+ let f_double_owned = take_fd_ownership(f);
+ assert_eq!(Some(Error::OwnershipTaken(f)), f_double_owned.err());
+ Ok(())
+ }
+
+ #[test]
+ fn cloexec() -> Result<()> {
+ let fixture = Fixture::setup(2)?;
+ let f = fixture.fds[0];
+
+ // SAFETY: assume files opened by Fixture are inherited ones
+ unsafe {
+ init_once()?;
+ }
+
+ // Intentionally cleaar cloexec to see if it is set by take_fd_ownership
+ fcntl(f, F_SETFD(FdFlag::empty()))?;
+
+ let f_owned = take_fd_ownership(f)?;
+ let flags = fcntl(f_owned.as_raw_fd(), F_GETFD)?;
+ assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
+ Ok(())
+ }
+}
diff --git a/libs/libsafe_ownedfd/Android.bp b/libs/libsafe_ownedfd/Android.bp
deleted file mode 100644
index 53e14dc..0000000
--- a/libs/libsafe_ownedfd/Android.bp
+++ /dev/null
@@ -1,38 +0,0 @@
-package {
- default_applicable_licenses: ["Android-Apache-2.0"],
-}
-
-rust_defaults {
- name: "libsafe_ownedfd.defaults",
- crate_name: "safe_ownedfd",
- srcs: ["src/lib.rs"],
- edition: "2021",
- rustlibs: [
- "libnix",
- "libthiserror",
- ],
-}
-
-rust_library {
- name: "libsafe_ownedfd",
- defaults: ["libsafe_ownedfd.defaults"],
- apex_available: [
- "com.android.compos",
- "com.android.microfuchsia",
- "com.android.virt",
- ],
-}
-
-rust_test {
- name: "libsafe_ownedfd.test",
- defaults: ["libsafe_ownedfd.defaults"],
- rustlibs: [
- "libanyhow",
- "libtempfile",
- ],
- host_supported: true,
- test_suites: ["general-tests"],
- test_options: {
- unit_test: true,
- },
-}
diff --git a/libs/libsafe_ownedfd/src/lib.rs b/libs/libsafe_ownedfd/src/lib.rs
deleted file mode 100644
index 52ae180..0000000
--- a/libs/libsafe_ownedfd/src/lib.rs
+++ /dev/null
@@ -1,127 +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.
-
-//! Library for a safer conversion from `RawFd` to `OwnedFd`
-
-use nix::fcntl::{fcntl, FdFlag, F_DUPFD, F_GETFD, F_SETFD};
-use nix::libc;
-use nix::unistd::close;
-use std::os::fd::FromRawFd;
-use std::os::fd::OwnedFd;
-use std::os::fd::RawFd;
-use std::sync::Mutex;
-use thiserror::Error;
-
-/// Errors that can occur while taking an ownership of `RawFd`
-#[derive(Debug, PartialEq, Error)]
-pub enum Error {
- /// RawFd is not a valid file descriptor
- #[error("{0} is not a file descriptor")]
- Invalid(RawFd),
-
- /// RawFd is either stdio, stdout, or stderr
- #[error("standard IO descriptors cannot be owned")]
- StdioNotAllowed,
-
- /// Generic UNIX error
- #[error("UNIX error")]
- Errno(#[from] nix::errno::Errno),
-}
-
-static LOCK: Mutex<()> = Mutex::new(());
-
-/// Takes the ownership of `RawFd` and converts it to `OwnedFd`. It is important to know that
-/// `RawFd` is closed when this function successfully returns. The raw file descriptor of the
-/// returned `OwnedFd` is different from `RawFd`. The returned file descriptor is CLOEXEC set.
-pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, Error> {
- fcntl(raw_fd, F_GETFD).map_err(|_| Error::Invalid(raw_fd))?;
-
- if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
- return Err(Error::StdioNotAllowed);
- }
-
- // sync is needed otherwise we can create multiple OwnedFds out of the same RawFd
- let lock = LOCK.lock().unwrap();
- let new_fd = fcntl(raw_fd, F_DUPFD(raw_fd))?;
- close(raw_fd)?;
- drop(lock);
-
- // This is not essential, but let's follow the common practice in the Rust ecosystem
- fcntl(new_fd, F_SETFD(FdFlag::FD_CLOEXEC)).map_err(Error::Errno)?;
-
- // SAFETY: In this function, we have checked that RawFd is actually an open file descriptor and
- // this is the first time to claim its ownership because we just created it by duping.
- Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use anyhow::Result;
- use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
- use std::os::fd::AsRawFd;
- use std::os::fd::IntoRawFd;
- use tempfile::tempfile;
-
- #[test]
- fn good_fd() -> Result<()> {
- let raw_fd = tempfile()?.into_raw_fd();
- assert!(take_fd_ownership(raw_fd).is_ok());
- Ok(())
- }
-
- #[test]
- fn invalid_fd() -> Result<()> {
- let raw_fd = 12345; // randomly chosen
- assert_eq!(take_fd_ownership(raw_fd).unwrap_err(), Error::Invalid(raw_fd));
- Ok(())
- }
-
- #[test]
- fn original_fd_closed() -> Result<()> {
- let raw_fd = tempfile()?.into_raw_fd();
- let owned_fd = take_fd_ownership(raw_fd)?;
- assert_ne!(raw_fd, owned_fd.as_raw_fd());
- assert!(fcntl(raw_fd, F_GETFD).is_err());
- Ok(())
- }
-
- #[test]
- fn cannot_use_same_rawfd_multiple_times() -> Result<()> {
- let raw_fd = tempfile()?.into_raw_fd();
-
- let owned_fd = take_fd_ownership(raw_fd); // once
- let owned_fd2 = take_fd_ownership(raw_fd); // twice
-
- assert!(owned_fd.is_ok());
- assert!(owned_fd2.is_err());
- Ok(())
- }
-
- #[test]
- fn cloexec() -> Result<()> {
- let raw_fd = tempfile()?.into_raw_fd();
-
- // intentionally clear cloexec to see if it is set by take_fd_ownership
- fcntl(raw_fd, F_SETFD(FdFlag::empty()))?;
- let flags = fcntl(raw_fd, F_GETFD)?;
- assert_eq!(flags, FdFlag::empty().bits());
-
- let owned_fd = take_fd_ownership(raw_fd)?;
- let flags = fcntl(owned_fd.as_raw_fd(), F_GETFD)?;
- assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
- drop(owned_fd);
- Ok(())
- }
-}
diff --git a/libs/vmconfig/src/lib.rs b/libs/vmconfig/src/lib.rs
index ff115f3..ef932c2 100644
--- a/libs/vmconfig/src/lib.rs
+++ b/libs/vmconfig/src/lib.rs
@@ -18,6 +18,7 @@
aidl::android::system::virtualizationservice::CpuTopology::CpuTopology,
aidl::android::system::virtualizationservice::DiskImage::DiskImage as AidlDiskImage,
aidl::android::system::virtualizationservice::Partition::Partition as AidlPartition,
+ aidl::android::system::virtualizationservice::UsbConfig::UsbConfig as AidlUsbConfig,
aidl::android::system::virtualizationservice::VirtualMachineAppConfig::DebugLevel::DebugLevel,
aidl::android::system::virtualizationservice::VirtualMachineConfig::VirtualMachineConfig,
aidl::android::system::virtualizationservice::VirtualMachineRawConfig::VirtualMachineRawConfig,
@@ -68,6 +69,8 @@
pub devices: Vec<PathBuf>,
/// The serial device for VM console input.
pub console_input_device: Option<String>,
+ /// The USB config of the VM.
+ pub usb_config: Option<UsbConfig>,
}
impl VmConfig {
@@ -110,6 +113,7 @@
Some("match_host") => CpuTopology::MATCH_HOST,
Some(cpu_topology) => bail!("Invalid cpu topology {}", cpu_topology),
};
+ let usb_config = self.usb_config.clone().map(|x| x.to_parcelable()).transpose()?;
Ok(VirtualMachineRawConfig {
kernel: maybe_open_parcel_file(&self.kernel, false)?,
initrd: maybe_open_parcel_file(&self.initrd, false)?,
@@ -128,6 +132,7 @@
})
.collect::<Result<_>>()?,
consoleInputDevice: self.console_input_device.clone(),
+ usbConfig: usb_config,
..Default::default()
})
}
@@ -193,6 +198,19 @@
}
}
+/// USB controller and available USB devices
+#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
+pub struct UsbConfig {
+ /// Enable USB controller
+ pub controller: bool,
+}
+
+impl UsbConfig {
+ fn to_parcelable(&self) -> Result<AidlUsbConfig> {
+ Ok(AidlUsbConfig { controller: self.controller })
+ }
+}
+
/// Try to open the given file and wrap it in a [`ParcelFileDescriptor`].
pub fn open_parcel_file(filename: &Path, writable: bool) -> Result<ParcelFileDescriptor> {
Ok(ParcelFileDescriptor::new(
diff --git a/microfuchsia/microfuchsiad/Android.bp b/microfuchsia/microfuchsiad/Android.bp
index ab3f865..ddf360d 100644
--- a/microfuchsia/microfuchsiad/Android.bp
+++ b/microfuchsia/microfuchsiad/Android.bp
@@ -15,9 +15,8 @@
"libandroid_logger",
"libanyhow",
"libbinder_rs",
- "liblibc",
"liblog_rust",
- "libsafe_ownedfd",
+ "liblibc",
"libvmclient",
],
apex_available: [
diff --git a/microfuchsia/microfuchsiad/src/instance_starter.rs b/microfuchsia/microfuchsiad/src/instance_starter.rs
index 6688447..15fcc06 100644
--- a/microfuchsia/microfuchsiad/src/instance_starter.rs
+++ b/microfuchsia/microfuchsiad/src/instance_starter.rs
@@ -23,10 +23,9 @@
use anyhow::{ensure, Context, Result};
use binder::{LazyServiceGuard, ParcelFileDescriptor};
use log::info;
-use safe_ownedfd::take_fd_ownership;
use std::ffi::CStr;
use std::fs::File;
-use std::os::fd::AsRawFd;
+use std::os::fd::FromRawFd;
use vmclient::VmInstance;
pub struct MicrofuchsiaInstance {
@@ -134,7 +133,6 @@
"failed to openpty"
);
}
- let leader = take_fd_ownership(leader)?;
// SAFETY: calling these libc functions with valid+initialized variables is safe.
unsafe {
@@ -147,25 +145,24 @@
c_line: 0,
c_cc: [0u8; 19],
};
- ensure!(
- libc::tcgetattr(leader.as_raw_fd(), &mut attr) == 0,
- "failed to get termios attributes"
- );
+ ensure!(libc::tcgetattr(leader, &mut attr) == 0, "failed to get termios attributes");
// Force it to be a raw pty and re-set it.
libc::cfmakeraw(&mut attr);
ensure!(
- libc::tcsetattr(leader.as_raw_fd(), libc::TCSANOW, &attr) == 0,
+ libc::tcsetattr(leader, libc::TCSANOW, &attr) == 0,
"failed to set termios attributes"
);
}
// Construct the return value.
+ // SAFETY: The file descriptors are valid because openpty returned without error (above).
+ let leader = unsafe { File::from_raw_fd(leader) };
let follower_name: Vec<u8> = follower_name.iter_mut().map(|x| *x as _).collect();
let follower_name = CStr::from_bytes_until_nul(&follower_name)
.context("pty filename missing NUL")?
.to_str()
.context("pty filename invalid utf8")?
.to_string();
- Ok(Pty { leader: File::from(leader), follower_name })
+ Ok(Pty { leader, follower_name })
}
diff --git a/tests/vm_accessor/accessor/src/accessor.rs b/tests/vm_accessor/accessor/src/accessor.rs
index 6a9ced6..966bffb 100644
--- a/tests/vm_accessor/accessor/src/accessor.rs
+++ b/tests/vm_accessor/accessor/src/accessor.rs
@@ -31,11 +31,12 @@
// because 'trait Interface' requires 'static.
vm: VmInstance,
port: i32,
+ instance: String,
}
impl Accessor {
- pub fn new(vm: VmInstance, port: i32) -> Self {
- Self { vm, port }
+ pub fn new(vm: VmInstance, port: i32, instance: &str) -> Self {
+ Self { vm, port, instance: instance.into() }
}
}
@@ -43,10 +44,13 @@
impl IAccessor for Accessor {
fn addConnection(&self) -> binder::Result<ParcelFileDescriptor> {
- self.vm.wait_until_ready(Duration::from_secs(10)).unwrap();
+ self.vm.wait_until_ready(Duration::from_secs(20)).unwrap();
info!("VM is ready. Connecting to service via port {}", self.port);
self.vm.vm.connectVsock(self.port)
}
+ fn getInstanceName(&self) -> binder::Result<String> {
+ Ok(self.instance.clone())
+ }
}
diff --git a/tests/vm_accessor/accessor/src/main.rs b/tests/vm_accessor/accessor/src/main.rs
index 27ce415..49f5794 100644
--- a/tests/vm_accessor/accessor/src/main.rs
+++ b/tests/vm_accessor/accessor/src/main.rs
@@ -42,7 +42,7 @@
let vm = run_vm()?;
// If you want to serve multiple services in a VM, then register Accessor impls multiple times.
- let accessor = Accessor::new(vm, PORT);
+ let accessor = Accessor::new(vm, PORT, SERVICE_NAME);
let accessor_binder = BnAccessor::new_binder(accessor, BinderFeatures::default());
binder::register_lazy_service(SERVICE_NAME, accessor_binder.as_binder()).map_err(|e| {
anyhow!("Failed to register lazy service, service={SERVICE_NAME}, err={e:?}",)