Jiyong Park | 551798f | 2023-05-09 23:34:57 +0900 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2023 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 | #include <aidl/android/system/virtualizationcommon/DeathReason.h> |
| 17 | #include <aidl/android/system/virtualizationcommon/ErrorCode.h> |
| 18 | #include <aidl/android/system/virtualizationservice/BnVirtualMachineCallback.h> |
| 19 | #include <aidl/android/system/virtualizationservice/IVirtualMachine.h> |
| 20 | #include <aidl/android/system/virtualizationservice/IVirtualMachineCallback.h> |
| 21 | #include <aidl/android/system/virtualizationservice/IVirtualizationService.h> |
| 22 | #include <aidl/android/system/virtualizationservice/VirtualMachineConfig.h> |
| 23 | #include <aidl/android/system/virtualizationservice/VirtualMachineState.h> |
| 24 | #include <aidl/com/android/microdroid/testservice/ITestService.h> |
| 25 | #include <android-base/errors.h> |
| 26 | #include <android-base/file.h> |
| 27 | #include <android-base/result.h> |
| 28 | #include <android-base/unique_fd.h> |
| 29 | #include <stdio.h> |
| 30 | #include <unistd.h> |
| 31 | |
| 32 | #include <binder_rpc_unstable.hpp> |
| 33 | #include <chrono> |
| 34 | #include <condition_variable> |
| 35 | #include <cstddef> |
| 36 | #include <memory> |
| 37 | #include <mutex> |
| 38 | #include <thread> |
| 39 | |
| 40 | using namespace std::chrono_literals; |
| 41 | |
| 42 | using android::base::ErrnoError; |
| 43 | using android::base::Error; |
| 44 | using android::base::Pipe; |
| 45 | using android::base::Result; |
| 46 | using android::base::Socketpair; |
| 47 | using android::base::unique_fd; |
| 48 | |
| 49 | using ndk::ScopedAStatus; |
| 50 | using ndk::ScopedFileDescriptor; |
| 51 | using ndk::SharedRefBase; |
| 52 | using ndk::SpAIBinder; |
| 53 | |
| 54 | using aidl::android::system::virtualizationcommon::DeathReason; |
| 55 | using aidl::android::system::virtualizationcommon::ErrorCode; |
| 56 | using aidl::android::system::virtualizationservice::BnVirtualMachineCallback; |
| 57 | using aidl::android::system::virtualizationservice::IVirtualizationService; |
| 58 | using aidl::android::system::virtualizationservice::IVirtualMachine; |
| 59 | using aidl::android::system::virtualizationservice::PartitionType; |
| 60 | using aidl::android::system::virtualizationservice::toString; |
| 61 | using aidl::android::system::virtualizationservice::VirtualMachineAppConfig; |
| 62 | using aidl::android::system::virtualizationservice::VirtualMachineConfig; |
| 63 | using aidl::android::system::virtualizationservice::VirtualMachinePayloadConfig; |
| 64 | using aidl::android::system::virtualizationservice::VirtualMachineState; |
| 65 | |
| 66 | using aidl::com::android::microdroid::testservice::ITestService; |
| 67 | |
| 68 | // This program demonstrates a way to run a VM and do something in the VM using AVF in the C++ |
| 69 | // language. Instructions for building and running this demo can be found in `README.md` in this |
| 70 | // directory. |
| 71 | |
| 72 | //-------------------------------------------------------------------------------------------------- |
| 73 | // Step 1: connect to IVirtualizationService |
| 74 | //-------------------------------------------------------------------------------------------------- |
| 75 | static constexpr const char VIRTMGR_PATH[] = "/apex/com.android.virt/bin/virtmgr"; |
| 76 | static constexpr size_t VIRTMGR_THREADS = 2; |
| 77 | |
| 78 | // Start IVirtualizationService instance and get FD for the unix domain socket that is connected to |
| 79 | // the service. The returned FD should be kept open until the service is no longer needed. |
| 80 | Result<unique_fd> get_service_fd() { |
| 81 | unique_fd server_fd, client_fd; |
| 82 | if (!Socketpair(SOCK_STREAM, &server_fd, &client_fd)) { |
| 83 | return ErrnoError() << "Failed to create socketpair"; |
| 84 | } |
| 85 | |
| 86 | unique_fd wait_fd, ready_fd; |
| 87 | if (!Pipe(&wait_fd, &ready_fd, 0)) { |
| 88 | return ErrnoError() << "Failed to create pipe"; |
| 89 | } |
| 90 | |
| 91 | if (fork() == 0) { |
| 92 | client_fd.reset(); |
| 93 | wait_fd.reset(); |
| 94 | |
| 95 | auto server_fd_str = std::to_string(server_fd.get()); |
| 96 | auto ready_fd_str = std::to_string(ready_fd.get()); |
| 97 | |
| 98 | if (execl(VIRTMGR_PATH, VIRTMGR_PATH, "--rpc-server-fd", server_fd_str.c_str(), |
| 99 | "--ready-fd", ready_fd_str.c_str(), nullptr) == -1) { |
| 100 | return ErrnoError() << "Failed to execute virtmgr"; |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | server_fd.reset(); |
| 105 | ready_fd.reset(); |
| 106 | |
| 107 | char buf; |
| 108 | if (read(wait_fd.get(), &buf, sizeof(buf)) < 0) { |
| 109 | return ErrnoError() << "Failed to wait for VirtualizationService to be ready"; |
| 110 | } |
| 111 | |
| 112 | return client_fd; |
| 113 | } |
| 114 | |
| 115 | // Establish a binder communication channel over the unix domain socket and returns the remote |
| 116 | // IVirtualizationService. |
| 117 | Result<std::shared_ptr<IVirtualizationService>> connect_service(int fd) { |
| 118 | std::unique_ptr<ARpcSession, decltype(&ARpcSession_free)> session(ARpcSession_new(), |
| 119 | &ARpcSession_free); |
| 120 | ARpcSession_setFileDescriptorTransportMode(session.get(), |
| 121 | ARpcSession_FileDescriptorTransportMode::Unix); |
| 122 | ARpcSession_setMaxIncomingThreads(session.get(), VIRTMGR_THREADS); |
| 123 | ARpcSession_setMaxOutgoingConnections(session.get(), VIRTMGR_THREADS); |
| 124 | AIBinder* binder = ARpcSession_setupUnixDomainBootstrapClient(session.get(), fd); |
| 125 | if (binder == nullptr) { |
| 126 | return Error() << "Failed to connect to VirtualizationService"; |
| 127 | } |
| 128 | return IVirtualizationService::fromBinder(SpAIBinder{binder}); |
| 129 | } |
| 130 | |
| 131 | //-------------------------------------------------------------------------------------------------- |
| 132 | // Step 2: construct VirtualMachineAppConfig |
| 133 | //-------------------------------------------------------------------------------------------------- |
| 134 | |
| 135 | // Utility function for opening a file at a given path and wrap the resulting FD in |
| 136 | // ScopedFileDescriptor so that it can be passed to the service. |
| 137 | Result<ScopedFileDescriptor> open_file(const std::string& path, int flags) { |
| 138 | int fd = open(path.c_str(), flags, S_IWUSR); |
| 139 | if (fd == -1) { |
| 140 | return ErrnoError() << "Failed to open " << path; |
| 141 | } |
| 142 | return ScopedFileDescriptor(fd); |
| 143 | } |
| 144 | |
| 145 | // Create or update idsig file for the given APK file. The idsig is essentially a hashtree of the |
| 146 | // APK file's content |
| 147 | Result<ScopedFileDescriptor> create_or_update_idsig_file(IVirtualizationService& service, |
| 148 | const std::string& work_dir, |
| 149 | ScopedFileDescriptor& main_apk) { |
| 150 | std::string path = work_dir + "/apk.idsig"; |
| 151 | ScopedFileDescriptor idsig = OR_RETURN(open_file(path, O_CREAT | O_RDWR)); |
| 152 | ScopedAStatus ret = service.createOrUpdateIdsigFile(main_apk, idsig); |
| 153 | if (!ret.isOk()) { |
| 154 | return Error() << "Failed to create or update idsig file: " << path; |
| 155 | } |
| 156 | return idsig; |
| 157 | } |
| 158 | |
| 159 | // Get or create the instance disk image file, if it doesn't exist. The VM will fill this disk with |
| 160 | // its own identity information in an encrypted form. |
| 161 | Result<ScopedFileDescriptor> create_instance_image_file_if_needed(IVirtualizationService& service, |
| 162 | const std::string& work_dir) { |
| 163 | std::string path = work_dir + "/instance.img"; |
| 164 | |
| 165 | // If instance.img already exists, use it. |
| 166 | if (access(path.c_str(), F_OK) == 0) { |
| 167 | return open_file(path, O_RDWR); |
| 168 | } |
| 169 | |
| 170 | // If not, create a new one. |
| 171 | ScopedFileDescriptor instance = OR_RETURN(open_file(path, O_CREAT | O_RDWR)); |
| 172 | long size = 10 * 1024 * 1024; // 10MB, but could be smaller. |
| 173 | ScopedAStatus ret = |
| 174 | service.initializeWritablePartition(instance, size, PartitionType::ANDROID_VM_INSTANCE); |
| 175 | if (!ret.isOk()) { |
| 176 | return Error() << "Failed to create instance disk image: " << path; |
| 177 | } |
| 178 | return instance; |
| 179 | } |
| 180 | |
| 181 | // Construct VirtualMachineAppConfig for a Microdroid-based VM named `vm_name` that executes a |
| 182 | // shared library named `paylaod_binary_name` in the apk `main_apk_path`. |
| 183 | Result<VirtualMachineAppConfig> create_vm_config( |
| 184 | IVirtualizationService& service, const std::string& work_dir, const std::string& vm_name, |
| 185 | const std::string& main_apk_path, const std::string& payload_binary_name, bool debuggable, |
| 186 | bool protected_vm, int32_t memory_mib) { |
| 187 | ScopedFileDescriptor main_apk = OR_RETURN(open_file(main_apk_path, O_RDONLY)); |
| 188 | ScopedFileDescriptor idsig = |
| 189 | OR_RETURN(create_or_update_idsig_file(service, work_dir, main_apk)); |
| 190 | ScopedFileDescriptor instance = |
| 191 | OR_RETURN(create_instance_image_file_if_needed(service, work_dir)); |
| 192 | |
| 193 | // There are two ways to specify the payload. The simpler way is by specifying the name of the |
| 194 | // payload binary as shown below. The other way (which is allowed only to system-level VMs) is |
| 195 | // by passing the path to the JSON file in the main APK which has detailed specification about |
| 196 | // what to load in Microdroid. See packages/modules/Virtualization/compos/apk/assets/*.json as |
| 197 | // examples. |
| 198 | VirtualMachinePayloadConfig payload; |
| 199 | payload.payloadBinaryName = payload_binary_name; |
| 200 | |
| 201 | VirtualMachineAppConfig app_config; |
| 202 | app_config.name = vm_name; |
| 203 | app_config.apk = std::move(main_apk); |
| 204 | app_config.idsig = std::move(idsig); |
| 205 | app_config.instanceImage = std::move(instance); |
| 206 | app_config.payload = std::move(payload); |
| 207 | if (debuggable) { |
| 208 | app_config.debugLevel = VirtualMachineAppConfig::DebugLevel::FULL; |
| 209 | } |
| 210 | app_config.protectedVm = protected_vm; |
| 211 | app_config.memoryMib = memory_mib; |
| 212 | |
| 213 | return app_config; |
| 214 | } |
| 215 | |
| 216 | //-------------------------------------------------------------------------------------------------- |
| 217 | // Step 3: create a VM and start it |
| 218 | //-------------------------------------------------------------------------------------------------- |
| 219 | |
| 220 | // Create a virtual machine with the config, but doesn't start it yet. |
| 221 | Result<std::shared_ptr<IVirtualMachine>> create_virtual_machine( |
| 222 | IVirtualizationService& service, VirtualMachineAppConfig& app_config) { |
| 223 | std::shared_ptr<IVirtualMachine> vm; |
| 224 | |
| 225 | VirtualMachineConfig config = std::move(app_config); |
Jiyong Park | e6fb167 | 2023-06-26 16:45:55 +0900 | [diff] [blame] | 226 | ScopedFileDescriptor console_out_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC)); |
| 227 | ScopedFileDescriptor console_in_fd(fcntl(fileno(stdin), F_DUPFD_CLOEXEC)); |
Jiyong Park | 551798f | 2023-05-09 23:34:57 +0900 | [diff] [blame] | 228 | ScopedFileDescriptor log_fd(fcntl(fileno(stdout), F_DUPFD_CLOEXEC)); |
| 229 | |
Jiyong Park | e6fb167 | 2023-06-26 16:45:55 +0900 | [diff] [blame] | 230 | ScopedAStatus ret = service.createVm(config, console_out_fd, console_in_fd, log_fd, &vm); |
Jiyong Park | 551798f | 2023-05-09 23:34:57 +0900 | [diff] [blame] | 231 | if (!ret.isOk()) { |
| 232 | return Error() << "Failed to create VM"; |
| 233 | } |
| 234 | return vm; |
| 235 | } |
| 236 | |
| 237 | // When a VM lifecycle changes, a corresponding method in this class is called. This also provides |
| 238 | // methods for blocking the current thread until the VM reaches a specific state. |
| 239 | class Callback : public BnVirtualMachineCallback { |
| 240 | public: |
| 241 | Callback(const std::shared_ptr<IVirtualMachine>& vm) : mVm(vm) {} |
| 242 | |
| 243 | ScopedAStatus onPayloadStarted(int32_t) { |
| 244 | std::unique_lock lock(mMutex); |
| 245 | mCv.notify_all(); |
| 246 | return ScopedAStatus::ok(); |
| 247 | } |
| 248 | |
| 249 | ScopedAStatus onPayloadReady(int32_t) { |
| 250 | std::unique_lock lock(mMutex); |
| 251 | mCv.notify_all(); |
| 252 | return ScopedAStatus::ok(); |
| 253 | } |
| 254 | |
| 255 | ScopedAStatus onPayloadFinished(int32_t, int32_t) { |
| 256 | std::unique_lock lock(mMutex); |
| 257 | mCv.notify_all(); |
| 258 | return ScopedAStatus::ok(); |
| 259 | } |
| 260 | |
| 261 | ScopedAStatus onError(int32_t, ErrorCode, const std::string&) { |
| 262 | std::unique_lock lock(mMutex); |
| 263 | mCv.notify_all(); |
| 264 | return ScopedAStatus::ok(); |
| 265 | } |
| 266 | |
| 267 | ScopedAStatus onDied(int32_t, DeathReason) { |
| 268 | std::unique_lock lock(mMutex); |
| 269 | mCv.notify_all(); |
| 270 | return ScopedAStatus::ok(); |
| 271 | } |
| 272 | |
| 273 | Result<void> wait_for_state(VirtualMachineState state) { |
| 274 | std::unique_lock lock(mMutex); |
| 275 | mCv.wait_for(lock, 5s, [this, &state] { |
| 276 | auto cur_state = get_vm_state(); |
| 277 | return cur_state.ok() && *cur_state == state; |
| 278 | }); |
| 279 | auto cur_state = get_vm_state(); |
| 280 | if (cur_state.ok()) { |
| 281 | if (*cur_state == state) { |
| 282 | return {}; |
| 283 | } else { |
| 284 | return Error() << "Timeout waiting for state becomes " << toString(state); |
| 285 | } |
| 286 | } |
| 287 | return cur_state.error(); |
| 288 | } |
| 289 | |
| 290 | private: |
| 291 | std::shared_ptr<IVirtualMachine> mVm; |
| 292 | std::condition_variable mCv; |
| 293 | std::mutex mMutex; |
| 294 | |
| 295 | Result<VirtualMachineState> get_vm_state() { |
| 296 | VirtualMachineState state; |
| 297 | ScopedAStatus ret = mVm->getState(&state); |
| 298 | if (!ret.isOk()) { |
| 299 | return Error() << "Failed to get state of virtual machine"; |
| 300 | } |
| 301 | return state; |
| 302 | } |
| 303 | }; |
| 304 | |
| 305 | // Start (i.e. boot) the virtual machine and return Callback monitoring the lifecycle event of the |
| 306 | // VM. |
| 307 | Result<std::shared_ptr<Callback>> start_virtual_machine(std::shared_ptr<IVirtualMachine> vm) { |
| 308 | std::shared_ptr<Callback> cb = SharedRefBase::make<Callback>(vm); |
| 309 | ScopedAStatus ret = vm->registerCallback(cb); |
| 310 | if (!ret.isOk()) { |
| 311 | return Error() << "Failed to register callback to virtual machine"; |
| 312 | } |
| 313 | ret = vm->start(); |
| 314 | if (!ret.isOk()) { |
| 315 | return Error() << "Failed to start virtual machine"; |
| 316 | } |
| 317 | return cb; |
| 318 | } |
| 319 | |
| 320 | //-------------------------------------------------------------------------------------------------- |
| 321 | // Step 4: connect to the payload and communicate with it over binder/vsock |
| 322 | //-------------------------------------------------------------------------------------------------- |
| 323 | |
| 324 | // Connect to the binder service running in the payload. |
| 325 | Result<std::shared_ptr<ITestService>> connect_to_vm_payload(std::shared_ptr<IVirtualMachine> vm) { |
| 326 | std::unique_ptr<ARpcSession, decltype(&ARpcSession_free)> session(ARpcSession_new(), |
| 327 | &ARpcSession_free); |
| 328 | ARpcSession_setMaxIncomingThreads(session.get(), 1); |
| 329 | |
| 330 | AIBinder* binder = ARpcSession_setupPreconnectedClient( |
| 331 | session.get(), |
| 332 | [](void* param) { |
| 333 | std::shared_ptr<IVirtualMachine> vm = |
| 334 | *static_cast<std::shared_ptr<IVirtualMachine>*>(param); |
| 335 | ScopedFileDescriptor sock_fd; |
| 336 | ScopedAStatus ret = vm->connectVsock(ITestService::PORT, &sock_fd); |
| 337 | if (!ret.isOk()) { |
| 338 | return -1; |
| 339 | } |
| 340 | return sock_fd.release(); |
| 341 | }, |
| 342 | &vm); |
| 343 | if (binder == nullptr) { |
| 344 | return Error() << "Failed to connect to vm payload"; |
| 345 | } |
| 346 | return ITestService::fromBinder(SpAIBinder{binder}); |
| 347 | } |
| 348 | |
| 349 | // Do something with the service in the VM |
| 350 | Result<void> do_something(ITestService& payload) { |
| 351 | int32_t result; |
| 352 | ScopedAStatus ret = payload.addInteger(10, 20, &result); |
| 353 | if (!ret.isOk()) { |
| 354 | return Error() << "Failed to call addInteger"; |
| 355 | } |
| 356 | std::cout << "The answer from VM is " << result << std::endl; |
| 357 | return {}; |
| 358 | } |
| 359 | |
| 360 | // This is the main routine that follows the steps in order |
| 361 | Result<void> inner_main() { |
| 362 | TemporaryDir work_dir; |
| 363 | std::string work_dir_path(work_dir.path); |
| 364 | |
| 365 | // Step 1: connect to the virtualizationservice |
| 366 | unique_fd fd = OR_RETURN(get_service_fd()); |
| 367 | std::shared_ptr<IVirtualizationService> service = OR_RETURN(connect_service(fd.get())); |
| 368 | |
| 369 | // Step 2: create vm config |
| 370 | VirtualMachineAppConfig app_config = OR_RETURN( |
| 371 | create_vm_config(*service, work_dir_path, "my_vm", |
| 372 | "/data/local/tmp/MicrodroidTestApp.apk", "MicrodroidTestNativeLib.so", |
| 373 | /* debuggable = */ true, // should be false for production VMs |
| 374 | /* protected_vm = */ true, 150)); |
| 375 | |
| 376 | // Step 3: start vm |
| 377 | std::shared_ptr<IVirtualMachine> vm = OR_RETURN(create_virtual_machine(*service, app_config)); |
| 378 | std::shared_ptr<Callback> cb = OR_RETURN(start_virtual_machine(vm)); |
| 379 | OR_RETURN(cb->wait_for_state(VirtualMachineState::READY)); |
| 380 | |
| 381 | // Step 4: do something in the vm |
| 382 | std::shared_ptr<ITestService> payload = OR_RETURN(connect_to_vm_payload(vm)); |
| 383 | OR_RETURN(do_something(*payload)); |
| 384 | |
| 385 | // Step 5: let VM quit by itself, and wait for the graceful shutdown |
| 386 | ScopedAStatus ret = payload->quit(); |
| 387 | if (!ret.isOk()) { |
| 388 | return Error() << "Failed to command quit to the VM"; |
| 389 | } |
| 390 | OR_RETURN(cb->wait_for_state(VirtualMachineState::DEAD)); |
| 391 | |
| 392 | return {}; |
| 393 | } |
| 394 | |
| 395 | int main() { |
| 396 | if (auto ret = inner_main(); !ret.ok()) { |
| 397 | std::cerr << ret.error() << std::endl; |
| 398 | return EXIT_FAILURE; |
| 399 | } |
| 400 | std::cout << "Done" << std::endl; |
| 401 | return EXIT_SUCCESS; |
| 402 | } |