| /* |
| * Copyright (C) 2019 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 com.android.server.os; |
| |
| import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; |
| |
| import android.Manifest; |
| import android.annotation.Nullable; |
| import android.annotation.RequiresPermission; |
| import android.app.ActivityManager; |
| import android.app.AppOpsManager; |
| import android.app.admin.DevicePolicyManager; |
| import android.app.role.RoleManager; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.pm.UserInfo; |
| import android.os.Binder; |
| import android.os.BugreportManager.BugreportCallback; |
| import android.os.BugreportParams; |
| import android.os.Environment; |
| import android.os.IDumpstate; |
| import android.os.IDumpstateListener; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.os.UserHandle; |
| import android.telephony.TelephonyManager; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| import android.util.AtomicFile; |
| import android.util.LocalLog; |
| import android.util.Pair; |
| import android.util.Slog; |
| import android.util.Xml; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.util.DumpUtils; |
| import com.android.internal.util.XmlUtils; |
| import com.android.modules.utils.TypedXmlPullParser; |
| import com.android.modules.utils.TypedXmlSerializer; |
| import com.android.server.SystemConfig; |
| import com.android.server.utils.Slogf; |
| |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileDescriptor; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.PrintWriter; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.OptionalInt; |
| import java.util.Set; |
| |
| /** |
| * Implementation of the service that provides a privileged API to capture and consume bugreports. |
| * |
| * <p>Delegates the actualy generation to a native implementation of {@code IDumpstate}. |
| */ |
| class BugreportManagerServiceImpl extends IDumpstate.Stub { |
| |
| private static final int LOCAL_LOG_SIZE = 20; |
| private static final String TAG = "BugreportManagerService"; |
| private static final boolean DEBUG = false; |
| private static final String ROLE_SYSTEM_AUTOMOTIVE_PROJECTION = |
| "android.app.role.SYSTEM_AUTOMOTIVE_PROJECTION"; |
| private static final String TAG_BUGREPORT_DATA = "bugreport-data"; |
| private static final String TAG_BUGREPORT_MAP = "bugreport-map"; |
| private static final String TAG_PERSISTENT_BUGREPORT = "persistent-bugreport"; |
| private static final String ATTR_CALLING_UID = "calling-uid"; |
| private static final String ATTR_CALLING_PACKAGE = "calling-package"; |
| private static final String ATTR_BUGREPORT_FILE = "bugreport-file"; |
| |
| private static final String BUGREPORT_SERVICE = "bugreportd"; |
| private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000; |
| |
| private final Object mLock = new Object(); |
| private final Context mContext; |
| private final AppOpsManager mAppOps; |
| private final TelephonyManager mTelephonyManager; |
| private final ArraySet<String> mBugreportAllowlistedPackages; |
| private final BugreportFileManager mBugreportFileManager; |
| |
| |
| @GuardedBy("mLock") |
| private OptionalInt mPreDumpedDataUid = OptionalInt.empty(); |
| |
| // Attributes below are just Used for dump() purposes |
| @Nullable |
| @GuardedBy("mLock") |
| private DumpstateListener mCurrentDumpstateListener; |
| @GuardedBy("mLock") |
| private int mNumberFinishedBugreports; |
| @GuardedBy("mLock") |
| private final LocalLog mFinishedBugreports = new LocalLog(LOCAL_LOG_SIZE); |
| |
| /** Helper class for associating previously generated bugreports with their callers. */ |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) |
| static class BugreportFileManager { |
| |
| private final Object mLock = new Object(); |
| private boolean mReadBugreportMapping = false; |
| private final AtomicFile mMappingFile; |
| |
| @GuardedBy("mLock") |
| private ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles = |
| new ArrayMap<>(); |
| |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) |
| @GuardedBy("mLock") |
| final Set<String> mBugreportFilesToPersist = new HashSet<>(); |
| |
| BugreportFileManager(AtomicFile mappingFile) { |
| mMappingFile = mappingFile; |
| } |
| |
| /** |
| * Checks that a given file was generated on behalf of the given caller. If the file was |
| * not generated on behalf of the caller, an |
| * {@link IllegalArgumentException} is thrown. |
| * |
| * @param callingInfo a (uid, package name) pair identifying the caller |
| * @param bugreportFile the file name which was previously given to the caller in the |
| * {@link BugreportCallback#onFinished(String)} callback. |
| * @param forceUpdateMapping if {@code true}, updates the bugreport mapping by reading from |
| * the mapping file. |
| * |
| * @throws IllegalArgumentException if {@code bugreportFile} is not associated with |
| * {@code callingInfo}. |
| */ |
| @RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS, |
| conditional = true) |
| void ensureCallerPreviouslyGeneratedFile( |
| Context context, Pair<Integer, String> callingInfo, int userId, |
| String bugreportFile, boolean forceUpdateMapping) { |
| synchronized (mLock) { |
| if (onboardingBugreportV2Enabled()) { |
| final int uidForUser = Binder.withCleanCallingIdentity(() -> { |
| try { |
| return context.getPackageManager() |
| .getPackageUidAsUser(callingInfo.second, userId); |
| } catch (PackageManager.NameNotFoundException exception) { |
| throwInvalidBugreportFileForCallerException( |
| bugreportFile, callingInfo.second); |
| return -1; |
| } |
| }); |
| if (uidForUser != callingInfo.first && context.checkCallingOrSelfPermission( |
| Manifest.permission.INTERACT_ACROSS_USERS) |
| != PackageManager.PERMISSION_GRANTED) { |
| throw new SecurityException( |
| callingInfo.second + " does not hold the " |
| + "INTERACT_ACROSS_USERS permission to access " |
| + "cross-user bugreports."); |
| } |
| if (!mReadBugreportMapping || forceUpdateMapping) { |
| readBugreportMappingLocked(); |
| } |
| ArraySet<String> bugreportFilesForUid = mBugreportFiles.get( |
| new Pair<>(uidForUser, callingInfo.second)); |
| if (bugreportFilesForUid == null |
| || !bugreportFilesForUid.contains(bugreportFile)) { |
| throwInvalidBugreportFileForCallerException( |
| bugreportFile, callingInfo.second); |
| } |
| } else { |
| ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(callingInfo); |
| if (bugreportFilesForCaller != null |
| && bugreportFilesForCaller.contains(bugreportFile)) { |
| bugreportFilesForCaller.remove(bugreportFile); |
| if (bugreportFilesForCaller.isEmpty()) { |
| mBugreportFiles.remove(callingInfo); |
| } |
| } else { |
| throwInvalidBugreportFileForCallerException( |
| bugreportFile, callingInfo.second); |
| |
| } |
| } |
| } |
| } |
| |
| private static void throwInvalidBugreportFileForCallerException( |
| String bugreportFile, String packageName) { |
| throw new IllegalArgumentException("File " + bugreportFile + " was not generated on" |
| + " behalf of calling package " + packageName); |
| } |
| |
| /** |
| * Associates a bugreport file with a caller, which is identified as a |
| * (uid, package name) pair. |
| */ |
| void addBugreportFileForCaller( |
| Pair<Integer, String> caller, String bugreportFile, boolean keepOnRetrieval) { |
| addBugreportMapping(caller, bugreportFile); |
| synchronized (mLock) { |
| if (onboardingBugreportV2Enabled()) { |
| if (keepOnRetrieval) { |
| mBugreportFilesToPersist.add(bugreportFile); |
| } |
| writeBugreportDataLocked(); |
| } |
| } |
| } |
| |
| private void addBugreportMapping(Pair<Integer, String> caller, String bugreportFile) { |
| synchronized (mLock) { |
| if (!mBugreportFiles.containsKey(caller)) { |
| mBugreportFiles.put(caller, new ArraySet<>()); |
| } |
| ArraySet<String> bugreportFilesForCaller = mBugreportFiles.get(caller); |
| bugreportFilesForCaller.add(bugreportFile); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void readBugreportMappingLocked() { |
| mBugreportFiles = new ArrayMap<>(); |
| try (InputStream inputStream = mMappingFile.openRead()) { |
| final TypedXmlPullParser parser = Xml.resolvePullParser(inputStream); |
| XmlUtils.beginDocument(parser, TAG_BUGREPORT_DATA); |
| int depth = parser.getDepth(); |
| while (XmlUtils.nextElementWithin(parser, depth)) { |
| String tag = parser.getName(); |
| switch (tag) { |
| case TAG_BUGREPORT_MAP: |
| readBugreportMapEntry(parser); |
| break; |
| case TAG_PERSISTENT_BUGREPORT: |
| readPersistentBugreportEntry(parser); |
| break; |
| default: |
| Slog.e(TAG, "Unknown tag while reading bugreport mapping file: " |
| + tag); |
| } |
| } |
| mReadBugreportMapping = true; |
| } catch (FileNotFoundException e) { |
| Slog.i(TAG, "Bugreport mapping file does not exist"); |
| } catch (IOException | XmlPullParserException e) { |
| mMappingFile.delete(); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void writeBugreportDataLocked() { |
| if (mBugreportFiles.isEmpty() && mBugreportFilesToPersist.isEmpty()) { |
| return; |
| } |
| try (FileOutputStream stream = mMappingFile.startWrite()) { |
| TypedXmlSerializer out = Xml.resolveSerializer(stream); |
| out.startDocument(null, true); |
| out.startTag(null, TAG_BUGREPORT_DATA); |
| for (Map.Entry<Pair<Integer, String>, ArraySet<String>> entry: |
| mBugreportFiles.entrySet()) { |
| Pair<Integer, String> callingInfo = entry.getKey(); |
| ArraySet<String> callersBugreports = entry.getValue(); |
| for (String bugreportFile: callersBugreports) { |
| writeBugreportMapEntry(callingInfo, bugreportFile, out); |
| } |
| } |
| for (String file : mBugreportFilesToPersist) { |
| writePersistentBugreportEntry(file, out); |
| } |
| out.endTag(null, TAG_BUGREPORT_DATA); |
| out.endDocument(); |
| mMappingFile.finishWrite(stream); |
| } catch (IOException e) { |
| Slog.e(TAG, "Failed to write bugreport mapping file", e); |
| } |
| } |
| |
| private void readBugreportMapEntry(TypedXmlPullParser parser) |
| throws XmlPullParserException { |
| int callingUid = parser.getAttributeInt(null, ATTR_CALLING_UID); |
| String callingPackage = parser.getAttributeValue(null, ATTR_CALLING_PACKAGE); |
| String bugreportFile = parser.getAttributeValue(null, ATTR_BUGREPORT_FILE); |
| addBugreportMapping(new Pair<>(callingUid, callingPackage), bugreportFile); |
| } |
| |
| private void readPersistentBugreportEntry(TypedXmlPullParser parser) |
| throws XmlPullParserException { |
| String bugreportFile = parser.getAttributeValue(null, ATTR_BUGREPORT_FILE); |
| synchronized (mLock) { |
| mBugreportFilesToPersist.add(bugreportFile); |
| } |
| } |
| |
| private void writeBugreportMapEntry(Pair<Integer, String> callingInfo, String bugreportFile, |
| TypedXmlSerializer out) throws IOException { |
| out.startTag(null, TAG_BUGREPORT_MAP); |
| out.attributeInt(null, ATTR_CALLING_UID, callingInfo.first); |
| out.attribute(null, ATTR_CALLING_PACKAGE, callingInfo.second); |
| out.attribute(null, ATTR_BUGREPORT_FILE, bugreportFile); |
| out.endTag(null, TAG_BUGREPORT_MAP); |
| } |
| |
| private void writePersistentBugreportEntry( |
| String bugreportFile, TypedXmlSerializer out) throws IOException { |
| out.startTag(null, TAG_PERSISTENT_BUGREPORT); |
| out.attribute(null, ATTR_BUGREPORT_FILE, bugreportFile); |
| out.endTag(null, TAG_PERSISTENT_BUGREPORT); |
| } |
| } |
| |
| static class Injector { |
| Context mContext; |
| ArraySet<String> mAllowlistedPackages; |
| AtomicFile mMappingFile; |
| |
| Injector(Context context, ArraySet<String> allowlistedPackages, AtomicFile mappingFile) { |
| mContext = context; |
| mAllowlistedPackages = allowlistedPackages; |
| mMappingFile = mappingFile; |
| } |
| |
| Context getContext() { |
| return mContext; |
| } |
| |
| ArraySet<String> getAllowlistedPackages() { |
| return mAllowlistedPackages; |
| } |
| |
| AtomicFile getMappingFile() { |
| return mMappingFile; |
| } |
| } |
| |
| BugreportManagerServiceImpl(Context context) { |
| this(new Injector( |
| context, SystemConfig.getInstance().getBugreportWhitelistedPackages(), |
| new AtomicFile(new File(new File( |
| Environment.getDataDirectory(), "system"), "bugreport-mapping.xml")))); |
| } |
| |
| @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) |
| BugreportManagerServiceImpl(Injector injector) { |
| mContext = injector.getContext(); |
| mAppOps = mContext.getSystemService(AppOpsManager.class); |
| mTelephonyManager = mContext.getSystemService(TelephonyManager.class); |
| mBugreportFileManager = new BugreportFileManager(injector.getMappingFile()); |
| mBugreportAllowlistedPackages = injector.getAllowlistedPackages(); |
| } |
| |
| @Override |
| @RequiresPermission(android.Manifest.permission.DUMP) |
| public void preDumpUiData(String callingPackage) { |
| enforcePermission(callingPackage, Binder.getCallingUid(), true); |
| |
| synchronized (mLock) { |
| preDumpUiDataLocked(callingPackage); |
| } |
| } |
| |
| @Override |
| @RequiresPermission(android.Manifest.permission.DUMP) |
| public void startBugreport(int callingUidUnused, String callingPackage, |
| FileDescriptor bugreportFd, FileDescriptor screenshotFd, |
| int bugreportMode, int bugreportFlags, IDumpstateListener listener, |
| boolean isScreenshotRequested) { |
| Objects.requireNonNull(callingPackage); |
| Objects.requireNonNull(bugreportFd); |
| Objects.requireNonNull(listener); |
| validateBugreportMode(bugreportMode); |
| validateBugreportFlags(bugreportFlags); |
| |
| int callingUid = Binder.getCallingUid(); |
| enforcePermission(callingPackage, callingUid, bugreportMode |
| == BugreportParams.BUGREPORT_MODE_TELEPHONY /* checkCarrierPrivileges */); |
| final long identity = Binder.clearCallingIdentity(); |
| try { |
| ensureUserCanTakeBugReport(bugreportMode); |
| } finally { |
| Binder.restoreCallingIdentity(identity); |
| } |
| |
| Slogf.i(TAG, "Starting bugreport for %s / %d", callingPackage, callingUid); |
| synchronized (mLock) { |
| startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd, |
| bugreportMode, bugreportFlags, listener, isScreenshotRequested); |
| } |
| } |
| |
| @Override |
| @RequiresPermission(android.Manifest.permission.DUMP) // or carrier privileges |
| public void cancelBugreport(int callingUidUnused, String callingPackage) { |
| int callingUid = Binder.getCallingUid(); |
| enforcePermission(callingPackage, callingUid, true /* checkCarrierPrivileges */); |
| |
| Slogf.i(TAG, "Cancelling bugreport for %s / %d", callingPackage, callingUid); |
| synchronized (mLock) { |
| IDumpstate ds = getDumpstateBinderServiceLocked(); |
| if (ds == null) { |
| Slog.w(TAG, "cancelBugreport: Could not find native dumpstate service"); |
| return; |
| } |
| try { |
| // Note: this may throw SecurityException back out to the caller if they aren't |
| // allowed to cancel the report, in which case we should NOT stop the dumpstate |
| // service, since that would unintentionally kill some other app's bugreport, which |
| // we specifically disallow. |
| ds.cancelBugreport(callingUid, callingPackage); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "RemoteException in cancelBugreport", e); |
| } |
| stopDumpstateBinderServiceLocked(); |
| } |
| } |
| |
| @Override |
| @RequiresPermission(value = Manifest.permission.DUMP, conditional = true) |
| public void retrieveBugreport(int callingUidUnused, String callingPackage, int userId, |
| FileDescriptor bugreportFd, String bugreportFile, |
| |
| boolean keepBugreportOnRetrievalUnused, IDumpstateListener listener) { |
| int callingUid = Binder.getCallingUid(); |
| enforcePermission(callingPackage, callingUid, false); |
| |
| Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid); |
| try { |
| mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( |
| mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile, |
| /* forceUpdateMapping= */ false); |
| } catch (IllegalArgumentException e) { |
| Slog.e(TAG, e.getMessage()); |
| reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); |
| return; |
| } |
| |
| synchronized (mLock) { |
| if (isDumpstateBinderServiceRunningLocked()) { |
| Slog.w(TAG, "'dumpstate' is already running. Cannot retrieve a bugreport" |
| + " while another one is currently in progress."); |
| reportError(listener, |
| IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); |
| return; |
| } |
| |
| IDumpstate ds = startAndGetDumpstateBinderServiceLocked(); |
| if (ds == null) { |
| Slog.w(TAG, "Unable to get bugreport service"); |
| reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR); |
| return; |
| } |
| |
| // Wrap the listener so we can intercept binder events directly. |
| DumpstateListener myListener = new DumpstateListener(listener, ds, |
| new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true); |
| |
| boolean keepBugreportOnRetrieval = false; |
| if (onboardingBugreportV2Enabled()) { |
| keepBugreportOnRetrieval = mBugreportFileManager.mBugreportFilesToPersist.contains( |
| bugreportFile); |
| } |
| |
| setCurrentDumpstateListenerLocked(myListener); |
| try { |
| ds.retrieveBugreport(callingUid, callingPackage, userId, bugreportFd, |
| bugreportFile, keepBugreportOnRetrieval, myListener); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "RemoteException in retrieveBugreport", e); |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private void setCurrentDumpstateListenerLocked(DumpstateListener listener) { |
| if (mCurrentDumpstateListener != null) { |
| Slogf.w(TAG, "setCurrentDumpstateListenerLocked(%s): called when " |
| + "mCurrentDumpstateListener is already set (%s)", listener, |
| mCurrentDumpstateListener); |
| } |
| mCurrentDumpstateListener = listener; |
| } |
| |
| private void validateBugreportMode(@BugreportParams.BugreportMode int mode) { |
| if (mode != BugreportParams.BUGREPORT_MODE_FULL |
| && mode != BugreportParams.BUGREPORT_MODE_INTERACTIVE |
| && mode != BugreportParams.BUGREPORT_MODE_REMOTE |
| && mode != BugreportParams.BUGREPORT_MODE_WEAR |
| && mode != BugreportParams.BUGREPORT_MODE_TELEPHONY |
| && mode != BugreportParams.BUGREPORT_MODE_WIFI |
| && mode != BugreportParams.BUGREPORT_MODE_ONBOARDING) { |
| Slog.w(TAG, "Unknown bugreport mode: " + mode); |
| throw new IllegalArgumentException("Unknown bugreport mode: " + mode); |
| } |
| } |
| |
| private void validateBugreportFlags(int flags) { |
| flags = clearBugreportFlag(flags, |
| BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA |
| | BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT |
| | BugreportParams.BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL); |
| if (flags != 0) { |
| Slog.w(TAG, "Unknown bugreport flags: " + flags); |
| throw new IllegalArgumentException("Unknown bugreport flags: " + flags); |
| } |
| } |
| |
| private void enforcePermission( |
| String callingPackage, int callingUid, boolean checkCarrierPrivileges) { |
| mAppOps.checkPackage(callingUid, callingPackage); |
| |
| // To gain access through the DUMP permission, the OEM has to allow this package explicitly |
| // via sysconfig and privileged permissions. |
| boolean allowlisted = mBugreportAllowlistedPackages.contains(callingPackage); |
| if (!allowlisted) { |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| allowlisted = mContext.getSystemService(RoleManager.class).getRoleHolders( |
| ROLE_SYSTEM_AUTOMOTIVE_PROJECTION).contains(callingPackage); |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| } |
| |
| if (allowlisted && mContext.checkCallingOrSelfPermission( |
| android.Manifest.permission.DUMP) == PackageManager.PERMISSION_GRANTED) { |
| return; |
| } |
| |
| // For carrier privileges, this can include user-installed apps. This is essentially a |
| // function of the current active SIM(s) in the device to let carrier apps through. |
| final long token = Binder.clearCallingIdentity(); |
| try { |
| if (checkCarrierPrivileges |
| && mTelephonyManager.checkCarrierPrivilegesForPackageAnyPhone(callingPackage) |
| == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { |
| return; |
| } |
| } finally { |
| Binder.restoreCallingIdentity(token); |
| } |
| |
| String message = |
| callingPackage |
| + " does not hold the DUMP permission or is not bugreport-whitelisted or " |
| + "does not have an allowed role " |
| + (checkCarrierPrivileges ? "and does not have carrier privileges " : "") |
| + "to request a bugreport"; |
| Slog.w(TAG, message); |
| throw new SecurityException(message); |
| } |
| |
| /** |
| * Validates that the current user is an admin user or, when bugreport is requested remotely |
| * that the current user is an affiliated user. |
| * |
| * @throws IllegalArgumentException if the current user is not an admin user |
| */ |
| private void ensureUserCanTakeBugReport(int bugreportMode) { |
| UserInfo currentUser = null; |
| try { |
| currentUser = ActivityManager.getService().getCurrentUser(); |
| } catch (RemoteException e) { |
| // Impossible to get RemoteException for an in-process call. |
| } |
| |
| if (currentUser == null) { |
| logAndThrow("There is no current user, so no bugreport can be requested."); |
| } |
| |
| if (!currentUser.isAdmin()) { |
| if (bugreportMode == BugreportParams.BUGREPORT_MODE_REMOTE |
| && isCurrentUserAffiliated(currentUser.id)) { |
| return; |
| } |
| logAndThrow(TextUtils.formatSimple("Current user %s is not an admin user." |
| + " Only admin users are allowed to take bugreport.", currentUser.id)); |
| } |
| } |
| |
| /** |
| * Returns {@code true} if the device has device owner and the current user is affiliated |
| * with the device owner. |
| */ |
| private boolean isCurrentUserAffiliated(int currentUserId) { |
| DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); |
| int deviceOwnerUid = dpm.getDeviceOwnerUserId(); |
| if (deviceOwnerUid == UserHandle.USER_NULL) { |
| return false; |
| } |
| |
| int callingUserId = UserHandle.getUserId(Binder.getCallingUid()); |
| |
| Slog.i(TAG, "callingUid: " + callingUserId + " deviceOwnerUid: " + deviceOwnerUid |
| + " currentUserId: " + currentUserId); |
| |
| if (callingUserId != deviceOwnerUid) { |
| logAndThrow("Caller is not device owner on provisioned device."); |
| } |
| if (!dpm.isAffiliatedUser(currentUserId)) { |
| logAndThrow("Current user is not affiliated to the device owner."); |
| } |
| return true; |
| } |
| |
| @GuardedBy("mLock") |
| private void preDumpUiDataLocked(String callingPackage) { |
| mPreDumpedDataUid = OptionalInt.empty(); |
| |
| if (isDumpstateBinderServiceRunningLocked()) { |
| Slog.e(TAG, "'dumpstate' is already running. " |
| + "Cannot pre-dump data while another operation is currently in progress."); |
| return; |
| } |
| |
| IDumpstate ds = startAndGetDumpstateBinderServiceLocked(); |
| if (ds == null) { |
| Slog.e(TAG, "Unable to get bugreport service"); |
| return; |
| } |
| |
| try { |
| ds.preDumpUiData(callingPackage); |
| } catch (RemoteException e) { |
| return; |
| } finally { |
| // dumpstate service is already started now. We need to kill it to manage the |
| // lifecycle correctly. If we don't subsequent callers will get |
| // BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error. |
| stopDumpstateBinderServiceLocked(); |
| } |
| |
| |
| mPreDumpedDataUid = OptionalInt.of(Binder.getCallingUid()); |
| } |
| |
| @GuardedBy("mLock") |
| private void startBugreportLocked(int callingUid, String callingPackage, |
| FileDescriptor bugreportFd, FileDescriptor screenshotFd, |
| int bugreportMode, int bugreportFlags, IDumpstateListener listener, |
| boolean isScreenshotRequested) { |
| if (isDumpstateBinderServiceRunningLocked()) { |
| Slog.w(TAG, "'dumpstate' is already running. Cannot start a new bugreport" |
| + " while another operation is currently in progress."); |
| reportError(listener, IDumpstateListener.BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS); |
| return; |
| } |
| |
| if ((bugreportFlags & BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA) != 0) { |
| if (mPreDumpedDataUid.isEmpty()) { |
| bugreportFlags = clearBugreportFlag(bugreportFlags, |
| BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA); |
| Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA." |
| + " No pre-dumped data is available."); |
| } else if (mPreDumpedDataUid.getAsInt() != callingUid) { |
| bugreportFlags = clearBugreportFlag(bugreportFlags, |
| BugreportParams.BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA); |
| Slog.w(TAG, "Ignoring BUGREPORT_FLAG_USE_PREDUMPED_UI_DATA." |
| + " Data was pre-dumped by a different UID."); |
| } |
| } |
| |
| boolean reportFinishedFile = |
| (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0; |
| |
| boolean keepBugreportOnRetrieval = |
| (bugreportFlags & BugreportParams.BUGREPORT_FLAG_KEEP_BUGREPORT_ON_RETRIEVAL) != 0; |
| |
| IDumpstate ds = startAndGetDumpstateBinderServiceLocked(); |
| if (ds == null) { |
| Slog.w(TAG, "Unable to get bugreport service"); |
| reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR); |
| return; |
| } |
| |
| DumpstateListener myListener = new DumpstateListener(listener, ds, |
| new Pair<>(callingUid, callingPackage), reportFinishedFile, |
| keepBugreportOnRetrieval); |
| setCurrentDumpstateListenerLocked(myListener); |
| try { |
| ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode, |
| bugreportFlags, myListener, isScreenshotRequested); |
| } catch (RemoteException e) { |
| // dumpstate service is already started now. We need to kill it to manage the |
| // lifecycle correctly. If we don't subsequent callers will get |
| // BUGREPORT_ERROR_ANOTHER_REPORT_IN_PROGRESS error. |
| // Note that listener will be notified by the death recipient below. |
| cancelBugreport(callingUid, callingPackage); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private boolean isDumpstateBinderServiceRunningLocked() { |
| return getDumpstateBinderServiceLocked() != null; |
| } |
| |
| @GuardedBy("mLock") |
| @Nullable |
| private IDumpstate getDumpstateBinderServiceLocked() { |
| // Note that the binder service on the native side is "dumpstate". |
| return IDumpstate.Stub.asInterface(ServiceManager.getService("dumpstate")); |
| } |
| |
| /* |
| * Start and get a handle to the native implementation of {@code IDumpstate} which does the |
| * actual bugreport generation. |
| * |
| * <p>Generating bugreports requires root privileges. To limit the footprint |
| * of the root access, the actual generation in Dumpstate binary is accessed as a |
| * oneshot service 'bugreport'. |
| * |
| * <p>Note that starting the service is achieved through setting a system property, which is |
| * not thread-safe. So the lock here offers thread-safety only among callers of the API. |
| */ |
| @GuardedBy("mLock") |
| private IDumpstate startAndGetDumpstateBinderServiceLocked() { |
| // Start bugreport service. |
| SystemProperties.set("ctl.start", BUGREPORT_SERVICE); |
| |
| IDumpstate ds = null; |
| boolean timedOut = false; |
| int totalTimeWaitedMillis = 0; |
| int seedWaitTimeMillis = 500; |
| while (!timedOut) { |
| ds = getDumpstateBinderServiceLocked(); |
| if (ds != null) { |
| Slog.i(TAG, "Got bugreport service handle."); |
| break; |
| } |
| SystemClock.sleep(seedWaitTimeMillis); |
| Slog.i(TAG, |
| "Waiting to get dumpstate service handle (" + totalTimeWaitedMillis + "ms)"); |
| totalTimeWaitedMillis += seedWaitTimeMillis; |
| seedWaitTimeMillis *= 2; |
| timedOut = totalTimeWaitedMillis > DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS; |
| } |
| if (timedOut) { |
| Slog.w(TAG, |
| "Timed out waiting to get dumpstate service handle (" |
| + totalTimeWaitedMillis + "ms)"); |
| } |
| return ds; |
| } |
| |
| @GuardedBy("mLock") |
| private void stopDumpstateBinderServiceLocked() { |
| // This tells init to cancel bugreportd service. Note that this is achieved through |
| // setting a system property which is not thread-safe. So the lock here offers |
| // thread-safety only among callers of the API. |
| SystemProperties.set("ctl.stop", BUGREPORT_SERVICE); |
| } |
| |
| @RequiresPermission(android.Manifest.permission.DUMP) |
| @Override |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return; |
| |
| pw.printf("Allow-listed packages: %s\n", mBugreportAllowlistedPackages); |
| |
| synchronized (mLock) { |
| pw.print("Pre-dumped data UID: "); |
| if (mPreDumpedDataUid.isEmpty()) { |
| pw.println("none"); |
| } else { |
| pw.println(mPreDumpedDataUid.getAsInt()); |
| } |
| |
| if (mCurrentDumpstateListener == null) { |
| pw.println("Not taking a bug report"); |
| } else { |
| mCurrentDumpstateListener.dump(pw); |
| } |
| |
| if (mNumberFinishedBugreports == 0) { |
| pw.println("No finished bugreports"); |
| } else { |
| pw.printf("%d finished bugreport%s. Last %d:\n", mNumberFinishedBugreports, |
| (mNumberFinishedBugreports > 1 ? "s" : ""), |
| Math.min(mNumberFinishedBugreports, LOCAL_LOG_SIZE)); |
| mFinishedBugreports.dump(" ", pw); |
| } |
| } |
| |
| synchronized (mBugreportFileManager.mLock) { |
| if (!mBugreportFileManager.mReadBugreportMapping) { |
| pw.println("Has not read bugreport mapping"); |
| } |
| int numberFiles = mBugreportFileManager.mBugreportFiles.size(); |
| pw.printf("%d pending file%s", numberFiles, (numberFiles > 1 ? "s" : "")); |
| if (numberFiles > 0) { |
| for (int i = 0; i < numberFiles; i++) { |
| Pair<Integer, String> caller = mBugreportFileManager.mBugreportFiles.keyAt(i); |
| ArraySet<String> files = mBugreportFileManager.mBugreportFiles.valueAt(i); |
| pw.printf(" %s: %s\n", callerToString(caller), files); |
| } |
| } else { |
| pw.println(); |
| } |
| } |
| } |
| |
| private static String callerToString(@Nullable Pair<Integer, String> caller) { |
| return (caller == null) ? "N/A" : caller.second + "/" + caller.first; |
| } |
| |
| private int clearBugreportFlag(int flags, @BugreportParams.BugreportFlag int flag) { |
| flags &= ~flag; |
| return flags; |
| } |
| |
| private void reportError(IDumpstateListener listener, int errorCode) { |
| try { |
| listener.onError(errorCode); |
| } catch (RemoteException e) { |
| // Something went wrong in binder or app process. There's nothing to do here. |
| Slog.w(TAG, "onError() transaction threw RemoteException: " + e.getMessage()); |
| } |
| } |
| |
| private void logAndThrow(String message) { |
| Slog.w(TAG, message); |
| throw new IllegalArgumentException(message); |
| } |
| |
| private final class DumpstateListener extends IDumpstateListener.Stub |
| implements DeathRecipient { |
| |
| private static int sNextId; |
| |
| private final int mId = ++sNextId; // used for debugging purposes only |
| private final IDumpstateListener mListener; |
| private final IDumpstate mDs; |
| private final Pair<Integer, String> mCaller; |
| private final boolean mReportFinishedFile; |
| private int mProgress; // used for debugging purposes only |
| private boolean mDone; |
| private boolean mKeepBugreportOnRetrieval; |
| |
| DumpstateListener(IDumpstateListener listener, IDumpstate ds, |
| Pair<Integer, String> caller, boolean reportFinishedFile) { |
| this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false); |
| } |
| |
| DumpstateListener(IDumpstateListener listener, IDumpstate ds, |
| Pair<Integer, String> caller, boolean reportFinishedFile, |
| boolean keepBugreportOnRetrieval) { |
| if (DEBUG) { |
| Slogf.d(TAG, "Starting DumpstateListener(id=%d) for caller %s", mId, caller); |
| } |
| mListener = listener; |
| mDs = ds; |
| mCaller = caller; |
| mReportFinishedFile = reportFinishedFile; |
| mKeepBugreportOnRetrieval = keepBugreportOnRetrieval; |
| try { |
| mDs.asBinder().linkToDeath(this, 0); |
| } catch (RemoteException e) { |
| Slog.e(TAG, "Unable to register Death Recipient for IDumpstate", e); |
| } |
| } |
| |
| @Override |
| public void onProgress(int progress) throws RemoteException { |
| if (DEBUG) { |
| Slogf.d(TAG, "onProgress: %d", progress); |
| } |
| mProgress = progress; |
| mListener.onProgress(progress); |
| } |
| |
| @Override |
| public void onError(int errorCode) throws RemoteException { |
| Slogf.e(TAG, "onError(): %d", errorCode); |
| synchronized (mLock) { |
| releaseItselfLocked(); |
| reportFinishedLocked("ErroCode: " + errorCode); |
| } |
| mListener.onError(errorCode); |
| } |
| |
| @Override |
| public void onFinished(String bugreportFile) throws RemoteException { |
| Slogf.i(TAG, "onFinished(): %s", bugreportFile); |
| synchronized (mLock) { |
| releaseItselfLocked(); |
| reportFinishedLocked("File: " + bugreportFile); |
| } |
| if (mReportFinishedFile) { |
| mBugreportFileManager.addBugreportFileForCaller( |
| mCaller, bugreportFile, mKeepBugreportOnRetrieval); |
| } else if (DEBUG) { |
| Slog.d(TAG, "Not reporting finished file"); |
| } |
| mListener.onFinished(bugreportFile); |
| } |
| |
| @Override |
| public void onScreenshotTaken(boolean success) throws RemoteException { |
| if (DEBUG) { |
| Slogf.d(TAG, "onScreenshotTaken(): %b", success); |
| } |
| mListener.onScreenshotTaken(success); |
| } |
| |
| @Override |
| public void onUiIntensiveBugreportDumpsFinished() throws RemoteException { |
| if (DEBUG) { |
| Slogf.d(TAG, "onUiIntensiveBugreportDumpsFinished()"); |
| } |
| mListener.onUiIntensiveBugreportDumpsFinished(); |
| } |
| |
| @Override |
| public void binderDied() { |
| try { |
| // Allow a small amount of time for any error or finished callbacks to be made. |
| // This ensures that the listener does not receive an erroneous runtime error |
| // callback. |
| Thread.sleep(1000); |
| } catch (InterruptedException ignored) { |
| } |
| synchronized (mLock) { |
| if (!mDone) { |
| // If we have not gotten a "done" callback this must be a crash. |
| Slog.e(TAG, "IDumpstate likely crashed. Notifying listener"); |
| try { |
| mListener.onError(IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR); |
| } catch (RemoteException ignored) { |
| // If listener is not around, there isn't anything to do here. |
| } |
| } |
| } |
| mDs.asBinder().unlinkToDeath(this, 0); |
| } |
| |
| @Override |
| public String toString() { |
| return "DumpstateListener[id=" + mId + ", progress=" + mProgress + "]"; |
| } |
| |
| @GuardedBy("mLock") |
| private void reportFinishedLocked(String message) { |
| mNumberFinishedBugreports++; |
| mFinishedBugreports.log("Caller: " + callerToString(mCaller) + " " + message); |
| } |
| |
| private void dump(PrintWriter pw) { |
| pw.println("DumpstateListener:"); |
| pw.printf(" id: %d\n", mId); |
| pw.printf(" caller: %s\n", callerToString(mCaller)); |
| pw.printf(" reports finished file: %b\n", mReportFinishedFile); |
| pw.printf(" progress: %d\n", mProgress); |
| pw.printf(" done: %b\n", mDone); |
| } |
| |
| @GuardedBy("mLock") |
| private void releaseItselfLocked() { |
| mDone = true; |
| if (mCurrentDumpstateListener == this) { |
| if (DEBUG) { |
| Slogf.d(TAG, "releaseItselfLocked(): releasing %s", this); |
| } |
| mCurrentDumpstateListener = null; |
| } else { |
| Slogf.w(TAG, "releaseItselfLocked(): " + this + " is finished, but current listener" |
| + " is " + mCurrentDumpstateListener); |
| } |
| } |
| } |
| } |