From 0260090727ebe5e651dcbec7fc26d82374cfe4d9 Mon Sep 17 00:00:00 2001 From: Nikita Ioffe Date: Mon, 17 May 2021 12:57:41 +0100 Subject: [PATCH] PackageManager plumbing to support non-staged APEX installs This is a minimal set of changes to support non-staged APEX installs in PackageManager and friends. In a nutshell, PackageManager simply passes the APEX around and delegates the actual installation to ApexManager, which will do the following: * Check that outer .apex container is well formed. * Check that outer .apex container is signed with the correct cert. * Check that install is not a downgrade. * Call into apexd to perform an actual install. * Update internal data structures used by PackageManager's query APIs (getPackageInfo, getAppliationInfo, getInstalledPackages, etc.). In addition this CL implements the following set of policies around non-staged APEX install (some of them are likely to be relaxd in T): * Multi apk install of both APEXes and APKs is not supported. * Rollback enabled non-staged install of an APEX is not supported. Since there are a bunch of host-side tests that expects `adb install foo.apex` to perform a staged install, I've temporarily added `--force-non-staged` option to provide a way to trigger a non-staged APEX update via adb. Once we've migrated all the test cases to explicitly mention `--staged` flow, we can remove this flag and make APEX adb install follow the same logic as APK one (if a developer wants to perform a staged install, they need to explicitly add --staged flag). Test: adb install --force-non-staged shim.apex Test: atest CtsStagedInstallHostTestCases Test: atest CtsRollbackManagerHostTestCases Bug: 187864524 Bug: 188714038 Change-Id: Ia1fb1d07dee465bae572321a510ae78b56c35614 Merged-In: Ia1fb1d07dee465bae572321a510ae78b56c35614 --- .../com/android/server/pm/ApexManager.java | 26 ++++++ .../server/pm/PackageInstallerService.java | 11 +-- .../server/pm/PackageInstallerSession.java | 20 ++--- .../server/pm/PackageManagerService.java | 82 ++++++++++++++++++- .../server/pm/PackageManagerShellCommand.java | 7 ++ 5 files changed, 127 insertions(+), 19 deletions(-) diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 0f6e3842388ea..c97095d8a54e9 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -389,6 +389,11 @@ public abstract class ApexManager { public abstract void reserveSpaceForCompressedApex(CompressedApexInfoList infoList) throws RemoteException; + /** + * Performs a non-staged install of an APEX package with given {@code packagePath}. + */ + abstract void installPackage(String packagePath) throws PackageManagerException; + /** * Dumps various state information to the provided {@link PrintWriter} object. * @@ -974,6 +979,22 @@ public abstract class ApexManager { waitForApexService().reserveSpaceForCompressedApex(infoList); } + @Override + void installPackage(String packagePath) throws PackageManagerException { + try { + // TODO(b/187864524): do pre-install verification. + waitForApexService().installAndActivatePackage(packagePath); + // TODO(b/187864524): update mAllPackagesCache. + } catch (RemoteException e) { + throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + "apexservice not available"); + } catch (Exception e) { + // TODO(b/187864524): is INSTALL_FAILED_INTERNAL_ERROR is the right error code here? + throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, + e.getMessage()); + } + } + /** * Dump information about the packages contained in a particular cache * @param packagesCache the cache to print information about. @@ -1240,6 +1261,11 @@ public abstract class ApexManager { throw new UnsupportedOperationException(); } + @Override + void installPackage(String packagePath) { + throw new UnsupportedOperationException("APEX updates are not supported"); + } + @Override void dump(PrintWriter pw, String packageName) { // No-op diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 62aca3da01a16..1f0a8caa0ccfa 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -635,13 +635,14 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements throw new IllegalArgumentException( "This device doesn't support the installation of APEX files"); } - if (!params.isStaged) { - throw new IllegalArgumentException( - "APEX files can only be installed as part of a staged session."); - } if (params.isMultiPackage) { throw new IllegalArgumentException("A multi-session can't be set as APEX."); } + if (!params.isStaged + && (params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) { + throw new IllegalArgumentException( + "Non-staged APEX session doesn't support INSTALL_ENABLE_ROLLBACK"); + } } if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0 @@ -877,7 +878,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } private File buildSessionDir(int sessionId, SessionParams params) { - if (params.isStaged) { + if (params.isStaged || (params.installFlags & PackageManager.INSTALL_APEX) != 0) { final File sessionStagingDir = Environment.getDataStagingDirectory(params.volumeUuid); return new File(sessionStagingDir, "session_" + sessionId); } diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 1da80f4b1f772..8202587ecf535 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -2246,12 +2246,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { return; } - if (isApexSession()) { - destroyInternal(); - dispatchSessionFinished(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, - "APEX packages can only be installed using staged sessions.", null); - return; - } verify(); } @@ -2276,6 +2270,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { synchronized (mLock) { childSessions = getChildSessionsLocked(); } + // Spot check to reject a non-staged multi package install of APEXes and APKs. + if (!params.isStaged && containsApkSession() + && sessionContains(s -> s.isApexSession())) { + throw new PackageManagerException( + PackageManager.INSTALL_FAILED_SESSION_INVALID, + "Non-staged multi package install of APEX and APK packages is not supported"); + } List verifyingChildSessions = new ArrayList<>(childSessions.size()); boolean success = true; @@ -2320,8 +2321,6 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private void installNonStaged() throws PackageManagerException { - Preconditions.checkArgument(containsApkSession()); - final PackageManagerService.InstallParams installingSession = makeInstallParams(); if (installingSession == null) { throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, @@ -2609,8 +2608,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } - // Do not try to install apex session. Parent session will have at least one apk session. - if (!isMultiPackage() && isApexSession()) { + // Do not try to install staged apex session. Parent session will have at least one apk + // session. + if (!isMultiPackage() && isApexSession() && params.isStaged) { sendUpdateToRemoteStatusReceiver(INSTALL_SUCCEEDED, "Apex package should have been installed by apexd", null); return null; diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 6fccbdd0370e8..7155a1d21b719 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -16428,25 +16428,95 @@ public class PackageManagerService extends IPackageManager.Stub private void processInstallRequestsAsync(boolean success, List installRequests) { mHandler.post(() -> { + List apexInstallRequests = new ArrayList<>(); + List apkInstallRequests = new ArrayList<>(); + for (InstallRequest request : installRequests) { + if ((request.args.installFlags & PackageManager.INSTALL_APEX) != 0) { + apexInstallRequests.add(request); + } else { + apkInstallRequests.add(request); + } + } + // Note: supporting multi package install of both APEXes and APKs might requir some + // thinking to ensure atomicity of the install. + if (!apexInstallRequests.isEmpty() && !apkInstallRequests.isEmpty()) { + // This should've been caught at the validation step, but for some reason wasn't. + throw new IllegalStateException( + "Attempted to do a multi package install of both APEXes and APKs"); + } + if (!apexInstallRequests.isEmpty()) { + if (success) { + // Since installApexPackages requires talking to external service (apexd), we + // schedule to run it async. Once it finishes, it will resume the install. + Thread t = new Thread(() -> installApexPackagesTraced(apexInstallRequests), + "installApexPackages"); + t.start(); + } else { + // Non-staged APEX installation failed somewhere before + // processInstallRequestAsync. In that case just notify the observer about the + // failure. + InstallRequest request = apexInstallRequests.get(0); + notifyInstallObserver(request.installResult, request.args.observer); + } + return; + } if (success) { - for (InstallRequest request : installRequests) { + for (InstallRequest request : apkInstallRequests) { request.args.doPreInstall(request.installResult.returnCode); } synchronized (mInstallLock) { - installPackagesTracedLI(installRequests); + installPackagesTracedLI(apkInstallRequests); } - for (InstallRequest request : installRequests) { + for (InstallRequest request : apkInstallRequests) { request.args.doPostInstall( request.installResult.returnCode, request.installResult.uid); } } - for (InstallRequest request : installRequests) { + for (InstallRequest request : apkInstallRequests) { restoreAndPostInstall(request.args.user.getIdentifier(), request.installResult, new PostInstallData(request.args, request.installResult, null)); } }); } + private void installApexPackagesTraced(List requests) { + try { + Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installApexPackages"); + installApexPackages(requests); + } finally { + Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + } + } + + private void installApexPackages(List requests) { + if (requests.isEmpty()) { + return; + } + if (requests.size() != 1) { + throw new IllegalStateException( + "Only a non-staged install of a single APEX is supported"); + } + InstallRequest request = requests.get(0); + try { + // Should directory scanning logic be moved to ApexManager for better test coverage? + final File dir = request.args.origin.resolvedFile; + final String[] apexes = dir.list(); + if (apexes == null) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + dir.getAbsolutePath() + " is not a directory"); + } + if (apexes.length != 1) { + throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, + "Expected exactly one .apex file under " + dir.getAbsolutePath() + + " got: " + apexes.length); + } + mApexManager.installPackage(dir.getAbsolutePath() + "/" + apexes[0]); + } catch (PackageManagerException e) { + request.installResult.setError("APEX installation failed", e); + } + notifyInstallObserver(request.installResult, request.args.observer); + } + private PackageInstalledInfo createPackageInstalledInfo( int currentStatus) { PackageInstalledInfo res = new PackageInstalledInfo(); @@ -17058,6 +17128,10 @@ public class PackageManagerService extends IPackageManager.Stub * on the install location. */ public void handleStartCopy() { + if ((installFlags & PackageManager.INSTALL_APEX) != 0) { + mRet = INSTALL_SUCCEEDED; + return; + } PackageInfoLite pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mContext, mPackageLite, origin.resolvedPath, installFlags, packageAbiOverride); diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index a69caa914846a..4963d64797e57 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -2689,6 +2689,7 @@ class PackageManagerShellCommand extends ShellCommand { String opt; boolean replaceExisting = true; + boolean forceNonStaged = false; while ((opt = getNextOption()) != null) { switch (opt) { case "-r": // ignore @@ -2783,6 +2784,9 @@ class PackageManagerShellCommand extends ShellCommand { sessionParams.setInstallAsApex(); sessionParams.setStaged(); break; + case "--force-non-staged": + forceNonStaged = true; + break; case "--multi-package": sessionParams.setMultiPackage(); break; @@ -2818,6 +2822,9 @@ class PackageManagerShellCommand extends ShellCommand { if (replaceExisting) { sessionParams.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING; } + if (forceNonStaged) { + sessionParams.isStaged = false; + } return params; }