| /* |
| * Copyright 2017 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 com.android.server.pm.dex; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.Mockito.atMost; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.doThrow; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyZeroInteractions; |
| import static org.mockito.Mockito.when; |
| |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IPackageManager; |
| import android.content.pm.PackageInfo; |
| import android.os.UserHandle; |
| import android.os.storage.StorageManager; |
| |
| import androidx.test.filters.SmallTest; |
| import androidx.test.runner.AndroidJUnit4; |
| |
| import com.android.server.pm.Installer; |
| import com.android.server.pm.Installer.InstallerException; |
| |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.ListMultimap; |
| |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnit; |
| import org.mockito.junit.MockitoRule; |
| import org.mockito.quality.Strictness; |
| import org.mockito.stubbing.Stubber; |
| |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class DynamicCodeLoggerTests { |
| private static final String OWNING_PACKAGE_NAME = "package.name"; |
| private static final String VOLUME_UUID = "volUuid"; |
| private static final String FILE_PATH = "/bar/foo.jar"; |
| private static final int STORAGE_FLAGS = StorageManager.FLAG_STORAGE_DE; |
| private static final int OWNER_UID = 43; |
| private static final int OWNER_USER_ID = 44; |
| |
| // Obtained via: echo -n "foo.jar" | sha256sum |
| private static final String FILENAME_HASH = |
| "91D7B844D7CC9673748FF057D8DC83972280FC28537D381AA42015A9CF214B9F"; |
| |
| private static final byte[] CONTENT_HASH_BYTES = new byte[]{ |
| 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, |
| 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 |
| }; |
| private static final String CONTENT_HASH = |
| "0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20"; |
| private static final byte[] EMPTY_BYTES = {}; |
| |
| private static final String EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH = |
| "dcl:" + FILENAME_HASH; |
| private static final String EXPECTED_MESSAGE_WITH_CONTENT_HASH = |
| EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH + " " + CONTENT_HASH; |
| private static final String EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH = |
| "dcln:" + FILENAME_HASH + " " + CONTENT_HASH; |
| |
| @Rule public MockitoRule mockito = MockitoJUnit.rule().strictness(Strictness.LENIENT); |
| |
| @Mock IPackageManager mPM; |
| @Mock Installer mInstaller; |
| |
| private DynamicCodeLogger mDynamicCodeLogger; |
| |
| private final ListMultimap<Integer, String> mMessagesForUid = ArrayListMultimap.create(); |
| private boolean mWriteTriggered = false; |
| |
| @Before |
| public void setup() throws Exception { |
| // Disable actually attempting to do file writes. |
| PackageDynamicCodeLoading packageDynamicCodeLoading = new PackageDynamicCodeLoading() { |
| @Override |
| void maybeWriteAsync() { |
| mWriteTriggered = true; |
| } |
| |
| @Override |
| protected void writeNow(Void data) { |
| throw new AssertionError("These tests should never call this method."); |
| } |
| }; |
| |
| // For test purposes capture log messages as well as sending to the event log. |
| mDynamicCodeLogger = new DynamicCodeLogger(mPM, mInstaller, packageDynamicCodeLoading) { |
| @Override |
| void writeDclEvent(String subtag, int uid, String message) { |
| super.writeDclEvent(subtag, uid, message); |
| mMessagesForUid.put(uid, subtag + ":" + message); |
| } |
| }; |
| |
| // Make the owning package exist in our mock PackageManager. |
| ApplicationInfo appInfo = new ApplicationInfo(); |
| appInfo.deviceProtectedDataDir = "/bar"; |
| appInfo.uid = OWNER_UID; |
| appInfo.volumeUuid = VOLUME_UUID; |
| PackageInfo packageInfo = new PackageInfo(); |
| packageInfo.applicationInfo = appInfo; |
| |
| doReturn(packageInfo).when(mPM) |
| .getPackageInfo(OWNING_PACKAGE_NAME, /*flags*/ 0, OWNER_USER_ID); |
| } |
| |
| @Test |
| public void testOneLoader_ownFile_withFileHash() throws Exception { |
| whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES)); |
| |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID); |
| assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH); |
| |
| assertThat(mWriteTriggered).isFalse(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) |
| .containsExactly(OWNING_PACKAGE_NAME); |
| } |
| |
| @Test |
| public void testOneLoader_ownFile_noFileHash() throws Exception { |
| whenFileIsHashed(FILE_PATH, doReturn(EMPTY_BYTES)); |
| |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID); |
| assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH); |
| |
| // File should be removed from the DCL list, since we can't hash it. |
| assertThat(mWriteTriggered).isTrue(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty(); |
| } |
| |
| @Test |
| public void testOneLoader_ownFile_hashingFails() throws Exception { |
| whenFileIsHashed(FILE_PATH, |
| doThrow(new InstallerException("Intentional failure for test"))); |
| |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID); |
| assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH); |
| |
| // File should be removed from the DCL list, since we can't hash it. |
| assertThat(mWriteTriggered).isTrue(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty(); |
| } |
| |
| @Test |
| public void testOneLoader_ownFile_unknownPath() { |
| recordLoad(OWNING_PACKAGE_NAME, "other/path"); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid).isEmpty(); |
| assertThat(mWriteTriggered).isTrue(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty(); |
| } |
| |
| @Test |
| public void testOneLoader_pathTraversal() throws Exception { |
| String filePath = "/bar/../secret/foo.jar"; |
| whenFileIsHashed(filePath, doReturn(CONTENT_HASH_BYTES)); |
| setPackageUid(OWNING_PACKAGE_NAME, -1); |
| |
| recordLoad(OWNING_PACKAGE_NAME, filePath); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid).isEmpty(); |
| } |
| |
| @Test |
| public void testOneLoader_differentOwner() throws Exception { |
| whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES)); |
| setPackageUid("other.package.name", 1001); |
| |
| recordLoad("other.package.name", FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(1001); |
| assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH); |
| assertThat(mWriteTriggered).isFalse(); |
| } |
| |
| @Test |
| public void testOneLoader_differentOwner_uninstalled() throws Exception { |
| whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES)); |
| setPackageUid("other.package.name", -1); |
| |
| recordLoad("other.package.name", FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid).isEmpty(); |
| assertThat(mWriteTriggered).isFalse(); |
| } |
| |
| @Test |
| public void testNativeCodeLoad() throws Exception { |
| whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES)); |
| |
| recordLoadNative(FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(OWNER_UID); |
| assertThat(mMessagesForUid) |
| .containsEntry(OWNER_UID, EXPECTED_MESSAGE_NATIVE_WITH_CONTENT_HASH); |
| |
| assertThat(mWriteTriggered).isFalse(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) |
| .containsExactly(OWNING_PACKAGE_NAME); |
| } |
| |
| @Test |
| public void testMultipleLoadersAndFiles() throws Exception { |
| String otherDexPath = "/bar/nosuchdir/foo.jar"; |
| whenFileIsHashed(FILE_PATH, doReturn(CONTENT_HASH_BYTES)); |
| whenFileIsHashed(otherDexPath, doReturn(EMPTY_BYTES)); |
| setPackageUid("other.package.name1", 1001); |
| setPackageUid("other.package.name2", 1002); |
| |
| recordLoad("other.package.name1", FILE_PATH); |
| recordLoad("other.package.name1", otherDexPath); |
| recordLoad("other.package.name2", FILE_PATH); |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid.keys()).containsExactly(1001, 1001, 1002, OWNER_UID); |
| assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITH_CONTENT_HASH); |
| assertThat(mMessagesForUid).containsEntry(1001, EXPECTED_MESSAGE_WITHOUT_CONTENT_HASH); |
| assertThat(mMessagesForUid).containsEntry(1002, EXPECTED_MESSAGE_WITH_CONTENT_HASH); |
| assertThat(mMessagesForUid).containsEntry(OWNER_UID, EXPECTED_MESSAGE_WITH_CONTENT_HASH); |
| |
| assertThat(mWriteTriggered).isTrue(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()) |
| .containsExactly(OWNING_PACKAGE_NAME); |
| |
| // Check the DynamicCodeLogger caching is working |
| verify(mPM, atMost(1)).getPackageInfo(OWNING_PACKAGE_NAME, /*flags*/ 0, OWNER_USER_ID); |
| } |
| |
| @Test |
| public void testUnknownOwner() { |
| reset(mPM); |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading("other.package.name"); |
| |
| assertThat(mMessagesForUid).isEmpty(); |
| assertThat(mWriteTriggered).isFalse(); |
| verifyZeroInteractions(mPM); |
| } |
| |
| @Test |
| public void testUninstalledPackage() { |
| reset(mPM); |
| recordLoad(OWNING_PACKAGE_NAME, FILE_PATH); |
| mDynamicCodeLogger.logDynamicCodeLoading(OWNING_PACKAGE_NAME); |
| |
| assertThat(mMessagesForUid).isEmpty(); |
| assertThat(mWriteTriggered).isTrue(); |
| assertThat(mDynamicCodeLogger.getAllPackagesWithDynamicCodeLoading()).isEmpty(); |
| } |
| |
| private void setPackageUid(String packageName, int uid) throws Exception { |
| doReturn(uid).when(mPM).getPackageUid(packageName, /*flags*/ 0, OWNER_USER_ID); |
| } |
| |
| private void whenFileIsHashed(String dexPath, Stubber stubber) throws Exception { |
| stubber.when(mInstaller).hashSecondaryDexFile( |
| dexPath, OWNING_PACKAGE_NAME, OWNER_UID, VOLUME_UUID, STORAGE_FLAGS); |
| } |
| |
| private void recordLoad(String loadingPackageName, String dexPath) { |
| mDynamicCodeLogger.recordDex( |
| OWNER_USER_ID, dexPath, OWNING_PACKAGE_NAME, loadingPackageName); |
| mWriteTriggered = false; |
| } |
| |
| private void recordLoadNative(String nativePath) throws Exception { |
| int loadingUid = UserHandle.getUid(OWNER_USER_ID, OWNER_UID); |
| String[] packageNames = { OWNING_PACKAGE_NAME }; |
| when(mPM.getPackagesForUid(loadingUid)).thenReturn(packageNames); |
| |
| mDynamicCodeLogger.recordNative(loadingUid, nativePath); |
| mWriteTriggered = false; |
| } |
| } |