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.tests.odsign;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assume.assumeTrue;
23 
24 import android.cts.install.lib.host.InstallUtilsHost;
25 
26 import com.android.tradefed.device.ITestDevice.ApexInfo;
27 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
28 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
29 import com.android.tradefed.testtype.junit4.DeviceTestRunOptions;
30 import com.android.tradefed.util.CommandResult;
31 
32 import org.junit.After;
33 import org.junit.Before;
34 import org.junit.FixMethodOrder;
35 import org.junit.Test;
36 import org.junit.runner.RunWith;
37 import org.junit.runners.MethodSorters;
38 
39 import java.time.Duration;
40 import java.util.Arrays;
41 import java.util.HashSet;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 
45 @RunWith(DeviceJUnit4ClassRunner.class)
46 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
47 public class OnDeviceSigningHostTest extends BaseHostJUnit4Test {
48 
49     private static final String APEX_FILENAME = "test_com.android.art.apex";
50 
51     private static final String ART_APEX_DALVIK_CACHE_DIRNAME =
52             "/data/misc/apexdata/com.android.art/dalvik-cache";
53 
54     private static final String ODREFRESH_COMPILATION_LOG =
55             "/data/misc/odrefresh/compilation-log.txt";
56 
57     private final String[] APP_ARTIFACT_EXTENSIONS = new String[] {".art", ".odex", ".vdex"};
58 
59     private final String[] BCP_ARTIFACT_EXTENSIONS = new String[] {".art", ".oat", ".vdex"};
60 
61     private static final String TEST_APP_PACKAGE_NAME = "com.android.tests.odsign";
62     private static final String TEST_APP_APK = "odsign_e2e_test_app.apk";
63 
64     private final InstallUtilsHost mInstallUtils = new InstallUtilsHost(this);
65 
66     private static final Duration BOOT_COMPLETE_TIMEOUT = Duration.ofMinutes(2);
67 
68     @Before
setUp()69     public void setUp() throws Exception {
70         assumeTrue("Updating APEX is not supported", mInstallUtils.isApexUpdateSupported());
71         installPackage(TEST_APP_APK);
72         mInstallUtils.installApexes(APEX_FILENAME);
73         removeCompilationLogToAvoidBackoff();
74         reboot();
75     }
76 
77     @After
cleanup()78     public void cleanup() throws Exception {
79         ApexInfo apex = mInstallUtils.getApexInfo(mInstallUtils.getTestFile(APEX_FILENAME));
80         getDevice().uninstallPackage(apex.name);
81         removeCompilationLogToAvoidBackoff();
82         reboot();
83     }
84 
85     @Test
verifyArtUpgradeSignsFiles()86     public void verifyArtUpgradeSignsFiles() throws Exception {
87         DeviceTestRunOptions options = new DeviceTestRunOptions(TEST_APP_PACKAGE_NAME);
88         options.setTestClassName(TEST_APP_PACKAGE_NAME + ".ArtifactsSignedTest");
89         options.setTestMethodName("testArtArtifactsHaveFsverity");
90         runDeviceTests(options);
91     }
92 
93     @Test
verifyArtUpgradeGeneratesRequiredArtifacts()94     public void verifyArtUpgradeGeneratesRequiredArtifacts() throws Exception {
95         DeviceTestRunOptions options = new DeviceTestRunOptions(TEST_APP_PACKAGE_NAME);
96         options.setTestClassName(TEST_APP_PACKAGE_NAME + ".ArtifactsSignedTest");
97         options.setTestMethodName("testGeneratesRequiredArtArtifacts");
98         runDeviceTests(options);
99     }
100 
getMappedArtifacts(String pid, String grepPattern)101     private Set<String> getMappedArtifacts(String pid, String grepPattern) throws Exception {
102         final String grepCommand = String.format("grep \"%s\" /proc/%s/maps", grepPattern, pid);
103         CommandResult result = getDevice().executeShellV2Command(grepCommand);
104         assertTrue(result.toString(), result.getExitCode() == 0);
105         Set<String> mappedFiles = new HashSet<>();
106         for (String line : result.getStdout().split("\\R")) {
107             int start = line.indexOf(ART_APEX_DALVIK_CACHE_DIRNAME);
108             if (line.contains("[")) {
109                 continue; // ignore anonymously mapped sections which are quoted in square braces.
110             }
111             mappedFiles.add(line.substring(start));
112         }
113         return mappedFiles;
114     }
115 
getSystemServerClasspath()116     private String[] getSystemServerClasspath() throws Exception {
117         String systemServerClasspath =
118                 getDevice().executeShellCommand("echo $SYSTEMSERVERCLASSPATH");
119         return systemServerClasspath.split(":");
120     }
121 
getSystemServerIsa(String mappedArtifact)122     private String getSystemServerIsa(String mappedArtifact) {
123         // Artifact path for system server artifacts has the form:
124         //    ART_APEX_DALVIK_CACHE_DIRNAME + "/<arch>/system@framework@some.jar@classes.odex"
125         // `mappedArtifacts` may include other artifacts, such as boot-framework.oat that are not
126         // prefixed by the architecture.
127         String[] pathComponents = mappedArtifact.split("/");
128         return pathComponents[pathComponents.length - 2];
129     }
130 
verifySystemServerLoadedArtifacts()131     private void verifySystemServerLoadedArtifacts() throws Exception {
132         String[] classpathElements = getSystemServerClasspath();
133         assertTrue("SYSTEMSERVERCLASSPATH is empty", classpathElements.length > 0);
134 
135         String systemServerPid = getDevice().executeShellCommand("pgrep system_server");
136         assertTrue(systemServerPid != null);
137 
138         // system_server artifacts are in the APEX data dalvik cache and names all contain
139         // the word "@classes". Look for mapped files that match this pattern in the proc map for
140         // system_server.
141         final String grepPattern = ART_APEX_DALVIK_CACHE_DIRNAME + ".*@classes";
142         final Set<String> mappedArtifacts = getMappedArtifacts(systemServerPid, grepPattern);
143         assertTrue(
144                 "No mapped artifacts under " + ART_APEX_DALVIK_CACHE_DIRNAME,
145                 mappedArtifacts.size() > 0);
146         final String isa = getSystemServerIsa(mappedArtifacts.iterator().next());
147         final String isaCacheDirectory = String.format("%s/%s", ART_APEX_DALVIK_CACHE_DIRNAME, isa);
148 
149         // Check the non-APEX components in the system_server classpath have mapped artifacts.
150         for (String element : classpathElements) {
151             // Skip system_server classpath elements from APEXes as these are not currently
152             // compiled.
153             if (element.startsWith("/apex")) {
154                 continue;
155             }
156             String escapedPath = element.substring(1).replace('/', '@');
157             for (String extension : APP_ARTIFACT_EXTENSIONS) {
158                 final String fullArtifactPath =
159                         String.format("%s/%s@classes%s", isaCacheDirectory, escapedPath, extension);
160                 assertTrue(
161                         "Missing " + fullArtifactPath, mappedArtifacts.contains(fullArtifactPath));
162             }
163         }
164 
165         for (String mappedArtifact : mappedArtifacts) {
166             // Check no APEX JAR artifacts are mapped for system_server since if there
167             // are, then the policy around not compiling APEX jars for system_server has
168             // changed and this test needs updating here and in the system_server classpath
169             // check above.
170             assertTrue(
171                     "Unexpected mapped artifact: " + mappedArtifact,
172                     mappedArtifact.contains("/apex"));
173 
174             // Check the mapped artifact has a .art, .odex or .vdex extension.
175             final boolean knownArtifactKind =
176                     Arrays.stream(APP_ARTIFACT_EXTENSIONS)
177                             .anyMatch(e -> mappedArtifact.endsWith(e));
178             assertTrue("Unknown artifact kind: " + mappedArtifact, knownArtifactKind);
179         }
180     }
181 
verifyZygoteLoadedArtifacts(String zygoteName, String zygotePid)182     private void verifyZygoteLoadedArtifacts(String zygoteName, String zygotePid) throws Exception {
183         final String bootExtensionName = "boot-framework";
184         final Set<String> mappedArtifacts = getMappedArtifacts(zygotePid, bootExtensionName);
185 
186         assertTrue("Expect 3 boot-framework artifacts", mappedArtifacts.size() == 3);
187 
188         String allArtifacts = mappedArtifacts.stream().collect(Collectors.joining(","));
189         for (String extension : BCP_ARTIFACT_EXTENSIONS) {
190             final String artifact = bootExtensionName + extension;
191             final boolean found = mappedArtifacts.stream().anyMatch(a -> a.endsWith(artifact));
192             assertTrue(zygoteName + " " + artifact + " not found: '" + allArtifacts + "'", found);
193         }
194     }
195 
verifyZygotesLoadedArtifacts()196     private void verifyZygotesLoadedArtifacts() throws Exception {
197         // There are potentially two zygote processes "zygote" and "zygote64". These are
198         // instances 32-bit and 64-bit unspecialized app_process processes.
199         // (frameworks/base/cmds/app_process).
200         int zygoteCount = 0;
201         for (String zygoteName : new String[] {"zygote", "zygote64"}) {
202             final CommandResult pgrepResult =
203                     getDevice().executeShellV2Command("pgrep " + zygoteName);
204             if (pgrepResult.getExitCode() != 0) {
205                 continue;
206             }
207             final String zygotePid = pgrepResult.getStdout();
208             verifyZygoteLoadedArtifacts(zygoteName, zygotePid);
209             zygoteCount += 1;
210         }
211         assertTrue("No zygote processes found", zygoteCount > 0);
212     }
213 
214     @Test
verifyGeneratedArtifactsLoaded()215     public void verifyGeneratedArtifactsLoaded() throws Exception {
216         // Checking zygote and system_server need the device have adb root to walk process maps.
217         final boolean adbEnabled = getDevice().enableAdbRoot();
218         assertTrue("ADB root failed and required to get process maps", adbEnabled);
219 
220         // Check there is a compilation log, we expect compilation to have occurred.
221         assertTrue("Compilation log not found", haveCompilationLog());
222 
223         // Check both zygote and system_server processes to see that they have loaded the
224         // artifacts compiled and signed by odrefresh and odsign. We check both here rather than
225         // having a separate test because the device reboots between each @Test method and
226         // that is an expensive use of time.
227         verifyZygotesLoadedArtifacts();
228         verifySystemServerLoadedArtifacts();
229     }
230 
231     @Test
verifyGeneratedArtifactsLoadedForSamegradeUpdate()232     public void verifyGeneratedArtifactsLoadedForSamegradeUpdate() throws Exception {
233         // Install the same APEX effecting a samegrade update. The setUp method has installed it
234         // before us.
235         mInstallUtils.installApexes(APEX_FILENAME);
236         reboot();
237 
238         final boolean adbEnabled = getDevice().enableAdbRoot();
239         assertTrue("ADB root failed and required to get odrefresh compilation log", adbEnabled);
240 
241         // Check that odrefresh logged a compilation attempt due to samegrade ART APEX install.
242         String[] logLines = getDevice().pullFileContents(ODREFRESH_COMPILATION_LOG).split("\n");
243         assertTrue(
244                 "Expected 3 lines in " + ODREFRESH_COMPILATION_LOG + ", found " + logLines.length,
245                 logLines.length == 3);
246 
247         // Check that the compilation log entries are reasonable, ie times move forward.
248         // The first line of the log is the log format version number.
249         String[] firstUpdateEntry = logLines[1].split(" ");
250         String[] secondUpdateEntry = logLines[2].split(" ");
251         final int LOG_ENTRY_FIELDS = 5;
252         assertTrue(
253                 "Unexpected number of fields: " + firstUpdateEntry.length + " != " +
254                 LOG_ENTRY_FIELDS,
255                 firstUpdateEntry.length == LOG_ENTRY_FIELDS);
256         assertTrue(firstUpdateEntry.length == secondUpdateEntry.length);
257 
258         final int LAST_UPDATE_MILLIS_INDEX = 1;
259         final int COMPILATION_TIME_INDEX = 3;
260         for (int i = 0; i < firstUpdateEntry.length; ++i) {
261             final long firstField = Long.parseLong(firstUpdateEntry[i]);
262             final long secondField = Long.parseLong(secondUpdateEntry[i]);
263             if (i == LAST_UPDATE_MILLIS_INDEX) {
264                 // The second APEX lastUpdateMillis should be after the first, but a clock
265                 // adjustment might reverse the order so we can't assert this (b/194365586).
266                 assertTrue(
267                         "Last update times are expected to differ, but they are equal " +
268                         firstField + " == " + secondField,
269                         firstField != secondField);
270             } else if (i == COMPILATION_TIME_INDEX) {
271                 // The second compilation time should be after the first compilation time, but
272                 // a clock adjustment might reverse the order so we can't assert this
273                 // (b/194365586).
274                 assertTrue(
275                         "Compilation times are expected to differ, but they are equal " +
276                         firstField + " == " + secondField,
277                         firstField != secondField);
278             } else {
279                 // The remaining fields should be the same, ie trigger for compilation.
280                 assertTrue(
281                         "Compilation entries differ for position " + i + ": " +
282                         firstField + " != " + secondField,
283                         firstField == secondField);
284             }
285         }
286 
287         verifyGeneratedArtifactsLoaded();
288     }
289 
haveCompilationLog()290     private boolean haveCompilationLog() throws Exception {
291         CommandResult result =
292                 getDevice().executeShellV2Command("stat " + ODREFRESH_COMPILATION_LOG);
293         return result.getExitCode() == 0;
294     }
295 
removeCompilationLogToAvoidBackoff()296     private void removeCompilationLogToAvoidBackoff() throws Exception {
297         getDevice().executeShellCommand("rm -f " + ODREFRESH_COMPILATION_LOG);
298     }
299 
reboot()300     private void reboot() throws Exception {
301         getDevice().reboot();
302         boolean success = getDevice().waitForBootComplete(BOOT_COMPLETE_TIMEOUT.toMillis());
303         assertWithMessage("Device didn't boot in %s", BOOT_COMPLETE_TIMEOUT).that(success).isTrue();
304     }
305 }
306