Skip to content

Commit 30d40a6

Browse files
authored
binder: Cancel checkAuthorization() request if still pending upon termination (#12167)
1 parent 9a6bdc7 commit 30d40a6

File tree

3 files changed

+120
-35
lines changed

3 files changed

+120
-35
lines changed

binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,14 @@
1717
package io.grpc.binder.internal;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static java.util.concurrent.TimeUnit.SECONDS;
2021

2122
import android.content.Context;
2223
import android.os.DeadObjectException;
2324
import android.os.Parcel;
2425
import android.os.RemoteException;
2526
import androidx.test.core.app.ApplicationProvider;
2627
import androidx.test.ext.junit.runners.AndroidJUnit4;
27-
import com.google.common.util.concurrent.Futures;
28-
import com.google.common.util.concurrent.ListenableFuture;
2928
import com.google.common.util.concurrent.SettableFuture;
3029
import com.google.errorprone.annotations.CanIgnoreReturnValue;
3130
import com.google.errorprone.annotations.concurrent.GuardedBy;
@@ -39,13 +38,13 @@
3938
import io.grpc.Status;
4039
import io.grpc.Status.Code;
4140
import io.grpc.binder.AndroidComponentAddress;
42-
import io.grpc.binder.AsyncSecurityPolicy;
4341
import io.grpc.binder.BinderServerBuilder;
4442
import io.grpc.binder.HostServices;
4543
import io.grpc.binder.SecurityPolicy;
4644
import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy;
4745
import io.grpc.binder.internal.OneWayBinderProxies.BlockingBinderDecorator;
4846
import io.grpc.binder.internal.OneWayBinderProxies.ThrowingOneWayBinderProxy;
47+
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
4948
import io.grpc.internal.ClientStream;
5049
import io.grpc.internal.ClientStreamListener;
5150
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
@@ -64,7 +63,6 @@
6463
import java.util.concurrent.Executors;
6564
import java.util.concurrent.LinkedBlockingQueue;
6665
import java.util.concurrent.ScheduledExecutorService;
67-
import java.util.concurrent.TimeUnit;
6866
import javax.annotation.Nullable;
6967
import org.junit.After;
7068
import org.junit.Before;
@@ -193,7 +191,7 @@ public void tearDown() throws Exception {
193191
private static void shutdownAndTerminate(ExecutorService executorService)
194192
throws InterruptedException {
195193
executorService.shutdownNow();
196-
if (!executorService.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
194+
if (!executorService.awaitTermination(TIMEOUT_SECONDS, SECONDS)) {
197195
throw new AssertionError("executor failed to terminate promptly");
198196
}
199197
}
@@ -375,26 +373,32 @@ public void testBlackHoleEndpointConnectTimeout() throws Exception {
375373

376374
@Test
377375
public void testBlackHoleSecurityPolicyConnectTimeout() throws Exception {
376+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
378377
transport =
379378
new BinderClientTransportBuilder()
380-
.setSecurityPolicy(blockingSecurityPolicy)
379+
.setSecurityPolicy(securityPolicy)
381380
.setReadyTimeoutMillis(1_234)
382381
.build();
383382
transport.start(transportListener).run();
383+
// Take the next authRequest but don't respond to it, in order to trigger the ready timeout.
384+
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
385+
384386
Status transportStatus = transportListener.awaitShutdown();
385387
assertThat(transportStatus.getCode()).isEqualTo(Code.DEADLINE_EXCEEDED);
386388
assertThat(transportStatus.getDescription()).contains("1234");
387389
transportListener.awaitTermination();
388-
blockingSecurityPolicy.provideNextCheckAuthorizationResult(Status.OK);
390+
391+
// If the transport gave up waiting on auth, it should cancel its request.
392+
assertThat(authRequest.isCancelled()).isTrue();
389393
}
390394

391395
@Test
392396
public void testAsyncSecurityPolicyFailure() throws Exception {
393397
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
394398
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
395399
RuntimeException exception = new NullPointerException();
396-
securityPolicy.setAuthorizationException(exception);
397400
transport.start(transportListener).run();
401+
securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS).setResult(exception);
398402
Status transportStatus = transportListener.awaitShutdown();
399403
assertThat(transportStatus.getCode()).isEqualTo(Code.INTERNAL);
400404
assertThat(transportStatus.getCause()).isEqualTo(exception);
@@ -405,13 +409,27 @@ public void testAsyncSecurityPolicyFailure() throws Exception {
405409
public void testAsyncSecurityPolicySuccess() throws Exception {
406410
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
407411
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
408-
securityPolicy.setAuthorizationResult(Status.PERMISSION_DENIED);
409412
transport.start(transportListener).run();
413+
securityPolicy
414+
.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS)
415+
.setResult(Status.PERMISSION_DENIED);
410416
Status transportStatus = transportListener.awaitShutdown();
411417
assertThat(transportStatus.getCode()).isEqualTo(Code.PERMISSION_DENIED);
412418
transportListener.awaitTermination();
413419
}
414420

421+
@Test
422+
public void testAsyncSecurityPolicyCancelledUponExternalTermination() throws Exception {
423+
SettableAsyncSecurityPolicy securityPolicy = new SettableAsyncSecurityPolicy();
424+
transport = new BinderClientTransportBuilder().setSecurityPolicy(securityPolicy).build();
425+
transport.start(transportListener).run();
426+
AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_SECONDS, SECONDS);
427+
transport.shutdownNow(Status.UNAVAILABLE); // 'authRequest' remains unanswered!
428+
transportListener.awaitShutdown();
429+
transportListener.awaitTermination();
430+
assertThat(authRequest.isCancelled()).isTrue();
431+
}
432+
415433
private static void startAndAwaitReady(
416434
BinderTransport.BinderClientTransport transport, TestTransportListener transportListener)
417435
throws Exception {
@@ -433,7 +451,7 @@ public void transportShutdown(Status shutdownStatus) {
433451
}
434452

435453
public Status awaitShutdown() throws Exception {
436-
return shutdownStatus.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
454+
return shutdownStatus.get(TIMEOUT_SECONDS, SECONDS);
437455
}
438456

439457
@Override
@@ -444,7 +462,7 @@ public void transportTerminated() {
444462
}
445463

446464
public void awaitTermination() throws Exception {
447-
isTerminated.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
465+
isTerminated.get(TIMEOUT_SECONDS, SECONDS);
448466
}
449467

450468
@Override
@@ -455,7 +473,7 @@ public void transportReady() {
455473
}
456474

457475
public void awaitReady() throws Exception {
458-
isReady.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
476+
isReady.get(TIMEOUT_SECONDS, SECONDS);
459477
}
460478

461479
@Override
@@ -571,25 +589,4 @@ public Status checkAuthorization(int uid) {
571589
}
572590
}
573591
}
574-
575-
/** An AsyncSecurityPolicy that lets a test specify the outcome of checkAuthorizationAsync(). */
576-
static class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy {
577-
private SettableFuture<Status> result = SettableFuture.create();
578-
579-
public void clearAuthorizationResult() {
580-
result = SettableFuture.create();
581-
}
582-
583-
public boolean setAuthorizationResult(Status status) {
584-
return result.set(status);
585-
}
586-
587-
public boolean setAuthorizationException(Throwable t) {
588-
return result.setException(t);
589-
}
590-
591-
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
592-
return Futures.nonCancellationPropagating(result);
593-
}
594-
}
595592
}

