/*
|
* Copyright (C) 2018 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.incident;
|
|
import android.app.ActivityManager;
|
import android.content.ComponentName;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.pm.ApplicationInfo;
|
import android.content.pm.PackageManager;
|
import android.content.pm.UserInfo;
|
import android.content.res.Resources;
|
import android.os.Binder;
|
import android.os.Build;
|
import android.os.IBinder;
|
import android.os.IIncidentAuthListener;
|
import android.os.IIncidentCompanion;
|
import android.os.IIncidentManager;
|
import android.os.IncidentManager;
|
import android.os.RemoteException;
|
import android.os.ServiceManager;
|
import android.os.UserHandle;
|
import android.os.UserManager;
|
import android.util.Log;
|
|
import com.android.internal.util.DumpUtils;
|
import com.android.server.SystemService;
|
|
import java.io.FileDescriptor;
|
import java.io.PrintWriter;
|
import java.util.List;
|
|
/**
|
* Helper service for incidentd and dumpstated to provide user feedback
|
* and authorization for bug and inicdent reports to be taken.
|
*/
|
public class IncidentCompanionService extends SystemService {
|
static final String TAG = "IncidentCompanionService";
|
|
/**
|
* Dump argument for proxying restricted image dumps to the services
|
* listed in the config.
|
*/
|
private static String[] RESTRICTED_IMAGE_DUMP_ARGS = new String[] {
|
"--hal", "--restricted_image" };
|
|
/**
|
* The two permissions, for sendBroadcastAsUserMultiplePermissions.
|
*/
|
private static final String[] DUMP_AND_USAGE_STATS_PERMISSIONS = new String[] {
|
android.Manifest.permission.DUMP,
|
android.Manifest.permission.PACKAGE_USAGE_STATS
|
};
|
|
/**
|
* Tracker for reports pending approval.
|
*/
|
private PendingReports mPendingReports;
|
|
/**
|
* Implementation of the IIncidentCompanion binder interface.
|
*/
|
private final class BinderService extends IIncidentCompanion.Stub {
|
/**
|
* ONEWAY binder call to initiate authorizing the report. If you don't need
|
* IncidentCompanionService to check whether the calling UID matches then
|
* pass 0 for callingUid. Either way, the caller must have DUMP and USAGE_STATS
|
* permissions to retrieve the data, so it ends up being about the same.
|
*/
|
@Override
|
public void authorizeReport(int callingUid, final String callingPackage,
|
final String receiverClass, final String reportId,
|
final int flags, final IIncidentAuthListener listener) {
|
enforceRequestAuthorizationPermission();
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
mPendingReports.authorizeReport(callingUid, callingPackage,
|
receiverClass, reportId, flags, listener);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* ONEWAY binder call to cancel the inbound authorization request.
|
* <p>
|
* This is a oneway call, and so is authorizeReport, so the
|
* caller's ordering is preserved. The other calls on this object are synchronous, so
|
* their ordering is not guaranteed with respect to these calls. So the implementation
|
* sends out extra broadcasts to allow for eventual consistency.
|
*/
|
public void cancelAuthorization(final IIncidentAuthListener listener) {
|
enforceRequestAuthorizationPermission();
|
|
// Caller can cancel if they don't want it anymore, and mRequestQueue elides
|
// authorize/cancel pairs.
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
mPendingReports.cancelAuthorization(listener);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* ONEWAY implementation to send broadcast from incidentd, which is native.
|
*/
|
@Override
|
public void sendReportReadyBroadcast(String pkg, String cls) {
|
enforceRequestAuthorizationPermission();
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
final Context context = getContext();
|
|
final int primaryUser = getAndValidateUser(context);
|
if (primaryUser == UserHandle.USER_NULL) {
|
return;
|
}
|
|
final Intent intent = new Intent(Intent.ACTION_INCIDENT_REPORT_READY);
|
intent.setComponent(new ComponentName(pkg, cls));
|
|
Log.d(TAG, "sendReportReadyBroadcast sending primaryUser=" + primaryUser
|
+ " userHandle=" + UserHandle.getUserHandleForUid(primaryUser)
|
+ " intent=" + intent);
|
|
// Send it to the primary user. Only they can do incident reports.
|
context.sendBroadcastAsUserMultiplePermissions(intent,
|
UserHandle.getUserHandleForUid(primaryUser),
|
DUMP_AND_USAGE_STATS_PERMISSIONS);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to get the list of reports that are pending confirmation
|
* by the user.
|
*/
|
@Override
|
public List<String> getPendingReports() {
|
enforceAuthorizePermission();
|
return mPendingReports.getPendingReports();
|
}
|
|
/**
|
* SYNCHRONOUS binder call to mark a report as approved.
|
*/
|
@Override
|
public void approveReport(String uri) {
|
enforceAuthorizePermission();
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
mPendingReports.approveReport(uri);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to mark a report as NOT approved.
|
*/
|
@Override
|
public void denyReport(String uri) {
|
enforceAuthorizePermission();
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
mPendingReports.denyReport(uri);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to get the list of incident reports waiting for a receiver.
|
*/
|
@Override
|
public List<String> getIncidentReportList(String pkg, String cls) throws RemoteException {
|
enforceAccessReportsPermissions(null);
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
return getIIncidentManager().getIncidentReportList(pkg, cls);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to commit an incident report
|
*/
|
@Override
|
public void deleteIncidentReports(String pkg, String cls, String id)
|
throws RemoteException {
|
if (pkg == null || cls == null || id == null
|
|| pkg.length() == 0 || cls.length() == 0 || id.length() == 0) {
|
throw new RuntimeException("Invalid pkg, cls or id");
|
}
|
enforceAccessReportsPermissions(pkg);
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
getIIncidentManager().deleteIncidentReports(pkg, cls, id);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to delete all incident reports for a package.
|
*/
|
@Override
|
public void deleteAllIncidentReports(String pkg) throws RemoteException {
|
if (pkg == null || pkg.length() == 0) {
|
throw new RuntimeException("Invalid pkg");
|
}
|
enforceAccessReportsPermissions(pkg);
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
getIIncidentManager().deleteAllIncidentReports(pkg);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS binder call to get the IncidentReport object.
|
*/
|
@Override
|
public IncidentManager.IncidentReport getIncidentReport(String pkg, String cls, String id)
|
throws RemoteException {
|
if (pkg == null || cls == null || id == null
|
|| pkg.length() == 0 || cls.length() == 0 || id.length() == 0) {
|
throw new RuntimeException("Invalid pkg, cls or id");
|
}
|
enforceAccessReportsPermissions(pkg);
|
|
final long ident = Binder.clearCallingIdentity();
|
try {
|
return getIIncidentManager().getIncidentReport(pkg, cls, id);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
/**
|
* SYNCHRONOUS implementation of adb shell dumpsys debugreportcompanion.
|
*/
|
@Override
|
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
|
if (!DumpUtils.checkDumpPermission(getContext(), TAG, writer)) {
|
return;
|
}
|
|
if (args.length == 1 && "--restricted_image".equals(args[0])) {
|
// Does NOT clearCallingIdentity
|
dumpRestrictedImages(fd);
|
} else {
|
// Regular dump
|
mPendingReports.dump(fd, writer, args);
|
}
|
}
|
|
/**
|
* Proxy for the restricted images section.
|
*/
|
private void dumpRestrictedImages(FileDescriptor fd) {
|
// Only supported on eng or userdebug.
|
if (!(Build.IS_ENG || Build.IS_USERDEBUG)) {
|
return;
|
}
|
|
final Resources res = getContext().getResources();
|
final String[] services = res.getStringArray(
|
com.android.internal.R.array.config_restrictedImagesServices);
|
final int servicesCount = services.length;
|
for (int i = 0; i < servicesCount; i++) {
|
final String name = services[i];
|
Log.d(TAG, "Looking up service " + name);
|
final IBinder service = ServiceManager.getService(name);
|
if (service != null) {
|
Log.d(TAG, "Calling dump on service: " + name);
|
try {
|
service.dump(fd, RESTRICTED_IMAGE_DUMP_ARGS);
|
} catch (RemoteException ex) {
|
Log.w(TAG, "dump --restricted_image of " + name + " threw", ex);
|
}
|
}
|
}
|
}
|
|
/**
|
* Inside the binder interface class because we want to do all of the authorization
|
* here, before calling out to the helper objects.
|
*/
|
private void enforceRequestAuthorizationPermission() {
|
getContext().enforceCallingOrSelfPermission(
|
android.Manifest.permission.REQUEST_INCIDENT_REPORT_APPROVAL, null);
|
}
|
|
/**
|
* Inside the binder interface class because we want to do all of the authorization
|
* here, before calling out to the helper objects.
|
*/
|
private void enforceAuthorizePermission() {
|
getContext().enforceCallingOrSelfPermission(
|
android.Manifest.permission.APPROVE_INCIDENT_REPORTS, null);
|
}
|
|
/**
|
* Enforce that the calling process either has APPROVE_INCIDENT_REPORTS or
|
* (DUMP and PACKAGE_USAGE_STATS). This lets the approver get, because showing
|
* information about the report is a prerequisite for letting the user decide.
|
*
|
* If pkg is null, it is not checked, so make sure that you check it for null first
|
* if you do need the packages to match.
|
*
|
* Inside the binder interface class because we want to do all of the authorization
|
* here, before calling out to the helper objects.
|
*/
|
private void enforceAccessReportsPermissions(String pkg) {
|
if (getContext().checkCallingPermission(
|
android.Manifest.permission.APPROVE_INCIDENT_REPORTS)
|
!= PackageManager.PERMISSION_GRANTED) {
|
getContext().enforceCallingOrSelfPermission(
|
android.Manifest.permission.DUMP, null);
|
getContext().enforceCallingOrSelfPermission(
|
android.Manifest.permission.PACKAGE_USAGE_STATS, null);
|
if (pkg != null) {
|
enforceCallerIsSameApp(pkg);
|
}
|
}
|
}
|
|
/**
|
* Throw a SecurityException if the incoming binder call is not from pkg.
|
*/
|
private void enforceCallerIsSameApp(String pkg) throws SecurityException {
|
try {
|
final int uid = Binder.getCallingUid();
|
final int userId = UserHandle.getCallingUserId();
|
final ApplicationInfo ai = getContext().getPackageManager()
|
.getApplicationInfoAsUser(pkg, 0, userId);
|
if (ai == null) {
|
throw new SecurityException("Unknown package " + pkg);
|
}
|
if (!UserHandle.isSameApp(ai.uid, uid)) {
|
throw new SecurityException("Calling uid " + uid + " gave package "
|
+ pkg + " which is owned by uid " + ai.uid);
|
}
|
} catch (PackageManager.NameNotFoundException re) {
|
throw new SecurityException("Unknown package " + pkg + "\n" + re);
|
}
|
}
|
}
|
|
/**
|
* Construct new IncidentCompanionService with the context.
|
*/
|
public IncidentCompanionService(Context context) {
|
super(context);
|
mPendingReports = new PendingReports(context);
|
}
|
|
/**
|
* Initialize the service. It is still not safe to do UI until
|
* onBootPhase(SystemService.PHASE_BOOT_COMPLETED).
|
*/
|
@Override
|
public void onStart() {
|
publishBinderService(Context.INCIDENT_COMPANION_SERVICE, new BinderService());
|
}
|
|
/**
|
* Handle the boot process... Starts everything running once the system is
|
* up enough for us to do UI.
|
*/
|
@Override
|
public void onBootPhase(int phase) {
|
super.onBootPhase(phase);
|
switch (phase) {
|
case SystemService.PHASE_BOOT_COMPLETED:
|
mPendingReports.onBootCompleted();
|
break;
|
}
|
}
|
|
/**
|
* Looks up incidentd every time, so we don't need a complex handshake between
|
* incidentd and IncidentCompanionService.
|
*/
|
private IIncidentManager getIIncidentManager() throws RemoteException {
|
return IIncidentManager.Stub.asInterface(
|
ServiceManager.getService(Context.INCIDENT_SERVICE));
|
}
|
|
/**
|
* Check whether the current user is the primary user, and return the user id if they are.
|
* Returns UserHandle.USER_NULL if not valid.
|
*/
|
public static int getAndValidateUser(Context context) {
|
// Current user
|
UserInfo currentUser;
|
try {
|
currentUser = ActivityManager.getService().getCurrentUser();
|
} catch (RemoteException ex) {
|
// We're already inside the system process.
|
throw new RuntimeException(ex);
|
}
|
|
// Primary user
|
final UserManager um = UserManager.get(context);
|
final UserInfo primaryUser = um.getPrimaryUser();
|
|
// Check that we're using the right user.
|
if (currentUser == null) {
|
Log.w(TAG, "No current user. Nobody to approve the report."
|
+ " The report will be denied.");
|
return UserHandle.USER_NULL;
|
}
|
if (primaryUser == null) {
|
Log.w(TAG, "No primary user. Nobody to approve the report."
|
+ " The report will be denied.");
|
return UserHandle.USER_NULL;
|
}
|
if (primaryUser.id != currentUser.id) {
|
Log.w(TAG, "Only the primary user can approve bugreports, but they are not"
|
+ " the current user. The report will be denied.");
|
return UserHandle.USER_NULL;
|
}
|
|
return primaryUser.id;
|
}
|
}
|