Loading services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +78 −44 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ import android.util.Slog; import android.view.ContentRecordingSession; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; Loading Loading @@ -111,7 +112,11 @@ public final class MediaProjectionManagerService extends SystemService @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 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 CallbackDelegate mCallbackDelegate; Loading @@ -127,7 +132,9 @@ public final class MediaProjectionManagerService extends SystemService private final MediaRouterCallback mMediaRouterCallback; private MediaRouter.RouteInfo mMediaRouteInfo; @GuardedBy("mLock") private IBinder mProjectionToken; @GuardedBy("mLock") private MediaProjection mProjectionGrant; public MediaProjectionManagerService(Context context) { Loading Loading @@ -314,9 +321,11 @@ public final class MediaProjectionManagerService extends SystemService */ @VisibleForTesting 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) { if (!mWmInternal.setContentRecordingSession( incomingSession)) { if (!setSessionSucceeded) { // Unable to start mirroring, so tear down this projection. if (mProjectionGrant != null) { mProjectionGrant.stop(); Loading Loading @@ -359,13 +368,20 @@ public final class MediaProjectionManagerService extends SystemService */ @VisibleForTesting void requestConsentForInvalidProjection() { Intent reviewConsentIntent; int uid; synchronized (mLock) { reviewConsentIntent = buildReviewGrantedConsentIntentLocked(); uid = 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(buildReviewGrantedConsentIntent(), UserHandle.getUserHandleForUid(mProjectionGrant.uid)); } mContext.startActivityAsUser(reviewConsentIntent, UserHandle.getUserHandleForUid(uid)); } /** Loading @@ -375,7 +391,7 @@ public final class MediaProjectionManagerService extends SystemService * <p>Consent dialog result handled in * {@link BinderService#setUserReviewGrantedConsentResult(int)}. */ private Intent buildReviewGrantedConsentIntent() { private Intent buildReviewGrantedConsentIntentLocked() { final String permissionDialogString = mContext.getResources().getString( R.string.config_mediaProjectionPermissionDialogComponent); final ComponentName mediaProjectionPermissionDialogComponent = Loading @@ -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. */ Loading Loading @@ -490,8 +507,10 @@ public final class MediaProjectionManagerService extends SystemService MediaProjection getProjectionInternal(int uid, String packageName) { final long callingToken = Binder.clearCallingIdentity(); try { // Supposedly the package has re-used the user's consent; confirm the provided details // against the current projection token before re-using the current projection. synchronized (mLock) { // Supposedly the package has re-used the user's consent; confirm the provided // details against the current projection token before re-using the current // projection. if (mProjectionGrant == null || mProjectionGrant.mSession == null || !mProjectionGrant.mSession.isWaitingForConsent()) { Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " Loading @@ -508,6 +527,7 @@ public final class MediaProjectionManagerService extends SystemService + "instance due to package details mismatching"); return null; } } } finally { Binder.restoreCallingIdentity(callingToken); } Loading Loading @@ -626,9 +646,11 @@ public final class MediaProjectionManagerService extends SystemService } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null) { mProjectionGrant.stop(); } } } finally { Binder.restoreCallingIdentity(token); } Loading @@ -641,14 +663,18 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content resize"); } synchronized (mLock) { if (!isCurrentProjection(mProjectionGrant)) { return; } } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null && mCallbackDelegate != null) { mCallbackDelegate.dispatchResize(mProjectionGrant, width, height); } } } finally { Binder.restoreCallingIdentity(token); } Loading @@ -661,14 +687,18 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content visibility changed"); } synchronized (mLock) { if (!isCurrentProjection(mProjectionGrant)) { return; } } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null && mCallbackDelegate != null) { mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible); } } } finally { Binder.restoreCallingIdentity(token); } Loading Loading @@ -712,10 +742,12 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session " + "details."); } synchronized (mLock) { if (!isCurrentProjection(projection)) { throw new SecurityException("Unable to set ContentRecordingSession on " + "non-current MediaProjection"); } } final long origId = Binder.clearCallingIdentity(); try { return MediaProjectionManagerService.this.setContentRecordingSession( Loading @@ -732,11 +764,13 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" + "projection is valid."); } synchronized (mLock) { if (!isCurrentProjection(projection)) { Slog.v(TAG, "Reusing token: Won't request consent again for a token that " + "isn't current"); return; } } // Remove calling app identity before performing any privileged operations. final long token = Binder.clearCallingIdentity(); Loading services/core/java/com/android/server/media/projection/mediaprojection.md 0 → 100644 +30 −0 Original line number Diff line number Diff line # 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. Loading
services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +78 −44 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ import android.util.Slog; import android.view.ContentRecordingSession; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; Loading Loading @@ -111,7 +112,11 @@ public final class MediaProjectionManagerService extends SystemService @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) 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 CallbackDelegate mCallbackDelegate; Loading @@ -127,7 +132,9 @@ public final class MediaProjectionManagerService extends SystemService private final MediaRouterCallback mMediaRouterCallback; private MediaRouter.RouteInfo mMediaRouteInfo; @GuardedBy("mLock") private IBinder mProjectionToken; @GuardedBy("mLock") private MediaProjection mProjectionGrant; public MediaProjectionManagerService(Context context) { Loading Loading @@ -314,9 +321,11 @@ public final class MediaProjectionManagerService extends SystemService */ @VisibleForTesting 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) { if (!mWmInternal.setContentRecordingSession( incomingSession)) { if (!setSessionSucceeded) { // Unable to start mirroring, so tear down this projection. if (mProjectionGrant != null) { mProjectionGrant.stop(); Loading Loading @@ -359,13 +368,20 @@ public final class MediaProjectionManagerService extends SystemService */ @VisibleForTesting void requestConsentForInvalidProjection() { Intent reviewConsentIntent; int uid; synchronized (mLock) { reviewConsentIntent = buildReviewGrantedConsentIntentLocked(); uid = 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(buildReviewGrantedConsentIntent(), UserHandle.getUserHandleForUid(mProjectionGrant.uid)); } mContext.startActivityAsUser(reviewConsentIntent, UserHandle.getUserHandleForUid(uid)); } /** Loading @@ -375,7 +391,7 @@ public final class MediaProjectionManagerService extends SystemService * <p>Consent dialog result handled in * {@link BinderService#setUserReviewGrantedConsentResult(int)}. */ private Intent buildReviewGrantedConsentIntent() { private Intent buildReviewGrantedConsentIntentLocked() { final String permissionDialogString = mContext.getResources().getString( R.string.config_mediaProjectionPermissionDialogComponent); final ComponentName mediaProjectionPermissionDialogComponent = Loading @@ -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. */ Loading Loading @@ -490,8 +507,10 @@ public final class MediaProjectionManagerService extends SystemService MediaProjection getProjectionInternal(int uid, String packageName) { final long callingToken = Binder.clearCallingIdentity(); try { // Supposedly the package has re-used the user's consent; confirm the provided details // against the current projection token before re-using the current projection. synchronized (mLock) { // Supposedly the package has re-used the user's consent; confirm the provided // details against the current projection token before re-using the current // projection. if (mProjectionGrant == null || mProjectionGrant.mSession == null || !mProjectionGrant.mSession.isWaitingForConsent()) { Slog.e(TAG, "Reusing token: Not possible to reuse the current projection " Loading @@ -508,6 +527,7 @@ public final class MediaProjectionManagerService extends SystemService + "instance due to package details mismatching"); return null; } } } finally { Binder.restoreCallingIdentity(callingToken); } Loading Loading @@ -626,9 +646,11 @@ public final class MediaProjectionManagerService extends SystemService } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null) { mProjectionGrant.stop(); } } } finally { Binder.restoreCallingIdentity(token); } Loading @@ -641,14 +663,18 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content resize"); } synchronized (mLock) { if (!isCurrentProjection(mProjectionGrant)) { return; } } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null && mCallbackDelegate != null) { mCallbackDelegate.dispatchResize(mProjectionGrant, width, height); } } } finally { Binder.restoreCallingIdentity(token); } Loading @@ -661,14 +687,18 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION in order to notify " + "on captured content visibility changed"); } synchronized (mLock) { if (!isCurrentProjection(mProjectionGrant)) { return; } } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { if (mProjectionGrant != null && mCallbackDelegate != null) { mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible); } } } finally { Binder.restoreCallingIdentity(token); } Loading Loading @@ -712,10 +742,12 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to set session " + "details."); } synchronized (mLock) { if (!isCurrentProjection(projection)) { throw new SecurityException("Unable to set ContentRecordingSession on " + "non-current MediaProjection"); } } final long origId = Binder.clearCallingIdentity(); try { return MediaProjectionManagerService.this.setContentRecordingSession( Loading @@ -732,11 +764,13 @@ public final class MediaProjectionManagerService extends SystemService throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given" + "projection is valid."); } synchronized (mLock) { if (!isCurrentProjection(projection)) { Slog.v(TAG, "Reusing token: Won't request consent again for a token that " + "isn't current"); return; } } // Remove calling app identity before performing any privileged operations. final long token = Binder.clearCallingIdentity(); Loading
services/core/java/com/android/server/media/projection/mediaprojection.md 0 → 100644 +30 −0 Original line number Diff line number Diff line # 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.