blob: 98de3ee4cd8d003ad18be051c82dcd04df4db0f4 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.pm.cts;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.checkIncrementalDeliveryFeature;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.installNonIncremental;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.isAppInstalledForUser;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.setDeviceProperty;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.setSystemProperty;
import static android.content.pm.cts.PackageManagerShellCommandIncrementalTest.uninstallPackageSilently;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.app.ActivityManager;
import android.app.UiAutomation;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.platform.test.annotations.AppModeFull;
import android.util.ArrayMap;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.MatcherUtils;
import com.android.incfs.install.IBlockFilter;
import com.android.incfs.install.IncrementalInstallSession;
import com.android.incfs.install.PendingBlock;
import com.example.helloworld.lib.TestUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
@RunWith(AndroidJUnit4.class)
@AppModeFull
@LargeTest
public class ResourcesHardeningTest {
private static final String TEST_APK_PATH = "/data/local/tmp/cts/content/";
private static final String[] TEST_APKS = {
"HelloWorldResHardening.apk",
"HelloWorldResHardening_mdpi-v4.apk",
"HelloWorldResHardening_hdpi-v4.apk"
};
private static final String RES_TABLE_PATH = "resources.arsc";
private static final int INCFS_BLOCK_SIZE = 4096;
private final Map<String, List<RestrictedBlockRange>> mRestrictedRanges = new ArrayMap<>();
@Before
public void onBefore() throws Exception {
checkIncrementalDeliveryFeature();
setDeviceProperty("incfs_default_timeouts", "1:1:1");
setDeviceProperty("known_digesters_list", TestUtils.TEST_APP_PACKAGE);
setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
"0");
setSystemProperty("debug.incremental.enable_read_timeouts_after_install", "0");
// Set up the blocks that need to be restricted in order to test resource hardening.
if (!mRestrictedRanges.isEmpty()) {
return;
}
for (final String apk : TEST_APKS) {
try (ZipFile zip = new ZipFile(TEST_APK_PATH + apk)) {
final List<RestrictedBlockRange> infos = new ArrayList<>();
RestrictedBlockRange info;
info = restrictZipEntry(zip, RES_TABLE_PATH);
if (info != null) {
infos.add(info);
}
// Restrict only the middle block of the compiled xml to test that the whole
// file needs to be present just to open the xml file.
info = restrictOnlyMiddleBlock(restrictZipEntry(zip, TestUtils.RES_XML_PATH));
if (info != null) {
infos.add(info);
}
// Restrict only the middle block of this file to test that the whole file does
// NOT need to be present just to create an input stream or fd.
info = restrictOnlyMiddleBlock(
restrictZipEntry(zip, TestUtils.RES_DRAWABLE_MDPI_PATH));
if (info != null) {
infos.add(info);
}
// Test that FileNotFoundExceptions are thrown when the file is missing.
info = restrictZipEntry(zip, TestUtils.RES_DRAWABLE_HDPI_PATH);
if (info != null) {
infos.add(info);
}
assertFalse(infos.isEmpty());
mRestrictedRanges.put(apk, infos);
}
}
}
@After
public void onAfter() throws Exception {
setSystemProperty("debug.incremental.always_enable_read_timeouts_for_system_dataloaders",
"1");
setSystemProperty("debug.incremental.enable_read_timeouts_after_install", "1");
}
@LargeTest
@Test
public void checkGetIdentifier() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkGetIdentifier);
}
@Test
public void checkGetResourceName() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkGetResourceName);
}
@Test
public void checkGetString() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkGetString);
}
@Test
public void checkGetStringArray() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkGetStringArray);
}
@Test
public void checkOpenXmlResourceParser() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkOpenXmlResourceParser);
}
@Test
public void checkApplyStyle() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkApplyStyle);
}
@Test
public void checkXmlAttributes() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkXmlAttributes);
}
@Test
public void checkOpenMissingFile() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkOpenMissingFile);
}
@Test
public void checkOpenMissingFdFile() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkOpenMissingFdFile);
}
@Test
public void checkOpen() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkOpen);
}
@Test
public void checkOpenFd() throws Exception {
testIncrementalForeignPackageResources(TestUtils::checkOpenFd);
}
@Test
public void checkGetIdentifierRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_GET_IDENTIFIER);
}
@Test
public void checkGetResourceNameRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_GET_RESOURCE_NAME);
}
@Test
public void checkGetStringRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_GET_STRING);
}
@Test
public void checkGetStringArrayRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_GET_STRING_ARRAY);
}
@Test
public void checkOpenXmlResourceParserRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_XML);
}
@Test
public void checkApplyStyleRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_APPLY_STYLE);
}
@Test
public void checkXmlAttributesRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_XML_ATTRIBUTES);
}
@Test
public void checkOpenMissingFileRemote() throws Exception {
// If a zip entry local header is missing, libziparchive hardening causes a
// FileNotFoundException to be thrown regardless of whether a process queries its own
// resources or the resources of another package.
testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_MISSING,
false /* expectCrash */);
}
@Test
public void checkOpenMissingFdFileRemote() throws Exception {
// If a zip entry local header is missing, libziparchive hardening causes a
// FileNotFoundException to be thrown regardless of whether a process queries its own
// resources or the resources of another package.
testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_FD_MISSING,
false /* expectCrash */);
}
@Test
public void checkOpenRemote() throws Exception {
testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE);
}
@Test
public void checkOpenFdRemote() throws Exception {
// Failing to read missing blocks through a file descriptor using read/pread causes an
// IOException to be thrown.
testIncrementalOwnPackageResources(TestUtils.TEST_OPEN_FILE_FD, false /* expectCrash */);
}
private interface TestFunction {
void apply(Resources res, TestUtils.AssertionType type) throws Exception;
}
/**
* Installs a package incrementally and tests that retrieval of that package's resources from
* within this process does not crash this process and instead falls back to some default
* behavior.
*/
private void testIncrementalForeignPackageResources(TestFunction test) throws Exception {
try (ShellInstallSession session = startInstallSession()) {
test.apply(session.getPackageResources(), TestUtils.AssertionType.ASSERT_SUCCESS);
}
// To disable verification.
installNonIncremental(TEST_APKS[0]);
try (ShellInstallSession session = startInstallSession()) {
session.enableBlockRestrictions();
test.apply(session.getPackageResources(), TestUtils.AssertionType.ASSERT_READ_FAILURE);
}
}
/**
* Installs a package incrementally and tests that the package crashes when it fails to retrieve
* its own resources due to incremental installation.
*/
private void testIncrementalOwnPackageResources(String testName, boolean expectCrash)
throws Exception {
try (RemoteTest session = new RemoteTest(startInstallSession(), testName)) {
session.mSession.getPackageResources();
session.start(true /* assertSuccess */);
}
// To disable verification.
installNonIncremental(TEST_APKS[0]);
try (RemoteTest session = new RemoteTest(startInstallSession(), testName)) {
session.mSession.getPackageResources();
session.mSession.enableBlockRestrictions();
if (expectCrash) {
MatcherUtils.assertThrows(instanceOf(RemoteProcessCrashedException.class),
() -> session.start(false /* assertSuccess */));
} else {
session.start(false /* assertSuccess */);
}
}
}
private void testIncrementalOwnPackageResources(String testName) throws Exception {
testIncrementalOwnPackageResources(testName, true /* expectCrash */);
}
private static class RemoteProcessCrashedException extends RuntimeException {
}
private static class RemoteTest implements AutoCloseable {
private static final int SPIN_SLEEP_MS = 500;
private static final long RESPONSE_TIMEOUT_MS = 60 * 1000;
private final ShellInstallSession mSession;
private final String mTestName;
RemoteTest(ShellInstallSession session, String testName) {
mSession = session;
mTestName = testName;
}
public void start(boolean assertSuccess) throws Exception {
final AtomicInteger pid = new AtomicInteger();
final IntentFilter statusFilter = new IntentFilter(TestUtils.TEST_STATUS_ACTION);
final TestUtils.BroadcastDetector pidDetector = new TestUtils.BroadcastDetector(
getContext(), statusFilter, (Context context, Intent intent) -> {
if (intent.hasExtra(TestUtils.PID_STATUS_PID_KEY)) {
pid.set(intent.getIntExtra(TestUtils.PID_STATUS_PID_KEY, -1));
return true;
}
return false;
});
final TestUtils.BroadcastDetector finishDetector = new TestUtils.BroadcastDetector(
getContext(), statusFilter, (Context context, Intent intent) -> {
if (intent.hasExtra(TestUtils.TEST_STATUS_RESULT_KEY)) {
final String reason = intent.getStringExtra(TestUtils.TEST_STATUS_RESULT_KEY);
if (!reason.equals(TestUtils.TEST_STATUS_RESULT_SUCCESS)) {
throw new IllegalStateException("Remote test failed: " + reason);
}
return true;
}
return false;
});
// Start the test app and indicate which test to run.
try (pidDetector; finishDetector) {
final Intent launchIntent = new Intent(Intent.ACTION_MAIN);
launchIntent.setClassName(TestUtils.TEST_APP_PACKAGE, TestUtils.TEST_ACTIVITY_NAME);
launchIntent.putExtra(TestUtils.TEST_NAME_EXTRA_KEY, mTestName);
launchIntent.putExtra(TestUtils.TEST_ASSERT_SUCCESS_EXTRA_KEY, assertSuccess);
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
getContext().startActivity(launchIntent);
// The test app must respond with a broadcast containing its pid so this test can
// check if the test app crashes.
assertTrue("Timed out while waiting for pid",
pidDetector.waitForBroadcast(RESPONSE_TIMEOUT_MS, TimeUnit.MILLISECONDS));
// Wait for the test app to finish testing or crash.
final ActivityManager am = getActivityManager();
final int remotePid = pid.get();
for (int i = 0; i < (RESPONSE_TIMEOUT_MS / SPIN_SLEEP_MS); i++) {
if (am.getRunningAppProcesses().stream().noneMatch(
info -> info.pid == remotePid)) {
throw new RemoteProcessCrashedException();
}
if (finishDetector.waitForBroadcast(SPIN_SLEEP_MS, TimeUnit.MILLISECONDS)) {
return;
}
}
throw new TimeoutException("Timed out while waiting for remote test to finish");
}
}
@Override
public void close() throws Exception {
mSession.close();
}
}
private ShellInstallSession startInstallSession() throws IOException,
InterruptedException {
return startInstallSession(TEST_APKS, TestUtils.TEST_APP_PACKAGE);
}
private ShellInstallSession startInstallSession(String[] apks, String packageName)
throws IOException, InterruptedException {
final String v4SignatureSuffix = ".idsig";
final TestBlockFilter filter = new TestBlockFilter();
final IncrementalInstallSession.Builder builder = new IncrementalInstallSession.Builder()
.addExtraArgs("--user", String.valueOf(getContext().getUserId()),
"-t", "-i", getContext().getPackageName(),
"--skip-verification")
.setLogger(new IncrementalDeviceConnection.Logger())
.setBlockFilter(filter);
for (final String apk : apks) {
final String path = TEST_APK_PATH + apk;
builder.addApk(Paths.get(path), Paths.get(path + v4SignatureSuffix));
}
final ShellInstallSession session = new ShellInstallSession(
builder.build(), filter, packageName);
session.session.start(Executors.newSingleThreadExecutor(),
IncrementalDeviceConnection.Factory.reliable());
session.session.waitForInstallCompleted(10, TimeUnit.SECONDS);
assertTrue(isAppInstalledForUser(packageName, getContext().getUserId()));
return session;
}
/**
* A wrapper for {@link IncrementalInstallSession} that uninstalls the installed package when
* testing is finished.
*/
private static class ShellInstallSession implements AutoCloseable {
public final IncrementalInstallSession session;
private final TestBlockFilter mFilter;
private final String mPackageName;
private ShellInstallSession(IncrementalInstallSession session,
TestBlockFilter filter, String packageName) {
this.session = session;
this.mFilter = filter;
this.mPackageName = packageName;
getUiAutomation().adoptShellPermissionIdentity();
}
public void enableBlockRestrictions() {
mFilter.enableBlockRestrictions();
}
public Resources getPackageResources() throws PackageManager.NameNotFoundException {
return getContext().createPackageContext(mPackageName, 0).getResources();
}
@Override
public void close() throws IOException {
session.close();
getUiAutomation().dropShellPermissionIdentity();
uninstallPackageSilently(mPackageName);
}
}
private class TestBlockFilter implements IBlockFilter {
private final AtomicBoolean mRestrictBlocks = new AtomicBoolean(false);
@Override
public boolean shouldServeBlock(PendingBlock block) {
if (!mRestrictBlocks.get() || block.getType() == PendingBlock.Type.SIGNATURE_TREE) {
// Always send signature blocks and always send blocks when enableBlockRestrictions
// has not been called.
return true;
}
// Allow the block to be served if it does not reside in a restricted range.
final String apkFileName = block.getPath().getFileName().toString();
return mRestrictedRanges.get(apkFileName).stream().noneMatch(
info -> info.dataStartBlockIndex <= block.getBlockIndex()
&& block.getBlockIndex() <= info.dataEndBlockIndex);
}
public void enableBlockRestrictions() {
mRestrictBlocks.set(true);
}
}
private static class RestrictedBlockRange {
public final String entryName;
public final int dataStartBlockIndex;
public final int dataEndBlockIndex;
RestrictedBlockRange(String zipEntryName, int dataStartBlockIndex,
int dataEndBlockIndex) {
this.entryName = zipEntryName;
this.dataStartBlockIndex = dataStartBlockIndex;
this.dataEndBlockIndex = dataEndBlockIndex;
}
}
private static RestrictedBlockRange restrictZipEntry(ZipFile file, String entryFileName) {
final ZipArchiveEntry info = file.getEntry(entryFileName);
if (info == null) return null;
final long headerSize = entryFileName.getBytes(StandardCharsets.UTF_8).length + 30;
final int dataStartBlock = (int) (info.getDataOffset() - headerSize) / INCFS_BLOCK_SIZE;
final int dataEndBlock = (int) (info.getDataOffset() + info.getCompressedSize())
/ INCFS_BLOCK_SIZE;
return new RestrictedBlockRange(entryFileName, dataStartBlock, dataEndBlock);
}
private static RestrictedBlockRange restrictOnlyMiddleBlock(RestrictedBlockRange info) {
if (info == null) return null;
assertTrue(info.dataEndBlockIndex - info.dataStartBlockIndex > 2);
final int middleBlock = (info.dataStartBlockIndex + info.dataEndBlockIndex) / 2;
return new RestrictedBlockRange(info.entryName, middleBlock, middleBlock);
}
private static Context getContext() {
return InstrumentationRegistry.getInstrumentation().getContext();
}
private static UiAutomation getUiAutomation() {
return InstrumentationRegistry.getInstrumentation().getUiAutomation();
}
private static ActivityManager getActivityManager() {
return (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
}
}