| /* |
| * 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_ALREADY_EXISTS; |
| 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.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED; |
| import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; |
| |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.IPackageInstallObserver2; |
| import android.content.pm.IPackageInstallerSession; |
| import android.content.pm.PackageInstallerParams; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.PackageLite; |
| import android.content.pm.Signature; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.FileUtils; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.os.SELinux; |
| import android.system.ErrnoException; |
| import android.system.OsConstants; |
| import android.system.StructStat; |
| import android.util.ArraySet; |
| import android.util.Slog; |
| |
| import com.android.internal.content.NativeLibraryHelper; |
| import com.android.internal.util.ArrayUtils; |
| import com.android.internal.util.Preconditions; |
| |
| import libcore.io.IoUtils; |
| import libcore.io.Libcore; |
| import libcore.io.Streams; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| |
| public class PackageInstallerSession extends IPackageInstallerSession.Stub { |
| private static final String TAG = "PackageInstaller"; |
| |
| private final PackageInstallerService.Callback mCallback; |
| private final PackageManagerService mPm; |
| private final Handler mHandler; |
| |
| public final int sessionId; |
| public final int userId; |
| public final String installerPackageName; |
| /** UID not persisted */ |
| public final int installerUid; |
| public final PackageInstallerParams params; |
| public final long createdMillis; |
| public final File sessionDir; |
| |
| private static final int MSG_INSTALL = 0; |
| |
| private Handler.Callback mHandlerCallback = new Handler.Callback() { |
| @Override |
| public boolean handleMessage(Message msg) { |
| synchronized (mLock) { |
| if (msg.obj != null) { |
| mRemoteObserver = (IPackageInstallObserver2) msg.obj; |
| } |
| |
| try { |
| installLocked(); |
| } catch (InstallFailedException e) { |
| Slog.e(TAG, "Install failed: " + e); |
| try { |
| mRemoteObserver.packageInstalled(mPackageName, null, e.error); |
| } catch (RemoteException ignored) { |
| } |
| } |
| |
| return true; |
| } |
| } |
| }; |
| |
| private final Object mLock = new Object(); |
| |
| private int mProgress; |
| |
| private String mPackageName; |
| private int mVersionCode; |
| private Signature[] mSignatures; |
| |
| private boolean mMutationsAllowed; |
| private boolean mVerifierConfirmed; |
| private boolean mPermissionsConfirmed; |
| private boolean mInvalid; |
| |
| private ArrayList<WritePipe> mPipes = new ArrayList<>(); |
| |
| private IPackageInstallObserver2 mRemoteObserver; |
| |
| public PackageInstallerSession(PackageInstallerService.Callback callback, |
| PackageManagerService pm, int sessionId, int userId, String installerPackageName, |
| int installerUid, PackageInstallerParams params, long createdMillis, File sessionDir, |
| Looper looper) { |
| mCallback = callback; |
| mPm = pm; |
| mHandler = new Handler(looper, mHandlerCallback); |
| |
| this.sessionId = sessionId; |
| this.userId = userId; |
| this.installerPackageName = installerPackageName; |
| this.installerUid = installerUid; |
| this.params = params; |
| this.createdMillis = createdMillis; |
| this.sessionDir = sessionDir; |
| |
| // Check against any explicitly provided signatures |
| mSignatures = params.signatures; |
| |
| // TODO: splice in flag when restoring persisted session |
| mMutationsAllowed = true; |
| |
| if (pm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES, installerPackageName) |
| == PackageManager.PERMISSION_GRANTED) { |
| mPermissionsConfirmed = true; |
| } |
| } |
| |
| @Override |
| public void updateProgress(int progress) { |
| mProgress = progress; |
| mCallback.onProgressChanged(this); |
| } |
| |
| @Override |
| public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { |
| // TODO: relay over to DCS when installing to ASEC |
| |
| // 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 WritePipe pipe; |
| synchronized (mLock) { |
| if (!mMutationsAllowed) { |
| throw new IllegalStateException("Mutations not allowed"); |
| } |
| |
| pipe = new WritePipe(); |
| mPipes.add(pipe); |
| } |
| |
| try { |
| // Use installer provided name for now; we always rename later |
| if (!FileUtils.isValidExtFilename(name)) { |
| throw new IllegalArgumentException("Invalid name: " + name); |
| } |
| final File target = new File(sessionDir, name); |
| |
| final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), |
| OsConstants.O_CREAT | OsConstants.O_WRONLY, 00700); |
| |
| // If caller specified a total length, allocate it for them. Free up |
| // cache space to grow, if needed. |
| if (lengthBytes > 0) { |
| final StructStat stat = Libcore.os.fstat(targetFd); |
| final long deltaBytes = lengthBytes - stat.st_size; |
| if (deltaBytes > 0) { |
| mPm.freeStorage(deltaBytes); |
| } |
| Libcore.os.posix_fallocate(targetFd, 0, lengthBytes); |
| } |
| |
| if (offsetBytes > 0) { |
| Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); |
| } |
| |
| pipe.setTargetFd(targetFd); |
| pipe.start(); |
| return pipe.getWriteFd(); |
| |
| } catch (ErrnoException e) { |
| throw new IllegalStateException("Failed to write", e); |
| } catch (IOException e) { |
| throw new IllegalStateException("Failed to write", e); |
| } |
| } |
| |
| @Override |
| public void install(IPackageInstallObserver2 observer) { |
| Preconditions.checkNotNull(observer); |
| mHandler.obtainMessage(MSG_INSTALL, observer).sendToTarget(); |
| } |
| |
| private void installLocked() throws InstallFailedException { |
| if (mInvalid) { |
| throw new InstallFailedException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session"); |
| } |
| |
| // Verify that all writers are hands-off |
| if (mMutationsAllowed) { |
| for (WritePipe pipe : mPipes) { |
| if (!pipe.isClosed()) { |
| throw new InstallFailedException(INSTALL_FAILED_PACKAGE_CHANGED, |
| "Files still open"); |
| } |
| } |
| mMutationsAllowed = false; |
| |
| // TODO: persist disabled mutations before going forward, since |
| // beyond this point we may have hardlinks to the valid install |
| } |
| |
| // Verify that stage looks sane with respect to existing application. |
| // This currently only ensures packageName, versionCode, and certificate |
| // consistency. |
| validateInstallLocked(); |
| |
| Preconditions.checkNotNull(mPackageName); |
| Preconditions.checkNotNull(mSignatures); |
| |
| if (!mVerifierConfirmed) { |
| // TODO: async communication with verifier |
| // when they confirm, we'll kick off another install() pass |
| mVerifierConfirmed = true; |
| } |
| |
| if (!mPermissionsConfirmed) { |
| // TODO: async confirm permissions with user |
| // when they confirm, we'll kick off another install() pass |
| mPermissionsConfirmed = true; |
| } |
| |
| // Unpack any native libraries contained in this session |
| unpackNativeLibraries(); |
| |
| // Inherit any packages and native libraries from existing install that |
| // haven't been overridden. |
| if (!params.fullInstall) { |
| spliceExistingFilesIntoStage(); |
| } |
| |
| // TODO: for ASEC based applications, grow and stream in packages |
| |
| // 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 remoteObserver = mRemoteObserver; |
| final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() { |
| @Override |
| public void packageInstalled(String basePackageName, Bundle extras, int returnCode) |
| throws RemoteException { |
| destroy(); |
| remoteObserver.packageInstalled(basePackageName, extras, returnCode); |
| } |
| }; |
| |
| mPm.installStage(mPackageName, this.sessionDir, localObserver, params.installFlags); |
| } |
| |
| /** |
| * Validate install by confirming that all application packages are have |
| * consistent package name, version code, and signing certificates. |
| * <p> |
| * Renames package files in stage to match split names defined inside. |
| */ |
| private void validateInstallLocked() throws InstallFailedException { |
| mPackageName = null; |
| mVersionCode = -1; |
| mSignatures = null; |
| |
| final File[] files = sessionDir.listFiles(); |
| if (ArrayUtils.isEmpty(files)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, "No packages staged"); |
| } |
| |
| final ArraySet<String> seenSplits = new ArraySet<>(); |
| |
| // Verify that all staged packages are internally consistent |
| for (File file : files) { |
| final PackageLite info = PackageParser.parsePackageLite(file.getAbsolutePath(), |
| PackageParser.PARSE_GET_SIGNATURES); |
| if (info == null) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Failed to parse " + file); |
| } |
| |
| if (!seenSplits.add(info.splitName)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Split " + info.splitName + " was defined multiple times"); |
| } |
| |
| // Use first package to define unknown values |
| if (mPackageName != null) { |
| mPackageName = info.packageName; |
| mVersionCode = info.versionCode; |
| } |
| if (mSignatures != null) { |
| mSignatures = info.signatures; |
| } |
| |
| assertPackageConsistent(String.valueOf(file), info.packageName, info.versionCode, |
| info.signatures); |
| |
| // Take this opportunity to enforce uniform naming |
| final String name; |
| if (info.splitName == null) { |
| name = info.packageName + ".apk"; |
| } else { |
| name = info.packageName + "-" + info.splitName + ".apk"; |
| } |
| if (!FileUtils.isValidExtFilename(name)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Invalid filename: " + name); |
| } |
| if (!file.getName().equals(name)) { |
| file.renameTo(new File(file.getParentFile(), name)); |
| } |
| } |
| |
| // TODO: shift package signature verification to installer; we're |
| // currently relying on PMS to do this. |
| // TODO: teach about compatible upgrade keysets. |
| |
| if (params.fullInstall) { |
| // Full installs must include a base package |
| if (!seenSplits.contains(null)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Full install must include a base package"); |
| } |
| |
| } else { |
| // Partial installs must be consistent with existing install. |
| final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); |
| if (app == null) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Missing existing base package for " + mPackageName); |
| } |
| |
| final PackageLite info = PackageParser.parsePackageLite(app.sourceDir, |
| PackageParser.PARSE_GET_SIGNATURES); |
| if (info == null) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| "Failed to parse existing base " + app.sourceDir); |
| } |
| |
| assertPackageConsistent("Existing base", info.packageName, info.versionCode, |
| info.signatures); |
| } |
| } |
| |
| private void assertPackageConsistent(String tag, String packageName, int versionCode, |
| Signature[] signatures) throws InstallFailedException { |
| if (!mPackageName.equals(packageName)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, tag + " package " |
| + packageName + " inconsistent with " + mPackageName); |
| } |
| if (mVersionCode != versionCode) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, tag |
| + " version code " + versionCode + " inconsistent with " |
| + mVersionCode); |
| } |
| if (!Signature.areExactMatch(mSignatures, signatures)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, |
| tag + " signatures are inconsistent"); |
| } |
| } |
| |
| /** |
| * Application is already installed; splice existing files that haven't been |
| * overridden into our stage. |
| */ |
| private void spliceExistingFilesIntoStage() throws InstallFailedException { |
| final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId); |
| final File existingDir = new File(app.sourceDir).getParentFile(); |
| |
| try { |
| linkTreeIgnoringExisting(existingDir, sessionDir); |
| } catch (ErrnoException e) { |
| throw new InstallFailedException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Failed to splice into stage"); |
| } |
| } |
| |
| /** |
| * Recursively hard link all files from source directory tree to target. |
| * When a file already exists in the target tree, it leaves that file |
| * intact. |
| */ |
| private void linkTreeIgnoringExisting(File sourceDir, File targetDir) throws ErrnoException { |
| final File[] sourceContents = sourceDir.listFiles(); |
| if (ArrayUtils.isEmpty(sourceContents)) return; |
| |
| for (File sourceFile : sourceContents) { |
| final File targetFile = new File(targetDir, sourceFile.getName()); |
| |
| if (sourceFile.isDirectory()) { |
| targetFile.mkdir(); |
| linkTreeIgnoringExisting(sourceFile, targetFile); |
| } else { |
| Libcore.os.link(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath()); |
| } |
| } |
| } |
| |
| private void unpackNativeLibraries() throws InstallFailedException { |
| final File libDir = new File(sessionDir, "lib"); |
| |
| if (!libDir.mkdir()) { |
| throw new InstallFailedException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Failed to create " + libDir); |
| } |
| |
| try { |
| Libcore.os.chmod(libDir.getAbsolutePath(), 0755); |
| } catch (ErrnoException e) { |
| throw new InstallFailedException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Failed to prepare " + libDir + ": " + e); |
| } |
| |
| if (!SELinux.restorecon(libDir)) { |
| throw new InstallFailedException(INSTALL_FAILED_INTERNAL_ERROR, |
| "Failed to set context on " + libDir); |
| } |
| |
| // Unpack all native libraries under stage |
| final File[] files = sessionDir.listFiles(); |
| if (ArrayUtils.isEmpty(files)) { |
| throw new InstallFailedException(INSTALL_FAILED_INVALID_APK, "No packages staged"); |
| } |
| |
| for (File file : files) { |
| final NativeLibraryHelper.ApkHandle handle = new NativeLibraryHelper.ApkHandle(file); |
| try { |
| final int abiIndex = NativeLibraryHelper.findSupportedAbi(handle, |
| Build.SUPPORTED_ABIS); |
| if (abiIndex >= 0) { |
| int copyRet = NativeLibraryHelper.copyNativeBinariesIfNeededLI(handle, libDir, |
| Build.SUPPORTED_ABIS[abiIndex]); |
| if (copyRet != INSTALL_SUCCEEDED) { |
| throw new InstallFailedException(copyRet, |
| "Failed to copy native libraries for " + file); |
| } |
| } else if (abiIndex != PackageManager.NO_NATIVE_LIBRARIES) { |
| throw new InstallFailedException(abiIndex, |
| "Failed to copy native libraries for " + file); |
| } |
| } finally { |
| handle.close(); |
| } |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| try { |
| synchronized (mLock) { |
| mInvalid = true; |
| } |
| FileUtils.deleteContents(sessionDir); |
| sessionDir.delete(); |
| } finally { |
| mCallback.onSessionInvalid(this); |
| } |
| } |
| |
| private static class WritePipe extends Thread { |
| private final ParcelFileDescriptor[] mPipe; |
| |
| private FileDescriptor mTargetFd; |
| |
| private volatile boolean mClosed; |
| |
| public WritePipe() { |
| try { |
| mPipe = ParcelFileDescriptor.createPipe(); |
| } catch (IOException e) { |
| throw new IllegalStateException("Failed to create pipe"); |
| } |
| } |
| |
| public boolean isClosed() { |
| return mClosed; |
| } |
| |
| public void setTargetFd(FileDescriptor targetFd) { |
| mTargetFd = targetFd; |
| } |
| |
| public ParcelFileDescriptor getWriteFd() { |
| return mPipe[1]; |
| } |
| |
| @Override |
| public void run() { |
| FileInputStream in = null; |
| FileOutputStream out = null; |
| try { |
| // TODO: look at switching to sendfile(2) to speed up |
| in = new FileInputStream(mPipe[0].getFileDescriptor()); |
| out = new FileOutputStream(mTargetFd); |
| Streams.copy(in, out); |
| } catch (IOException e) { |
| Slog.w(TAG, "Failed to stream data: " + e); |
| } finally { |
| IoUtils.closeQuietly(mPipe[0]); |
| IoUtils.closeQuietly(mTargetFd); |
| mClosed = true; |
| } |
| } |
| } |
| |
| private class InstallFailedException extends Exception { |
| private final int error; |
| |
| public InstallFailedException(int error, String detailMessage) { |
| super(detailMessage); |
| this.error = error; |
| } |
| } |
| } |