Reject non-staged APEX install if there is staged install of same APEX

There is a interesting interaction between staged and non-staged
installs of the same APEX. Let's say an installer staged v1 -> v2 APEX
update, and then does a non-staged update to v3. After device is
rebooted, apexd will apply the staged v1 -> v2 session, silently
downgrading an APEX from v3.

For apks, this problem is solved by storing an expected version. When an
APK session is being applied during boot, Package Manager will check if
the currently installed version is equal to the expected one stored in
the staged session. If they mismatch, an install is failed.
Unfortunately, implementing the same logic in apexd will require a
non-trivial refactoring which is too late to do in S. Instead we are
just going to fail the non-staged installation.

Test: atest StagedInstallInternalTest
Bug: 187864524
Change-Id: I9000f40cede9a324a5059a09deb8eb5be13b21f9
This commit is contained in:
Nikita Ioffe
2021-07-22 01:37:20 +01:00
parent e501fd8379
commit db9892300e
6 changed files with 120 additions and 0 deletions

View File

@@ -2260,6 +2260,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
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) {

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

@@ -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

@@ -57,6 +57,9 @@ public class StagedInstallInternalTest {
private static final TestApp APEX_WRONG_SHA_V2 = new TestApp(
"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(),
@@ -388,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

@@ -470,6 +470,14 @@ public class StagedInstallInternalTest extends BaseHostJUnit4Test {
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 {