1 /* 2 * Copyright (C) 2019 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.stagedinstall.host; 18 19 import static com.android.cts.shim.lib.ShimPackage.PRIVILEGED_SHIM_PACKAGE_NAME; 20 import static com.android.cts.shim.lib.ShimPackage.SHIM_APEX_PACKAGE_NAME; 21 import static com.android.cts.shim.lib.ShimPackage.SHIM_PACKAGE_NAME; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.junit.Assume.assumeTrue; 27 28 import android.cts.install.lib.host.InstallUtilsHost; 29 import android.platform.test.annotations.LargeTest; 30 31 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 32 import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; 33 import com.android.tradefed.util.AaptParser; 34 import com.android.tradefed.util.CommandResult; 35 import com.android.tradefed.util.CommandStatus; 36 import com.android.tradefed.util.FileUtil; 37 import com.android.tradefed.util.RunUtil; 38 import com.android.tradefed.util.ZipUtil; 39 40 import org.junit.After; 41 import org.junit.Before; 42 import org.junit.Test; 43 import org.junit.runner.RunWith; 44 45 import java.io.File; 46 import java.io.IOException; 47 import java.util.ArrayList; 48 import java.util.Arrays; 49 import java.util.Enumeration; 50 import java.util.List; 51 import java.util.stream.Collectors; 52 import java.util.zip.ZipEntry; 53 import java.util.zip.ZipFile; 54 55 /** 56 * Tests to validate that only what is considered a correct shim apex can be installed. 57 * 58 * <p>Shim apex is considered correct iff: 59 * <ul> 60 * <li>It doesn't have any pre or post install hooks.</li> 61 * <li>It's {@code apex_payload.img} contains only a regular text file called 62 * {@code hash.txt}.</li> 63 * <li>It's {@code sha512} hash is whitelisted in the {@code hash.txt} of pre-installed on the 64 * {@code /system} partition shim apex.</li> 65 * </ul> 66 */ 67 @RunWith(DeviceJUnit4ClassRunner.class) 68 public class ApexShimValidationTest extends BaseHostJUnit4Test { 69 70 private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); 71 72 private static final String SHIM_APK_CODE_PATH_PREFIX = "/apex/" + SHIM_APEX_PACKAGE_NAME + "/"; 73 private static final String STAGED_INSTALL_TEST_FILE_NAME = "StagedInstallTest.apk"; 74 private static final String APEX_FILE_SUFFIX = ".apex"; 75 private static final String DEAPEXER_ZIP_FILE_NAME = "deapexer.zip"; 76 private static final String DEAPEXING_FOLDER_NAME = "deapexing_"; 77 private static final String DEAPEXER_FILE_NAME = "deapexer"; 78 private static final String DEBUGFS_STATIC_FILE_NAME = "debugfs_static"; 79 private static final String BLKID_FILE_NAME = "blkid"; 80 private static final String FSCKEROFS_FILE_NAME = "fsck.erofs"; 81 82 private static final long DEFAULT_RUN_TIMEOUT_MS = 30 * 1000L; 83 84 private static final List<String> ALLOWED_SHIM_PACKAGE_NAMES = Arrays.asList( 85 SHIM_PACKAGE_NAME, PRIVILEGED_SHIM_PACKAGE_NAME); 86 87 private File mDeapexingDir; 88 private File mDeapexerZip; 89 private File mAllApexesZip; 90 91 /** 92 * Runs the given phase of a test by calling into the device. 93 * Throws an exception if the test phase fails. 94 * <p> 95 * For example, <code>runPhase("testInstallStagedApkCommit");</code> 96 */ runPhase(String phase)97 private void runPhase(String phase) throws Exception { 98 assertThat(runDeviceTests("com.android.tests.stagedinstall", 99 "com.android.tests.stagedinstall.ApexShimValidationTest", 100 phase)).isTrue(); 101 } 102 cleanUp()103 private void cleanUp() throws Exception { 104 assertThat(runDeviceTests("com.android.tests.stagedinstall", 105 "com.android.tests.stagedinstall.StagedInstallTest", 106 "cleanUp")).isTrue(); 107 if (mDeapexingDir != null) { 108 FileUtil.recursiveDelete(mDeapexingDir); 109 } 110 } 111 112 @Before setUp()113 public void setUp() throws Exception { 114 assumeTrue("Device doesn't support updating APEX", mHostUtils.isApexUpdateSupported()); 115 cleanUp(); 116 mDeapexerZip = getTestInformation().getDependencyFile(DEAPEXER_ZIP_FILE_NAME, false); 117 mAllApexesZip = getTestInformation().getDependencyFile(STAGED_INSTALL_TEST_FILE_NAME, 118 false); 119 } 120 121 @After tearDown()122 public void tearDown() throws Exception { 123 cleanUp(); 124 } 125 126 @Test testShimApexIsPreInstalled()127 public void testShimApexIsPreInstalled() throws Exception { 128 boolean isShimApexPreInstalled = 129 getDevice().getActiveApexes().stream().anyMatch( 130 apex -> apex.name.equals(SHIM_APEX_PACKAGE_NAME)); 131 assertWithMessage("Shim APEX is not pre-installed").that( 132 isShimApexPreInstalled).isTrue(); 133 } 134 135 @Test testPackageNameOfShimApkIsAllowed()136 public void testPackageNameOfShimApkIsAllowed() throws Exception { 137 final List<String> shimPackages = getDevice().getAppPackageInfos().stream() 138 .filter(pkg -> pkg.getCodePath().startsWith(SHIM_APK_CODE_PATH_PREFIX)) 139 .map(pkg -> pkg.getPackageName()).collect(Collectors.toList()); 140 assertWithMessage("Packages in the shim apex are not allowed") 141 .that(shimPackages).containsExactlyElementsIn(ALLOWED_SHIM_PACKAGE_NAMES); 142 } 143 144 /** 145 * Deapexing all the apexes bundled in the staged install test. Verifies the package name of 146 * shim apk in the apex. 147 */ 148 @Test testPackageNameOfShimApkInAllBundledApexesIsAllowed()149 public void testPackageNameOfShimApkInAllBundledApexesIsAllowed() throws Exception { 150 mDeapexingDir = FileUtil.createTempDir(DEAPEXING_FOLDER_NAME); 151 final File deapexer = extractDeapexer(mDeapexingDir); 152 final File debugfs = new File(mDeapexingDir, DEBUGFS_STATIC_FILE_NAME); 153 final File blkid = new File(mDeapexingDir, BLKID_FILE_NAME); 154 final File fsckerofs = new File(mDeapexingDir, FSCKEROFS_FILE_NAME); 155 final List<File> apexes = extractApexes(mDeapexingDir); 156 for (File apex : apexes) { 157 final File outDir = new File(apex.getParent(), apex.getName().substring( 158 0, apex.getName().length() - APEX_FILE_SUFFIX.length())); 159 try { 160 runDeapexerExtract(deapexer, debugfs, blkid, fsckerofs, apex, outDir); 161 final List<File> apkFiles = FileUtil.findFiles(outDir, ".+\\.apk").stream() 162 .map(str -> new File(str)).collect(Collectors.toList()); 163 for (File apkFile : apkFiles) { 164 final AaptParser parser = AaptParser.parse(apkFile); 165 assertWithMessage("Apk " + apkFile + " in apex " + apex + " is not valid") 166 .that(parser).isNotNull(); 167 assertWithMessage("Apk " + apkFile + " in apex " + apex 168 + " has incorrect package name " + parser.getPackageName()) 169 .that(ALLOWED_SHIM_PACKAGE_NAMES).contains(parser.getPackageName()); 170 } 171 } finally { 172 FileUtil.recursiveDelete(outDir); 173 } 174 } 175 } 176 177 @Test 178 @LargeTest testRejectsApexWithAdditionalFile()179 public void testRejectsApexWithAdditionalFile() throws Exception { 180 runPhase("testRejectsApexWithAdditionalFile_Commit"); 181 getDevice().reboot(); 182 runPhase("testInstallRejected_VerifyPostReboot"); 183 } 184 185 @Test 186 @LargeTest testRejectsApexWithAdditionalFolder()187 public void testRejectsApexWithAdditionalFolder() throws Exception { 188 runPhase("testRejectsApexWithAdditionalFolder_Commit"); 189 getDevice().reboot(); 190 runPhase("testInstallRejected_VerifyPostReboot"); 191 } 192 193 @Test 194 @LargeTest testRejectsApexWithPostInstallHook()195 public void testRejectsApexWithPostInstallHook() throws Exception { 196 runPhase("testRejectsApexWithPostInstallHook_Commit"); 197 getDevice().reboot(); 198 runPhase("testInstallRejected_VerifyPostReboot"); 199 } 200 201 @Test 202 @LargeTest testRejectsApexWithPreInstallHook()203 public void testRejectsApexWithPreInstallHook() throws Exception { 204 runPhase("testRejectsApexWithPreInstallHook_Commit"); 205 getDevice().reboot(); 206 runPhase("testInstallRejected_VerifyPostReboot"); 207 } 208 209 @Test 210 @LargeTest testRejectsApexWrongSHA()211 public void testRejectsApexWrongSHA() throws Exception { 212 runPhase("testRejectsApexWrongSHA_Commit"); 213 getDevice().reboot(); 214 runPhase("testInstallRejected_VerifyPostReboot"); 215 } 216 217 @Test testRejectsApexWithAdditionalFile_rebootless()218 public void testRejectsApexWithAdditionalFile_rebootless() throws Exception { 219 runPhase("testRejectsApexWithAdditionalFile_rebootless"); 220 } 221 222 @Test testRejectsApexWithAdditionalFolder_rebootless()223 public void testRejectsApexWithAdditionalFolder_rebootless() throws Exception { 224 runPhase("testRejectsApexWithAdditionalFolder_rebootless"); 225 } 226 227 @Test testRejectsApexWithPostInstallHook_rebootless()228 public void testRejectsApexWithPostInstallHook_rebootless() throws Exception { 229 runPhase("testRejectsApexWithPostInstallHook_rebootless"); 230 } 231 232 @Test testRejectsApexWithPreInstallHook_rebootless()233 public void testRejectsApexWithPreInstallHook_rebootless() throws Exception { 234 runPhase("testRejectsApexWithPreInstallHook_rebootless"); 235 } 236 237 @Test testRejectsApexWrongSHA_rebootless()238 public void testRejectsApexWrongSHA_rebootless() throws Exception { 239 runPhase("testRejectsApexWrongSHA_rebootless"); 240 } 241 242 /** 243 * Extracts {@link #DEAPEXER_ZIP_FILE_NAME} into the destination folder. Updates executable 244 * attribute for the binaries of deapexer and debugfs_static. 245 * 246 * @param destDir A tmp folder for the deapexing. 247 * @return the deapexer file. 248 */ extractDeapexer(File destDir)249 private File extractDeapexer(File destDir) throws IOException { 250 ZipUtil.extractZip(new ZipFile(mDeapexerZip), destDir); 251 final File deapexer = FileUtil.findFile(destDir, DEAPEXER_FILE_NAME); 252 assertWithMessage("Can't find " + DEAPEXER_FILE_NAME + " binary file") 253 .that(deapexer).isNotNull(); 254 deapexer.setExecutable(true); 255 final File debugfs = FileUtil.findFile(destDir, DEBUGFS_STATIC_FILE_NAME); 256 assertWithMessage("Can't find " + DEBUGFS_STATIC_FILE_NAME + " binary file") 257 .that(debugfs).isNotNull(); 258 debugfs.setExecutable(true); 259 final File blkid = FileUtil.findFile(destDir, BLKID_FILE_NAME); 260 assertWithMessage("Can't find " + BLKID_FILE_NAME + " binary file") 261 .that(debugfs).isNotNull(); 262 blkid.setExecutable(true); 263 final File fsckerofs = FileUtil.findFile(destDir, FSCKEROFS_FILE_NAME); 264 assertWithMessage("Can't find " + FSCKEROFS_FILE_NAME + " binary file") 265 .that(debugfs).isNotNull(); 266 fsckerofs.setExecutable(true); 267 return deapexer; 268 } 269 270 /** 271 * Extracts all bundled apex files from {@link #STAGED_INSTALL_TEST_FILE_NAME} into the 272 * destination folder. 273 * 274 * @param destDir A tmp folder for the deapexing. 275 * @return A list of apex files. 276 */ extractApexes(File destDir)277 private List<File> extractApexes(File destDir) throws IOException { 278 final List<File> apexes = new ArrayList<>(); 279 final ZipFile apexZip = new ZipFile(mAllApexesZip); 280 final Enumeration<? extends ZipEntry> entries = apexZip.entries(); 281 while (entries.hasMoreElements()) { 282 final ZipEntry entry = entries.nextElement(); 283 if (entry.isDirectory() || !entry.getName().matches( 284 SHIM_APEX_PACKAGE_NAME + ".*\\" + APEX_FILE_SUFFIX)) { 285 continue; 286 } 287 final File apex = new File(destDir, entry.getName()); 288 apex.getParentFile().mkdirs(); 289 FileUtil.writeToFile(apexZip.getInputStream(entry), apex); 290 apexes.add(apex); 291 } 292 assertWithMessage("No apex file in the " + mAllApexesZip) 293 .that(apexes).isNotEmpty(); 294 return apexes; 295 } 296 297 /** 298 * Extracts all contents of the apex file into the {@code outDir} using the deapexer. 299 * 300 * @param deapexer The deapexer file. 301 * @param debugfs The debugfs file. 302 * @param apex The apex file to be extracted. 303 * @param outDir The out folder. 304 */ runDeapexerExtract(File deapexer, File debugfs, File blkid, File fsckerofs, File apex, File outDir)305 private void runDeapexerExtract(File deapexer, File debugfs, File blkid, File fsckerofs, 306 File apex, File outDir) { 307 final RunUtil runUtil = new RunUtil(); 308 final String os = System.getProperty("os.name").toLowerCase(); 309 final boolean isMacOs = (os.startsWith("mac") || os.startsWith("darwin")); 310 if (isMacOs) { 311 runUtil.setEnvVariable("DYLD_LIBRARY_PATH", mDeapexingDir.getAbsolutePath()); 312 } else { 313 runUtil.setEnvVariable("LD_LIBRARY_PATH", mDeapexingDir.getAbsolutePath()); 314 } 315 final CommandResult result = runUtil.runTimedCmd(DEFAULT_RUN_TIMEOUT_MS, 316 deapexer.getAbsolutePath(), 317 "--debugfs_path", 318 debugfs.getAbsolutePath(), 319 "--blkid_path", 320 blkid.getAbsolutePath(), 321 "--fsckerofs_path", 322 fsckerofs.getAbsolutePath(), 323 "extract", 324 apex.getAbsolutePath(), 325 outDir.getAbsolutePath()); 326 assertWithMessage("deapexer(" + apex + ") failed: " + result) 327 .that(result.getStatus()).isEqualTo(CommandStatus.SUCCESS); 328 assertWithMessage("deapexer(" + apex + ") failed: no outDir created") 329 .that(outDir.exists()).isTrue(); 330 } 331 } 332