PackageInstallerSession.java revision ad11eb52fe62b6696df2194cb9e176f7d799e844
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.pm;
18
19import static android.content.pm.PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
20import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
21import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
22import static android.content.pm.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED;
23import static android.system.OsConstants.O_CREAT;
24import static android.system.OsConstants.O_WRONLY;
25
26import android.content.pm.ApplicationInfo;
27import android.content.pm.IPackageInstallObserver2;
28import android.content.pm.IPackageInstallerSession;
29import android.content.pm.InstallSessionInfo;
30import android.content.pm.InstallSessionParams;
31import android.content.pm.PackageManager;
32import android.content.pm.PackageParser;
33import android.content.pm.PackageParser.ApkLite;
34import android.content.pm.PackageParser.PackageParserException;
35import android.content.pm.Signature;
36import android.os.Bundle;
37import android.os.FileBridge;
38import android.os.FileUtils;
39import android.os.Handler;
40import android.os.Looper;
41import android.os.Message;
42import android.os.ParcelFileDescriptor;
43import android.os.Process;
44import android.os.RemoteException;
45import android.os.UserHandle;
46import android.system.ErrnoException;
47import android.system.Os;
48import android.system.OsConstants;
49import android.system.StructStat;
50import android.util.ArraySet;
51import android.util.ExceptionUtils;
52import android.util.MathUtils;
53import android.util.Slog;
54
55import com.android.internal.util.ArrayUtils;
56import com.android.internal.util.IndentingPrintWriter;
57import com.android.internal.util.Preconditions;
58
59import libcore.io.Libcore;
60
61import java.io.File;
62import java.io.FileDescriptor;
63import java.io.IOException;
64import java.util.ArrayList;
65
66public class PackageInstallerSession extends IPackageInstallerSession.Stub {
67    private static final String TAG = "PackageInstaller";
68
69    // TODO: enforce INSTALL_ALLOW_TEST
70    // TODO: enforce INSTALL_ALLOW_DOWNGRADE
71    // TODO: handle INSTALL_EXTERNAL, INSTALL_INTERNAL
72
73    private final PackageInstallerService.Callback mCallback;
74    private final PackageManagerService mPm;
75    private final Handler mHandler;
76
77    public final int sessionId;
78    public final int userId;
79    public final String installerPackageName;
80    /** UID not persisted */
81    public final int installerUid;
82    public final InstallSessionParams params;
83    public final long createdMillis;
84    public final File sessionStageDir;
85
86    private static final int MSG_INSTALL = 0;
87
88    private Handler.Callback mHandlerCallback = new Handler.Callback() {
89        @Override
90        public boolean handleMessage(Message msg) {
91            synchronized (mLock) {
92                if (msg.obj != null) {
93                    mRemoteObserver = (IPackageInstallObserver2) msg.obj;
94                }
95
96                try {
97                    installLocked();
98                } catch (PackageManagerException e) {
99                    Slog.e(TAG, "Install failed: " + e);
100                    destroyInternal();
101                    try {
102                        mRemoteObserver.packageInstalled(mPackageName, null, e.error,
103                                e.getMessage());
104                    } catch (RemoteException ignored) {
105                    }
106                    mCallback.onSessionFinished(PackageInstallerSession.this, false);
107                }
108
109                return true;
110            }
111        }
112    };
113
114    private final Object mLock = new Object();
115
116    private int mClientProgress;
117    private int mProgress = 0;
118
119    private String mPackageName;
120    private int mVersionCode;
121    private Signature[] mSignatures;
122
123    private boolean mMutationsAllowed;
124    private boolean mPermissionsConfirmed;
125    private boolean mInvalid;
126
127    private ArrayList<FileBridge> mBridges = new ArrayList<>();
128
129    private IPackageInstallObserver2 mRemoteObserver;
130
131    public PackageInstallerSession(PackageInstallerService.Callback callback,
132            PackageManagerService pm, int sessionId, int userId, String installerPackageName,
133            int installerUid, InstallSessionParams params, long createdMillis, File sessionStageDir,
134            Looper looper) {
135        mCallback = callback;
136        mPm = pm;
137        mHandler = new Handler(looper, mHandlerCallback);
138
139        this.sessionId = sessionId;
140        this.userId = userId;
141        this.installerPackageName = installerPackageName;
142        this.installerUid = installerUid;
143        this.params = params;
144        this.createdMillis = createdMillis;
145        this.sessionStageDir = sessionStageDir;
146
147        // Check against any explicitly provided signatures
148        mSignatures = params.signatures;
149
150        // TODO: splice in flag when restoring persisted session
151        mMutationsAllowed = true;
152
153        if (pm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES, installerPackageName)
154                == PackageManager.PERMISSION_GRANTED) {
155            mPermissionsConfirmed = true;
156        }
157        if (installerUid == Process.SHELL_UID || installerUid == 0) {
158            mPermissionsConfirmed = true;
159        }
160    }
161
162    public InstallSessionInfo generateInfo() {
163        final InstallSessionInfo info = new InstallSessionInfo();
164
165        info.sessionId = sessionId;
166        info.installerPackageName = installerPackageName;
167        info.progress = mProgress;
168
169        info.fullInstall = params.fullInstall;
170        info.packageName = params.packageName;
171        info.icon = params.icon;
172        info.title = params.title;
173
174        return info;
175    }
176
177    @Override
178    public void setClientProgress(int progress) {
179        mClientProgress = progress;
180        mProgress = MathUtils.constrain(
181                (int) (((float) mClientProgress) / ((float) params.progressMax)) * 80, 0, 80);
182        mCallback.onSessionProgress(this, mProgress);
183    }
184
185    @Override
186    public void addClientProgress(int progress) {
187        setClientProgress(mClientProgress + progress);
188    }
189
190    @Override
191    public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) {
192        try {
193            return openWriteInternal(name, offsetBytes, lengthBytes);
194        } catch (IOException e) {
195            throw ExceptionUtils.wrap(e);
196        }
197    }
198
199    private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes)
200            throws IOException {
201        // TODO: relay over to DCS when installing to ASEC
202
203        // Quick sanity check of state, and allocate a pipe for ourselves. We
204        // then do heavy disk allocation outside the lock, but this open pipe
205        // will block any attempted install transitions.
206        final FileBridge bridge;
207        synchronized (mLock) {
208            if (!mMutationsAllowed) {
209                throw new IllegalStateException("Mutations not allowed");
210            }
211
212            bridge = new FileBridge();
213            mBridges.add(bridge);
214        }
215
216        try {
217            // Use installer provided name for now; we always rename later
218            if (!FileUtils.isValidExtFilename(name)) {
219                throw new IllegalArgumentException("Invalid name: " + name);
220            }
221            final File target = new File(sessionStageDir, name);
222
223            final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(),
224                    O_CREAT | O_WRONLY, 0644);
225            Os.chmod(target.getAbsolutePath(), 0644);
226
227            // If caller specified a total length, allocate it for them. Free up
228            // cache space to grow, if needed.
229            if (lengthBytes > 0) {
230                final StructStat stat = Libcore.os.fstat(targetFd);
231                final long deltaBytes = lengthBytes - stat.st_size;
232                if (deltaBytes > 0) {
233                    mPm.freeStorage(deltaBytes);
234                }
235                Libcore.os.posix_fallocate(targetFd, 0, lengthBytes);
236            }
237
238            if (offsetBytes > 0) {
239                Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET);
240            }
241
242            bridge.setTargetFile(targetFd);
243            bridge.start();
244            return new ParcelFileDescriptor(bridge.getClientSocket());
245
246        } catch (ErrnoException e) {
247            throw e.rethrowAsIOException();
248        }
249    }
250
251    @Override
252    public void install(IPackageInstallObserver2 observer) {
253        Preconditions.checkNotNull(observer);
254        mHandler.obtainMessage(MSG_INSTALL, observer).sendToTarget();
255    }
256
257    private void installLocked() throws PackageManagerException {
258        if (mInvalid) {
259            throw new PackageManagerException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session");
260        }
261
262        // Verify that all writers are hands-off
263        if (mMutationsAllowed) {
264            for (FileBridge bridge : mBridges) {
265                if (!bridge.isClosed()) {
266                    throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
267                            "Files still open");
268                }
269            }
270            mMutationsAllowed = false;
271
272            // TODO: persist disabled mutations before going forward, since
273            // beyond this point we may have hardlinks to the valid install
274        }
275
276        // Verify that stage looks sane with respect to existing application.
277        // This currently only ensures packageName, versionCode, and certificate
278        // consistency.
279        validateInstallLocked();
280
281        Preconditions.checkNotNull(mPackageName);
282        Preconditions.checkNotNull(mSignatures);
283
284        if (!mPermissionsConfirmed) {
285            // TODO: async confirm permissions with user
286            // when they confirm, we'll kick off another install() pass
287            throw new SecurityException("Caller must hold INSTALL permission");
288        }
289
290        // Inherit any packages and native libraries from existing install that
291        // haven't been overridden.
292        if (!params.fullInstall) {
293            spliceExistingFilesIntoStage();
294        }
295
296        // TODO: surface more granular state from dexopt
297        mCallback.onSessionProgress(this, 90);
298
299        // TODO: for ASEC based applications, grow and stream in packages
300
301        // We've reached point of no return; call into PMS to install the stage.
302        // Regardless of success or failure we always destroy session.
303        final IPackageInstallObserver2 remoteObserver = mRemoteObserver;
304        final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() {
305            @Override
306            public void packageInstalled(String basePackageName, Bundle extras, int returnCode,
307                    String msg) {
308                destroyInternal();
309                try {
310                    remoteObserver.packageInstalled(basePackageName, extras, returnCode, msg);
311                } catch (RemoteException ignored) {
312                }
313                final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED);
314                mCallback.onSessionFinished(PackageInstallerSession.this, success);
315            }
316        };
317
318        mPm.installStage(mPackageName, this.sessionStageDir, localObserver, params,
319                installerPackageName, installerUid, new UserHandle(userId));
320    }
321
322    /**
323     * Validate install by confirming that all application packages are have
324     * consistent package name, version code, and signing certificates.
325     * <p>
326     * Renames package files in stage to match split names defined inside.
327     */
328    private void validateInstallLocked() throws PackageManagerException {
329        mPackageName = null;
330        mVersionCode = -1;
331
332        final File[] files = sessionStageDir.listFiles();
333        if (ArrayUtils.isEmpty(files)) {
334            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged");
335        }
336
337        final ArraySet<String> seenSplits = new ArraySet<>();
338
339        // Verify that all staged packages are internally consistent
340        for (File file : files) {
341            final ApkLite info;
342            try {
343                info = PackageParser.parseApkLite(file, PackageParser.PARSE_GET_SIGNATURES);
344            } catch (PackageParserException e) {
345                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
346                        "Failed to parse " + file + ": " + e);
347            }
348
349            if (!seenSplits.add(info.splitName)) {
350                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
351                        "Split " + info.splitName + " was defined multiple times");
352            }
353
354            // Use first package to define unknown values
355            if (mPackageName == null) {
356                mPackageName = info.packageName;
357                mVersionCode = info.versionCode;
358            }
359            if (mSignatures == null) {
360                mSignatures = info.signatures;
361            }
362
363            assertPackageConsistent(String.valueOf(file), info.packageName, info.versionCode,
364                    info.signatures);
365
366            // Take this opportunity to enforce uniform naming
367            final String name;
368            if (info.splitName == null) {
369                name = "base.apk";
370            } else {
371                name = "split_" + info.splitName + ".apk";
372            }
373            if (!FileUtils.isValidExtFilename(name)) {
374                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
375                        "Invalid filename: " + name);
376            }
377            if (!file.getName().equals(name)) {
378                file.renameTo(new File(file.getParentFile(), name));
379            }
380        }
381
382        // TODO: shift package signature verification to installer; we're
383        // currently relying on PMS to do this.
384        // TODO: teach about compatible upgrade keysets.
385
386        if (params.fullInstall) {
387            // Full installs must include a base package
388            if (!seenSplits.contains(null)) {
389                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
390                        "Full install must include a base package");
391            }
392
393        } else {
394            // Partial installs must be consistent with existing install.
395            final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId);
396            if (app == null) {
397                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
398                        "Missing existing base package for " + mPackageName);
399            }
400
401            final ApkLite info;
402            try {
403                info = PackageParser.parseApkLite(new File(app.getBaseCodePath()),
404                        PackageParser.PARSE_GET_SIGNATURES);
405            } catch (PackageParserException e) {
406                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
407                        "Failed to parse existing base " + app.getBaseCodePath() + ": " + e);
408            }
409
410            assertPackageConsistent("Existing base", info.packageName, info.versionCode,
411                    info.signatures);
412        }
413    }
414
415    private void assertPackageConsistent(String tag, String packageName, int versionCode,
416            Signature[] signatures) throws PackageManagerException {
417        if (!mPackageName.equals(packageName)) {
418            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package "
419                    + packageName + " inconsistent with " + mPackageName);
420        }
421        if (mVersionCode != versionCode) {
422            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag
423                    + " version code " + versionCode + " inconsistent with "
424                    + mVersionCode);
425        }
426        if (!Signature.areExactMatch(mSignatures, signatures)) {
427            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
428                    tag + " signatures are inconsistent");
429        }
430    }
431
432    /**
433     * Application is already installed; splice existing files that haven't been
434     * overridden into our stage.
435     */
436    private void spliceExistingFilesIntoStage() throws PackageManagerException {
437        final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId);
438        final File existingDir = new File(app.getBaseCodePath());
439
440        try {
441            linkTreeIgnoringExisting(existingDir, sessionStageDir);
442        } catch (ErrnoException e) {
443            throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
444                    "Failed to splice into stage");
445        }
446    }
447
448    /**
449     * Recursively hard link all files from source directory tree to target.
450     * When a file already exists in the target tree, it leaves that file
451     * intact.
452     */
453    private void linkTreeIgnoringExisting(File sourceDir, File targetDir) throws ErrnoException {
454        final File[] sourceContents = sourceDir.listFiles();
455        if (ArrayUtils.isEmpty(sourceContents)) return;
456
457        for (File sourceFile : sourceContents) {
458            final File targetFile = new File(targetDir, sourceFile.getName());
459
460            if (sourceFile.isDirectory()) {
461                targetFile.mkdir();
462                linkTreeIgnoringExisting(sourceFile, targetFile);
463            } else {
464                Libcore.os.link(sourceFile.getAbsolutePath(), targetFile.getAbsolutePath());
465            }
466        }
467    }
468
469    @Override
470    public void destroy() {
471        try {
472            destroyInternal();
473        } finally {
474            mCallback.onSessionFinished(this, false);
475        }
476    }
477
478    private void destroyInternal() {
479        synchronized (mLock) {
480            mInvalid = true;
481        }
482        FileUtils.deleteContents(sessionStageDir);
483        sessionStageDir.delete();
484    }
485
486    void dump(IndentingPrintWriter pw) {
487        pw.println("Session " + sessionId + ":");
488        pw.increaseIndent();
489
490        pw.printPair("userId", userId);
491        pw.printPair("installerPackageName", installerPackageName);
492        pw.printPair("installerUid", installerUid);
493        pw.printPair("createdMillis", createdMillis);
494        pw.printPair("sessionStageDir", sessionStageDir);
495        pw.println();
496
497        params.dump(pw);
498
499        pw.printPair("mClientProgress", mClientProgress);
500        pw.printPair("mProgress", mProgress);
501        pw.printPair("mMutationsAllowed", mMutationsAllowed);
502        pw.printPair("mPermissionsConfirmed", mPermissionsConfirmed);
503        pw.printPair("mBridges", mBridges.size());
504        pw.println();
505
506        pw.decreaseIndent();
507    }
508}
509