Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b1a35994 authored by Naomi Musgrave's avatar Naomi Musgrave
Browse files

[MediaProjection][Security] Consistent locking on token

Fix deadlock in MediaProjectionManagerService by ensuring
MediaProjectionManagerService never holds the lock when invoking a
method that will acquire the WindowManagerService lock.

Bug: 261168256
Test: Manual
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:e4797c76888a2e6d896f4d54d0893ec9513d099e)
Merged-In: I12506081e1d173e8210ff29ef3992c3bfcf82841
Change-Id: I12506081e1d173e8210ff29ef3992c3bfcf82841
parent 83c3b51e
Loading
Loading
Loading
Loading
+78 −44
Original line number Original line Diff line number Diff line
@@ -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) {
@@ -311,9 +318,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();
@@ -356,13 +365,20 @@ public final class MediaProjectionManagerService extends SystemService
     */
     */
    @VisibleForTesting
    @VisibleForTesting
    void requestConsentForInvalidProjection() {
    void requestConsentForInvalidProjection() {
        Intent reviewConsentIntent;
        int uid;
        synchronized (mLock) {
        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.");
        Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
        // Trigger the permission dialog again in SysUI
        // Trigger the permission dialog again in SysUI
        // Do not handle the result; SysUI will update us when the user has consented.
        // Do not handle the result; SysUI will update us when the user has consented.
            mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
        mContext.startActivityAsUser(reviewConsentIntent,
                    UserHandle.getUserHandleForUid(mProjectionGrant.uid));
                UserHandle.getUserHandleForUid(uid));
        }
    }
    }


    /**
    /**
@@ -372,7 +388,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 =
@@ -385,7 +401,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.
     */
     */
@@ -487,8 +504,10 @@ 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
                // details against the current projection token before re-using the current
                // projection.
                if (mProjectionGrant == null || mProjectionGrant.mSession == null
                if (mProjectionGrant == null || mProjectionGrant.mSession == null
                        || !mProjectionGrant.mSession.isWaitingForConsent()) {
                        || !mProjectionGrant.mSession.isWaitingForConsent()) {
                    Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
                    Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
@@ -505,6 +524,7 @@ public final class MediaProjectionManagerService extends SystemService
                            + "instance due to package details mismatching");
                            + "instance due to package details mismatching");
                    return null;
                    return null;
                }
                }
            }
        } finally {
        } finally {
            Binder.restoreCallingIdentity(callingToken);
            Binder.restoreCallingIdentity(callingToken);
        }
        }
@@ -617,9 +637,11 @@ public final class MediaProjectionManagerService extends SystemService
            stopActiveProjection_enforcePermission();
            stopActiveProjection_enforcePermission();
            final long token = Binder.clearCallingIdentity();
            final long token = Binder.clearCallingIdentity();
            try {
            try {
                synchronized (mLock) {
                    if (mProjectionGrant != null) {
                    if (mProjectionGrant != null) {
                        mProjectionGrant.stop();
                        mProjectionGrant.stop();
                    }
                    }
                }
            } finally {
            } finally {
                Binder.restoreCallingIdentity(token);
                Binder.restoreCallingIdentity(token);
            }
            }
@@ -629,14 +651,18 @@ public final class MediaProjectionManagerService extends SystemService
        @Override // Binder call
        @Override // Binder call
        public void notifyActiveProjectionCapturedContentResized(int width, int height) {
        public void notifyActiveProjectionCapturedContentResized(int width, int height) {
            notifyActiveProjectionCapturedContentResized_enforcePermission();
            notifyActiveProjectionCapturedContentResized_enforcePermission();
            synchronized (mLock) {
                if (!isCurrentProjection(mProjectionGrant)) {
                if (!isCurrentProjection(mProjectionGrant)) {
                    return;
                    return;
                }
                }
            }
            final long token = Binder.clearCallingIdentity();
            final long token = Binder.clearCallingIdentity();
            try {
            try {
                synchronized (mLock) {
                    if (mProjectionGrant != null && mCallbackDelegate != null) {
                    if (mProjectionGrant != null && mCallbackDelegate != null) {
                        mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
                        mCallbackDelegate.dispatchResize(mProjectionGrant, width, height);
                    }
                    }
                }
            } finally {
            } finally {
                Binder.restoreCallingIdentity(token);
                Binder.restoreCallingIdentity(token);
            }
            }
@@ -646,14 +672,18 @@ public final class MediaProjectionManagerService extends SystemService
        @Override
        @Override
        public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) {
        public void notifyActiveProjectionCapturedContentVisibilityChanged(boolean isVisible) {
            notifyActiveProjectionCapturedContentVisibilityChanged_enforcePermission();
            notifyActiveProjectionCapturedContentVisibilityChanged_enforcePermission();
            synchronized (mLock) {
                if (!isCurrentProjection(mProjectionGrant)) {
                if (!isCurrentProjection(mProjectionGrant)) {
                    return;
                    return;
                }
                }
            }
            final long token = Binder.clearCallingIdentity();
            final long token = Binder.clearCallingIdentity();
            try {
            try {
                synchronized (mLock) {
                    if (mProjectionGrant != null && mCallbackDelegate != null) {
                    if (mProjectionGrant != null && mCallbackDelegate != null) {
                        mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
                        mCallbackDelegate.dispatchVisibilityChanged(mProjectionGrant, isVisible);
                    }
                    }
                }
            } finally {
            } finally {
                Binder.restoreCallingIdentity(token);
                Binder.restoreCallingIdentity(token);
            }
            }
@@ -694,10 +724,12 @@ public final class MediaProjectionManagerService extends SystemService
        public boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession,
        public boolean setContentRecordingSession(@Nullable ContentRecordingSession incomingSession,
                @NonNull IMediaProjection projection) {
                @NonNull IMediaProjection projection) {
            setContentRecordingSession_enforcePermission();
            setContentRecordingSession_enforcePermission();
            synchronized (mLock) {
                if (!isCurrentProjection(projection)) {
                if (!isCurrentProjection(projection)) {
                    throw new SecurityException("Unable to set ContentRecordingSession on "
                    throw new SecurityException("Unable to set ContentRecordingSession on "
                            + "non-current MediaProjection");
                            + "non-current MediaProjection");
                }
                }
            }
            final long origId = Binder.clearCallingIdentity();
            final long origId = Binder.clearCallingIdentity();
            try {
            try {
                return MediaProjectionManagerService.this.setContentRecordingSession(
                return MediaProjectionManagerService.this.setContentRecordingSession(
@@ -711,11 +743,13 @@ public final class MediaProjectionManagerService extends SystemService
        @Override
        @Override
        public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
        public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
            requestConsentForInvalidProjection_enforcePermission();
            requestConsentForInvalidProjection_enforcePermission();
            synchronized (mLock) {
                if (!isCurrentProjection(projection)) {
                if (!isCurrentProjection(projection)) {
                    Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
                    Slog.v(TAG, "Reusing token: Won't request consent again for a token that "
                            + "isn't current");
                            + "isn't current");
                    return;
                    return;
                }
                }
            }


            // Remove calling app identity before performing any privileged operations.
            // Remove calling app identity before performing any privileged operations.
            final long token = Binder.clearCallingIdentity();
            final long token = Binder.clearCallingIdentity();
+30 −0
Original line number Original line 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.