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