| /* |
| * Copyright (C) 2014 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; |
| |
| import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; |
| import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA; |
| import static android.content.pm.PackageManager.INSTALL_FAILED_CONTAINER_ERROR; |
| import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; |
| import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR; |
| import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK; |
| import static android.content.pm.PackageParser.APK_FILE_EXTENSION; |
| import static android.system.OsConstants.O_CREAT; |
| import static android.system.OsConstants.O_RDONLY; |
| import static android.system.OsConstants.O_WRONLY; |
| |
| import static com.android.internal.util.XmlUtils.readBitmapAttribute; |
| import static com.android.internal.util.XmlUtils.readBooleanAttribute; |
| import static com.android.internal.util.XmlUtils.readIntAttribute; |
| import static com.android.internal.util.XmlUtils.readLongAttribute; |
| import static com.android.internal.util.XmlUtils.readStringAttribute; |
| import static com.android.internal.util.XmlUtils.readUriAttribute; |
| import static com.android.internal.util.XmlUtils.writeBooleanAttribute; |
| import static com.android.internal.util.XmlUtils.writeIntAttribute; |
| import static com.android.internal.util.XmlUtils.writeLongAttribute; |
| import static com.android.internal.util.XmlUtils.writeStringAttribute; |
| import static com.android.internal.util.XmlUtils.writeUriAttribute; |
| import static com.android.server.pm.PackageInstallerService.prepareStageDir; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.admin.DeviceAdminInfo; |
| import android.app.admin.DevicePolicyManagerInternal; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IPackageInstallObserver2; |
| import android.content.pm.IPackageInstallerSession; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageInstaller; |
| import android.content.pm.PackageInstaller.SessionInfo; |
| import android.content.pm.PackageInstaller.SessionParams; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.ApkLite; |
| import android.content.pm.PackageParser.PackageLite; |
| import android.content.pm.PackageParser.PackageParserException; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.os.Binder; |
| import android.os.Bundle; |
| import android.os.FileBridge; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.ParcelFileDescriptor; |
| import android.os.ParcelableException; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.RevocableFileDescriptor; |
| import android.os.UserHandle; |
| import android.os.storage.StorageManager; |
| import android.system.ErrnoException; |
| import android.system.Int64Ref; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.system.StructStat; |
| import android.text.TextUtils; |
| import android.util.ArraySet; |
| import android.util.ExceptionUtils; |
| import android.util.MathUtils; |
| import android.util.Slog; |
| import android.util.apk.ApkSignatureVerifier; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.content.NativeLibraryHelper; |
| import com.android.internal.content.PackageHelper; |
| import com.android.internal.os.SomeArgs; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.internal.util.Preconditions; |
| import com.android.server.LocalServices; |
| import com.android.server.pm.Installer.InstallerException; |
| import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapter; |
| |
| import android.content.pm.dex.DexMetadataHelper; |
| import libcore.io.IoUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| import org.xmlpull.v1.XmlSerializer; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileFilter; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.security.cert.CertificateException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicInteger; |
| |
| public class PackageInstallerSession extends IPackageInstallerSession.Stub { |
| private static final String TAG = "PackageInstaller"; |
| private static final boolean LOGD = true; |
| private static final String REMOVE_SPLIT_MARKER_EXTENSION = ".removed"; |
| |
| private static final int MSG_EARLY_BIND = 0; |
| private static final int MSG_COMMIT = 1; |
| private static final int MSG_ON_PACKAGE_INSTALLED = 2; |
| |
| /** XML constants used for persisting a session */ |
| static final String TAG_SESSION = "session"; |
| private static final String TAG_GRANTED_RUNTIME_PERMISSION = "granted-runtime-permission"; |
| private static final String ATTR_SESSION_ID = "sessionId"; |
| private static final String ATTR_USER_ID = "userId"; |
| private static final String ATTR_INSTALLER_PACKAGE_NAME = "installerPackageName"; |
| private static final String ATTR_INSTALLER_UID = "installerUid"; |
| private static final String ATTR_CREATED_MILLIS = "createdMillis"; |
| private static final String ATTR_SESSION_STAGE_DIR = "sessionStageDir"; |
| private static final String ATTR_SESSION_STAGE_CID = "sessionStageCid"; |
| private static final String ATTR_PREPARED = "prepared"; |
| private static final String ATTR_SEALED = "sealed"; |
| private static final String ATTR_MODE = "mode"; |
| private static final String ATTR_INSTALL_FLAGS = "installFlags"; |
| private static final String ATTR_INSTALL_LOCATION = "installLocation"; |
| private static final String ATTR_SIZE_BYTES = "sizeBytes"; |
| private static final String ATTR_APP_PACKAGE_NAME = "appPackageName"; |
| @Deprecated |
| private static final String ATTR_APP_ICON = "appIcon"; |
| private static final String ATTR_APP_LABEL = "appLabel"; |
| private static final String ATTR_ORIGINATING_URI = "originatingUri"; |
| private static final String ATTR_ORIGINATING_UID = "originatingUid"; |
| private static final String ATTR_REFERRER_URI = "referrerUri"; |
| private static final String ATTR_ABI_OVERRIDE = "abiOverride"; |
| private static final String ATTR_VOLUME_UUID = "volumeUuid"; |
| private static final String ATTR_NAME = "name"; |
| private static final String ATTR_INSTALL_REASON = "installRason"; |
| |
| // TODO: enforce INSTALL_ALLOW_TEST |
| // TODO: enforce INSTALL_ALLOW_DOWNGRADE |
| |
| private final PackageInstallerService.InternalCallback mCallback; |
| private final Context mContext; |
| private final PackageManagerService mPm; |
| private final Handler mHandler; |
| |
| final int sessionId; |
| final int userId; |
| final SessionParams params; |
| final long createdMillis; |
| final int defaultContainerGid; |
| |
| /** Staging location where client data is written. */ |
| final File stageDir; |
| final String stageCid; |
| |
| private final AtomicInteger mActiveCount = new AtomicInteger(); |
| |
| private final Object mLock = new Object(); |
| |
| /** Uid of the creator of this session. */ |
| private final int mOriginalInstallerUid; |
| |
| /** Package of the owner of the installer session */ |
| @GuardedBy("mLock") |
| private String mInstallerPackageName; |
| |
| /** Uid of the owner of the installer session */ |
| @GuardedBy("mLock") |
| private int mInstallerUid; |
| |
| @GuardedBy("mLock") |
| private float mClientProgress = 0; |
| @GuardedBy("mLock") |
| private float mInternalProgress = 0; |
| |
| @GuardedBy("mLock") |
| private float mProgress = 0; |
| @GuardedBy("mLock") |
| private float mReportedProgress = -1; |
| |
| /** State of the session. */ |
| @GuardedBy("mLock") |
| private boolean mPrepared = false; |
| @GuardedBy("mLock") |
| private boolean mSealed = false; |
| @GuardedBy("mLock") |
| private boolean mCommitted = false; |
| @GuardedBy("mLock") |
| private boolean mRelinquished = false; |
| @GuardedBy("mLock") |
| private boolean mDestroyed = false; |
| |
| /** Permissions have been accepted by the user (see {@link #setPermissionsResult}) */ |
| @GuardedBy("mLock") |
| private boolean mPermissionsManuallyAccepted = false; |
| |
| @GuardedBy("mLock") |
| private int mFinalStatus; |
| @GuardedBy("mLock") |
| private String mFinalMessage; |
| |
| @GuardedBy("mLock") |
| private final ArrayList<RevocableFileDescriptor> mFds = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final ArrayList<FileBridge> mBridges = new ArrayList<>(); |
| |
| @GuardedBy("mLock") |
| private IPackageInstallObserver2 mRemoteObserver; |
| |
| /** Fields derived from commit parsing */ |
| @GuardedBy("mLock") |
| private String mPackageName; |
| @GuardedBy("mLock") |
| private long mVersionCode; |
| @GuardedBy("mLock") |
| private PackageParser.SigningDetails mSigningDetails; |
| |
| /** |
| * Path to the validated base APK for this session, which may point at an |
| * APK inside the session (when the session defines the base), or it may |
| * point at the existing base APK (when adding splits to an existing app). |
| * <p> |
| * This is used when confirming permissions, since we can't fully stage the |
| * session inside an ASEC before confirming with user. |
| */ |
| @GuardedBy("mLock") |
| private File mResolvedBaseFile; |
| |
| @GuardedBy("mLock") |
| private File mResolvedStageDir; |
| |
| @GuardedBy("mLock") |
| private final List<File> mResolvedStagedFiles = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final List<File> mResolvedInheritedFiles = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private final List<String> mResolvedInstructionSets = new ArrayList<>(); |
| @GuardedBy("mLock") |
| private File mInheritedFilesBase; |
| |
| private static final FileFilter sAddedFilter = new FileFilter() { |
| @Override |
| public boolean accept(File file) { |
| // Installers can't stage directories, so it's fine to ignore |
| // entries like "lost+found". |
| if (file.isDirectory()) return false; |
| if (file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false; |
| if (DexMetadataHelper.isDexMetadataFile(file)) return false; |
| return true; |
| } |
| }; |
| private static final FileFilter sRemovedFilter = new FileFilter() { |
| @Override |
| public boolean accept(File file) { |
| if (file.isDirectory()) return false; |
| if (!file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false; |
| return true; |
| } |
| }; |
| |
| private final Handler.Callback mHandlerCallback = new Handler.Callback() { |
| @Override |
| public boolean handleMessage(Message msg) { |
| switch (msg.what) { |
| case MSG_EARLY_BIND: |
| earlyBindToDefContainer(); |
| break; |
| case MSG_COMMIT: |
| synchronized (mLock) { |
| try { |
| commitLocked(); |
| } catch (PackageManagerException e) { |
| final String completeMsg = ExceptionUtils.getCompleteMessage(e); |
| Slog.e(TAG, |
| "Commit of session " + sessionId + " failed: " + completeMsg); |
| destroyInternal(); |
| dispatchSessionFinished(e.error, completeMsg, null); |
| } |
| } |
| |
| break; |
| case MSG_ON_PACKAGE_INSTALLED: |
| final SomeArgs args = (SomeArgs) msg.obj; |
| final String packageName = (String) args.arg1; |
| final String message = (String) args.arg2; |
| final Bundle extras = (Bundle) args.arg3; |
| final IPackageInstallObserver2 observer = (IPackageInstallObserver2) args.arg4; |
| final int returnCode = args.argi1; |
| args.recycle(); |
| |
| try { |
| observer.onPackageInstalled(packageName, returnCode, message, extras); |
| } catch (RemoteException ignored) { |
| } |
| |
| break; |
| } |
| |
| return true; |
| } |
| }; |
| |
| private void earlyBindToDefContainer() { |
| mPm.earlyBindToDefContainer(); |
| } |
| |
| /** |
| * @return {@code true} iff the installing is app an device owner or affiliated profile owner. |
| */ |
| @GuardedBy("mLock") |
| private boolean isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked() { |
| DevicePolicyManagerInternal dpmi = |
| LocalServices.getService(DevicePolicyManagerInternal.class); |
| return dpmi != null && dpmi.isActiveAdminWithPolicy(mInstallerUid, |
| DeviceAdminInfo.USES_POLICY_PROFILE_OWNER) && dpmi.isUserAffiliatedWithDevice( |
| userId); |
| } |
| |
| /** |
| * Checks if the permissions still need to be confirmed. |
| * |
| * <p>This is dependant on the identity of the installer, hence this cannot be cached if the |
| * installer might still {@link #transfer(String) change}. |
| * |
| * @return {@code true} iff we need to ask to confirm the permissions? |
| */ |
| @GuardedBy("mLock") |
| private boolean needToAskForPermissionsLocked() { |
| if (mPermissionsManuallyAccepted) { |
| return false; |
| } |
| |
| final boolean isInstallPermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final boolean isSelfUpdatePermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_SELF_UPDATES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final boolean isUpdatePermissionGranted = |
| (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGE_UPDATES, |
| mInstallerUid) == PackageManager.PERMISSION_GRANTED); |
| final int targetPackageUid = mPm.getPackageUid(mPackageName, 0, userId); |
| final boolean isPermissionGranted = isInstallPermissionGranted |
| || (isUpdatePermissionGranted && targetPackageUid != -1) |
| || (isSelfUpdatePermissionGranted && targetPackageUid == mInstallerUid); |
| final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID); |
| final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID); |
| final boolean forcePermissionPrompt = |
| (params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0; |
| |
| // Device owners and affiliated profile owners are allowed to silently install packages, so |
| // the permission check is waived if the installer is the device owner. |
| return forcePermissionPrompt || !(isPermissionGranted || isInstallerRoot |
| || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked()); |
| } |
| |
| public PackageInstallerSession(PackageInstallerService.InternalCallback callback, |
| Context context, PackageManagerService pm, Looper looper, int sessionId, int userId, |
| String installerPackageName, int installerUid, SessionParams params, long createdMillis, |
| File stageDir, String stageCid, boolean prepared, boolean sealed) { |
| mCallback = callback; |
| mContext = context; |
| mPm = pm; |
| mHandler = new Handler(looper, mHandlerCallback); |
| |
| this.sessionId = sessionId; |
| this.userId = userId; |
| mOriginalInstallerUid = installerUid; |
| mInstallerPackageName = installerPackageName; |
| mInstallerUid = installerUid; |
| this.params = params; |
| this.createdMillis = createdMillis; |
| this.stageDir = stageDir; |
| this.stageCid = stageCid; |
| |
| if ((stageDir == null) == (stageCid == null)) { |
| throw new IllegalArgumentException( |
| "Exactly one of stageDir or stageCid stage must be set"); |
| } |
| |
| mPrepared = prepared; |
| |
| if (sealed) { |
| synchronized (mLock) { |
| try { |
| sealAndValidateLocked(); |
| } catch (PackageManagerException | IOException e) { |
| destroyInternal(); |
| throw new IllegalArgumentException(e); |
| } |
| } |
| } |
| |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| final int uid = mPm.getPackageUid(PackageManagerService.DEFAULT_CONTAINER_PACKAGE, |
| PackageManager.MATCH_SYSTEM_ONLY, UserHandle.USER_SYSTEM); |
| defaultContainerGid = UserHandle.getSharedAppGid(uid); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| // attempt to bind to the DefContainer as early as possible |
| if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) { |
| mHandler.sendMessage(mHandler.obtainMessage(MSG_EARLY_BIND)); |
| } |
| } |
| |
| public SessionInfo generateInfo() { |
| return generateInfo(true); |
| } |
| |
| public SessionInfo generateInfo(boolean includeIcon) { |
| final SessionInfo info = new SessionInfo(); |
| synchronized (mLock) { |
| info.sessionId = sessionId; |
| info.installerPackageName = mInstallerPackageName; |
| info.resolvedBaseCodePath = (mResolvedBaseFile != null) ? |
| mResolvedBaseFile.getAbsolutePath() : null; |
| info.progress = mProgress; |
| info.sealed = mSealed; |
| info.active = mActiveCount.get() > 0; |
| |
| info.mode = params.mode; |
| info.installReason = params.installReason; |
| info.sizeBytes = params.sizeBytes; |
| info.appPackageName = params.appPackageName; |
| if (includeIcon) { |
| info.appIcon = params.appIcon; |
| } |
| info.appLabel = params.appLabel; |
| |
| info.installLocation = params.installLocation; |
| info.originatingUri = params.originatingUri; |
| info.originatingUid = params.originatingUid; |
| info.referrerUri = params.referrerUri; |
| info.grantedRuntimePermissions = params.grantedRuntimePermissions; |
| info.installFlags = params.installFlags; |
| } |
| return info; |
| } |
| |
| public boolean isPrepared() { |
| synchronized (mLock) { |
| return mPrepared; |
| } |
| } |
| |
| public boolean isSealed() { |
| synchronized (mLock) { |
| return mSealed; |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void assertPreparedAndNotSealedLocked(String cookie) { |
| assertPreparedAndNotCommittedOrDestroyedLocked(cookie); |
| if (mSealed) { |
| throw new SecurityException(cookie + " not allowed after sealing"); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void assertPreparedAndNotCommittedOrDestroyedLocked(String cookie) { |
| assertPreparedAndNotDestroyedLocked(cookie); |
| if (mCommitted) { |
| throw new SecurityException(cookie + " not allowed after commit"); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void assertPreparedAndNotDestroyedLocked(String cookie) { |
| if (!mPrepared) { |
| throw new IllegalStateException(cookie + " before prepared"); |
| } |
| if (mDestroyed) { |
| throw new SecurityException(cookie + " not allowed after destruction"); |
| } |
| } |
| |
| /** |
| * Resolve the actual location where staged data should be written. This |
| * might point at an ASEC mount point, which is why we delay path resolution |
| * until someone actively works with the session. |
| */ |
| @GuardedBy("mLock") |
| private File resolveStageDirLocked() throws IOException { |
| if (mResolvedStageDir == null) { |
| if (stageDir != null) { |
| mResolvedStageDir = stageDir; |
| } else { |
| throw new IOException("Missing stageDir"); |
| } |
| } |
| return mResolvedStageDir; |
| } |
| |
| @Override |
| public void setClientProgress(float progress) { |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| |
| // Always publish first staging movement |
| final boolean forcePublish = (mClientProgress == 0); |
| mClientProgress = progress; |
| computeProgressLocked(forcePublish); |
| } |
| } |
| |
| @Override |
| public void addClientProgress(float progress) { |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| |
| setClientProgress(mClientProgress + progress); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void computeProgressLocked(boolean forcePublish) { |
| mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f) |
| + MathUtils.constrain(mInternalProgress * 0.2f, 0f, 0.2f); |
| |
| // Only publish when meaningful change |
| if (forcePublish || Math.abs(mProgress - mReportedProgress) >= 0.01) { |
| mReportedProgress = mProgress; |
| mCallback.onSessionProgressChanged(this, mProgress); |
| } |
| } |
| |
| @Override |
| public String[] getNames() { |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotCommittedOrDestroyedLocked("getNames"); |
| |
| try { |
| return resolveStageDirLocked().list(); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| } |
| |
| @Override |
| public void removeSplit(String splitName) { |
| if (TextUtils.isEmpty(params.appPackageName)) { |
| throw new IllegalStateException("Must specify package name to remove a split"); |
| } |
| |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotCommittedOrDestroyedLocked("removeSplit"); |
| |
| try { |
| createRemoveSplitMarkerLocked(splitName); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| } |
| |
| private void createRemoveSplitMarkerLocked(String splitName) throws IOException { |
| try { |
| final String markerName = splitName + REMOVE_SPLIT_MARKER_EXTENSION; |
| if (!FileUtils.isValidExtFilename(markerName)) { |
| throw new IllegalArgumentException("Invalid marker: " + markerName); |
| } |
| final File target = new File(resolveStageDirLocked(), markerName); |
| target.createNewFile(); |
| Os.chmod(target.getAbsolutePath(), 0 /*mode*/); |
| } catch (ErrnoException e) { |
| throw e.rethrowAsIOException(); |
| } |
| } |
| |
| @Override |
| public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { |
| try { |
| return doWriteInternal(name, offsetBytes, lengthBytes, null); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| |
| @Override |
| public void write(String name, long offsetBytes, long lengthBytes, |
| ParcelFileDescriptor fd) { |
| try { |
| doWriteInternal(name, offsetBytes, lengthBytes, fd); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| |
| private ParcelFileDescriptor doWriteInternal(String name, long offsetBytes, long lengthBytes, |
| ParcelFileDescriptor incomingFd) throws IOException { |
| // Quick sanity check of state, and allocate a pipe for ourselves. We |
| // then do heavy disk allocation outside the lock, but this open pipe |
| // will block any attempted install transitions. |
| final RevocableFileDescriptor fd; |
| final FileBridge bridge; |
| final File stageDir; |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotSealedLocked("openWrite"); |
| |
| if (PackageInstaller.ENABLE_REVOCABLE_FD) { |
| fd = new RevocableFileDescriptor(); |
| bridge = null; |
| mFds.add(fd); |
| } else { |
| fd = null; |
| bridge = new FileBridge(); |
| mBridges.add(bridge); |
| } |
| |
| stageDir = resolveStageDirLocked(); |
| } |
| |
| try { |
| // Use installer provided name for now; we always rename later |
| if (!FileUtils.isValidExtFilename(name)) { |
| throw new IllegalArgumentException("Invalid name: " + name); |
| } |
| final File target; |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| target = new File(stageDir, name); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| |
| // TODO: this should delegate to DCS so the system process avoids |
| // holding open FDs into containers. |
| final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), |
| O_CREAT | O_WRONLY, 0644); |
| Os.chmod(target.getAbsolutePath(), 0644); |
| |
| // If caller specified a total length, allocate it for them. Free up |
| // cache space to grow, if needed. |
| if (stageDir != null && lengthBytes > 0) { |
| mContext.getSystemService(StorageManager.class).allocateBytes(targetFd, lengthBytes, |
| PackageHelper.translateAllocateFlags(params.installFlags)); |
| } |
| |
| if (offsetBytes > 0) { |
| Os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); |
| } |
| |
| if (incomingFd != null) { |
| switch (Binder.getCallingUid()) { |
| case android.os.Process.SHELL_UID: |
| case android.os.Process.ROOT_UID: |
| break; |
| default: |
| throw new SecurityException("Reverse mode only supported from shell"); |
| } |
| |
| // In "reverse" mode, we're streaming data ourselves from the |
| // incoming FD, which means we never have to hand out our |
| // sensitive internal FD. We still rely on a "bridge" being |
| // inserted above to hold the session active. |
| try { |
| final Int64Ref last = new Int64Ref(0); |
| FileUtils.copy(incomingFd.getFileDescriptor(), targetFd, (long progress) -> { |
| if (params.sizeBytes > 0) { |
| final long delta = progress - last.value; |
| last.value = progress; |
| addClientProgress((float) delta / (float) params.sizeBytes); |
| } |
| }, null, lengthBytes); |
| } finally { |
| IoUtils.closeQuietly(targetFd); |
| IoUtils.closeQuietly(incomingFd); |
| |
| // We're done here, so remove the "bridge" that was holding |
| // the session active. |
| synchronized (mLock) { |
| if (PackageInstaller.ENABLE_REVOCABLE_FD) { |
| mFds.remove(fd); |
| } else { |
| mBridges.remove(bridge); |
| } |
| } |
| } |
| return null; |
| } else if (PackageInstaller.ENABLE_REVOCABLE_FD) { |
| fd.init(mContext, targetFd); |
| return fd.getRevocableFileDescriptor(); |
| } else { |
| bridge.setTargetFile(targetFd); |
| bridge.start(); |
| return new ParcelFileDescriptor(bridge.getClientSocket()); |
| } |
| |
| } catch (ErrnoException e) { |
| throw e.rethrowAsIOException(); |
| } |
| } |
| |
| @Override |
| public ParcelFileDescriptor openRead(String name) { |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotCommittedOrDestroyedLocked("openRead"); |
| try { |
| return openReadInternalLocked(name); |
| } catch (IOException e) { |
| throw ExceptionUtils.wrap(e); |
| } |
| } |
| } |
| |
| private ParcelFileDescriptor openReadInternalLocked(String name) throws IOException { |
| try { |
| if (!FileUtils.isValidExtFilename(name)) { |
| throw new IllegalArgumentException("Invalid name: " + name); |
| } |
| final File target = new File(resolveStageDirLocked(), name); |
| final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), O_RDONLY, 0); |
| return new ParcelFileDescriptor(targetFd); |
| } catch (ErrnoException e) { |
| throw e.rethrowAsIOException(); |
| } |
| } |
| |
| /** |
| * Check if the caller is the owner of this session. Otherwise throw a |
| * {@link SecurityException}. |
| */ |
| @GuardedBy("mLock") |
| private void assertCallerIsOwnerOrRootLocked() { |
| final int callingUid = Binder.getCallingUid(); |
| if (callingUid != Process.ROOT_UID && callingUid != mInstallerUid) { |
| throw new SecurityException("Session does not belong to uid " + callingUid); |
| } |
| } |
| |
| /** |
| * If anybody is reading or writing data of the session, throw an {@link SecurityException}. |
| */ |
| @GuardedBy("mLock") |
| private void assertNoWriteFileTransfersOpenLocked() { |
| // Verify that all writers are hands-off |
| for (RevocableFileDescriptor fd : mFds) { |
| if (!fd.isRevoked()) { |
| throw new SecurityException("Files still open"); |
| } |
| } |
| for (FileBridge bridge : mBridges) { |
| if (!bridge.isClosed()) { |
| throw new SecurityException("Files still open"); |
| } |
| } |
| } |
| |
| @Override |
| public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) { |
| Preconditions.checkNotNull(statusReceiver); |
| |
| final boolean wasSealed; |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotDestroyedLocked("commit"); |
| |
| final PackageInstallObserverAdapter adapter = new PackageInstallObserverAdapter( |
| mContext, statusReceiver, sessionId, |
| isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked(), userId); |
| mRemoteObserver = adapter.getBinder(); |
| |
| if (forTransfer) { |
| mContext.enforceCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES, null); |
| |
| if (mInstallerUid == mOriginalInstallerUid) { |
| throw new IllegalArgumentException("Session has not been transferred"); |
| } |
| } else { |
| if (mInstallerUid != mOriginalInstallerUid) { |
| throw new IllegalArgumentException("Session has been transferred"); |
| } |
| } |
| |
| wasSealed = mSealed; |
| if (!mSealed) { |
| try { |
| sealAndValidateLocked(); |
| } catch (IOException e) { |
| throw new IllegalArgumentException(e); |
| } catch (PackageManagerException e) { |
| // Do now throw an exception here to stay compatible with O and older |
| destroyInternal(); |
| dispatchSessionFinished(e.error, ExceptionUtils.getCompleteMessage(e), null); |
| return; |
| } |
| } |
| |
| // Client staging is fully done at this point |
| mClientProgress = 1f; |
| computeProgressLocked(true); |
| |
| // This ongoing commit should keep session active, even though client |
| // will probably close their end. |
| mActiveCount.incrementAndGet(); |
| |
| mCommitted = true; |
| mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); |
| } |
| |
| if (!wasSealed) { |
| // Persist the fact that we've sealed ourselves to prevent |
| // mutations of any hard links we create. We do this without holding |
| // the session lock, since otherwise it's a lock inversion. |
| mCallback.onSessionSealedBlocking(this); |
| } |
| } |
| |
| /** |
| * Seal the session to prevent further modification and validate the contents of it. |
| * |
| * <p>The session will be sealed after calling this method even if it failed. |
| * |
| * @throws PackageManagerException if the session was sealed but something went wrong. If the |
| * session was sealed this is the only possible exception. |
| */ |
| @GuardedBy("mLock") |
| private void sealAndValidateLocked() throws PackageManagerException, IOException { |
| assertNoWriteFileTransfersOpenLocked(); |
| assertPreparedAndNotDestroyedLocked("sealing of session"); |
| |
| final PackageInfo pkgInfo = mPm.getPackageInfo( |
| params.appPackageName, PackageManager.GET_SIGNATURES |
| | PackageManager.MATCH_STATIC_SHARED_LIBRARIES /*flags*/, userId); |
| |
| resolveStageDirLocked(); |
| |
| mSealed = true; |
| |
| // Verify that stage looks sane with respect to existing application. |
| // This currently only ensures packageName, versionCode, and certificate |
| // consistency. |
| try { |
| validateInstallLocked(pkgInfo); |
| } catch (PackageManagerException e) { |
| throw e; |
| } catch (Throwable e) { |
| // Convert all exceptions into package manager exceptions as only those are handled |
| // in the code above |
| throw new PackageManagerException(e); |
| } |
| |
| // Read transfers from the original owner stay open, but as the session's data |
| // cannot be modified anymore, there is no leak of information. |
| } |
| |
| @Override |
| public void transfer(String packageName) { |
| Preconditions.checkNotNull(packageName); |
| |
| ApplicationInfo newOwnerAppInfo = mPm.getApplicationInfo(packageName, 0, userId); |
| if (newOwnerAppInfo == null) { |
| throw new ParcelableException(new PackageManager.NameNotFoundException(packageName)); |
| } |
| |
| if (PackageManager.PERMISSION_GRANTED != mPm.checkUidPermission( |
| Manifest.permission.INSTALL_PACKAGES, newOwnerAppInfo.uid)) { |
| throw new SecurityException("Destination package " + packageName + " does not have " |
| + "the " + Manifest.permission.INSTALL_PACKAGES + " permission"); |
| } |
| |
| // Only install flags that can be verified by the app the session is transferred to are |
| // allowed. The parameters can be read via PackageInstaller.SessionInfo. |
| if (!params.areHiddenOptionsSet()) { |
| throw new SecurityException("Can only transfer sessions that use public options"); |
| } |
| |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| assertPreparedAndNotSealedLocked("transfer"); |
| |
| try { |
| sealAndValidateLocked(); |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } catch (PackageManagerException e) { |
| // Session is sealed but could not be verified, we need to destroy it |
| destroyInternal(); |
| dispatchSessionFinished(e.error, ExceptionUtils.getCompleteMessage(e), null); |
| |
| throw new IllegalArgumentException("Package is not valid", e); |
| } |
| |
| if (!mPackageName.equals(mInstallerPackageName)) { |
| throw new SecurityException("Can only transfer sessions that update the original " |
| + "installer"); |
| } |
| |
| mInstallerPackageName = packageName; |
| mInstallerUid = newOwnerAppInfo.uid; |
| } |
| |
| // Persist the fact that we've sealed ourselves to prevent |
| // mutations of any hard links we create. We do this without holding |
| // the session lock, since otherwise it's a lock inversion. |
| mCallback.onSessionSealedBlocking(this); |
| } |
| |
| @GuardedBy("mLock") |
| private void commitLocked() |
| throws PackageManagerException { |
| if (mDestroyed) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session destroyed"); |
| } |
| if (!mSealed) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, "Session not sealed"); |
| } |
| |
| Preconditions.checkNotNull(mPackageName); |
| Preconditions.checkNotNull(mSigningDetails); |
| Preconditions.checkNotNull(mResolvedBaseFile); |
| |
| if (needToAskForPermissionsLocked()) { |
| // User needs to accept permissions; give installer an intent they |
| // can use to involve user. |
| final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS); |
| intent.setPackage(mContext.getPackageManager().getPermissionControllerPackageName()); |
| intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId); |
| try { |
| mRemoteObserver.onUserActionRequired(intent); |
| } catch (RemoteException ignored) { |
| } |
| |
| // Commit was keeping session marked as active until now; release |
| // that extra refcount so session appears idle. |
| closeInternal(false); |
| return; |
| } |
| |
| // Inherit any packages and native libraries from existing install that |
| // haven't been overridden. |
| if (params.mode == SessionParams.MODE_INHERIT_EXISTING) { |
| try { |
| final List<File> fromFiles = mResolvedInheritedFiles; |
| final File toDir = resolveStageDirLocked(); |
| |
| if (LOGD) Slog.d(TAG, "Inherited files: " + mResolvedInheritedFiles); |
| if (!mResolvedInheritedFiles.isEmpty() && mInheritedFilesBase == null) { |
| throw new IllegalStateException("mInheritedFilesBase == null"); |
| } |
| |
| if (isLinkPossible(fromFiles, toDir)) { |
| if (!mResolvedInstructionSets.isEmpty()) { |
| final File oatDir = new File(toDir, "oat"); |
| createOatDirs(mResolvedInstructionSets, oatDir); |
| } |
| linkFiles(fromFiles, toDir, mInheritedFilesBase); |
| } else { |
| // TODO: this should delegate to DCS so the system process |
| // avoids holding open FDs into containers. |
| copyFiles(fromFiles, toDir); |
| } |
| } catch (IOException e) { |
| throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE, |
| "Failed to inherit existing install", e); |
| } |
| } |
| |
| // TODO: surface more granular state from dexopt |
| mInternalProgress = 0.5f; |
| computeProgressLocked(true); |
| |
| // Unpack native libraries |
| extractNativeLibraries(mResolvedStageDir, params.abiOverride); |
| |
| // We've reached point of no return; call into PMS to install the stage. |
| // Regardless of success or failure we always destroy session. |
| final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { |
| @Override |
| public void onUserActionRequired(Intent intent) { |
| throw new IllegalStateException(); |
| } |
| |
| @Override |
| public void onPackageInstalled(String basePackageName, int returnCode, String msg, |
| Bundle extras) { |
| destroyInternal(); |
| dispatchSessionFinished(returnCode, msg, extras); |
| } |
| }; |
| |
| final UserHandle user; |
| if ((params.installFlags & PackageManager.INSTALL_ALL_USERS) != 0) { |
| user = UserHandle.ALL; |
| } else { |
| user = new UserHandle(userId); |
| } |
| |
| mRelinquished = true; |
| mPm.installStage(mPackageName, stageDir, localObserver, params, |
| mInstallerPackageName, mInstallerUid, user, mSigningDetails); |
| } |
| |
| private static void maybeRenameFile(File from, File to) throws PackageManagerException { |
| if (!from.equals(to)) { |
| if (!from.renameTo(to)) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Could not rename file " + from + " to " + to); |
| } |
| } |
| } |
| |
| /** |
| * Validate install by confirming that all application packages are have |
| * consistent package name, version code, and signing certificates. |
| * <p> |
| * Clears and populates {@link #mResolvedBaseFile}, |
| * {@link #mResolvedStagedFiles}, and {@link #mResolvedInheritedFiles}. |
| * <p> |
| * Renames package files in stage to match split names defined inside. |
| * <p> |
| * Note that upgrade compatibility is still performed by |
| * {@link PackageManagerService}. |
| */ |
| @GuardedBy("mLock") |
| private void validateInstallLocked(@Nullable PackageInfo pkgInfo) |
| throws PackageManagerException { |
| mPackageName = null; |
| mVersionCode = -1; |
| mSigningDetails = PackageParser.SigningDetails.UNKNOWN; |
| |
| mResolvedBaseFile = null; |
| mResolvedStagedFiles.clear(); |
| mResolvedInheritedFiles.clear(); |
| |
| try { |
| resolveStageDirLocked(); |
| } catch (IOException e) { |
| throw new PackageManagerException(INSTALL_FAILED_CONTAINER_ERROR, |
| "Failed to resolve stage location", e); |
| } |
| |
| final File[] removedFiles = mResolvedStageDir.listFiles(sRemovedFilter); |
| final List<String> removeSplitList = new ArrayList<>(); |
| if (!ArrayUtils.isEmpty(removedFiles)) { |
| for (File removedFile : removedFiles) { |
| final String fileName = removedFile.getName(); |
| final String splitName = fileName.substring( |
| 0, fileName.length() - REMOVE_SPLIT_MARKER_EXTENSION.length()); |
| removeSplitList.add(splitName); |
| } |
| } |
| |
| final File[] addedFiles = mResolvedStageDir.listFiles(sAddedFilter); |
| if (ArrayUtils.isEmpty(addedFiles) && removeSplitList.size() == 0) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged"); |
| } |
| |
| // Verify that all staged packages are internally consistent |
| final ArraySet<String> stagedSplits = new ArraySet<>(); |
| for (File addedFile : addedFiles) { |
| final ApkLite apk; |
| try { |
| apk = PackageParser.parseApkLite( |
| addedFile, PackageParser.PARSE_COLLECT_CERTIFICATES); |
| } catch (PackageParserException e) { |
| throw PackageManagerException.from(e); |
| } |
| |
| if (!stagedSplits.add(apk.splitName)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Split " + apk.splitName + " was defined multiple times"); |
| } |
| |
| // Use first package to define unknown values |
| if (mPackageName == null) { |
| mPackageName = apk.packageName; |
| mVersionCode = apk.getLongVersionCode(); |
| } |
| if (mSigningDetails == PackageParser.SigningDetails.UNKNOWN) { |
| mSigningDetails = apk.signingDetails; |
| } |
| |
| assertApkConsistentLocked(String.valueOf(addedFile), apk); |
| |
| // Take this opportunity to enforce uniform naming |
| final String targetName; |
| if (apk.splitName == null) { |
| targetName = "base" + APK_FILE_EXTENSION; |
| } else { |
| targetName = "split_" + apk.splitName + APK_FILE_EXTENSION; |
| } |
| if (!FileUtils.isValidExtFilename(targetName)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Invalid filename: " + targetName); |
| } |
| |
| final File targetFile = new File(mResolvedStageDir, targetName); |
| maybeRenameFile(addedFile, targetFile); |
| |
| // Base is coming from session |
| if (apk.splitName == null) { |
| mResolvedBaseFile = targetFile; |
| } |
| |
| mResolvedStagedFiles.add(targetFile); |
| |
| final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(addedFile); |
| if (dexMetadataFile != null) { |
| if (!FileUtils.isValidExtFilename(dexMetadataFile.getName())) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Invalid filename: " + dexMetadataFile); |
| } |
| final File targetDexMetadataFile = new File(mResolvedStageDir, |
| DexMetadataHelper.buildDexMetadataPathForApk(targetName)); |
| mResolvedStagedFiles.add(targetDexMetadataFile); |
| maybeRenameFile(dexMetadataFile, targetDexMetadataFile); |
| } |
| } |
| |
| if (removeSplitList.size() > 0) { |
| if (pkgInfo == null) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Missing existing base package for " + mPackageName); |
| } |
| |
| // validate split names marked for removal |
| for (String splitName : removeSplitList) { |
| if (!ArrayUtils.contains(pkgInfo.splitNames, splitName)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Split not found: " + splitName); |
| } |
| } |
| |
| // ensure we've got appropriate package name, version code and signatures |
| if (mPackageName == null) { |
| mPackageName = pkgInfo.packageName; |
| mVersionCode = pkgInfo.getLongVersionCode(); |
| } |
| if (mSigningDetails == PackageParser.SigningDetails.UNKNOWN) { |
| try { |
| mSigningDetails = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts( |
| pkgInfo.applicationInfo.sourceDir, |
| PackageParser.SigningDetails.SignatureSchemeVersion.JAR); |
| } catch (PackageParserException e) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Couldn't obtain signatures from base APK"); |
| } |
| } |
| } |
| |
| if (params.mode == SessionParams.MODE_FULL_INSTALL) { |
| // Full installs must include a base package |
| if (!stagedSplits.contains(null)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Full install must include a base package"); |
| } |
| |
| } else { |
| // Partial installs must be consistent with existing install |
| if (pkgInfo == null || pkgInfo.applicationInfo == null) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| "Missing existing base package for " + mPackageName); |
| } |
| |
| final PackageLite existing; |
| final ApkLite existingBase; |
| ApplicationInfo appInfo = pkgInfo.applicationInfo; |
| try { |
| existing = PackageParser.parsePackageLite(new File(appInfo.getCodePath()), 0); |
| existingBase = PackageParser.parseApkLite(new File(appInfo.getBaseCodePath()), |
| PackageParser.PARSE_COLLECT_CERTIFICATES); |
| } catch (PackageParserException e) { |
| throw PackageManagerException.from(e); |
| } |
| |
| assertApkConsistentLocked("Existing base", existingBase); |
| |
| // Inherit base if not overridden |
| if (mResolvedBaseFile == null) { |
| mResolvedBaseFile = new File(appInfo.getBaseCodePath()); |
| mResolvedInheritedFiles.add(mResolvedBaseFile); |
| // Inherit the dex metadata if present. |
| final File baseDexMetadataFile = |
| DexMetadataHelper.findDexMetadataForFile(mResolvedBaseFile); |
| if (baseDexMetadataFile != null) { |
| mResolvedInheritedFiles.add(baseDexMetadataFile); |
| } |
| } |
| |
| // Inherit splits if not overridden |
| if (!ArrayUtils.isEmpty(existing.splitNames)) { |
| for (int i = 0; i < existing.splitNames.length; i++) { |
| final String splitName = existing.splitNames[i]; |
| final File splitFile = new File(existing.splitCodePaths[i]); |
| final boolean splitRemoved = removeSplitList.contains(splitName); |
| if (!stagedSplits.contains(splitName) && !splitRemoved) { |
| mResolvedInheritedFiles.add(splitFile); |
| // Inherit the dex metadata if present. |
| final File splitDexMetadataFile = |
| DexMetadataHelper.findDexMetadataForFile(splitFile); |
| if (splitDexMetadataFile != null) { |
| mResolvedInheritedFiles.add(splitDexMetadataFile); |
| } |
| } |
| } |
| } |
| |
| // Inherit compiled oat directory. |
| final File packageInstallDir = (new File(appInfo.getBaseCodePath())).getParentFile(); |
| mInheritedFilesBase = packageInstallDir; |
| final File oatDir = new File(packageInstallDir, "oat"); |
| if (oatDir.exists()) { |
| final File[] archSubdirs = oatDir.listFiles(); |
| |
| // Keep track of all instruction sets we've seen compiled output for. |
| // If we're linking (and not copying) inherited files, we can recreate the |
| // instruction set hierarchy and link compiled output. |
| if (archSubdirs != null && archSubdirs.length > 0) { |
| final String[] instructionSets = InstructionSets.getAllDexCodeInstructionSets(); |
| for (File archSubDir : archSubdirs) { |
| // Skip any directory that isn't an ISA subdir. |
| if (!ArrayUtils.contains(instructionSets, archSubDir.getName())) { |
| continue; |
| } |
| |
| mResolvedInstructionSets.add(archSubDir.getName()); |
| List<File> oatFiles = Arrays.asList(archSubDir.listFiles()); |
| if (!oatFiles.isEmpty()) { |
| mResolvedInheritedFiles.addAll(oatFiles); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void assertApkConsistentLocked(String tag, ApkLite apk) |
| throws PackageManagerException { |
| if (!mPackageName.equals(apk.packageName)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package " |
| + apk.packageName + " inconsistent with " + mPackageName); |
| } |
| if (params.appPackageName != null && !params.appPackageName.equals(apk.packageName)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag |
| + " specified package " + params.appPackageName |
| + " inconsistent with " + apk.packageName); |
| } |
| if (mVersionCode != apk.getLongVersionCode()) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag |
| + " version code " + apk.versionCode + " inconsistent with " |
| + mVersionCode); |
| } |
| if (!mSigningDetails.signaturesMatchExactly(apk.signingDetails)) { |
| throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, |
| tag + " signatures are inconsistent"); |
| } |
| } |
| |
| /** |
| * Determine if creating hard links between source and destination is |
| * possible. That is, do they all live on the same underlying device. |
| */ |
| private boolean isLinkPossible(List<File> fromFiles, File toDir) { |
| try { |
| final StructStat toStat = Os.stat(toDir.getAbsolutePath()); |
| for (File fromFile : fromFiles) { |
| final StructStat fromStat = Os.stat(fromFile.getAbsolutePath()); |
| if (fromStat.st_dev != toStat.st_dev) { |
| return false; |
| } |
| } |
| } catch (ErrnoException e) { |
| Slog.w(TAG, "Failed to detect if linking possible: " + e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * @return the uid of the owner this session |
| */ |
| public int getInstallerUid() { |
| synchronized (mLock) { |
| return mInstallerUid; |
| } |
| } |
| |
| private static String getRelativePath(File file, File base) throws IOException { |
| final String pathStr = file.getAbsolutePath(); |
| final String baseStr = base.getAbsolutePath(); |
| // Don't allow relative paths. |
| if (pathStr.contains("/.") ) { |
| throw new IOException("Invalid path (was relative) : " + pathStr); |
| } |
| |
| if (pathStr.startsWith(baseStr)) { |
| return pathStr.substring(baseStr.length()); |
| } |
| |
| throw new IOException("File: " + pathStr + " outside base: " + baseStr); |
| } |
| |
| private void createOatDirs(List<String> instructionSets, File fromDir) |
| throws PackageManagerException { |
| for (String instructionSet : instructionSets) { |
| try { |
| mPm.mInstaller.createOatDir(fromDir.getAbsolutePath(), instructionSet); |
| } catch (InstallerException e) { |
| throw PackageManagerException.from(e); |
| } |
| } |
| } |
| |
| private void linkFiles(List<File> fromFiles, File toDir, File fromDir) |
| throws IOException { |
| for (File fromFile : fromFiles) { |
| final String relativePath = getRelativePath(fromFile, fromDir); |
| try { |
| mPm.mInstaller.linkFile(relativePath, fromDir.getAbsolutePath(), |
| toDir.getAbsolutePath()); |
| } catch (InstallerException e) { |
| throw new IOException("failed linkOrCreateDir(" + relativePath + ", " |
| + fromDir + ", " + toDir + ")", e); |
| } |
| } |
| |
| Slog.d(TAG, "Linked " + fromFiles.size() + " files into " + toDir); |
| } |
| |
| private static void copyFiles(List<File> fromFiles, File toDir) throws IOException { |
| // Remove any partial files from previous attempt |
| for (File file : toDir.listFiles()) { |
| if (file.getName().endsWith(".tmp")) { |
| file.delete(); |
| } |
| } |
| |
| for (File fromFile : fromFiles) { |
| final File tmpFile = File.createTempFile("inherit", ".tmp", toDir); |
| if (LOGD) Slog.d(TAG, "Copying " + fromFile + " to " + tmpFile); |
| if (!FileUtils.copyFile(fromFile, tmpFile)) { |
| throw new IOException("Failed to copy " + fromFile + " to " + tmpFile); |
| } |
| try { |
| Os.chmod(tmpFile.getAbsolutePath(), 0644); |
| } catch (ErrnoException e) { |
| throw new IOException("Failed to chmod " + tmpFile); |
| } |
| final File toFile = new File(toDir, fromFile.getName()); |
| if (LOGD) Slog.d(TAG, "Renaming " + tmpFile + " to " + toFile); |
| if (!tmpFile.renameTo(toFile)) { |
| throw new IOException("Failed to rename " + tmpFile + " to " + toFile); |
| } |
| } |
| Slog.d(TAG, "Copied " + fromFiles.size() + " files into " + toDir); |
| } |
| |
| private static void extractNativeLibraries(File packageDir, String abiOverride) |
| throws PackageManagerException { |
| // Always start from a clean slate |
| final File libDir = new File(packageDir, NativeLibraryHelper.LIB_DIR_NAME); |
| NativeLibraryHelper.removeNativeBinariesFromDirLI(libDir, true); |
| |
| NativeLibraryHelper.Handle handle = null; |
| try { |
| handle = NativeLibraryHelper.Handle.create(packageDir); |
| final int res = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libDir, |
| abiOverride); |
| if (res != PackageManager.INSTALL_SUCCEEDED) { |
| throw new PackageManagerException(res, |
| "Failed to extract native libraries, res=" + res); |
| } |
| } catch (IOException e) { |
| throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Failed to extract native libraries", e); |
| } finally { |
| IoUtils.closeQuietly(handle); |
| } |
| } |
| |
| void setPermissionsResult(boolean accepted) { |
| if (!mSealed) { |
| throw new SecurityException("Must be sealed to accept permissions"); |
| } |
| |
| if (accepted) { |
| // Mark and kick off another install pass |
| synchronized (mLock) { |
| mPermissionsManuallyAccepted = true; |
| mHandler.obtainMessage(MSG_COMMIT).sendToTarget(); |
| } |
| } else { |
| destroyInternal(); |
| dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null); |
| } |
| } |
| |
| public void open() throws IOException { |
| if (mActiveCount.getAndIncrement() == 0) { |
| mCallback.onSessionActiveChanged(this, true); |
| } |
| |
| boolean wasPrepared; |
| synchronized (mLock) { |
| wasPrepared = mPrepared; |
| if (!mPrepared) { |
| if (stageDir != null) { |
| prepareStageDir(stageDir); |
| } else { |
| throw new IllegalArgumentException("stageDir must be set"); |
| } |
| |
| mPrepared = true; |
| } |
| } |
| |
| if (!wasPrepared) { |
| mCallback.onSessionPrepared(this); |
| } |
| } |
| |
| @Override |
| public void close() { |
| closeInternal(true); |
| } |
| |
| private void closeInternal(boolean checkCaller) { |
| int activeCount; |
| synchronized (mLock) { |
| if (checkCaller) { |
| assertCallerIsOwnerOrRootLocked(); |
| } |
| |
| activeCount = mActiveCount.decrementAndGet(); |
| } |
| |
| if (activeCount == 0) { |
| mCallback.onSessionActiveChanged(this, false); |
| } |
| } |
| |
| @Override |
| public void abandon() { |
| synchronized (mLock) { |
| assertCallerIsOwnerOrRootLocked(); |
| |
| if (mRelinquished) { |
| Slog.d(TAG, "Ignoring abandon after commit relinquished control"); |
| return; |
| } |
| destroyInternal(); |
| } |
| |
| dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null); |
| } |
| |
| private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) { |
| final IPackageInstallObserver2 observer; |
| final String packageName; |
| synchronized (mLock) { |
| mFinalStatus = returnCode; |
| mFinalMessage = msg; |
| |
| observer = mRemoteObserver; |
| packageName = mPackageName; |
| } |
| |
| if (observer != null) { |
| // Execute observer.onPackageInstalled on different tread as we don't want callers |
| // inside the system server have to worry about catching the callbacks while they are |
| // calling into the session |
| final SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = packageName; |
| args.arg2 = msg; |
| args.arg3 = extras; |
| args.arg4 = observer; |
| args.argi1 = returnCode; |
| |
| mHandler.obtainMessage(MSG_ON_PACKAGE_INSTALLED, args).sendToTarget(); |
| } |
| |
| final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED); |
| |
| // Send broadcast to default launcher only if it's a new install |
| final boolean isNewInstall = extras == null || !extras.getBoolean(Intent.EXTRA_REPLACING); |
| if (success && isNewInstall) { |
| mPm.sendSessionCommitBroadcast(generateInfo(), userId); |
| } |
| |
| mCallback.onSessionFinished(this, success); |
| } |
| |
| private void destroyInternal() { |
| synchronized (mLock) { |
| mSealed = true; |
| mDestroyed = true; |
| |
| // Force shut down all bridges |
| for (RevocableFileDescriptor fd : mFds) { |
| fd.revoke(); |
| } |
| for (FileBridge bridge : mBridges) { |
| bridge.forceClose(); |
| } |
| } |
| if (stageDir != null) { |
| try { |
| mPm.mInstaller.rmPackageDir(stageDir.getAbsolutePath()); |
| } catch (InstallerException ignored) { |
| } |
| } |
| } |
| |
| void dump(IndentingPrintWriter pw) { |
| synchronized (mLock) { |
| dumpLocked(pw); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void dumpLocked(IndentingPrintWriter pw) { |
| pw.println("Session " + sessionId + ":"); |
| pw.increaseIndent(); |
| |
| pw.printPair("userId", userId); |
| pw.printPair("mOriginalInstallerUid", mOriginalInstallerUid); |
| pw.printPair("mInstallerPackageName", mInstallerPackageName); |
| pw.printPair("mInstallerUid", mInstallerUid); |
| pw.printPair("createdMillis", createdMillis); |
| pw.printPair("stageDir", stageDir); |
| pw.printPair("stageCid", stageCid); |
| pw.println(); |
| |
| params.dump(pw); |
| |
| pw.printPair("mClientProgress", mClientProgress); |
| pw.printPair("mProgress", mProgress); |
| pw.printPair("mSealed", mSealed); |
| pw.printPair("mPermissionsManuallyAccepted", mPermissionsManuallyAccepted); |
| pw.printPair("mRelinquished", mRelinquished); |
| pw.printPair("mDestroyed", mDestroyed); |
| pw.printPair("mFds", mFds.size()); |
| pw.printPair("mBridges", mBridges.size()); |
| pw.printPair("mFinalStatus", mFinalStatus); |
| pw.printPair("mFinalMessage", mFinalMessage); |
| pw.println(); |
| |
| pw.decreaseIndent(); |
| } |
| |
| private static void writeGrantedRuntimePermissionsLocked(XmlSerializer out, |
| String[] grantedRuntimePermissions) throws IOException { |
| if (grantedRuntimePermissions != null) { |
| for (String permission : grantedRuntimePermissions) { |
| out.startTag(null, TAG_GRANTED_RUNTIME_PERMISSION); |
| writeStringAttribute(out, ATTR_NAME, permission); |
| out.endTag(null, TAG_GRANTED_RUNTIME_PERMISSION); |
| } |
| } |
| } |
| |
| private static File buildAppIconFile(int sessionId, @NonNull File sessionsDir) { |
| return new File(sessionsDir, "app_icon." + sessionId + ".png"); |
| } |
| |
| /** |
| * Write this session to a {@link XmlSerializer}. |
| * |
| * @param out Where to write the session to |
| * @param sessionsDir The directory containing the sessions |
| */ |
| void write(@NonNull XmlSerializer out, @NonNull File sessionsDir) throws IOException { |
| synchronized (mLock) { |
| if (mDestroyed) { |
| return; |
| } |
| |
| out.startTag(null, TAG_SESSION); |
| |
| writeIntAttribute(out, ATTR_SESSION_ID, sessionId); |
| writeIntAttribute(out, ATTR_USER_ID, userId); |
| writeStringAttribute(out, ATTR_INSTALLER_PACKAGE_NAME, |
| mInstallerPackageName); |
| writeIntAttribute(out, ATTR_INSTALLER_UID, mInstallerUid); |
| writeLongAttribute(out, ATTR_CREATED_MILLIS, createdMillis); |
| if (stageDir != null) { |
| writeStringAttribute(out, ATTR_SESSION_STAGE_DIR, |
| stageDir.getAbsolutePath()); |
| } |
| if (stageCid != null) { |
| writeStringAttribute(out, ATTR_SESSION_STAGE_CID, stageCid); |
| } |
| writeBooleanAttribute(out, ATTR_PREPARED, isPrepared()); |
| writeBooleanAttribute(out, ATTR_SEALED, isSealed()); |
| |
| writeIntAttribute(out, ATTR_MODE, params.mode); |
| writeIntAttribute(out, ATTR_INSTALL_FLAGS, params.installFlags); |
| writeIntAttribute(out, ATTR_INSTALL_LOCATION, params.installLocation); |
| writeLongAttribute(out, ATTR_SIZE_BYTES, params.sizeBytes); |
| writeStringAttribute(out, ATTR_APP_PACKAGE_NAME, params.appPackageName); |
| writeStringAttribute(out, ATTR_APP_LABEL, params.appLabel); |
| writeUriAttribute(out, ATTR_ORIGINATING_URI, params.originatingUri); |
| writeIntAttribute(out, ATTR_ORIGINATING_UID, params.originatingUid); |
| writeUriAttribute(out, ATTR_REFERRER_URI, params.referrerUri); |
| writeStringAttribute(out, ATTR_ABI_OVERRIDE, params.abiOverride); |
| writeStringAttribute(out, ATTR_VOLUME_UUID, params.volumeUuid); |
| writeIntAttribute(out, ATTR_INSTALL_REASON, params.installReason); |
| |
| writeGrantedRuntimePermissionsLocked(out, params.grantedRuntimePermissions); |
| |
| // Persist app icon if changed since last written |
| File appIconFile = buildAppIconFile(sessionId, sessionsDir); |
| if (params.appIcon == null && appIconFile.exists()) { |
| appIconFile.delete(); |
| } else if (params.appIcon != null |
| && appIconFile.lastModified() != params.appIconLastModified) { |
| if (LOGD) Slog.w(TAG, "Writing changed icon " + appIconFile); |
| FileOutputStream os = null; |
| try { |
| os = new FileOutputStream(appIconFile); |
| params.appIcon.compress(Bitmap.CompressFormat.PNG, 90, os); |
| } catch (IOException e) { |
| Slog.w(TAG, "Failed to write icon " + appIconFile + ": " + e.getMessage()); |
| } finally { |
| IoUtils.closeQuietly(os); |
| } |
| |
| params.appIconLastModified = appIconFile.lastModified(); |
| } |
| } |
| |
| out.endTag(null, TAG_SESSION); |
| } |
| |
| private static String[] readGrantedRuntimePermissions(XmlPullParser in) |
| throws IOException, XmlPullParserException { |
| List<String> permissions = null; |
| |
| final int outerDepth = in.getDepth(); |
| int type; |
| while ((type = in.next()) != XmlPullParser.END_DOCUMENT |
| && (type != XmlPullParser.END_TAG || in.getDepth() > outerDepth)) { |
| if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { |
| continue; |
| } |
| if (TAG_GRANTED_RUNTIME_PERMISSION.equals(in.getName())) { |
| String permission = readStringAttribute(in, ATTR_NAME); |
| if (permissions == null) { |
| permissions = new ArrayList<>(); |
| } |
| permissions.add(permission); |
| } |
| } |
| |
| if (permissions == null) { |
| return null; |
| } |
| |
| String[] permissionsArray = new String[permissions.size()]; |
| permissions.toArray(permissionsArray); |
| return permissionsArray; |
| } |
| |
| /** |
| * Read new session from a {@link XmlPullParser xml description} and create it. |
| * |
| * @param in The source of the description |
| * @param callback Callback the session uses to notify about changes of it's state |
| * @param context Context to be used by the session |
| * @param pm PackageManager to use by the session |
| * @param installerThread Thread to be used for callbacks of this session |
| * @param sessionsDir The directory the sessions are stored in |
| * |
| * @return The newly created session |
| */ |
| public static PackageInstallerSession readFromXml(@NonNull XmlPullParser in, |
| @NonNull PackageInstallerService.InternalCallback callback, @NonNull Context context, |
| @NonNull PackageManagerService pm, Looper installerThread, @NonNull File sessionsDir) |
| throws IOException, XmlPullParserException { |
| final int sessionId = readIntAttribute(in, ATTR_SESSION_ID); |
| final int userId = readIntAttribute(in, ATTR_USER_ID); |
| final String installerPackageName = readStringAttribute(in, ATTR_INSTALLER_PACKAGE_NAME); |
| final int installerUid = readIntAttribute(in, ATTR_INSTALLER_UID, pm.getPackageUid( |
| installerPackageName, PackageManager.MATCH_UNINSTALLED_PACKAGES, userId)); |
| final long createdMillis = readLongAttribute(in, ATTR_CREATED_MILLIS); |
| final String stageDirRaw = readStringAttribute(in, ATTR_SESSION_STAGE_DIR); |
| final File stageDir = (stageDirRaw != null) ? new File(stageDirRaw) : null; |
| final String stageCid = readStringAttribute(in, ATTR_SESSION_STAGE_CID); |
| final boolean prepared = readBooleanAttribute(in, ATTR_PREPARED, true); |
| final boolean sealed = readBooleanAttribute(in, ATTR_SEALED); |
| |
| final SessionParams params = new SessionParams( |
| SessionParams.MODE_INVALID); |
| params.mode = readIntAttribute(in, ATTR_MODE); |
| params.installFlags = readIntAttribute(in, ATTR_INSTALL_FLAGS); |
| params.installLocation = readIntAttribute(in, ATTR_INSTALL_LOCATION); |
| params.sizeBytes = readLongAttribute(in, ATTR_SIZE_BYTES); |
| params.appPackageName = readStringAttribute(in, ATTR_APP_PACKAGE_NAME); |
| params.appIcon = readBitmapAttribute(in, ATTR_APP_ICON); |
| params.appLabel = readStringAttribute(in, ATTR_APP_LABEL); |
| params.originatingUri = readUriAttribute(in, ATTR_ORIGINATING_URI); |
| params.originatingUid = |
| readIntAttribute(in, ATTR_ORIGINATING_UID, SessionParams.UID_UNKNOWN); |
| params.referrerUri = readUriAttribute(in, ATTR_REFERRER_URI); |
| params.abiOverride = readStringAttribute(in, ATTR_ABI_OVERRIDE); |
| params.volumeUuid = readStringAttribute(in, ATTR_VOLUME_UUID); |
| params.installReason = readIntAttribute(in, ATTR_INSTALL_REASON); |
| |
| params.grantedRuntimePermissions = readGrantedRuntimePermissions(in); |
| |
| final File appIconFile = buildAppIconFile(sessionId, sessionsDir); |
| if (appIconFile.exists()) { |
| params.appIcon = BitmapFactory.decodeFile(appIconFile.getAbsolutePath()); |
| params.appIconLastModified = appIconFile.lastModified(); |
| } |
| |
| return new PackageInstallerSession(callback, context, pm, |
| installerThread, sessionId, userId, installerPackageName, installerUid, |
| params, createdMillis, stageDir, stageCid, prepared, sealed); |
| } |
| } |