From: Daniel Colascione Date: Thu, 22 Mar 2018 02:13:57 +0000 (-0700) Subject: Teach PinnerService to pin parts of APKs X-Git-Tag: android-x86-9.0-r1~104^2~17^2 X-Git-Url: http://git.osdn.net/view?a=commitdiff_plain;h=9779b5ec4f4a5ba43ebbfb3461ec840105900e8c;p=android-x86%2Fframeworks-base.git Teach PinnerService to pin parts of APKs This change teaches PinnerService how to pin parts of APK files, with the specific regions of interest described by a small metadata file in the APK root directory. Apksig has been modified to annotate signed APKs with pinning metadata. This CL also fixes a few resource management bugs and enhances the dumpsys output. Test: built dummy camera app, pinned stuff, verified log output Change-Id: If72709ad2c0f2ec748f547a48a289473e4c60bab Bug: 65316207 --- diff --git a/services/core/java/com/android/server/PinnerService.java b/services/core/java/com/android/server/PinnerService.java index 2869114601b7..5a25f48c38e3 100644 --- a/services/core/java/com/android/server/PinnerService.java +++ b/services/core/java/com/android/server/PinnerService.java @@ -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; + /** *

PinnerService pins important files for key processes in memory.

*

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 mPinnedFiles = new ArrayList(); private final ArrayList mPinnedCameraFiles = new ArrayList(); - 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,64 +158,43 @@ 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); - 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]); - } + // 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; } - } - } - /** - * 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 (DEBUG) { - Slog.v(TAG, "Failed to pin camera."); - } - } + synchronized (this) { + mPinnedFiles.add(pf); } } } /** - * determine if the camera app is already pinned by comparing the - * intent resolution to the pinned files list + * Handler for camera pinning message */ - 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; + private void handlePinCamera(int userHandle) { + if (!mShouldPinCamera) return; + if (!pinCamera(userHandle)) { + if (DEBUG) { + Slog.v(TAG, "Failed to pin camera."); } } - return false; } private void unpinCameraApp() { - for (int i = 0; i < mPinnedCameraFiles.size(); i++) { - unpinFile(mPinnedCameraFiles.get(i)); + ArrayList pinnedCameraFiles; + synchronized (this) { + pinnedCameraFiles = new ArrayList<>(mPinnedCameraFiles); + mPinnedCameraFiles.clear(); + } + for (PinnedFile pinnedFile : pinnedCameraFiles) { + pinnedFile.close(); } - mPinnedCameraFiles.clear(); } private boolean isResolverActivity(ActivityInfo info) { @@ -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); } - 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) { - mPinnedCameraFiles.add(pf); + 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: + * + *

+     *   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
+     * 
+ * + * (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); - - StructStat sb = Os.fstat(fd); - - 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; + if (attemptPinIntrospection) { + fileAsZip = maybeOpenZip(fileToPin); } - if (length == 0) { - length = sb.st_size - offset; + if (fileAsZip != null) { + pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin); } - if (maxSize > 0 && length > maxSize) { - Slog.e(TAG, "Could not pin file " + fileToPin + - ", size = " + length + ", maxSize = " + maxSize); - Os.close(fd); - 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 + } + } + + /** + * 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; + } + + /** + * 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; + } + + 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); + } + + private static final class PinRangeSourceStatic extends PinRangeSource { + private final int mPinStart; + private final int mPinLength; + private boolean mDone = false; + + PinRangeSourceStatic(int pinStart, int pinLength) { + mPinStart = pinStart; + mPinLength = pinLength; + } - long address = Os.mmap(0, length, OsConstants.PROT_READ, - OsConstants.MAP_PRIVATE, fd, offset); - Os.close(fd); + @Override + boolean read(PinRange outPinRange) { + outPinRange.start = mPinStart; + outPinRange.length = mPinLength; + boolean done = mDone; + mDone = true; + return !done; + } + } - Os.mlock(address, length); + private static final class PinRangeSourceStream extends PinRangeSource { + private final DataInputStream mStream; + 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()) { + 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 ); + } + + /** + * 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); + } } - return true; + } + + private synchronized ArrayList snapshotPinnedFiles() { + int nrPinnedFiles = mPinnedFiles.size() + mPinnedCameraFiles.size(); + ArrayList pinnedFiles = new ArrayList<>(nrPinnedFiles); + pinnedFiles.addAll(mPinnedFiles); + pinnedFiles.addAll(mPinnedCameraFiles); + return pinnedFiles; } private final class BinderService extends Binder { @@ -373,31 +594,44 @@ 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 {