1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.microdroid.test;
18 
19 import static com.android.microdroid.test.host.CommandResultSubject.command_results;
20 import static com.android.tradefed.device.TestDevice.MicrodroidBuilder;
21 import static com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
22 import com.android.tradefed.device.DeviceRuntimeException;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 import static com.google.common.truth.Truth.assertWithMessage;
26 
27 import static org.hamcrest.CoreMatchers.containsString;
28 import static org.junit.Assert.assertThat;
29 import static org.junit.Assert.assertThrows;
30 import static org.junit.Assert.assertTrue;
31 import static org.junit.Assume.assumeFalse;
32 import static org.junit.Assume.assumeTrue;
33 
34 import static java.util.stream.Collectors.toList;
35 
36 import android.cts.host.utils.DeviceJUnit4ClassRunnerWithParameters;
37 import android.cts.host.utils.DeviceJUnit4Parameterized;
38 import android.cts.statsdatom.lib.ConfigUtils;
39 import android.cts.statsdatom.lib.ReportUtils;
40 
41 import com.android.compatibility.common.util.CddTest;
42 import com.android.microdroid.test.common.ProcessUtil;
43 import com.android.microdroid.test.host.CommandRunner;
44 import com.android.microdroid.test.host.MicrodroidHostTestCaseBase;
45 import com.android.os.AtomsProto;
46 import com.android.os.StatsLog;
47 import com.android.tradefed.device.DeviceNotAvailableException;
48 import com.android.tradefed.device.ITestDevice;
49 import com.android.tradefed.device.TestDevice;
50 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
51 import com.android.tradefed.util.CommandResult;
52 import com.android.tradefed.util.CommandStatus;
53 import com.android.tradefed.util.FileUtil;
54 import com.android.tradefed.util.RunUtil;
55 import com.android.tradefed.util.xml.AbstractXmlParser;
56 import com.android.virt.PayloadMetadata;
57 
58 import org.json.JSONArray;
59 import org.json.JSONObject;
60 import org.junit.After;
61 import org.junit.Before;
62 import org.junit.Ignore;
63 import org.junit.Rule;
64 import org.junit.Test;
65 import org.junit.rules.TestName;
66 import org.junit.runner.RunWith;
67 import org.junit.runners.Parameterized;
68 import org.junit.runners.Parameterized.UseParametersRunnerFactory;
69 import org.xml.sax.Attributes;
70 import org.xml.sax.helpers.DefaultHandler;
71 
72 import java.io.ByteArrayInputStream;
73 import java.io.File;
74 import java.io.PipedInputStream;
75 import java.io.PipedOutputStream;
76 import java.util.ArrayList;
77 import java.util.Arrays;
78 import java.util.Collection;
79 import java.util.Collections;
80 import java.util.List;
81 import java.util.Map;
82 import java.util.concurrent.Callable;
83 import java.util.concurrent.TimeUnit;
84 import java.util.function.Function;
85 import java.util.regex.Matcher;
86 import java.util.regex.Pattern;
87 import java.util.stream.Collectors;
88 import java.util.Objects;
89 
90 @RunWith(DeviceJUnit4Parameterized.class)
91 @UseParametersRunnerFactory(DeviceJUnit4ClassRunnerWithParameters.RunnerFactory.class)
92 public class MicrodroidHostTests extends MicrodroidHostTestCaseBase {
93     private static final String APK_NAME = "MicrodroidTestApp.apk";
94     private static final String APK_UPDATED_NAME = "MicrodroidTestAppUpdated.apk";
95     private static final String PACKAGE_NAME = "com.android.microdroid.test";
96     private static final String SHELL_PACKAGE_NAME = "com.android.shell";
97     private static final String VIRT_APEX = "/apex/com.android.virt/";
98     private static final String INSTANCE_IMG = TEST_ROOT + "instance.img";
99     private static final String INSTANCE_ID_FILE = TEST_ROOT + "instance_id";
100 
101     private static final int MIN_MEM_ARM64 = 170;
102     private static final int MIN_MEM_X86_64 = 196;
103 
104     private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
105 
106     private static class VmInfo {
107         final Process mProcess;
108 
VmInfo(Process process)109         VmInfo(Process process) {
110             mProcess = process;
111         }
112     }
113 
114     @Parameterized.Parameters(name = "protectedVm={0},gki={1}")
params()115     public static Collection<Object[]> params() {
116         List<Object[]> ret = new ArrayList<>();
117         ret.add(new Object[] {true /* protectedVm */, null /* use microdroid kernel */});
118         ret.add(new Object[] {false /* protectedVm */, null /* use microdroid kernel */});
119         // TODO(b/302465542): run only the latest GKI on presubmit to reduce running time
120         for (String gki : SUPPORTED_GKI_VERSIONS) {
121             ret.add(new Object[] {true /* protectedVm */, gki});
122             ret.add(new Object[] {false /* protectedVm */, gki});
123         }
124         return ret;
125     }
126 
127     @Parameterized.Parameter(0)
128     public boolean mProtectedVm;
129 
130     @Parameterized.Parameter(1)
131     public String mGki;
132 
133     private String mOs;
134 
135     @Rule public TestLogData mTestLogs = new TestLogData();
136     @Rule public TestName mTestName = new TestName();
137     @Rule public TestMetrics mMetrics = new TestMetrics();
138 
139     private String mMetricPrefix;
140 
141     private ITestDevice mMicrodroidDevice;
142 
minMemorySize()143     private int minMemorySize() throws DeviceNotAvailableException {
144         CommandRunner android = new CommandRunner(getDevice());
145         String abi = android.run("getprop", "ro.product.cpu.abi");
146         assertThat(abi).isNotEmpty();
147         if (abi.startsWith("arm64")) {
148             return MIN_MEM_ARM64;
149         } else if (abi.startsWith("x86_64")) {
150             return MIN_MEM_X86_64;
151         }
152         throw new AssertionError("Unsupported ABI: " + abi);
153     }
154 
newPartition(String label, String path)155     private static JSONObject newPartition(String label, String path) {
156         return new JSONObject(Map.of("label", label, "path", path));
157     }
158 
createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)159     private void createPayloadMetadata(List<ActiveApexInfo> apexes, File payloadMetadata)
160             throws Exception {
161         PayloadMetadata.write(
162                 PayloadMetadata.metadata(
163                         "/mnt/apk/assets/vm_config.json",
164                         PayloadMetadata.apk("microdroid-apk"),
165                         apexes.stream()
166                                 .map(apex -> PayloadMetadata.apex(apex.name))
167                                 .collect(toList())),
168                 payloadMetadata);
169     }
170 
resignVirtApex( File virtApexDir, File signingKey, Map<String, File> keyOverrides, boolean updateBootconfigs)171     private void resignVirtApex(
172             File virtApexDir,
173             File signingKey,
174             Map<String, File> keyOverrides,
175             boolean updateBootconfigs) {
176         File signVirtApex = findTestFile("sign_virt_apex");
177 
178         RunUtil runUtil = createRunUtil();
179         // Set the parent dir on the PATH (e.g. <workdir>/bin)
180         String separator = System.getProperty("path.separator");
181         String path = signVirtApex.getParentFile().getPath() + separator + System.getenv("PATH");
182         runUtil.setEnvVariable("PATH", path);
183 
184         List<String> command = new ArrayList<>();
185         command.add(signVirtApex.getAbsolutePath());
186         if (!updateBootconfigs) {
187             command.add("--do_not_update_bootconfigs");
188         }
189         // In some cases we run a CTS binary that is built from a different branch that the /system
190         // image under test. In such cases we might end up in a situation when avb_version used in
191         // CTS binary and avb_version used to sign the com.android.virt APEX do not match.
192         // This is a weird configuration, but unfortunately it can happen, hence we pass here
193         // --do_not_validate_avb_version flag to make sure that CTS doesn't fail on it.
194         command.add("--do_not_validate_avb_version");
195         keyOverrides.forEach(
196                 (filename, keyFile) ->
197                         command.add("--key_override " + filename + "=" + keyFile.getPath()));
198         command.add(signingKey.getPath());
199         command.add(virtApexDir.getPath());
200 
201         CommandResult result =
202                 runUtil.runTimedCmd(
203                         // sign_virt_apex is so slow on CI server that this often times
204                         // out. Until we can make it fast, use 50s for timeout
205                         50 * 1000, "/bin/bash", "-c", String.join(" ", command));
206         String out = result.getStdout();
207         String err = result.getStderr();
208         assertWithMessage(
209                         "resigning the Virt APEX failed:\n\tout: " + out + "\n\terr: " + err + "\n")
210                 .about(command_results())
211                 .that(result)
212                 .isSuccess();
213     }
214 
assertThatEventually( long timeoutMillis, Callable<T> callable, org.hamcrest.Matcher<T> matcher)215     private static <T> void assertThatEventually(
216             long timeoutMillis, Callable<T> callable, org.hamcrest.Matcher<T> matcher)
217             throws Exception {
218         long start = System.currentTimeMillis();
219         while ((System.currentTimeMillis() - start < timeoutMillis)
220                 && !matcher.matches(callable.call())) {
221             RunUtil.getDefault().sleep(500);
222         }
223         assertThat(callable.call(), matcher);
224     }
225 
getDeviceNumCpus(CommandRunner runner)226     private int getDeviceNumCpus(CommandRunner runner) throws DeviceNotAvailableException {
227         return Integer.parseInt(runner.run("nproc --all").trim());
228     }
229 
getDeviceNumCpus(ITestDevice device)230     private int getDeviceNumCpus(ITestDevice device) throws DeviceNotAvailableException {
231         return getDeviceNumCpus(new CommandRunner(device));
232     }
233 
234     static class ActiveApexInfo {
235         public String name;
236         public String path;
237         public boolean provideSharedApexLibs;
238 
ActiveApexInfo(String name, String path, boolean provideSharedApexLibs)239         ActiveApexInfo(String name, String path, boolean provideSharedApexLibs) {
240             this.name = name;
241             this.path = path;
242             this.provideSharedApexLibs = provideSharedApexLibs;
243         }
244     }
245 
246     static class ActiveApexInfoList {
247         private List<ActiveApexInfo> mList;
248 
ActiveApexInfoList(List<ActiveApexInfo> list)249         ActiveApexInfoList(List<ActiveApexInfo> list) {
250             this.mList = list;
251         }
252 
get(String apexName)253         ActiveApexInfo get(String apexName) {
254             return mList.stream()
255                     .filter(info -> apexName.equals(info.name))
256                     .findFirst()
257                     .orElse(null);
258         }
259 
getSharedLibApexes()260         List<ActiveApexInfo> getSharedLibApexes() {
261             return mList.stream().filter(info -> info.provideSharedApexLibs).collect(toList());
262         }
263     }
264 
getActiveApexInfoList()265     private ActiveApexInfoList getActiveApexInfoList() throws Exception {
266         String apexInfoListXml = getDevice().pullFileContents("/apex/apex-info-list.xml");
267         List<ActiveApexInfo> list = new ArrayList<>();
268         new AbstractXmlParser() {
269             @Override
270             protected DefaultHandler createXmlHandler() {
271                 return new DefaultHandler() {
272                     @Override
273                     public void startElement(
274                             String uri, String localName, String qName, Attributes attributes) {
275                         if (localName.equals("apex-info")
276                                 && attributes.getValue("isActive").equals("true")) {
277                             String name = attributes.getValue("moduleName");
278                             String path = attributes.getValue("modulePath");
279                             String sharedApex = attributes.getValue("provideSharedApexLibs");
280                             list.add(new ActiveApexInfo(name, path, "true".equals(sharedApex)));
281                         }
282                     }
283                 };
284             }
285         }.parse(new ByteArrayInputStream(apexInfoListXml.getBytes()));
286         return new ActiveApexInfoList(list);
287     }
288 
289     private VmInfo runMicrodroidWithResignedImages(
290             File key,
291             Map<String, File> keyOverrides,
292             boolean isProtected,
293             boolean updateBootconfigs)
294             throws Exception {
295         CommandRunner android = new CommandRunner(getDevice());
296 
297         File virtApexDir = FileUtil.createTempDir("virt_apex");
298 
299         // Pull the virt apex's etc/ directory (which contains images and microdroid.json)
300         File virtApexEtcDir = new File(virtApexDir, "etc");
301         // We need only etc/ directory for images
302         assertWithMessage("Failed to mkdir " + virtApexEtcDir)
303                 .that(virtApexEtcDir.mkdirs())
304                 .isTrue();
305         assertWithMessage("Failed to pull " + VIRT_APEX + "etc")
306                 .that(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir))
307                 .isTrue();
308 
309         resignVirtApex(virtApexDir, key, keyOverrides, updateBootconfigs);
310 
311         // Push back re-signed virt APEX contents and updated microdroid.json
312         getDevice().pushDir(virtApexDir, TEST_ROOT);
313 
314         // Create the idsig file for the APK
315         final String apkPath = getPathForPackage(PACKAGE_NAME);
316         final String idSigPath = TEST_ROOT + "idsig";
317         android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath);
318 
319         // Create the instance image for the VM
320         final String instanceImgPath = TEST_ROOT + "instance.img";
321         android.run(
322                 VIRT_APEX + "bin/vm",
323                 "create-partition",
324                 "--type instance",
325                 instanceImgPath,
326                 Integer.toString(10 * 1024 * 1024));
327 
328         // payload-metadata is created on device
329         final String payloadMetadataPath = TEST_ROOT + "payload-metadata.img";
330 
331         // Load /apex/apex-info-list.xml to get paths to APEXes required for the VM.
332         ActiveApexInfoList list = getActiveApexInfoList();
333 
334         // Since Java APP can't start a VM with a custom image, here, we start a VM using `vm run`
335         // command with a VM Raw config which is equiv. to what virtualizationservice creates with
336         // a VM App config.
337         //
338         // 1. use etc/microdroid.json as base
339         // 2. add partitions: bootconfig, vbmeta, instance image
340         // 3. add a payload image disk with
341         //   - payload-metadata
342         //   - apexes
343         //   - test apk
344         //   - its idsig
345 
346         // Load etc/microdroid.json
347         File microdroidConfigFile = new File(virtApexEtcDir, mOs + ".json");
348         JSONObject config = new JSONObject(FileUtil.readStringFromFile(microdroidConfigFile));
349 
350         // Replace paths so that the config uses re-signed images from TEST_ROOT
351         config.put("kernel", config.getString("kernel").replace(VIRT_APEX, TEST_ROOT));
352         JSONArray disks = config.getJSONArray("disks");
353         for (int diskIndex = 0; diskIndex < disks.length(); diskIndex++) {
354             JSONObject disk = disks.getJSONObject(diskIndex);
355             JSONArray partitions = disk.getJSONArray("partitions");
356             for (int partIndex = 0; partIndex < partitions.length(); partIndex++) {
357                 JSONObject part = partitions.getJSONObject(partIndex);
358                 part.put("path", part.getString("path").replace(VIRT_APEX, TEST_ROOT));
359             }
360         }
361 
362         // Add partitions to the second disk
363         final String initrdPath = TEST_ROOT + "etc/" + mOs + "_initrd_debuggable.img";
364         config.put("initrd", initrdPath);
365         // Add instance image as a partition in disks[1]
366         disks.put(
367                 new JSONObject()
368                         .put("writable", true)
369                         .put(
370                                 "partitions",
371                                 new JSONArray().put(newPartition("vm-instance", instanceImgPath))));
372         // Add payload image disk with partitions:
373         // - payload-metadata
374         // - apexes: com.android.os.statsd, com.android.adbd, [sharedlib apex](optional)
375         // - apk and idsig
376         List<ActiveApexInfo> apexesForVm = new ArrayList<>();
377         apexesForVm.add(list.get("com.android.os.statsd"));
378         apexesForVm.add(list.get("com.android.adbd"));
379         apexesForVm.addAll(list.getSharedLibApexes());
380 
381         final JSONArray partitions = new JSONArray();
382         partitions.put(newPartition("payload-metadata", payloadMetadataPath));
383         for (ActiveApexInfo apex : apexesForVm) {
384             partitions.put(newPartition(apex.name, apex.path));
385         }
386         partitions
387                 .put(newPartition("microdroid-apk", apkPath))
388                 .put(newPartition("microdroid-apk-idsig", idSigPath));
389         disks.put(new JSONObject().put("writable", false).put("partitions", partitions));
390 
391         final File localPayloadMetadata = new File(virtApexDir, "payload-metadata.img");
392         createPayloadMetadata(apexesForVm, localPayloadMetadata);
393         getDevice().pushFile(localPayloadMetadata, payloadMetadataPath);
394 
395         config.put("protected", isProtected);
396 
397         // Write updated raw config
398         final String configPath = TEST_ROOT + "raw_config.json";
399         getDevice().pushString(config.toString(), configPath);
400 
401         List<String> args =
402                 Arrays.asList(
403                         "adb",
404                         "-s",
405                         getDevice().getSerialNumber(),
406                         "shell",
407                         VIRT_APEX + "bin/vm run",
408                         "--console " + CONSOLE_PATH,
409                         "--log " + LOG_PATH,
410                         configPath);
411 
412         PipedInputStream pis = new PipedInputStream();
413         Process process = createRunUtil().runCmdInBackground(args, new PipedOutputStream(pis));
414         return new VmInfo(process);
415     }
416 
417     @Test
418     @CddTest
419     public void UpgradedPackageIsAcceptedWithSecretkeeper() throws Exception {
420         assumeUpdatableVmSupported();
421         getDevice().uninstallPackage(PACKAGE_NAME);
422         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ true);
423         ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
424 
425         getDevice().uninstallPackage(PACKAGE_NAME);
426         cleanUpVirtualizationTestSetup(getDevice());
427         // Install the updated version of app (versionCode 6)
428         getDevice().installPackage(findTestFile(APK_UPDATED_NAME), /* reinstall= */ true);
429         ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
430     }
431 
432     @Test
433     @CddTest
434     public void DowngradedPackageIsRejectedProtectedVm() throws Exception {
435         assumeProtectedVm(); // Rollback protection is provided only for protected VM.
436 
437         // Install the upgraded version (v6)
438         getDevice().uninstallPackage(PACKAGE_NAME);
439         getDevice().installPackage(findTestFile(APK_UPDATED_NAME), /* reinstall= */ true);
440         ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG);
441 
442         getDevice().uninstallPackage(PACKAGE_NAME);
443         cleanUpVirtualizationTestSetup(getDevice());
444         // Install the older version (v5)
445         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ true);
446 
447         assertThrows(
448                 "pVM must fail to boot with downgraded payload apk",
449                 DeviceRuntimeException.class,
450                 () -> ensureMicrodroidBootsSuccessfully(INSTANCE_ID_FILE, INSTANCE_IMG));
451     }
452 
453     private void ensureMicrodroidBootsSuccessfully(String instanceIdPath, String instanceImgPath)
454             throws DeviceNotAvailableException {
455         final String configPath = "assets/vm_config.json";
456         ITestDevice microdroid = null;
457         int timeout = 30000; // 30 seconds
458         try {
459             microdroid =
460                     MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
461                             .debugLevel("full")
462                             .memoryMib(minMemorySize())
463                             .cpuTopology("match_host")
464                             .protectedVm(mProtectedVm)
465                             .instanceIdFile(instanceIdPath)
466                             .instanceImgFile(instanceImgPath)
467                             .setAdbConnectTimeoutMs(timeout)
468                             .build(getAndroidDevice());
469             assertThat(microdroid.waitForBootComplete(timeout)).isTrue();
470             assertThat(microdroid.enableAdbRoot()).isTrue();
471         } finally {
472             if (microdroid != null) {
473                 getAndroidDevice().shutdownMicrodroid(microdroid);
474             }
475         }
476     }
477 
478     @Test
479     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
480     public void protectedVmRunsPvmfw() throws Exception {
481         // Arrange
482         assumeProtectedVm();
483         final String configPath = "assets/vm_config_apex.json";
484 
485         // Act
486         mMicrodroidDevice =
487                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
488                         .debugLevel("full")
489                         .memoryMib(minMemorySize())
490                         .cpuTopology("match_host")
491                         .protectedVm(true)
492                         .gki(mGki)
493                         .build(getAndroidDevice());
494 
495         // Assert
496         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
497         String consoleLog = getDevice().pullFileContents(TRADEFED_CONSOLE_PATH);
498         assertWithMessage("Failed to verify that pvmfw started")
499                 .that(consoleLog)
500                 .contains("pVM firmware");
501         assertWithMessage("pvmfw failed to start kernel")
502                 .that(consoleLog)
503                 .contains("Starting payload...");
504         // TODO(b/260994818): Investigate the feasibility of checking DeathReason.
505     }
506 
507     @Test
508     @CddTest(requirements = {"9.17/C-2-1", "9.17/C-2-2", "9.17/C-2-6"})
509     public void protectedVmWithImageSignedWithDifferentKeyFailsToVerifyPayload() throws Exception {
510         // Arrange
511         assumeProtectedVm();
512         File key = findTestFile("test.com.android.virt.pem");
513 
514         // Act
515         VmInfo vmInfo =
516                 runMicrodroidWithResignedImages(
517                         key,
518                         /* keyOverrides= */ Map.of(),
519                         /* isProtected= */ true,
520                         /* updateBootconfigs= */ true);
521 
522         // Assert
523         vmInfo.mProcess.waitFor(5L, TimeUnit.SECONDS);
524         String consoleLog = getDevice().pullFileContents(CONSOLE_PATH);
525         assertWithMessage("pvmfw should start").that(consoleLog).contains("pVM firmware");
526         assertWithMessage("pvmfw should fail to verify the payload")
527                 .that(consoleLog)
528                 .contains("Failed to verify the payload");
529         vmInfo.mProcess.destroy();
530     }
531 
532     @Test
533     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
534     public void testBootSucceedsWhenNonProtectedVmStartsWithImagesSignedWithDifferentKey()
535             throws Exception {
536         assumeNonProtectedVm();
537         File key = findTestFile("test.com.android.virt.pem");
538         Map<String, File> keyOverrides = Map.of();
539         VmInfo vmInfo =
540                 runMicrodroidWithResignedImages(
541                         key, keyOverrides, /* isProtected= */ false, /* updateBootconfigs= */ true);
542         assertThatEventually(
543                 100000,
544                 () ->
545                         getDevice().pullFileContents(CONSOLE_PATH)
546                                 + getDevice().pullFileContents(LOG_PATH),
547                 containsString("boot completed, time to run payload"));
548 
549         vmInfo.mProcess.destroy();
550     }
551 
552     @Test
553     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
554     public void testBootFailsWhenVbMetaDigestDoesNotMatchBootconfig() throws Exception {
555         // protectedVmWithImageSignedWithDifferentKeyRunsPvmfw() is the protected case.
556         assumeNonProtectedVm();
557         // Sign everything with key1 except vbmeta
558         File key = findTestFile("test.com.android.virt.pem");
559         // To be able to stop it, it should be a daemon.
560         VmInfo vmInfo =
561                 runMicrodroidWithResignedImages(
562                         key, Map.of(), /* isProtected= */ false, /* updateBootconfigs= */ false);
563         // Wait so that init can print errors to console (time in cuttlefish >> in real device)
564         assertThatEventually(
565                 100000,
566                 () -> getDevice().pullFileContents(CONSOLE_PATH),
567                 containsString("init: [libfs_avb] Failed to verify vbmeta digest"));
568         vmInfo.mProcess.destroy();
569     }
570 
571     private void waitForCrosvmExit(CommandRunner android, String testStartTime) throws Exception {
572         // TODO: improve crosvm exit check. b/258848245
573         android.runWithTimeout(
574                 15000,
575                 "logcat",
576                 "-m",
577                 "1",
578                 "-e",
579                 "'virtualizationmanager::crosvm.*exited with status exit status:'",
580                 "-T",
581                 "'" + testStartTime + "'");
582     }
583 
584     private boolean isTombstoneReceivedFromHostLogcat(String testStartTime) throws Exception {
585         // Note this method relies on logcat values being printed by the receiver on host
586         // userspace crash log: virtualizationservice/src/aidl.rs
587         // kernel ramdump log: virtualizationmanager/src/crosvm.rs
588         String ramdumpRegex =
589                 "Received [0-9]+ bytes from guest & wrote to tombstone file|"
590                         + "Ramdump \"[^ ]+/ramdump\" sent to tombstoned";
591 
592         String result =
593                 tryRunOnHost(
594                         "timeout",
595                         "3s",
596                         "adb",
597                         "-s",
598                         getDevice().getSerialNumber(),
599                         "logcat",
600                         "-m",
601                         "1",
602                         "-e",
603                         ramdumpRegex,
604                         "-T",
605                         testStartTime);
606         return !result.trim().isEmpty();
607     }
608 
609     private boolean isTombstoneGeneratedWithCmd(
610             boolean protectedVm, String configPath, String... crashCommand) throws Exception {
611         CommandRunner android = new CommandRunner(getDevice());
612         String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
613 
614         mMicrodroidDevice =
615                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
616                         .debugLevel("full")
617                         .memoryMib(minMemorySize())
618                         .cpuTopology("match_host")
619                         .protectedVm(protectedVm)
620                         .gki(mGki)
621                         .build(getAndroidDevice());
622         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
623         mMicrodroidDevice.enableAdbRoot();
624 
625         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
626         // can crash in the middle of crashCommand; fail is ok
627         microdroid.tryRun(crashCommand);
628 
629         // check until microdroid is shut down
630         waitForCrosvmExit(android, testStartTime);
631 
632         return isTombstoneReceivedFromHostLogcat(testStartTime);
633     }
634 
635     @Test
636     public void testTombstonesAreGeneratedUponUserspaceCrash() throws Exception {
637         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
638         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
639         assertThat(
640                         isTombstoneGeneratedWithCmd(
641                                 mProtectedVm,
642                                 "assets/vm_config.json",
643                                 "kill",
644                                 "-SIGSEGV",
645                                 "$(pidof microdroid_launcher)"))
646                 .isTrue();
647     }
648 
649     @Test
650     public void testTombstonesAreNotGeneratedIfNotExportedUponUserspaceCrash() throws Exception {
651         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
652         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
653         assertThat(
654                         isTombstoneGeneratedWithCmd(
655                                 mProtectedVm,
656                                 "assets/vm_config_no_tombstone.json",
657                                 "kill",
658                                 "-SIGSEGV",
659                                 "$(pidof microdroid_launcher)"))
660                 .isFalse();
661     }
662 
663     @Test
664     @Ignore("b/341087884") // TODO(b/341087884): fix & re-enable
665     public void testTombstonesAreGeneratedUponKernelCrash() throws Exception {
666         assumeFalse("Cuttlefish is not supported", isCuttlefish());
667         assumeFalse("Skipping test because ramdump is disabled on user build", isUserBuild());
668         assertThat(
669                         isTombstoneGeneratedWithCmd(
670                                 mProtectedVm,
671                                 "assets/vm_config.json",
672                                 "echo",
673                                 "c",
674                                 ">",
675                                 "/proc/sysrq-trigger"))
676                 .isTrue();
677     }
678 
679     private boolean isTombstoneGeneratedWithVmRunApp(
680             boolean protectedVm, boolean debuggable, String... additionalArgs) throws Exception {
681         // we can't use microdroid builder as it wants ADB connection (debuggable)
682         CommandRunner android = new CommandRunner(getDevice());
683         String testStartTime = android.runWithTimeout(1000, "date", "'+%Y-%m-%d %H:%M:%S.%N'");
684 
685         android.run("rm", "-rf", TEST_ROOT + "*");
686         android.run("mkdir", "-p", TEST_ROOT + "*");
687 
688         final String apkPath = getPathForPackage(PACKAGE_NAME);
689         final String idsigPath = TEST_ROOT + "idsig";
690         final String instanceImgPath = TEST_ROOT + "instance.img";
691         final String instanceIdPath = TEST_ROOT + "instance_id";
692         List<String> cmd =
693                 new ArrayList<>(
694                         Arrays.asList(
695                                 VIRT_APEX + "bin/vm",
696                                 "run-app",
697                                 "--debug",
698                                 debuggable ? "full" : "none",
699                                 apkPath,
700                                 idsigPath,
701                                 instanceImgPath));
702         if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
703             cmd.add("--instance-id-file");
704             cmd.add(instanceIdPath);
705         }
706         ;
707         if (protectedVm) {
708             cmd.add("--protected");
709         }
710         if (mGki != null) {
711             cmd.add("--gki");
712             cmd.add(mGki);
713         }
714         Collections.addAll(cmd, additionalArgs);
715 
716         android.run(cmd.toArray(new String[0]));
717         return isTombstoneReceivedFromHostLogcat(testStartTime);
718     }
719 
720     private boolean isTombstoneGeneratedWithCrashPayload(boolean protectedVm, boolean debuggable)
721             throws Exception {
722         return isTombstoneGeneratedWithVmRunApp(
723                 protectedVm, debuggable, "--payload-binary-name", "MicrodroidCrashNativeLib.so");
724     }
725 
726     @Test
727     public void testTombstonesAreGeneratedWithCrashPayload() throws Exception {
728         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
729         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
730         assertThat(isTombstoneGeneratedWithCrashPayload(mProtectedVm, /* debuggable= */ true))
731                 .isTrue();
732     }
733 
734     @Test
735     public void testTombstonesAreNotGeneratedWithCrashPayloadWhenNonDebuggable() throws Exception {
736         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
737         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
738         assertThat(isTombstoneGeneratedWithCrashPayload(mProtectedVm, /* debuggable= */ false))
739                 .isFalse();
740     }
741 
742     private boolean isTombstoneGeneratedWithCrashConfig(boolean protectedVm, boolean debuggable)
743             throws Exception {
744         return isTombstoneGeneratedWithVmRunApp(
745                 protectedVm, debuggable, "--config-path", "assets/vm_config_crash.json");
746     }
747 
748     @Test
749     public void testTombstonesAreGeneratedWithCrashConfig() throws Exception {
750         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
751         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
752         assertThat(isTombstoneGeneratedWithCrashConfig(mProtectedVm, /* debuggable= */ true))
753                 .isTrue();
754     }
755 
756     @Test
757     public void testTombstonesAreNotGeneratedWithCrashConfigWhenNonDebuggable() throws Exception {
758         // TODO(b/291867858): tombstones are failing in HWASAN enabled Microdroid.
759         assumeFalse("tombstones are failing in HWASAN enabled Microdroid.", isHwasan());
760         assertThat(isTombstoneGeneratedWithCrashConfig(mProtectedVm, /* debuggable= */ false))
761                 .isFalse();
762     }
763 
764     @Test
765     public void testTelemetryPushedAtoms() throws Exception {
766         // Reset statsd config and report before the test
767         ConfigUtils.removeConfig(getDevice());
768         ReportUtils.clearReports(getDevice());
769 
770         // Setup statsd config
771         int[] atomIds = {
772             AtomsProto.Atom.VM_CREATION_REQUESTED_FIELD_NUMBER,
773             AtomsProto.Atom.VM_BOOTED_FIELD_NUMBER,
774             AtomsProto.Atom.VM_EXITED_FIELD_NUMBER,
775         };
776         ConfigUtils.uploadConfigForPushedAtoms(getDevice(), PACKAGE_NAME, atomIds);
777 
778         // Create VM with microdroid
779         TestDevice device = getAndroidDevice();
780         final String configPath = "assets/vm_config_apex.json"; // path inside the APK
781         ITestDevice microdroid =
782                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
783                         .debugLevel("full")
784                         .memoryMib(minMemorySize())
785                         .cpuTopology("match_host")
786                         .protectedVm(mProtectedVm)
787                         .gki(mGki)
788                         .build(device);
789         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
790         device.shutdownMicrodroid(microdroid);
791 
792         // Try to collect atoms for 60000 milliseconds.
793         List<StatsLog.EventMetricData> data = new ArrayList<>();
794         long start = System.currentTimeMillis();
795         while ((System.currentTimeMillis() - start < 60000) && data.size() < 3) {
796             data.addAll(ReportUtils.getEventMetricDataList(getDevice()));
797             Thread.sleep(500);
798         }
799         assertThat(
800                         data.stream()
801                                 .map(x -> x.getAtom().getPushedCase().getNumber())
802                                 .collect(Collectors.toList()))
803                 .containsExactly(
804                         AtomsProto.Atom.VM_CREATION_REQUESTED_FIELD_NUMBER,
805                         AtomsProto.Atom.VM_BOOTED_FIELD_NUMBER,
806                         AtomsProto.Atom.VM_EXITED_FIELD_NUMBER)
807                 .inOrder();
808 
809         // Check VmCreationRequested atom
810         AtomsProto.VmCreationRequested atomVmCreationRequested =
811                 data.get(0).getAtom().getVmCreationRequested();
812         if (isPkvmHypervisor()) {
813             assertThat(atomVmCreationRequested.getHypervisor())
814                     .isEqualTo(AtomsProto.VmCreationRequested.Hypervisor.PKVM);
815         }
816         assertThat(atomVmCreationRequested.getIsProtected()).isEqualTo(mProtectedVm);
817         assertThat(atomVmCreationRequested.getCreationSucceeded()).isTrue();
818         assertThat(atomVmCreationRequested.getBinderExceptionCode()).isEqualTo(0);
819         assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
820         assertThat(atomVmCreationRequested.getConfigType())
821                 .isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
822         assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
823         assertThat(atomVmCreationRequested.getMemoryMib()).isEqualTo(minMemorySize());
824         assertThat(atomVmCreationRequested.getApexes())
825                 .isEqualTo("com.android.art:com.android.compos:com.android.sdkext");
826 
827         // Check VmBooted atom
828         AtomsProto.VmBooted atomVmBooted = data.get(1).getAtom().getVmBooted();
829         assertThat(atomVmBooted.getVmIdentifier()).isEqualTo("VmRunApp");
830 
831         // Check VmExited atom
832         AtomsProto.VmExited atomVmExited = data.get(2).getAtom().getVmExited();
833         assertThat(atomVmExited.getVmIdentifier()).isEqualTo("VmRunApp");
834         assertThat(atomVmExited.getDeathReason()).isEqualTo(AtomsProto.VmExited.DeathReason.KILLED);
835         assertThat(atomVmExited.getExitSignal()).isEqualTo(9);
836         // In CPU & memory related fields, check whether positive values are collected or not.
837         if (isPkvmHypervisor()) {
838             // Guest Time may not be updated on other hypervisors.
839             // Checking only if the hypervisor is PKVM.
840             assertThat(atomVmExited.getGuestTimeMillis()).isGreaterThan(0);
841         }
842         assertThat(atomVmExited.getRssVmKb()).isGreaterThan(0);
843         assertThat(atomVmExited.getRssCrosvmKb()).isGreaterThan(0);
844 
845         // Check UID and elapsed_time by comparing each other.
846         assertThat(atomVmBooted.getUid()).isEqualTo(atomVmCreationRequested.getUid());
847         assertThat(atomVmExited.getUid()).isEqualTo(atomVmCreationRequested.getUid());
848         assertThat(atomVmBooted.getElapsedTimeMillis())
849                 .isLessThan(atomVmExited.getElapsedTimeMillis());
850     }
851 
852     private void testMicrodroidBootsWithBuilder(MicrodroidBuilder builder) throws Exception {
853         CommandRunner android = new CommandRunner(getDevice());
854 
855         mMicrodroidDevice = builder.build(getAndroidDevice());
856         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
857         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
858 
859         String vmList = android.run("/apex/com.android.virt/bin/vm list");
860         assertThat(vmList).contains("requesterUid: " + android.run("id -u"));
861 
862         // Test writing to /data partition
863         microdroid.run("echo MicrodroidTest > /data/local/tmp/test.txt");
864         assertThat(microdroid.run("cat /data/local/tmp/test.txt")).isEqualTo("MicrodroidTest");
865 
866         // Check if the APK & its idsig partitions exist
867         final String apkPartition = "/dev/block/by-name/microdroid-apk";
868         assertThat(microdroid.run("ls", apkPartition)).isEqualTo(apkPartition);
869         final String apkIdsigPartition = "/dev/block/by-name/microdroid-apk-idsig";
870         assertThat(microdroid.run("ls", apkIdsigPartition)).isEqualTo(apkIdsigPartition);
871         // Check the vm-instance partition as well
872         final String vmInstancePartition = "/dev/block/by-name/vm-instance";
873         assertThat(microdroid.run("ls", vmInstancePartition)).isEqualTo(vmInstancePartition);
874 
875         // Check if the native library in the APK is has correct filesystem info
876         final String[] abis = microdroid.run("getprop", "ro.product.cpu.abilist").split(",");
877         assertWithMessage("Incorrect ABI list").that(abis).hasLength(1);
878 
879         // Check that no denials have happened so far
880         String consoleText = getDevice().pullFileContents(TRADEFED_CONSOLE_PATH);
881         assertWithMessage("Console output shouldn't be empty").that(consoleText).isNotEmpty();
882         String logText = getDevice().pullFileContents(TRADEFED_LOG_PATH);
883         assertWithMessage("Log output shouldn't be empty").that(logText).isNotEmpty();
884 
885         assertWithMessage("Unexpected denials during VM boot")
886                 .that(consoleText + logText)
887                 .doesNotContainMatch("avc:\\s+denied");
888 
889         assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
890 
891         // Check that selinux is enabled
892         assertWithMessage("SELinux should be in enforcing mode")
893                 .that(microdroid.run("getenforce"))
894                 .isEqualTo("Enforcing");
895 
896         // TODO(b/176805428): adb is broken for nested VM
897         if (!isCuttlefish()) {
898             // Check neverallow rules on microdroid
899             File policyFile = mMicrodroidDevice.pullFile("/sys/fs/selinux/policy");
900             File generalPolicyConfFile = findTestFile("microdroid_general_sepolicy.conf");
901             File sepolicyAnalyzeBin = findTestFile("sepolicy-analyze");
902 
903             CommandResult result =
904                     createRunUtil()
905                             .runTimedCmd(
906                                     10000,
907                                     sepolicyAnalyzeBin.getPath(),
908                                     policyFile.getPath(),
909                                     "neverallow",
910                                     "-w",
911                                     "-f",
912                                     generalPolicyConfFile.getPath());
913             assertWithMessage("neverallow check failed: " + result.getStderr().trim())
914                     .about(command_results())
915                     .that(result)
916                     .isSuccess();
917         }
918     }
919 
920     @Test
921     @CddTest(requirements = {"9.17/C-1-1", "9.17/C-1-2", "9.17/C/1-3"})
922     public void testMicrodroidBoots() throws Exception {
923         final String configPath = "assets/vm_config.json"; // path inside the APK
924         testMicrodroidBootsWithBuilder(
925                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
926                         .debugLevel("full")
927                         .memoryMib(minMemorySize())
928                         .cpuTopology("match_host")
929                         .protectedVm(mProtectedVm)
930                         .gki(mGki));
931     }
932 
933     @Test
934     public void testMicrodroidRamUsage() throws Exception {
935         final String configPath = "assets/vm_config.json";
936         mMicrodroidDevice =
937                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
938                         .debugLevel("full")
939                         .memoryMib(minMemorySize())
940                         .cpuTopology("match_host")
941                         .protectedVm(mProtectedVm)
942                         .gki(mGki)
943                         .build(getAndroidDevice());
944         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
945         mMicrodroidDevice.enableAdbRoot();
946 
947         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
948         Function<String, String> microdroidExec =
949                 (cmd) -> {
950                     try {
951                         return microdroid.run(cmd);
952                     } catch (Exception ex) {
953                         throw new IllegalStateException(ex);
954                     }
955                 };
956 
957         for (Map.Entry<String, Long> stat :
958                 ProcessUtil.getProcessMemoryMap(microdroidExec).entrySet()) {
959             mMetrics.addTestMetric(
960                     mMetricPrefix + "meminfo/" + stat.getKey().toLowerCase(),
961                     stat.getValue().toString());
962         }
963 
964         for (Map.Entry<Integer, String> proc :
965                 ProcessUtil.getProcessMap(microdroidExec).entrySet()) {
966             for (Map.Entry<String, Long> stat :
967                     ProcessUtil.getProcessSmapsRollup(proc.getKey(), microdroidExec).entrySet()) {
968                 String name = stat.getKey().toLowerCase();
969                 mMetrics.addTestMetric(
970                         mMetricPrefix + "smaps/" + name + "/" + proc.getValue(),
971                         stat.getValue().toString());
972             }
973         }
974     }
975 
976     @Test
977     public void testPathToBinaryIsRejected() throws Exception {
978         CommandRunner android = new CommandRunner(getDevice());
979 
980         // Create the idsig file for the APK
981         final String apkPath = getPathForPackage(PACKAGE_NAME);
982         final String idSigPath = TEST_ROOT + "idsig";
983         android.run(VIRT_APEX + "bin/vm", "create-idsig", apkPath, idSigPath);
984         // Create the instance image for the VM
985         final String instanceImgPath = TEST_ROOT + "instance.img";
986         android.run(
987                 VIRT_APEX + "bin/vm",
988                 "create-partition",
989                 "--type instance",
990                 instanceImgPath,
991                 Integer.toString(10 * 1024 * 1024));
992 
993         List<String> cmd =
994                 new ArrayList<>(
995                         Arrays.asList(
996                                 VIRT_APEX + "bin/vm",
997                                 "run-app",
998                                 "--payload-binary-name",
999                                 "./MicrodroidTestNativeLib.so",
1000                                 apkPath,
1001                                 idSigPath,
1002                                 instanceImgPath));
1003         if (isFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
1004             cmd.add("--instance-id-file");
1005             cmd.add(TEST_ROOT + "instance_id");
1006         }
1007 
1008         final String ret = android.runForResult(String.join(" ", cmd)).getStderr().trim();
1009 
1010         assertThat(ret).contains("Payload binary name must not specify a path");
1011     }
1012 
1013     @Test
1014     @CddTest(requirements = {"9.17/C-2-2", "9.17/C-2-6"})
1015     public void testAllVbmetaUseSHA256() throws Exception {
1016         File virtApexDir = FileUtil.createTempDir("virt_apex");
1017         // Pull the virt apex's etc/ directory (which contains images)
1018         File virtApexEtcDir = new File(virtApexDir, "etc");
1019         // We need only etc/ directory for images
1020         assertWithMessage("Failed to mkdir " + virtApexEtcDir)
1021                 .that(virtApexEtcDir.mkdirs())
1022                 .isTrue();
1023         assertWithMessage("Failed to pull " + VIRT_APEX + "etc")
1024                 .that(getDevice().pullDir(VIRT_APEX + "etc", virtApexEtcDir))
1025                 .isTrue();
1026 
1027         checkHashAlgorithm(virtApexEtcDir);
1028     }
1029 
1030     @Test
1031     @CddTest
1032     public void testNoAvfDebugPolicyInLockedDevice() throws Exception {
1033         ITestDevice device = getDevice();
1034 
1035         // Check device's locked state with ro.boot.verifiedbootstate. ro.boot.flash.locked
1036         // may not be set if ro.oem_unlock_supported is false.
1037         String lockProp = device.getProperty("ro.boot.verifiedbootstate");
1038         assumeFalse("Unlocked devices may have AVF debug policy", lockProp.equals("orange"));
1039 
1040         // Test that AVF debug policy doesn't exist.
1041         boolean hasDebugPolicy = device.doesFileExist("/proc/device-tree/avf/guest");
1042         assertThat(hasDebugPolicy).isFalse();
1043     }
1044 
1045     private boolean isLz4(String path) throws Exception {
1046         File lz4tool = findTestFile("lz4");
1047         CommandResult result =
1048                 createRunUtil().runTimedCmd(5000, lz4tool.getAbsolutePath(), "-t", path);
1049         return result.getStatus() == CommandStatus.SUCCESS;
1050     }
1051 
1052     private void decompressLz4(String inputPath, String outputPath) throws Exception {
1053         File lz4tool = findTestFile("lz4");
1054         CommandResult result =
1055                 createRunUtil()
1056                         .runTimedCmd(
1057                                 5000, lz4tool.getAbsolutePath(), "-d", "-f", inputPath, outputPath);
1058         String out = result.getStdout();
1059         String err = result.getStderr();
1060         assertWithMessage(
1061                         "lz4 image "
1062                                 + inputPath
1063                                 + " decompression failed."
1064                                 + "\n\tout: "
1065                                 + out
1066                                 + "\n\terr: "
1067                                 + err
1068                                 + "\n")
1069                 .about(command_results())
1070                 .that(result)
1071                 .isSuccess();
1072     }
1073 
1074     private String avbInfo(String image_path) throws Exception {
1075         if (isLz4(image_path)) {
1076             File decompressedImage = FileUtil.createTempFile("decompressed", ".img");
1077             decompressedImage.deleteOnExit();
1078             decompressLz4(image_path, decompressedImage.getAbsolutePath());
1079             image_path = decompressedImage.getAbsolutePath();
1080         }
1081 
1082         File avbtool = findTestFile("avbtool");
1083         List<String> command =
1084                 Arrays.asList(avbtool.getAbsolutePath(), "info_image", "--image", image_path);
1085         CommandResult result =
1086                 createRunUtil().runTimedCmd(5000, "/bin/bash", "-c", String.join(" ", command));
1087         String out = result.getStdout();
1088         String err = result.getStderr();
1089         assertWithMessage(
1090                         "Command "
1091                                 + command
1092                                 + " failed."
1093                                 + ":\n\tout: "
1094                                 + out
1095                                 + "\n\terr: "
1096                                 + err
1097                                 + "\n")
1098                 .about(command_results())
1099                 .that(result)
1100                 .isSuccess();
1101         return out;
1102     }
1103 
1104     private void checkHashAlgorithm(File virtApexEtcDir) throws Exception {
1105         List<String> images =
1106                 Arrays.asList(
1107                         // kernel image (contains descriptors from initrd(s) as well)
1108                         "/fs/microdroid_kernel",
1109                         // vbmeta partition (contains descriptors from vendor/system images)
1110                         "/fs/microdroid_vbmeta.img");
1111 
1112         for (String path : images) {
1113             String info = avbInfo(virtApexEtcDir + path);
1114             Pattern pattern = Pattern.compile("Hash Algorithm:[ ]*(sha1|sha256)");
1115             Matcher m = pattern.matcher(info);
1116             while (m.find()) {
1117                 assertThat(m.group(1)).isEqualTo("sha256");
1118             }
1119         }
1120     }
1121 
1122     @Test
1123     public void testDeviceAssignment() throws Exception {
1124         // Check for preconditions
1125         assumeVfioPlatformSupported();
1126 
1127         List<AssignableDevice> devices = getAssignableDevices();
1128         assumeFalse("no assignable devices", devices.isEmpty());
1129 
1130         String dtSysfsPath = "/proc/device-tree/";
1131 
1132         // Try assign devices one by one
1133         for (AssignableDevice device : devices) {
1134             launchWithDeviceAssignment(device.node);
1135 
1136             String dtPath =
1137                     new CommandRunner(mMicrodroidDevice)
1138                             .run("cat", dtSysfsPath + "__symbols__/" + device.dtbo_label);
1139             assertThat(dtPath).isNotEmpty();
1140 
1141             String resolvedDtPath =
1142                     new CommandRunner(mMicrodroidDevice)
1143                             .run("readlink", "-e", dtSysfsPath + dtPath);
1144             assertThat(resolvedDtPath).isNotEmpty();
1145 
1146             String allDevices =
1147                     new CommandRunner(mMicrodroidDevice)
1148                             .run("readlink", "-e", "/sys/bus/platform/devices/*/of_node");
1149             assertThat(allDevices.split("\n")).asList().contains(resolvedDtPath);
1150 
1151             getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
1152             mMicrodroidDevice = null;
1153         }
1154     }
1155 
1156     private void launchWithDeviceAssignment(String device) throws Exception {
1157         Objects.requireNonNull(device);
1158         final String configPath = "assets/vm_config.json";
1159 
1160         mMicrodroidDevice =
1161                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
1162                         .debugLevel("full")
1163                         .memoryMib(minMemorySize())
1164                         .cpuTopology("match_host")
1165                         .protectedVm(mProtectedVm)
1166                         .gki(mGki)
1167                         .addAssignableDevice(device)
1168                         .build(getAndroidDevice());
1169 
1170         assertThat(mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT)).isTrue();
1171         assertThat(mMicrodroidDevice.enableAdbRoot()).isTrue();
1172     }
1173 
1174     @Test
1175     public void testGkiVersions() throws Exception {
1176         for (String gki : getSupportedGKIVersions()) {
1177             assertTrue(
1178                     "Unknown gki \"" + gki + "\". Supported gkis: " + SUPPORTED_GKI_VERSIONS,
1179                     SUPPORTED_GKI_VERSIONS.contains(gki));
1180         }
1181     }
1182 
1183     @Test
1184     public void testHugePages() throws Exception {
1185         ITestDevice device = getDevice();
1186         boolean disableRoot = !device.isAdbRoot();
1187         CommandRunner android = new CommandRunner(device);
1188 
1189         final String SHMEM_ENABLED_PATH = "/sys/kernel/mm/transparent_hugepage/shmem_enabled";
1190         String thpShmemStr = android.run("cat", SHMEM_ENABLED_PATH);
1191 
1192         assumeFalse("shmem already enabled, skip", thpShmemStr.contains("[advise]"));
1193         assumeTrue("Unsupported shmem, skip", thpShmemStr.contains("[never]"));
1194 
1195         device.enableAdbRoot();
1196         assumeTrue("adb root is not enabled", device.isAdbRoot());
1197         android.run("echo advise > " + SHMEM_ENABLED_PATH);
1198 
1199         final String configPath = "assets/vm_config.json";
1200         mMicrodroidDevice =
1201                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
1202                         .debugLevel("full")
1203                         .memoryMib(minMemorySize())
1204                         .cpuTopology("match_host")
1205                         .protectedVm(mProtectedVm)
1206                         .gki(mGki)
1207                         .hugePages(true)
1208                         .build(getAndroidDevice());
1209         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
1210 
1211         android.run("echo never >" + SHMEM_ENABLED_PATH);
1212         if (disableRoot) {
1213             device.disableAdbRoot();
1214         }
1215     }
1216 
1217     @Before
1218     public void setUp() throws Exception {
1219         assumeDeviceIsCapable(getDevice());
1220         mMetricPrefix = getMetricPrefix() + "microdroid/";
1221         mMicrodroidDevice = null;
1222 
1223         prepareVirtualizationTestSetup(getDevice());
1224 
1225         getDevice().installPackage(findTestFile(APK_NAME), /* reinstall= */ false);
1226 
1227         // Skip test if given device doesn't support protected or non-protected VM.
1228         assumeTrue(
1229                 "Microdroid is not supported for specific VM protection type",
1230                 getAndroidDevice().supportsMicrodroid(mProtectedVm));
1231 
1232         if (mGki != null) {
1233             assumeTrue(
1234                     "GKI version \"" + mGki + "\" is not supported on this device",
1235                     getSupportedGKIVersions().contains(mGki));
1236         }
1237 
1238         mOs = (mGki != null) ? "microdroid_gki-" + mGki : "microdroid";
1239 
1240         new CommandRunner(getDevice())
1241                 .tryRun(
1242                         "pm",
1243                         "grant",
1244                         SHELL_PACKAGE_NAME,
1245                         "android.permission.USE_CUSTOM_VIRTUAL_MACHINE");
1246     }
1247 
1248     @After
1249     public void shutdown() throws Exception {
1250         if (mMicrodroidDevice != null) {
1251             getAndroidDevice().shutdownMicrodroid(mMicrodroidDevice);
1252         }
1253 
1254         cleanUpVirtualizationTestSetup(getDevice());
1255 
1256         archiveLogThenDelete(
1257                 mTestLogs, getDevice(), LOG_PATH, "vm.log-" + mTestName.getMethodName());
1258 
1259         getDevice().uninstallPackage(PACKAGE_NAME);
1260     }
1261 
1262     private void assumeProtectedVm() {
1263         assumeTrue("This test is only for protected VM.", mProtectedVm);
1264     }
1265 
1266     private void assumeNonProtectedVm() {
1267         assumeFalse("This test is only for non-protected VM.", mProtectedVm);
1268     }
1269 
1270     private void assumeVfioPlatformSupported() throws Exception {
1271         TestDevice device = getAndroidDevice();
1272         assumeTrue(
1273                 "Test skipped because VFIO platform is not supported.",
1274                 device.doesFileExist("/dev/vfio/vfio")
1275                         && device.doesFileExist("/sys/bus/platform/drivers/vfio-platform"));
1276     }
1277 
1278     private void assumeUpdatableVmSupported() throws DeviceNotAvailableException {
1279         assumeTrue(
1280                 "This test is only applicable if if Updatable VMs are supported",
1281                 isUpdatableVmSupported());
1282     }
1283 
1284     private TestDevice getAndroidDevice() {
1285         TestDevice androidDevice = (TestDevice) getDevice();
1286         assertThat(androidDevice).isNotNull();
1287         return androidDevice;
1288     }
1289 
1290     // The TradeFed Dockerfile sets LD_LIBRARY_PATH to a directory with an older libc++.so, which
1291     // breaks binaries that are linked against a newer libc++.so. Binaries commonly use DT_RUNPATH
1292     // to find an adjacent libc++.so (e.g. `$ORIGIN/../lib64`), but LD_LIBRARY_PATH overrides
1293     // DT_RUNPATH, so clear LD_LIBRARY_PATH. See b/332593805 and b/333782216.
1294     private static RunUtil createRunUtil() {
1295         RunUtil runUtil = new RunUtil();
1296         runUtil.unsetEnvVariable("LD_LIBRARY_PATH");
1297         return runUtil;
1298     }
1299 }
1300