binder/src/main/java/io/grpc/binder/internal/BinderTransport.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,8 @@ public static final class BinderClientTransport extends BinderTransport
582582

583583
@GuardedBy("this")
584584
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
585+
@GuardedBy("this")
586+
@Nullable private ListenableFuture<Status> authResultFuture; // null before we check auth.
585587

586588
/**
587589
* Constructs a new transport instance.
@@ -756,6 +758,9 @@ void notifyTerminated() {
756758
readyTimeoutFuture.cancel(false);
757759
readyTimeoutFuture = null;
758760
}
761+
if (authResultFuture != null) {
762+
authResultFuture.cancel(false); // No effect if already complete.
763+
}
759764
serviceBinding.unbind();
760765
clientTransportListener.transportTerminated();
761766
}
@@ -775,13 +780,13 @@ protected void handleSetupTransport(Parcel parcel) {
775780
shutdownInternal(
776781
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
777782
} else {
778-
ListenableFuture<Status> authFuture =
783+
authResultFuture =
779784
(securityPolicy instanceof AsyncSecurityPolicy)
780785
? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid)
781786
: Futures.submit(
782787
() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
783788
Futures.addCallback(
784-
authFuture,
789+
authResultFuture,
785790
new FutureCallback<Status>() {
786791
@Override
787792
public void onSuccess(Status result) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2025 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.binder.internal;
18+
19+
import static com.google.common.base.Preconditions.checkState;
20+
21+
import com.google.common.util.concurrent.ListenableFuture;
22+
import com.google.common.util.concurrent.SettableFuture;
23+
import io.grpc.Status;
24+
import io.grpc.binder.AsyncSecurityPolicy;
25+
import java.util.concurrent.LinkedBlockingDeque;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.concurrent.TimeoutException;
28+
29+
/**
30+
* An {@link AsyncSecurityPolicy} that lets unit tests verify the exact order of authorization
31+
* requests and respond to them one at a time.
32+
*/
33+
public class SettableAsyncSecurityPolicy extends AsyncSecurityPolicy {
34+
private final LinkedBlockingDeque<AuthRequest> pendingRequests = new LinkedBlockingDeque<>();
35+
36+
@Override
37+
public ListenableFuture<Status> checkAuthorizationAsync(int uid) {
38+
AuthRequest request = new AuthRequest(uid);
39+
pendingRequests.add(request);
40+
return request.resultFuture;
41+
}
42+
43+
/**
44+
* Waits for the next "check authorization" request to be made and returns it, throwing in case no
45+
* request arrives in time.
46+
*/
47+
public AuthRequest takeNextAuthRequest(long timeout, TimeUnit unit)
48+
throws InterruptedException, TimeoutException {
49+
AuthRequest nextAuthRequest = pendingRequests.poll(timeout, unit);
50+
if (nextAuthRequest == null) {
51+
throw new TimeoutException();
52+
}
53+
return nextAuthRequest;
54+
}
55+
56+
/** Represents a single call to {@link AsyncSecurityPolicy#checkAuthorizationAsync(int)}. */
57+
public static class AuthRequest {
58+
59+
/** The argument passed to {@link AsyncSecurityPolicy#checkAuthorizationAsync(int)}. */
60+
public final int uid;
61+
62+
private final SettableFuture<Status> resultFuture = SettableFuture.create();
63+
64+
private AuthRequest(int uid) {
65+
this.uid = uid;
66+
}
67+
68+
/** Provides this SecurityPolicy's response to this authorization request. */
69+
public void setResult(Status result) {
70+
checkState(resultFuture.set(result));
71+
}
72+
73+
/** Simulates an exceptional response to this authorization request. */
74+
public void setResult(Throwable t) {
75+
checkState(resultFuture.setException(t));
76+
}
77+
78+
/** Tests if the future returned for this authorization request was cancelled by the caller. */
79+
public boolean isCancelled() {
80+
return resultFuture.isCancelled();
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)