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