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