/*
|
* 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 android.annotation.RequiresPermission;
|
import android.app.ActivityManager;
|
import android.app.AppOpsManager;
|
import android.content.Context;
|
import android.content.pm.UserInfo;
|
import android.os.Binder;
|
import android.os.BugreportParams;
|
import android.os.IDumpstate;
|
import android.os.IDumpstateListener;
|
import android.os.IDumpstateToken;
|
import android.os.RemoteException;
|
import android.os.ServiceManager;
|
import android.os.SystemClock;
|
import android.os.SystemProperties;
|
import android.os.UserManager;
|
import android.util.ArraySet;
|
import android.util.Slog;
|
|
import com.android.internal.annotations.GuardedBy;
|
import com.android.internal.util.Preconditions;
|
import com.android.server.SystemConfig;
|
|
import java.io.FileDescriptor;
|
|
/**
|
* 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 String TAG = "BugreportManagerService";
|
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 ArraySet<String> mBugreportWhitelistedPackages;
|
|
BugreportManagerServiceImpl(Context context) {
|
mContext = context;
|
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
|
mBugreportWhitelistedPackages =
|
SystemConfig.getInstance().getBugreportWhitelistedPackages();
|
}
|
|
@Override
|
@RequiresPermission(android.Manifest.permission.DUMP)
|
public IDumpstateToken setListener(String name, IDumpstateListener listener,
|
boolean getSectionDetails) {
|
throw new UnsupportedOperationException("setListener is not allowed on this service");
|
}
|
|
@Override
|
@RequiresPermission(android.Manifest.permission.DUMP)
|
public void startBugreport(int callingUidUnused, String callingPackage,
|
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
|
int bugreportMode, IDumpstateListener listener) {
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, "startBugreport");
|
Preconditions.checkNotNull(callingPackage);
|
Preconditions.checkNotNull(bugreportFd);
|
Preconditions.checkNotNull(listener);
|
validateBugreportMode(bugreportMode);
|
final long identity = Binder.clearCallingIdentity();
|
try {
|
ensureIsPrimaryUser();
|
} finally {
|
Binder.restoreCallingIdentity(identity);
|
}
|
|
int callingUid = Binder.getCallingUid();
|
mAppOps.checkPackage(callingUid, callingPackage);
|
|
if (!mBugreportWhitelistedPackages.contains(callingPackage)) {
|
throw new SecurityException(
|
callingPackage + " is not whitelisted to use Bugreport API");
|
}
|
synchronized (mLock) {
|
startBugreportLocked(callingUid, callingPackage, bugreportFd, screenshotFd,
|
bugreportMode, listener);
|
}
|
}
|
|
@Override
|
@RequiresPermission(android.Manifest.permission.DUMP)
|
public void cancelBugreport() {
|
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, "startBugreport");
|
// 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.
|
synchronized (mLock) {
|
SystemProperties.set("ctl.stop", BUGREPORT_SERVICE);
|
}
|
}
|
|
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) {
|
Slog.w(TAG, "Unknown bugreport mode: " + mode);
|
throw new IllegalArgumentException("Unknown bugreport mode: " + mode);
|
}
|
}
|
|
/**
|
* Validates that the current user is the primary user.
|
*
|
* @throws IllegalArgumentException if the current user is not the primary user
|
*/
|
private void ensureIsPrimaryUser() {
|
UserInfo currentUser = null;
|
try {
|
currentUser = ActivityManager.getService().getCurrentUser();
|
} catch (RemoteException e) {
|
// Impossible to get RemoteException for an in-process call.
|
}
|
|
UserInfo primaryUser = UserManager.get(mContext).getPrimaryUser();
|
if (currentUser == null) {
|
logAndThrow("No current user. Only primary user is allowed to take bugreports.");
|
}
|
if (primaryUser == null) {
|
logAndThrow("No primary user. Only primary user is allowed to take bugreports.");
|
}
|
if (primaryUser.id != currentUser.id) {
|
logAndThrow("Current user not primary user. Only primary user"
|
+ " is allowed to take bugreports.");
|
}
|
}
|
|
@GuardedBy("mLock")
|
private void startBugreportLocked(int callingUid, String callingPackage,
|
FileDescriptor bugreportFd, FileDescriptor screenshotFd,
|
int bugreportMode, IDumpstateListener listener) {
|
if (isDumpstateBinderServiceRunningLocked()) {
|
Slog.w(TAG, "'dumpstate' is already running. Cannot start a new 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.
|
IDumpstateListener myListener = new DumpstateListener(listener, ds);
|
try {
|
ds.startBugreport(callingUid, callingPackage,
|
bugreportFd, screenshotFd, bugreportMode, myListener);
|
} catch (RemoteException e) {
|
// bugreportd 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();
|
}
|
}
|
|
@GuardedBy("mLock")
|
private boolean isDumpstateBinderServiceRunningLocked() {
|
IDumpstate ds = IDumpstate.Stub.asInterface(ServiceManager.getService("dumpstate"));
|
return ds != null;
|
}
|
|
/*
|
* 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) {
|
// Note that the binder service on the native side is "dumpstate".
|
ds = IDumpstate.Stub.asInterface(ServiceManager.getService("dumpstate"));
|
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;
|
}
|
|
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 final IDumpstateListener mListener;
|
private final IDumpstate mDs;
|
private boolean mDone = false;
|
|
DumpstateListener(IDumpstateListener listener, IDumpstate ds) {
|
mListener = listener;
|
mDs = ds;
|
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 {
|
mListener.onProgress(progress);
|
}
|
|
@Override
|
public void onError(int errorCode) throws RemoteException {
|
synchronized (mLock) {
|
mDone = true;
|
}
|
mListener.onError(errorCode);
|
}
|
|
@Override
|
public void onFinished() throws RemoteException {
|
synchronized (mLock) {
|
mDone = true;
|
}
|
mListener.onFinished();
|
}
|
|
@Override
|
public void binderDied() {
|
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);
|
}
|
|
// Old methods; unused in the API flow.
|
@Override
|
public void onProgressUpdated(int progress) throws RemoteException {
|
}
|
|
@Override
|
public void onMaxProgressUpdated(int maxProgress) throws RemoteException {
|
}
|
|
@Override
|
public void onSectionComplete(String title, int status, int size, int durationMs)
|
throws RemoteException {
|
}
|
}
|
}
|