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 android.content.pm.cts; 18 19 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.checkIncrementalDeliveryFeature; 20 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.installNonIncremental; 21 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.isAppInstalledForUser; 22 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.setDeviceProperty; 23 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.setSystemProperty; 24 import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.uninstallPackageSilently; 25 26 import static org.hamcrest.core.IsInstanceOf.instanceOf; 27 import static org.junit.Assert.assertFalse; 28 import static org.junit.Assert.assertTrue; 29 import static org.junit.Assume.assumeTrue; 30 31 import android.app.ActivityManager; 32 import android.app.UiAutomation; 33 import android.content.Context; 34 import android.content.Intent; 35 import android.content.IntentFilter; 36 import android.content.pm.PackageManager; 37 import android.content.res.Resources; 38 import android.platform.test.annotations.AppModeFull; 39 import android.platform.test.annotations.AppModeNonSdkSandbox; 40 import android.util.ArrayMap; 41 42 import androidx.test.InstrumentationRegistry; 43 import androidx.test.filters.LargeTest; 44 import androidx.test.runner.AndroidJUnit4; 45 46 import com.android.compatibility.common.util.MatcherUtils; 47 import com.android.incfs.install.IBlockFilter; 48 import com.android.incfs.install.IncrementalInstallSession; 49 import com.android.incfs.install.PendingBlock; 50 51 import com.example.helloworld.lib.TestUtils; 52 53 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; 54 import org.apache.commons.compress.archivers.zip.ZipFile; 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Test; 58 import org.junit.runner.RunWith; 59 60 import java.io.IOException; 61 import java.nio.charset.StandardCharsets; 62 import java.nio.file.Paths; 63 import java.util.ArrayList; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.concurrent.Executors; 67 import java.util.concurrent.TimeUnit; 68 import java.util.concurrent.TimeoutException; 69 import java.util.concurrent.atomic.AtomicBoolean; 70 import java.util.concurrent.atomic.AtomicInteger; 71 72 @RunWith(AndroidJUnit4.class) 73 @AppModeFull 74 @AppModeNonSdkSandbox 75 @LargeTest 76 public class ResourcesHardeningTest { 77 private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/"; 78 private static final String[] TEST_APKS = { 79 "HelloWorldResHardening.apk", 80 "HelloWorldResHardening_mdpi-v4.apk", 81 "HelloWorldResHardening_hdpi-v4.apk" 82 }; 83 84 private static final String RES_TABLE_PATH = "resources.arsc"; 85 private static final int INCFS_BLOCK_SIZE = 4096; 86 87 private final Map<String, List<RestrictedBlockRange>> mRestrictedRanges = new ArrayMap<>(); 88 89 @Before onBefore()90 public void onBefore() throws Exception { 91 // TODO(b/280484615): remove once test is deflaked. 92 assumeTrue(false); 93 checkIncrementalDeliveryFeature(); 94 95 setDeviceProperty("incfs_default_timeouts", "1:1:1"); 96 setDeviceProperty("known_digesters_list", TestUtils.TEST_APP_PACKAGE); 97 setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", 98 "1"); 99 setSystemProperty("debug.incremental.enable_read_timeouts_after_install", "0"); 100 101 // Set up the blocks that need to be restricted in order to test resource hardening. 102 if (!mRestrictedRanges.isEmpty()) { 103 return; 104 } 105 for (final String apk : TEST_APKS) { 106 try (ZipFile zip = new ZipFile(TEST_APK_PATH + apk)) { 107 final List<RestrictedBlockRange> infos = new ArrayList<>(); 108 RestrictedBlockRange info; 109 info = restrictZipEntry(zip, RES_TABLE_PATH); 110 if (info != null) { 111 infos.add(info); 112 } 113 // Restrict only the middle block of the compiled xml to test that the whole 114 // file needs to be present just to open the xml file. 115 info = restrictOnlyMiddleBlock(restrictZipEntry(zip, TestUtils.RES_XML_PATH)); 116 if (info != null) { 117 infos.add(info); 118 } 119 // Restrict only the middle block of this file to test that the whole file does 120 // NOT need to be present just to create an input stream or fd. 121 info = restrictOnlyMiddleBlock( 122 restrictZipEntry(zip, TestUtils.RES_DRAWABLE_MDPI_PATH)); 123 if (info != null) { 124 infos.add(info); 125 } 126 // Test that FileNotFoundExceptions are thrown when the file is missing. 127 info = restrictZipEntry(zip, TestUtils.RES_DRAWABLE_HDPI_PATH); 128 if (info != null) { 129 infos.add(info); 130 } 131 assertFalse(infos.isEmpty()); 132 mRestrictedRanges.put(apk, infos); 133 } 134 } 135 } 136 137 @After onAfter()138 public void onAfter() throws Exception { 139 setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders", 140 "1"); 141 setSystemProperty("debug.incremental.enable_read_timeouts_after_install", "1"); 142 } 143 144 @LargeTest 145 @Test checkGetIdentifier()146 public void checkGetIdentifier() throws Exception { 147 testIncrementalForeignPackageResources(TestUtils::checkGetIdentifier); 148 } 149 150 @Test checkGetResourceName()151 public void checkGetResourceName() throws Exception { 152 testIncrementalForeignPackageResources(TestUtils::checkGetResourceName); 153 } 154 155 @Test checkGetString()156 public void checkGetString() throws Exception { 157 testIncrementalForeignPackageResources(TestUtils::checkGetString); 158 } 159 160 @Test checkGetStringArray()161 public void checkGetStringArray() throws Exception { 162 testIncrementalForeignPackageResources(TestUtils::checkGetStringArray); 163 } 164 165 @Test checkOpenXmlResourceParser()166 public void checkOpenXmlResourceParser() throws Exception { 167 testIncrementalForeignPackageResources(TestUtils::checkOpenXmlResourceParser); 168 } 169 170 @Test checkApplyStyle()171 public void checkApplyStyle() throws Exception { 172 testIncrementalForeignPackageResources(TestUtils::checkApplyStyle); 173 } 174 175 @Test checkXmlAttributes()176 public void checkXmlAttributes() throws Exception { 177 testIncrementalForeignPackageResources(TestUtils::checkXmlAttributes); 178 } 179 180 @Test checkOpenMissingFile()181 public void checkOpenMissingFile() throws Exception { 182 testIncrementalForeignPackageResources(TestUtils::checkOpenMissingFile); 183 } 184 185 @Test checkOpenMissingFdFile()186 public void checkOpenMissingFdFile() throws Exception { 187 testIncrementalForeignPackageResources(TestUtils::checkOpenMissingFdFile); 188 } 189 190 @Test checkOpen()191 public void checkOpen() throws Exception { 192 testIncrementalForeignPackageResources(TestUtils::checkOpen); 193 } 194 195 @Test checkOpenFd()196 public void checkOpenFd() throws Exception { 197 testIncrementalForeignPackageResources(TestUtils::checkOpenFd); 198 } 199 200 @Test checkGetIdentifierRemote()201 public void checkGetIdentifierRemote() throws Exception { 202 testIncrementalOwnPackageResources(TestUtils.TEST_GET_IDENTIFIER); 203 } 204 205 @Test checkGetResourceNameRemote()206 public void checkGetResourceNameRemote() throws Exception { 207 testIncrementalOwnPackageResources(TestUtils.TEST_GET_RESOURCE_NAME); 208 } 209 210 @Test checkGetStringRemote()211 public void checkGetStringRemote() throws Exception { 212 testIncrementalOwnPackageResources(TestUtils.TEST_GET_STRING); 213 } 214 215 @Test checkGetStringArrayRemote()216 public void checkGetStringArrayRemote() throws Exception { 217 testIncrementalOwnPackageResources(TestUtils.TEST_GET_STRING_ARRAY); 218 } 219 220 @Test checkOpenXmlResourceParserRemote()221 public void checkOpenXmlResourceParserRemote() throws Exception { 222 testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_XML); 223 } 224 225 @Test checkApplyStyleRemote()226 public void checkApplyStyleRemote() throws Exception { 227 testIncrementalOwnPackageResources(TestUtils.TEST_APPLY_STYLE); 228 } 229 230 @Test checkXmlAttributesRemote()231 public void checkXmlAttributesRemote() throws Exception { 232 testIncrementalOwnPackageResources(TestUtils.TEST_XML_ATTRIBUTES); 233 } 234 235 @Test checkOpenMissingFileRemote()236 public void checkOpenMissingFileRemote() throws Exception { 237 // If a zip entry local header is missing, libziparchive hardening causes a 238 // FileNotFoundException to be thrown regardless of whether a process queries its own 239 // resources or the resources of another package. 240 testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_MISSING, 241 false /* expectCrash */); 242 } 243 244 @Test checkOpenMissingFdFileRemote()245 public void checkOpenMissingFdFileRemote() throws Exception { 246 // If a zip entry local header is missing, libziparchive hardening causes a 247 // FileNotFoundException to be thrown regardless of whether a process queries its own 248 // resources or the resources of another package. 249 testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_FD_MISSING, 250 false /* expectCrash */); 251 } 252 253 @Test checkOpenRemote()254 public void checkOpenRemote() throws Exception { 255 testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE); 256 } 257 258 @Test checkOpenFdRemote()259 public void checkOpenFdRemote() throws Exception { 260 // Failing to read missing blocks through a file descriptor using read/pread causes an 261 // IOException to be thrown. 262 testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_FD, false /* expectCrash */); 263 } 264 265 private interface TestFunction { apply(Resources res, TestUtils.AssertionType type)266 void apply(Resources res, TestUtils.AssertionType type) throws Exception; 267 } 268 269 /** 270 * Installs a package incrementally and tests that retrieval of that package's resources from 271 * within this process does not crash this process and instead falls back to some default 272 * behavior. 273 */ testIncrementalForeignPackageResources(TestFunction test)274 private void testIncrementalForeignPackageResources(TestFunction test) throws Exception { 275 try (ShellInstallSession session = startInstallSession()) { 276 test.apply(session.getPackageResources(), TestUtils.AssertionType.ASSERT_SUCCESS); 277 } 278 // To disable verification. 279 installNonIncremental(TEST_APKS[0]); 280 try (ShellInstallSession session = startInstallSession()) { 281 session.enableBlockRestrictions(); 282 test.apply(session.getPackageResources(), TestUtils.AssertionType.ASSERT_READ_FAILURE); 283 } 284 } 285 286 /** 287 * Installs a package incrementally and tests that the package crashes when it fails to retrieve 288 * its own resources due to incremental installation. 289 */ testIncrementalOwnPackageResources(String testName, boolean expectCrash)290 private void testIncrementalOwnPackageResources(String testName, boolean expectCrash) 291 throws Exception { 292 try (RemoteTest session = new RemoteTest(startInstallSession(), testName)) { 293 session.mSession.getPackageResources(); 294 session.start(true /* assertSuccess */); 295 } 296 // To disable verification. 297 installNonIncremental(TEST_APKS[0]); 298 try (RemoteTest session = new RemoteTest(startInstallSession(), testName)) { 299 session.mSession.getPackageResources(); 300 session.mSession.enableBlockRestrictions(); 301 if (expectCrash) { 302 MatcherUtils.assertThrows(instanceOf(RemoteProcessCrashedException.class), 303 () -> session.start(false /* assertSuccess */)); 304 } else { 305 session.start(false /* assertSuccess */); 306 } 307 } 308 } 309 testIncrementalOwnPackageResources(String testName)310 private void testIncrementalOwnPackageResources(String testName) throws Exception { 311 testIncrementalOwnPackageResources(testName, true /* expectCrash */); 312 } 313 314 private static class RemoteProcessCrashedException extends RuntimeException { 315 } 316 317 private static class RemoteTest implements AutoCloseable { 318 private static final int SPIN_SLEEP_MS = 500; 319 private static final long RESPONSE_TIMEOUT_MS = 120 * 1000; 320 321 private final ShellInstallSession mSession; 322 private final String mTestName; 323 RemoteTest(ShellInstallSession session, String testName)324 RemoteTest(ShellInstallSession session, String testName) { 325 mSession = session; 326 mTestName = testName; 327 } 328 start(boolean assertSuccess)329 public void start(boolean assertSuccess) throws Exception { 330 final AtomicInteger pid = new AtomicInteger(); 331 final IntentFilter statusFilter = new IntentFilter(TestUtils.TEST_STATUS_ACTION); 332 333 final TestUtils.BroadcastDetector pidDetector = new TestUtils.BroadcastDetector( 334 getContext(), statusFilter, (Context context, Intent intent) -> { 335 if (intent.hasExtra(TestUtils.PID_STATUS_PID_KEY)) { 336 pid.set(intent.getIntExtra(TestUtils.PID_STATUS_PID_KEY, -1)); 337 return true; 338 } 339 return false; 340 }); 341 342 final TestUtils.BroadcastDetector finishDetector = new TestUtils.BroadcastDetector( 343 getContext(), statusFilter, (Context context, Intent intent) -> { 344 if (intent.hasExtra(TestUtils.TEST_STATUS_RESULT_KEY)) { 345 final String reason = intent.getStringExtra(TestUtils.TEST_STATUS_RESULT_KEY); 346 if (!reason.equals(TestUtils.TEST_STATUS_RESULT_SUCCESS)) { 347 throw new IllegalStateException("Remote test failed: " + reason); 348 } 349 return true; 350 } 351 return false; 352 }); 353 354 // Start the test app and indicate which test to run. 355 try (pidDetector; finishDetector) { 356 final Intent launchIntent = new Intent(Intent.ACTION_MAIN); 357 launchIntent.setClassName(TestUtils.TEST_APP_PACKAGE, TestUtils.TEST_ACTIVITY_NAME); 358 launchIntent.putExtra(TestUtils.TEST_NAME_EXTRA_KEY, mTestName); 359 launchIntent.putExtra(TestUtils.TEST_ASSERT_SUCCESS_EXTRA_KEY, assertSuccess); 360 launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 361 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 362 363 getContext().startActivity(launchIntent); 364 365 // The test app must respond with a broadcast containing its pid so this test can 366 // check if the test app crashes. 367 assertTrue("Timed out while waiting for pid", 368 pidDetector.waitForBroadcast(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS)); 369 370 // Wait for the test app to finish testing or crash. 371 final ActivityManager am = getActivityManager(); 372 final int remotePid = pid.get(); 373 for (int i = 0; i < (RESPONSE_TIMEOUT_MS / SPIN_SLEEP_MS); i++) { 374 if (am.getRunningAppProcesses().stream().noneMatch( 375 info -> info.pid == remotePid)) { 376 throw new RemoteProcessCrashedException(); 377 } 378 if (finishDetector.waitForBroadcast(SPIN_SLEEP_MS, TimeUnit.MILLISECONDS)) { 379 return; 380 } 381 } 382 throw new TimeoutException("Timed out while waiting for remote test to finish"); 383 } 384 } 385 386 @Override close()387 public void close() throws Exception { 388 mSession.close(); 389 } 390 } 391 startInstallSession()392 private ShellInstallSession startInstallSession() throws IOException, 393 InterruptedException { 394 return startInstallSession(TEST_APKS, TestUtils.TEST_APP_PACKAGE); 395 } 396 startInstallSession(String[] apks, String packageName)397 private ShellInstallSession startInstallSession(String[] apks, String packageName) 398 throws IOException, InterruptedException { 399 final String v4SignatureSuffix = ".idsig"; 400 final TestBlockFilter filter = new TestBlockFilter(); 401 final IncrementalInstallSession.Builder builder = new IncrementalInstallSession.Builder() 402 .addExtraArgs("--user", String.valueOf(getContext().getUserId()), 403 "-t", "-i", getContext().getPackageName(), 404 "--skip-verification") 405 .setLogger(new IncrementalDeviceConnection.Logger()) 406 .setBlockFilter(filter); 407 for (final String apk : apks) { 408 final String path = TEST_APK_PATH + apk; 409 builder.addApk(Paths.get(path), Paths.get(path + v4SignatureSuffix)); 410 } 411 final ShellInstallSession session = new ShellInstallSession( 412 builder.build(), filter, packageName); 413 session.session.start(Executors.newSingleThreadExecutor(), 414 IncrementalDeviceConnection.Factory.reliable()); 415 session.session.waitForInstallCompleted(10, TimeUnit.SECONDS); 416 assertTrue(isAppInstalledForUser(packageName, getContext().getUserId())); 417 return session; 418 } 419 420 /** 421 * A wrapper for {@link IncrementalInstallSession} that uninstalls the installed package when 422 * testing is finished. 423 */ 424 private static class ShellInstallSession implements AutoCloseable { 425 public final IncrementalInstallSession session; 426 private final TestBlockFilter mFilter; 427 private final String mPackageName; 428 ShellInstallSession(IncrementalInstallSession session, TestBlockFilter filter, String packageName)429 private ShellInstallSession(IncrementalInstallSession session, 430 TestBlockFilter filter, String packageName) { 431 this.session = session; 432 this.mFilter = filter; 433 this.mPackageName = packageName; 434 getUiAutomation().adoptShellPermissionIdentity(); 435 } 436 enableBlockRestrictions()437 public void enableBlockRestrictions() { 438 mFilter.enableBlockRestrictions(); 439 } 440 getPackageResources()441 public Resources getPackageResources() throws PackageManager.NameNotFoundException { 442 return getContext().createPackageContext(mPackageName, 0).getResources(); 443 } 444 445 @Override close()446 public void close() throws IOException { 447 session.close(); 448 getUiAutomation().dropShellPermissionIdentity(); 449 uninstallPackageSilently(mPackageName); 450 } 451 } 452 453 private class TestBlockFilter implements IBlockFilter { 454 private final AtomicBoolean mRestrictBlocks = new AtomicBoolean(false); 455 456 @Override shouldServeBlock(PendingBlock block)457 public boolean shouldServeBlock(PendingBlock block) { 458 if (!mRestrictBlocks.get() || block.getType() == PendingBlock.Type.SIGNATURE_TREE) { 459 // Always send signature blocks and always send blocks when enableBlockRestrictions 460 // has not been called. 461 return true; 462 } 463 464 // Allow the block to be served if it does not reside in a restricted range. 465 final String apkFileName = block.getPath().getFileName().toString(); 466 return mRestrictedRanges.get(apkFileName).stream().noneMatch( 467 info -> info.dataStartBlockIndex <= block.getBlockIndex() 468 && block.getBlockIndex() <= info.dataEndBlockIndex); 469 } 470 enableBlockRestrictions()471 public void enableBlockRestrictions() { 472 mRestrictBlocks.set(true); 473 } 474 } 475 476 private static class RestrictedBlockRange { 477 public final String entryName; 478 public final int dataStartBlockIndex; 479 public final int dataEndBlockIndex; 480 RestrictedBlockRange(String zipEntryName, int dataStartBlockIndex, int dataEndBlockIndex)481 RestrictedBlockRange(String zipEntryName, int dataStartBlockIndex, 482 int dataEndBlockIndex) { 483 this.entryName = zipEntryName; 484 this.dataStartBlockIndex = dataStartBlockIndex; 485 this.dataEndBlockIndex = dataEndBlockIndex; 486 } 487 } 488 restrictZipEntry(ZipFile file, String entryFileName)489 private static RestrictedBlockRange restrictZipEntry(ZipFile file, String entryFileName) { 490 final ZipArchiveEntry info = file.getEntry(entryFileName); 491 if (info == null) return null; 492 final long headerSize = entryFileName.getBytes(StandardCharsets.UTF_8).length + 30; 493 final int dataStartBlock = (int) (info.getDataOffset() - headerSize) / INCFS_BLOCK_SIZE; 494 final int dataEndBlock = (int) (info.getDataOffset() + info.getCompressedSize()) 495 / INCFS_BLOCK_SIZE; 496 return new RestrictedBlockRange(entryFileName, dataStartBlock, dataEndBlock); 497 } 498 restrictOnlyMiddleBlock(RestrictedBlockRange info)499 private static RestrictedBlockRange restrictOnlyMiddleBlock(RestrictedBlockRange info) { 500 if (info == null) return null; 501 assertTrue(info.dataEndBlockIndex - info.dataStartBlockIndex > 2); 502 final int middleBlock = (info.dataStartBlockIndex + info.dataEndBlockIndex) / 2; 503 return new RestrictedBlockRange(info.entryName, middleBlock, middleBlock); 504 } 505 getContext()506 private static Context getContext() { 507 return InstrumentationRegistry.getInstrumentation().getContext(); 508 } 509 getUiAutomation()510 private static UiAutomation getUiAutomation() { 511 return InstrumentationRegistry.getInstrumentation().getUiAutomation(); 512 } 513 getActivityManager()514 private static ActivityManager getActivityManager() { 515 return (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE); 516 } 517 } 518