Merge "pvmfw: Add DtPathTokens" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index f146b4e..ec9042c 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -12,6 +12,9 @@
"name": "MicrodroidTestApp"
},
{
+ "name": "MicrodroidTestAppNoPerm"
+ },
+ {
"name": "VmAttestationTestApp"
},
{
@@ -54,6 +57,11 @@
},
{
"name": "AVFHostTestCases"
+ },
+ {
+ // TODO(b/325610326): Add this target to presubmit once there is enough
+ // SLO data for it.
+ "name": "AvfRkpdAppIntegrationTests"
}
],
"postsubmit": [
diff --git a/apex/Android.bp b/apex/Android.bp
index 399ad97..3b5141e 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -254,6 +254,7 @@
"initrd_bootconfig",
"lpmake",
"lpunpack",
+ "lz4",
"simg2img",
],
}
@@ -274,6 +275,7 @@
"initrd_bootconfig",
"lpmake",
"lpunpack",
+ "lz4",
"sign_virt_apex",
"simg2img",
],
diff --git a/apex/sign_virt_apex.py b/apex/sign_virt_apex.py
index 0b6137b..7c59b54 100644
--- a/apex/sign_virt_apex.py
+++ b/apex/sign_virt_apex.py
@@ -153,12 +153,18 @@
'--key', key, '--output', output])
+def is_lz4(args, path):
+ # error 44: Unrecognized header
+ result = RunCommand(args, ['lz4', '-t', path], expected_return_values={0, 44})
+ return result[1] == 0
+
+
def AvbInfo(args, image_path):
"""Parses avbtool --info image output
Args:
args: program arguments.
- image_path: The path to the image.
+ image_path: The path to the image, either raw or lz4 compressed
descriptor_name: Descriptor name of interest.
Returns:
@@ -169,6 +175,11 @@
if not os.path.exists(image_path):
raise ValueError(f'Failed to find image: {image_path}')
+ if is_lz4(args, image_path):
+ with tempfile.NamedTemporaryFile() as decompressed_image:
+ RunCommand(args, ['lz4', '-d', '-f', image_path, decompressed_image.name])
+ return AvbInfo(args, decompressed_image.name)
+
output, ret_code = RunCommand(
args, ['avbtool', 'info_image', '--image', image_path], expected_return_values={0, 1})
if ret_code == 1:
@@ -560,11 +571,7 @@
wait=[vbmeta_f])
# Re-sign kernel. Note kernel's vbmeta contain addition descriptor from ramdisk(s)
- def resign_kernel(kernel, initrd_normal, initrd_debug):
- kernel_file = files[kernel]
- initrd_normal_file = files[initrd_normal]
- initrd_debug_file = files[initrd_debug]
-
+ def resign_decompressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file):
_, kernel_image_descriptors = AvbInfo(args, kernel_file)
salts = extract_hash_descriptors(
kernel_image_descriptors, lambda descriptor: descriptor['Salt'])
@@ -580,21 +587,47 @@
additional_images=[initrd_normal_hashdesc, initrd_debug_hashdesc],
wait=[initrd_n_f, initrd_d_f])
+ def resign_compressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file):
+ # decompress, re-sign, compress again
+ with tempfile.TemporaryDirectory() as work_dir:
+ decompressed_kernel_file = os.path.join(work_dir, os.path.basename(kernel_file))
+ RunCommand(args, ['lz4', '-d', kernel_file, decompressed_kernel_file])
+ resign_decompressed_kernel(decompressed_kernel_file, initrd_normal_file,
+ initrd_debug_file).result()
+ RunCommand(args, ['lz4', '-9', '-f', decompressed_kernel_file, kernel_file])
+
+ def resign_kernel(kernel, initrd_normal, initrd_debug):
+ kernel_file = files[kernel]
+ initrd_normal_file = files[initrd_normal]
+ initrd_debug_file = files[initrd_debug]
+
+ # kernel may be compressed with lz4.
+ if is_lz4(args, kernel_file):
+ return Async(resign_compressed_kernel, kernel_file, initrd_normal_file,
+ initrd_debug_file)
+ else:
+ return resign_decompressed_kernel(kernel_file, initrd_normal_file, initrd_debug_file)
+
_, original_kernel_descriptors = AvbInfo(args, files['kernel'])
- resign_kernel_task = resign_kernel('kernel', 'initrd_normal.img', 'initrd_debuggable.img')
+ resign_kernel_tasks = [resign_kernel('kernel', 'initrd_normal.img', 'initrd_debuggable.img')]
+ original_kernels = {"kernel" : original_kernel_descriptors}
for ver in gki_versions:
if f'gki-{ver}_kernel' in files:
- resign_kernel(
- f'gki-{ver}_kernel',
+ kernel_name = f'gki-{ver}_kernel'
+ _, original_kernel_descriptors = AvbInfo(args, files[kernel_name])
+ task = resign_kernel(
+ kernel_name,
f'gki-{ver}_initrd_normal.img',
f'gki-{ver}_initrd_debuggable.img')
+ resign_kernel_tasks.append(task)
+ original_kernels[kernel_name] = original_kernel_descriptors
# Re-sign rialto if it exists. Rialto only exists in arm64 environment.
if os.path.exists(files['rialto']):
update_initrd_digests_task = Async(
- update_initrd_digests_in_rialto, original_kernel_descriptors, args,
- files, wait=[resign_kernel_task])
+ update_initrd_digests_of_kernels_in_rialto, original_kernels, args, files,
+ wait=resign_kernel_tasks)
Async(resign_rialto, args, key, files['rialto'], wait=[update_initrd_digests_task])
def resign_rialto(args, key, rialto_path):
@@ -628,18 +661,7 @@
f"Value of '{key}' should change for '{context}'" \
f"Original value: {original[key]}, updated value: {updated[key]}"
-def update_initrd_digests_in_rialto(original_descriptors, args, files):
- _, updated_descriptors = AvbInfo(args, files['kernel'])
-
- original_digests = extract_hash_descriptors(
- original_descriptors, lambda x: binascii.unhexlify(x['Digest']))
- updated_digests = extract_hash_descriptors(
- updated_descriptors, lambda x: binascii.unhexlify(x['Digest']))
- assert original_digests.pop("boot") == updated_digests.pop("boot"), \
- "Hash descriptor of boot should not change for kernel. " \
- f"Original descriptors: {original_descriptors}, " \
- f"updated descriptors: {updated_descriptors}"
-
+def update_initrd_digests_of_kernels_in_rialto(original_kernels, args, files):
# Update the hashes of initrd_normal and initrd_debug in rialto if the
# bootconfigs in them are updated.
if args.do_not_update_bootconfigs:
@@ -648,6 +670,26 @@
with open(files['rialto'], "rb") as file:
content = file.read()
+ for kernel_name, descriptors in original_kernels.items():
+ content = update_initrd_digests_in_rialto(
+ descriptors, args, files, kernel_name, content)
+
+ with open(files['rialto'], "wb") as file:
+ file.write(content)
+
+def update_initrd_digests_in_rialto(
+ original_descriptors, args, files, kernel_name, content):
+ _, updated_descriptors = AvbInfo(args, files[kernel_name])
+
+ original_digests = extract_hash_descriptors(
+ original_descriptors, lambda x: binascii.unhexlify(x['Digest']))
+ updated_digests = extract_hash_descriptors(
+ updated_descriptors, lambda x: binascii.unhexlify(x['Digest']))
+ assert original_digests.pop("boot") == updated_digests.pop("boot"), \
+ "Hash descriptor of boot should not change for " + kernel_name + \
+ f"\nOriginal descriptors: {original_descriptors}, " \
+ f"\nUpdated descriptors: {updated_descriptors}"
+
# Check that the original and updated digests are different before updating rialto.
partition_names = {'initrd_normal', 'initrd_debug'}
assert set(original_digests.keys()) == set(updated_digests.keys()) == partition_names, \
@@ -671,8 +713,7 @@
f"original digest of the partition {partition_name} not found."
content = new_content
- with open(files['rialto'], "wb") as file:
- file.write(content)
+ return content
def extract_hash_descriptors(descriptors, f=lambda x: x):
return {desc["Partition Name"]: f(desc) for desc in
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index abaa74c..077a0ef 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -72,6 +72,7 @@
/// Start a new CompOS VM instance using the specified instance image file and parameters.
pub fn start(
service: &dyn IVirtualizationService,
+ instance_id: [u8; 64],
instance_image: File,
idsig: &Path,
idsig_manifest_apk: &Path,
@@ -121,6 +122,7 @@
name: parameters.name.clone(),
apk: Some(apk_fd),
idsig: Some(idsig_fd),
+ instanceId: instance_id,
instanceImage: Some(instance_fd),
encryptedStorageImage: None,
payload: Payload::ConfigPath(config_path),
diff --git a/compos/common/lib.rs b/compos/common/lib.rs
index 1f937c9..9d90717 100644
--- a/compos/common/lib.rs
+++ b/compos/common/lib.rs
@@ -39,6 +39,9 @@
/// tests.
pub const TEST_INSTANCE_DIR: &str = "test";
+/// The file that holds the instance_id of CompOS instance.
+pub const INSTANCE_ID_FILE: &str = "instance_id";
+
/// The file that holds the instance image for a CompOS instance.
pub const INSTANCE_IMAGE_FILE: &str = "instance.img";
diff --git a/compos/composd/src/instance_starter.rs b/compos/composd/src/instance_starter.rs
index 457520f..76001a4 100644
--- a/compos/composd/src/instance_starter.rs
+++ b/compos/composd/src/instance_starter.rs
@@ -20,13 +20,13 @@
use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
};
-use anyhow::{Context, Result};
+use anyhow::{anyhow, Context, Result};
use binder::{LazyServiceGuard, ParcelFileDescriptor, Strong};
use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
use compos_common::compos_client::{ComposClient, VmParameters};
use compos_common::{
COMPOS_DATA_ROOT, IDSIG_FILE, IDSIG_MANIFEST_APK_FILE, IDSIG_MANIFEST_EXT_APK_FILE,
- INSTANCE_IMAGE_FILE,
+ INSTANCE_ID_FILE, INSTANCE_IMAGE_FILE,
};
use log::info;
use std::fs;
@@ -66,6 +66,7 @@
pub struct InstanceStarter {
instance_name: String,
instance_root: PathBuf,
+ instance_id_file: PathBuf,
instance_image: PathBuf,
idsig: PathBuf,
idsig_manifest_apk: PathBuf,
@@ -77,6 +78,7 @@
pub fn new(instance_name: &str, vm_parameters: VmParameters) -> Self {
let instance_root = Path::new(COMPOS_DATA_ROOT).join(instance_name);
let instance_root_path = instance_root.as_path();
+ let instance_id_file = instance_root_path.join(INSTANCE_ID_FILE);
let instance_image = instance_root_path.join(INSTANCE_IMAGE_FILE);
let idsig = instance_root_path.join(IDSIG_FILE);
let idsig_manifest_apk = instance_root_path.join(IDSIG_MANIFEST_APK_FILE);
@@ -84,6 +86,7 @@
Self {
instance_name: instance_name.to_owned(),
instance_root,
+ instance_id_file,
instance_image,
idsig,
idsig_manifest_apk,
@@ -103,7 +106,10 @@
// Overwrite any existing instance - it's unlikely to be valid with the current set
// of APEXes, and finding out it isn't is much more expensive than creating a new one.
self.create_instance_image(virtualization_service)?;
-
+ // TODO(b/294177871): Ping VS to delete the old instance's secret.
+ if cfg!(llpvm_changes) {
+ self.allocate_instance_id(virtualization_service)?;
+ }
// Delete existing idsig files. Ignore error in case idsig doesn't exist.
let _ignored1 = fs::remove_file(&self.idsig);
let _ignored2 = fs::remove_file(&self.idsig_manifest_apk);
@@ -122,6 +128,14 @@
&self,
virtualization_service: &dyn IVirtualizationService,
) -> Result<CompOsInstance> {
+ let instance_id: [u8; 64] = if cfg!(llpvm_changes) {
+ fs::read(&self.instance_id_file)?
+ .try_into()
+ .map_err(|_| anyhow!("Failed to get instance_id"))?
+ } else {
+ [0u8; 64]
+ };
+
let instance_image = fs::OpenOptions::new()
.read(true)
.write(true)
@@ -129,6 +143,7 @@
.context("Failed to open instance image")?;
let vm_instance = ComposClient::start(
virtualization_service,
+ instance_id,
instance_image,
&self.idsig,
&self.idsig_manifest_apk,
@@ -164,4 +179,13 @@
.context("Writing instance image file")?;
Ok(())
}
+
+ fn allocate_instance_id(
+ &self,
+ virtualization_service: &dyn IVirtualizationService,
+ ) -> Result<()> {
+ let id = virtualization_service.allocateInstanceId().context("Allocating Instance Id")?;
+ fs::write(&self.instance_id_file, id)?;
+ Ok(())
+ }
}
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 567083d..a3f18d5 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -18,7 +18,7 @@
//! public key. The tool is intended to be run by odsign during boot.
use android_logger::LogId;
-use anyhow::{bail, Context, Result};
+use anyhow::{anyhow, bail, Context, Result};
use binder::ProcessState;
use clap::{Parser, ValueEnum};
use compos_common::compos_client::{ComposClient, VmCpuTopology, VmParameters};
@@ -28,9 +28,10 @@
};
use compos_common::{
COMPOS_DATA_ROOT, CURRENT_INSTANCE_DIR, IDSIG_FILE, IDSIG_MANIFEST_APK_FILE,
- IDSIG_MANIFEST_EXT_APK_FILE, INSTANCE_IMAGE_FILE, TEST_INSTANCE_DIR,
+ IDSIG_MANIFEST_EXT_APK_FILE, INSTANCE_ID_FILE, INSTANCE_IMAGE_FILE, TEST_INSTANCE_DIR,
};
use log::error;
+use std::fs;
use std::fs::File;
use std::io::Read;
use std::panic;
@@ -90,11 +91,17 @@
bail!("{:?} is not a directory", instance_dir);
}
+ let instance_id_file = instance_dir.join(INSTANCE_ID_FILE);
let instance_image = instance_dir.join(INSTANCE_IMAGE_FILE);
let idsig = instance_dir.join(IDSIG_FILE);
let idsig_manifest_apk = instance_dir.join(IDSIG_MANIFEST_APK_FILE);
let idsig_manifest_ext_apk = instance_dir.join(IDSIG_MANIFEST_EXT_APK_FILE);
+ let instance_id: [u8; 64] = if cfg!(llpvm_changes) {
+ fs::read(instance_id_file)?.try_into().map_err(|_| anyhow!("Failed to get instance_id"))?
+ } else {
+ [0u8; 64]
+ };
let instance_image = File::open(instance_image).context("Failed to open instance image")?;
let info = artifacts_dir.join("compos.info");
@@ -110,6 +117,7 @@
let virtualization_service = virtmgr.connect()?;
let vm_instance = ComposClient::start(
&*virtualization_service,
+ instance_id,
instance_image,
&idsig,
&idsig_manifest_apk,
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index 6b03cfe..a5c8062 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -190,6 +190,9 @@
/** Name of the instance image file for a VM. (Not implemented) */
private static final String INSTANCE_IMAGE_FILE = "instance.img";
+ /** Name of the file for a VM containing Id. */
+ private static final String INSTANCE_ID_FILE = "instance_id";
+
/** Name of the idsig file for a VM */
private static final String IDSIG_FILE = "idsig";
@@ -227,6 +230,9 @@
/** File that backs the encrypted storage - Will be null if not enabled. */
@Nullable private final File mEncryptedStoreFilePath;
+ /** File that contains the Id. This is NULL iff FEATURE_LLPVM is disabled */
+ @Nullable private final File mInstanceIdPath;
+
/**
* Unmodifiable list of extra apks. Apks are specified by the vm config, and corresponding
* idsigs are to be generated.
@@ -376,6 +382,16 @@
File thisVmDir = getVmDir(context, mName);
mVmRootPath = thisVmDir;
mConfigFilePath = new File(thisVmDir, CONFIG_FILE);
+ try {
+ mInstanceIdPath =
+ (mVirtualizationService
+ .getBinder()
+ .isFeatureEnabled(IVirtualizationService.FEATURE_LLPVM_CHANGES))
+ ? new File(thisVmDir, INSTANCE_ID_FILE)
+ : null;
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
mInstanceFilePath = new File(thisVmDir, INSTANCE_IMAGE_FILE);
mIdsigFilePath = new File(thisVmDir, IDSIG_FILE);
mExtraApks = setupExtraApks(context, config, thisVmDir);
@@ -414,6 +430,10 @@
VirtualMachineConfig config = VirtualMachineConfig.from(vmDescriptor.getConfigFd());
vm = new VirtualMachine(context, name, config, VirtualizationService.getInstance());
config.serialize(vm.mConfigFilePath);
+ if (vm.mInstanceIdPath != null) {
+ vm.importInstanceIdFrom(vmDescriptor.getInstanceIdFd());
+ }
+
try {
vm.mInstanceFilePath.createNewFile();
} catch (IOException e) {
@@ -475,6 +495,21 @@
IVirtualizationService service = vm.mVirtualizationService.getBinder();
+ if (vm.mInstanceIdPath != null) {
+ try (FileOutputStream stream = new FileOutputStream(vm.mInstanceIdPath)) {
+ byte[] id = service.allocateInstanceId();
+ stream.write(id);
+ } catch (FileNotFoundException e) {
+ throw new VirtualMachineException("instance_id file missing", e);
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to persist instance_id", e);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ } catch (ServiceSpecificException | IllegalArgumentException e) {
+ throw new VirtualMachineException("failed to create instance_id", e);
+ }
+ }
+
try {
service.initializeWritablePartition(
ParcelFileDescriptor.open(vm.mInstanceFilePath, MODE_READ_WRITE),
@@ -530,6 +565,9 @@
VirtualMachine vm =
new VirtualMachine(context, name, config, VirtualizationService.getInstance());
+ if (vm.mInstanceIdPath != null && !vm.mInstanceIdPath.exists()) {
+ throw new VirtualMachineException("instance_id file missing");
+ }
if (!vm.mInstanceFilePath.exists()) {
throw new VirtualMachineException("instance image missing");
}
@@ -547,6 +585,7 @@
// if a new VM is created with the same name (and files) that's unrelated.
mWasDeleted = true;
}
+ // TODO(b/294177871): Request deletion of VM secrets.
deleteVmDirectory(context, name);
}
@@ -813,7 +852,19 @@
VirtualMachineConfig vmConfig = getConfig();
VirtualMachineAppConfig appConfig =
vmConfig.toVsConfig(mContext.getPackageManager());
+ appConfig.instanceImage =
+ ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
appConfig.name = mName;
+ if (mInstanceIdPath != null) {
+ appConfig.instanceId = Files.readAllBytes(mInstanceIdPath.toPath());
+ } else {
+ // FEATURE_LLPVM_CHANGES is disabled, instance_id is not used.
+ appConfig.instanceId = new byte[64];
+ }
+ if (mEncryptedStoreFilePath != null) {
+ appConfig.encryptedStorageImage =
+ ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
+ }
if (!vmConfig.getExtraApks().isEmpty()) {
// Extra APKs were specified directly, rather than via config file.
@@ -835,7 +886,7 @@
}
try {
- createIdSigs(service, appConfig);
+ createIdSigsAndUpdateConfig(service, appConfig);
} catch (FileNotFoundException e) {
throw new VirtualMachineException("Failed to generate APK signature", e);
}
@@ -850,6 +901,8 @@
mVirtualMachine.registerCallback(new CallbackTranslator(service));
mContext.registerComponentCallbacks(mMemoryManagementCallbacks);
mVirtualMachine.start();
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to persist files", e);
} catch (IllegalStateException | ServiceSpecificException e) {
throw new VirtualMachineException(e);
} catch (RemoteException e) {
@@ -858,7 +911,8 @@
}
}
- private void createIdSigs(IVirtualizationService service, VirtualMachineAppConfig appConfig)
+ private void createIdSigsAndUpdateConfig(
+ IVirtualizationService service, VirtualMachineAppConfig appConfig)
throws RemoteException, FileNotFoundException {
// Fill the idsig file by hashing the apk
service.createOrUpdateIdsigFile(
@@ -872,11 +926,6 @@
// Re-open idsig files in read-only mode
appConfig.idsig = ParcelFileDescriptor.open(mIdsigFilePath, MODE_READ_ONLY);
- appConfig.instanceImage = ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_WRITE);
- if (mEncryptedStoreFilePath != null) {
- appConfig.encryptedStorageImage =
- ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_WRITE);
- }
List<ParcelFileDescriptor> extraIdsigs = new ArrayList<>();
for (ExtraApkSpec extraApk : mExtraApks) {
extraIdsigs.add(ParcelFileDescriptor.open(extraApk.idsig, MODE_READ_ONLY));
@@ -1249,6 +1298,9 @@
try {
return new VirtualMachineDescriptor(
ParcelFileDescriptor.open(mConfigFilePath, MODE_READ_ONLY),
+ mInstanceIdPath != null
+ ? ParcelFileDescriptor.open(mInstanceIdPath, MODE_READ_ONLY)
+ : null,
ParcelFileDescriptor.open(mInstanceFilePath, MODE_READ_ONLY),
mEncryptedStoreFilePath != null
? ParcelFileDescriptor.open(mEncryptedStoreFilePath, MODE_READ_ONLY)
@@ -1384,6 +1436,16 @@
return extraApks;
}
+ private void importInstanceIdFrom(@NonNull ParcelFileDescriptor instanceIdFd)
+ throws VirtualMachineException {
+ try (FileChannel idOutput = new FileOutputStream(mInstanceIdPath).getChannel();
+ FileChannel idInput = new AutoCloseInputStream(instanceIdFd).getChannel()) {
+ idOutput.transferFrom(idInput, /*position=*/ 0, idInput.size());
+ } catch (IOException e) {
+ throw new VirtualMachineException("failed to copy instance_id", e);
+ }
+ }
+
private void importInstanceFrom(@NonNull ParcelFileDescriptor instanceFd)
throws VirtualMachineException {
try (FileChannel instance = new FileOutputStream(mInstanceFilePath).getChannel();
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
index 710925d..ee79256 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineDescriptor.java
@@ -40,6 +40,9 @@
public final class VirtualMachineDescriptor implements Parcelable, AutoCloseable {
private volatile boolean mClosed = false;
@NonNull private final ParcelFileDescriptor mConfigFd;
+ // File descriptor of the file containing the instance id - will be null iff
+ // FEATURE_LLPVM_CHANGES is disabled.
+ @Nullable private final ParcelFileDescriptor mInstanceIdFd;
@NonNull private final ParcelFileDescriptor mInstanceImgFd;
// File descriptor of the image backing the encrypted storage - Will be null if encrypted
// storage is not enabled. */
@@ -54,6 +57,7 @@
public void writeToParcel(@NonNull Parcel out, int flags) {
checkNotClosed();
out.writeParcelable(mConfigFd, flags);
+ out.writeParcelable(mInstanceIdFd, flags);
out.writeParcelable(mInstanceImgFd, flags);
out.writeParcelable(mEncryptedStoreFd, flags);
}
@@ -80,6 +84,15 @@
}
/**
+ * @return File descriptor of the file containing instance_id of the VM.
+ */
+ @Nullable
+ ParcelFileDescriptor getInstanceIdFd() {
+ checkNotClosed();
+ return mInstanceIdFd;
+ }
+
+ /**
* @return File descriptor of the instance.img of the VM.
*/
@NonNull
@@ -100,15 +113,18 @@
VirtualMachineDescriptor(
@NonNull ParcelFileDescriptor configFd,
+ @Nullable ParcelFileDescriptor instanceIdFd,
@NonNull ParcelFileDescriptor instanceImgFd,
@Nullable ParcelFileDescriptor encryptedStoreFd) {
mConfigFd = requireNonNull(configFd);
+ mInstanceIdFd = instanceIdFd;
mInstanceImgFd = requireNonNull(instanceImgFd);
mEncryptedStoreFd = encryptedStoreFd;
}
private VirtualMachineDescriptor(Parcel in) {
mConfigFd = requireNonNull(readParcelFileDescriptor(in));
+ mInstanceIdFd = readParcelFileDescriptor(in);
mInstanceImgFd = requireNonNull(readParcelFileDescriptor(in));
mEncryptedStoreFd = readParcelFileDescriptor(in);
}
@@ -127,6 +143,7 @@
mClosed = true;
// Let the compiler do the work: close everything, throw if any of them fail, skipping null.
try (mConfigFd;
+ mInstanceIdFd;
mInstanceImgFd;
mEncryptedStoreFd) {
} catch (IOException ignored) {
diff --git a/java/service/Android.bp b/java/service/Android.bp
index fdfb203..8bac7be 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -29,6 +29,9 @@
"framework",
"services.core",
],
+ static_libs: [
+ "android.system.virtualizationmaintenance-java",
+ ],
sdk_version: "core_platform",
apex_available: ["com.android.virt"],
installable: true,
diff --git a/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
new file mode 100644
index 0000000..473fbfb
--- /dev/null
+++ b/java/service/src/com/android/system/virtualmachine/SecretkeeperJobService.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.system.virtualmachine;
+
+import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.os.UserHandle;
+import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+import android.util.Log;
+
+import com.android.server.LocalServices;
+import com.android.server.pm.UserManagerInternal;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A job scheduler service responsible for triggering the Virtualization Service reconciliation
+ * process when scheduled. The job is scheduled to run once per day while idle and charging.
+ *
+ * <p>The reconciliation process ensures that Secretkeeper secrets belonging to apps or users that
+ * have been removed get deleted.
+ *
+ * @hide
+ */
+public class SecretkeeperJobService extends JobService {
+ private static final String TAG = SecretkeeperJobService.class.getName();
+ private static final String JOBSCHEDULER_NAMESPACE = "VirtualizationSystemService";
+ private static final int JOB_ID = 1;
+ private static final AtomicReference<SecretkeeperJob> sJob = new AtomicReference<>();
+
+ static void scheduleJob(JobScheduler scheduler) {
+ try {
+ ComponentName serviceName =
+ new ComponentName("android", SecretkeeperJobService.class.getName());
+ scheduler = scheduler.forNamespace(JOBSCHEDULER_NAMESPACE);
+ if (scheduler.schedule(
+ new JobInfo.Builder(JOB_ID, serviceName)
+ // We consume CPU and power
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setPeriodic(24 * 60 * 60 * 1000L)
+ .build())
+ != JobScheduler.RESULT_SUCCESS) {
+ Log.e(TAG, "Unable to schedule job");
+ return;
+ }
+ Log.i(TAG, "Scheduled job");
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to schedule job", e);
+ }
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ Log.i(TAG, "Starting job");
+
+ SecretkeeperJob job = new SecretkeeperJob(getPackageManager());
+ sJob.set(job);
+
+ new Thread("SecretkeeperJob") {
+ @Override
+ public void run() {
+ try {
+ job.run();
+ Log.i(TAG, "Job finished");
+ } catch (Exception e) {
+ Log.e(TAG, "Job failed", e);
+ }
+ sJob.set(null);
+ // We don't reschedule on error, we will try again the next day anyway.
+ jobFinished(params, /*wantReschedule=*/ false);
+ }
+ }.start();
+
+ return true; // Job is running in the background
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ Log.i(TAG, "Stopping job");
+ SecretkeeperJob job = sJob.getAndSet(null);
+ if (job != null) {
+ job.stop();
+ }
+ return false; // Idle jobs get rescheduled anyway
+ }
+
+ private static class SecretkeeperJob {
+ private final UserManagerInternal mUserManager =
+ LocalServices.getService(UserManagerInternal.class);
+ private volatile boolean mStopRequested = false;
+ private PackageManager mPackageManager;
+
+ public SecretkeeperJob(PackageManager packageManager) {
+ mPackageManager = packageManager;
+ }
+
+ public void run() throws RemoteException {
+ IVirtualizationMaintenance maintenance =
+ VirtualizationSystemService.connectToMaintenanceService();
+ maintenance.performReconciliation(new Callback());
+ }
+
+ public void stop() {
+ mStopRequested = true;
+ }
+
+ class Callback extends IVirtualizationReconciliationCallback.Stub {
+ @Override
+ public boolean[] doUsersExist(int[] userIds) {
+ checkForStop();
+ int[] currentUsers = mUserManager.getUserIds();
+ boolean[] results = new boolean[userIds.length];
+ for (int i = 0; i < userIds.length; i++) {
+ // The total number of users is likely to be small, so no need to make this
+ // better than O(N).
+ for (int user : currentUsers) {
+ if (user == userIds[i]) {
+ results[i] = true;
+ break;
+ }
+ }
+ }
+ return results;
+ }
+
+ @Override
+ public boolean[] doAppsExist(int userId, int[] appIds) {
+ checkForStop();
+
+ // If an app has been uninstalled but its data is still present we want to include
+ // it, since that might include a VM which will be used in the future.
+ ApplicationInfoFlags flags = ApplicationInfoFlags.of(MATCH_UNINSTALLED_PACKAGES);
+ List<ApplicationInfo> appInfos =
+ mPackageManager.getInstalledApplicationsAsUser(flags, userId);
+ int[] currentAppIds = new int[appInfos.size()];
+ for (int i = 0; i < appInfos.size(); i++) {
+ currentAppIds[i] = UserHandle.getAppId(appInfos.get(i).uid);
+ }
+ Arrays.sort(currentAppIds);
+
+ boolean[] results = new boolean[appIds.length];
+ for (int i = 0; i < appIds.length; i++) {
+ results[i] = Arrays.binarySearch(currentAppIds, appIds[i]) >= 0;
+ }
+
+ return results;
+ }
+
+ private void checkForStop() {
+ if (mStopRequested) {
+ throw new ServiceSpecificException(ERROR_STOP_REQUESTED, "Stop requested");
+ }
+ }
+ }
+ }
+}
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2905acd..2461755 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -16,16 +16,124 @@
package com.android.system.virtualmachine;
+import android.app.job.JobScheduler;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.util.Log;
+
+import com.android.internal.os.BackgroundThread;
import com.android.server.SystemService;
-/** TODO */
+/**
+ * This class exists to notify virtualization service of relevant things happening in the Android
+ * framework.
+ *
+ * <p>It currently is responsible for Secretkeeper-related maintenance - ensuring that we are not
+ * storing secrets for apps or users that no longer exist.
+ */
public class VirtualizationSystemService extends SystemService {
+ private static final String TAG = VirtualizationSystemService.class.getName();
+ private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
+ private Handler mHandler;
public VirtualizationSystemService(Context context) {
super(context);
}
@Override
- public void onStart() {}
+ public void onStart() {
+ // Nothing needed here - we don't expose any binder service. The binder service we use is
+ // exposed as a lazy service by the virtualizationservice native binary.
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ if (phase != PHASE_BOOT_COMPLETED) return;
+
+ mHandler = BackgroundThread.getHandler();
+ new Receiver().registerForBroadcasts();
+
+ SecretkeeperJobService.scheduleJob(getContext().getSystemService(JobScheduler.class));
+ }
+
+ private void notifyAppRemoved(int uid) {
+ try {
+ IVirtualizationMaintenance maintenance = connectToMaintenanceService();
+ maintenance.appRemoved(UserHandle.getUserId(uid), UserHandle.getAppId(uid));
+ } catch (Exception e) {
+ Log.e(TAG, "notifyAppRemoved failed", e);
+ }
+ }
+
+ private void notifyUserRemoved(int userId) {
+ try {
+ IVirtualizationMaintenance maintenance = connectToMaintenanceService();
+ maintenance.userRemoved(userId);
+ } catch (Exception e) {
+ Log.e(TAG, "notifyUserRemoved failed", e);
+ }
+ }
+
+ static IVirtualizationMaintenance connectToMaintenanceService() {
+ IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
+ IVirtualizationMaintenance maintenance =
+ IVirtualizationMaintenance.Stub.asInterface(binder);
+ if (maintenance == null) {
+ throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
+ }
+ return maintenance;
+ }
+
+ private class Receiver extends BroadcastReceiver {
+ public void registerForBroadcasts() {
+ Context allUsers = getContext().createContextAsUser(UserHandle.ALL, 0 /* flags */);
+
+ allUsers.registerReceiver(this, new IntentFilter(Intent.ACTION_USER_REMOVED));
+
+ IntentFilter packageFilter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+ packageFilter.addDataScheme("package");
+ allUsers.registerReceiver(this, packageFilter);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case Intent.ACTION_USER_REMOVED:
+ onUserRemoved(intent);
+ break;
+ case Intent.ACTION_PACKAGE_REMOVED:
+ onPackageRemoved(intent);
+ break;
+ default:
+ Log.e(TAG, "received unexpected intent: " + intent.getAction());
+ break;
+ }
+ }
+
+ private void onUserRemoved(Intent intent) {
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+ if (userId != UserHandle.USER_NULL) {
+ mHandler.post(() -> notifyUserRemoved(userId));
+ }
+ }
+
+ private void onPackageRemoved(Intent intent) {
+ if (intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)
+ || !intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false)) {
+ // Package is being updated rather than uninstalled.
+ return;
+ }
+ int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
+ if (uid != -1) {
+ mHandler.post(() -> notifyAppRemoved(uid));
+ }
+ }
+ }
}
diff --git a/microdroid/Android.bp b/microdroid/Android.bp
index 36688fc..999dc52 100644
--- a/microdroid/Android.bp
+++ b/microdroid/Android.bp
@@ -559,7 +559,7 @@
avb_add_hash_footer {
name: "microdroid_gki-android14-6.1_kernel_signed",
defaults: ["microdroid_kernel_signed_defaults"],
- filename: "microdroid_gki-android14-6.1_kernel",
+ filename: "microdroid_gki-android14-6.1_kernel_signed",
arch: {
arm64: {
src: ":microdroid_gki_kernel_prebuilts-6.1-arm64",
@@ -574,13 +574,29 @@
],
}
+// HACK: use cc_genrule for arch-specific properties
+cc_genrule {
+ name: "microdroid_gki-android14-6.1_kernel_signed-lz4",
+ out: ["microdroid_gki-android14-6.1_kernel_signed-lz4"],
+ srcs: [":empty_file"],
+ arch: {
+ arm64: {
+ srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+ exclude_srcs: [":empty_file"],
+ },
+ },
+ tools: ["lz4"],
+ cmd: "$(location lz4) -9 $(in) $(out)",
+}
+
prebuilt_etc {
name: "microdroid_gki-android14-6.1_kernel",
+ filename: "microdroid_gki-android14-6.1_kernel",
src: ":empty_file",
relative_install_path: "fs",
arch: {
arm64: {
- src: ":microdroid_gki-android14-6.1_kernel_signed",
+ src: ":microdroid_gki-android14-6.1_kernel_signed-lz4",
},
x86_64: {
src: ":microdroid_gki-android14-6.1_kernel_signed",
@@ -605,21 +621,25 @@
srcs: ["extract_microdroid_kernel_hashes.py"],
}
-genrule {
+// HACK: use cc_genrule for arch-specific properties
+cc_genrule {
name: "microdroid_kernel_hashes_rs",
- srcs: [
- ":microdroid_kernel",
- ":microdroid_gki-android14-6.1_kernel",
- ],
+ srcs: [":microdroid_kernel"],
+ arch: {
+ arm64: {
+ srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+ },
+ x86_64: {
+ srcs: [":microdroid_gki-android14-6.1_kernel_signed"],
+ },
+ },
out: ["lib.rs"],
tools: [
"extract_microdroid_kernel_hashes",
"avbtool",
],
cmd: "$(location extract_microdroid_kernel_hashes) --avbtool $(location avbtool) " +
- "--kernel $(location :microdroid_kernel) " +
- "$(location :microdroid_gki-android14-6.1_kernel) " +
- "> $(out)",
+ "--kernel $(in) > $(out)",
}
rust_library_rlib {
diff --git a/microdroid/README.md b/microdroid/README.md
index c82ef0b..6e7b20c 100644
--- a/microdroid/README.md
+++ b/microdroid/README.md
@@ -107,6 +107,7 @@
PATH_TO_YOUR_APP \
$TEST_ROOT/MyApp.apk.idsig \
$TEST_ROOT/instance.img \
+--instance-id-file $TEST_ROOT/instance_id \
--payload-binary-name MyMicrodroidPayload.so
```
diff --git a/microdroid_manager/src/main.rs b/microdroid_manager/src/main.rs
index 86284a5..0d67632 100644
--- a/microdroid_manager/src/main.rs
+++ b/microdroid_manager/src/main.rs
@@ -45,8 +45,6 @@
use microdroid_metadata::PayloadMetadata;
use microdroid_payload_config::{ApkConfig, OsConfig, Task, TaskType, VmPayloadConfig};
use nix::sys::signal::Signal;
-use openssl::hkdf::hkdf;
-use openssl::md::Md;
use payload::load_metadata;
use rpcbinder::RpcSession;
use rustutils::sockets::android_get_control_socket;
@@ -73,6 +71,8 @@
const AVF_DEBUG_POLICY_RAMDUMP: &str = "/proc/device-tree/avf/guest/common/ramdump";
const DEBUG_MICRODROID_NO_VERIFIED_BOOT: &str =
"/proc/device-tree/virtualization/guest/debug-microdroid,no-verified-boot";
+const SECRETKEEPER_KEY: &str = "/proc/device-tree/avf/secretkeeper_public_key";
+const INSTANCE_ID_PATH: &str = "/proc/device-tree/avf/untrusted/instance-id";
const ENCRYPTEDSTORE_BIN: &str = "/system/bin/encryptedstore";
const ZIPFUSE_BIN: &str = "/system/bin/zipfuse";
@@ -145,6 +145,23 @@
Ok(())
}
+/// The (host allocated) instance_id can be found at node /avf/untrusted/ in the device tree.
+fn get_instance_id() -> Result<Option<[u8; ID_SIZE]>> {
+ let path = Path::new(INSTANCE_ID_PATH);
+ let instance_id = if path.exists() {
+ Some(
+ fs::read(path)?
+ .try_into()
+ .map_err(|x: Vec<_>| anyhow!("Expected {ID_SIZE} bytes, found {:?}", x.len()))?,
+ )
+ } else {
+ // TODO(b/325094712): x86 support for Device tree in nested guest is limited/broken/
+ // untested. So instance_id will not be present in cuttlefish.
+ None
+ };
+ Ok(instance_id)
+}
+
fn main() -> Result<()> {
// If debuggable, print full backtrace to console log with stdio_to_kmsg
if is_debuggable()? {
@@ -284,19 +301,8 @@
// To minimize the exposure to untrusted data, derive dice profile as soon as possible.
info!("DICE derivation for payload");
let dice_artifacts = dice_derivation(dice, &instance_data, &payload_metadata)?;
- // TODO(b/291213394): This will be the Id for non-pVM only, instance_data.salt is all 0
- // for protected VM, implement a mechanism for pVM!
- let mut vm_id = [0u8; ID_SIZE];
- hkdf(
- &mut vm_id,
- Md::sha256(),
- &instance_data.salt,
- /* salt */ b"",
- /* info */ b"VM_ID",
- )
- .context("hkdf failed")?;
let vm_secret =
- VmSecret::new(vm_id, dice_artifacts, service).context("Failed to create VM secrets")?;
+ VmSecret::new(dice_artifacts, service).context("Failed to create VM secrets")?;
if cfg!(dice_changes) {
// Now that the DICE derivation is done, it's ok to allow payload code to run.
diff --git a/microdroid_manager/src/vm_secret.rs b/microdroid_manager/src/vm_secret.rs
index 0e1ec71..5ceedea 100644
--- a/microdroid_manager/src/vm_secret.rs
+++ b/microdroid_manager/src/vm_secret.rs
@@ -14,12 +14,12 @@
//! Class for encapsulating & managing represent VM secrets.
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{anyhow, ensure, Context, Result};
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice::IVirtualMachineService::IVirtualMachineService;
use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::ISecretkeeper::ISecretkeeper;
use secretkeeper_comm::data_types::request::Request;
use binder::{Strong};
-use coset::CborSerializable;
+use coset::{CoseKey, CborSerializable, CborOrdering};
use dice_policy_builder::{CertIndex, ConstraintSpec, ConstraintType, policy_for_dice_chain, MissingAction, WILDCARD_FULL_ARRAY};
use diced_open_dice::{DiceArtifacts, OwnedDiceArtifacts};
use keystore2_crypto::ZVec;
@@ -34,6 +34,7 @@
use secretkeeper_comm::data_types::request_response_impl::{
StoreSecretRequest, GetSecretResponse, GetSecretRequest};
use secretkeeper_comm::data_types::error::SecretkeeperError;
+use std::fs;
use zeroize::Zeroizing;
const ENCRYPTEDSTORE_KEY_IDENTIFIER: &str = "encryptedstore_key";
@@ -58,11 +59,6 @@
0x55, 0xF8, 0x08, 0x23, 0x81, 0x5F, 0xF5, 0x16, 0x20, 0x3E, 0xBE, 0xBA, 0xB7, 0xA8, 0x43, 0x92,
];
-const SKP_SECRET_NP_VM: [u8; SECRET_SIZE] = [
- 0xA9, 0x89, 0x97, 0xFE, 0xAE, 0x97, 0x55, 0x4B, 0x32, 0x35, 0xF0, 0xE8, 0x93, 0xDA, 0xEA, 0x24,
- 0x06, 0xAC, 0x36, 0x8B, 0x3C, 0x95, 0x50, 0x16, 0x67, 0x71, 0x65, 0x26, 0xEB, 0xD0, 0xC3, 0x98,
-];
-
pub enum VmSecret {
// V2 secrets are derived from 2 independently secured secrets:
// 1. Secretkeeper protected secrets (skp secret).
@@ -81,41 +77,57 @@
V1 { dice_artifacts: OwnedDiceArtifacts },
}
+// For supporting V2 secrets, guest expects the public key to be present in the Linux device tree.
+fn get_secretkeeper_identity() -> Result<CoseKey> {
+ let key = fs::read(super::SECRETKEEPER_KEY)?;
+ let mut key = CoseKey::from_slice(&key)?;
+ key.canonicalize(CborOrdering::Lexicographic);
+ Ok(key)
+}
+
impl VmSecret {
pub fn new(
- id: [u8; ID_SIZE],
dice_artifacts: OwnedDiceArtifacts,
vm_service: &Strong<dyn IVirtualMachineService>,
) -> Result<Self> {
ensure!(dice_artifacts.bcc().is_some(), "Dice chain missing");
- let Some(sk_service) = is_sk_supported(vm_service)? else {
+ let Some(sk_service) =
+ is_sk_supported(vm_service).context("Failed to check if Secretkeeper is supported")?
+ else {
// Use V1 secrets if Secretkeeper is not supported.
return Ok(Self::V1 { dice_artifacts });
};
- let explicit_dice =
- OwnedDiceArtifactsWithExplicitKey::from_owned_artifacts(dice_artifacts)?;
- let explicit_dice_chain = explicit_dice
- .explicit_key_dice_chain()
- .ok_or(anyhow!("Missing explicit dice chain, this is unusual"))?;
- let policy = sealing_policy(explicit_dice_chain).map_err(anyhow_err)?;
- // Start a new session with Secretkeeper!
- let mut session = SkSession::new(sk_service, &explicit_dice)?;
+ let explicit_dice = OwnedDiceArtifactsWithExplicitKey::from_owned_artifacts(dice_artifacts)
+ .context("Failed to get Dice artifacts in explicit key format")?;
+ // For pVM, skp_secret are stored in Secretkeeper. For non-protected it is all 0s.
let mut skp_secret = Zeroizing::new([0u8; SECRET_SIZE]);
if super::is_strict_boot() {
+ let mut session = SkSession::new(
+ sk_service,
+ &explicit_dice,
+ Some(get_secretkeeper_identity().context("Failed to get secretkeeper identity")?),
+ )
+ .context("Failed to setup a Secretkeeper session")?;
+ let id = super::get_instance_id()
+ .context("Failed to get instance-id")?
+ .ok_or(anyhow!("Missing instance-id"))?;
+ let explicit_dice_chain = explicit_dice
+ .explicit_key_dice_chain()
+ .ok_or(anyhow!("Missing explicit dice chain, this is unusual"))?;
+ let policy = sealing_policy(explicit_dice_chain)
+ .map_err(|e| anyhow!("Failed to build a sealing_policy: {e}"))?;
if super::is_new_instance() {
+ // New instance -> create a secret & store in Secretkeeper.
*skp_secret = rand::random();
- store_secret(&mut session, id, skp_secret.clone(), policy)?;
+ store_secret(&mut session, id, skp_secret.clone(), policy)
+ .context("Failed to store secret in Secretkeeper")?;
} else {
// Subsequent run of the pVM -> get the secret stored in Secretkeeper.
- *skp_secret = get_secret(&mut session, id, Some(policy))?;
+ *skp_secret = get_secret(&mut session, id, Some(policy))
+ .context("Failed to get secret from Secretkeeper")?;
}
- } else {
- // TODO(b/291213394): Non protected VM don't need to use Secretkeeper, remove this
- // once we have sufficient testing on protected VM.
- store_secret(&mut session, id, SKP_SECRET_NP_VM.into(), policy)?;
- *skp_secret = get_secret(&mut session, id, None)?;
}
Ok(Self::V2 {
dice_artifacts: explicit_dice,
@@ -273,17 +285,13 @@
host: &Strong<dyn IVirtualMachineService>,
) -> Result<Option<Strong<dyn ISecretkeeper>>> {
let sk = if cfg!(llpvm_changes) {
- if super::is_strict_boot() {
- // TODO: For protected VM check for Secretkeeper authentication data in device tree.
- None
- } else {
- // For non-protected VM, believe what host claims.
- host.getSecretkeeper()
- // TODO rename this error!
- .map_err(|e| {
- super::MicrodroidError::FailedToConnectToVirtualizationService(e.to_string())
- })?
- }
+ host.getSecretkeeper()
+ // TODO rename this error!
+ .map_err(|e| {
+ super::MicrodroidError::FailedToConnectToVirtualizationService(format!(
+ "Failed to get Secretkeeper: {e:?}"
+ ))
+ })?
} else {
// LLPVM flag is disabled
None
diff --git a/service_vm/demo_apk/README.md b/service_vm/demo_apk/README.md
index 551d47b..8f67f6e 100644
--- a/service_vm/demo_apk/README.md
+++ b/service_vm/demo_apk/README.md
@@ -42,7 +42,9 @@
--config-path assets/config.json --debug full \
$(adb shell pm path com.android.virt.vm_attestation.demo | cut -c 9-) \
$TEST_ROOT/VmAttestationDemoApp.apk.idsig \
- $TEST_ROOT/instance.vm_attestation.debug.img --protected
+ $TEST_ROOT/instance.vm_attestation.debug.img \
+ --instance-id-file $TEST_ROOT/instance_id \
+ --protected
```
Please note that remote attestation is only available for protected VMs.
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 0d1efb0..8ea4e65 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -23,10 +23,10 @@
result,
};
use vm_payload_bindgen::{
- attestation_status_t, AVmAttestationResult, AVmAttestationResult_free,
- AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
- AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
- AVmAttestationResult_sign, AVmPayload_requestAttestation,
+ AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+ AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+ AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+ AVmPayload_requestAttestation,
};
/// Entry point of the Service VM client.
@@ -56,7 +56,7 @@
ensure!(res.is_err());
let status = res.unwrap_err();
ensure!(
- status == attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE,
+ status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
"Unexpected status: {:?}",
status
);
@@ -89,7 +89,7 @@
struct AttestationResult(NonNull<AVmAttestationResult>);
impl AttestationResult {
- fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+ fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
let mut res: *mut AVmAttestationResult = ptr::null_mut();
// SAFETY: It is safe as we only read the challenge within its bounds and the
// function does not retain any reference to it.
@@ -100,7 +100,7 @@
&mut res,
)
};
- if status == attestation_status_t::ATTESTATION_OK {
+ if status == AVmAttestationStatus::ATTESTATION_OK {
info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
let res = NonNull::new(res).expect("The attestation result is null");
Ok(Self(res))
@@ -219,11 +219,11 @@
Ok(signature.into_boxed_slice())
}
-fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
// SAFETY: The function only reads the given enum status and returns a pointer to a
// static string.
- let message = unsafe { AVmAttestationResult_resultToString(status) };
- // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+ let message = unsafe { AVmAttestationStatus_toString(status) };
+ // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
// point to a valid C String that lives forever.
unsafe { CStr::from_ptr(message) }
}
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index d5d599d..199b45c 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -31,10 +31,10 @@
sync::{Arc, Mutex},
};
use vm_payload_bindgen::{
- attestation_status_t, AIBinder, AVmAttestationResult, AVmAttestationResult_free,
+ AIBinder, AVmAttestationResult, AVmAttestationResult_free,
AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
- AVmAttestationResult_getPrivateKey, AVmAttestationResult_resultToString,
- AVmAttestationResult_sign, AVmPayload_notifyPayloadReady,
+ AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
+ AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady,
AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
};
@@ -116,7 +116,7 @@
unsafe impl Send for AttestationResult {}
impl AttestationResult {
- fn request_attestation(challenge: &[u8]) -> result::Result<Self, attestation_status_t> {
+ fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
let mut res: *mut AVmAttestationResult = ptr::null_mut();
// SAFETY: It is safe as we only read the challenge within its bounds and the
// function does not retain any reference to it.
@@ -127,7 +127,7 @@
&mut res,
)
};
- if status == attestation_status_t::ATTESTATION_OK {
+ if status == AVmAttestationStatus::ATTESTATION_OK {
info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
let res = NonNull::new(res).expect("The attestation result is null");
Ok(Self(res))
@@ -261,11 +261,11 @@
Ok(signature.into_boxed_slice())
}
-fn status_to_cstr(status: attestation_status_t) -> &'static CStr {
+fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
// SAFETY: The function only reads the given enum status and returns a pointer to a
// static string.
- let message = unsafe { AVmAttestationResult_resultToString(status) };
- // SAFETY: The pointer returned by `AVmAttestationResult_resultToString` is guaranteed to
+ let message = unsafe { AVmAttestationStatus_toString(status) };
+ // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
// point to a valid C String that lives forever.
unsafe { CStr::from_ptr(message) }
}
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index ba02067..acd6f2c 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -127,12 +127,21 @@
}
}
+ public MicrodroidBenchmarks() throws IOException {
+ // See b/325745564#comment28. Calling this method here ensures that threads spawned for
+ // @Test methods are with the desired task profile. If this is called in @Before, the task
+ // profile may not be set to the test threads because they may be spanwed prior to the
+ // execution of the @Before method (though the test methods will be executed after the
+ // @Before method). With this, children of this benchmark process (virtmgr and crosvm) also
+ // run in the desired task profile.
+ setMaxPerformanceTaskProfile();
+ }
+
@Before
public void setup() throws IOException {
grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION);
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
prepareTestSetup(mProtectedVm, mGki);
- setMaxPerformanceTaskProfile();
mInstrumentation = getInstrumentation();
}
diff --git a/tests/hostside/Android.bp b/tests/hostside/Android.bp
index 13a9925..41d244d 100644
--- a/tests/hostside/Android.bp
+++ b/tests/hostside/Android.bp
@@ -35,6 +35,7 @@
"initrd_bootconfig",
"lpmake",
"lpunpack",
+ "lz4",
"sign_virt_apex",
"simg2img",
"dtdiff",
diff --git a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
index 9e19b7d..2abf110 100644
--- a/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
+++ b/tests/hostside/helper/java/com/android/microdroid/test/host/MicrodroidHostTestCaseBase.java
@@ -52,6 +52,7 @@
private static final int TEST_VM_ADB_PORT = 8000;
private static final String MICRODROID_SERIAL = "localhost:" + TEST_VM_ADB_PORT;
private static final String INSTANCE_IMG = "instance.img";
+ protected static final String VIRT_APEX = "/apex/com.android.virt/";
private static final long MICRODROID_ADB_CONNECT_TIMEOUT_MINUTES = 5;
protected static final long MICRODROID_COMMAND_TIMEOUT_MILLIS = 30000;
@@ -186,6 +187,12 @@
return ret;
}
+ public boolean isFeatureEnabled(String feature) throws Exception {
+ CommandRunner android = new CommandRunner(getDevice());
+ String result = android.run(VIRT_APEX + "bin/vm", "check-feature-enabled", feature);
+ return result.contains("enabled");
+ }
+
public List<String> getAssignableDevices() throws Exception {
return parseStringArrayFieldsFromVmInfo("Assignable devices: ");
}
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index 0901fd4..4f502ab 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -617,6 +617,7 @@
final String apkPath = getPathForPackage(PACKAGE_NAME);
final String idsigPath = TEST_ROOT + "idsig";
final String instanceImgPath = TEST_ROOT + "instance.img";
+ final String instanceIdPath = TEST_ROOT + "instance_id";
List<String> cmd =
new ArrayList<>(
Arrays.asList(
@@ -627,6 +628,11 @@
apkPath,
idsigPath,
instanceImgPath));
+ if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+ cmd.add("--instance-id-file");
+ cmd.add(instanceIdPath);
+ }
+ ;
if (protectedVm) {
cmd.add("--protected");
}
@@ -887,7 +893,6 @@
final String apkPath = getPathForPackage(PACKAGE_NAME);
final String idSigPath = TEST_ROOT + "idsig";
android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath);
-
// Create the instance image for the VM
final String instanceImgPath = TEST_ROOT + "instance.img";
android.run(
@@ -897,17 +902,22 @@
instanceImgPath,
Integer.toString(10 * 1024 * 1024));
- final String ret =
- android.runForResult(
+ List<String> cmd =
+ new ArrayList<>(
+ Arrays.asList(
VIRT_APEX + "bin/vm",
"run-app",
"--payload-binary-name",
"./MicrodroidTestNativeLib.so",
apkPath,
idSigPath,
- instanceImgPath)
- .getStderr()
- .trim();
+ instanceImgPath));
+ if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+ cmd.add("--instance-id-file");
+ cmd.add(TEST_ROOT + "instance_id");
+ }
+
+ final String ret = android.runForResult(String.join(" ", cmd)).getStderr().trim();
assertThat(ret).contains("Payload binary name must not specify a path");
}
@@ -944,7 +954,43 @@
assertThat(hasDebugPolicy).isFalse();
}
+ private boolean isLz4(String path) throws Exception {
+ File lz4tool = findTestFile("lz4");
+ CommandResult result =
+ new RunUtil().runTimedCmd(5000, lz4tool.getAbsolutePath(), "-t", path);
+ return result.getStatus() == CommandStatus.SUCCESS;
+ }
+
+ private void decompressLz4(String inputPath, String outputPath) throws Exception {
+ File lz4tool = findTestFile("lz4");
+ CommandResult result =
+ new RunUtil()
+ .runTimedCmd(
+ 5000, lz4tool.getAbsolutePath(), "-d", "-f", inputPath, outputPath);
+ String out = result.getStdout();
+ String err = result.getStderr();
+ assertWithMessage(
+ "lz4 image "
+ + inputPath
+ + " decompression failed."
+ + "\n\tout: "
+ + out
+ + "\n\terr: "
+ + err
+ + "\n")
+ .about(command_results())
+ .that(result)
+ .isSuccess();
+ }
+
private String avbInfo(String image_path) throws Exception {
+ if (isLz4(image_path)) {
+ File decompressedImage = FileUtil.createTempFile("decompressed", ".img");
+ decompressedImage.deleteOnExit();
+ decompressLz4(image_path, decompressedImage.getAbsolutePath());
+ image_path = decompressedImage.getAbsolutePath();
+ }
+
File avbtool = findTestFile("avbtool");
List<String> command =
Arrays.asList(avbtool.getAbsolutePath(), "info_image", "--image", image_path);
diff --git a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
index b84d7dc..26f5993 100644
--- a/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
+++ b/tests/pvmfw/java/com/android/pvmfw/test/DebugPolicyHostTests.java
@@ -301,7 +301,7 @@
// Try to launch protected non-debuggable VM for a while and quit.
// Non-debuggable VM might not enable adb, so there's no ITestDevice instance of it.
- private CommandResult tryLaunchProtectedNonDebuggableVm() throws DeviceNotAvailableException {
+ private CommandResult tryLaunchProtectedNonDebuggableVm() throws Exception {
// Can't use MicrodroidBuilder because it expects adb connection
// but non-debuggable VM may not enable adb.
CommandRunner runner = new CommandRunner(mAndroidDevice);
@@ -314,7 +314,7 @@
String command =
String.join(
" ",
- "/apex/com.android.virt/bin/vm",
+ VIRT_APEX + "bin/vm",
"run-app",
"--log",
MICRODROID_LOG_PATH,
@@ -324,6 +324,9 @@
TEST_ROOT + "instance.img",
"--config-path",
MICRODROID_CONFIG_PATH);
+ if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
+ command = String.join(" ", command, "--instance-id-file", TEST_ROOT + "instance_id");
+ }
return mAndroidDevice.executeShellV2Command(
command, CONSOLE_OUTPUT_WAIT_MS, TimeUnit.MILLISECONDS, /* retryAttempts= */ 0);
}
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 3b8b4ac..51aace4 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -1070,6 +1070,10 @@
})
public void instancesOfSameVmHaveDifferentCdis() throws Exception {
assumeSupportedDevice();
+ // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
+ // `instance-id` which is input to DICE is contained in DT which is missing in CF.
+ assumeFalse(
+ "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
VirtualMachineConfig normalConfig =
@@ -1504,6 +1508,10 @@
@CddTest(requirements = {"9.17/C-1-1"})
public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception {
assumeSupportedDevice();
+ // TODO(b/325094712): VMs on CF with same payload have the same secret. This is because
+ // `instance-id` which is input to DICE is contained in DT which is missing in CF.
+ assumeFalse(
+ "Cuttlefish doesn't support device tree under /proc/device-tree", isCuttlefish());
VirtualMachineConfig config =
newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
diff --git a/tests/testapk_no_perm/Android.bp b/tests/testapk_no_perm/Android.bp
new file mode 100644
index 0000000..22616de
--- /dev/null
+++ b/tests/testapk_no_perm/Android.bp
@@ -0,0 +1,26 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "MicrodroidTestAppNoPerm",
+ static_libs: [
+ "MicrodroidDeviceTestHelper",
+ "MicrodroidTestHelper",
+ "androidx.test.runner",
+ "androidx.test.ext.junit",
+ "com.android.microdroid.testservice-java",
+ "truth",
+ "compatibility-common-util-devicesidelib",
+ ],
+ jni_libs: [
+ "MicrodroidTestNativeLib",
+ ],
+ test_suites: [
+ "general-tests",
+ "cts",
+ ],
+ srcs: ["src/java/**/*.java"],
+ defaults: ["MicrodroidTestAppsDefaults"],
+ min_sdk_version: "33",
+}
diff --git a/tests/testapk_no_perm/AndroidManifest.xml b/tests/testapk_no_perm/AndroidManifest.xml
new file mode 100644
index 0000000..44aa92a
--- /dev/null
+++ b/tests/testapk_no_perm/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.microdroid.test_no_perm">
+ <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
+ <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+ <application />
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.microdroid.test_no_perm"
+ android:label="No Permission Microdroid Test" />
+</manifest>
diff --git a/tests/testapk_no_perm/AndroidTest.xml b/tests/testapk_no_perm/AndroidTest.xml
new file mode 100644
index 0000000..d4a818f
--- /dev/null
+++ b/tests/testapk_no_perm/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+<configuration description="Runs Microdroid Tests with no permission">
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="security" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="MicrodroidTestAppNoPerm.apk" />
+ </target_preparer>
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.microdroid.test_no_perm" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="shell-timeout" value="300000" />
+ <option name="test-timeout" value="300000" />
+ </test>
+</configuration>
diff --git a/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
new file mode 100644
index 0000000..1772e6b
--- /dev/null
+++ b/tests/testapk_no_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoPerm.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.microdroid.test;
+
+import android.system.virtualmachine.VirtualMachineConfig;
+
+import com.android.compatibility.common.util.CddTest;
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.Before;
+import org.junit.runners.Parameterized;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
+ * a VM without said permission.
+ */
+@RunWith(Parameterized.class)
+public class MicrodroidTestAppNoPerm extends MicrodroidDeviceTestBase {
+
+ @Parameterized.Parameters(name = "protectedVm={0}")
+ public static Object[] protectedVmConfigs() {
+ return new Object[] {false, true};
+ }
+
+ @Parameterized.Parameter public boolean mProtectedVm;
+
+ @Before
+ public void setup() {
+ prepareTestSetup(mProtectedVm, null);
+ }
+
+ @Test
+ @CddTest(
+ requirements = {
+ "9.17/C-1-1",
+ "9.17/C-1-2",
+ "9.17/C-1-4",
+ })
+ public void createVmRequiresPermission() {
+ assumeSupportedDevice();
+
+ VirtualMachineConfig config =
+ newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so").build();
+
+ SecurityException e =
+ assertThrows(
+ SecurityException.class,
+ () -> forceCreateNewVirtualMachine("test_vm_requires_permission", config));
+ assertThat(e)
+ .hasMessageThat()
+ .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission");
+ }
+}
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 0655e5f..ea3a481 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -83,7 +83,7 @@
use std::io::{BufRead, BufReader, Error, ErrorKind, Seek, SeekFrom, Write};
use std::iter;
use std::num::{NonZeroU16, NonZeroU32};
-use std::os::unix::io::{FromRawFd, IntoRawFd};
+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};
@@ -109,9 +109,8 @@
const MICRODROID_OS_NAME: &str = "microdroid";
-// TODO(b/291213394): Use 'default' instance for secretkeeper instead of 'nonsecure'
const SECRETKEEPER_IDENTIFIER: &str =
- "android.hardware.security.secretkeeper.ISecretkeeper/nonsecure";
+ "android.hardware.security.secretkeeper.ISecretkeeper/default";
const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
@@ -230,6 +229,11 @@
ret
}
+ /// Allocate a new instance_id to the VM
+ fn allocateInstanceId(&self) -> binder::Result<[u8; 64]> {
+ GLOBAL_SERVICE.allocateInstanceId()
+ }
+
/// Initialise an empty partition image of the given size to be used as a writable partition.
fn initializeWritablePartition(
&self,
@@ -371,7 +375,8 @@
check_gdb_allowed(config)?;
}
- // Currently, VirtMgr adds the host copy of reference DT & an untrusted prop (instance-id)
+ // Currently, VirtMgr adds the host copy of reference DT & untrusted properties
+ // (e.g. instance-id)
let host_ref_dt = Path::new(VM_REFERENCE_DT_ON_HOST_PATH);
let host_ref_dt = if host_ref_dt.exists()
&& read_dir(host_ref_dt).or_service_specific_exception(-1)?.next().is_some()
@@ -399,13 +404,17 @@
vec![]
};
- let untrusted_props = if cfg!(llpvm_changes) {
- // TODO(b/291213394): Replace this with a per-VM instance Id.
- let instance_id = b"sixtyfourbyteslonghardcoded_indeed_sixtyfourbyteslonghardcoded_h";
- vec![(cstr!("instance-id"), &instance_id[..])]
- } else {
- vec![]
- };
+ let instance_id;
+ let mut untrusted_props = Vec::with_capacity(2);
+ if cfg!(llpvm_changes) {
+ instance_id = extract_instance_id(config);
+ untrusted_props.push((cstr!("instance-id"), &instance_id[..]));
+ if is_secretkeeper_supported() {
+ // Let guest know that it can defer rollback protection to Secretkeeper by setting
+ // an empty property in untrusted node in DT. This enables Updatable VMs.
+ untrusted_props.push((cstr!("defer-rollback-protection"), &[]))
+ }
+ }
let device_tree_overlay =
if host_ref_dt.is_some() || !untrusted_props.is_empty() || !trusted_props.is_empty() {
@@ -484,6 +493,11 @@
.try_for_each(check_label_for_partition)
.or_service_specific_exception(-1)?;
+ // Check if files for payloads and bases are NOT coming from /vendor and /odm, as they may
+ // have unstable interfaces.
+ // TODO(b/316431494): remove once Treble interfaces are stabilized.
+ check_partitions_for_files(config).or_service_specific_exception(-1)?;
+
let kernel = maybe_clone_file(&config.kernel)?;
let initrd = maybe_clone_file(&config.initrd)?;
@@ -855,6 +869,38 @@
Ok(vm_config)
}
+fn check_partition_for_file(fd: &ParcelFileDescriptor) -> Result<()> {
+ let path = format!("/proc/self/fd/{}", fd.as_raw_fd());
+ let link = fs::read_link(&path).context(format!("can't read_link {path}"))?;
+
+ // microdroid vendor image is OK
+ if cfg!(vendor_modules) && link == Path::new("/vendor/etc/avf/microdroid/microdroid_vendor.img")
+ {
+ return Ok(());
+ }
+
+ if link.starts_with("/vendor") || link.starts_with("/odm") {
+ bail!("vendor or odm file {} can't be used for VM", link.display());
+ }
+
+ Ok(())
+}
+
+fn check_partitions_for_files(config: &VirtualMachineRawConfig) -> Result<()> {
+ config
+ .disks
+ .iter()
+ .flat_map(|disk| disk.partitions.iter())
+ .filter_map(|partition| partition.image.as_ref())
+ .try_for_each(check_partition_for_file)?;
+
+ config.kernel.as_ref().map_or(Ok(()), check_partition_for_file)?;
+ config.initrd.as_ref().map_or(Ok(()), check_partition_for_file)?;
+ config.bootloader.as_ref().map_or(Ok(()), check_partition_for_file)?;
+
+ Ok(())
+}
+
fn load_vm_payload_config_from_file(apk_file: &File, config_path: &str) -> Result<VmPayloadConfig> {
let mut apk_zip = ZipArchive::new(apk_file)?;
let config_file = apk_zip.by_name(config_path)?;
@@ -1269,6 +1315,13 @@
}
}
+fn extract_instance_id(config: &VirtualMachineConfig) -> [u8; 64] {
+ match config {
+ VirtualMachineConfig::RawConfig(config) => config.instanceId,
+ VirtualMachineConfig::AppConfig(config) => config.instanceId,
+ }
+}
+
fn extract_gdb_port(config: &VirtualMachineConfig) -> Option<NonZeroU16> {
match config {
VirtualMachineConfig::RawConfig(config) => NonZeroU16::new(config.gdbPort as u16),
@@ -1453,7 +1506,7 @@
}
fn getSecretkeeper(&self) -> binder::Result<Option<Strong<dyn ISecretkeeper>>> {
- let sk = if is_secretkeeper_present() {
+ let sk = if is_secretkeeper_supported() {
Some(binder::wait_for_interface(SECRETKEEPER_IDENTIFIER)?)
} else {
None
@@ -1466,7 +1519,7 @@
}
}
-fn is_secretkeeper_present() -> bool {
+fn is_secretkeeper_supported() -> bool {
binder::is_declared(SECRETKEEPER_IDENTIFIER)
.expect("Could not check for declared Secretkeeper interface")
}
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 38a9ec5..fc7fcd2 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -5,7 +5,10 @@
rust_defaults {
name: "virtualizationservice_defaults",
crate_name: "virtualizationservice",
- defaults: ["avf_build_flags_rust"],
+ defaults: [
+ "avf_build_flags_rust",
+ "secretkeeper_use_latest_hal_aidl_rust",
+ ],
edition: "2021",
srcs: ["src/main.rs"],
// Only build on targets which crosvm builds on.
@@ -32,13 +35,17 @@
"libanyhow",
"libavflog",
"libbinder_rs",
+ "libhex",
"libhypervisor_props",
"liblazy_static",
"liblibc",
+ "liblibsqlite3_sys",
"liblog_rust",
"libnix",
"libopenssl",
+ "librand",
"librkpd_client",
+ "librusqlite",
"librustutils",
"libstatslog_virtualization_rust",
"libtombstoned_client_rust",
@@ -65,7 +72,10 @@
rust_test {
name: "virtualizationservice_test",
- defaults: ["virtualizationservice_defaults"],
+ defaults: [
+ "authgraph_use_latest_hal_aidl_rust",
+ "virtualizationservice_defaults",
+ ],
test_suites: ["general-tests"],
data: [
":test_rkp_cert_chain",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index 66de092..112e1cc 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -61,6 +61,9 @@
backend: {
java: {
sdk_version: "module_current",
+ apex_available: [
+ "com.android.virt",
+ ],
},
rust: {
enabled: true,
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
index 161673a..08d61c1 100644
--- a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationMaintenance.aidl
@@ -16,10 +16,31 @@
package android.system.virtualizationmaintenance;
+import android.system.virtualizationmaintenance.IVirtualizationReconciliationCallback;
+
interface IVirtualizationMaintenance {
+ /**
+ * Notification that an app has been permanently removed, to allow related global state to
+ * be removed.
+ *
+ * @param userId The Android user ID for whom the notification applies.
+ */
void appRemoved(int userId, int appId);
+ /**
+ * Notification that a user has been removed, to allow related global state to be removed.
+ *
+ * @param userId The Android user ID of the user.
+ */
void userRemoved(int userId);
- // TODO: Something for daily reconciliation
+ /*
+ * Requests virtualization service to perform reconciliation of Secretkeeper secrets.
+ * Secrets belonging to apps or users that no longer exist should be deleted.
+ * The supplied callback allows for querying of existence.
+ * This method should return on successful completion of the reconciliation process.
+ * It should throw an exception if there is any failure, or if any of the callback
+ * functions return {@code ERROR_STOP_REQUESTED}.
+ */
+ void performReconciliation(IVirtualizationReconciliationCallback callback);
}
diff --git a/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
new file mode 100644
index 0000000..6466aa2
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationmaintenance/IVirtualizationReconciliationCallback.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.system.virtualizationmaintenance;
+
+/*
+ * Callback interface provided when reconciliation is performed to allow verifying whether users
+ * and apps currently exist.
+ */
+interface IVirtualizationReconciliationCallback {
+ /*
+ * Service-specific error code indicating that the job scheduler has requested that we
+ * stop
+ */
+ const int ERROR_STOP_REQUESTED = 1;
+
+ /*
+ * Determine whether users with selected IDs currently exist. The result is an array of booleans
+ * which indicate whether the corresponding entry in the {@code userIds} array is a valid
+ * user ID.
+ */
+ boolean[] doUsersExist(in int[] userIds);
+
+ /*
+ * Determine whether apps with selected app IDs currently exist for a specific user.
+ * The result is an array of booleans which indicate whether the corresponding entry in the
+ * {@code appIds} array is a current app ID for the user.
+ */
+ boolean[] doAppsExist(int userId, in int[] appIds);
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index f7bbf55..e11d8b8 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -41,6 +41,11 @@
in @nullable ParcelFileDescriptor osLogFd);
/**
+ * Allocate an instance_id to the (newly created) VM.
+ */
+ byte[64] allocateInstanceId();
+
+ /**
* Initialise an empty partition image of the given size to be used as a writable partition.
*
* The file must be open with both read and write permissions, and should be a new empty file.
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 8302a2f..29232ff 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -23,6 +23,9 @@
/** Name of VM */
String name;
+ /** Id of the VM instance */
+ byte[64] instanceId;
+
/** Main APK */
ParcelFileDescriptor apk;
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 6be2833..b2116c4 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -23,6 +23,9 @@
/** Name of VM */
String name;
+ /** Id of the VM instance */
+ byte[64] instanceId;
+
/** The kernel image, if any. */
@nullable ParcelFileDescriptor kernel;
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
index abfc45a..8af881b 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.aidl
@@ -91,4 +91,16 @@
/** Returns a read-only file descriptor of the VM DTBO file. */
ParcelFileDescriptor getDtboFile();
+
+ /**
+ * Allocate an instance_id to the (newly created) VM.
+ */
+ byte[64] allocateInstanceId();
+
+ /**
+ * Notification that state associated with a VM should be removed.
+ *
+ * @param instanceId The ID for the VM.
+ */
+ void removeVmInstance(in byte[64] instanceId);
}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index c7a33ba..bbfb220 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -15,11 +15,13 @@
//! Implementation of the AIDL interface of the VirtualizationService.
use crate::atom::{forward_vm_booted_atom, forward_vm_creation_atom, forward_vm_exited_atom};
+use crate::maintenance;
use crate::remote_provisioning;
use crate::rkpvm::{generate_ecdsa_p256_key_pair, request_attestation};
use crate::{get_calling_pid, get_calling_uid, REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME};
use android_os_permissions_aidl::aidl::android::os::IPermissionController;
use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon;
+use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
use android_system_virtualizationservice_internal as android_vs_internal;
use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
@@ -35,6 +37,7 @@
use log::{error, info, warn};
use nix::unistd::{chown, Uid};
use openssl::x509::X509;
+use rand::Fill;
use rkpd_client::get_rkpd_attestation_key;
use rustutils::system_properties;
use serde::Deserialize;
@@ -48,6 +51,10 @@
use std::sync::{Arc, Mutex, Weak};
use tombstoned_client::{DebuggerdDumpType, TombstonedConnection};
use virtualizationcommon::Certificate::Certificate;
+use virtualizationmaintenance::{
+ IVirtualizationMaintenance::IVirtualizationMaintenance,
+ IVirtualizationReconciliationCallback::IVirtualizationReconciliationCallback,
+};
use virtualizationservice::{
AssignableDevice::AssignableDevice, VirtualMachineDebugInfo::VirtualMachineDebugInfo,
};
@@ -59,9 +66,7 @@
IGlobalVmContext::{BnGlobalVmContext, IGlobalVmContext},
IVfioHandler::VfioDev::VfioDev,
IVfioHandler::{BpVfioHandler, IVfioHandler},
- IVirtualizationServiceInternal::{
- BnVirtualizationServiceInternal, IVirtualizationServiceInternal,
- },
+ IVirtualizationServiceInternal::IVirtualizationServiceInternal,
};
use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
use vsock::{VsockListener, VsockStream};
@@ -159,14 +164,15 @@
/// Singleton service for allocating globally-unique VM resources, such as the CID, and running
/// singleton servers, like tombstone receiver.
-#[derive(Debug, Default)]
+#[derive(Clone)]
pub struct VirtualizationServiceInternal {
state: Arc<Mutex<GlobalState>>,
}
impl VirtualizationServiceInternal {
- pub fn init() -> Strong<dyn IVirtualizationServiceInternal> {
- let service = VirtualizationServiceInternal::default();
+ pub fn init() -> VirtualizationServiceInternal {
+ let service =
+ VirtualizationServiceInternal { state: Arc::new(Mutex::new(GlobalState::new())) };
std::thread::spawn(|| {
if let Err(e) = handle_stream_connection_tombstoned() {
@@ -174,7 +180,7 @@
}
});
- BnVirtualizationServiceInternal::new_binder(service, BinderFeatures::default())
+ service
}
}
@@ -378,6 +384,60 @@
let file = state.get_dtbo_file().or_service_specific_exception(-1)?;
Ok(ParcelFileDescriptor::new(file))
}
+
+ // TODO(b/294177871) Persist this Id, along with client uuid.
+ fn allocateInstanceId(&self) -> binder::Result<[u8; 64]> {
+ let mut id = [0u8; 64];
+ id.try_fill(&mut rand::thread_rng())
+ .context("Failed to allocate instance_id")
+ .or_service_specific_exception(-1)?;
+ let uid = get_calling_uid();
+ info!("Allocated a VM's instance_id: {:?}, for uid: {:?}", hex::encode(id), uid);
+ Ok(id)
+ }
+
+ fn removeVmInstance(&self, instance_id: &[u8; 64]) -> binder::Result<()> {
+ let state = &mut *self.state.lock().unwrap();
+ if let Some(sk_state) = &mut state.sk_state {
+ info!("removeVmInstance(): delete secret");
+ sk_state.delete_ids(&[*instance_id]);
+ } else {
+ info!("ignoring removeVmInstance() as no ISecretkeeper");
+ }
+ Ok(())
+ }
+}
+
+impl IVirtualizationMaintenance for VirtualizationServiceInternal {
+ fn appRemoved(&self, user_id: i32, app_id: i32) -> binder::Result<()> {
+ let state = &mut *self.state.lock().unwrap();
+ if let Some(sk_state) = &mut state.sk_state {
+ info!("packageRemoved(user_id={user_id}, app_id={app_id})");
+ sk_state.delete_ids_for_app(user_id, app_id).or_service_specific_exception(-1)?;
+ } else {
+ info!("ignoring packageRemoved(user_id={user_id}, app_id={app_id})");
+ }
+ Ok(())
+ }
+
+ fn userRemoved(&self, user_id: i32) -> binder::Result<()> {
+ let state = &mut *self.state.lock().unwrap();
+ if let Some(sk_state) = &mut state.sk_state {
+ info!("userRemoved({user_id})");
+ sk_state.delete_ids_for_user(user_id).or_service_specific_exception(-1)?;
+ } else {
+ info!("ignoring userRemoved(user_id={user_id})");
+ }
+ Ok(())
+ }
+
+ fn performReconciliation(
+ &self,
+ _callback: &Strong<dyn IVirtualizationReconciliationCallback>,
+ ) -> binder::Result<()> {
+ Err(anyhow!("performReconciliation not supported"))
+ .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+ }
}
// KEEP IN SYNC WITH assignable_devices.xsd
@@ -462,7 +522,6 @@
/// The mutable state of the VirtualizationServiceInternal. There should only be one instance
/// of this struct.
-#[derive(Debug, Default)]
struct GlobalState {
/// VM contexts currently allocated to running VMs. A CID is never recycled as long
/// as there is a strong reference held by a GlobalVmContext.
@@ -470,9 +529,20 @@
/// Cached read-only FD of VM DTBO file. Also serves as a lock for creating the file.
dtbo_file: Mutex<Option<File>>,
+
+ /// State relating to secrets held by (optional) Secretkeeper instance on behalf of VMs.
+ sk_state: Option<maintenance::State>,
}
impl GlobalState {
+ fn new() -> Self {
+ Self {
+ held_contexts: HashMap::new(),
+ dtbo_file: Mutex::new(None),
+ sk_state: maintenance::State::new(),
+ }
+ }
+
/// Get the next available CID, or an error if we have run out. The last CID used is stored in
/// a system property so that restart of virtualizationservice doesn't reuse CID while the host
/// Android is up.
@@ -717,7 +787,6 @@
#[cfg(test)]
mod tests {
use super::*;
- use std::fs;
const TEST_RKP_CERT_CHAIN_PATH: &str = "testdata/rkp_cert_chain.der";
diff --git a/virtualizationservice/src/main.rs b/virtualizationservice/src/main.rs
index 97bb38f..bcea1bc 100644
--- a/virtualizationservice/src/main.rs
+++ b/virtualizationservice/src/main.rs
@@ -20,10 +20,16 @@
mod remote_provisioning;
mod rkpvm;
-use crate::aidl::{remove_temporary_dir, VirtualizationServiceInternal, TEMPORARY_DIRECTORY};
+use crate::aidl::{remove_temporary_dir, TEMPORARY_DIRECTORY, VirtualizationServiceInternal};
use android_logger::{Config, FilterBuilder};
+use android_system_virtualizationservice_internal::aidl::android::system::{
+ virtualizationservice_internal::IVirtualizationServiceInternal::BnVirtualizationServiceInternal
+};
+use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance::{
+ IVirtualizationMaintenance::BnVirtualizationMaintenance
+};
use anyhow::{bail, Context, Error, Result};
-use binder::{register_lazy_service, ProcessState, ThreadState};
+use binder::{register_lazy_service, BinderFeatures, ProcessState, ThreadState};
use log::{error, info, LevelFilter};
use std::fs::{create_dir, read_dir};
use std::os::unix::raw::{pid_t, uid_t};
@@ -69,16 +75,25 @@
create_dir(common_dir_path).context("Failed to create common directory")?;
ProcessState::start_thread_pool();
- register(INTERNAL_SERVICE_NAME, VirtualizationServiceInternal::init())?;
+
+ // One instance of `VirtualizationServiceInternal` implements both the internal interface
+ // and (optionally) the maintenance interface.
+ let service = VirtualizationServiceInternal::init();
+ let internal_service =
+ BnVirtualizationServiceInternal::new_binder(service.clone(), BinderFeatures::default());
+ register(INTERNAL_SERVICE_NAME, internal_service)?;
if cfg!(remote_attestation) {
// The IRemotelyProvisionedComponent service is only supposed to be triggered by rkpd for
// RKP VM attestation.
- register(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME, remote_provisioning::new_binder())?;
+ let remote_provisioning_service = remote_provisioning::new_binder();
+ register(REMOTELY_PROVISIONED_COMPONENT_SERVICE_NAME, remote_provisioning_service)?;
}
if cfg!(llpvm_changes) {
- register(MAINTENANCE_SERVICE_NAME, maintenance::new_binder())?;
+ let maintenance_service =
+ BnVirtualizationMaintenance::new_binder(service.clone(), BinderFeatures::default());
+ register(MAINTENANCE_SERVICE_NAME, maintenance_service)?;
}
ProcessState::join_thread_pool();
diff --git a/virtualizationservice/src/maintenance.rs b/virtualizationservice/src/maintenance.rs
index 191d39a..7fc2f37 100644
--- a/virtualizationservice/src/maintenance.rs
+++ b/virtualizationservice/src/maintenance.rs
@@ -12,33 +12,245 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use android_system_virtualizationmaintenance::aidl::android::system::virtualizationmaintenance;
-use anyhow::anyhow;
-use binder::{BinderFeatures, ExceptionCode, Interface, IntoBinderResult, Strong};
-use virtualizationmaintenance::IVirtualizationMaintenance::{
- BnVirtualizationMaintenance, IVirtualizationMaintenance,
+use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
+ ISecretkeeper::ISecretkeeper, SecretId::SecretId,
};
+use anyhow::Result;
+use log::{error, info, warn};
-pub(crate) fn new_binder() -> Strong<dyn IVirtualizationMaintenance> {
- BnVirtualizationMaintenance::new_binder(
- VirtualizationMaintenanceService {},
- BinderFeatures::default(),
- )
+mod vmdb;
+use vmdb::{VmId, VmIdDb};
+
+/// Interface name for the Secretkeeper HAL.
+const SECRETKEEPER_SERVICE: &str = "android.hardware.security.secretkeeper.ISecretkeeper/default";
+
+/// Directory in which to write persistent state.
+const PERSISTENT_DIRECTORY: &str = "/data/misc/apexdata/com.android.virt";
+
+/// Maximum number of VM IDs to delete at once. Needs to be smaller than both the maximum
+/// number of SQLite parameters (999) and also small enough that an ISecretkeeper::deleteIds
+/// parcel fits within max AIDL message size.
+const DELETE_MAX_BATCH_SIZE: usize = 100;
+
+/// State related to VM secrets.
+pub struct State {
+ sk: binder::Strong<dyn ISecretkeeper>,
+ /// Database of VM IDs,
+ vm_id_db: VmIdDb,
+ batch_size: usize,
}
-pub struct VirtualizationMaintenanceService;
-
-impl Interface for VirtualizationMaintenanceService {}
-
-#[allow(non_snake_case)]
-impl IVirtualizationMaintenance for VirtualizationMaintenanceService {
- fn appRemoved(&self, _user_id: i32, _app_id: i32) -> binder::Result<()> {
- Err(anyhow!("appRemoved not supported"))
- .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+impl State {
+ pub fn new() -> Option<Self> {
+ let sk = match Self::find_sk() {
+ Some(sk) => sk,
+ None => {
+ warn!("failed to find a Secretkeeper instance; skipping secret management");
+ return None;
+ }
+ };
+ let (vm_id_db, created) = match VmIdDb::new(PERSISTENT_DIRECTORY) {
+ Ok(v) => v,
+ Err(e) => {
+ error!("skipping secret management, failed to connect to database: {e:?}");
+ return None;
+ }
+ };
+ if created {
+ // If the database did not previously exist, then this appears to be the first run of
+ // `virtualizationservice` since device setup or factory reset. In case of the latter,
+ // delete any secrets that may be left over from before reset, thus ensuring that the
+ // local database state matches that of the TA (i.e. empty).
+ warn!("no existing VM ID DB; clearing any previous secrets to match fresh DB");
+ if let Err(e) = sk.deleteAll() {
+ error!("failed to delete previous secrets, dropping database: {e:?}");
+ vm_id_db.delete_db_file(PERSISTENT_DIRECTORY);
+ return None;
+ }
+ } else {
+ info!("re-using existing VM ID DB");
+ }
+ Some(Self { sk, vm_id_db, batch_size: DELETE_MAX_BATCH_SIZE })
}
- fn userRemoved(&self, _user_id: i32) -> binder::Result<()> {
- Err(anyhow!("userRemoved not supported"))
- .or_binder_exception(ExceptionCode::UNSUPPORTED_OPERATION)
+ fn find_sk() -> Option<binder::Strong<dyn ISecretkeeper>> {
+ if let Ok(true) = binder::is_declared(SECRETKEEPER_SERVICE) {
+ match binder::get_interface(SECRETKEEPER_SERVICE) {
+ Ok(sk) => Some(sk),
+ Err(e) => {
+ error!("failed to connect to {SECRETKEEPER_SERVICE}: {e:?}");
+ None
+ }
+ }
+ } else {
+ info!("instance {SECRETKEEPER_SERVICE} not declared");
+ None
+ }
+ }
+
+ /// Delete the VM IDs associated with Android user ID `user_id`.
+ pub fn delete_ids_for_user(&mut self, user_id: i32) -> Result<()> {
+ let vm_ids = self.vm_id_db.vm_ids_for_user(user_id)?;
+ info!(
+ "delete_ids_for_user(user_id={user_id}) triggers deletion of {} secrets",
+ vm_ids.len()
+ );
+ self.delete_ids(&vm_ids);
+ Ok(())
+ }
+
+ /// Delete the VM IDs associated with `(user_id, app_id)`.
+ pub fn delete_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<()> {
+ let vm_ids = self.vm_id_db.vm_ids_for_app(user_id, app_id)?;
+ info!(
+ "delete_ids_for_app(user_id={user_id}, app_id={app_id}) removes {} secrets",
+ vm_ids.len()
+ );
+ self.delete_ids(&vm_ids);
+ Ok(())
+ }
+
+ /// Delete the provided VM IDs from both Secretkeeper and the database.
+ pub fn delete_ids(&mut self, mut vm_ids: &[VmId]) {
+ while !vm_ids.is_empty() {
+ let len = std::cmp::min(vm_ids.len(), self.batch_size);
+ let batch = &vm_ids[..len];
+ self.delete_ids_batch(batch);
+ vm_ids = &vm_ids[len..];
+ }
+ }
+
+ /// Delete a batch of VM IDs from both Secretkeeper and the database. The batch is assumed
+ /// to be smaller than both:
+ /// - the corresponding limit for number of database parameters
+ /// - the corresponding limit for maximum size of a single AIDL message for `ISecretkeeper`.
+ fn delete_ids_batch(&mut self, vm_ids: &[VmId]) {
+ let secret_ids: Vec<SecretId> = vm_ids.iter().map(|id| SecretId { id: *id }).collect();
+ if let Err(e) = self.sk.deleteIds(&secret_ids) {
+ error!("failed to delete all secrets from Secretkeeper: {e:?}");
+ }
+ if let Err(e) = self.vm_id_db.delete_vm_ids(vm_ids) {
+ error!("failed to remove secret IDs from database: {e:?}");
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::sync::{Arc, Mutex};
+ use android_hardware_security_authgraph::aidl::android::hardware::security::authgraph::{
+ IAuthGraphKeyExchange::IAuthGraphKeyExchange,
+ };
+ use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{
+ ISecretkeeper::BnSecretkeeper
+ };
+
+ /// Fake implementation of Secretkeeper that keeps a history of what operations were invoked.
+ #[derive(Default)]
+ struct FakeSk {
+ history: Arc<Mutex<Vec<SkOp>>>,
+ }
+
+ #[derive(Clone, PartialEq, Eq, Debug)]
+ enum SkOp {
+ Management,
+ DeleteIds(Vec<VmId>),
+ DeleteAll,
+ }
+
+ impl ISecretkeeper for FakeSk {
+ fn processSecretManagementRequest(&self, _req: &[u8]) -> binder::Result<Vec<u8>> {
+ self.history.lock().unwrap().push(SkOp::Management);
+ Ok(vec![])
+ }
+
+ fn getAuthGraphKe(&self) -> binder::Result<binder::Strong<dyn IAuthGraphKeyExchange>> {
+ unimplemented!()
+ }
+
+ fn deleteIds(&self, ids: &[SecretId]) -> binder::Result<()> {
+ self.history.lock().unwrap().push(SkOp::DeleteIds(ids.iter().map(|s| s.id).collect()));
+ Ok(())
+ }
+
+ fn deleteAll(&self) -> binder::Result<()> {
+ self.history.lock().unwrap().push(SkOp::DeleteAll);
+ Ok(())
+ }
+ }
+ impl binder::Interface for FakeSk {}
+
+ fn new_test_state(history: Arc<Mutex<Vec<SkOp>>>, batch_size: usize) -> State {
+ let vm_id_db = vmdb::new_test_db();
+ let sk = FakeSk { history };
+ let sk = BnSecretkeeper::new_binder(sk, binder::BinderFeatures::default());
+ State { sk, vm_id_db, batch_size }
+ }
+
+ const VM_ID1: VmId = [1u8; 64];
+ const VM_ID2: VmId = [2u8; 64];
+ const VM_ID3: VmId = [3u8; 64];
+ const VM_ID4: VmId = [4u8; 64];
+ const VM_ID5: VmId = [5u8; 64];
+
+ #[test]
+ fn test_sk_state_batching() {
+ let history = Arc::new(Mutex::new(Vec::new()));
+ let mut sk_state = new_test_state(history.clone(), 2);
+ sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+ let got = (*history.lock().unwrap()).clone();
+ assert_eq!(
+ got,
+ vec![
+ SkOp::DeleteIds(vec![VM_ID1, VM_ID2]),
+ SkOp::DeleteIds(vec![VM_ID3, VM_ID4]),
+ SkOp::DeleteIds(vec![VM_ID5]),
+ ]
+ );
+ }
+
+ #[test]
+ fn test_sk_state_no_batching() {
+ let history = Arc::new(Mutex::new(Vec::new()));
+ let mut sk_state = new_test_state(history.clone(), 6);
+ sk_state.delete_ids(&[VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5]);
+ let got = (*history.lock().unwrap()).clone();
+ assert_eq!(got, vec![SkOp::DeleteIds(vec![VM_ID1, VM_ID2, VM_ID3, VM_ID4, VM_ID5])]);
+ }
+
+ #[test]
+ fn test_sk_state() {
+ const USER1: i32 = 1;
+ const USER2: i32 = 2;
+ const USER3: i32 = 3;
+ const APP_A: i32 = 50;
+ const APP_B: i32 = 60;
+ const APP_C: i32 = 70;
+
+ let history = Arc::new(Mutex::new(Vec::new()));
+ let mut sk_state = new_test_state(history.clone(), 2);
+
+ sk_state.vm_id_db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+ sk_state.vm_id_db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+ sk_state.vm_id_db.add_vm_id(&VM_ID3, USER2, APP_B).unwrap();
+ sk_state.vm_id_db.add_vm_id(&VM_ID4, USER3, APP_A).unwrap();
+ sk_state.vm_id_db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+ assert_eq!((*history.lock().unwrap()).clone(), vec![]);
+
+ sk_state.delete_ids_for_app(USER2, APP_B).unwrap();
+ assert_eq!((*history.lock().unwrap()).clone(), vec![SkOp::DeleteIds(vec![VM_ID3])]);
+
+ sk_state.delete_ids_for_user(USER3).unwrap();
+ assert_eq!(
+ (*history.lock().unwrap()).clone(),
+ vec![SkOp::DeleteIds(vec![VM_ID3]), SkOp::DeleteIds(vec![VM_ID4, VM_ID5]),]
+ );
+
+ assert_eq!(vec![VM_ID1, VM_ID2], sk_state.vm_id_db.vm_ids_for_user(USER1).unwrap());
+ assert_eq!(vec![VM_ID1, VM_ID2], sk_state.vm_id_db.vm_ids_for_app(USER1, APP_A).unwrap());
+ let empty: Vec<VmId> = Vec::new();
+ assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_app(USER2, APP_B).unwrap());
+ assert_eq!(empty, sk_state.vm_id_db.vm_ids_for_user(USER3).unwrap());
}
}
diff --git a/virtualizationservice/src/maintenance/vmdb.rs b/virtualizationservice/src/maintenance/vmdb.rs
new file mode 100644
index 0000000..bdff034
--- /dev/null
+++ b/virtualizationservice/src/maintenance/vmdb.rs
@@ -0,0 +1,265 @@
+// 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.
+
+//! Database of VM IDs.
+
+use anyhow::{Context, Result};
+use log::{debug, error, info, warn};
+use rusqlite::{params, params_from_iter, Connection, OpenFlags, Rows};
+use std::path::PathBuf;
+
+/// Subdirectory to hold the database.
+const DB_DIR: &str = "vmdb";
+
+/// Name of the file that holds the database.
+const DB_FILENAME: &str = "vmids.sqlite";
+
+/// Maximum number of host parameters in a single SQL statement.
+/// (Default value of `SQLITE_LIMIT_VARIABLE_NUMBER` for <= 3.32.0)
+const MAX_VARIABLES: usize = 999;
+
+/// Identifier for a VM and its corresponding secret.
+pub type VmId = [u8; 64];
+
+/// Representation of an on-disk database of VM IDs.
+pub struct VmIdDb {
+ conn: Connection,
+}
+
+impl VmIdDb {
+ /// Connect to the VM ID database file held in the given directory, creating it if necessary.
+ /// The second return value indicates whether a new database file was created.
+ ///
+ /// This function assumes no other threads/processes are attempting to connect concurrently.
+ pub fn new(db_dir: &str) -> Result<(Self, bool)> {
+ let mut db_path = PathBuf::from(db_dir);
+ db_path.push(DB_DIR);
+ if !db_path.exists() {
+ std::fs::create_dir(&db_path).context("failed to create {db_path:?}")?;
+ info!("created persistent db dir {db_path:?}");
+ }
+
+ db_path.push(DB_FILENAME);
+ let (flags, created) = if db_path.exists() {
+ debug!("connecting to existing database {db_path:?}");
+ (
+ OpenFlags::SQLITE_OPEN_READ_WRITE
+ | OpenFlags::SQLITE_OPEN_URI
+ | OpenFlags::SQLITE_OPEN_NO_MUTEX,
+ false,
+ )
+ } else {
+ info!("creating fresh database {db_path:?}");
+ (
+ OpenFlags::SQLITE_OPEN_READ_WRITE
+ | OpenFlags::SQLITE_OPEN_CREATE
+ | OpenFlags::SQLITE_OPEN_URI
+ | OpenFlags::SQLITE_OPEN_NO_MUTEX,
+ true,
+ )
+ };
+ let mut result = Self {
+ conn: Connection::open_with_flags(db_path, flags)
+ .context(format!("failed to open/create DB with {flags:?}"))?,
+ };
+
+ if created {
+ result.init_tables().context("failed to create tables")?;
+ }
+ Ok((result, created))
+ }
+
+ /// Delete the associated database file.
+ pub fn delete_db_file(self, db_dir: &str) {
+ let mut db_path = PathBuf::from(db_dir);
+ db_path.push(DB_DIR);
+ db_path.push(DB_FILENAME);
+
+ // Drop the connection before removing the backing file.
+ drop(self);
+ warn!("removing database file {db_path:?}");
+ if let Err(e) = std::fs::remove_file(&db_path) {
+ error!("failed to remove database file {db_path:?}: {e:?}");
+ }
+ }
+
+ /// Create the database table and indices.
+ fn init_tables(&mut self) -> Result<()> {
+ self.conn
+ .execute(
+ "CREATE TABLE IF NOT EXISTS main.vmids (
+ vm_id BLOB PRIMARY KEY,
+ user_id INTEGER,
+ app_id INTEGER
+ ) WITHOUT ROWID;",
+ (),
+ )
+ .context("failed to create table")?;
+ self.conn
+ .execute("CREATE INDEX IF NOT EXISTS main.vmids_user_index ON vmids(user_id);", [])
+ .context("Failed to create user index")?;
+ self.conn
+ .execute(
+ "CREATE INDEX IF NOT EXISTS main.vmids_app_index ON vmids(user_id, app_id);",
+ [],
+ )
+ .context("Failed to create app index")?;
+ Ok(())
+ }
+
+ /// Add the given VM ID into the database.
+ #[allow(dead_code)] // TODO(b/294177871): connect this up
+ pub fn add_vm_id(&mut self, vm_id: &VmId, user_id: i32, app_id: i32) -> Result<()> {
+ let _rows = self
+ .conn
+ .execute(
+ "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+ params![vm_id, &user_id, &app_id],
+ )
+ .context("failed to add VM ID")?;
+ Ok(())
+ }
+
+ /// Remove the given VM IDs from the database. The collection of IDs is assumed to be smaller
+ /// than the maximum number of SQLite parameters.
+ pub fn delete_vm_ids(&mut self, vm_ids: &[VmId]) -> Result<()> {
+ assert!(vm_ids.len() < MAX_VARIABLES);
+ let mut vars = "?,".repeat(vm_ids.len());
+ vars.pop(); // remove trailing comma
+ let sql = format!("DELETE FROM main.vmids WHERE vm_id IN ({});", vars);
+ let mut stmt = self.conn.prepare(&sql).context("failed to prepare DELETE stmt")?;
+ let _rows = stmt.execute(params_from_iter(vm_ids)).context("failed to delete VM IDs")?;
+ Ok(())
+ }
+
+ /// Return the VM IDs associated with Android user ID `user_id`.
+ pub fn vm_ids_for_user(&mut self, user_id: i32) -> Result<Vec<VmId>> {
+ let mut stmt = self
+ .conn
+ .prepare("SELECT vm_id FROM main.vmids WHERE user_id = ?;")
+ .context("failed to prepare SELECT stmt")?;
+ let rows = stmt.query(params![user_id]).context("query failed")?;
+ Self::vm_ids_from_rows(rows)
+ }
+
+ /// Return the VM IDs associated with `(user_id, app_id)`.
+ pub fn vm_ids_for_app(&mut self, user_id: i32, app_id: i32) -> Result<Vec<VmId>> {
+ let mut stmt = self
+ .conn
+ .prepare("SELECT vm_id FROM main.vmids WHERE user_id = ? AND app_id = ?;")
+ .context("failed to prepare SELECT stmt")?;
+ let rows = stmt.query(params![user_id, app_id]).context("query failed")?;
+ Self::vm_ids_from_rows(rows)
+ }
+
+ /// Retrieve a collection of VM IDs from database rows.
+ fn vm_ids_from_rows(mut rows: Rows) -> Result<Vec<VmId>> {
+ let mut vm_ids: Vec<VmId> = Vec::new();
+ while let Some(row) = rows.next().context("failed row unpack")? {
+ match row.get(0) {
+ Ok(vm_id) => vm_ids.push(vm_id),
+ Err(e) => log::error!("failed to parse row: {e:?}"),
+ }
+ }
+
+ Ok(vm_ids)
+ }
+}
+
+#[cfg(test)]
+pub fn new_test_db() -> VmIdDb {
+ let mut db = VmIdDb { conn: Connection::open_in_memory().unwrap() };
+ db.init_tables().unwrap();
+ db
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ const VM_ID1: VmId = [1u8; 64];
+ const VM_ID2: VmId = [2u8; 64];
+ const VM_ID3: VmId = [3u8; 64];
+ const VM_ID4: VmId = [4u8; 64];
+ const VM_ID5: VmId = [5u8; 64];
+ const USER1: i32 = 1;
+ const USER2: i32 = 2;
+ const USER3: i32 = 3;
+ const USER_UNKNOWN: i32 = 4;
+ const APP_A: i32 = 50;
+ const APP_B: i32 = 60;
+ const APP_C: i32 = 70;
+ const APP_UNKNOWN: i32 = 99;
+
+ #[test]
+ fn test_add_remove() {
+ let mut db = new_test_db();
+ db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID4, USER2, APP_B).unwrap();
+ db.add_vm_id(&VM_ID5, USER3, APP_A).unwrap();
+ db.add_vm_id(&VM_ID5, USER3, APP_C).unwrap();
+ let empty: Vec<VmId> = Vec::new();
+
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+ assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+ assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
+ assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
+ assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+
+ db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
+
+ assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
+ assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+
+ // OK to delete things that don't exist.
+ db.delete_vm_ids(&[VM_ID2, VM_ID3]).unwrap();
+
+ assert_eq!(vec![VM_ID1], db.vm_ids_for_user(USER1).unwrap());
+ assert_eq!(vec![VM_ID1], db.vm_ids_for_app(USER1, APP_A).unwrap());
+
+ db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
+
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_app(USER1, APP_A).unwrap());
+ assert_eq!(vec![VM_ID4], db.vm_ids_for_app(USER2, APP_B).unwrap());
+ assert_eq!(vec![VM_ID5], db.vm_ids_for_user(USER3).unwrap());
+ assert_eq!(empty, db.vm_ids_for_user(USER_UNKNOWN).unwrap());
+ assert_eq!(empty, db.vm_ids_for_app(USER1, APP_UNKNOWN).unwrap());
+ }
+
+ #[test]
+ fn test_invalid_vm_id() {
+ let mut db = new_test_db();
+ db.add_vm_id(&VM_ID3, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID2, USER1, APP_A).unwrap();
+ db.add_vm_id(&VM_ID1, USER1, APP_A).unwrap();
+
+ // Note that results are returned in `vm_id` order, because the table is `WITHOUT ROWID`.
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+
+ // Manually insert a row with a VM ID that's the wrong size.
+ db.conn
+ .execute(
+ "REPLACE INTO main.vmids (vm_id, user_id, app_id) VALUES (?1, ?2, ?3);",
+ params![&[99u8; 60], &USER1, APP_A],
+ )
+ .unwrap();
+
+ // Invalid row is skipped and remainder returned.
+ assert_eq!(vec![VM_ID1, VM_ID2, VM_ID3], db.vm_ids_for_user(USER1).unwrap());
+ }
+}
diff --git a/vm/src/main.rs b/vm/src/main.rs
index 355e193..063f992 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -22,6 +22,8 @@
CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
PartitionType::PartitionType, VirtualMachineAppConfig::DebugLevel::DebugLevel,
};
+#[cfg(not(llpvm_changes))]
+use anyhow::anyhow;
use anyhow::{Context, Error};
use binder::{ProcessState, Strong};
use clap::{Args, Parser};
@@ -162,6 +164,11 @@
/// Path to the instance image. Created if not exists.
instance: PathBuf,
+ /// Path to file containing instance_id. Required iff llpvm feature is enabled.
+ #[cfg(llpvm_changes)]
+ #[arg(long = "instance-id-file")]
+ instance_id: PathBuf,
+
/// Path to VM config JSON within APK (e.g. assets/vm_config.json)
#[arg(long)]
config_path: Option<String>,
@@ -192,6 +199,27 @@
fn extra_apks(&self) -> &[PathBuf] {
&[]
}
+
+ #[cfg(llpvm_changes)]
+ fn instance_id(&self) -> Result<PathBuf, Error> {
+ Ok(self.instance_id.clone())
+ }
+
+ #[cfg(not(llpvm_changes))]
+ fn instance_id(&self) -> Result<PathBuf, Error> {
+ Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+ }
+
+ #[cfg(llpvm_changes)]
+ fn set_instance_id(&mut self, instance_id_file: PathBuf) -> Result<(), Error> {
+ self.instance_id = instance_id_file;
+ Ok(())
+ }
+
+ #[cfg(not(llpvm_changes))]
+ fn set_instance_id(&mut self, _: PathBuf) -> Result<(), Error> {
+ Err(anyhow!("LLPVM feature is disabled, --instance_id flag not supported"))
+ }
}
#[derive(Args, Default)]
diff --git a/vm/src/run.rs b/vm/src/run.rs
index 5a4a459..57b7641 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -35,6 +35,7 @@
use std::fs;
use std::fs::File;
use std::io;
+use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::path::{Path, PathBuf};
use vmclient::{ErrorCode, VmInstance};
@@ -84,6 +85,24 @@
)?;
}
+ let instance_id = if cfg!(llpvm_changes) {
+ let id_file = config.instance_id()?;
+ if id_file.exists() {
+ let mut id = [0u8; 64];
+ let mut instance_id_file = File::open(id_file)?;
+ instance_id_file.read_exact(&mut id)?;
+ id
+ } else {
+ let id = service.allocateInstanceId().context("Failed to allocate instance_id")?;
+ let mut instance_id_file = File::create(id_file)?;
+ instance_id_file.write_all(&id)?;
+ id
+ }
+ } else {
+ // if llpvm feature flag is disabled, instance_id is not used.
+ [0u8; 64]
+ };
+
let storage = if let Some(ref path) = config.microdroid.storage {
if !path.exists() {
command_create_partition(
@@ -153,6 +172,7 @@
idsig: idsig_fd.into(),
extraIdsigs: extra_idsig_fds,
instanceImage: open_parcel_file(&config.instance, true /* writable */)?.into(),
+ instanceId: instance_id,
encryptedStorageImage: storage,
payload,
debugLevel: config.debug.debug,
@@ -204,7 +224,7 @@
let instance_img = work_dir.join("instance.img");
println!("instance.img path: {}", instance_img.display());
- let app_config = RunAppConfig {
+ let mut app_config = RunAppConfig {
common: config.common,
debug: config.debug,
microdroid: config.microdroid,
@@ -214,6 +234,12 @@
payload_binary_name: Some("MicrodroidEmptyPayloadJniLib.so".to_owned()),
..Default::default()
};
+
+ if cfg!(llpvm_changes) {
+ app_config.set_instance_id(work_dir.join("instance_id"))?;
+ println!("instance_id file path: {}", app_config.instance_id()?.display());
+ }
+
command_run_app(app_config)
}
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index a745fd6..80d289b 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -34,7 +34,7 @@
source_stem: "bindings",
bindgen_flags: [
"--default-enum-style rust",
- "--allowlist-type=attestation_status_t",
+ "--allowlist-type=AVmAttestationStatus",
],
visibility: [":__subpackages__"],
}
diff --git a/vm_payload/include-restricted/vm_payload_restricted.h b/vm_payload/include-restricted/vm_payload_restricted.h
index d7324a8..5dd12ad 100644
--- a/vm_payload/include-restricted/vm_payload_restricted.h
+++ b/vm_payload/include-restricted/vm_payload_restricted.h
@@ -72,7 +72,7 @@
* succeeds. The result remains valid until it is freed with
* `AVmPayload_freeAttestationResult`.
*/
-attestation_status_t AVmPayload_requestAttestationForTesting(
+AVmAttestationStatus AVmPayload_requestAttestationForTesting(
const void* _Nonnull challenge, size_t challenge_size,
struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
diff --git a/vm_payload/include/vm_payload.h b/vm_payload/include/vm_payload.h
index af755c9..5e15607 100644
--- a/vm_payload/include/vm_payload.h
+++ b/vm_payload/include/vm_payload.h
@@ -25,20 +25,19 @@
__BEGIN_DECLS
-struct AIBinder;
typedef struct AIBinder AIBinder;
/**
* Introduced in API 35.
* Remote attestation result if the attestation succeeds.
*/
-struct AVmAttestationResult;
+typedef struct AVmAttestationResult AVmAttestationResult;
/**
* Introduced in API 35.
* Remote attestation status types returned from remote attestation functions.
*/
-typedef enum attestation_status_t : int32_t {
+typedef enum AVmAttestationStatus : int32_t {
/** The remote attestation completes successfully. */
ATTESTATION_OK = 0,
@@ -50,7 +49,7 @@
/** Remote attestation is not supported in the current environment. */
ATTESTATION_ERROR_UNSUPPORTED = -10003,
-} attestation_status_t;
+} AVmAttestationStatus;
/**
* Notifies the host that the payload is ready.
@@ -151,9 +150,10 @@
*
* \return ATTESTATION_OK upon successful attestation.
*/
-attestation_status_t AVmPayload_requestAttestation(
- const void* _Nonnull challenge, size_t challenge_size,
- struct AVmAttestationResult* _Nullable* _Nonnull result) __INTRODUCED_IN(__ANDROID_API_V__);
+AVmAttestationStatus AVmPayload_requestAttestation(const void* _Nonnull challenge,
+ size_t challenge_size,
+ AVmAttestationResult* _Nullable* _Nonnull result)
+ __INTRODUCED_IN(__ANDROID_API_V__);
/**
* Converts the return value from `AVmPayload_requestAttestation` to a text string
@@ -162,7 +162,7 @@
* \return a constant string value representing the status code. The string should not
* be deleted or freed by the application and remains valid for the lifetime of the VM.
*/
-const char* _Nonnull AVmAttestationResult_resultToString(attestation_status_t status)
+const char* _Nonnull AVmAttestationStatus_toString(AVmAttestationStatus status)
__INTRODUCED_IN(__ANDROID_API_V__);
/**
@@ -173,7 +173,7 @@
*
* \param result A pointer to the attestation result.
*/
-void AVmAttestationResult_free(struct AVmAttestationResult* _Nullable result)
+void AVmAttestationResult_free(AVmAttestationResult* _Nullable result)
__INTRODUCED_IN(__ANDROID_API_V__);
/**
@@ -192,7 +192,7 @@
*
* [RFC 5915 s3]: https://datatracker.ietf.org/doc/html/rfc5915#section-3
*/
-size_t AVmAttestationResult_getPrivateKey(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_getPrivateKey(const AVmAttestationResult* _Nonnull result,
void* _Nullable data, size_t size)
__INTRODUCED_IN(__ANDROID_API_V__);
@@ -215,7 +215,7 @@
*
* [RFC 6979]: https://datatracker.ietf.org/doc/html/rfc6979
*/
-size_t AVmAttestationResult_sign(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_sign(const AVmAttestationResult* _Nonnull result,
const void* _Nonnull message, size_t message_size,
void* _Nullable data, size_t size)
__INTRODUCED_IN(__ANDROID_API_V__);
@@ -232,7 +232,7 @@
*
* \return The number of certificates in the certificate chain.
*/
-size_t AVmAttestationResult_getCertificateCount(const struct AVmAttestationResult* _Nonnull result)
+size_t AVmAttestationResult_getCertificateCount(const AVmAttestationResult* _Nonnull result)
__INTRODUCED_IN(__ANDROID_API_V__);
/**
@@ -256,7 +256,7 @@
*
* \return The total size of the certificate at the given `index`.
*/
-size_t AVmAttestationResult_getCertificateAt(const struct AVmAttestationResult* _Nonnull result,
+size_t AVmAttestationResult_getCertificateAt(const AVmAttestationResult* _Nonnull result,
size_t index, void* _Nullable data, size_t size)
__INTRODUCED_IN(__ANDROID_API_V__);
diff --git a/vm_payload/libvm_payload.map.txt b/vm_payload/libvm_payload.map.txt
index caf8f84..3daad00 100644
--- a/vm_payload/libvm_payload.map.txt
+++ b/vm_payload/libvm_payload.map.txt
@@ -12,7 +12,7 @@
AVmAttestationResult_getPrivateKey; # systemapi introduced=VanillaIceCream
AVmAttestationResult_sign; # systemapi introduced=VanillaIceCream
AVmAttestationResult_free; # systemapi introduced=VanillaIceCream
- AVmAttestationResult_resultToString; # systemapi introduced=VanillaIceCream
+ AVmAttestationStatus_toString; # systemapi introduced=VanillaIceCream
AVmAttestationResult_getCertificateCount; # systemapi introduced=VanillaIceCream
AVmAttestationResult_getCertificateAt; # systemapi introduced=VanillaIceCream
local:
diff --git a/vm_payload/src/lib.rs b/vm_payload/src/lib.rs
index 6188b21..5cc4431 100644
--- a/vm_payload/src/lib.rs
+++ b/vm_payload/src/lib.rs
@@ -37,7 +37,7 @@
atomic::{AtomicBool, Ordering},
Mutex,
};
-use vm_payload_status_bindgen::attestation_status_t;
+use vm_payload_status_bindgen::AVmAttestationStatus;
/// Maximum size of an ECDSA signature for EC P-256 key is 72 bytes.
const MAX_ECDSA_P256_SIGNATURE_SIZE: usize = 72;
@@ -283,7 +283,7 @@
challenge: *const u8,
challenge_size: usize,
res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
// SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
// for writes.
unsafe {
@@ -310,7 +310,7 @@
challenge: *const u8,
challenge_size: usize,
res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
// SAFETY: The caller guarantees that `challenge` is valid for reads and `res` is valid
// for writes.
unsafe {
@@ -337,11 +337,11 @@
challenge_size: usize,
test_mode: bool,
res: &mut *mut AttestationResult,
-) -> attestation_status_t {
+) -> AVmAttestationStatus {
initialize_logging();
const MAX_CHALLENGE_SIZE: usize = 64;
if challenge_size > MAX_CHALLENGE_SIZE {
- return attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE;
+ return AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE;
}
let challenge = if challenge_size == 0 {
&[]
@@ -354,7 +354,7 @@
match service.requestAttestation(challenge, test_mode) {
Ok(attestation_res) => {
*res = Box::into_raw(Box::new(attestation_res));
- attestation_status_t::ATTESTATION_OK
+ AVmAttestationStatus::ATTESTATION_OK
}
Err(e) => {
error!("Remote attestation failed: {e:?}");
@@ -363,31 +363,29 @@
}
}
-fn binder_status_to_attestation_status(status: binder::Status) -> attestation_status_t {
+fn binder_status_to_attestation_status(status: binder::Status) -> AVmAttestationStatus {
match status.exception_code() {
- ExceptionCode::UNSUPPORTED_OPERATION => attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED,
- _ => attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED,
+ ExceptionCode::UNSUPPORTED_OPERATION => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+ _ => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
}
}
/// Converts the return value from `AVmPayload_requestAttestation` to a text string
/// representing the error code.
#[no_mangle]
-pub extern "C" fn AVmAttestationResult_resultToString(
- status: attestation_status_t,
-) -> *const c_char {
+pub extern "C" fn AVmAttestationStatus_toString(status: AVmAttestationStatus) -> *const c_char {
let message = match status {
- attestation_status_t::ATTESTATION_OK => {
+ AVmAttestationStatus::ATTESTATION_OK => {
CStr::from_bytes_with_nul(b"The remote attestation completes successfully.\0").unwrap()
}
- attestation_status_t::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+ AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
CStr::from_bytes_with_nul(b"The challenge size is not between 0 and 64.\0").unwrap()
}
- attestation_status_t::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+ AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
CStr::from_bytes_with_nul(b"Failed to attest the VM. Please retry at a later time.\0")
.unwrap()
}
- attestation_status_t::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
+ AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => CStr::from_bytes_with_nul(
b"Remote attestation is not supported in the current environment.\0",
)
.unwrap(),