diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index a021ea7a6503..1e96cfc9c859 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -7086,6 +7086,15 @@ public String getLaunchedFromPackage() { return ActivityClient.getInstance().getLaunchedFromPackage(getActivityToken()); } + /** + * @hide + */ + @PackageManager.PermissionResult + public int checkLaunchedFromPermission(@NonNull String permission) { + return ActivityClient.getInstance().checkLaunchedFromPermission(getActivityToken(), + permission, getDeviceId()); + } + /** * Control whether this activity's main window is visible. This is intended * only for the special case of an activity that is not going to show a diff --git a/core/java/android/app/ActivityClient.java b/core/java/android/app/ActivityClient.java index b8bd030872c1..410f7cb39fee 100644 --- a/core/java/android/app/ActivityClient.java +++ b/core/java/android/app/ActivityClient.java @@ -694,4 +694,12 @@ protected IActivityClientController create() { } } } + + public int checkLaunchedFromPermission(IBinder token, String permission, int deviceId) { + try { + return getActivityClientController().checkLaunchedFromPermission(token, permission, deviceId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 7370fc36c23e..acd548c2f8f7 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -201,4 +201,10 @@ interface IActivityClientController { @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.INTERNAL_SYSTEM_WINDOW)") oneway void setActivityRecordInputSinkEnabled(in IBinder activityToken, boolean enabled); + + /** + * @return {@link PackageManager#PERMISSION_GRANTED} if the app launching the activity has the permission. + * This method is only accessible to select system apps such as browser app. + */ + int checkLaunchedFromPermission(in IBinder token, String permission, int deviceId); } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 315e7d85df24..c85a3270eeef 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -1705,4 +1705,16 @@ public void setActivityRecordInputSinkEnabled(IBinder activityToken, boolean ena } } } + + @Override + public int checkLaunchedFromPermission(IBinder token, String permission, int deviceId) { + if (!ActivityClientControllerHooks.canAccessLaunchedFromPackagePermission(permission)) { + throw new SecurityException(); + } + final ActivityRecord r; + synchronized (mGlobalLock) { + r = ActivityRecord.forTokenLocked(token); + } + return ActivityClientControllerHooks.checkLaunchedFromPermission(r, permission, deviceId); + } } diff --git a/services/core/java/com/android/server/wm/ActivityClientControllerHooks.java b/services/core/java/com/android/server/wm/ActivityClientControllerHooks.java new file mode 100644 index 000000000000..0a36c2ba4d82 --- /dev/null +++ b/services/core/java/com/android/server/wm/ActivityClientControllerHooks.java @@ -0,0 +1,106 @@ +package com.android.server.wm; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.app.AppGlobals; +import android.content.Context; +import android.content.pm.PackageManager; +import android.ext.BrowserUtils; +import android.os.Binder; +import android.os.Build; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.util.Slog; + +import com.android.internal.util.ArrayUtils; + +import com.android.server.LocalServices; +import com.android.server.pm.permission.PermissionManagerServiceInternal; + +class ActivityClientControllerHooks { + + private static final String TAG = "ActivityClientControllerHooks"; + private static final boolean DEBUG_LOGS = false; + + static boolean canAccessLaunchedFromPackagePermission(@NonNull String permission) { + if (!isPermissionEligibleForChecking(permission)) { + Slog.w(TAG, "Permission supplied isn't eligible for permission checks from calling package"); + return false; + } + + final String[] callerPkgs; + try { + callerPkgs = AppGlobals.getPackageManager().getPackagesForUid(Binder.getCallingUid()); + } catch (RemoteException e) { + Slog.w(TAG, "No packages found for calling app", e); + return false; + } + + for (String callerPkgName: callerPkgs) { + if (isCallerEligibleForChecking(callerPkgName)) { + return true; + } + } + + return false; + } + + private static boolean isCallerEligibleForChecking(@NonNull String callerPkgName) { + if (canAccessForDebuggingPurposes(callerPkgName)) { + if (DEBUG_LOGS) { + Slog.d(TAG, "Current package " + callerPkgName + " has been allowed " + + "to fetch permission state of calling app that opened its activities."); + } + return true; + } + + Context ctx = ActivityThread.currentActivityThread().getSystemContext(); + if (BrowserUtils.isSystemBrowser(ctx, callerPkgName)) { + return true; + } + + return false; + } + + private static boolean isPermissionEligibleForChecking(@NonNull String permission) { + return switch (permission) { + case Manifest.permission.INTERNET -> true; + default -> false; + }; + } + + private static boolean canAccessForDebuggingPurposes(@NonNull String packageName) { + if (!Build.IS_DEBUGGABLE) { + return false; + } + + String testPkgs = SystemProperties.get("persist.launchedFromPackagePermission_test_pkgs"); + return ArrayUtils.contains(testPkgs.split(","), packageName); + } + + @PackageManager.PermissionResult + static int checkLaunchedFromPermission(@Nullable ActivityRecord r, @NonNull String permission, + int deviceId) { + if (r == null) { + Slog.w(TAG, "Treating null previous activity record as permission denied"); + return PackageManager.PERMISSION_DENIED; + } + + // Fields acquired from ActivityRecord are final, no need to hold the global wm lock. + final String launchedFromPkgName = r.launchedFromPackage; + final int launchedFromUid = r.launchedFromUid; + final int userId = UserHandle.getUserId(launchedFromUid); + var permService = LocalServices.getService(PermissionManagerServiceInternal.class); + + // Do not take into account of calling app's package visibility towards launchedFromPackage. + long token = Binder.clearCallingIdentity(); + try { + return permService.checkPermission(launchedFromPkgName, permission, deviceId, userId); + } finally { + Binder.restoreCallingIdentity(token); + } + } +} \ No newline at end of file