Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e54efcef authored by Daniel Colascione's avatar Daniel Colascione Committed by Android (Google) Code Review
Browse files

Merge "Teach PinnerService to pin parts of APKs" into pi-dev

parents 1561bb41 9779b5ec
Loading
Loading
Loading
Loading
+349 −115
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server;

import android.annotation.Nullable;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -47,10 +49,18 @@ import dalvik.system.DexFile;
import dalvik.system.VMRuntime;

import java.io.FileDescriptor;
import java.io.Closeable;
import java.io.InputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.EOFException;
import java.io.PrintWriter;
import java.util.ArrayList;

import java.util.zip.ZipFile;
import java.util.zip.ZipException;
import java.util.zip.ZipEntry;

/**
 * <p>PinnerService pins important files for key processes in memory.</p>
 * <p>Files to pin are specified in the config_defaultPinnerServiceFiles
@@ -60,16 +70,18 @@ import java.util.ArrayList;
public final class PinnerService extends SystemService {
    private static final boolean DEBUG = false;
    private static final String TAG = "PinnerService";
    private static final int MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); //80MB max
    private static final String PIN_META_FILENAME = "pinlist.meta";
    private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);

    private final Context mContext;
    private final boolean mShouldPinCamera;

    /* These lists protected by PinnerService monitor lock */
    private final ArrayList<PinnedFile> mPinnedFiles = new ArrayList<PinnedFile>();
    private final ArrayList<PinnedFile> mPinnedCameraFiles = new ArrayList<PinnedFile>();
    private final boolean mShouldPinCamera;

    private BinderService mBinderService;

    private final long MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); //80MB max

    private PinnerHandler mPinnerHandler = null;

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -146,18 +158,18 @@ public final class PinnerService extends SystemService {
         // Files to pin come from the overlay and can be specified per-device config
        String[] filesToPin = mContext.getResources().getStringArray(
                com.android.internal.R.array.config_defaultPinnerServiceFiles);
        // Continue trying to pin each file even if we fail to pin some of them
        for (String fileToPin : filesToPin) {
            PinnedFile pf = pinFile(fileToPin,
                                    Integer.MAX_VALUE,
                                    /*attemptPinIntrospection=*/false);
            if (pf == null) {
                Slog.e(TAG, "Failed to pin file = " + fileToPin);
                continue;
            }

            synchronized (this) {
            // Continue trying to pin remaining files even if there is a failure
            for (int i = 0; i < filesToPin.length; i++){
                PinnedFile pf = pinFile(filesToPin[i], 0, 0, 0);
                if (pf != null) {
                mPinnedFiles.add(pf);
                    if (DEBUG) {
                        Slog.i(TAG, "Pinned file = " + pf.mFilename);
                    }
                } else {
                    Slog.e(TAG, "Failed to pin file = " + filesToPin[i]);
                }
            }
        }
    }
@@ -166,45 +178,24 @@ public final class PinnerService extends SystemService {
     * Handler for camera pinning message
     */
    private void handlePinCamera(int userHandle) {
        if (mShouldPinCamera) {
            synchronized(this) {
                boolean success = pinCamera(userHandle);
                if (!success) {
                    //this is not necessarily an error
        if (!mShouldPinCamera) return;
        if (!pinCamera(userHandle)) {
            if (DEBUG) {
                Slog.v(TAG, "Failed to pin camera.");
            }
        }
    }
        }
    }

    /**
     *  determine if the camera app is already pinned by comparing the
     *  intent resolution to the pinned files list
     */
    private boolean alreadyPinned(int userHandle) {
        ApplicationInfo cameraInfo = getCameraInfo(userHandle);
        if (cameraInfo == null ) {
            return false;
        }
        for (int i = 0; i < mPinnedCameraFiles.size(); i++) {
            if (mPinnedCameraFiles.get(i).mFilename.equals(cameraInfo.sourceDir)) {
                if (DEBUG) {
                  Slog.v(TAG, "Camera is already pinned");
                }
                return true;
            }
        }
        return false;
    }

    private void unpinCameraApp() {
        for (int i = 0; i < mPinnedCameraFiles.size(); i++) {
            unpinFile(mPinnedCameraFiles.get(i));
        }
        ArrayList<PinnedFile> pinnedCameraFiles;
        synchronized (this) {
            pinnedCameraFiles = new ArrayList<>(mPinnedCameraFiles);
            mPinnedCameraFiles.clear();
        }
        for (PinnedFile pinnedFile : pinnedCameraFiles) {
            pinnedFile.close();
        }
    }

    private boolean isResolverActivity(ActivityInfo info) {
        return ResolverActivity.class.getName().equals(info.name);
@@ -255,15 +246,19 @@ public final class PinnerService extends SystemService {

        //pin APK
        String camAPK = cameraInfo.sourceDir;
        PinnedFile pf = pinFile(camAPK, 0, 0, MAX_CAMERA_PIN_SIZE);
        PinnedFile pf = pinFile(camAPK,
                                MAX_CAMERA_PIN_SIZE,
                                /*attemptPinIntrospection=*/true);
        if (pf == null) {
            Slog.e(TAG, "Failed to pin " + camAPK);
            return false;
        }
        if (DEBUG) {
            Slog.i(TAG, "Pinned " + pf.mFilename);
            Slog.i(TAG, "Pinned " + pf.fileName);
        }
        synchronized (this) {
            mPinnedCameraFiles.add(pf);
        }

        // determine the ABI from either ApplicationInfo or Build
        String arch = "arm";
@@ -289,11 +284,13 @@ public final class PinnerService extends SystemService {

        //not pinning the oat/odex is not a fatal error
        for (String file : files) {
            pf = pinFile(file, 0, 0, MAX_CAMERA_PIN_SIZE);
            pf = pinFile(file, MAX_CAMERA_PIN_SIZE, /*attemptPinIntrospection=*/false);
            if (pf != null) {
                synchronized (this) {
                    mPinnedCameraFiles.add(pf);
                }
                if (DEBUG) {
                    Slog.i(TAG, "Pinned " + pf.mFilename);
                    Slog.i(TAG, "Pinned " + pf.fileName);
                }
            }
        }
@@ -302,70 +299,294 @@ public final class PinnerService extends SystemService {
    }


    /** mlock length bytes of fileToPin in memory, starting at offset
     *  length == 0 means pin from offset to end of file
     *  maxSize == 0 means infinite
    /** mlock length bytes of fileToPin in memory
     *
     * If attemptPinIntrospection is true, then treat the file to pin as a zip file and
     * look for a "pinlist.meta" file in the archive root directory. The structure of this
     * file is a PINLIST_META as described below:
     *
     * <pre>
     *   PINLIST_META: PIN_RANGE*
     *   PIN_RANGE: PIN_START PIN_LENGTH
     *   PIN_START: big endian i32: offset in bytes of pin region from file start
     *   PIN_LENGTH: big endian i32: length of pin region in bytes
     * </pre>
     *
     * (We use big endian because that's what DataInputStream is hardcoded to use.)
     *
     * If attemptPinIntrospection is false, then we use a single implicit PIN_RANGE of (0,
     * maxBytesToPin); that is, we attempt to pin the first maxBytesToPin bytes of the file.
     *
     * After we open a file, we march through the list of pin ranges and attempt to pin
     * each one, stopping after we've pinned maxBytesToPin bytes. (We may truncate the last
     * pinned range to fit.)  In this way, by choosing to emit certain PIN_RANGE pairs
     * before others, file generators can express pins in priority order, making most
     * effective use of the pinned-page quota.
     *
     * N.B. Each PIN_RANGE is clamped to the actual bounds of the file; all inputs have a
     * meaningful interpretation. Also, a range locking a single byte of a page locks the
     * whole page. Any truncated PIN_RANGE at EOF is ignored. Overlapping pinned entries
     * are legal, but each pin of a byte counts toward the pin quota regardless of whether
     * that byte has already been pinned, so the generator of PINLIST_META ought to ensure
     * that ranges are non-overlapping.
     *
     * @param fileToPin Path to file to pin
     * @param maxBytesToPin Maximum number of bytes to pin
     * @param attemptPinIntrospection If true, try to open file as a
     *   zip in order to extract the
     * @return Pinned memory resource owner thing or null on error
     */
    private static PinnedFile pinFile(String fileToPin, long offset, long length, long maxSize) {
        FileDescriptor fd = new FileDescriptor();
    private static PinnedFile pinFile(String fileToPin,
                                      int maxBytesToPin,
                                      boolean attemptPinIntrospection) {
        ZipFile fileAsZip = null;
        InputStream pinRangeStream = null;
        try {
            fd = Os.open(fileToPin,
                    OsConstants.O_RDONLY | OsConstants.O_CLOEXEC | OsConstants.O_NOFOLLOW,
                    OsConstants.O_RDONLY);
            if (attemptPinIntrospection) {
                fileAsZip = maybeOpenZip(fileToPin);
            }

            StructStat sb = Os.fstat(fd);
            if (fileAsZip != null) {
                pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin);
            }

            if (offset + length > sb.st_size) {
                Os.close(fd);
                Slog.e(TAG, "Failed to pin file " + fileToPin +
                        ", request extends beyond end of file.  offset + length =  "
                        + (offset + length) + ", file length = " + sb.st_size);
                return null;
            Slog.d(TAG, "pinRangeStream: " + pinRangeStream);

            PinRangeSource pinRangeSource = (pinRangeStream != null)
                ? new PinRangeSourceStream(pinRangeStream)
                : new PinRangeSourceStatic(0, Integer.MAX_VALUE /* will be clipped */);
            return pinFileRanges(fileToPin, maxBytesToPin, pinRangeSource);
        } finally {
            safeClose(pinRangeStream);
            safeClose(fileAsZip);  // Also closes any streams we've opened
        }
    }

            if (length == 0) {
                length = sb.st_size - offset;
    /**
     * Attempt to open a file as a zip file. On any sort of corruption, log, swallow the
     * error, and return null.
     */
    private static ZipFile maybeOpenZip(String fileName) {
        ZipFile zip = null;
        try {
            zip = new ZipFile(fileName);
        } catch (IOException ex) {
            Slog.w(TAG,
                   String.format(
                       "could not open \"%s\" as zip: pinning as blob",
                                 fileName),
                   ex);
        }
        return zip;
    }

            if (maxSize > 0 && length > maxSize) {
                Slog.e(TAG, "Could not pin file " + fileToPin +
                        ", size = " + length + ", maxSize = " + maxSize);
                Os.close(fd);
                return null;
    /**
     * Open a pin metadata file in the zip if one is present.
     *
     * @param zipFile Zip file to search
     * @return Open input stream or null on any error
     */
    private static InputStream maybeOpenPinMetaInZip(ZipFile zipFile, String fileName) {
        ZipEntry pinMetaEntry = zipFile.getEntry(PIN_META_FILENAME);
        InputStream pinMetaStream = null;
        if (pinMetaEntry != null) {
            try {
                pinMetaStream = zipFile.getInputStream(pinMetaEntry);
            } catch (IOException ex) {
                Slog.w(TAG,
                       String.format("error reading pin metadata \"%s\": pinning as blob",
                                     fileName),
                       ex);
            }
        }
        return pinMetaStream;
    }

            long address = Os.mmap(0, length, OsConstants.PROT_READ,
                    OsConstants.MAP_PRIVATE, fd, offset);
            Os.close(fd);
    private static abstract class PinRangeSource {
        /** Retrive a range to pin.
         *
         * @param outPinRange Receives the pin region
         * @return True if we filled in outPinRange or false if we're out of pin entries
         */
        abstract boolean read(PinRange outPinRange);
    }

            Os.mlock(address, length);
    private static final class PinRangeSourceStatic extends PinRangeSource {
        private final int mPinStart;
        private final int mPinLength;
        private boolean mDone = false;

            return new PinnedFile(address, length, fileToPin);
        } catch (ErrnoException e) {
            Slog.e(TAG, "Could not pin file " + fileToPin + " with error " + e.getMessage());
            if(fd.valid()) {
        PinRangeSourceStatic(int pinStart, int pinLength) {
            mPinStart = pinStart;
            mPinLength = pinLength;
        }

        @Override
        boolean read(PinRange outPinRange) {
            outPinRange.start = mPinStart;
            outPinRange.length = mPinLength;
            boolean done = mDone;
            mDone = true;
            return !done;
        }
    }

    private static final class PinRangeSourceStream extends PinRangeSource {
        private final DataInputStream mStream;
        private boolean mDone = false;

        PinRangeSourceStream(InputStream stream) {
            mStream = new DataInputStream(stream);
        }

        @Override
        boolean read(PinRange outPinRange) {
            if (!mDone) {
                try {
                    Os.close(fd);
                    outPinRange.start = mStream.readInt();
                    outPinRange.length = mStream.readInt();
                } catch (IOException ex) {
                    mDone = true;
                }
            }
                catch (ErrnoException eClose) {
                    Slog.e(TAG, "Failed to close fd, error = " + eClose.getMessage());
            return !mDone;
        }
    }

    /**
     * Helper for pinFile.
     *
     * @param fileToPin Name of file to pin
     * @param maxBytesToPin Maximum number of bytes to pin
     * @param pinRangeSource Read PIN_RANGE entries from this stream to tell us what bytes
     *   to pin.
     * @return PinnedFile or null on error
     */
    private static PinnedFile pinFileRanges(
        String fileToPin,
        int maxBytesToPin,
        PinRangeSource pinRangeSource)
    {
        FileDescriptor fd = new FileDescriptor();
        long address = -1;
        int mapSize = 0;

        try {
            int openFlags = (OsConstants.O_RDONLY |
                             OsConstants.O_CLOEXEC |
                             OsConstants.O_NOFOLLOW);
            fd = Os.open(fileToPin, openFlags, 0);
            mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
            address = Os.mmap(0, mapSize,
                              OsConstants.PROT_READ,
                              OsConstants.MAP_SHARED,
                              fd, /*offset=*/0);

            PinRange pinRange = new PinRange();
            int bytesPinned = 0;

            // We pin at page granularity, so make sure the limit is page-aligned
            if (maxBytesToPin % PAGE_SIZE != 0) {
                maxBytesToPin -= maxBytesToPin % PAGE_SIZE;
            }

            while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
                int pinStart = pinRange.start;
                int pinLength = pinRange.length;
                pinStart = clamp(0, pinStart, mapSize);
                pinLength = clamp(0, pinLength, mapSize - pinStart);
                pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);

                // mlock doesn't require the region to be page-aligned, but we snap the
                // lock region to page boundaries anyway so that we don't under-count
                // locking a single byte of a page as a charge of one byte even though the
                // OS will retain the whole page. Thanks to this adjustment, we slightly
                // over-count the pin charge of back-to-back pins touching the same page,
                // but better that than undercounting. Besides: nothing stops pin metafile
                // creators from making the actual regions page-aligned.
                pinLength += pinStart % PAGE_SIZE;
                pinStart -= pinStart % PAGE_SIZE;
                if (pinLength % PAGE_SIZE != 0) {
                    pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
                }
                pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);

                if (pinLength > 0) {
                    if (DEBUG) {
                        Slog.d(TAG,
                               String.format(
                                   "pinning at %s %s bytes of %s",
                                   pinStart, pinLength, fileToPin));
                    }
                    Os.mlock(address + pinStart, pinLength);
                }
                bytesPinned += pinLength;
            }

            PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
            address = -1;  // Ownership transferred
            return pinnedFile;
        } catch (ErrnoException ex) {
            Slog.e(TAG, "Could not pin file " + fileToPin, ex);
            return null;
        } finally {
            safeClose(fd);
            if (address >= 0) {
                safeMunmap(address, mapSize);
            }
        }
    }

    private static boolean unpinFile(PinnedFile pf) {
    private static int clamp(int min, int value, int max) {
        return Math.max(min, Math.min(value, max));
    }

    private static void safeMunmap(long address, long mapSize) {
        try {
            Os.munlock(pf.mAddress, pf.mLength);
        } catch (ErrnoException e) {
            Slog.e(TAG, "Failed to unpin file " + pf.mFilename + " with error " + e.getMessage());
            return false;
            Os.munmap(address, mapSize);
        } catch (ErrnoException ex) {
            Slog.w(TAG, "ignoring error in unmap", ex);
        }
        if (DEBUG) {
            Slog.i(TAG, "Unpinned file " + pf.mFilename );
    }
        return true;

    /**
     * Close FD, swallowing irrelevant errors.
     */
    private static void safeClose(@Nullable FileDescriptor fd) {
        if (fd != null && fd.valid()) {
            try {
                Os.close(fd);
            } catch (ErrnoException ex) {
                // Swallow the exception: non-EBADF errors in close(2)
                // indicate deferred paging write errors, which we
                // don't care about here. The underlying file
                // descriptor is always closed.
                if (ex.errno == OsConstants.EBADF) {
                    throw new AssertionError(ex);
                }
            }
        }
    }

    /**
     * Close closeable thing, swallowing errors.
     */
    private static void safeClose(@Nullable Closeable thing) {
        if (thing != null) {
            try {
                thing.close();
            } catch (IOException ex) {
                Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
            }
        }
    }

    private synchronized ArrayList<PinnedFile> snapshotPinnedFiles() {
        int nrPinnedFiles = mPinnedFiles.size() + mPinnedCameraFiles.size();
        ArrayList<PinnedFile> pinnedFiles = new ArrayList<>(nrPinnedFiles);
        pinnedFiles.addAll(mPinnedFiles);
        pinnedFiles.addAll(mPinnedCameraFiles);
        return pinnedFiles;
    }

    private final class BinderService extends Binder {
@@ -373,33 +594,46 @@ public final class PinnerService extends SystemService {
        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
            long totalSize = 0;
            pw.println("Pinned Files:");
            synchronized(this) {
                for (int i = 0; i < mPinnedFiles.size(); i++) {
                    pw.println(mPinnedFiles.get(i).mFilename);
                    totalSize += mPinnedFiles.get(i).mLength;
                }
                for (int i = 0; i < mPinnedCameraFiles.size(); i++) {
                    pw.println(mPinnedCameraFiles.get(i).mFilename);
                    totalSize += mPinnedCameraFiles.get(i).mLength;
            for (PinnedFile pinnedFile : snapshotPinnedFiles()) {
                pw.format("%s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned);
                totalSize += pinnedFile.bytesPinned;
            }
            }
            pw.println("Total size: " + totalSize);
            pw.format("Total size: %s\n", totalSize);
        }
    }

    private static class PinnedFile {
        long mAddress;
        long mLength;
        String mFilename;
    private static final class PinnedFile implements AutoCloseable {
        private long mAddress;
        final int mapSize;
        final String fileName;
        final int bytesPinned;

        PinnedFile(long address, long length, String filename) {
        PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
             mAddress = address;
             mLength = length;
             mFilename = filename;
             this.mapSize = mapSize;
             this.fileName = fileName;
             this.bytesPinned = bytesPinned;
        }

        @Override
        public void close() {
            if (mAddress >= 0) {
                safeMunmap(mAddress, mapSize);
                mAddress = -1;
            }
        }

        @Override
        public void finalize() {
            close();
        }
    }

    final static class PinRange {
        int start;
        int length;
    }

    final class PinnerHandler extends Handler {
        static final int PIN_CAMERA_MSG  = 4000;
        static final int PIN_ONSTART_MSG = 4001;