1 /* 2 * Copyright (C) 2021 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.os; 18 19 import static android.app.ApplicationExitInfo.REASON_CRASH_NATIVE; 20 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; 21 import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; 22 import static android.os.Process.THREAD_PRIORITY_BACKGROUND; 23 24 import android.annotation.AppIdInt; 25 import android.annotation.CurrentTimeMillisLong; 26 import android.annotation.Nullable; 27 import android.annotation.UserIdInt; 28 import android.app.ActivityManager.RunningAppProcessInfo; 29 import android.app.ApplicationExitInfo; 30 import android.app.IParcelFileDescriptorRetriever; 31 import android.content.BroadcastReceiver; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.IntentFilter; 35 import android.os.FileObserver; 36 import android.os.Handler; 37 import android.os.ParcelFileDescriptor; 38 import android.os.UserHandle; 39 import android.system.ErrnoException; 40 import android.system.Os; 41 import android.system.StructStat; 42 import android.util.Slog; 43 import android.util.SparseArray; 44 import android.util.proto.ProtoInputStream; 45 import android.util.proto.ProtoParseException; 46 47 import com.android.internal.annotations.GuardedBy; 48 import com.android.server.BootReceiver; 49 import com.android.server.ServiceThread; 50 import com.android.server.os.TombstoneProtos.Cause; 51 import com.android.server.os.TombstoneProtos.Tombstone; 52 53 import libcore.io.IoUtils; 54 55 import java.io.File; 56 import java.io.FileInputStream; 57 import java.io.FileNotFoundException; 58 import java.io.IOException; 59 import java.util.ArrayList; 60 import java.util.Collections; 61 import java.util.Optional; 62 import java.util.concurrent.CompletableFuture; 63 import java.util.concurrent.ExecutionException; 64 65 /** 66 * A class to manage native tombstones. 67 */ 68 public final class NativeTombstoneManager { 69 private static final String TAG = NativeTombstoneManager.class.getSimpleName(); 70 71 private static final File TOMBSTONE_DIR = new File("/data/tombstones"); 72 73 private final Context mContext; 74 private final Handler mHandler; 75 private final TombstoneWatcher mWatcher; 76 77 private final Object mLock = new Object(); 78 79 @GuardedBy("mLock") 80 private final SparseArray<TombstoneFile> mTombstones; 81 NativeTombstoneManager(Context context)82 NativeTombstoneManager(Context context) { 83 mTombstones = new SparseArray<TombstoneFile>(); 84 mContext = context; 85 86 final ServiceThread thread = new ServiceThread(TAG + ":tombstoneWatcher", 87 THREAD_PRIORITY_BACKGROUND, true /* allowIo */); 88 thread.start(); 89 mHandler = thread.getThreadHandler(); 90 91 mWatcher = new TombstoneWatcher(); 92 mWatcher.startWatching(); 93 } 94 onSystemReady()95 void onSystemReady() { 96 registerForUserRemoval(); 97 registerForPackageRemoval(); 98 99 // Scan existing tombstones. 100 mHandler.post(() -> { 101 final File[] tombstoneFiles = TOMBSTONE_DIR.listFiles(); 102 for (int i = 0; tombstoneFiles != null && i < tombstoneFiles.length; i++) { 103 if (tombstoneFiles[i].isFile()) { 104 handleTombstone(tombstoneFiles[i]); 105 } 106 } 107 }); 108 } 109 handleTombstone(File path)110 private void handleTombstone(File path) { 111 final String filename = path.getName(); 112 if (!filename.startsWith("tombstone_")) { 113 return; 114 } 115 116 if (filename.endsWith(".pb")) { 117 handleProtoTombstone(path); 118 BootReceiver.addTombstoneToDropBox(mContext, path, true); 119 } else { 120 BootReceiver.addTombstoneToDropBox(mContext, path, false); 121 } 122 } 123 handleProtoTombstone(File path)124 private void handleProtoTombstone(File path) { 125 final String filename = path.getName(); 126 if (!filename.endsWith(".pb")) { 127 Slog.w(TAG, "unexpected tombstone name: " + path); 128 return; 129 } 130 131 final String suffix = filename.substring("tombstone_".length()); 132 final String numberStr = suffix.substring(0, suffix.length() - 3); 133 134 int number; 135 try { 136 number = Integer.parseInt(numberStr); 137 if (number < 0 || number > 99) { 138 Slog.w(TAG, "unexpected tombstone name: " + path); 139 return; 140 } 141 } catch (NumberFormatException ex) { 142 Slog.w(TAG, "unexpected tombstone name: " + path); 143 return; 144 } 145 146 ParcelFileDescriptor pfd; 147 try { 148 pfd = ParcelFileDescriptor.open(path, MODE_READ_WRITE); 149 } catch (FileNotFoundException ex) { 150 Slog.w(TAG, "failed to open " + path, ex); 151 return; 152 } 153 154 final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd); 155 if (!parsedTombstone.isPresent()) { 156 IoUtils.closeQuietly(pfd); 157 return; 158 } 159 160 synchronized (mLock) { 161 TombstoneFile previous = mTombstones.get(number); 162 if (previous != null) { 163 previous.dispose(); 164 } 165 166 mTombstones.put(number, parsedTombstone.get()); 167 } 168 } 169 170 /** 171 * Remove native tombstones matching a user and/or app. 172 * 173 * @param userId user id to filter by, selects all users if empty 174 * @param appId app id to filter by, selects all users if empty 175 */ purge(Optional<Integer> userId, Optional<Integer> appId)176 public void purge(Optional<Integer> userId, Optional<Integer> appId) { 177 mHandler.post(() -> { 178 synchronized (mLock) { 179 for (int i = mTombstones.size() - 1; i >= 0; --i) { 180 TombstoneFile tombstone = mTombstones.valueAt(i); 181 if (tombstone.matches(userId, appId)) { 182 tombstone.purge(); 183 mTombstones.removeAt(i); 184 } 185 } 186 } 187 }); 188 } 189 purgePackage(int uid, boolean allUsers)190 private void purgePackage(int uid, boolean allUsers) { 191 final int appId = UserHandle.getAppId(uid); 192 Optional<Integer> userId; 193 if (allUsers) { 194 userId = Optional.empty(); 195 } else { 196 userId = Optional.of(UserHandle.getUserId(uid)); 197 } 198 purge(userId, Optional.of(appId)); 199 } 200 purgeUser(int uid)201 private void purgeUser(int uid) { 202 purge(Optional.of(uid), Optional.empty()); 203 } 204 registerForPackageRemoval()205 private void registerForPackageRemoval() { 206 final IntentFilter filter = new IntentFilter(); 207 filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); 208 filter.addDataScheme("package"); 209 mContext.registerReceiverForAllUsers(new BroadcastReceiver() { 210 @Override 211 public void onReceive(Context context, Intent intent) { 212 final int uid = intent.getIntExtra(Intent.EXTRA_UID, UserHandle.USER_NULL); 213 if (uid == UserHandle.USER_NULL) return; 214 215 final boolean allUsers = intent.getBooleanExtra( 216 Intent.EXTRA_REMOVED_FOR_ALL_USERS, false); 217 218 purgePackage(uid, allUsers); 219 } 220 }, filter, null, mHandler); 221 } 222 registerForUserRemoval()223 private void registerForUserRemoval() { 224 final IntentFilter filter = new IntentFilter(); 225 filter.addAction(Intent.ACTION_USER_REMOVED); 226 mContext.registerReceiverForAllUsers(new BroadcastReceiver() { 227 @Override 228 public void onReceive(Context context, Intent intent) { 229 final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); 230 if (userId < 1) return; 231 232 purgeUser(userId); 233 } 234 }, filter, null, mHandler); 235 } 236 237 /** 238 * Collect native tombstones. 239 * 240 * @param output list to append to 241 * @param callingUid POSIX uid to filter by 242 * @param pid pid to filter by, ignored if zero 243 * @param maxNum maximum number of elements in output 244 */ collectTombstones(ArrayList<ApplicationExitInfo> output, int callingUid, int pid, int maxNum)245 public void collectTombstones(ArrayList<ApplicationExitInfo> output, int callingUid, int pid, 246 int maxNum) { 247 CompletableFuture<Object> future = new CompletableFuture<>(); 248 249 if (!UserHandle.isApp(callingUid)) { 250 return; 251 } 252 253 final int userId = UserHandle.getUserId(callingUid); 254 final int appId = UserHandle.getAppId(callingUid); 255 256 mHandler.post(() -> { 257 boolean appendedTombstones = false; 258 259 synchronized (mLock) { 260 final int tombstonesSize = mTombstones.size(); 261 262 tombstoneIter: 263 for (int i = 0; i < tombstonesSize; ++i) { 264 TombstoneFile tombstone = mTombstones.valueAt(i); 265 if (tombstone.matches(Optional.of(userId), Optional.of(appId))) { 266 if (pid != 0 && tombstone.mPid != pid) { 267 continue; 268 } 269 270 // Try to attach to an existing REASON_CRASH_NATIVE. 271 final int outputSize = output.size(); 272 for (int j = 0; j < outputSize; ++j) { 273 ApplicationExitInfo exitInfo = output.get(j); 274 if (tombstone.matches(exitInfo)) { 275 exitInfo.setNativeTombstoneRetriever(tombstone.getPfdRetriever()); 276 continue tombstoneIter; 277 } 278 } 279 280 if (output.size() < maxNum) { 281 appendedTombstones = true; 282 output.add(tombstone.toAppExitInfo()); 283 } 284 } 285 } 286 } 287 288 if (appendedTombstones) { 289 Collections.sort(output, (lhs, rhs) -> { 290 // Reports should be ordered with newest reports first. 291 long diff = rhs.getTimestamp() - lhs.getTimestamp(); 292 if (diff < 0) { 293 return -1; 294 } else if (diff == 0) { 295 return 0; 296 } else { 297 return 1; 298 } 299 }); 300 } 301 future.complete(null); 302 }); 303 304 try { 305 future.get(); 306 } catch (ExecutionException | InterruptedException ex) { 307 throw new RuntimeException(ex); 308 } 309 } 310 311 static class TombstoneFile { 312 final ParcelFileDescriptor mPfd; 313 314 @UserIdInt int mUserId; 315 @AppIdInt int mAppId; 316 317 int mPid; 318 int mUid; 319 String mProcessName; 320 @CurrentTimeMillisLong long mTimestampMs; 321 String mCrashReason; 322 323 boolean mPurged = false; 324 final IParcelFileDescriptorRetriever mRetriever = new ParcelFileDescriptorRetriever(); 325 TombstoneFile(ParcelFileDescriptor pfd)326 TombstoneFile(ParcelFileDescriptor pfd) { 327 mPfd = pfd; 328 } 329 matches(Optional<Integer> userId, Optional<Integer> appId)330 public boolean matches(Optional<Integer> userId, Optional<Integer> appId) { 331 if (mPurged) { 332 return false; 333 } 334 335 if (userId.isPresent() && userId.get() != mUserId) { 336 return false; 337 } 338 339 if (appId.isPresent() && appId.get() != mAppId) { 340 return false; 341 } 342 343 return true; 344 } 345 matches(ApplicationExitInfo exitInfo)346 public boolean matches(ApplicationExitInfo exitInfo) { 347 if (exitInfo.getReason() != REASON_CRASH_NATIVE) { 348 return false; 349 } 350 351 if (exitInfo.getPid() != mPid) { 352 return false; 353 } 354 355 if (exitInfo.getRealUid() != mUid) { 356 return false; 357 } 358 359 if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 5000) { 360 return false; 361 } 362 363 return true; 364 } 365 dispose()366 public void dispose() { 367 IoUtils.closeQuietly(mPfd); 368 } 369 purge()370 public void purge() { 371 if (!mPurged) { 372 // There's no way to atomically unlink a specific file for which we have an fd from 373 // a path, which means that we can't safely delete a tombstone without coordination 374 // with tombstoned (which has a risk of deadlock if for example, system_server hangs 375 // with a flock). Do the next best thing, and just truncate the file. 376 // 377 // We don't have to worry about inflicting a SIGBUS on a process that has the 378 // tombstone mmaped, because we only clear if the package has been removed, which 379 // means no one with access to the tombstone should be left. 380 try { 381 Os.ftruncate(mPfd.getFileDescriptor(), 0); 382 } catch (ErrnoException ex) { 383 Slog.e(TAG, "Failed to truncate tombstone", ex); 384 } 385 mPurged = true; 386 } 387 } 388 parse(ParcelFileDescriptor pfd)389 static Optional<TombstoneFile> parse(ParcelFileDescriptor pfd) { 390 final FileInputStream is = new FileInputStream(pfd.getFileDescriptor()); 391 final ProtoInputStream stream = new ProtoInputStream(is); 392 393 int pid = 0; 394 int uid = 0; 395 String processName = null; 396 String crashReason = ""; 397 String selinuxLabel = ""; 398 399 try { 400 while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 401 switch (stream.getFieldNumber()) { 402 case (int) Tombstone.PID: 403 pid = stream.readInt(Tombstone.PID); 404 break; 405 406 case (int) Tombstone.UID: 407 uid = stream.readInt(Tombstone.UID); 408 break; 409 410 case (int) Tombstone.COMMAND_LINE: 411 if (processName == null) { 412 processName = stream.readString(Tombstone.COMMAND_LINE); 413 } 414 break; 415 416 case (int) Tombstone.CAUSES: 417 if (!crashReason.equals("")) { 418 // Causes appear in decreasing order of likelihood. For now we only 419 // want the most likely crash reason here, so ignore all others. 420 break; 421 } 422 long token = stream.start(Tombstone.CAUSES); 423 cause: 424 while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 425 switch (stream.getFieldNumber()) { 426 case (int) Cause.HUMAN_READABLE: 427 crashReason = stream.readString(Cause.HUMAN_READABLE); 428 break cause; 429 430 default: 431 break; 432 } 433 } 434 stream.end(token); 435 break; 436 437 case (int) Tombstone.SELINUX_LABEL: 438 selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL); 439 break; 440 441 default: 442 break; 443 } 444 } 445 } catch (IOException | ProtoParseException ex) { 446 Slog.e(TAG, "Failed to parse tombstone", ex); 447 return Optional.empty(); 448 } 449 450 if (!UserHandle.isApp(uid)) { 451 Slog.e(TAG, "Tombstone's UID (" + uid + ") not an app, ignoring"); 452 return Optional.empty(); 453 } 454 455 long timestampMs = 0; 456 try { 457 StructStat stat = Os.fstat(pfd.getFileDescriptor()); 458 timestampMs = stat.st_atim.tv_sec * 1000 + stat.st_atim.tv_nsec / 1000000; 459 } catch (ErrnoException ex) { 460 Slog.e(TAG, "Failed to get timestamp of tombstone", ex); 461 } 462 463 final int userId = UserHandle.getUserId(uid); 464 final int appId = UserHandle.getAppId(uid); 465 466 if (!selinuxLabel.startsWith("u:r:untrusted_app")) { 467 Slog.e(TAG, "Tombstone has invalid selinux label (" + selinuxLabel + "), ignoring"); 468 return Optional.empty(); 469 } 470 471 TombstoneFile result = new TombstoneFile(pfd); 472 473 result.mUserId = userId; 474 result.mAppId = appId; 475 result.mPid = pid; 476 result.mUid = uid; 477 result.mProcessName = processName == null ? "" : processName; 478 result.mTimestampMs = timestampMs; 479 result.mCrashReason = crashReason; 480 481 return Optional.of(result); 482 } 483 getPfdRetriever()484 public IParcelFileDescriptorRetriever getPfdRetriever() { 485 return mRetriever; 486 } 487 toAppExitInfo()488 public ApplicationExitInfo toAppExitInfo() { 489 ApplicationExitInfo info = new ApplicationExitInfo(); 490 info.setPid(mPid); 491 info.setRealUid(mUid); 492 info.setPackageUid(mUid); 493 info.setDefiningUid(mUid); 494 info.setProcessName(mProcessName); 495 info.setReason(ApplicationExitInfo.REASON_CRASH_NATIVE); 496 497 // Signal numbers are architecture-specific! 498 // We choose to provide nothing here, to avoid leading users astray. 499 info.setStatus(0); 500 501 // No way for us to find out. 502 info.setImportance(RunningAppProcessInfo.IMPORTANCE_GONE); 503 info.setPackageName(""); 504 info.setProcessStateSummary(null); 505 506 // We could find out, but they didn't get OOM-killed... 507 info.setPss(0); 508 info.setRss(0); 509 510 info.setTimestamp(mTimestampMs); 511 info.setDescription(mCrashReason); 512 513 info.setSubReason(ApplicationExitInfo.SUBREASON_UNKNOWN); 514 info.setNativeTombstoneRetriever(mRetriever); 515 516 return info; 517 } 518 519 520 class ParcelFileDescriptorRetriever extends IParcelFileDescriptorRetriever.Stub { ParcelFileDescriptorRetriever()521 ParcelFileDescriptorRetriever() {} 522 getPfd()523 public @Nullable ParcelFileDescriptor getPfd() { 524 if (mPurged) { 525 return null; 526 } 527 528 // Reopen the file descriptor as read-only. 529 try { 530 final String path = "/proc/self/fd/" + mPfd.getFd(); 531 ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path), 532 MODE_READ_ONLY); 533 return pfd; 534 } catch (FileNotFoundException ex) { 535 Slog.e(TAG, "failed to reopen file descriptor as read-only", ex); 536 return null; 537 } 538 } 539 } 540 } 541 542 class TombstoneWatcher extends FileObserver { TombstoneWatcher()543 TombstoneWatcher() { 544 // Tombstones can be created either by linking an O_TMPFILE temporary file (CREATE), 545 // or by moving a named temporary file in the same directory on kernels where O_TMPFILE 546 // isn't supported (MOVED_TO). 547 super(TOMBSTONE_DIR, FileObserver.CREATE | FileObserver.MOVED_TO); 548 } 549 550 @Override onEvent(int event, @Nullable String path)551 public void onEvent(int event, @Nullable String path) { 552 mHandler.post(() -> { 553 handleTombstone(new File(TOMBSTONE_DIR, path)); 554 }); 555 } 556 } 557 } 558