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:
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user