xref: /aosp_15_r20/cts/tests/tests/content/src/android/content/pm/cts/ResourcesHardeningTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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