PackageInstallerSession.java revision f06009542390472872da986486d385001e91a2a7
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_ABORTED;
20import static android.content.pm.PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
21import static android.content.pm.PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
22import static android.content.pm.PackageManager.INSTALL_FAILED_INVALID_APK;
23import static android.content.pm.PackageManager.INSTALL_FAILED_PACKAGE_CHANGED;
24import static android.system.OsConstants.O_CREAT;
25import static android.system.OsConstants.O_RDONLY;
26import static android.system.OsConstants.O_WRONLY;
27
28import android.content.Intent;
29import android.content.pm.ApplicationInfo;
30import android.content.pm.IPackageInstallObserver2;
31import android.content.pm.IPackageInstallerSession;
32import android.content.pm.InstallSessionInfo;
33import android.content.pm.InstallSessionParams;
34import android.content.pm.PackageInstaller;
35import android.content.pm.PackageManager;
36import android.content.pm.PackageParser;
37import android.content.pm.PackageParser.ApkLite;
38import android.content.pm.PackageParser.PackageParserException;
39import android.content.pm.Signature;
40import android.os.Bundle;
41import android.os.FileBridge;
42import android.os.FileUtils;
43import android.os.Handler;
44import android.os.Looper;
45import android.os.Message;
46import android.os.ParcelFileDescriptor;
47import android.os.RemoteException;
48import android.os.UserHandle;
49import android.system.ErrnoException;
50import android.system.Os;
51import android.system.OsConstants;
52import android.system.StructStat;
53import android.util.ArraySet;
54import android.util.ExceptionUtils;
55import android.util.MathUtils;
56import android.util.Slog;
57
58import com.android.internal.annotations.GuardedBy;
59import com.android.internal.util.ArrayUtils;
60import com.android.internal.util.IndentingPrintWriter;
61import com.android.internal.util.Preconditions;
62
63import libcore.io.Libcore;
64
65import java.io.File;
66import java.io.FileDescriptor;
67import java.io.IOException;
68import java.util.ArrayList;
69import java.util.concurrent.atomic.AtomicInteger;
70
71public class PackageInstallerSession extends IPackageInstallerSession.Stub {
72    private static final String TAG = "PackageInstaller";
73    private static final boolean LOGD = true;
74
75    private static final int MSG_COMMIT = 0;
76
77    // TODO: enforce INSTALL_ALLOW_TEST
78    // TODO: enforce INSTALL_ALLOW_DOWNGRADE
79    // TODO: handle INSTALL_EXTERNAL, INSTALL_INTERNAL
80
81    // TODO: treat INHERIT_EXISTING as installExistingPackage()
82
83    private final PackageInstallerService.InternalCallback mCallback;
84    private final PackageManagerService mPm;
85    private final Handler mHandler;
86
87    final int sessionId;
88    final int userId;
89    final String installerPackageName;
90    final InstallSessionParams params;
91    final long createdMillis;
92    final File sessionStageDir;
93
94    /** Note that UID is not persisted; it's always derived at runtime. */
95    final int installerUid;
96
97    AtomicInteger openCount = new AtomicInteger();
98
99    private final Object mLock = new Object();
100
101    @GuardedBy("mLock")
102    private float mClientProgress = 0;
103    @GuardedBy("mLock")
104    private float mProgress = 0;
105    @GuardedBy("mLock")
106    private float mReportedProgress = -1;
107
108    @GuardedBy("mLock")
109    private boolean mSealed = false;
110    @GuardedBy("mLock")
111    private boolean mPermissionsAccepted = false;
112    @GuardedBy("mLock")
113    private boolean mDestroyed = false;
114
115    private int mFinalStatus;
116    private String mFinalMessage;
117
118    /**
119     * Path to the resolved base APK for this session, which may point at an APK
120     * inside the session (when the session defines the base), or it may point
121     * at the existing base APK (when adding splits to an existing app).
122     * <p>
123     * This is used when confirming permissions, since we can't fully stage the
124     * session inside an ASEC before confirming with user.
125     */
126    @GuardedBy("mLock")
127    private String mResolvedBaseCodePath;
128
129    @GuardedBy("mLock")
130    private ArrayList<FileBridge> mBridges = new ArrayList<>();
131
132    @GuardedBy("mLock")
133    private IPackageInstallObserver2 mRemoteObserver;
134
135    /** Fields derived from commit parsing */
136    private String mPackageName;
137    private int mVersionCode;
138    private Signature[] mSignatures;
139
140    private final Handler.Callback mHandlerCallback = new Handler.Callback() {
141        @Override
142        public boolean handleMessage(Message msg) {
143            synchronized (mLock) {
144                if (msg.obj != null) {
145                    mRemoteObserver = (IPackageInstallObserver2) msg.obj;
146                }
147
148                try {
149                    commitLocked();
150                } catch (PackageManagerException e) {
151                    Slog.e(TAG, "Install failed: " + e);
152                    destroyInternal();
153                    dispatchSessionFinished(e.error, e.getMessage(), null);
154                }
155
156                return true;
157            }
158        }
159    };
160
161    public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
162            PackageManagerService pm, Looper looper, int sessionId, int userId,
163            String installerPackageName, InstallSessionParams params, long createdMillis,
164            File sessionStageDir, boolean sealed) {
165        mCallback = callback;
166        mPm = pm;
167        mHandler = new Handler(looper, mHandlerCallback);
168
169        this.sessionId = sessionId;
170        this.userId = userId;
171        this.installerPackageName = installerPackageName;
172        this.params = params;
173        this.createdMillis = createdMillis;
174        this.sessionStageDir = sessionStageDir;
175
176        mSealed = sealed;
177
178        // Always derived at runtime
179        installerUid = mPm.getPackageUid(installerPackageName, userId);
180
181        if (mPm.checkPermission(android.Manifest.permission.INSTALL_PACKAGES,
182                installerPackageName) == PackageManager.PERMISSION_GRANTED) {
183            mPermissionsAccepted = true;
184        } else {
185            mPermissionsAccepted = false;
186        }
187
188        computeProgressLocked();
189    }
190
191    public InstallSessionInfo generateInfo() {
192        final InstallSessionInfo info = new InstallSessionInfo();
193
194        info.sessionId = sessionId;
195        info.installerPackageName = installerPackageName;
196        info.resolvedBaseCodePath = mResolvedBaseCodePath;
197        info.progress = mProgress;
198        info.sealed = mSealed;
199        info.open = openCount.get() > 0;
200
201        info.mode = params.mode;
202        info.sizeBytes = params.sizeBytes;
203        info.appPackageName = params.appPackageName;
204        info.appIcon = params.appIcon;
205        info.appLabel = params.appLabel;
206
207        return info;
208    }
209
210    private void assertNotSealed(String cookie) {
211        synchronized (mLock) {
212            if (mSealed) {
213                throw new SecurityException(cookie + " not allowed after commit");
214            }
215        }
216    }
217
218    @Override
219    public void setClientProgress(float progress) {
220        synchronized (mLock) {
221            mClientProgress = progress;
222            computeProgressLocked();
223        }
224        maybePublishProgress();
225    }
226
227    @Override
228    public void addClientProgress(float progress) {
229        synchronized (mLock) {
230            mClientProgress += progress;
231            computeProgressLocked();
232        }
233        maybePublishProgress();
234    }
235
236    private void computeProgressLocked() {
237        mProgress = MathUtils.constrain(mClientProgress * 0.8f, 0f, 0.8f);
238    }
239
240    private void maybePublishProgress() {
241        // Only publish when meaningful change
242        if (Math.abs(mProgress - mReportedProgress) > 0.01) {
243            mReportedProgress = mProgress;
244            mCallback.onSessionProgressChanged(this, mProgress);
245        }
246    }
247
248    @Override
249    public String[] list() {
250        assertNotSealed("list");
251        return sessionStageDir.list();
252    }
253
254    @Override
255    public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) {
256        try {
257            return openWriteInternal(name, offsetBytes, lengthBytes);
258        } catch (IOException e) {
259            throw ExceptionUtils.wrap(e);
260        }
261    }
262
263    private ParcelFileDescriptor openWriteInternal(String name, long offsetBytes, long lengthBytes)
264            throws IOException {
265        // TODO: relay over to DCS when installing to ASEC
266
267        // Quick sanity check of state, and allocate a pipe for ourselves. We
268        // then do heavy disk allocation outside the lock, but this open pipe
269        // will block any attempted install transitions.
270        final FileBridge bridge;
271        synchronized (mLock) {
272            assertNotSealed("openWrite");
273
274            bridge = new FileBridge();
275            mBridges.add(bridge);
276        }
277
278        try {
279            // Use installer provided name for now; we always rename later
280            if (!FileUtils.isValidExtFilename(name)) {
281                throw new IllegalArgumentException("Invalid name: " + name);
282            }
283            final File target = new File(sessionStageDir, name);
284
285            final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(),
286                    O_CREAT | O_WRONLY, 0644);
287            Os.chmod(target.getAbsolutePath(), 0644);
288
289            // If caller specified a total length, allocate it for them. Free up
290            // cache space to grow, if needed.
291            if (lengthBytes > 0) {
292                final StructStat stat = Libcore.os.fstat(targetFd);
293                final long deltaBytes = lengthBytes - stat.st_size;
294                if (deltaBytes > 0) {
295                    mPm.freeStorage(deltaBytes);
296                }
297                Libcore.os.posix_fallocate(targetFd, 0, lengthBytes);
298            }
299
300            if (offsetBytes > 0) {
301                Libcore.os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET);
302            }
303
304            bridge.setTargetFile(targetFd);
305            bridge.start();
306            return new ParcelFileDescriptor(bridge.getClientSocket());
307
308        } catch (ErrnoException e) {
309            throw e.rethrowAsIOException();
310        }
311    }
312
313    @Override
314    public ParcelFileDescriptor openRead(String name) {
315        try {
316            return openReadInternal(name);
317        } catch (IOException e) {
318            throw ExceptionUtils.wrap(e);
319        }
320    }
321
322    private ParcelFileDescriptor openReadInternal(String name) throws IOException {
323        assertNotSealed("openRead");
324
325        try {
326            if (!FileUtils.isValidExtFilename(name)) {
327                throw new IllegalArgumentException("Invalid name: " + name);
328            }
329            final File target = new File(sessionStageDir, name);
330
331            final FileDescriptor targetFd = Libcore.os.open(target.getAbsolutePath(), O_RDONLY, 0);
332            return new ParcelFileDescriptor(targetFd);
333
334        } catch (ErrnoException e) {
335            throw e.rethrowAsIOException();
336        }
337    }
338
339    @Override
340    public void commit(IPackageInstallObserver2 observer) {
341        Preconditions.checkNotNull(observer);
342        mHandler.obtainMessage(MSG_COMMIT, observer).sendToTarget();
343    }
344
345    private void commitLocked() throws PackageManagerException {
346        if (mDestroyed) {
347            throw new PackageManagerException(INSTALL_FAILED_ALREADY_EXISTS, "Invalid session");
348        }
349
350        // Verify that all writers are hands-off
351        if (!mSealed) {
352            for (FileBridge bridge : mBridges) {
353                if (!bridge.isClosed()) {
354                    throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
355                            "Files still open");
356                }
357            }
358            mSealed = true;
359
360            // TODO: persist disabled mutations before going forward, since
361            // beyond this point we may have hardlinks to the valid install
362        }
363
364        // Verify that stage looks sane with respect to existing application.
365        // This currently only ensures packageName, versionCode, and certificate
366        // consistency.
367        validateInstallLocked();
368
369        Preconditions.checkNotNull(mPackageName);
370        Preconditions.checkNotNull(mSignatures);
371        Preconditions.checkNotNull(mResolvedBaseCodePath);
372
373        if (!mPermissionsAccepted) {
374            // User needs to accept permissions; give installer an intent they
375            // can use to involve user.
376            final Intent intent = new Intent(PackageInstaller.ACTION_CONFIRM_PERMISSIONS);
377            intent.setPackage("com.android.packageinstaller");
378            intent.putExtra(PackageInstaller.EXTRA_SESSION_ID, sessionId);
379            try {
380                mRemoteObserver.onUserActionRequired(intent);
381            } catch (RemoteException ignored) {
382            }
383            return;
384        }
385
386        // Inherit any packages and native libraries from existing install that
387        // haven't been overridden.
388        if (params.mode == InstallSessionParams.MODE_INHERIT_EXISTING) {
389            spliceExistingFilesIntoStage();
390        }
391
392        // TODO: surface more granular state from dexopt
393        mCallback.onSessionProgressChanged(this, 0.9f);
394
395        // TODO: for ASEC based applications, grow and stream in packages
396
397        // We've reached point of no return; call into PMS to install the stage.
398        // Regardless of success or failure we always destroy session.
399        final IPackageInstallObserver2 remoteObserver = mRemoteObserver;
400        final IPackageInstallObserver2 localObserver = new IPackageInstallObserver2.Stub() {
401            @Override
402            public void onUserActionRequired(Intent intent) {
403                throw new IllegalStateException();
404            }
405
406            @Override
407            public void onPackageInstalled(String basePackageName, int returnCode, String msg,
408                    Bundle extras) {
409                destroyInternal();
410                dispatchSessionFinished(returnCode, msg, extras);
411            }
412        };
413
414        mPm.installStage(mPackageName, this.sessionStageDir, localObserver, params,
415                installerPackageName, installerUid, new UserHandle(userId));
416    }
417
418    /**
419     * Validate install by confirming that all application packages are have
420     * consistent package name, version code, and signing certificates.
421     * <p>
422     * Renames package files in stage to match split names defined inside.
423     */
424    private void validateInstallLocked() throws PackageManagerException {
425        mPackageName = null;
426        mVersionCode = -1;
427        mSignatures = null;
428        mResolvedBaseCodePath = null;
429
430        final File[] files = sessionStageDir.listFiles();
431        if (ArrayUtils.isEmpty(files)) {
432            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged");
433        }
434
435        final ArraySet<String> seenSplits = new ArraySet<>();
436
437        // Verify that all staged packages are internally consistent
438        for (File file : files) {
439            final ApkLite info;
440            try {
441                info = PackageParser.parseApkLite(file, PackageParser.PARSE_GET_SIGNATURES);
442            } catch (PackageParserException e) {
443                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
444                        "Failed to parse " + file + ": " + e);
445            }
446
447            if (!seenSplits.add(info.splitName)) {
448                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
449                        "Split " + info.splitName + " was defined multiple times");
450            }
451
452            // Use first package to define unknown values
453            if (mPackageName == null) {
454                mPackageName = info.packageName;
455                mVersionCode = info.versionCode;
456            }
457            if (mSignatures == null) {
458                mSignatures = info.signatures;
459            }
460
461            assertPackageConsistent(String.valueOf(file), info.packageName, info.versionCode,
462                    info.signatures);
463
464            // Take this opportunity to enforce uniform naming
465            final String targetName;
466            if (info.splitName == null) {
467                targetName = "base.apk";
468            } else {
469                targetName = "split_" + info.splitName + ".apk";
470            }
471            if (!FileUtils.isValidExtFilename(targetName)) {
472                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
473                        "Invalid filename: " + targetName);
474            }
475
476            final File targetFile = new File(sessionStageDir, targetName);
477            if (!file.equals(targetFile)) {
478                file.renameTo(targetFile);
479            }
480
481            // Base is coming from session
482            if (info.splitName == null) {
483                mResolvedBaseCodePath = targetFile.getAbsolutePath();
484            }
485        }
486
487        // TODO: shift package signature verification to installer; we're
488        // currently relying on PMS to do this.
489        // TODO: teach about compatible upgrade keysets.
490
491        if (params.mode == InstallSessionParams.MODE_FULL_INSTALL) {
492            // Full installs must include a base package
493            if (!seenSplits.contains(null)) {
494                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
495                        "Full install must include a base package");
496            }
497
498        } else {
499            // Partial installs must be consistent with existing install
500            final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId);
501            if (app == null) {
502                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
503                        "Missing existing base package for " + mPackageName);
504            }
505
506            // Base might be inherited from existing install
507            if (mResolvedBaseCodePath == null) {
508                mResolvedBaseCodePath = app.getBaseCodePath();
509            }
510
511            final ApkLite info;
512            try {
513                info = PackageParser.parseApkLite(new File(app.getBaseCodePath()),
514                        PackageParser.PARSE_GET_SIGNATURES);
515            } catch (PackageParserException e) {
516                throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
517                        "Failed to parse existing base " + app.getBaseCodePath() + ": " + e);
518            }
519
520            assertPackageConsistent("Existing base", info.packageName, info.versionCode,
521                    info.signatures);
522        }
523    }
524
525    private void assertPackageConsistent(String tag, String packageName, int versionCode,
526            Signature[] signatures) throws PackageManagerException {
527        if (!mPackageName.equals(packageName)) {
528            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag + " package "
529                    + packageName + " inconsistent with " + mPackageName);
530        }
531        if (mVersionCode != versionCode) {
532            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, tag
533                    + " version code " + versionCode + " inconsistent with "
534                    + mVersionCode);
535        }
536        if (!Signature.areExactMatch(mSignatures, signatures)) {
537            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
538                    tag + " signatures are inconsistent");
539        }
540    }
541
542    /**
543     * Application is already installed; splice existing files that haven't been
544     * overridden into our stage.
545     */
546    private void spliceExistingFilesIntoStage() throws PackageManagerException {
547        final ApplicationInfo app = mPm.getApplicationInfo(mPackageName, 0, userId);
548
549        int n = 0;
550        final File[] oldFiles = new File(app.getCodePath()).listFiles();
551        if (!ArrayUtils.isEmpty(oldFiles)) {
552            for (File oldFile : oldFiles) {
553                if (!PackageParser.isApkFile(oldFile)) continue;
554
555                final File newFile = new File(sessionStageDir, oldFile.getName());
556                try {
557                    Os.link(oldFile.getAbsolutePath(), newFile.getAbsolutePath());
558                    n++;
559                } catch (ErrnoException e) {
560                    throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
561                            "Failed to splice into stage", e);
562                }
563            }
564        }
565
566        if (LOGD) Slog.d(TAG, "Spliced " + n + " existing APKs into stage");
567    }
568
569    void setPermissionsResult(boolean accepted) {
570        if (!mSealed) {
571            throw new SecurityException("Must be sealed to accept permissions");
572        }
573
574        if (accepted) {
575            // Mark and kick off another install pass
576            mPermissionsAccepted = true;
577            mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
578        } else {
579            destroyInternal();
580            dispatchSessionFinished(INSTALL_FAILED_ABORTED, "User rejected permissions", null);
581        }
582    }
583
584    @Override
585    public void close() {
586        if (openCount.decrementAndGet() == 0) {
587            mCallback.onSessionClosed(this);
588        }
589    }
590
591    @Override
592    public void abandon() {
593        destroyInternal();
594        dispatchSessionFinished(INSTALL_FAILED_ABORTED, "Session was abandoned", null);
595    }
596
597    private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
598        mFinalStatus = returnCode;
599        mFinalMessage = msg;
600
601        if (mRemoteObserver != null) {
602            try {
603                mRemoteObserver.onPackageInstalled(mPackageName, returnCode, msg, extras);
604            } catch (RemoteException ignored) {
605            }
606        }
607
608        final boolean success = (returnCode == PackageManager.INSTALL_SUCCEEDED);
609        mCallback.onSessionFinished(this, success);
610    }
611
612    private void destroyInternal() {
613        synchronized (mLock) {
614            mSealed = true;
615            mDestroyed = true;
616        }
617        FileUtils.deleteContents(sessionStageDir);
618        sessionStageDir.delete();
619    }
620
621    void dump(IndentingPrintWriter pw) {
622        pw.println("Session " + sessionId + ":");
623        pw.increaseIndent();
624
625        pw.printPair("userId", userId);
626        pw.printPair("installerPackageName", installerPackageName);
627        pw.printPair("installerUid", installerUid);
628        pw.printPair("createdMillis", createdMillis);
629        pw.printPair("sessionStageDir", sessionStageDir);
630        pw.println();
631
632        params.dump(pw);
633
634        pw.printPair("mClientProgress", mClientProgress);
635        pw.printPair("mProgress", mProgress);
636        pw.printPair("mSealed", mSealed);
637        pw.printPair("mPermissionsAccepted", mPermissionsAccepted);
638        pw.printPair("mDestroyed", mDestroyed);
639        pw.printPair("mBridges", mBridges.size());
640        pw.printPair("mFinalStatus", mFinalStatus);
641        pw.printPair("mFinalMessage", mFinalMessage);
642        pw.println();
643
644        pw.decreaseIndent();
645    }
646
647    Snapshot snapshot() {
648        return new Snapshot(this);
649    }
650
651    static class Snapshot {
652        final float clientProgress;
653        final boolean sealed;
654
655        public Snapshot(PackageInstallerSession session) {
656            clientProgress = session.mClientProgress;
657            sealed = session.mSealed;
658        }
659    }
660}
661