blob: 7992ba33c8daeac24fecc1f268b879d32e34d981 [file] [log] [blame]
/*
* 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;
}
}