blob: b8e7d4971790048afb4565031edfee77b1626a55 [file] [log] [blame]
/*
* 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.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();
// Get the current admin user. Only they can do incident reports.
final int currentAdminUser = getCurrentUserIfAdmin();
if (currentAdminUser == 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 currentUser=" + currentAdminUser
+ " userHandle=" + UserHandle.of(currentAdminUser)
+ " intent=" + intent);
context.sendBroadcastAsUserMultiplePermissions(intent,
UserHandle.of(currentAdminUser),
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 an admin user, and return the user id if they are.
* Returns UserHandle.USER_NULL if not valid.
*/
public static int getCurrentUserIfAdmin() {
// Current user
UserInfo currentUser;
try {
currentUser = ActivityManager.getService().getCurrentUser();
} catch (RemoteException ex) {
// We're already inside the system process.
throw new RuntimeException(ex);
}
// 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 (!currentUser.isAdmin()) {
Log.w(TAG, "Only an admin user running in foreground can approve "
+ "bugreports, but the current foreground user is not an admin user. "
+ "The report will be denied.");
return UserHandle.USER_NULL;
}
return currentUser.id;
}
}