content: /*
   * 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 {
          }
      }
  }
  