1 /*
2 * Copyright (C) 2021 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 //! Support for starting CompOS in a VM and connecting to the service
18
19 use crate::timeouts::TIMEOUTS;
20 use crate::{
21 get_vm_config_path, BUILD_MANIFEST_APK_PATH, BUILD_MANIFEST_SYSTEM_EXT_APK_PATH,
22 COMPOS_APEX_ROOT, COMPOS_VSOCK_PORT,
23 };
24 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
25 CpuTopology::CpuTopology,
26 IVirtualizationService::IVirtualizationService,
27 VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
28 VirtualMachineConfig::VirtualMachineConfig,
29 };
30 use anyhow::{anyhow, bail, Context, Result};
31 use binder::{ParcelFileDescriptor, Strong};
32 use compos_aidl_interface::aidl::com::android::compos::ICompOsService::ICompOsService;
33 use glob::glob;
34 use log::{info, warn};
35 use rustutils::system_properties;
36 use std::fs::File;
37 use std::path::{Path, PathBuf};
38 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
39
40 /// This owns an instance of the CompOS VM.
41 pub struct ComposClient(VmInstance);
42
43 /// CPU topology configuration for a virtual machine.
44 #[derive(Default, Debug, Clone)]
45 pub enum VmCpuTopology {
46 /// Run VM with 1 vCPU only.
47 #[default]
48 OneCpu,
49 /// Run VM vCPU topology matching that of the host.
50 MatchHost,
51 }
52
53 /// Parameters to be used when creating a virtual machine instance.
54 #[derive(Default, Debug, Clone)]
55 pub struct VmParameters {
56 /// The name of VM for identifying.
57 pub name: String,
58 /// Whether the VM should be debuggable.
59 pub debug_mode: bool,
60 /// CPU topology of the VM. Defaults to 1 vCPU.
61 pub cpu_topology: VmCpuTopology,
62 /// List of task profiles to apply to the VM
63 pub task_profiles: Vec<String>,
64 /// If present, overrides the amount of RAM to give the VM
65 pub memory_mib: Option<i32>,
66 /// Whether the VM prefers staged APEXes or activated ones (false; default)
67 pub prefer_staged: bool,
68 }
69
70 impl ComposClient {
71 /// Start a new CompOS VM instance using the specified instance image file and parameters.
start( service: &dyn IVirtualizationService, instance_image: File, idsig: &Path, idsig_manifest_apk: &Path, idsig_manifest_ext_apk: &Path, parameters: &VmParameters, ) -> Result<Self>72 pub fn start(
73 service: &dyn IVirtualizationService,
74 instance_image: File,
75 idsig: &Path,
76 idsig_manifest_apk: &Path,
77 idsig_manifest_ext_apk: &Path,
78 parameters: &VmParameters,
79 ) -> Result<Self> {
80 let protected_vm = want_protected_vm()?;
81
82 let instance_fd = ParcelFileDescriptor::new(instance_image);
83
84 let apex_dir = Path::new(COMPOS_APEX_ROOT);
85
86 let config_apk = locate_config_apk(apex_dir)?;
87 let apk_fd = File::open(config_apk).context("Failed to open config APK file")?;
88 let apk_fd = ParcelFileDescriptor::new(apk_fd);
89 let idsig_fd = prepare_idsig(service, &apk_fd, idsig)?;
90
91 let manifest_apk_fd = File::open(BUILD_MANIFEST_APK_PATH)
92 .context("Failed to open build manifest APK file")?;
93 let manifest_apk_fd = ParcelFileDescriptor::new(manifest_apk_fd);
94 let idsig_manifest_apk_fd = prepare_idsig(service, &manifest_apk_fd, idsig_manifest_apk)?;
95
96 // Prepare a few things based on whether /system_ext exists, including:
97 // 1. generate the additional idsig FD for the APK from /system_ext, then pass to VS
98 // 2. select the correct VM config json
99 let (extra_idsigs, has_system_ext) =
100 if let Ok(manifest_ext_apk_fd) = File::open(BUILD_MANIFEST_SYSTEM_EXT_APK_PATH) {
101 // Optional idsig in /system_ext is found, so prepare additionally.
102 let manifest_ext_apk_fd = ParcelFileDescriptor::new(manifest_ext_apk_fd);
103 let idsig_manifest_ext_apk_fd =
104 prepare_idsig(service, &manifest_ext_apk_fd, idsig_manifest_ext_apk)?;
105
106 (vec![idsig_manifest_apk_fd, idsig_manifest_ext_apk_fd], true)
107 } else {
108 (vec![idsig_manifest_apk_fd], false)
109 };
110 let config_path = get_vm_config_path(has_system_ext, parameters.prefer_staged);
111
112 let debug_level = if parameters.debug_mode { DebugLevel::FULL } else { DebugLevel::NONE };
113
114 let cpu_topology = match parameters.cpu_topology {
115 VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
116 VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
117 };
118
119 let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
120 name: parameters.name.clone(),
121 apk: Some(apk_fd),
122 idsig: Some(idsig_fd),
123 instanceImage: Some(instance_fd),
124 encryptedStorageImage: None,
125 payload: Payload::ConfigPath(config_path),
126 debugLevel: debug_level,
127 extraIdsigs: extra_idsigs,
128 protectedVm: protected_vm,
129 memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
130 cpuTopology: cpu_topology,
131 taskProfiles: parameters.task_profiles.clone(),
132 gdbPort: 0, // Don't start gdb-server
133 });
134
135 // Let logs go to logcat.
136 let (console_fd, log_fd) = (None, None);
137 let callback = Box::new(Callback {});
138 let instance = VmInstance::create(service, &config, console_fd, log_fd, Some(callback))
139 .context("Failed to create VM")?;
140
141 instance.start()?;
142
143 let ready = instance.wait_until_ready(TIMEOUTS.vm_max_time_to_ready);
144 if ready == Err(VmWaitError::Finished) && debug_level != DebugLevel::NONE {
145 // The payload has (unexpectedly) finished, but the VM is still running. Give it
146 // some time to shutdown to maximize our chances of getting useful logs.
147 if let Some(death_reason) =
148 instance.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit)
149 {
150 bail!("VM died during startup - reason {:?}", death_reason);
151 }
152 }
153 ready?;
154
155 Ok(Self(instance))
156 }
157
158 /// Create and return an RPC Binder connection to the Comp OS service in the VM.
connect_service(&self) -> Result<Strong<dyn ICompOsService>>159 pub fn connect_service(&self) -> Result<Strong<dyn ICompOsService>> {
160 self.0.connect_service(COMPOS_VSOCK_PORT).context("Connecting to CompOS service")
161 }
162
163 /// Shut down the VM cleanly, by sending a quit request to the service, giving time for any
164 /// relevant logs to be written.
shutdown(self, service: Strong<dyn ICompOsService>)165 pub fn shutdown(self, service: Strong<dyn ICompOsService>) {
166 info!("Requesting CompOS VM to shutdown");
167 let _ = service.quit(); // If this fails, the VM is probably dying anyway
168 self.wait_for_shutdown();
169 }
170
171 /// Wait for the instance to shut down. If it fails to shutdown within a reasonable time the
172 /// instance is dropped, which forcibly terminates it.
173 /// This should only be called when the instance has been requested to quit, or we believe that
174 /// it is already in the process of exiting due to some failure.
wait_for_shutdown(self)175 fn wait_for_shutdown(self) {
176 let death_reason = self.0.wait_for_death_with_timeout(TIMEOUTS.vm_max_time_to_exit);
177 match death_reason {
178 Some(DeathReason::Shutdown) => info!("VM has exited normally"),
179 Some(reason) => warn!("VM died with reason {:?}", reason),
180 None => warn!("VM failed to exit, dropping"),
181 }
182 }
183 }
184
locate_config_apk(apex_dir: &Path) -> Result<PathBuf>185 fn locate_config_apk(apex_dir: &Path) -> Result<PathBuf> {
186 // Our config APK will be in a directory under app, but the name of the directory is at the
187 // discretion of the build system. So just look in each sub-directory until we find it.
188 // (In practice there will be exactly one directory, so this shouldn't take long.)
189 let app_glob = apex_dir.join("app").join("**").join("CompOSPayloadApp*.apk");
190 let mut entries: Vec<PathBuf> =
191 glob(app_glob.to_str().ok_or_else(|| anyhow!("Invalid path: {}", app_glob.display()))?)
192 .context("failed to glob")?
193 .filter_map(|e| e.ok())
194 .collect();
195 if entries.len() > 1 {
196 bail!("Found more than one apk matching {}", app_glob.display());
197 }
198 match entries.pop() {
199 Some(path) => Ok(path),
200 None => Err(anyhow!("No apks match {}", app_glob.display())),
201 }
202 }
203
prepare_idsig( service: &dyn IVirtualizationService, apk_fd: &ParcelFileDescriptor, idsig_path: &Path, ) -> Result<ParcelFileDescriptor>204 fn prepare_idsig(
205 service: &dyn IVirtualizationService,
206 apk_fd: &ParcelFileDescriptor,
207 idsig_path: &Path,
208 ) -> Result<ParcelFileDescriptor> {
209 if !idsig_path.exists() {
210 // Prepare idsig file via VirtualizationService
211 let idsig_file = File::create(idsig_path).context("Failed to create idsig file")?;
212 let idsig_fd = ParcelFileDescriptor::new(idsig_file);
213 service
214 .createOrUpdateIdsigFile(apk_fd, &idsig_fd)
215 .context("Failed to update idsig file")?;
216 }
217
218 // Open idsig as read-only
219 let idsig_file = File::open(idsig_path).context("Failed to open idsig file")?;
220 let idsig_fd = ParcelFileDescriptor::new(idsig_file);
221 Ok(idsig_fd)
222 }
223
want_protected_vm() -> Result<bool>224 fn want_protected_vm() -> Result<bool> {
225 let have_protected_vm =
226 system_properties::read_bool("ro.boot.hypervisor.protected_vm.supported", false)?;
227 if have_protected_vm {
228 info!("Starting protected VM");
229 return Ok(true);
230 }
231
232 let is_debug_build = system_properties::read("ro.debuggable")?.as_deref().unwrap_or("0") == "1";
233 if !is_debug_build {
234 bail!("Protected VM not supported, unable to start VM");
235 }
236
237 let have_non_protected_vm =
238 system_properties::read_bool("ro.boot.hypervisor.vm.supported", false)?;
239 if have_non_protected_vm {
240 warn!("Protected VM not supported, falling back to non-protected on debuggable build");
241 return Ok(false);
242 }
243
244 bail!("No VM support available")
245 }
246
247 struct Callback {}
248 impl vmclient::VmCallback for Callback {
on_payload_started(&self, cid: i32)249 fn on_payload_started(&self, cid: i32) {
250 log::info!("VM payload started, cid = {}", cid);
251 }
252
on_payload_ready(&self, cid: i32)253 fn on_payload_ready(&self, cid: i32) {
254 log::info!("VM payload ready, cid = {}", cid);
255 }
256
on_payload_finished(&self, cid: i32, exit_code: i32)257 fn on_payload_finished(&self, cid: i32, exit_code: i32) {
258 log::warn!("VM payload finished, cid = {}, exit code = {}", cid, exit_code);
259 }
260
on_error(&self, cid: i32, error_code: ErrorCode, message: &str)261 fn on_error(&self, cid: i32, error_code: ErrorCode, message: &str) {
262 log::warn!("VM error, cid = {}, error code = {:?}, message = {}", cid, error_code, message);
263 }
264
on_died(&self, cid: i32, death_reason: DeathReason)265 fn on_died(&self, cid: i32, death_reason: DeathReason) {
266 log::warn!("VM died, cid = {}, reason = {:?}", cid, death_reason);
267 }
268 }
269