Merge "[MediaProjection][Security] Consistent locking on token" into udc-dev am: fcbbe3562a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23243459

Change-Id: Id11fe8aaaad5b811aa4982627493af69f86226e4
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Naomi Musgrave
2023-05-19 11:44:50 +00:00
committed by Automerger Merge Worker
2 changed files with 107 additions and 43 deletions

View File

@@ -72,6 +72,7 @@ import android.util.Slog;
import android.view.ContentRecordingSession; import android.view.ContentRecordingSession;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils; import com.android.internal.util.DumpUtils;
@@ -111,7 +112,11 @@ public final class MediaProjectionManagerService extends SystemService
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
static final long MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id static final long MEDIA_PROJECTION_PREVENTS_REUSING_CONSENT = 266201607L; // buganizer id
private final Object mLock = new Object(); // Protects the list of media projections // Protects access to state at service level & IMediaProjection level.
// Invocation order while holding locks must follow below to avoid deadlock:
// WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService
// See mediaprojection.md
private final Object mLock = new Object();
private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters; private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters;
private final CallbackDelegate mCallbackDelegate; private final CallbackDelegate mCallbackDelegate;
@@ -127,7 +132,9 @@ public final class MediaProjectionManagerService extends SystemService
private final MediaRouterCallback mMediaRouterCallback; private final MediaRouterCallback mMediaRouterCallback;
private MediaRouter.RouteInfo mMediaRouteInfo; private MediaRouter.RouteInfo mMediaRouteInfo;
@GuardedBy("mLock")
private IBinder mProjectionToken; private IBinder mProjectionToken;
@GuardedBy("mLock")
private MediaProjection mProjectionGrant; private MediaProjection mProjectionGrant;
public MediaProjectionManagerService(Context context) { public MediaProjectionManagerService(Context context) {
@@ -314,9 +321,11 @@ public final class MediaProjectionManagerService extends SystemService
*/ */
@VisibleForTesting @VisibleForTesting
boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) { boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession) {
// NEVER lock while calling into WindowManagerService, since WindowManagerService is
// ALWAYS locked when it invokes MediaProjectionManagerService.
final boolean setSessionSucceeded = mWmInternal.setContentRecordingSession(incomingSession);
synchronized (mLock) { synchronized (mLock) {
if (!mWmInternal.setContentRecordingSession( if (!setSessionSucceeded) {
incomingSession)) {
// Unable to start mirroring, so tear down this projection. // Unable to start mirroring, so tear down this projection.
if (mProjectionGrant != null) { if (mProjectionGrant != null) {
mProjectionGrant.stop(); mProjectionGrant.stop();
@@ -359,13 +368,20 @@ public final class MediaProjectionManagerService extends SystemService
*/ */
@VisibleForTesting @VisibleForTesting
void requestConsentForInvalidProjection() { void requestConsentForInvalidProjection() {
Intent reviewConsentIntent;
int uid;
synchronized (mLock) { synchronized (mLock) {
Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection."); reviewConsentIntent = buildReviewGrantedConsentIntentLocked();
// Trigger the permission dialog again in SysUI uid = mProjectionGrant.uid;
// Do not handle the result; SysUI will update us when the user has consented.
mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
UserHandle.getUserHandleForUid(mProjectionGrant.uid));
} }
// NEVER lock while calling into a method that eventually acquires the WindowManagerService
// lock, since WindowManagerService is ALWAYS locked when it invokes
// MediaProjectionManagerService.
Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
// Trigger the permission dialog again in SysUI
// Do not handle the result; SysUI will update us when the user has consented.
mContext.startActivityAsUser(reviewConsentIntent,
UserHandle.getUserHandleForUid(uid));
} }
/** /**
@@ -375,7 +391,7 @@ public final class MediaProjectionManagerService extends SystemService
* <p>Consent dialog result handled in * <p>Consent dialog result handled in
* {@link BinderService#setUserReviewGrantedConsentResult(int)}. * {@link BinderService#setUserReviewGrantedConsentResult(int)}.
*/ */
private Intent buildReviewGrantedConsentIntent() { private Intent buildReviewGrantedConsentIntentLocked() {
final String permissionDialogString = mContext.getResources().getString( final String permissionDialogString = mContext.getResources().getString(
R.string.config_mediaProjectionPermissionDialogComponent); R.string.config_mediaProjectionPermissionDialogComponent);
final ComponentName mediaProjectionPermissionDialogComponent = final ComponentName mediaProjectionPermissionDialogComponent =
@@ -388,7 +404,8 @@ public final class MediaProjectionManagerService extends SystemService
} }
/** /**
* Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}. * Handles result of dialog shown from
* {@link BinderService#buildReviewGrantedConsentIntentLocked()}.
* *
* <p>Tears down session if user did not consent, or starts mirroring if user did consent. * <p>Tears down session if user did not consent, or starts mirroring if user did consent.
*/ */
@@ -490,23 +507,26 @@ public final class MediaProjectionManagerService extends SystemService
MediaProjection getProjectionInternal(int uid, String packageName) { MediaProjection getProjectionInternal(int uid, String packageName) {
final long callingToken = Binder.clearCallingIdentity(); final long callingToken = Binder.clearCallingIdentity();
try { try {
// Supposedly the package has re-used the user's consent; confirm the provided details synchronized (mLock) {
// against the current projection token before re-using the current projection. // Supposedly the package has re-used the user's consent; confirm the provided
if (mProjectionGrant == null || mProjectionGrant.mSession == null // details against the current projection token before re-using the current
|| !mProjectionGrant.mSession.isWaitingForConsent()) { // projection.
Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " if (mProjectionGrant == null || mProjectionGrant.mSession == null
+ "instance"); || !mProjectionGrant.mSession.isWaitingForConsent()) {
return null; Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
} + "instance");
return null;
}
// The package matches, go ahead and re-use the token for this request. // The package matches, go ahead and re-use the token for this request.
if (mProjectionGrant.uid == uid if (mProjectionGrant.uid == uid
&& Objects.equals(mProjectionGrant.packageName, packageName)) { && Objects.equals(mProjectionGrant.packageName, packageName)) {
Slog.v(TAG, "Reusing token: getProjection can reuse the current projection"); Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
return mProjectionGrant; return mProjectionGrant;
} else { } else {
Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ "instance due to package details mismatching"); + "instance due to package details mismatching");
return null; return null;
}
} }
} finally { } finally {
Binder.restoreCallingIdentity(callingToken); Binder.restoreCallingIdentity(callingToken);
@@ -626,8 +646,10 @@ public final class MediaProjectionManagerService extends SystemService
} }
final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity();
try { try {
if (mProjectionGrant != null) { synchronized (mLock) {
mProjectionGrant.stop(); if (mProjectionGrant != null) {
mProjectionGrant.stop();
}
} }
} finally { } finally {
Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token);
@@ -641,13 +663,17 @@ public final class MediaProjectionManagerService extends SystemService
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+ "on captured content resize"); + "on captured content resize");
} }
if (!isCurrentProjection(mProjectionGrant)) { synchronized (mLock) {
return; if (!isCurrentProjection(mProjectionGrant)) {
return;
}
} }
final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity();
try { try {
if (mProjectionGrant != null && mCallbackDelegate != null) { synchronized (mLock) {
mCallbackDelegate.dispatchResize(mProjectionGrant, width, height); if (mProjectionGrant != null && mCallbackDelegate != null) {
mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
}
} }
} finally { } finally {
Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token);
@@ -661,13 +687,17 @@ public final class MediaProjectionManagerService extends SystemService
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify "
+ "on captured content visibility changed"); + "on captured content visibility changed");
} }
if (!isCurrentProjection(mProjectionGrant)) { synchronized (mLock) {
return; if (!isCurrentProjection(mProjectionGrant)) {
return;
}
} }
final long token = Binder.clearCallingIdentity(); final long token = Binder.clearCallingIdentity();
try { try {
if (mProjectionGrant != null && mCallbackDelegate != null) { synchronized (mLock) {
mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible); if (mProjectionGrant != null && mCallbackDelegate != null) {
mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
}
} }
} finally { } finally {
Binder.restoreCallingIdentity(token); Binder.restoreCallingIdentity(token);
@@ -712,9 +742,11 @@ public final class MediaProjectionManagerService extends SystemService
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session " throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session "
+ "details."); + "details.");
} }
if (!isCurrentProjection(projection)) { synchronized (mLock) {
throw new SecurityException("Unable to set ContentRecordingSession on " if (!isCurrentProjection(projection)) {
+ "non-current MediaProjection"); throw new SecurityException("Unable to set ContentRecordingSession on "
+ "non-current MediaProjection");
}
} }
final long origId = Binder.clearCallingIdentity(); final long origId = Binder.clearCallingIdentity();
try { try {
@@ -732,10 +764,12 @@ public final class MediaProjectionManagerService extends SystemService
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given"
+ "projection is valid."); + "projection is valid.");
} }
if (!isCurrentProjection(projection)) { synchronized (mLock) {
Slog.v(TAG, "Reusing token: Won't request consent again for a token that " if (!isCurrentProjection(projection)) {
+ "isn't current"); Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
return; + "isn't current");
return;
}
} }
// Remove calling app identity before performing any privileged operations. // Remove calling app identity before performing any privileged operations.

View File

@@ -0,0 +1,30 @@
# MediaProjection
## Locking model
`MediaProjectionManagerService` needs to have consistent lock ordering with its interactions with
`WindowManagerService` to prevent deadlock.
### TLDR
`MediaProjectionManagerService` must lock when updating its own fields.
Calls must follow the below invocation order while holding locks:
`WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService`
### Justification
`MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling
each invocation, `WindowManagerService` acquires its own lock:
* setting a `ContentRecordingSession`
* starting a new `MediaProjection` recording session through
`MediaProjection#createVirtualDisplay`
* indicating the user has granted consent to reuse the consent token
`WindowManagerService` calls into `MediaProjectionManagerService`, always while holding
`WindowManagerGlobalLock`:
* `ContentRecorder` handling various events such as resizing recorded content
Since `WindowManagerService -> MediaProjectionManagerService` is guaranteed to always hold the
`WindowManagerService` lock, we must ensure that `MediaProjectionManagerService ->
WindowManagerService` is NEVER holding the `MediaProjectionManagerService` lock.