1 /* 2 * Copyright (C) 2016 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 17 package com.android.server; 18 19 import android.annotation.Nullable; 20 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.content.pm.ActivityInfo; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ResolveInfo; 29 import android.net.Uri; 30 import android.os.Binder; 31 import android.os.Build; 32 import android.os.Handler; 33 import android.os.Looper; 34 import android.os.Message; 35 import android.os.UserHandle; 36 import android.provider.MediaStore; 37 import android.system.ErrnoException; 38 import android.system.Os; 39 import android.system.OsConstants; 40 import android.system.StructStat; 41 import android.util.ArraySet; 42 import android.util.Slog; 43 44 import com.android.internal.app.ResolverActivity; 45 import com.android.internal.os.BackgroundThread; 46 import com.android.internal.util.DumpUtils; 47 48 import dalvik.system.DexFile; 49 import dalvik.system.VMRuntime; 50 51 import java.io.FileDescriptor; 52 import java.io.Closeable; 53 import java.io.InputStream; 54 import java.io.DataInputStream; 55 import java.io.IOException; 56 import java.io.EOFException; 57 import java.io.PrintWriter; 58 import java.util.ArrayList; 59 60 import java.util.zip.ZipFile; 61 import java.util.zip.ZipException; 62 import java.util.zip.ZipEntry; 63 64 /** 65 * <p>PinnerService pins important files for key processes in memory.</p> 66 * <p>Files to pin are specified in the config_defaultPinnerServiceFiles 67 * overlay.</p> 68 * <p>Pin the default camera application if specified in config_pinnerCameraApp.</p> 69 */ 70 public final class PinnerService extends SystemService { 71 private static final boolean DEBUG = false; 72 private static final String TAG = "PinnerService"; 73 private static final int MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); //80MB max 74 private static final String PIN_META_FILENAME = "pinlist.meta"; 75 private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE); 76 77 private final Context mContext; 78 private final boolean mShouldPinCamera; 79 80 /* These lists protected by PinnerService monitor lock */ 81 private final ArrayList<PinnedFile> mPinnedFiles = new ArrayList<PinnedFile>(); 82 private final ArrayList<PinnedFile> mPinnedCameraFiles = new ArrayList<PinnedFile>(); 83 84 private BinderService mBinderService; 85 private PinnerHandler mPinnerHandler = null; 86 87 private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 88 @Override 89 public void onReceive(Context context, Intent intent) { 90 // If this user's camera app has been updated, update pinned files accordingly. 91 if (intent.getAction() == Intent.ACTION_PACKAGE_REPLACED) { 92 Uri packageUri = intent.getData(); 93 String packageName = packageUri.getSchemeSpecificPart(); 94 ArraySet<String> updatedPackages = new ArraySet<>(); 95 updatedPackages.add(packageName); 96 update(updatedPackages); 97 } 98 } 99 }; 100 101 public PinnerService(Context context) { 102 super(context); 103 104 mContext = context; 105 mShouldPinCamera = context.getResources().getBoolean( 106 com.android.internal.R.bool.config_pinnerCameraApp); 107 mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper()); 108 109 IntentFilter filter = new IntentFilter(); 110 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 111 filter.addDataScheme("package"); 112 mContext.registerReceiver(mBroadcastReceiver, filter); 113 } 114 115 @Override 116 public void onStart() { 117 if (DEBUG) { 118 Slog.i(TAG, "Starting PinnerService"); 119 } 120 mBinderService = new BinderService(); 121 publishBinderService("pinner", mBinderService); 122 publishLocalService(PinnerService.class, this); 123 124 mPinnerHandler.obtainMessage(PinnerHandler.PIN_ONSTART_MSG).sendToTarget(); 125 mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, UserHandle.USER_SYSTEM, 0) 126 .sendToTarget(); 127 } 128 129 /** 130 * Pin camera on user switch. 131 * If more than one user is using the device 132 * each user may set a different preference for the camera app. 133 * Make sure that user's preference is pinned into memory. 134 */ 135 @Override 136 public void onSwitchUser(int userHandle) { 137 mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, userHandle, 0).sendToTarget(); 138 } 139 140 /** 141 * Update the currently pinned files. 142 * Specifically, this only updates camera pinning. 143 * The other files pinned in onStart will not need to be updated. 144 */ 145 public void update(ArraySet<String> updatedPackages) { 146 ApplicationInfo cameraInfo = getCameraInfo(UserHandle.USER_SYSTEM); 147 if (cameraInfo != null && updatedPackages.contains(cameraInfo.packageName)) { 148 Slog.i(TAG, "Updating pinned files."); 149 mPinnerHandler.obtainMessage(PinnerHandler.PIN_CAMERA_MSG, UserHandle.USER_SYSTEM, 0) 150 .sendToTarget(); 151 } 152 } 153 154 /** 155 * Handler for on start pinning message 156 */ 157 private void handlePinOnStart() { 158 // Files to pin come from the overlay and can be specified per-device config 159 String[] filesToPin = mContext.getResources().getStringArray( 160 com.android.internal.R.array.config_defaultPinnerServiceFiles); 161 // Continue trying to pin each file even if we fail to pin some of them 162 for (String fileToPin : filesToPin) { 163 PinnedFile pf = pinFile(fileToPin, 164 Integer.MAX_VALUE, 165 /*attemptPinIntrospection=*/false); 166 if (pf == null) { 167 Slog.e(TAG, "Failed to pin file = " + fileToPin); 168 continue; 169 } 170 171 synchronized (this) { 172 mPinnedFiles.add(pf); 173 } 174 } 175 } 176 177 /** 178 * Handler for camera pinning message 179 */ 180 private void handlePinCamera(int userHandle) { 181 if (!mShouldPinCamera) return; 182 if (!pinCamera(userHandle)) { 183 if (DEBUG) { 184 Slog.v(TAG, "Failed to pin camera."); 185 } 186 } 187 } 188 189 private void unpinCameraApp() { 190 ArrayList<PinnedFile> pinnedCameraFiles; 191 synchronized (this) { 192 pinnedCameraFiles = new ArrayList<>(mPinnedCameraFiles); 193 mPinnedCameraFiles.clear(); 194 } 195 for (PinnedFile pinnedFile : pinnedCameraFiles) { 196 pinnedFile.close(); 197 } 198 } 199 200 private boolean isResolverActivity(ActivityInfo info) { 201 return ResolverActivity.class.getName().equals(info.name); 202 } 203 204 private ApplicationInfo getCameraInfo(int userHandle) { 205 // find the camera via an intent 206 // use INTENT_ACTION_STILL_IMAGE_CAMERA instead of _SECURE. On a 207 // device without a fbe enabled, the _SECURE intent will never get set. 208 Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); 209 PackageManager pm = mContext.getPackageManager(); 210 ResolveInfo cameraResolveInfo = pm.resolveActivityAsUser(cameraIntent, 211 PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_DIRECT_BOOT_AWARE 212 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 213 userHandle); 214 if (cameraResolveInfo == null ) { 215 //this is not necessarily an error 216 if (DEBUG) { 217 Slog.v(TAG, "Unable to resolve camera intent"); 218 } 219 return null; 220 } 221 222 if (isResolverActivity(cameraResolveInfo.activityInfo)) 223 { 224 if (DEBUG) { 225 Slog.v(TAG, "cameraIntent returned resolverActivity"); 226 } 227 return null; 228 } 229 230 return cameraResolveInfo.activityInfo.applicationInfo; 231 } 232 233 /** 234 * If the camera app is already pinned, unpin and repin it. 235 */ 236 private boolean pinCamera(int userHandle){ 237 ApplicationInfo cameraInfo = getCameraInfo(userHandle); 238 if (cameraInfo == null) { 239 return false; 240 } 241 242 //unpin after checking that the camera intent has resolved 243 //this prevents us from thrashing when switching users with 244 //FBE enabled, because the intent won't resolve until the unlock 245 unpinCameraApp(); 246 247 //pin APK 248 String camAPK = cameraInfo.sourceDir; 249 PinnedFile pf = pinFile(camAPK, 250 MAX_CAMERA_PIN_SIZE, 251 /*attemptPinIntrospection=*/true); 252 if (pf == null) { 253 Slog.e(TAG, "Failed to pin " + camAPK); 254 return false; 255 } 256 if (DEBUG) { 257 Slog.i(TAG, "Pinned " + pf.fileName); 258 } 259 synchronized (this) { 260 mPinnedCameraFiles.add(pf); 261 } 262 263 // determine the ABI from either ApplicationInfo or Build 264 String arch = "arm"; 265 if (cameraInfo.primaryCpuAbi != null) { 266 if (VMRuntime.is64BitAbi(cameraInfo.primaryCpuAbi)) { 267 arch = arch + "64"; 268 } 269 } else { 270 if (VMRuntime.is64BitAbi(Build.SUPPORTED_ABIS[0])) { 271 arch = arch + "64"; 272 } 273 } 274 275 // get the path to the odex or oat file 276 String baseCodePath = cameraInfo.getBaseCodePath(); 277 String[] files = null; 278 try { 279 files = DexFile.getDexFileOutputPaths(baseCodePath, arch); 280 } catch (IOException ioe) {} 281 if (files == null) { 282 return true; 283 } 284 285 //not pinning the oat/odex is not a fatal error 286 for (String file : files) { 287 pf = pinFile(file, MAX_CAMERA_PIN_SIZE, /*attemptPinIntrospection=*/false); 288 if (pf != null) { 289 synchronized (this) { 290 mPinnedCameraFiles.add(pf); 291 } 292 if (DEBUG) { 293 Slog.i(TAG, "Pinned " + pf.fileName); 294 } 295 } 296 } 297 298 return true; 299 } 300 301 302 /** mlock length bytes of fileToPin in memory 303 * 304 * If attemptPinIntrospection is true, then treat the file to pin as a zip file and 305 * look for a "pinlist.meta" file in the archive root directory. The structure of this 306 * file is a PINLIST_META as described below: 307 * 308 * <pre> 309 * PINLIST_META: PIN_RANGE* 310 * PIN_RANGE: PIN_START PIN_LENGTH 311 * PIN_START: big endian i32: offset in bytes of pin region from file start 312 * PIN_LENGTH: big endian i32: length of pin region in bytes 313 * </pre> 314 * 315 * (We use big endian because that's what DataInputStream is hardcoded to use.) 316 * 317 * If attemptPinIntrospection is false, then we use a single implicit PIN_RANGE of (0, 318 * maxBytesToPin); that is, we attempt to pin the first maxBytesToPin bytes of the file. 319 * 320 * After we open a file, we march through the list of pin ranges and attempt to pin 321 * each one, stopping after we've pinned maxBytesToPin bytes. (We may truncate the last 322 * pinned range to fit.) In this way, by choosing to emit certain PIN_RANGE pairs 323 * before others, file generators can express pins in priority order, making most 324 * effective use of the pinned-page quota. 325 * 326 * N.B. Each PIN_RANGE is clamped to the actual bounds of the file; all inputs have a 327 * meaningful interpretation. Also, a range locking a single byte of a page locks the 328 * whole page. Any truncated PIN_RANGE at EOF is ignored. Overlapping pinned entries 329 * are legal, but each pin of a byte counts toward the pin quota regardless of whether 330 * that byte has already been pinned, so the generator of PINLIST_META ought to ensure 331 * that ranges are non-overlapping. 332 * 333 * @param fileToPin Path to file to pin 334 * @param maxBytesToPin Maximum number of bytes to pin 335 * @param attemptPinIntrospection If true, try to open file as a 336 * zip in order to extract the 337 * @return Pinned memory resource owner thing or null on error 338 */ 339 private static PinnedFile pinFile(String fileToPin, 340 int maxBytesToPin, 341 boolean attemptPinIntrospection) { 342 ZipFile fileAsZip = null; 343 InputStream pinRangeStream = null; 344 try { 345 if (attemptPinIntrospection) { 346 fileAsZip = maybeOpenZip(fileToPin); 347 } 348 349 if (fileAsZip != null) { 350 pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin); 351 } 352 353 Slog.d(TAG, "pinRangeStream: " + pinRangeStream); 354 355 PinRangeSource pinRangeSource = (pinRangeStream != null) 356 ? new PinRangeSourceStream(pinRangeStream) 357 : new PinRangeSourceStatic(0, Integer.MAX_VALUE /* will be clipped */); 358 return pinFileRanges(fileToPin, maxBytesToPin, pinRangeSource); 359 } finally { 360 safeClose(pinRangeStream); 361 safeClose(fileAsZip); // Also closes any streams we've opened 362 } 363 } 364 365 /** 366 * Attempt to open a file as a zip file. On any sort of corruption, log, swallow the 367 * error, and return null. 368 */ 369 private static ZipFile maybeOpenZip(String fileName) { 370 ZipFile zip = null; 371 try { 372 zip = new ZipFile(fileName); 373 } catch (IOException ex) { 374 Slog.w(TAG, 375 String.format( 376 "could not open \"%s\" as zip: pinning as blob", 377 fileName), 378 ex); 379 } 380 return zip; 381 } 382 383 /** 384 * Open a pin metadata file in the zip if one is present. 385 * 386 * @param zipFile Zip file to search 387 * @return Open input stream or null on any error 388 */ 389 private static InputStream maybeOpenPinMetaInZip(ZipFile zipFile, String fileName) { 390 ZipEntry pinMetaEntry = zipFile.getEntry(PIN_META_FILENAME); 391 InputStream pinMetaStream = null; 392 if (pinMetaEntry != null) { 393 try { 394 pinMetaStream = zipFile.getInputStream(pinMetaEntry); 395 } catch (IOException ex) { 396 Slog.w(TAG, 397 String.format("error reading pin metadata \"%s\": pinning as blob", 398 fileName), 399 ex); 400 } 401 } 402 return pinMetaStream; 403 } 404 405 private static abstract class PinRangeSource { 406 /** Retrive a range to pin. 407 * 408 * @param outPinRange Receives the pin region 409 * @return True if we filled in outPinRange or false if we're out of pin entries 410 */ 411 abstract boolean read(PinRange outPinRange); 412 } 413 414 private static final class PinRangeSourceStatic extends PinRangeSource { 415 private final int mPinStart; 416 private final int mPinLength; 417 private boolean mDone = false; 418 419 PinRangeSourceStatic(int pinStart, int pinLength) { 420 mPinStart = pinStart; 421 mPinLength = pinLength; 422 } 423 424 @Override 425 boolean read(PinRange outPinRange) { 426 outPinRange.start = mPinStart; 427 outPinRange.length = mPinLength; 428 boolean done = mDone; 429 mDone = true; 430 return !done; 431 } 432 } 433 434 private static final class PinRangeSourceStream extends PinRangeSource { 435 private final DataInputStream mStream; 436 private boolean mDone = false; 437 438 PinRangeSourceStream(InputStream stream) { 439 mStream = new DataInputStream(stream); 440 } 441 442 @Override 443 boolean read(PinRange outPinRange) { 444 if (!mDone) { 445 try { 446 outPinRange.start = mStream.readInt(); 447 outPinRange.length = mStream.readInt(); 448 } catch (IOException ex) { 449 mDone = true; 450 } 451 } 452 return !mDone; 453 } 454 } 455 456 /** 457 * Helper for pinFile. 458 * 459 * @param fileToPin Name of file to pin 460 * @param maxBytesToPin Maximum number of bytes to pin 461 * @param pinRangeSource Read PIN_RANGE entries from this stream to tell us what bytes 462 * to pin. 463 * @return PinnedFile or null on error 464 */ 465 private static PinnedFile pinFileRanges( 466 String fileToPin, 467 int maxBytesToPin, 468 PinRangeSource pinRangeSource) 469 { 470 FileDescriptor fd = new FileDescriptor(); 471 long address = -1; 472 int mapSize = 0; 473 474 try { 475 int openFlags = (OsConstants.O_RDONLY | 476 OsConstants.O_CLOEXEC | 477 OsConstants.O_NOFOLLOW); 478 fd = Os.open(fileToPin, openFlags, 0); 479 mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE); 480 address = Os.mmap(0, mapSize, 481 OsConstants.PROT_READ, 482 OsConstants.MAP_SHARED, 483 fd, /*offset=*/0); 484 485 PinRange pinRange = new PinRange(); 486 int bytesPinned = 0; 487 488 // We pin at page granularity, so make sure the limit is page-aligned 489 if (maxBytesToPin % PAGE_SIZE != 0) { 490 maxBytesToPin -= maxBytesToPin % PAGE_SIZE; 491 } 492 493 while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) { 494 int pinStart = pinRange.start; 495 int pinLength = pinRange.length; 496 pinStart = clamp(0, pinStart, mapSize); 497 pinLength = clamp(0, pinLength, mapSize - pinStart); 498 pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength); 499 500 // mlock doesn't require the region to be page-aligned, but we snap the 501 // lock region to page boundaries anyway so that we don't under-count 502 // locking a single byte of a page as a charge of one byte even though the 503 // OS will retain the whole page. Thanks to this adjustment, we slightly 504 // over-count the pin charge of back-to-back pins touching the same page, 505 // but better that than undercounting. Besides: nothing stops pin metafile 506 // creators from making the actual regions page-aligned. 507 pinLength += pinStart % PAGE_SIZE; 508 pinStart -= pinStart % PAGE_SIZE; 509 if (pinLength % PAGE_SIZE != 0) { 510 pinLength += PAGE_SIZE - pinLength % PAGE_SIZE; 511 } 512 pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned); 513 514 if (pinLength > 0) { 515 if (DEBUG) { 516 Slog.d(TAG, 517 String.format( 518 "pinning at %s %s bytes of %s", 519 pinStart, pinLength, fileToPin)); 520 } 521 Os.mlock(address + pinStart, pinLength); 522 } 523 bytesPinned += pinLength; 524 } 525 526 PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned); 527 address = -1; // Ownership transferred 528 return pinnedFile; 529 } catch (ErrnoException ex) { 530 Slog.e(TAG, "Could not pin file " + fileToPin, ex); 531 return null; 532 } finally { 533 safeClose(fd); 534 if (address >= 0) { 535 safeMunmap(address, mapSize); 536 } 537 } 538 } 539 540 private static int clamp(int min, int value, int max) { 541 return Math.max(min, Math.min(value, max)); 542 } 543 544 private static void safeMunmap(long address, long mapSize) { 545 try { 546 Os.munmap(address, mapSize); 547 } catch (ErrnoException ex) { 548 Slog.w(TAG, "ignoring error in unmap", ex); 549 } 550 } 551 552 /** 553 * Close FD, swallowing irrelevant errors. 554 */ 555 private static void safeClose(@Nullable FileDescriptor fd) { 556 if (fd != null && fd.valid()) { 557 try { 558 Os.close(fd); 559 } catch (ErrnoException ex) { 560 // Swallow the exception: non-EBADF errors in close(2) 561 // indicate deferred paging write errors, which we 562 // don't care about here. The underlying file 563 // descriptor is always closed. 564 if (ex.errno == OsConstants.EBADF) { 565 throw new AssertionError(ex); 566 } 567 } 568 } 569 } 570 571 /** 572 * Close closeable thing, swallowing errors. 573 */ 574 private static void safeClose(@Nullable Closeable thing) { 575 if (thing != null) { 576 try { 577 thing.close(); 578 } catch (IOException ex) { 579 Slog.w(TAG, "ignoring error closing resource: " + thing, ex); 580 } 581 } 582 } 583 584 private synchronized ArrayList<PinnedFile> snapshotPinnedFiles() { 585 int nrPinnedFiles = mPinnedFiles.size() + mPinnedCameraFiles.size(); 586 ArrayList<PinnedFile> pinnedFiles = new ArrayList<>(nrPinnedFiles); 587 pinnedFiles.addAll(mPinnedFiles); 588 pinnedFiles.addAll(mPinnedCameraFiles); 589 return pinnedFiles; 590 } 591 592 private final class BinderService extends Binder { 593 @Override 594 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 595 if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; 596 long totalSize = 0; 597 for (PinnedFile pinnedFile : snapshotPinnedFiles()) { 598 pw.format("%s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned); 599 totalSize += pinnedFile.bytesPinned; 600 } 601 pw.format("Total size: %s\n", totalSize); 602 } 603 } 604 605 private static final class PinnedFile implements AutoCloseable { 606 private long mAddress; 607 final int mapSize; 608 final String fileName; 609 final int bytesPinned; 610 611 PinnedFile(long address, int mapSize, String fileName, int bytesPinned) { 612 mAddress = address; 613 this.mapSize = mapSize; 614 this.fileName = fileName; 615 this.bytesPinned = bytesPinned; 616 } 617 618 @Override 619 public void close() { 620 if (mAddress >= 0) { 621 safeMunmap(mAddress, mapSize); 622 mAddress = -1; 623 } 624 } 625 626 @Override 627 public void finalize() { 628 close(); 629 } 630 } 631 632 final static class PinRange { 633 int start; 634 int length; 635 } 636 637 final class PinnerHandler extends Handler { 638 static final int PIN_CAMERA_MSG = 4000; 639 static final int PIN_ONSTART_MSG = 4001; 640 641 public PinnerHandler(Looper looper) { 642 super(looper, null, true); 643 } 644 645 @Override 646 public void handleMessage(Message msg) { 647 switch (msg.what) { 648 649 case PIN_CAMERA_MSG: 650 { 651 handlePinCamera(msg.arg1); 652 } 653 break; 654 655 case PIN_ONSTART_MSG: 656 { 657 handlePinOnStart(); 658 } 659 break; 660 661 default: 662 super.handleMessage(msg); 663 } 664 } 665 } 666 667 } 668