diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index 69d5c8d0d2ffbdc939e9effc879b442fdd43e300..854f5a4ab7240fa215f0ad81a67b9aa0138afd8d 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -428,6 +428,13 @@ public final class ApplicationExitInfo implements Parcelable { */ private IAppTraceRetriever mAppTraceRetriever; + /** + * ParcelFileDescriptor pointing to a native tombstone. + * + * @see #getTraceInputStream + */ + private IParcelFileDescriptorRetriever mNativeTombstoneRetriever; + /** @hide */ @IntDef(prefix = { "REASON_" }, value = { REASON_UNKNOWN, @@ -603,22 +610,38 @@ public final class ApplicationExitInfo implements Parcelable { * prior to the death of the process; typically it'll be available when * the reason is {@link #REASON_ANR}, though if the process gets an ANR * but recovers, and dies for another reason later, this trace will be included - * in the record of {@link ApplicationExitInfo} still. + * in the record of {@link ApplicationExitInfo} still. Beginning with API 31, + * tombstone traces will be returned for + * {@link #REASON_CRASH_NATIVE}, with an InputStream containing a protobuf with + * <a href="https://android.googlesource.com/platform/system/core/+/refs/heads/master/debuggerd/proto/tombstone.proto">this schema</a>. + * Note thatbecause these traces are kept in a separate global circular buffer, crashes may be + * overwritten by newer crashes (including from other applications), so this may still return + * null. * * @return The input stream to the traces that was taken by the system * prior to the death of the process. */ public @Nullable InputStream getTraceInputStream() throws IOException { - if (mAppTraceRetriever == null) { + if (mAppTraceRetriever == null && mNativeTombstoneRetriever == null) { return null; } + try { - final ParcelFileDescriptor fd = mAppTraceRetriever.getTraceFileDescriptor( - mPackageName, mPackageUid, mPid); - if (fd == null) { - return null; + if (mNativeTombstoneRetriever != null) { + final ParcelFileDescriptor pfd = mNativeTombstoneRetriever.getPfd(); + if (pfd == null) { + return null; + } + + return new ParcelFileDescriptor.AutoCloseInputStream(pfd); + } else { + final ParcelFileDescriptor fd = mAppTraceRetriever.getTraceFileDescriptor( + mPackageName, mPackageUid, mPid); + if (fd == null) { + return null; + } + return new GZIPInputStream(new ParcelFileDescriptor.AutoCloseInputStream(fd)); } - return new GZIPInputStream(new ParcelFileDescriptor.AutoCloseInputStream(fd)); } catch (RemoteException e) { return null; } @@ -849,6 +872,15 @@ public final class ApplicationExitInfo implements Parcelable { mAppTraceRetriever = retriever; } + /** + * @see mNativeTombstoneRetriever + * + * @hide + */ + public void setNativeTombstoneRetriever(final IParcelFileDescriptorRetriever retriever) { + mNativeTombstoneRetriever = retriever; + } + @Override public int describeContents() { return 0; @@ -878,6 +910,12 @@ public final class ApplicationExitInfo implements Parcelable { } else { dest.writeInt(0); } + if (mNativeTombstoneRetriever != null) { + dest.writeInt(1); + dest.writeStrongBinder(mNativeTombstoneRetriever.asBinder()); + } else { + dest.writeInt(0); + } } /** @hide */ @@ -906,6 +944,7 @@ public final class ApplicationExitInfo implements Parcelable { mState = other.mState; mTraceFile = other.mTraceFile; mAppTraceRetriever = other.mAppTraceRetriever; + mNativeTombstoneRetriever = other.mNativeTombstoneRetriever; } private ApplicationExitInfo(@NonNull Parcel in) { @@ -928,6 +967,10 @@ public final class ApplicationExitInfo implements Parcelable { if (in.readInt() == 1) { mAppTraceRetriever = IAppTraceRetriever.Stub.asInterface(in.readStrongBinder()); } + if (in.readInt() == 1) { + mNativeTombstoneRetriever = IParcelFileDescriptorRetriever.Stub.asInterface( + in.readStrongBinder()); + } } public @NonNull static final Creator<ApplicationExitInfo> CREATOR = @@ -986,6 +1029,7 @@ public final class ApplicationExitInfo implements Parcelable { sb.append(" state=").append(ArrayUtils.isEmpty(mState) ? "empty" : Integer.toString(mState.length) + " bytes"); sb.append(" trace=").append(mTraceFile); + return sb.toString(); } diff --git a/core/java/android/app/IParcelFileDescriptorRetriever.aidl b/core/java/android/app/IParcelFileDescriptorRetriever.aidl new file mode 100644 index 0000000000000000000000000000000000000000..7e808e74bd5d0060bc2b1dffbb45f36d1391da42 --- /dev/null +++ b/core/java/android/app/IParcelFileDescriptorRetriever.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.os.ParcelFileDescriptor; + +/** + * An interface used to lazily provide a ParcelFileDescriptor to apps. + * + * @hide + */ +interface IParcelFileDescriptorRetriever { + /** + * Retrieve the ParcelFileDescriptor. + */ + ParcelFileDescriptor getPfd(); +} diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 661d49690a00470deb33e7b6322f4d2dbc8a2e78..900871dfbbb4c922f0d5d5aa4c85ec94e1816989 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -364,6 +364,7 @@ import com.android.server.compat.PlatformCompat; import com.android.server.contentcapture.ContentCaptureManagerInternal; import com.android.server.firewall.IntentFirewall; import com.android.server.job.JobSchedulerInternal; +import com.android.server.os.NativeTombstoneManager; import com.android.server.pm.Installer; import com.android.server.pm.permission.PermissionManagerServiceInternal; import com.android.server.uri.GrantUri; @@ -10410,6 +10411,9 @@ public class ActivityManagerService extends IActivityManager.Stub mUserController.handleIncomingUser(callingPid, callingUid, userId, true, ALLOW_NON_FULL, "getHistoricalProcessExitReasons", null); + NativeTombstoneManager tombstoneService = LocalServices.getService( + NativeTombstoneManager.class); + final ArrayList<ApplicationExitInfo> results = new ArrayList<ApplicationExitInfo>(); if (!TextUtils.isEmpty(packageName)) { final int uid = enforceDumpPermissionForPackage(packageName, userId, callingUid, @@ -10417,11 +10421,13 @@ public class ActivityManagerService extends IActivityManager.Stub if (uid != Process.INVALID_UID) { mProcessList.mAppExitInfoTracker.getExitInfo( packageName, uid, pid, maxNum, results); + tombstoneService.collectTombstones(results, uid, pid, maxNum); } } else { // If no package name is given, use the caller's uid as the filter uid. mProcessList.mAppExitInfoTracker.getExitInfo( packageName, callingUid, pid, maxNum, results); + tombstoneService.collectTombstones(results, callingUid, pid, maxNum); } return new ParceledListSlice<ApplicationExitInfo>(results); diff --git a/services/core/java/com/android/server/am/AppExitInfoTracker.java b/services/core/java/com/android/server/am/AppExitInfoTracker.java index 374c215fc6d09fa65437d52b519e211a2b9871a8..f2c1f554afdea6cc0b69ee594ef401a048583357 100644 --- a/services/core/java/com/android/server/am/AppExitInfoTracker.java +++ b/services/core/java/com/android/server/am/AppExitInfoTracker.java @@ -63,8 +63,10 @@ import com.android.internal.app.ProcessMap; import com.android.internal.util.ArrayUtils; import com.android.internal.util.function.pooled.PooledLambda; import com.android.server.IoThread; +import com.android.server.LocalServices; import com.android.server.ServiceThread; import com.android.server.SystemServiceManager; +import com.android.server.os.NativeTombstoneManager; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -78,6 +80,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -770,6 +773,10 @@ public final class AppExitInfoTracker { * Helper function for shell command */ void clearHistoryProcessExitInfo(String packageName, int userId) { + NativeTombstoneManager tombstoneService = LocalServices.getService( + NativeTombstoneManager.class); + Optional<Integer> appId = Optional.empty(); + if (TextUtils.isEmpty(packageName)) { synchronized (mLock) { removeByUserIdLocked(userId); @@ -777,10 +784,13 @@ public final class AppExitInfoTracker { } else { final int uid = mService.mPackageManagerInt.getPackageUid(packageName, PackageManager.MATCH_ALL, userId); + appId = Optional.of(UserHandle.getAppId(uid)); synchronized (mLock) { removePackageLocked(packageName, uid, true, userId); } } + + tombstoneService.purge(Optional.of(userId), appId); schedulePersistProcessExitInfo(true); } diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index 3c5d45636e3e7af890970874ee32423c0f7c8096..dc191a954a59bbc71223d74551ed2216a4d4d14c 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -16,12 +16,18 @@ package com.android.server.os; +import static android.app.ApplicationExitInfo.REASON_CRASH_NATIVE; +import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static android.os.ParcelFileDescriptor.MODE_READ_WRITE; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; import android.annotation.AppIdInt; +import android.annotation.CurrentTimeMillisLong; import android.annotation.Nullable; import android.annotation.UserIdInt; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.app.ApplicationExitInfo; +import android.app.IParcelFileDescriptorRetriever; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -32,6 +38,7 @@ import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.system.ErrnoException; import android.system.Os; +import android.system.StructStat; import android.util.Slog; import android.util.SparseArray; import android.util.proto.ProtoInputStream; @@ -39,6 +46,7 @@ import android.util.proto.ProtoInputStream; import com.android.internal.annotations.GuardedBy; import com.android.server.BootReceiver; import com.android.server.ServiceThread; +import com.android.server.os.TombstoneProtos.Cause; import com.android.server.os.TombstoneProtos.Tombstone; import libcore.io.IoUtils; @@ -47,7 +55,11 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; /** * A class to manage native tombstones. @@ -153,7 +165,13 @@ public final class NativeTombstoneManager { } } - private void purge(Optional<Integer> userId, Optional<Integer> appId) { + /** + * Remove native tombstones matching a user and/or app. + * + * @param userId user id to filter by, selects all users if empty + * @param appId app id to filter by, selects all users if empty + */ + public void purge(Optional<Integer> userId, Optional<Integer> appId) { mHandler.post(() -> { synchronized (mLock) { for (int i = mTombstones.size() - 1; i >= 0; --i) { @@ -214,18 +232,97 @@ public final class NativeTombstoneManager { }, filter, null, mHandler); } + /** + * Collect native tombstones. + * + * @param output list to append to + * @param callingUid POSIX uid to filter by + * @param pid pid to filter by, ignored if zero + * @param maxNum maximum number of elements in output + */ + public void collectTombstones(ArrayList<ApplicationExitInfo> output, int callingUid, int pid, + int maxNum) { + CompletableFuture<Object> future = new CompletableFuture<>(); + + if (!UserHandle.isApp(callingUid)) { + return; + } + + final int userId = UserHandle.getUserId(callingUid); + final int appId = UserHandle.getAppId(callingUid); + + mHandler.post(() -> { + boolean appendedTombstones = false; + + synchronized (mLock) { + final int tombstonesSize = mTombstones.size(); + + tombstoneIter: + for (int i = 0; i < tombstonesSize; ++i) { + TombstoneFile tombstone = mTombstones.valueAt(i); + if (tombstone.matches(Optional.of(userId), Optional.of(appId))) { + if (pid != 0 && tombstone.mPid != pid) { + continue; + } + + // Try to attach to an existing REASON_CRASH_NATIVE. + final int outputSize = output.size(); + for (int j = 0; j < outputSize; ++j) { + ApplicationExitInfo exitInfo = output.get(j); + if (tombstone.matches(exitInfo)) { + exitInfo.setNativeTombstoneRetriever(tombstone.getPfdRetriever()); + continue tombstoneIter; + } + } + + if (output.size() < maxNum) { + appendedTombstones = true; + output.add(tombstone.toAppExitInfo()); + } + } + } + } + + if (appendedTombstones) { + Collections.sort(output, (lhs, rhs) -> { + // Reports should be ordered with newest reports first. + long diff = rhs.getTimestamp() - lhs.getTimestamp(); + if (diff < 0) { + return -1; + } else if (diff == 0) { + return 0; + } else { + return 1; + } + }); + } + future.complete(null); + }); + + try { + future.get(); + } catch (ExecutionException | InterruptedException ex) { + throw new RuntimeException(ex); + } + } + static class TombstoneFile { final ParcelFileDescriptor mPfd; - final @UserIdInt int mUserId; - final @AppIdInt int mAppId; + @UserIdInt int mUserId; + @AppIdInt int mAppId; + + int mPid; + int mUid; + String mProcessName; + @CurrentTimeMillisLong long mTimestampMs; + String mCrashReason; boolean mPurged = false; + final IParcelFileDescriptorRetriever mRetriever = new ParcelFileDescriptorRetriever(); - TombstoneFile(ParcelFileDescriptor pfd, @UserIdInt int userId, @AppIdInt int appId) { + TombstoneFile(ParcelFileDescriptor pfd) { mPfd = pfd; - mUserId = userId; - mAppId = appId; } public boolean matches(Optional<Integer> userId, Optional<Integer> appId) { @@ -244,6 +341,26 @@ public final class NativeTombstoneManager { return true; } + public boolean matches(ApplicationExitInfo exitInfo) { + if (exitInfo.getReason() != REASON_CRASH_NATIVE) { + return false; + } + + if (exitInfo.getPid() != mPid) { + return false; + } + + if (exitInfo.getRealUid() != mUid) { + return false; + } + + if (Math.abs(exitInfo.getTimestamp() - mTimestampMs) > 1000) { + return false; + } + + return true; + } + public void dispose() { IoUtils.closeQuietly(mPfd); } @@ -271,16 +388,43 @@ public final class NativeTombstoneManager { final FileInputStream is = new FileInputStream(pfd.getFileDescriptor()); final ProtoInputStream stream = new ProtoInputStream(is); + int pid = 0; int uid = 0; + String processName = ""; + String crashReason = ""; String selinuxLabel = ""; try { while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { switch (stream.getFieldNumber()) { + case (int) Tombstone.PID: + pid = stream.readInt(Tombstone.PID); + break; + case (int) Tombstone.UID: uid = stream.readInt(Tombstone.UID); break; + case (int) Tombstone.PROCESS_NAME: + processName = stream.readString(Tombstone.PROCESS_NAME); + break; + + case (int) Tombstone.CAUSE: + long token = stream.start(Tombstone.CAUSE); + cause: + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) Cause.HUMAN_READABLE: + crashReason = stream.readString(Cause.HUMAN_READABLE); + break cause; + + default: + break; + } + } + stream.end(token); + + case (int) Tombstone.SELINUX_LABEL: selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL); break; @@ -299,6 +443,14 @@ public final class NativeTombstoneManager { return Optional.empty(); } + long timestampMs = 0; + try { + StructStat stat = Os.fstat(pfd.getFileDescriptor()); + timestampMs = stat.st_atim.tv_sec * 1000 + stat.st_atim.tv_nsec / 1000000; + } catch (ErrnoException ex) { + Slog.e(TAG, "Failed to get timestamp of tombstone", ex); + } + final int userId = UserHandle.getUserId(uid); final int appId = UserHandle.getAppId(uid); @@ -307,7 +459,74 @@ public final class NativeTombstoneManager { return Optional.empty(); } - return Optional.of(new TombstoneFile(pfd, userId, appId)); + TombstoneFile result = new TombstoneFile(pfd); + + result.mUserId = userId; + result.mAppId = appId; + result.mPid = pid; + result.mUid = uid; + result.mProcessName = processName; + result.mTimestampMs = timestampMs; + result.mCrashReason = crashReason; + + return Optional.of(result); + } + + public IParcelFileDescriptorRetriever getPfdRetriever() { + return mRetriever; + } + + public ApplicationExitInfo toAppExitInfo() { + ApplicationExitInfo info = new ApplicationExitInfo(); + info.setPid(mPid); + info.setRealUid(mUid); + info.setPackageUid(mUid); + info.setDefiningUid(mUid); + info.setProcessName(mProcessName); + info.setReason(ApplicationExitInfo.REASON_CRASH_NATIVE); + + // Signal numbers are architecture-specific! + // We choose to provide nothing here, to avoid leading users astray. + info.setStatus(0); + + // No way for us to find out. + info.setImportance(RunningAppProcessInfo.IMPORTANCE_GONE); + info.setPackageName(""); + info.setProcessStateSummary(null); + + // We could find out, but they didn't get OOM-killed... + info.setPss(0); + info.setRss(0); + + info.setTimestamp(mTimestampMs); + info.setDescription(mCrashReason); + + info.setSubReason(ApplicationExitInfo.SUBREASON_UNKNOWN); + info.setNativeTombstoneRetriever(mRetriever); + + return info; + } + + + class ParcelFileDescriptorRetriever extends IParcelFileDescriptorRetriever.Stub { + ParcelFileDescriptorRetriever() {} + + public @Nullable ParcelFileDescriptor getPfd() { + if (mPurged) { + return null; + } + + // Reopen the file descriptor as read-only. + try { + final String path = "/proc/self/fd/" + mPfd.getFd(); + ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(path), + MODE_READ_ONLY); + return pfd; + } catch (FileNotFoundException ex) { + Slog.e(TAG, "failed to reopen file descriptor as read-only", ex); + return null; + } + } } }