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 package com.android.microdroid.test; 17 18 import static android.system.virtualmachine.VirtualMachine.STATUS_DELETED; 19 import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING; 20 import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED; 21 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST; 22 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU; 23 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL; 24 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE; 25 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM; 26 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_PROTECTED_VM; 27 28 import static com.google.common.truth.Truth.assertThat; 29 import static com.google.common.truth.Truth.assertWithMessage; 30 import static com.google.common.truth.TruthJUnit.assume; 31 32 import static org.junit.Assert.assertThrows; 33 import static org.junit.Assert.assertTrue; 34 import static org.junit.Assume.assumeTrue; 35 36 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 37 38 import android.app.Instrumentation; 39 import android.app.UiAutomation; 40 import android.content.ComponentName; 41 import android.content.Context; 42 import android.content.ContextWrapper; 43 import android.content.Intent; 44 import android.content.ServiceConnection; 45 import android.os.Build; 46 import android.os.IBinder; 47 import android.os.Parcel; 48 import android.os.ParcelFileDescriptor; 49 import android.os.ParcelFileDescriptor.AutoCloseInputStream; 50 import android.os.ParcelFileDescriptor.AutoCloseOutputStream; 51 import android.os.SystemProperties; 52 import android.system.OsConstants; 53 import android.system.virtualmachine.VirtualMachine; 54 import android.system.virtualmachine.VirtualMachineCallback; 55 import android.system.virtualmachine.VirtualMachineConfig; 56 import android.system.virtualmachine.VirtualMachineDescriptor; 57 import android.system.virtualmachine.VirtualMachineException; 58 import android.system.virtualmachine.VirtualMachineManager; 59 60 import androidx.test.platform.app.InstrumentationRegistry; 61 62 import com.android.compatibility.common.util.CddTest; 63 import com.android.compatibility.common.util.VsrTest; 64 import com.android.microdroid.test.device.MicrodroidDeviceTestBase; 65 import com.android.microdroid.test.vmshare.IVmShareTestService; 66 import com.android.microdroid.testservice.IAppCallback; 67 import com.android.microdroid.testservice.ITestService; 68 import com.android.microdroid.testservice.IVmCallback; 69 70 import com.google.common.base.Strings; 71 import com.google.common.truth.BooleanSubject; 72 73 import org.junit.After; 74 import org.junit.Before; 75 import org.junit.Rule; 76 import org.junit.Test; 77 import org.junit.function.ThrowingRunnable; 78 import org.junit.rules.Timeout; 79 import org.junit.runner.RunWith; 80 import org.junit.runners.Parameterized; 81 82 import java.io.BufferedReader; 83 import java.io.ByteArrayInputStream; 84 import java.io.File; 85 import java.io.FileInputStream; 86 import java.io.IOException; 87 import java.io.InputStream; 88 import java.io.InputStreamReader; 89 import java.io.OutputStream; 90 import java.io.OutputStreamWriter; 91 import java.io.RandomAccessFile; 92 import java.io.Writer; 93 import java.nio.file.Files; 94 import java.nio.file.Path; 95 import java.nio.file.Paths; 96 import java.time.LocalDateTime; 97 import java.time.format.DateTimeFormatter; 98 import java.util.Arrays; 99 import java.util.List; 100 import java.util.OptionalLong; 101 import java.util.UUID; 102 import java.util.concurrent.CompletableFuture; 103 import java.util.concurrent.CountDownLatch; 104 import java.util.concurrent.TimeUnit; 105 import java.util.concurrent.atomic.AtomicReference; 106 107 import co.nstant.in.cbor.CborDecoder; 108 import co.nstant.in.cbor.model.Array; 109 import co.nstant.in.cbor.model.DataItem; 110 import co.nstant.in.cbor.model.MajorType; 111 112 @RunWith(Parameterized.class) 113 public class MicrodroidTests extends MicrodroidDeviceTestBase { 114 private static final String TAG = "MicrodroidTests"; 115 116 @Rule public Timeout globalTimeout = Timeout.seconds(300); 117 118 private static final String KERNEL_VERSION = SystemProperties.get("ro.kernel.version"); 119 120 @Parameterized.Parameters(name = "protectedVm={0}") protectedVmConfigs()121 public static Object[] protectedVmConfigs() { 122 return new Object[] { false, true }; 123 } 124 125 @Parameterized.Parameter public boolean mProtectedVm; 126 127 @Before setup()128 public void setup() { 129 grantPermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION); 130 prepareTestSetup(mProtectedVm); 131 } 132 133 @After tearDown()134 public void tearDown() { 135 revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION); 136 revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 137 } 138 139 private static final long ONE_MEBI = 1024 * 1024; 140 141 private static final long MIN_MEM_ARM64 = 150 * ONE_MEBI; 142 private static final long MIN_MEM_X86_64 = 196 * ONE_MEBI; 143 private static final String EXAMPLE_STRING = "Literally any string!! :)"; 144 145 private static final String VM_SHARE_APP_PACKAGE_NAME = "com.android.microdroid.vmshare_app"; 146 createAndConnectToVmHelper(int cpuTopology)147 private void createAndConnectToVmHelper(int cpuTopology) throws Exception { 148 assumeSupportedDevice(); 149 150 VirtualMachineConfig config = 151 newVmConfigBuilder() 152 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 153 .setMemoryBytes(minMemoryRequired()) 154 .setDebugLevel(DEBUG_LEVEL_FULL) 155 .setCpuTopology(cpuTopology) 156 .build(); 157 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 158 159 TestResults testResults = 160 runVmTestService( 161 TAG, 162 vm, 163 (ts, tr) -> { 164 tr.mAddInteger = ts.addInteger(123, 456); 165 tr.mAppRunProp = ts.readProperty("debug.microdroid.app.run"); 166 tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run"); 167 tr.mApkContentsPath = ts.getApkContentsPath(); 168 tr.mEncryptedStoragePath = ts.getEncryptedStoragePath(); 169 }); 170 testResults.assertNoException(); 171 assertThat(testResults.mAddInteger).isEqualTo(123 + 456); 172 assertThat(testResults.mAppRunProp).isEqualTo("true"); 173 assertThat(testResults.mSublibRunProp).isEqualTo("true"); 174 assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk"); 175 assertThat(testResults.mEncryptedStoragePath).isEqualTo(""); 176 } 177 178 @Test 179 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) createAndConnectToVm()180 public void createAndConnectToVm() throws Exception { 181 createAndConnectToVmHelper(CPU_TOPOLOGY_ONE_CPU); 182 } 183 184 @Test 185 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) createAndConnectToVm_HostCpuTopology()186 public void createAndConnectToVm_HostCpuTopology() throws Exception { 187 createAndConnectToVmHelper(CPU_TOPOLOGY_MATCH_HOST); 188 } 189 190 @Test 191 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) createAndRunNoDebugVm()192 public void createAndRunNoDebugVm() throws Exception { 193 assumeSupportedDevice(); 194 195 // For most of our tests we use a debug VM so failures can be diagnosed. 196 // But we do need non-debug VMs to work, so run one. 197 VirtualMachineConfig config = 198 newVmConfigBuilder() 199 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 200 .setMemoryBytes(minMemoryRequired()) 201 .setDebugLevel(DEBUG_LEVEL_NONE) 202 .setVmOutputCaptured(false) 203 .build(); 204 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 205 206 TestResults testResults = 207 runVmTestService(TAG, vm, (ts, tr) -> tr.mAddInteger = ts.addInteger(37, 73)); 208 testResults.assertNoException(); 209 assertThat(testResults.mAddInteger).isEqualTo(37 + 73); 210 } 211 212 @Test 213 @CddTest( 214 requirements = { 215 "9.17/C-1-1", 216 "9.17/C-1-2", 217 "9.17/C-1-4", 218 }) createVmRequiresPermission()219 public void createVmRequiresPermission() { 220 assumeSupportedDevice(); 221 222 revokePermission(VirtualMachine.MANAGE_VIRTUAL_MACHINE_PERMISSION); 223 224 VirtualMachineConfig config = 225 newVmConfigBuilder() 226 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 227 .setMemoryBytes(minMemoryRequired()) 228 .build(); 229 230 SecurityException e = 231 assertThrows( 232 SecurityException.class, 233 () -> forceCreateNewVirtualMachine("test_vm_requires_permission", config)); 234 assertThat(e).hasMessageThat() 235 .contains("android.permission.MANAGE_VIRTUAL_MACHINE permission"); 236 } 237 238 @Test 239 @CddTest(requirements = {"9.17/C-1-1"}) autoCloseVm()240 public void autoCloseVm() throws Exception { 241 assumeSupportedDevice(); 242 243 VirtualMachineConfig config = 244 newVmConfigBuilder() 245 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 246 .setMemoryBytes(minMemoryRequired()) 247 .setDebugLevel(DEBUG_LEVEL_FULL) 248 .build(); 249 250 try (VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config)) { 251 assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED); 252 // close() implicitly called on stopped VM. 253 } 254 255 try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) { 256 vm.run(); 257 assertThat(vm.getStatus()).isEqualTo(STATUS_RUNNING); 258 // close() implicitly called on running VM. 259 } 260 261 try (VirtualMachine vm = getVirtualMachineManager().get("test_vm")) { 262 assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED); 263 getVirtualMachineManager().delete("test_vm"); 264 assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED); 265 // close() implicitly called on deleted VM. 266 } 267 } 268 269 @Test 270 @CddTest(requirements = {"9.17/C-1-1"}) autoCloseVmDescriptor()271 public void autoCloseVmDescriptor() throws Exception { 272 VirtualMachineConfig config = 273 newVmConfigBuilder() 274 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 275 .setDebugLevel(DEBUG_LEVEL_FULL) 276 .build(); 277 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 278 VirtualMachineDescriptor descriptor = vm.toDescriptor(); 279 280 Parcel parcel = Parcel.obtain(); 281 try (descriptor) { 282 // It should be ok to use at this point 283 descriptor.writeToParcel(parcel, 0); 284 } 285 286 // But not now - it's been closed. 287 assertThrows(IllegalStateException.class, () -> descriptor.writeToParcel(parcel, 0)); 288 assertThrows( 289 IllegalStateException.class, 290 () -> getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor)); 291 292 // Closing again is fine. 293 descriptor.close(); 294 295 // Tidy up 296 parcel.recycle(); 297 } 298 299 @Test 300 @CddTest(requirements = {"9.17/C-1-1"}) vmDescriptorClosedOnImport()301 public void vmDescriptorClosedOnImport() throws Exception { 302 VirtualMachineConfig config = 303 newVmConfigBuilder() 304 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 305 .setDebugLevel(DEBUG_LEVEL_FULL) 306 .build(); 307 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 308 VirtualMachineDescriptor descriptor = vm.toDescriptor(); 309 310 getVirtualMachineManager().importFromDescriptor("imported_vm", descriptor); 311 try { 312 // Descriptor has been implicitly closed 313 assertThrows( 314 IllegalStateException.class, 315 () -> 316 getVirtualMachineManager() 317 .importFromDescriptor("imported_vm2", descriptor)); 318 } finally { 319 getVirtualMachineManager().delete("imported_vm"); 320 } 321 } 322 323 @Test 324 @CddTest(requirements = {"9.17/C-1-1"}) vmLifecycleChecks()325 public void vmLifecycleChecks() throws Exception { 326 assumeSupportedDevice(); 327 328 VirtualMachineConfig config = 329 newVmConfigBuilder() 330 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 331 .setMemoryBytes(minMemoryRequired()) 332 .setDebugLevel(DEBUG_LEVEL_FULL) 333 .build(); 334 335 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 336 assertThat(vm.getStatus()).isEqualTo(STATUS_STOPPED); 337 338 // These methods require a running VM 339 assertThrowsVmExceptionContaining( 340 () -> vm.connectVsock(VirtualMachine.MIN_VSOCK_PORT), "not in running state"); 341 assertThrowsVmExceptionContaining( 342 () -> vm.connectToVsockServer(VirtualMachine.MIN_VSOCK_PORT), 343 "not in running state"); 344 345 vm.run(); 346 assertThat(vm.getStatus()).isEqualTo(STATUS_RUNNING); 347 348 // These methods require a stopped VM 349 assertThrowsVmExceptionContaining(() -> vm.run(), "not in stopped state"); 350 assertThrowsVmExceptionContaining(() -> vm.setConfig(config), "not in stopped state"); 351 assertThrowsVmExceptionContaining(() -> vm.toDescriptor(), "not in stopped state"); 352 assertThrowsVmExceptionContaining( 353 () -> getVirtualMachineManager().delete("test_vm"), "not in stopped state"); 354 355 vm.stop(); 356 getVirtualMachineManager().delete("test_vm"); 357 assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED); 358 359 // None of these should work for a deleted VM 360 assertThrowsVmExceptionContaining( 361 () -> vm.connectVsock(VirtualMachine.MIN_VSOCK_PORT), "deleted"); 362 assertThrowsVmExceptionContaining( 363 () -> vm.connectToVsockServer(VirtualMachine.MIN_VSOCK_PORT), "deleted"); 364 assertThrowsVmExceptionContaining(() -> vm.run(), "deleted"); 365 assertThrowsVmExceptionContaining(() -> vm.setConfig(config), "deleted"); 366 assertThrowsVmExceptionContaining(() -> vm.toDescriptor(), "deleted"); 367 // This is indistinguishable from the VM having never existed, so the message 368 // is non-specific. 369 assertThrowsVmException(() -> getVirtualMachineManager().delete("test_vm")); 370 } 371 372 @Test 373 @CddTest(requirements = {"9.17/C-1-1"}) connectVsock()374 public void connectVsock() throws Exception { 375 assumeSupportedDevice(); 376 377 VirtualMachineConfig config = 378 newVmConfigBuilder() 379 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 380 .setMemoryBytes(minMemoryRequired()) 381 .setDebugLevel(DEBUG_LEVEL_FULL) 382 .build(); 383 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_vsock", config); 384 385 AtomicReference<String> response = new AtomicReference<>(); 386 String request = "Look not into the abyss"; 387 388 TestResults testResults = 389 runVmTestService( 390 TAG, 391 vm, 392 (service, results) -> { 393 service.runEchoReverseServer(); 394 395 ParcelFileDescriptor pfd = 396 vm.connectVsock(ITestService.ECHO_REVERSE_PORT); 397 try (InputStream input = new AutoCloseInputStream(pfd); 398 OutputStream output = new AutoCloseOutputStream(pfd)) { 399 BufferedReader reader = 400 new BufferedReader(new InputStreamReader(input)); 401 Writer writer = new OutputStreamWriter(output); 402 writer.write(request + "\n"); 403 writer.flush(); 404 response.set(reader.readLine()); 405 } 406 }); 407 testResults.assertNoException(); 408 assertThat(response.get()).isEqualTo(new StringBuilder(request).reverse().toString()); 409 } 410 411 @Test 412 @CddTest(requirements = {"9.17/C-1-1"}) binderCallbacksWork()413 public void binderCallbacksWork() throws Exception { 414 assumeSupportedDevice(); 415 416 VirtualMachineConfig config = 417 newVmConfigBuilder() 418 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 419 .setMemoryBytes(minMemoryRequired()) 420 .setDebugLevel(DEBUG_LEVEL_FULL) 421 .build(); 422 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 423 424 String request = "Hello"; 425 CompletableFuture<String> response = new CompletableFuture<>(); 426 427 IAppCallback appCallback = 428 new IAppCallback.Stub() { 429 @Override 430 public void setVmCallback(IVmCallback vmCallback) { 431 // Do this on a separate thread to simulate an asynchronous trigger, 432 // and to make sure it doesn't happen in the context of an inbound binder 433 // call. 434 new Thread() { 435 @Override 436 public void run() { 437 try { 438 vmCallback.echoMessage(request); 439 } catch (Exception e) { 440 response.completeExceptionally(e); 441 } 442 } 443 }.start(); 444 } 445 446 @Override 447 public void onEchoRequestReceived(String message) { 448 response.complete(message); 449 } 450 }; 451 452 TestResults testResults = 453 runVmTestService( 454 TAG, 455 vm, 456 (service, results) -> { 457 service.requestCallback(appCallback); 458 response.get(10, TimeUnit.SECONDS); 459 }); 460 testResults.assertNoException(); 461 assertThat(response.getNow("no response")).isEqualTo("Received: " + request); 462 } 463 464 @Test 465 @CddTest(requirements = {"9.17/C-1-1"}) vmConfigGetAndSetTests()466 public void vmConfigGetAndSetTests() { 467 // Minimal has as little as specified as possible; everything that can be is defaulted. 468 VirtualMachineConfig.Builder minimalBuilder = newVmConfigBuilder(); 469 VirtualMachineConfig minimal = minimalBuilder.setPayloadBinaryName("binary.so").build(); 470 471 assertThat(minimal.getApkPath()).isNull(); 472 assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE); 473 assertThat(minimal.getMemoryBytes()).isEqualTo(0); 474 assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU); 475 assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so"); 476 assertThat(minimal.getPayloadConfigPath()).isNull(); 477 assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm()); 478 assertThat(minimal.isEncryptedStorageEnabled()).isFalse(); 479 assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0); 480 assertThat(minimal.isVmOutputCaptured()).isEqualTo(false); 481 482 // Maximal has everything that can be set to some non-default value. (And has different 483 // values than minimal for the required fields.) 484 VirtualMachineConfig.Builder maximalBuilder = 485 new VirtualMachineConfig.Builder(getContext()) 486 .setProtectedVm(mProtectedVm) 487 .setPayloadConfigPath("config/path") 488 .setApkPath("/apk/path") 489 .setDebugLevel(DEBUG_LEVEL_FULL) 490 .setMemoryBytes(42) 491 .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST) 492 .setEncryptedStorageBytes(1_000_000) 493 .setVmOutputCaptured(true); 494 VirtualMachineConfig maximal = maximalBuilder.build(); 495 496 assertThat(maximal.getApkPath()).isEqualTo("/apk/path"); 497 assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL); 498 assertThat(maximal.getMemoryBytes()).isEqualTo(42); 499 assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST); 500 assertThat(maximal.getPayloadBinaryName()).isNull(); 501 assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path"); 502 assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm()); 503 assertThat(maximal.isEncryptedStorageEnabled()).isTrue(); 504 assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000); 505 assertThat(maximal.isVmOutputCaptured()).isEqualTo(true); 506 507 assertThat(minimal.isCompatibleWith(maximal)).isFalse(); 508 assertThat(minimal.isCompatibleWith(minimal)).isTrue(); 509 assertThat(maximal.isCompatibleWith(maximal)).isTrue(); 510 } 511 512 @Test 513 @CddTest(requirements = {"9.17/C-1-1"}) vmConfigBuilderValidationTests()514 public void vmConfigBuilderValidationTests() { 515 VirtualMachineConfig.Builder builder = newVmConfigBuilder(); 516 517 // All your null are belong to me. 518 assertThrows(NullPointerException.class, () -> new VirtualMachineConfig.Builder(null)); 519 assertThrows(NullPointerException.class, () -> builder.setApkPath(null)); 520 assertThrows(NullPointerException.class, () -> builder.setPayloadConfigPath(null)); 521 assertThrows(NullPointerException.class, () -> builder.setPayloadBinaryName(null)); 522 assertThrows(NullPointerException.class, () -> builder.setPayloadConfigPath(null)); 523 524 // Individual property checks. 525 assertThrows( 526 IllegalArgumentException.class, () -> builder.setApkPath("relative/path/to.apk")); 527 assertThrows( 528 IllegalArgumentException.class, () -> builder.setPayloadBinaryName("dir/file.so")); 529 assertThrows(IllegalArgumentException.class, () -> builder.setDebugLevel(-1)); 530 assertThrows(IllegalArgumentException.class, () -> builder.setMemoryBytes(0)); 531 assertThrows(IllegalArgumentException.class, () -> builder.setCpuTopology(-1)); 532 assertThrows(IllegalArgumentException.class, () -> builder.setEncryptedStorageBytes(0)); 533 534 // Consistency checks enforced at build time. 535 Exception e; 536 e = assertThrows(IllegalStateException.class, () -> builder.build()); 537 assertThat(e).hasMessageThat().contains("setPayloadBinaryName must be called"); 538 539 VirtualMachineConfig.Builder protectedNotSet = 540 new VirtualMachineConfig.Builder(getContext()).setPayloadBinaryName("binary.so"); 541 e = assertThrows(IllegalStateException.class, () -> protectedNotSet.build()); 542 assertThat(e).hasMessageThat().contains("setProtectedVm must be called"); 543 544 VirtualMachineConfig.Builder captureOutputOnNonDebuggable = 545 newVmConfigBuilder() 546 .setPayloadBinaryName("binary.so") 547 .setDebugLevel(VirtualMachineConfig.DEBUG_LEVEL_NONE) 548 .setVmOutputCaptured(true); 549 e = assertThrows(IllegalStateException.class, () -> captureOutputOnNonDebuggable.build()); 550 assertThat(e).hasMessageThat().contains("debug level must be FULL to capture output"); 551 } 552 553 @Test 554 @CddTest(requirements = {"9.17/C-1-1"}) compatibleConfigTests()555 public void compatibleConfigTests() { 556 VirtualMachineConfig baseline = newBaselineBuilder().build(); 557 558 // A config must be compatible with itself 559 assertConfigCompatible(baseline, newBaselineBuilder()).isTrue(); 560 561 // Changes that must always be compatible 562 assertConfigCompatible(baseline, newBaselineBuilder().setMemoryBytes(99)).isTrue(); 563 assertConfigCompatible( 564 baseline, newBaselineBuilder().setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)) 565 .isTrue(); 566 567 // Changes that must be incompatible, since they must change the VM identity. 568 assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL)) 569 .isFalse(); 570 assertConfigCompatible(baseline, newBaselineBuilder().setPayloadBinaryName("different")) 571 .isFalse(); 572 int capabilities = getVirtualMachineManager().getCapabilities(); 573 if ((capabilities & CAPABILITY_PROTECTED_VM) != 0 574 && (capabilities & CAPABILITY_NON_PROTECTED_VM) != 0) { 575 assertConfigCompatible(baseline, newBaselineBuilder().setProtectedVm(!isProtectedVm())) 576 .isFalse(); 577 } 578 579 // Changes that are currently incompatible for ease of implementation, but this might change 580 // in the future. 581 assertConfigCompatible(baseline, newBaselineBuilder().setApkPath("/different")).isFalse(); 582 assertConfigCompatible(baseline, newBaselineBuilder().setEncryptedStorageBytes(100_000)) 583 .isFalse(); 584 585 VirtualMachineConfig.Builder debuggableBuilder = 586 newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL); 587 VirtualMachineConfig debuggable = debuggableBuilder.build(); 588 assertConfigCompatible(debuggable, debuggableBuilder.setVmOutputCaptured(true)).isFalse(); 589 590 VirtualMachineConfig currentContextConfig = 591 new VirtualMachineConfig.Builder(getContext()) 592 .setProtectedVm(isProtectedVm()) 593 .setPayloadBinaryName("binary.so") 594 .build(); 595 596 // packageName is not directly exposed by the config, so we have to be a bit creative 597 // to modify it. 598 Context otherContext = 599 new ContextWrapper(getContext()) { 600 @Override 601 public String getPackageName() { 602 return "other.package.name"; 603 } 604 }; 605 VirtualMachineConfig.Builder otherContextBuilder = 606 new VirtualMachineConfig.Builder(otherContext) 607 .setProtectedVm(isProtectedVm()) 608 .setPayloadBinaryName("binary.so"); 609 assertConfigCompatible(currentContextConfig, otherContextBuilder).isFalse(); 610 } 611 newBaselineBuilder()612 private VirtualMachineConfig.Builder newBaselineBuilder() { 613 return newVmConfigBuilder().setPayloadBinaryName("binary.so").setApkPath("/apk/path"); 614 } 615 assertConfigCompatible( VirtualMachineConfig baseline, VirtualMachineConfig.Builder builder)616 private BooleanSubject assertConfigCompatible( 617 VirtualMachineConfig baseline, VirtualMachineConfig.Builder builder) { 618 return assertThat(builder.build().isCompatibleWith(baseline)); 619 } 620 621 @Test 622 @CddTest(requirements = {"9.17/C-1-1"}) vmUnitTests()623 public void vmUnitTests() throws Exception { 624 VirtualMachineConfig.Builder builder = 625 newVmConfigBuilder().setPayloadBinaryName("binary.so"); 626 VirtualMachineConfig config = builder.build(); 627 VirtualMachine vm = forceCreateNewVirtualMachine("vm_name", config); 628 629 assertThat(vm.getName()).isEqualTo("vm_name"); 630 assertThat(vm.getConfig().getPayloadBinaryName()).isEqualTo("binary.so"); 631 assertThat(vm.getConfig().getMemoryBytes()).isEqualTo(0); 632 633 VirtualMachineConfig compatibleConfig = builder.setMemoryBytes(42).build(); 634 vm.setConfig(compatibleConfig); 635 636 assertThat(vm.getName()).isEqualTo("vm_name"); 637 assertThat(vm.getConfig().getPayloadBinaryName()).isEqualTo("binary.so"); 638 assertThat(vm.getConfig().getMemoryBytes()).isEqualTo(42); 639 640 assertThat(getVirtualMachineManager().get("vm_name")).isSameInstanceAs(vm); 641 } 642 643 @Test 644 @CddTest(requirements = {"9.17/C-1-1"}) testAvfRequiresUpdatableApex()645 public void testAvfRequiresUpdatableApex() throws Exception { 646 assertWithMessage("Devices that support AVF must also support updatable APEX") 647 .that(SystemProperties.getBoolean("ro.apex.updatable", false)) 648 .isTrue(); 649 } 650 651 @Test 652 @CddTest(requirements = {"9.17/C-1-1"}) vmmGetAndCreate()653 public void vmmGetAndCreate() throws Exception { 654 assumeSupportedDevice(); 655 656 VirtualMachineConfig config = 657 newVmConfigBuilder() 658 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 659 .setMemoryBytes(minMemoryRequired()) 660 .setDebugLevel(DEBUG_LEVEL_FULL) 661 .build(); 662 663 VirtualMachineManager vmm = getVirtualMachineManager(); 664 String vmName = "vmName"; 665 666 try { 667 // VM does not yet exist 668 assertThat(vmm.get(vmName)).isNull(); 669 670 VirtualMachine vm1 = vmm.create(vmName, config); 671 672 // Now it does, and we should get the same instance back 673 assertThat(vmm.get(vmName)).isSameInstanceAs(vm1); 674 assertThat(vmm.getOrCreate(vmName, config)).isSameInstanceAs(vm1); 675 676 // Can't recreate it though 677 assertThrowsVmException(() -> vmm.create(vmName, config)); 678 679 vmm.delete(vmName); 680 assertThat(vmm.get(vmName)).isNull(); 681 682 // Now that we deleted the old one, this should create rather than get, and it should be 683 // a new instance. 684 VirtualMachine vm2 = vmm.getOrCreate(vmName, config); 685 assertThat(vm2).isNotSameInstanceAs(vm1); 686 687 // The old one must remain deleted, or we'd have two VirtualMachine instances referring 688 // to the same VM. 689 assertThat(vm1.getStatus()).isEqualTo(STATUS_DELETED); 690 691 // Subsequent gets should return this new one. 692 assertThat(vmm.get(vmName)).isSameInstanceAs(vm2); 693 assertThat(vmm.getOrCreate(vmName, config)).isSameInstanceAs(vm2); 694 } finally { 695 vmm.delete(vmName); 696 } 697 } 698 699 @Test 700 @CddTest(requirements = {"9.17/C-1-1"}) vmFilesStoredInDeDirWhenCreatedFromDEContext()701 public void vmFilesStoredInDeDirWhenCreatedFromDEContext() throws Exception { 702 final Context ctx = getContext().createDeviceProtectedStorageContext(); 703 final int userId = ctx.getUserId(); 704 final VirtualMachineManager vmm = ctx.getSystemService(VirtualMachineManager.class); 705 VirtualMachineConfig config = 706 newVmConfigBuilder().setPayloadBinaryName("binary.so").build(); 707 try { 708 VirtualMachine vm = vmm.create("vm-name", config); 709 // TODO(b/261430346): what about non-primary user? 710 assertThat(vm.getRootDir().getAbsolutePath()) 711 .isEqualTo( 712 "/data/user_de/" + userId + "/com.android.microdroid.test/vm/vm-name"); 713 } finally { 714 vmm.delete("vm-name"); 715 } 716 } 717 718 @Test 719 @CddTest(requirements = {"9.17/C-1-1"}) vmFilesStoredInCeDirWhenCreatedFromCEContext()720 public void vmFilesStoredInCeDirWhenCreatedFromCEContext() throws Exception { 721 final Context ctx = getContext().createCredentialProtectedStorageContext(); 722 final int userId = ctx.getUserId(); 723 final VirtualMachineManager vmm = ctx.getSystemService(VirtualMachineManager.class); 724 VirtualMachineConfig config = 725 newVmConfigBuilder().setPayloadBinaryName("binary.so").build(); 726 try { 727 VirtualMachine vm = vmm.create("vm-name", config); 728 // TODO(b/261430346): what about non-primary user? 729 assertThat(vm.getRootDir().getAbsolutePath()) 730 .isEqualTo("/data/user/" + userId + "/com.android.microdroid.test/vm/vm-name"); 731 } finally { 732 vmm.delete("vm-name"); 733 } 734 } 735 736 @Test 737 @CddTest(requirements = {"9.17/C-1-1"}) differentManagersForDifferentContexts()738 public void differentManagersForDifferentContexts() throws Exception { 739 final Context ceCtx = getContext().createCredentialProtectedStorageContext(); 740 final Context deCtx = getContext().createDeviceProtectedStorageContext(); 741 assertThat(ceCtx.getSystemService(VirtualMachineManager.class)) 742 .isNotSameInstanceAs(deCtx.getSystemService(VirtualMachineManager.class)); 743 } 744 745 @Test 746 @CddTest(requirements = { 747 "9.17/C-1-1", 748 "9.17/C-1-2", 749 "9.17/C-1-4", 750 }) createVmWithConfigRequiresPermission()751 public void createVmWithConfigRequiresPermission() throws Exception { 752 assumeSupportedDevice(); 753 754 VirtualMachineConfig config = 755 newVmConfigBuilder() 756 .setPayloadConfigPath("assets/vm_config.json") 757 .setMemoryBytes(minMemoryRequired()) 758 .build(); 759 760 VirtualMachine vm = 761 forceCreateNewVirtualMachine("test_vm_config_requires_permission", config); 762 763 SecurityException e = 764 assertThrows( 765 SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {})); 766 assertThat(e).hasMessageThat() 767 .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission"); 768 } 769 770 @Test 771 @CddTest(requirements = { 772 "9.17/C-1-1", 773 }) deleteVm()774 public void deleteVm() throws Exception { 775 assumeSupportedDevice(); 776 777 VirtualMachineConfig config = 778 newVmConfigBuilder() 779 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 780 .setMemoryBytes(minMemoryRequired()) 781 .build(); 782 783 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_delete", config); 784 VirtualMachineManager vmm = getVirtualMachineManager(); 785 vmm.delete("test_vm_delete"); 786 787 // VM should no longer exist 788 assertThat(vmm.get("test_vm_delete")).isNull(); 789 790 // Can't start the VM even with an existing reference 791 assertThrowsVmException(vm::run); 792 793 // Can't delete the VM since it no longer exists 794 assertThrowsVmException(() -> vmm.delete("test_vm_delete")); 795 } 796 797 @Test 798 @CddTest( 799 requirements = { 800 "9.17/C-1-1", 801 }) deleteVmFiles()802 public void deleteVmFiles() throws Exception { 803 assumeSupportedDevice(); 804 805 VirtualMachineConfig config = 806 newVmConfigBuilder() 807 .setPayloadBinaryName("MicrodroidExitNativeLib.so") 808 .setMemoryBytes(minMemoryRequired()) 809 .build(); 810 811 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_delete", config); 812 vm.run(); 813 // If we explicitly stop a VM, that triggers some tidy up; so for this test we start a VM 814 // that immediately stops itself. 815 while (vm.getStatus() == STATUS_RUNNING) { 816 Thread.sleep(100); 817 } 818 819 // Delete the files without telling VMM. This isn't a good idea, but we can't stop an 820 // app doing it, and we should recover from it. 821 for (File f : vm.getRootDir().listFiles()) { 822 Files.delete(f.toPath()); 823 } 824 vm.getRootDir().delete(); 825 826 VirtualMachineManager vmm = getVirtualMachineManager(); 827 assertThat(vmm.get("test_vm_delete")).isNull(); 828 assertThat(vm.getStatus()).isEqualTo(STATUS_DELETED); 829 } 830 831 @Test 832 @CddTest(requirements = { 833 "9.17/C-1-1", 834 }) validApkPathIsAccepted()835 public void validApkPathIsAccepted() throws Exception { 836 assumeSupportedDevice(); 837 838 VirtualMachineConfig config = 839 newVmConfigBuilder() 840 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 841 .setApkPath(getContext().getPackageCodePath()) 842 .setMemoryBytes(minMemoryRequired()) 843 .setDebugLevel(DEBUG_LEVEL_FULL) 844 .build(); 845 846 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_explicit_apk_path", config); 847 848 TestResults testResults = 849 runVmTestService( 850 TAG, 851 vm, 852 (ts, tr) -> { 853 tr.mApkContentsPath = ts.getApkContentsPath(); 854 }); 855 testResults.assertNoException(); 856 assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk"); 857 } 858 859 @Test 860 @CddTest(requirements = {"9.17/C-1-1"}) invalidVmNameIsRejected()861 public void invalidVmNameIsRejected() { 862 VirtualMachineManager vmm = getVirtualMachineManager(); 863 assertThrows(IllegalArgumentException.class, () -> vmm.get("../foo")); 864 assertThrows(IllegalArgumentException.class, () -> vmm.get("..")); 865 } 866 867 @Test 868 @CddTest(requirements = { 869 "9.17/C-1-1", 870 "9.17/C-2-1" 871 }) extraApk()872 public void extraApk() throws Exception { 873 assumeSupportedDevice(); 874 875 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 876 VirtualMachineConfig config = 877 newVmConfigBuilder() 878 .setPayloadConfigPath("assets/vm_config_extra_apk.json") 879 .setMemoryBytes(minMemoryRequired()) 880 .setDebugLevel(DEBUG_LEVEL_FULL) 881 .build(); 882 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_extra_apk", config); 883 884 TestResults testResults = 885 runVmTestService( 886 TAG, 887 vm, 888 (ts, tr) -> { 889 tr.mExtraApkTestProp = 890 ts.readProperty("debug.microdroid.test.extra_apk"); 891 }); 892 assertThat(testResults.mExtraApkTestProp).isEqualTo("PASS"); 893 } 894 895 @Test bootFailsWhenLowMem()896 public void bootFailsWhenLowMem() throws Exception { 897 for (int memMib : new int[]{ 10, 20, 40 }) { 898 VirtualMachineConfig lowMemConfig = 899 newVmConfigBuilder() 900 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 901 .setMemoryBytes(memMib) 902 .setDebugLevel(DEBUG_LEVEL_NONE) 903 .setVmOutputCaptured(false) 904 .build(); 905 VirtualMachine vm = forceCreateNewVirtualMachine("low_mem", lowMemConfig); 906 final CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>(); 907 final CompletableFuture<Boolean> onStoppedExecuted = new CompletableFuture<>(); 908 VmEventListener listener = 909 new VmEventListener() { 910 @Override 911 public void onPayloadReady(VirtualMachine vm) { 912 onPayloadReadyExecuted.complete(true); 913 super.onPayloadReady(vm); 914 } 915 @Override 916 public void onStopped(VirtualMachine vm, int reason) { 917 onStoppedExecuted.complete(true); 918 super.onStopped(vm, reason); 919 } 920 }; 921 listener.runToFinish(TAG, vm); 922 // Assert that onStopped() was executed but onPayloadReady() was never run 923 assertThat(onStoppedExecuted.getNow(false)).isTrue(); 924 assertThat(onPayloadReadyExecuted.getNow(false)).isFalse(); 925 } 926 } 927 928 @Test 929 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"}) changingNonDebuggableVmDebuggableInvalidatesVmIdentity()930 public void changingNonDebuggableVmDebuggableInvalidatesVmIdentity() throws Exception { 931 changeDebugLevel(DEBUG_LEVEL_NONE, DEBUG_LEVEL_FULL); 932 } 933 934 @Test 935 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-7"}) changingDebuggableVmNonDebuggableInvalidatesVmIdentity()936 public void changingDebuggableVmNonDebuggableInvalidatesVmIdentity() throws Exception { 937 changeDebugLevel(DEBUG_LEVEL_FULL, DEBUG_LEVEL_NONE); 938 } 939 changeDebugLevel(int fromLevel, int toLevel)940 private void changeDebugLevel(int fromLevel, int toLevel) throws Exception { 941 assumeSupportedDevice(); 942 943 VirtualMachineConfig.Builder builder = 944 newVmConfigBuilder() 945 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 946 .setDebugLevel(fromLevel) 947 .setVmOutputCaptured(false); 948 VirtualMachineConfig normalConfig = builder.build(); 949 forceCreateNewVirtualMachine("test_vm", normalConfig); 950 assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue(); 951 952 // Try to run the VM again with the previous instance.img 953 // We need to make sure that no changes on config don't invalidate the identity, to compare 954 // the result with the below "different debug level" test. 955 File vmInstance = getVmFile("test_vm", "instance.img"); 956 File vmInstanceBackup = File.createTempFile("instance", ".img"); 957 Files.copy(vmInstance.toPath(), vmInstanceBackup.toPath(), REPLACE_EXISTING); 958 forceCreateNewVirtualMachine("test_vm", normalConfig); 959 Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING); 960 assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isTrue(); 961 962 // Launch the same VM with a different debug level. The Java API prohibits this 963 // (thankfully). 964 // For testing, we do that by creating a new VM with debug level, and copy the old instance 965 // image to the new VM instance image. 966 VirtualMachineConfig debugConfig = builder.setDebugLevel(toLevel).build(); 967 forceCreateNewVirtualMachine("test_vm", debugConfig); 968 Files.copy(vmInstanceBackup.toPath(), vmInstance.toPath(), REPLACE_EXISTING); 969 assertThat(tryBootVm(TAG, "test_vm").payloadStarted).isFalse(); 970 } 971 972 private static class VmCdis { 973 public byte[] cdiAttest; 974 public byte[] instanceSecret; 975 } 976 launchVmAndGetCdis(String instanceName)977 private VmCdis launchVmAndGetCdis(String instanceName) throws Exception { 978 VirtualMachine vm = getVirtualMachineManager().get(instanceName); 979 VmCdis vmCdis = new VmCdis(); 980 CompletableFuture<Exception> exception = new CompletableFuture<>(); 981 VmEventListener listener = 982 new VmEventListener() { 983 @Override 984 public void onPayloadReady(VirtualMachine vm) { 985 try { 986 ITestService testService = 987 ITestService.Stub.asInterface( 988 vm.connectToVsockServer(ITestService.SERVICE_PORT)); 989 vmCdis.cdiAttest = testService.insecurelyExposeAttestationCdi(); 990 vmCdis.instanceSecret = testService.insecurelyExposeVmInstanceSecret(); 991 } catch (Exception e) { 992 exception.complete(e); 993 } finally { 994 forceStop(vm); 995 } 996 } 997 }; 998 listener.runToFinish(TAG, vm); 999 Exception e = exception.getNow(null); 1000 if (e != null) { 1001 throw new RuntimeException(e); 1002 } 1003 return vmCdis; 1004 } 1005 1006 @Test 1007 @CddTest(requirements = { 1008 "9.17/C-1-1", 1009 "9.17/C-2-7" 1010 }) instancesOfSameVmHaveDifferentCdis()1011 public void instancesOfSameVmHaveDifferentCdis() throws Exception { 1012 assumeSupportedDevice(); 1013 1014 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 1015 VirtualMachineConfig normalConfig = 1016 newVmConfigBuilder() 1017 .setPayloadConfigPath("assets/vm_config.json") 1018 .setDebugLevel(DEBUG_LEVEL_FULL) 1019 .build(); 1020 forceCreateNewVirtualMachine("test_vm_a", normalConfig); 1021 forceCreateNewVirtualMachine("test_vm_b", normalConfig); 1022 VmCdis vm_a_cdis = launchVmAndGetCdis("test_vm_a"); 1023 VmCdis vm_b_cdis = launchVmAndGetCdis("test_vm_b"); 1024 assertThat(vm_a_cdis.cdiAttest).isNotNull(); 1025 assertThat(vm_b_cdis.cdiAttest).isNotNull(); 1026 assertThat(vm_a_cdis.cdiAttest).isNotEqualTo(vm_b_cdis.cdiAttest); 1027 assertThat(vm_a_cdis.instanceSecret).isNotNull(); 1028 assertThat(vm_b_cdis.instanceSecret).isNotNull(); 1029 assertThat(vm_a_cdis.instanceSecret).isNotEqualTo(vm_b_cdis.instanceSecret); 1030 } 1031 1032 @Test 1033 @CddTest(requirements = { 1034 "9.17/C-1-1", 1035 "9.17/C-2-7" 1036 }) sameInstanceKeepsSameCdis()1037 public void sameInstanceKeepsSameCdis() throws Exception { 1038 assumeSupportedDevice(); 1039 assume().withMessage("Skip on CF. Too Slow. b/257270529").that(isCuttlefish()).isFalse(); 1040 1041 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 1042 VirtualMachineConfig normalConfig = 1043 newVmConfigBuilder() 1044 .setPayloadConfigPath("assets/vm_config.json") 1045 .setDebugLevel(DEBUG_LEVEL_FULL) 1046 .build(); 1047 forceCreateNewVirtualMachine("test_vm", normalConfig); 1048 1049 VmCdis first_boot_cdis = launchVmAndGetCdis("test_vm"); 1050 VmCdis second_boot_cdis = launchVmAndGetCdis("test_vm"); 1051 // The attestation CDI isn't specified to be stable, though it might be 1052 assertThat(first_boot_cdis.instanceSecret).isNotNull(); 1053 assertThat(second_boot_cdis.instanceSecret).isNotNull(); 1054 assertThat(first_boot_cdis.instanceSecret).isEqualTo(second_boot_cdis.instanceSecret); 1055 } 1056 1057 @Test 1058 @CddTest(requirements = { 1059 "9.17/C-1-1", 1060 "9.17/C-2-7" 1061 }) bccIsSuperficiallyWellFormed()1062 public void bccIsSuperficiallyWellFormed() throws Exception { 1063 assumeSupportedDevice(); 1064 1065 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 1066 VirtualMachineConfig normalConfig = 1067 newVmConfigBuilder() 1068 .setPayloadConfigPath("assets/vm_config.json") 1069 .setDebugLevel(DEBUG_LEVEL_FULL) 1070 .build(); 1071 VirtualMachine vm = forceCreateNewVirtualMachine("bcc_vm", normalConfig); 1072 TestResults testResults = 1073 runVmTestService( 1074 TAG, 1075 vm, 1076 (service, results) -> { 1077 results.mBcc = service.getBcc(); 1078 }); 1079 testResults.assertNoException(); 1080 byte[] bccBytes = testResults.mBcc; 1081 assertThat(bccBytes).isNotNull(); 1082 1083 ByteArrayInputStream bais = new ByteArrayInputStream(bccBytes); 1084 List<DataItem> dataItems = new CborDecoder(bais).decode(); 1085 assertThat(dataItems.size()).isEqualTo(1); 1086 assertThat(dataItems.get(0).getMajorType()).isEqualTo(MajorType.ARRAY); 1087 List<DataItem> rootArrayItems = ((Array) dataItems.get(0)).getDataItems(); 1088 assertThat(rootArrayItems.size()).isAtLeast(2); // Public key and one certificate 1089 if (mProtectedVm) { 1090 // pvmfw truncates the DICE chain it gets, so we expect exactly entries for: public key, 1091 // Microdroid and app payload. 1092 assertThat(rootArrayItems.size()).isEqualTo(3); 1093 } 1094 } 1095 1096 @Test 1097 @CddTest(requirements = { 1098 "9.17/C-1-1", 1099 "9.17/C-1-2" 1100 }) accessToCdisIsRestricted()1101 public void accessToCdisIsRestricted() throws Exception { 1102 assumeSupportedDevice(); 1103 1104 VirtualMachineConfig config = 1105 newVmConfigBuilder() 1106 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1107 .setDebugLevel(DEBUG_LEVEL_FULL) 1108 .build(); 1109 forceCreateNewVirtualMachine("test_vm", config); 1110 1111 assertThrows(Exception.class, () -> launchVmAndGetCdis("test_vm")); 1112 } 1113 1114 1115 private static final UUID MICRODROID_PARTITION_UUID = 1116 UUID.fromString("cf9afe9a-0662-11ec-a329-c32663a09d75"); 1117 private static final UUID PVM_FW_PARTITION_UUID = 1118 UUID.fromString("90d2174a-038a-4bc6-adf3-824848fc5825"); 1119 private static final long BLOCK_SIZE = 512; 1120 1121 // Find the starting offset which holds the data of a partition having UUID. 1122 // This is a kind of hack; rather than parsing QCOW2 we exploit the fact that the cluster size 1123 // is normally greater than 512. It implies that the partition data should exist at a block 1124 // which follows the header block findPartitionDataOffset(RandomAccessFile file, UUID uuid)1125 private OptionalLong findPartitionDataOffset(RandomAccessFile file, UUID uuid) 1126 throws IOException { 1127 // For each 512-byte block in file, check header 1128 long fileSize = file.length(); 1129 1130 for (long idx = 0; idx + BLOCK_SIZE < fileSize; idx += BLOCK_SIZE) { 1131 file.seek(idx); 1132 long high = file.readLong(); 1133 long low = file.readLong(); 1134 if (uuid.equals(new UUID(high, low))) return OptionalLong.of(idx + BLOCK_SIZE); 1135 } 1136 return OptionalLong.empty(); 1137 } 1138 flipBit(RandomAccessFile file, long offset)1139 private void flipBit(RandomAccessFile file, long offset) throws IOException { 1140 file.seek(offset); 1141 int b = file.readByte(); 1142 file.seek(offset); 1143 file.writeByte(b ^ 1); 1144 } 1145 prepareInstanceImage(String vmName)1146 private RandomAccessFile prepareInstanceImage(String vmName) throws Exception { 1147 VirtualMachineConfig config = 1148 newVmConfigBuilder() 1149 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1150 .setDebugLevel(DEBUG_LEVEL_FULL) 1151 .build(); 1152 1153 forceCreateNewVirtualMachine(vmName, config); 1154 assertThat(tryBootVm(TAG, vmName).payloadStarted).isTrue(); 1155 File instanceImgPath = getVmFile(vmName, "instance.img"); 1156 return new RandomAccessFile(instanceImgPath, "rw"); 1157 } 1158 assertThatPartitionIsMissing(UUID partitionUuid)1159 private void assertThatPartitionIsMissing(UUID partitionUuid) throws Exception { 1160 RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity"); 1161 assertThat(findPartitionDataOffset(instanceFile, partitionUuid).isPresent()) 1162 .isFalse(); 1163 } 1164 1165 // Flips a bit of given partition, and then see if boot fails. assertThatBootFailsAfterCompromisingPartition(UUID partitionUuid)1166 private void assertThatBootFailsAfterCompromisingPartition(UUID partitionUuid) 1167 throws Exception { 1168 RandomAccessFile instanceFile = prepareInstanceImage("test_vm_integrity"); 1169 OptionalLong offset = findPartitionDataOffset(instanceFile, partitionUuid); 1170 assertThat(offset.isPresent()).isTrue(); 1171 1172 flipBit(instanceFile, offset.getAsLong()); 1173 1174 BootResult result = tryBootVm(TAG, "test_vm_integrity"); 1175 assertThat(result.payloadStarted).isFalse(); 1176 1177 // This failure should shut the VM down immediately and shouldn't trigger a hangup. 1178 assertThat(result.deathReason).isNotEqualTo(VirtualMachineCallback.STOP_REASON_HANGUP); 1179 } 1180 1181 @Test 1182 @CddTest(requirements = { 1183 "9.17/C-1-1", 1184 "9.17/C-2-7" 1185 }) bootFailsWhenMicrodroidDataIsCompromised()1186 public void bootFailsWhenMicrodroidDataIsCompromised() throws Exception { 1187 assertThatBootFailsAfterCompromisingPartition(MICRODROID_PARTITION_UUID); 1188 } 1189 1190 @Test 1191 @CddTest(requirements = { 1192 "9.17/C-1-1", 1193 "9.17/C-2-7" 1194 }) bootFailsWhenPvmFwDataIsCompromised()1195 public void bootFailsWhenPvmFwDataIsCompromised() throws Exception { 1196 if (mProtectedVm) { 1197 assertThatBootFailsAfterCompromisingPartition(PVM_FW_PARTITION_UUID); 1198 } else { 1199 // non-protected VM shouldn't have pvmfw data 1200 assertThatPartitionIsMissing(PVM_FW_PARTITION_UUID); 1201 } 1202 } 1203 1204 @Test bootFailsWhenConfigIsInvalid()1205 public void bootFailsWhenConfigIsInvalid() throws Exception { 1206 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 1207 VirtualMachineConfig normalConfig = 1208 newVmConfigBuilder() 1209 .setPayloadConfigPath("assets/vm_config_no_task.json") 1210 .setDebugLevel(DEBUG_LEVEL_FULL) 1211 .build(); 1212 forceCreateNewVirtualMachine("test_vm_invalid_config", normalConfig); 1213 1214 BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_config"); 1215 assertThat(bootResult.payloadStarted).isFalse(); 1216 assertThat(bootResult.deathReason).isEqualTo( 1217 VirtualMachineCallback.STOP_REASON_MICRODROID_INVALID_PAYLOAD_CONFIG); 1218 } 1219 1220 @Test bootFailsWhenBinaryNameIsInvalid()1221 public void bootFailsWhenBinaryNameIsInvalid() throws Exception { 1222 VirtualMachineConfig.Builder builder = 1223 newVmConfigBuilder().setPayloadBinaryName("DoesNotExist.so"); 1224 VirtualMachineConfig normalConfig = builder.setDebugLevel(DEBUG_LEVEL_FULL).build(); 1225 forceCreateNewVirtualMachine("test_vm_invalid_binary_path", normalConfig); 1226 1227 BootResult bootResult = tryBootVm(TAG, "test_vm_invalid_binary_path"); 1228 assertThat(bootResult.payloadStarted).isFalse(); 1229 assertThat(bootResult.deathReason).isEqualTo( 1230 VirtualMachineCallback.STOP_REASON_MICRODROID_UNKNOWN_RUNTIME_ERROR); 1231 } 1232 1233 // Checks whether microdroid_launcher started but payload failed. reason must be recorded in the 1234 // console output. assertThatPayloadFailsDueTo(VirtualMachine vm, String reason)1235 private void assertThatPayloadFailsDueTo(VirtualMachine vm, String reason) throws Exception { 1236 final CompletableFuture<Boolean> payloadStarted = new CompletableFuture<>(); 1237 final CompletableFuture<Integer> exitCodeFuture = new CompletableFuture<>(); 1238 VmEventListener listener = 1239 new VmEventListener() { 1240 @Override 1241 public void onPayloadStarted(VirtualMachine vm) { 1242 payloadStarted.complete(true); 1243 } 1244 1245 @Override 1246 public void onPayloadFinished(VirtualMachine vm, int exitCode) { 1247 exitCodeFuture.complete(exitCode); 1248 } 1249 }; 1250 listener.runToFinish(TAG, vm); 1251 1252 assertThat(payloadStarted.getNow(false)).isTrue(); 1253 assertThat(exitCodeFuture.getNow(0)).isNotEqualTo(0); 1254 assertThat(listener.getConsoleOutput()).contains(reason); 1255 } 1256 1257 @Test bootFailsWhenBinaryIsMissingEntryFunction()1258 public void bootFailsWhenBinaryIsMissingEntryFunction() throws Exception { 1259 VirtualMachineConfig normalConfig = 1260 newVmConfigBuilder() 1261 .setPayloadBinaryName("MicrodroidEmptyNativeLib.so") 1262 .setDebugLevel(DEBUG_LEVEL_FULL) 1263 .setVmOutputCaptured(true) 1264 .build(); 1265 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_missing_entry", normalConfig); 1266 1267 assertThatPayloadFailsDueTo(vm, "Failed to find entrypoint"); 1268 } 1269 1270 @Test bootFailsWhenBinaryTriesToLinkAgainstPrivateLibs()1271 public void bootFailsWhenBinaryTriesToLinkAgainstPrivateLibs() throws Exception { 1272 VirtualMachineConfig normalConfig = 1273 newVmConfigBuilder() 1274 .setPayloadBinaryName("MicrodroidPrivateLinkingNativeLib.so") 1275 .setDebugLevel(DEBUG_LEVEL_FULL) 1276 .setVmOutputCaptured(true) 1277 .build(); 1278 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_private_linking", normalConfig); 1279 1280 assertThatPayloadFailsDueTo(vm, "Failed to dlopen"); 1281 } 1282 1283 @Test sameInstancesShareTheSameVmObject()1284 public void sameInstancesShareTheSameVmObject() throws Exception { 1285 VirtualMachineConfig config = 1286 newVmConfigBuilder().setPayloadBinaryName("MicrodroidTestNativeLib.so").build(); 1287 1288 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 1289 VirtualMachine vm2 = getVirtualMachineManager().get("test_vm"); 1290 assertThat(vm).isEqualTo(vm2); 1291 1292 VirtualMachine newVm = forceCreateNewVirtualMachine("test_vm", config); 1293 VirtualMachine newVm2 = getVirtualMachineManager().get("test_vm"); 1294 assertThat(newVm).isEqualTo(newVm2); 1295 1296 assertThat(vm).isNotEqualTo(newVm); 1297 } 1298 1299 @Test importedVmAndOriginalVmHaveTheSameCdi()1300 public void importedVmAndOriginalVmHaveTheSameCdi() throws Exception { 1301 assumeSupportedDevice(); 1302 // Arrange 1303 grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION); 1304 VirtualMachineConfig config = 1305 newVmConfigBuilder() 1306 .setPayloadConfigPath("assets/vm_config.json") 1307 .setDebugLevel(DEBUG_LEVEL_FULL) 1308 .build(); 1309 String vmNameOrig = "test_vm_orig"; 1310 String vmNameImport = "test_vm_import"; 1311 VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config); 1312 VmCdis origCdis = launchVmAndGetCdis(vmNameOrig); 1313 assertThat(origCdis.instanceSecret).isNotNull(); 1314 VirtualMachineManager vmm = getVirtualMachineManager(); 1315 if (vmm.get(vmNameImport) != null) { 1316 vmm.delete(vmNameImport); 1317 } 1318 1319 // Action 1320 // The imported VM will be fetched by name later. 1321 vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor()); 1322 1323 // Asserts 1324 VmCdis importCdis = launchVmAndGetCdis(vmNameImport); 1325 assertThat(origCdis.instanceSecret).isEqualTo(importCdis.instanceSecret); 1326 } 1327 1328 @Test 1329 @CddTest(requirements = {"9.17/C-1-1"}) importedVmIsEqualToTheOriginalVm_WithoutStorage()1330 public void importedVmIsEqualToTheOriginalVm_WithoutStorage() throws Exception { 1331 TestResults testResults = importedVmIsEqualToTheOriginalVm(false); 1332 assertThat(testResults.mEncryptedStoragePath).isEqualTo(""); 1333 } 1334 1335 @Test 1336 @CddTest(requirements = {"9.17/C-1-1"}) importedVmIsEqualToTheOriginalVm_WithStorage()1337 public void importedVmIsEqualToTheOriginalVm_WithStorage() throws Exception { 1338 TestResults testResults = importedVmIsEqualToTheOriginalVm(true); 1339 assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore"); 1340 } 1341 importedVmIsEqualToTheOriginalVm(boolean encryptedStoreEnabled)1342 private TestResults importedVmIsEqualToTheOriginalVm(boolean encryptedStoreEnabled) 1343 throws Exception { 1344 // Arrange 1345 VirtualMachineConfig.Builder builder = 1346 newVmConfigBuilder() 1347 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1348 .setDebugLevel(DEBUG_LEVEL_FULL); 1349 if (encryptedStoreEnabled) { 1350 builder.setEncryptedStorageBytes(4_000_000); 1351 } 1352 VirtualMachineConfig config = builder.build(); 1353 String vmNameOrig = "test_vm_orig"; 1354 String vmNameImport = "test_vm_import"; 1355 VirtualMachine vmOrig = forceCreateNewVirtualMachine(vmNameOrig, config); 1356 // Run something to make the instance.img different with the initialized one. 1357 TestResults origTestResults = 1358 runVmTestService( 1359 TAG, 1360 vmOrig, 1361 (ts, tr) -> { 1362 tr.mAddInteger = ts.addInteger(123, 456); 1363 tr.mEncryptedStoragePath = ts.getEncryptedStoragePath(); 1364 }); 1365 origTestResults.assertNoException(); 1366 assertThat(origTestResults.mAddInteger).isEqualTo(123 + 456); 1367 VirtualMachineManager vmm = getVirtualMachineManager(); 1368 if (vmm.get(vmNameImport) != null) { 1369 vmm.delete(vmNameImport); 1370 } 1371 1372 // Action 1373 VirtualMachine vmImport = vmm.importFromDescriptor(vmNameImport, vmOrig.toDescriptor()); 1374 1375 // Asserts 1376 assertFileContentsAreEqualInTwoVms("config.xml", vmNameOrig, vmNameImport); 1377 assertFileContentsAreEqualInTwoVms("instance.img", vmNameOrig, vmNameImport); 1378 if (encryptedStoreEnabled) { 1379 assertFileContentsAreEqualInTwoVms("storage.img", vmNameOrig, vmNameImport); 1380 } 1381 assertThat(vmImport).isNotEqualTo(vmOrig); 1382 vmm.delete(vmNameOrig); 1383 assertThat(vmImport).isEqualTo(vmm.get(vmNameImport)); 1384 TestResults testResults = 1385 runVmTestService( 1386 TAG, 1387 vmImport, 1388 (ts, tr) -> { 1389 tr.mAddInteger = ts.addInteger(123, 456); 1390 tr.mEncryptedStoragePath = ts.getEncryptedStoragePath(); 1391 }); 1392 testResults.assertNoException(); 1393 assertThat(testResults.mAddInteger).isEqualTo(123 + 456); 1394 return testResults; 1395 } 1396 1397 @Test 1398 @CddTest(requirements = {"9.17/C-1-1"}) encryptedStorageAvailable()1399 public void encryptedStorageAvailable() throws Exception { 1400 assumeSupportedDevice(); 1401 1402 VirtualMachineConfig config = 1403 newVmConfigBuilder() 1404 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1405 .setMemoryBytes(minMemoryRequired()) 1406 .setEncryptedStorageBytes(4_000_000) 1407 .setDebugLevel(DEBUG_LEVEL_FULL) 1408 .build(); 1409 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 1410 1411 TestResults testResults = 1412 runVmTestService( 1413 TAG, 1414 vm, 1415 (ts, tr) -> { 1416 tr.mEncryptedStoragePath = ts.getEncryptedStoragePath(); 1417 }); 1418 assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore"); 1419 } 1420 1421 @Test 1422 @CddTest(requirements = {"9.17/C-1-1"}) encryptedStorageIsInaccessibleToDifferentVm()1423 public void encryptedStorageIsInaccessibleToDifferentVm() throws Exception { 1424 assumeSupportedDevice(); 1425 1426 VirtualMachineConfig config = 1427 newVmConfigBuilder() 1428 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1429 .setMemoryBytes(minMemoryRequired()) 1430 .setEncryptedStorageBytes(4_000_000) 1431 .setDebugLevel(DEBUG_LEVEL_FULL) 1432 .build(); 1433 1434 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm", config); 1435 1436 TestResults testResults = 1437 runVmTestService( 1438 TAG, 1439 vm, 1440 (ts, tr) -> { 1441 ts.writeToFile( 1442 /* content= */ EXAMPLE_STRING, 1443 /* path= */ "/mnt/encryptedstore/test_file"); 1444 }); 1445 testResults.assertNoException(); 1446 1447 // Start a different vm (this changes the vm identity) 1448 VirtualMachine diff_test_vm = forceCreateNewVirtualMachine("diff_test_vm", config); 1449 1450 // Replace the backing storage image to the original one 1451 File storageImgOrig = getVmFile("test_vm", "storage.img"); 1452 File storageImgNew = getVmFile("diff_test_vm", "storage.img"); 1453 Files.copy(storageImgOrig.toPath(), storageImgNew.toPath(), REPLACE_EXISTING); 1454 assertFileContentsAreEqualInTwoVms("storage.img", "test_vm", "diff_test_vm"); 1455 1456 CompletableFuture<Boolean> onPayloadReadyExecuted = new CompletableFuture<>(); 1457 CompletableFuture<Boolean> onErrorExecuted = new CompletableFuture<>(); 1458 CompletableFuture<String> errorMessage = new CompletableFuture<>(); 1459 VmEventListener listener = 1460 new VmEventListener() { 1461 @Override 1462 public void onPayloadReady(VirtualMachine vm) { 1463 onPayloadReadyExecuted.complete(true); 1464 super.onPayloadReady(vm); 1465 } 1466 1467 @Override 1468 public void onError(VirtualMachine vm, int errorCode, String message) { 1469 onErrorExecuted.complete(true); 1470 errorMessage.complete(message); 1471 super.onError(vm, errorCode, message); 1472 } 1473 }; 1474 listener.runToFinish(TAG, diff_test_vm); 1475 1476 // Assert that payload never started & error message reflects storage error. 1477 assertThat(onPayloadReadyExecuted.getNow(false)).isFalse(); 1478 assertThat(onErrorExecuted.getNow(false)).isTrue(); 1479 assertThat(errorMessage.getNow("")).contains("Unable to prepare encrypted storage"); 1480 } 1481 1482 @Test 1483 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) microdroidLauncherHasEmptyCapabilities()1484 public void microdroidLauncherHasEmptyCapabilities() throws Exception { 1485 assumeSupportedDevice(); 1486 1487 final VirtualMachineConfig vmConfig = 1488 newVmConfigBuilder() 1489 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1490 .setMemoryBytes(minMemoryRequired()) 1491 .setDebugLevel(DEBUG_LEVEL_FULL) 1492 .build(); 1493 final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_caps", vmConfig); 1494 1495 final TestResults testResults = 1496 runVmTestService( 1497 TAG, 1498 vm, 1499 (ts, tr) -> { 1500 tr.mEffectiveCapabilities = ts.getEffectiveCapabilities(); 1501 }); 1502 1503 testResults.assertNoException(); 1504 assertThat(testResults.mEffectiveCapabilities).isEmpty(); 1505 } 1506 1507 @Test 1508 @CddTest(requirements = {"9.17/C-1-1"}) encryptedStorageIsPersistent()1509 public void encryptedStorageIsPersistent() throws Exception { 1510 assumeSupportedDevice(); 1511 1512 VirtualMachineConfig config = 1513 newVmConfigBuilder() 1514 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1515 .setMemoryBytes(minMemoryRequired()) 1516 .setEncryptedStorageBytes(4_000_000) 1517 .setDebugLevel(DEBUG_LEVEL_FULL) 1518 .build(); 1519 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_a", config); 1520 TestResults testResults = 1521 runVmTestService( 1522 TAG, 1523 vm, 1524 (ts, tr) -> { 1525 ts.writeToFile( 1526 /* content= */ EXAMPLE_STRING, 1527 /* path= */ "/mnt/encryptedstore/test_file"); 1528 }); 1529 testResults.assertNoException(); 1530 1531 // Re-run the same VM & verify the file persisted. Note, the previous `runVmTestService` 1532 // stopped the VM 1533 testResults = 1534 runVmTestService( 1535 TAG, 1536 vm, 1537 (ts, tr) -> { 1538 tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/test_file"); 1539 }); 1540 testResults.assertNoException(); 1541 assertThat(testResults.mFileContent).isEqualTo(EXAMPLE_STRING); 1542 } 1543 1544 @Test 1545 @CddTest(requirements = {"9.17/C-1-1", "9.17/C-2-1"}) canReadFileFromAssets_debugFull()1546 public void canReadFileFromAssets_debugFull() throws Exception { 1547 assumeSupportedDevice(); 1548 1549 VirtualMachineConfig config = 1550 newVmConfigBuilder() 1551 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1552 .setMemoryBytes(minMemoryRequired()) 1553 .setDebugLevel(DEBUG_LEVEL_FULL) 1554 .build(); 1555 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_read_from_assets", config); 1556 1557 TestResults testResults = 1558 runVmTestService( 1559 TAG, 1560 vm, 1561 (testService, ts) -> { 1562 ts.mFileContent = testService.readFromFile("/mnt/apk/assets/file.txt"); 1563 }); 1564 1565 testResults.assertNoException(); 1566 assertThat(testResults.mFileContent).isEqualTo("Hello, I am a file!"); 1567 } 1568 1569 @Test outputShouldBeExplicitlyCaptured()1570 public void outputShouldBeExplicitlyCaptured() throws Exception { 1571 assumeSupportedDevice(); 1572 1573 final VirtualMachineConfig vmConfig = 1574 new VirtualMachineConfig.Builder(getContext()) 1575 .setProtectedVm(mProtectedVm) 1576 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1577 .setDebugLevel(DEBUG_LEVEL_FULL) 1578 .build(); 1579 final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_forward_log", vmConfig); 1580 vm.run(); 1581 1582 try { 1583 assertThrowsVmExceptionContaining( 1584 () -> vm.getConsoleOutput(), "Capturing vm outputs is turned off"); 1585 assertThrowsVmExceptionContaining( 1586 () -> vm.getLogOutput(), "Capturing vm outputs is turned off"); 1587 } finally { 1588 vm.stop(); 1589 } 1590 } 1591 checkVmOutputIsRedirectedToLogcat(boolean debuggable)1592 private boolean checkVmOutputIsRedirectedToLogcat(boolean debuggable) throws Exception { 1593 String time = 1594 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")); 1595 final VirtualMachineConfig vmConfig = 1596 new VirtualMachineConfig.Builder(getContext()) 1597 .setProtectedVm(mProtectedVm) 1598 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1599 .setDebugLevel(debuggable ? DEBUG_LEVEL_FULL : DEBUG_LEVEL_NONE) 1600 .setVmOutputCaptured(false) 1601 .build(); 1602 final VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_logcat", vmConfig); 1603 1604 runVmTestService(TAG, vm, (service, results) -> {}); 1605 1606 // only check logs printed after this test 1607 Process logcatProcess = 1608 new ProcessBuilder() 1609 .command( 1610 "logcat", 1611 "-e", 1612 "virtualizationmanager::aidl: Console.*executing main task", 1613 "-t", 1614 time) 1615 .start(); 1616 logcatProcess.waitFor(); 1617 BufferedReader reader = 1618 new BufferedReader(new InputStreamReader(logcatProcess.getInputStream())); 1619 return !Strings.isNullOrEmpty(reader.readLine()); 1620 } 1621 1622 @Test outputIsRedirectedToLogcatIfNotCaptured()1623 public void outputIsRedirectedToLogcatIfNotCaptured() throws Exception { 1624 assumeSupportedDevice(); 1625 1626 assertThat(checkVmOutputIsRedirectedToLogcat(true)).isTrue(); 1627 } 1628 setSystemProperties(String name, String value)1629 private boolean setSystemProperties(String name, String value) { 1630 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 1631 UiAutomation uiAutomation = instrumentation.getUiAutomation(); 1632 String cmd = "setprop " + name + " " + (value.isEmpty() ? "\"\"" : value); 1633 return runInShellWithStderr(TAG, uiAutomation, cmd).trim().isEmpty(); 1634 } 1635 1636 @Test outputIsNotRedirectedToLogcatIfNotDebuggable()1637 public void outputIsNotRedirectedToLogcatIfNotDebuggable() throws Exception { 1638 assumeSupportedDevice(); 1639 1640 // Disable debug policy to ensure no log output. 1641 String sysprop = "hypervisor.virtualizationmanager.debug_policy.path"; 1642 String old = SystemProperties.get(sysprop); 1643 assumeTrue( 1644 "Can't disable debug policy. Perhapse user build?", 1645 setSystemProperties(sysprop, "")); 1646 1647 try { 1648 assertThat(checkVmOutputIsRedirectedToLogcat(false)).isFalse(); 1649 } finally { 1650 assertThat(setSystemProperties(sysprop, old)).isTrue(); 1651 } 1652 } 1653 1654 @Test testStartVmWithPayloadOfAnotherApp()1655 public void testStartVmWithPayloadOfAnotherApp() throws Exception { 1656 assumeSupportedDevice(); 1657 1658 Context ctx = getContext(); 1659 Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0); 1660 1661 VirtualMachineConfig config = 1662 new VirtualMachineConfig.Builder(otherAppCtx) 1663 .setDebugLevel(DEBUG_LEVEL_FULL) 1664 .setProtectedVm(isProtectedVm()) 1665 .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so") 1666 .build(); 1667 1668 try (VirtualMachine vm = forceCreateNewVirtualMachine("vm_from_another_app", config)) { 1669 TestResults results = 1670 runVmTestService( 1671 TAG, 1672 vm, 1673 (ts, tr) -> { 1674 tr.mAddInteger = ts.addInteger(101, 303); 1675 }); 1676 assertThat(results.mAddInteger).isEqualTo(404); 1677 } 1678 1679 getVirtualMachineManager().delete("vm_from_another_app"); 1680 } 1681 1682 @Test testVmDescriptorParcelUnparcel_noTrustedStorage()1683 public void testVmDescriptorParcelUnparcel_noTrustedStorage() throws Exception { 1684 assumeSupportedDevice(); 1685 1686 VirtualMachineConfig config = 1687 newVmConfigBuilder() 1688 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1689 .setDebugLevel(DEBUG_LEVEL_FULL) 1690 .build(); 1691 1692 VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config); 1693 // Just start & stop the VM. 1694 runVmTestService(TAG, originalVm, (ts, tr) -> {}); 1695 1696 // Now create the descriptor and manually parcel & unparcel it. 1697 VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor()); 1698 1699 if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) { 1700 getVirtualMachineManager().delete("import_vm_from_unparceled"); 1701 } 1702 1703 VirtualMachine importVm = 1704 getVirtualMachineManager() 1705 .importFromDescriptor("import_vm_from_unparceled", vmDescriptor); 1706 1707 assertFileContentsAreEqualInTwoVms( 1708 "config.xml", "original_vm", "import_vm_from_unparceled"); 1709 assertFileContentsAreEqualInTwoVms( 1710 "instance.img", "original_vm", "import_vm_from_unparceled"); 1711 1712 // Check that we can start and stop imported vm as well 1713 runVmTestService(TAG, importVm, (ts, tr) -> {}); 1714 } 1715 1716 @Test testVmDescriptorParcelUnparcel_withTrustedStorage()1717 public void testVmDescriptorParcelUnparcel_withTrustedStorage() throws Exception { 1718 assumeSupportedDevice(); 1719 1720 VirtualMachineConfig config = 1721 newVmConfigBuilder() 1722 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1723 .setDebugLevel(DEBUG_LEVEL_FULL) 1724 .setEncryptedStorageBytes(1_000_000) 1725 .build(); 1726 1727 VirtualMachine originalVm = forceCreateNewVirtualMachine("original_vm", config); 1728 // Just start & stop the VM. 1729 { 1730 TestResults testResults = 1731 runVmTestService( 1732 TAG, 1733 originalVm, 1734 (ts, tr) -> { 1735 ts.writeToFile("not a secret!", "/mnt/encryptedstore/secret.txt"); 1736 }); 1737 assertThat(testResults.mException).isNull(); 1738 } 1739 1740 // Now create the descriptor and manually parcel & unparcel it. 1741 VirtualMachineDescriptor vmDescriptor = toParcelFromParcel(originalVm.toDescriptor()); 1742 1743 if (getVirtualMachineManager().get("import_vm_from_unparceled") != null) { 1744 getVirtualMachineManager().delete("import_vm_from_unparceled"); 1745 } 1746 1747 VirtualMachine importVm = 1748 getVirtualMachineManager() 1749 .importFromDescriptor("import_vm_from_unparceled", vmDescriptor); 1750 1751 assertFileContentsAreEqualInTwoVms( 1752 "config.xml", "original_vm", "import_vm_from_unparceled"); 1753 assertFileContentsAreEqualInTwoVms( 1754 "instance.img", "original_vm", "import_vm_from_unparceled"); 1755 assertFileContentsAreEqualInTwoVms( 1756 "storage.img", "original_vm", "import_vm_from_unparceled"); 1757 1758 TestResults testResults = 1759 runVmTestService( 1760 TAG, 1761 importVm, 1762 (ts, tr) -> { 1763 tr.mFileContent = ts.readFromFile("/mnt/encryptedstore/secret.txt"); 1764 }); 1765 1766 assertThat(testResults.mException).isNull(); 1767 assertThat(testResults.mFileContent).isEqualTo("not a secret!"); 1768 } 1769 1770 @Test testShareVmWithAnotherApp()1771 public void testShareVmWithAnotherApp() throws Exception { 1772 assumeSupportedDevice(); 1773 1774 Context ctx = getContext(); 1775 Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0); 1776 1777 VirtualMachineConfig config = 1778 new VirtualMachineConfig.Builder(otherAppCtx) 1779 .setDebugLevel(DEBUG_LEVEL_FULL) 1780 .setProtectedVm(isProtectedVm()) 1781 .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so") 1782 .build(); 1783 1784 VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config); 1785 // Just start & stop the VM. 1786 runVmTestService(TAG, vm, (ts, tr) -> {}); 1787 // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME) 1788 VirtualMachineDescriptor vmDesc = vm.toDescriptor(); 1789 1790 Intent serviceIntent = new Intent(); 1791 serviceIntent.setComponent( 1792 new ComponentName( 1793 VM_SHARE_APP_PACKAGE_NAME, 1794 "com.android.microdroid.test.sharevm.VmShareServiceImpl")); 1795 serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService"); 1796 1797 VmShareServiceConnection connection = new VmShareServiceConnection(); 1798 boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE); 1799 assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue(); 1800 1801 IVmShareTestService service = connection.waitForService(); 1802 assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull(); 1803 1804 try { 1805 // Send the VM descriptor to the other app. When received, it will reconstruct the VM 1806 // from the descriptor, start it, connect to the ITestService in it, creates a "proxy" 1807 // ITestService binder that delegates all the calls to the VM, and share it with this 1808 // app. It will allow us to verify assertions on the running VM in the other app. 1809 ITestService testServiceProxy = service.startVm(vmDesc); 1810 1811 int result = testServiceProxy.addInteger(37, 73); 1812 assertThat(result).isEqualTo(110); 1813 } finally { 1814 ctx.unbindService(connection); 1815 } 1816 } 1817 1818 @Test testShareVmWithAnotherApp_encryptedStorage()1819 public void testShareVmWithAnotherApp_encryptedStorage() throws Exception { 1820 assumeSupportedDevice(); 1821 1822 Context ctx = getContext(); 1823 Context otherAppCtx = ctx.createPackageContext(VM_SHARE_APP_PACKAGE_NAME, 0); 1824 1825 VirtualMachineConfig config = 1826 new VirtualMachineConfig.Builder(otherAppCtx) 1827 .setDebugLevel(DEBUG_LEVEL_FULL) 1828 .setProtectedVm(isProtectedVm()) 1829 .setEncryptedStorageBytes(3_000_000) 1830 .setPayloadBinaryName("MicrodroidPayloadInOtherAppNativeLib.so") 1831 .build(); 1832 1833 VirtualMachine vm = forceCreateNewVirtualMachine("vm_to_share", config); 1834 // Just start & stop the VM. 1835 runVmTestService( 1836 TAG, 1837 vm, 1838 (ts, tr) -> { 1839 ts.writeToFile(EXAMPLE_STRING, "/mnt/encryptedstore/private.key"); 1840 }); 1841 // Get a descriptor that we will share with another app (VM_SHARE_APP_PACKAGE_NAME) 1842 VirtualMachineDescriptor vmDesc = vm.toDescriptor(); 1843 1844 Intent serviceIntent = new Intent(); 1845 serviceIntent.setComponent( 1846 new ComponentName( 1847 VM_SHARE_APP_PACKAGE_NAME, 1848 "com.android.microdroid.test.sharevm.VmShareServiceImpl")); 1849 serviceIntent.setAction("com.android.microdroid.test.sharevm.VmShareService"); 1850 1851 VmShareServiceConnection connection = new VmShareServiceConnection(); 1852 boolean ret = ctx.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE); 1853 assertWithMessage("Failed to bind to " + serviceIntent).that(ret).isTrue(); 1854 1855 IVmShareTestService service = connection.waitForService(); 1856 assertWithMessage("Timed out connecting to " + serviceIntent).that(service).isNotNull(); 1857 1858 try { 1859 // Send the VM descriptor to the other app. When received, it will reconstruct the VM 1860 // from the descriptor, start it, connect to the ITestService in it, creates a "proxy" 1861 // ITestService binder that delegates all the calls to the VM, and share it with this 1862 // app. It will allow us to verify assertions on the running VM in the other app. 1863 ITestService testServiceProxy = service.startVm(vmDesc); 1864 1865 String result = testServiceProxy.readFromFile("/mnt/encryptedstore/private.key"); 1866 assertThat(result).isEqualTo(EXAMPLE_STRING); 1867 } finally { 1868 ctx.unbindService(connection); 1869 } 1870 } 1871 1872 @Test 1873 @CddTest(requirements = {"9.17/C-1-5"}) testFileUnderBinHasExecutePermission()1874 public void testFileUnderBinHasExecutePermission() throws Exception { 1875 assumeSupportedDevice(); 1876 1877 VirtualMachineConfig vmConfig = 1878 newVmConfigBuilder() 1879 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1880 .setMemoryBytes(minMemoryRequired()) 1881 .setDebugLevel(DEBUG_LEVEL_FULL) 1882 .build(); 1883 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_perms", vmConfig); 1884 1885 TestResults testResults = 1886 runVmTestService( 1887 TAG, 1888 vm, 1889 (ts, tr) -> { 1890 tr.mFileMode = ts.getFilePermissions("/mnt/apk/bin/measure_io"); 1891 }); 1892 1893 testResults.assertNoException(); 1894 int allPermissionsMask = 1895 OsConstants.S_IRUSR 1896 | OsConstants.S_IWUSR 1897 | OsConstants.S_IXUSR 1898 | OsConstants.S_IRGRP 1899 | OsConstants.S_IWGRP 1900 | OsConstants.S_IXGRP 1901 | OsConstants.S_IROTH 1902 | OsConstants.S_IWOTH 1903 | OsConstants.S_IXOTH; 1904 assertThat(testResults.mFileMode & allPermissionsMask) 1905 .isEqualTo(OsConstants.S_IRUSR | OsConstants.S_IXUSR); 1906 } 1907 1908 // Taken from bionic/libs/kernel/uapi/linux/mounth.h. 1909 private static final int MS_NOEXEC = 8; 1910 1911 @Test 1912 @CddTest(requirements = {"9.17/C-1-5"}) dataIsMountedWithNoExec()1913 public void dataIsMountedWithNoExec() throws Exception { 1914 assumeSupportedDevice(); 1915 1916 VirtualMachineConfig vmConfig = 1917 newVmConfigBuilder() 1918 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1919 .setDebugLevel(DEBUG_LEVEL_FULL) 1920 .build(); 1921 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_data_mount", vmConfig); 1922 1923 TestResults testResults = 1924 runVmTestService( 1925 TAG, 1926 vm, 1927 (ts, tr) -> { 1928 tr.mMountFlags = ts.getMountFlags("/data"); 1929 }); 1930 1931 assertThat(testResults.mException).isNull(); 1932 assertWithMessage("/data should be mounted with MS_NOEXEC") 1933 .that(testResults.mMountFlags & MS_NOEXEC) 1934 .isEqualTo(MS_NOEXEC); 1935 } 1936 1937 @Test 1938 @CddTest(requirements = {"9.17/C-1-5"}) encryptedStoreIsMountedWithNoExec()1939 public void encryptedStoreIsMountedWithNoExec() throws Exception { 1940 assumeSupportedDevice(); 1941 1942 VirtualMachineConfig vmConfig = 1943 newVmConfigBuilder() 1944 .setPayloadBinaryName("MicrodroidTestNativeLib.so") 1945 .setDebugLevel(DEBUG_LEVEL_FULL) 1946 .setEncryptedStorageBytes(4_000_000) 1947 .build(); 1948 VirtualMachine vm = forceCreateNewVirtualMachine("test_vm_encstore_no_exec", vmConfig); 1949 1950 TestResults testResults = 1951 runVmTestService( 1952 TAG, 1953 vm, 1954 (ts, tr) -> { 1955 tr.mMountFlags = ts.getMountFlags("/mnt/encryptedstore"); 1956 }); 1957 1958 assertThat(testResults.mException).isNull(); 1959 assertWithMessage("/mnt/encryptedstore should be mounted with MS_NOEXEC") 1960 .that(testResults.mMountFlags & MS_NOEXEC) 1961 .isEqualTo(MS_NOEXEC); 1962 } 1963 1964 @Test 1965 @VsrTest(requirements = {"VSR-7.1-001.003"}) kernelVersionRequirement()1966 public void kernelVersionRequirement() throws Exception { 1967 int firstApiLevel = SystemProperties.getInt("ro.product.first_api_level", 0); 1968 assume().withMessage("Skip on devices launched before Android 14 (API level 34)") 1969 .that(firstApiLevel) 1970 .isAtLeast(34); 1971 1972 String[] tokens = KERNEL_VERSION.split("\\."); 1973 int major = Integer.parseInt(tokens[0]); 1974 int minor = Integer.parseInt(tokens[1]); 1975 1976 // Check kernel version >= 5.15 1977 assertTrue(major >= 5); 1978 if (major == 5) { 1979 assertTrue(minor >= 15); 1980 } 1981 } 1982 1983 private static class VmShareServiceConnection implements ServiceConnection { 1984 1985 private final CountDownLatch mLatch = new CountDownLatch(1); 1986 1987 private IVmShareTestService mVmShareTestService; 1988 1989 @Override onServiceConnected(ComponentName name, IBinder service)1990 public void onServiceConnected(ComponentName name, IBinder service) { 1991 mVmShareTestService = IVmShareTestService.Stub.asInterface(service); 1992 mLatch.countDown(); 1993 } 1994 1995 @Override onServiceDisconnected(ComponentName name)1996 public void onServiceDisconnected(ComponentName name) {} 1997 waitForService()1998 private IVmShareTestService waitForService() throws Exception { 1999 if (!mLatch.await(1, TimeUnit.MINUTES)) { 2000 return null; 2001 } 2002 return mVmShareTestService; 2003 } 2004 } 2005 toParcelFromParcel(VirtualMachineDescriptor descriptor)2006 private VirtualMachineDescriptor toParcelFromParcel(VirtualMachineDescriptor descriptor) { 2007 Parcel parcel = Parcel.obtain(); 2008 descriptor.writeToParcel(parcel, 0); 2009 parcel.setDataPosition(0); 2010 return VirtualMachineDescriptor.CREATOR.createFromParcel(parcel); 2011 } 2012 assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2)2013 private void assertFileContentsAreEqualInTwoVms(String fileName, String vmName1, String vmName2) 2014 throws IOException { 2015 File file1 = getVmFile(vmName1, fileName); 2016 File file2 = getVmFile(vmName2, fileName); 2017 try (FileInputStream input1 = new FileInputStream(file1); 2018 FileInputStream input2 = new FileInputStream(file2)) { 2019 assertThat(Arrays.equals(input1.readAllBytes(), input2.readAllBytes())).isTrue(); 2020 } 2021 } 2022 getVmFile(String vmName, String fileName)2023 private File getVmFile(String vmName, String fileName) { 2024 Context context = getContext(); 2025 Path filePath = Paths.get(context.getDataDir().getPath(), "vm", vmName, fileName); 2026 return filePath.toFile(); 2027 } 2028 assertThrowsVmException(ThrowingRunnable runnable)2029 private void assertThrowsVmException(ThrowingRunnable runnable) { 2030 assertThrows(VirtualMachineException.class, runnable); 2031 } 2032 assertThrowsVmExceptionContaining( ThrowingRunnable runnable, String expectedContents)2033 private void assertThrowsVmExceptionContaining( 2034 ThrowingRunnable runnable, String expectedContents) { 2035 Exception e = assertThrows(VirtualMachineException.class, runnable); 2036 assertThat(e).hasMessageThat().contains(expectedContents); 2037 } 2038 minMemoryRequired()2039 private long minMemoryRequired() { 2040 if (Build.SUPPORTED_ABIS.length > 0) { 2041 String primaryAbi = Build.SUPPORTED_ABIS[0]; 2042 switch (primaryAbi) { 2043 case "x86_64": 2044 return MIN_MEM_X86_64; 2045 case "arm64-v8a": 2046 return MIN_MEM_ARM64; 2047 } 2048 } 2049 return 0; 2050 } 2051 assumeSupportedDevice()2052 private void assumeSupportedDevice() { 2053 assume() 2054 .withMessage("Skip on 5.4 kernel. b/218303240") 2055 .that(KERNEL_VERSION) 2056 .isNotEqualTo("5.4"); 2057 } 2058 } 2059