From d11850cd44dcf1114f53bd0e12569a584d557039 Mon Sep 17 00:00:00 2001 From: Nandana Dutt Date: Wed, 12 Dec 2018 17:26:57 +0000 Subject: [PATCH] Add a privileged API for capturing and consuming bugreports The API is mostly implemented; except for hooking up the listener and handling an already running bugreport. BugreportManager is the handle to the new API exposed to apps. Generating bugreports requires root privileges. To limit the footprint of the root access, the actual bugreport generation in Dumpstate binary, is accessed as a oneshot service and dies after it finishes running. System server accesses Dumpstate via a binder interface since it does not have root privileges. Starting a oneshot service is done via setting a system property, which needs to be done from system server. BugreportManagerService is the new system server service that does this. BugreportManager calls into BugreportManagerService via a binder interface, since the former is in the app's process. Both app to system server as well as system server to native service calls are via implementations of IDumpstate binder interface. Bug: 111441001 Test: builds. Flashed & verified it boots. Test: wrote a test client (not included) and verified invoking startBugreport works. Change-Id: I4abeb753388c055c36ae0dd916af1ec8d40b7bf0 --- .../android/app/SystemServiceRegistry.java | 12 ++ core/java/android/content/Context.java | 10 ++ core/java/android/os/BugreportManager.java | 148 ++++++++++++++++++ core/java/android/os/BugreportParams.java | 90 +++++++++++ .../server/os/BugreportManagerService.java | 43 +++++ .../os/BugreportManagerServiceImpl.java | 130 +++++++++++++++ .../java/com/android/server/SystemServer.java | 6 + 7 files changed, 439 insertions(+) create mode 100644 core/java/android/os/BugreportManager.java create mode 100644 core/java/android/os/BugreportParams.java create mode 100644 services/core/java/com/android/server/os/BugreportManagerService.java create mode 100644 services/core/java/com/android/server/os/BugreportManagerServiceImpl.java diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9ddf4bd0870c8..c021b72959ccf 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -120,6 +120,7 @@ import android.net.wifi.rtt.WifiRttManager; import android.nfc.NfcManager; import android.os.BatteryManager; import android.os.BatteryStats; +import android.os.BugreportManager; import android.os.Build; import android.os.DeviceIdleManager; import android.os.DropBoxManager; @@ -127,6 +128,7 @@ import android.os.HardwarePropertiesManager; import android.os.IBatteryPropertiesRegistrar; import android.os.IBinder; import android.os.IDeviceIdleController; +import android.os.IDumpstate; import android.os.IHardwarePropertiesManager; import android.os.IPowerManager; import android.os.IRecoverySystem; @@ -1069,6 +1071,16 @@ final class SystemServiceRegistry { return new IncidentManager(ctx); }}); + registerService(Context.BUGREPORT_SERVICE, BugreportManager.class, + new CachedServiceFetcher() { + @Override + public BugreportManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + IBinder b = ServiceManager.getServiceOrThrow(Context.BUGREPORT_SERVICE); + return new BugreportManager(ctx.getOuterContext(), + IDumpstate.Stub.asInterface(b)); + }}); + registerService(Context.AUTOFILL_MANAGER_SERVICE, AutofillManager.class, new CachedServiceFetcher() { @Override diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index eb7be6f8a6b06..5e1be83a2cc8c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4426,6 +4426,16 @@ public abstract class Context { @SystemApi public static final String STATS_MANAGER = "stats"; + /** + * Service to capture a bugreport. + * @see #getSystemService(String) + * @see android.os.BugreportManager + * @hide + */ + // TODO: Expose API when the implementation is more complete. + // @SystemApi + public static final String BUGREPORT_SERVICE = "bugreport"; + /** * Use with {@link #getSystemService(String)} to retrieve a {@link * android.content.om.OverlayManager} for managing overlay packages. diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java new file mode 100644 index 0000000000000..1343d24d0d94a --- /dev/null +++ b/core/java/android/os/BugreportManager.java @@ -0,0 +1,148 @@ +/* + * 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 android.os; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemService; +import android.content.Context; +import android.os.IBinder.DeathRecipient; + +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Class that provides a privileged API to capture and consume bugreports. + * + * @hide + */ +// TODO: Expose API when the implementation is more complete. +// @SystemApi +@SystemService(Context.BUGREPORT_SERVICE) +public class BugreportManager { + private final Context mContext; + private final IDumpstate mBinder; + + /** @hide */ + public BugreportManager(@NonNull Context context, IDumpstate binder) { + mContext = context; + mBinder = binder; + } + + /** + * An interface describing the listener for bugreport progress and status. + */ + public interface BugreportListener { + /** + * Called when there is a progress update. + * @param progress the progress in [0.0, 100.0] + */ + void onProgress(float progress); + + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "BUGREPORT_ERROR_" }, value = { + BUGREPORT_ERROR_INVALID_INPUT, + BUGREPORT_ERROR_RUNTIME + }) + + /** Possible error codes taking a bugreport can encounter */ + @interface BugreportErrorCode {} + + /** The input options were invalid */ + int BUGREPORT_ERROR_INVALID_INPUT = 1; + + /** A runtime error occured */ + int BUGREPORT_ERROR_RUNTIME = 2; + + /** + * Called when taking bugreport resulted in an error. + * + * @param errorCode the error that occurred. Possible values are + * {@code BUGREPORT_ERROR_INVALID_INPUT}, {@code BUGREPORT_ERROR_RUNTIME}. + */ + void onError(@BugreportErrorCode int errorCode); + + /** + * Called when taking bugreport finishes successfully + * + * @param durationMs time capturing bugreport took in milliseconds + * @param title title for the bugreport; helpful in reminding the user why they took it + * @param description detailed description for the bugreport + */ + void onFinished(long durationMs, @NonNull String title, + @NonNull String description); + } + + /** + * Starts a bugreport asynchronously. + * + * @param bugreportFd file to write the bugreport. This should be opened in write-only, + * append mode. + * @param screenshotFd file to write the screenshot, if necessary. This should be opened + * in write-only, append mode. + * @param params options that specify what kind of a bugreport should be taken + * @param listener callback for progress and status updates + */ + @RequiresPermission(android.Manifest.permission.DUMP) + public void startBugreport(@NonNull FileDescriptor bugreportFd, + @Nullable FileDescriptor screenshotFd, + @NonNull BugreportParams params, @Nullable BugreportListener listener) { + // TODO(b/111441001): Enforce android.Manifest.permission.DUMP if necessary. + DumpstateListener dsListener = new DumpstateListener(listener); + + try { + mBinder.startBugreport(bugreportFd, screenshotFd, params.getMode(), dsListener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + // TODO(b/111441001) Connect up with BugreportListener methods. + private final class DumpstateListener extends IDumpstateListener.Stub + implements DeathRecipient { + private final BugreportListener mListener; + + DumpstateListener(@Nullable BugreportListener listener) { + mListener = listener; + } + + @Override + public void binderDied() { + // TODO(b/111441001): implement + } + + @Override + public void onProgressUpdated(int progress) throws RemoteException { + // TODO(b/111441001): implement + } + + @Override + public void onMaxProgressUpdated(int maxProgress) throws RemoteException { + // TODO(b/111441001): implement + } + + @Override + public void onSectionComplete(String title, int status, int size, int durationMs) + throws RemoteException { + // TODO(b/111441001): implement + } + } +} diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java new file mode 100644 index 0000000000000..4e696aed1fa85 --- /dev/null +++ b/core/java/android/os/BugreportParams.java @@ -0,0 +1,90 @@ +/* + * 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 android.os; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Parameters that specify what kind of bugreport should be taken. + * + * @hide + */ +// TODO: Expose API when the implementation is more complete. +// @SystemApi +public final class BugreportParams { + private final int mMode; + + public BugreportParams(@BugreportMode int mode) { + mMode = mode; + } + + public int getMode() { + return mMode; + } + + /** + * Defines acceptable types of bugreports. + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "BUGREPORT_MODE_" }, value = { + BUGREPORT_MODE_FULL, + BUGREPORT_MODE_INTERACTIVE, + BUGREPORT_MODE_REMOTE, + BUGREPORT_MODE_WEAR, + BUGREPORT_MODE_TELEPHONY, + BUGREPORT_MODE_WIFI + }) + public @interface BugreportMode {} + + /** + * Options for a bugreport without user interference (and hence causing less + * interference to the system), but includes all sections. + */ + public static final int BUGREPORT_MODE_FULL = IDumpstate.BUGREPORT_MODE_FULL; + + /** + * Options that allow user to monitor progress and enter additional data; might not + * include all sections. + */ + public static final int BUGREPORT_MODE_INTERACTIVE = IDumpstate.BUGREPORT_MODE_INTERACTIVE; + + /** + * Options for a bugreport requested remotely by administrator of the Device Owner app, + * not the device's user. + */ + public static final int BUGREPORT_MODE_REMOTE = IDumpstate.BUGREPORT_MODE_REMOTE; + + /** + * Options for a bugreport on a wearable device. + */ + public static final int BUGREPORT_MODE_WEAR = IDumpstate.BUGREPORT_MODE_WEAR; + + /** + * Options for a lightweight version of bugreport that only includes a few, urgent + * sections used to report telephony bugs. + */ + public static final int BUGREPORT_MODE_TELEPHONY = IDumpstate.BUGREPORT_MODE_TELEPHONY; + + /** + * Options for a lightweight bugreport that only includes a few sections related to + * Wifi. + */ + public static final int BUGREPORT_MODE_WIFI = IDumpstate.BUGREPORT_MODE_WIFI; +} diff --git a/services/core/java/com/android/server/os/BugreportManagerService.java b/services/core/java/com/android/server/os/BugreportManagerService.java new file mode 100644 index 0000000000000..e2415911e929d --- /dev/null +++ b/services/core/java/com/android/server/os/BugreportManagerService.java @@ -0,0 +1,43 @@ +/* + * 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.content.Context; + +import com.android.server.SystemService; + +/** + * Service that provides a privileged API to capture and consume bugreports. + * + * @hide + */ +public class BugreportManagerService extends SystemService { + private static final String TAG = "BugreportManagerService"; + + private BugreportManagerServiceImpl mService; + + public BugreportManagerService(Context context) { + super(context); + } + + @Override + public void onStart() { + mService = new BugreportManagerServiceImpl(getContext()); + // TODO(b/111441001): Needs sepolicy to be submitted first. + // publishBinderService(Context.BUGREPORT_SERVICE, mService); + } +} diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java new file mode 100644 index 0000000000000..faa4714a8697b --- /dev/null +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -0,0 +1,130 @@ +/* + * 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.content.Context; +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.util.Slog; + +import java.io.FileDescriptor; + +// TODO(b/111441001): +// 1. Handle the case where another bugreport is in progress +// 2. Make everything threadsafe +// 3. Pass validation & other errors on listener + +/** + * Implementation of the service that provides a privileged API to capture and consume bugreports. + * + *

Delegates the actualy generation to a native implementation of {@code Dumpstate}. + */ +class BugreportManagerServiceImpl extends IDumpstate.Stub { + private static final String TAG = "BugreportManagerService"; + private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000; + + private IDumpstate mDs = null; + private final Context mContext; + + BugreportManagerServiceImpl(Context context) { + mContext = context; + } + + @Override + @RequiresPermission(android.Manifest.permission.DUMP) + public IDumpstateToken setListener(String name, IDumpstateListener listener, + boolean getSectionDetails) throws RemoteException { + // TODO(b/111441001): Figure out if lazy setting of listener should be allowed + // and if so how to handle it. + throw new UnsupportedOperationException("setListener is not allowed on this service"); + } + + + @Override + @RequiresPermission(android.Manifest.permission.DUMP) + public void startBugreport(FileDescriptor bugreportFd, FileDescriptor screenshotFd, + int bugreportMode, IDumpstateListener listener) throws RemoteException { + + validate(bugreportMode); + + mDs = getDumpstateService(); + if (mDs == null) { + Slog.w(TAG, "Unable to get bugreport service"); + // TODO(b/111441001): pass error on listener + return; + } + mDs.startBugreport(bugreportFd, screenshotFd, bugreportMode, listener); + } + + private boolean validate(@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); + return false; + } + return true; + } + + /* + * Start and get a handle to the native implementation of {@code IDumpstate} which does the + * actual bugreport generation. + * + *

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'. + */ + private IDumpstate getDumpstateService() { + // Start bugreport service. + SystemProperties.set("ctl.start", "bugreport"); + + 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; + } +} diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 4326c39c43a29..a73ff9db90699 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -106,6 +106,7 @@ import com.android.server.net.watchlist.NetworkWatchlistService; import com.android.server.notification.NotificationManagerService; import com.android.server.oemlock.OemLockService; import com.android.server.om.OverlayManagerService; +import com.android.server.os.BugreportManagerService; import com.android.server.os.DeviceIdentifiersPolicyService; import com.android.server.os.SchedulingPolicyService; import com.android.server.pm.BackgroundDexOptService; @@ -786,6 +787,11 @@ public final class SystemServer { traceBeginAndSlog("StartRollbackManagerService"); mSystemServiceManager.startService(RollbackManagerService.class); traceEnd(); + + // Service to capture bugreports. + traceBeginAndSlog("StartBugreportManagerService"); + mSystemServiceManager.startService(BugreportManagerService.class); + traceEnd(); } /**