Merge changes from topic "apex-installers-restriction" into sc-dev

* changes:
  Reject non-staged APEX install if there is staged install of same APEX
  Restrict what APEXes different installers can update
This commit is contained in:
Nikita Ioffe
2021-07-22 16:53:05 +00:00
committed by Android (Google) Code Review
8 changed files with 383 additions and 14 deletions

View File

@@ -241,7 +241,11 @@ public class SystemConfig {
private final ArraySet<String> mRollbackWhitelistedPackages = new ArraySet<>();
private final ArraySet<String> mWhitelistedStagedInstallers = new ArraySet<>();
private final ArraySet<String> mAllowedVendorApexes = new ArraySet<>();
// A map from package name of vendor APEXes that can be updated to an installer package name
// allowed to install updates for it.
private final ArrayMap<String, String> mAllowedVendorApexes = new ArrayMap<>();
private String mModulesInstallerPackageName;
/**
* Map of system pre-defined, uniquely named actors; keys are namespace,
@@ -412,10 +416,14 @@ public class SystemConfig {
return mWhitelistedStagedInstallers;
}
public Set<String> getAllowedVendorApexes() {
public Map<String, String> getAllowedVendorApexes() {
return mAllowedVendorApexes;
}
public String getModulesInstallerPackageName() {
return mModulesInstallerPackageName;
}
public ArraySet<String> getAppDataIsolationWhitelistedApps() {
return mAppDataIsolationWhitelistedApps;
}
@@ -1210,12 +1218,21 @@ public class SystemConfig {
case "whitelisted-staged-installer": {
if (allowAppConfigs) {
String pkgname = parser.getAttributeValue(null, "package");
boolean isModulesInstaller = XmlUtils.readBooleanAttribute(
parser, "isModulesInstaller", false);
if (pkgname == null) {
Slog.w(TAG, "<" + name + "> without package in " + permFile
+ " at " + parser.getPositionDescription());
} else {
mWhitelistedStagedInstallers.add(pkgname);
}
if (isModulesInstaller) {
if (mModulesInstallerPackageName != null) {
throw new IllegalStateException(
"Multiple modules installers");
}
mModulesInstallerPackageName = pkgname;
}
} else {
logNotAllowedInPartition(name, permFile, parser);
}
@@ -1224,11 +1241,18 @@ public class SystemConfig {
case "allowed-vendor-apex": {
if (allowVendorApex) {
String pkgName = parser.getAttributeValue(null, "package");
String installerPkgName = parser.getAttributeValue(
null, "installerPackage");
if (pkgName == null) {
Slog.w(TAG, "<" + name + "> without package in " + permFile
+ " at " + parser.getPositionDescription());
} else {
mAllowedVendorApexes.add(pkgName);
}
if (installerPkgName == null) {
Slog.w(TAG, "<" + name + "> without installerPackage in " + permFile
+ " at " + parser.getPositionDescription());
}
if (pkgName != null && installerPkgName != null) {
mAllowedVendorApexes.put(pkgName, installerPkgName);
}
} else {
logNotAllowedInPartition(name, permFile, parser);

View File

@@ -2252,12 +2252,29 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
(params.installFlags & PackageManager.INSTALL_DISABLE_ALLOWED_APEX_UPDATE_CHECK)
== 0;
synchronized (mLock) {
if (checkApexUpdateAllowed && !isApexUpdateAllowed(mPackageName)) {
if (checkApexUpdateAllowed && !isApexUpdateAllowed(mPackageName,
mInstallSource.installerPackageName)) {
onSessionValidationFailure(PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
"Update of APEX package " + mPackageName + " is not allowed");
"Update of APEX package " + mPackageName + " is not allowed for "
+ mInstallSource.installerPackageName);
return;
}
}
if (!params.isStaged) {
// For non-staged APEX installs also check if there is a staged session that
// contains the same APEX. If that's the case, we should fail this session.
synchronized (mLock) {
int sessionId = mStagingManager.getSessionIdByPackageName(mPackageName);
if (sessionId != -1) {
onSessionValidationFailure(
PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE,
"Staged session " + sessionId + " already contains "
+ mPackageName);
return;
}
}
}
}
if (params.isStaged) {
@@ -2798,9 +2815,23 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
return sessionContains((s) -> !s.isApexSession());
}
private boolean isApexUpdateAllowed(String apexPackageName) {
return mPm.getModuleInfo(apexPackageName, 0) != null
|| SystemConfig.getInstance().getAllowedVendorApexes().contains(apexPackageName);
private boolean isApexUpdateAllowed(String apexPackageName, String installerPackageName) {
if (mPm.getModuleInfo(apexPackageName, 0) != null) {
final String modulesInstaller =
SystemConfig.getInstance().getModulesInstallerPackageName();
if (modulesInstaller == null) {
Slog.w(TAG, "No modules installer defined");
return false;
}
return modulesInstaller.equals(installerPackageName);
}
final String vendorApexInstaller =
SystemConfig.getInstance().getAllowedVendorApexes().get(apexPackageName);
if (vendorApexInstaller == null) {
Slog.w(TAG, apexPackageName + " is not allowed to be updated");
return false;
}
return vendorApexInstaller.equals(installerPackageName);
}
/**

View File

@@ -777,6 +777,26 @@ public class StagingManager {
}
}
/**
* Returns id of a committed and non-finalized stated session that contains same
* {@code packageName}, or {@code -1} if no sessions have this {@code packageName} staged.
*/
int getSessionIdByPackageName(@NonNull String packageName) {
synchronized (mStagedSessions) {
for (int i = 0; i < mStagedSessions.size(); i++) {
StagedSession stagedSession = mStagedSessions.valueAt(i);
if (!stagedSession.isCommitted() || stagedSession.isDestroyed()
|| stagedSession.isInTerminalState()) {
continue;
}
if (stagedSession.getPackageName().equals(packageName)) {
return stagedSession.sessionId();
}
}
}
return -1;
}
@VisibleForTesting
void createSession(@NonNull StagedSession sessionInfo) {
synchronized (mStagedSessions) {

View File

@@ -459,6 +459,66 @@ public class StagingManagerTest {
assertThat(apkSession.getErrorMessage()).isEqualTo("Another apex session failed");
}
@Test
public void getSessionIdByPackageName() throws Exception {
FakeStagedSession session = new FakeStagedSession(239);
session.setCommitted(true);
session.setSessionReady();
session.setPackageName("com.foo");
mStagingManager.createSession(session);
assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(239);
}
@Test
public void getSessionIdByPackageName_appliedSession_ignores() throws Exception {
FakeStagedSession session = new FakeStagedSession(37);
session.setCommitted(true);
session.setSessionApplied();
session.setPackageName("com.foo");
mStagingManager.createSession(session);
assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
}
@Test
public void getSessionIdByPackageName_failedSession_ignores() throws Exception {
FakeStagedSession session = new FakeStagedSession(73);
session.setCommitted(true);
session.setSessionFailed(1, "whatevs");
session.setPackageName("com.foo");
mStagingManager.createSession(session);
assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
}
@Test
public void getSessionIdByPackageName_destroyedSession_ignores() throws Exception {
FakeStagedSession session = new FakeStagedSession(23);
session.setCommitted(true);
session.setDestroyed(true);
session.setPackageName("com.foo");
mStagingManager.createSession(session);
assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
}
@Test
public void getSessionIdByPackageName_noSessions() throws Exception {
assertThat(mStagingManager.getSessionIdByPackageName("com.foo")).isEqualTo(-1);
}
@Test
public void getSessionIdByPackageName_noSessionHasThisPackage() throws Exception {
FakeStagedSession session = new FakeStagedSession(37);
session.setCommitted(true);
session.setSessionApplied();
session.setPackageName("com.foo");
mStagingManager.createSession(session);
assertThat(mStagingManager.getSessionIdByPackageName("com.bar")).isEqualTo(-1);
}
private StagingManager.StagedSession createSession(int sessionId, String packageName,
long committedMillis) {
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(

View File

@@ -19,6 +19,7 @@ package com.android.server.systemconfig;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.testng.Assert.expectThrows;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
@@ -200,6 +201,46 @@ public class SystemConfigTest {
assertThat(mSysConfig.getWhitelistedStagedInstallers())
.containsExactly("com.android.package1");
assertThat(mSysConfig.getModulesInstallerPackageName()).isNull();
}
@Test
public void readPermissions_parsesStagedInstallerWhitelist_modulesInstaller()
throws IOException {
final String contents =
"<config>\n"
+ " <whitelisted-staged-installer package=\"com.android.package1\" "
+ " isModulesInstaller=\"true\" />\n"
+ "</config>";
final File folder = createTempSubfolder("folder");
createTempFile(folder, "staged-installer-whitelist.xml", contents);
mSysConfig.readPermissions(folder, /* Grant all permission flags */ ~0);
assertThat(mSysConfig.getWhitelistedStagedInstallers())
.containsExactly("com.android.package1");
assertThat(mSysConfig.getModulesInstallerPackageName())
.isEqualTo("com.android.package1");
}
@Test
public void readPermissions_parsesStagedInstallerWhitelist_multipleModulesInstallers()
throws IOException {
final String contents =
"<config>\n"
+ " <whitelisted-staged-installer package=\"com.android.package1\" "
+ " isModulesInstaller=\"true\" />\n"
+ " <whitelisted-staged-installer package=\"com.android.package2\" "
+ " isModulesInstaller=\"true\" />\n"
+ "</config>";
final File folder = createTempSubfolder("folder");
createTempFile(folder, "staged-installer-whitelist.xml", contents);
IllegalStateException e = expectThrows(
IllegalStateException.class,
() -> mSysConfig.readPermissions(folder, /* Grant all permission flags */ ~0));
assertThat(e).hasMessageThat().contains("Multiple modules installers");
}
/**
@@ -230,14 +271,16 @@ public class SystemConfigTest {
throws IOException {
final String contents =
"<config>\n"
+ " <allowed-vendor-apex package=\"com.android.apex1\" />\n"
+ " <allowed-vendor-apex package=\"com.android.apex1\" "
+ "installerPackage=\"com.installer\" />\n"
+ "</config>";
final File folder = createTempSubfolder("folder");
createTempFile(folder, "vendor-apex-allowlist.xml", contents);
mSysConfig.readPermissions(folder, /* Grant all permission flags */ ~0);
assertThat(mSysConfig.getAllowedVendorApexes()).containsExactly("com.android.apex1");
assertThat(mSysConfig.getAllowedVendorApexes())
.containsExactly("com.android.apex1", "com.installer");
}
/**

View File

@@ -32,6 +32,7 @@ android_test_helper_app {
test_suites: ["general-tests"],
java_resources: [
":com.android.apex.apkrollback.test_v2",
":StagedInstallTestApexV2",
":StagedInstallTestApexV2_WrongSha",
":test.rebootless_apex_v1",
":test.rebootless_apex_v2",

View File

@@ -55,8 +55,11 @@ public class StagedInstallInternalTest {
private static final TestApp TEST_APEX_WITH_APK_V2 = new TestApp("TestApexWithApkV2",
APK_IN_APEX_TESTAPEX_NAME, 2, /*isApex*/true, APK_IN_APEX_TESTAPEX_NAME + "_v2.apex");
private static final TestApp APEX_WRONG_SHA_V2 = new TestApp(
"ApexWrongSha2", SHIM_APEX_PACKAGE_NAME, 2, /*isApex*/true,
"ApexWrongSha2", SHIM_APEX_PACKAGE_NAME, 2, /* isApex= */ true,
"com.android.apex.cts.shim.v2_wrong_sha.apex");
private static final TestApp APEX_V2 = new TestApp(
"ApexV2", SHIM_APEX_PACKAGE_NAME, 2, /* isApex= */ true,
"com.android.apex.cts.shim.v2.apex");
private File mTestStateFile = new File(
InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
@@ -236,6 +239,96 @@ public class StagedInstallInternalTest {
assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
}
@Test
public void testApexInstallerNotInAllowListCanNotInstall_staged() throws Exception {
assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
// We don't really care which APEX we are trying to install here, since the session creation
// should fail immediately.
InstallUtils.commitExpectingFailure(
SecurityException.class,
"Installer not allowed to commit staged install",
Install.single(APEX_WRONG_SHA_V2).setBypassStangedInstallerCheck(false)
.setStaged());
}
@Test
public void testApexInstallerNotInAllowListCanNotInstall_nonStaged() throws Exception {
assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
// We don't really care which APEX we are trying to install here, since the session creation
// should fail immediately.
InstallUtils.commitExpectingFailure(
SecurityException.class,
"Installer not allowed to commit non-staged APEX install",
Install.single(APEX_WRONG_SHA_V2).setBypassStangedInstallerCheck(false));
}
@Test
public void testApexNotInAllowListCanNotInstall_staged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
InstallUtils.commitExpectingFailure(
AssertionError.class,
"Update of APEX package test.apex.rebootless is not allowed "
+ "for com.android.tests.stagedinstallinternal",
Install.single(apex).setBypassAllowedApexUpdateCheck(false).setStaged());
}
@Test
public void testApexNotInAllowListCanNotInstall_nonStaged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
InstallUtils.commitExpectingFailure(
AssertionError.class,
"Update of APEX package test.apex.rebootless is not allowed "
+ "for com.android.tests.stagedinstallinternal",
Install.single(apex).setBypassAllowedApexUpdateCheck(false));
}
@Test
public void testVendorApexWrongInstaller_staged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
InstallUtils.commitExpectingFailure(
AssertionError.class,
"Update of APEX package test.apex.rebootless is not allowed "
+ "for com.android.tests.stagedinstallinternal",
Install.single(apex).setBypassAllowedApexUpdateCheck(false).setStaged());
}
@Test
public void testVendorApexWrongInstaller_nonStaged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
InstallUtils.commitExpectingFailure(
AssertionError.class,
"Update of APEX package test.apex.rebootless is not allowed "
+ "for com.android.tests.stagedinstallinternal",
Install.single(apex).setBypassAllowedApexUpdateCheck(false));
}
@Test
public void testVendorApexCorrectInstaller_staged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
int sessionId =
Install.single(apex).setBypassAllowedApexUpdateCheck(false).setStaged().commit();
InstallUtils.getPackageInstaller().abandonSession(sessionId);
}
@Test
public void testVendorApexCorrectInstaller_nonStaged() throws Exception {
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(1);
TestApp apex = new TestApp("apex", "test.apex.rebootless", 2,
/* isApex= */ true, "test.rebootless_apex_v2.apex");
Install.single(apex).setBypassAllowedApexUpdateCheck(false).commit();
assertThat(InstallUtils.getInstalledVersion("test.apex.rebootless")).isEqualTo(2);
}
@Test
public void testRebootlessUpdates() throws Exception {
InstallUtils.dropShellPermissionIdentity();
@@ -298,6 +391,19 @@ public class StagedInstallInternalTest {
}
}
@Test
public void testRebootlessUpdate_hasStagedSessionWithSameApex_fails() throws Exception {
assertThat(InstallUtils.getInstalledVersion(SHIM_APEX_PACKAGE_NAME)).isEqualTo(1);
int sessionId = Install.single(APEX_V2).setStaged().commit();
assertSessionReady(sessionId);
InstallUtils.commitExpectingFailure(
AssertionError.class,
"Staged session " + sessionId + " already contains " + SHIM_APEX_PACKAGE_NAME,
Install.single(APEX_V2));
}
private static void assertSessionApplied(int sessionId) {
assertSessionState(sessionId, (session) -> {
assertThat(session.isStagedSessionApplied()).isTrue();

View File

@@ -43,7 +43,9 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.util.List;
import java.util.stream.Collectors;
@@ -60,6 +62,9 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
private static final String APK_A = "TestAppAv1.apk";
private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test";
private static final String TEST_VENDOR_APEX_ALLOW_LIST =
"/vendor/etc/sysconfig/test-vendor-apex-allow-list.xml";
private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this);
/**
@@ -87,7 +92,8 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
"/data/apex/active/" + APK_IN_APEX_TESTAPEX_NAME + "*.apex",
"/data/apex/active/" + SHIM_APEX_PACKAGE_NAME + "*.apex",
"/system/apex/test.rebootless_apex_v1.apex",
"/data/apex/active/test.apex.rebootless*.apex");
"/data/apex/active/test.apex.rebootless*.apex",
TEST_VENDOR_APEX_ALLOW_LIST);
}
@Before
@@ -134,7 +140,23 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
}
getDevice().remountSystemWritable();
assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName));
getDevice().reboot();
}
private void pushTestVendorApexAllowList(String installerPackageName) throws Exception {
if (!getDevice().isAdbRoot()) {
getDevice().enableAdbRoot();
}
getDevice().remountSystemWritable();
File file = File.createTempFile("test-vendor-apex-allow-list", ".xml");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
final String fmt =
"<config>\n"
+ " <allowed-vendor-apex package=\"test.apex.rebootless\" "
+ " installerPackage=\"%s\" />\n"
+ "</config>";
writer.write(String.format(fmt, installerPackageName));
}
getDevice().pushFile(file, TEST_VENDOR_APEX_ALLOW_LIST);
}
/**
@@ -144,6 +166,8 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
@LargeTest
public void testDuplicateApkInApexShouldFail() throws Exception {
pushTestApex(APK_IN_APEX_TESTAPEX_NAME + "_v1.apex");
getDevice().reboot();
runPhase("testDuplicateApkInApexShouldFail_Commit");
getDevice().reboot();
runPhase("testDuplicateApkInApexShouldFail_Verify");
@@ -388,12 +412,72 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
runPhase("testApexIsNotActivatedIfNotInCheckpointMode_VerifyPostReboot");
}
@Test
public void testApexInstallerNotInAllowListCanNotInstall() throws Exception {
assumeTrue("Device does not support updating APEX",
mHostUtils.isApexUpdateSupported());
runPhase("testApexInstallerNotInAllowListCanNotInstall_staged");
runPhase("testApexInstallerNotInAllowListCanNotInstall_nonStaged");
}
@Test
@LargeTest
public void testApexNotInAllowListCanNotInstall() throws Exception {
assumeTrue("Device does not support updating APEX",
mHostUtils.isApexUpdateSupported());
pushTestApex("test.rebootless_apex_v1.apex");
getDevice().reboot();
runPhase("testApexNotInAllowListCanNotInstall_staged");
runPhase("testApexNotInAllowListCanNotInstall_nonStaged");
}
@Test
@LargeTest
public void testVendorApexWrongInstaller() throws Exception {
assumeTrue("Device does not support updating APEX",
mHostUtils.isApexUpdateSupported());
pushTestVendorApexAllowList("com.wrong.installer");
pushTestApex("test.rebootless_apex_v1.apex");
getDevice().reboot();
runPhase("testVendorApexWrongInstaller_staged");
runPhase("testVendorApexWrongInstaller_nonStaged");
}
@Test
@LargeTest
public void testVendorApexCorrectInstaller() throws Exception {
assumeTrue("Device does not support updating APEX",
mHostUtils.isApexUpdateSupported());
pushTestVendorApexAllowList("com.android.tests.stagedinstallinternal");
pushTestApex("test.rebootless_apex_v1.apex");
getDevice().reboot();
runPhase("testVendorApexCorrectInstaller_staged");
runPhase("testVendorApexCorrectInstaller_nonStaged");
}
@Test
public void testRebootlessUpdates() throws Exception {
pushTestApex("test.rebootless_apex_v1.apex");
getDevice().reboot();
runPhase("testRebootlessUpdates");
}
@Test
public void testRebootlessUpdate_hasStagedSessionWithSameApex_fails() throws Exception {
assumeTrue("Device does not support updating APEX",
mHostUtils.isApexUpdateSupported());
runPhase("testRebootlessUpdate_hasStagedSessionWithSameApex_fails");
}
private List<String> getStagingDirectories() throws DeviceNotAvailableException {
String baseDir = "/data/app-staging";
try {