Skip to content

Commit b2fbe47

Browse files
committed
binder: Cancel pending authorizations upon transport termination
Fixes #12929 TAG=agy
1 parent aa1d11e commit b2fbe47

5 files changed

Lines changed: 100 additions & 5 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ void notifyShutdown(Status status) {
127127
// Nothing to do.
128128
}
129129

130+
@Override
131+
void notifyTerminatedUnlocked() {
132+
BinderTransportSecurity.notifyTerminatedUnlocked(getAttributes());
133+
}
134+
130135
@Override
131136
@GuardedBy("this")
132137
void notifyTerminated() {

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ final boolean isReady() {
251251
@GuardedBy("this")
252252
abstract void notifyTerminated();
253253

254+
void notifyTerminatedUnlocked() {}
255+
254256
void releaseExecutors() {
255257
executorServicePool.returnObject(scheduledExecutorService);
256258
}
@@ -335,6 +337,8 @@ final void shutdownInternal(Status shutdownStatus, boolean forceTerminate) {
335337
future.cancel(false); // No effect if already isDone().
336338
}
337339

340+
notifyTerminatedUnlocked();
341+
338342
synchronized (this) {
339343
notifyTerminated();
340344
}

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ public static void attachAuthAttrs(
8787
.set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY);
8888
}
8989

90+
/**
91+
* Informs this module that the transport with the specified 'attributes' is terminating.
92+
*
93+
* <p>Any resources allocated by this module will be released and no further resources will be
94+
* allocated. Any ongoing background work will be canceled and no further background work will be
95+
* initiated.
96+
*
97+
* <p>This method completes futures and therefore may execute arbitrary listener code on a
98+
* potentially direct Executor. To avoid deadlock, callers must not hold any locks.
99+
*/
100+
@Internal
101+
public static void notifyTerminatedUnlocked(Attributes attributes) {
102+
TransportAuthorizationState state = attributes.get(TRANSPORT_AUTHORIZATION_STATE);
103+
if (state != null) {
104+
state.notifyTerminatedUnlocked();
105+
}
106+
}
107+
90108
/**
91109
* Intercepts server calls and ensures they're authorized before allowing them to proceed.
92110
* Authentication state is fetched from the call attributes, inherited from the transport.
@@ -173,6 +191,8 @@ static final class TransportAuthorizationState {
173191
private final ConcurrentHashMap<String, ListenableFuture<Status>> serviceAuthorization;
174192
private final Executor executor;
175193

194+
private volatile boolean isTerminated;
195+
176196
/**
177197
* @param executor used for calling into the application. Must outlive the transport.
178198
*/
@@ -203,11 +223,17 @@ ListenableFuture<Status> checkAuthorization(MethodDescriptor<?, ?> method) {
203223
return nonCancellationPropagating(checkThenActRaceWinner);
204224
}
205225

206-
// newPendingAuthResult is visible to other threads at this point but not as a SettableFuture
207-
// and always wrapped with nonCancellationPropagating(). Since it must not be complete, we can
208-
// ignore the result of setFuture() -- it must always succeed.
209-
newPendingAuthResult.setFuture(
210-
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName));
226+
// We only check isTerminated *after* the new future is visible to other threads in
227+
// serviceAuthorization. In case of a race with a simultaneous call to notifyTerminated(),
228+
// better to harmlessly cancel the new future twice rather than not cancel it at all.
229+
if (!isTerminated) {
230+
// If notifyTerminated() already cancelled newPendingAuthResult, setFuture() will forward
231+
// that cancellation to its argument so it's safe to ignore the return value here.
232+
newPendingAuthResult.setFuture(
233+
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName));
234+
} else {
235+
newPendingAuthResult.cancel(false);
236+
}
211237

212238
Futures.addCallback(
213239
newPendingAuthResult,
@@ -233,6 +259,22 @@ public void onFailure(Throwable t) {
233259

234260
return nonCancellationPropagating(newPendingAuthResult);
235261
}
262+
263+
/**
264+
* After this method returns, every future ever returned by a prior or concurrent call to
265+
* checkAuthorization() is guaranteed to be complete. Every future returned by subsequent call
266+
* to checkAuthorization() is also guaranteed to be complete.
267+
*/
268+
void notifyTerminatedUnlocked() {
269+
// Any entries added to serviceAuthorization *after* this assignment will be immediately
270+
// canceled by the adding thread in checkAuthorization().
271+
isTerminated = true;
272+
273+
// Cancel any entries added to serviceAuthorization *before* the volatile assignment above.
274+
for (ListenableFuture<Status> authResult : serviceAuthorization.values()) {
275+
authResult.cancel(false); // No-op for cached results (already done).
276+
}
277+
}
236278
}
237279

238280
/**

binder/src/test/java/io/grpc/binder/RobolectricBinderSecurityTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020
import static com.google.common.truth.Truth.assertThat;
2121
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
22+
import static java.util.concurrent.TimeUnit.SECONDS;
23+
import static org.junit.Assert.fail;
2224
import static org.robolectric.Shadows.shadowOf;
2325

2426
import android.app.Application;
@@ -42,12 +44,16 @@
4244
import io.grpc.ServerMethodDefinition;
4345
import io.grpc.ServerServiceDefinition;
4446
import io.grpc.Status;
47+
import io.grpc.Status.Code;
4548
import io.grpc.StatusRuntimeException;
4649
import io.grpc.protobuf.lite.ProtoLiteUtils;
4750
import io.grpc.stub.ClientCalls;
4851
import io.grpc.stub.ServerCalls;
4952
import java.io.IOException;
5053
import java.util.concurrent.ArrayBlockingQueue;
54+
import java.util.concurrent.CancellationException;
55+
import java.util.concurrent.ExecutionException;
56+
import java.util.concurrent.TimeUnit;
5157
import org.junit.After;
5258
import org.junit.Before;
5359
import org.junit.Test;
@@ -175,6 +181,24 @@ public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Excep
175181
assertThat(status.get().getCode()).isEqualTo(Status.Code.OK);
176182
}
177183

184+
@Test
185+
public void testAsyncServerSecurityPolicy_shutdownNow_cancelsAuthFutures() throws Exception {
186+
ListenableFuture<Status> callResult = makeCall();
187+
SettableFuture<Status> authResultFuture = statusesToSet.take();
188+
189+
channel.shutdownNow();
190+
boolean terminationResult = channel.awaitTermination(10, SECONDS);
191+
assertThat(terminationResult).isTrue();
192+
193+
try {
194+
Status authResult = authResultFuture.get(10, SECONDS);
195+
fail("Expected authResultFuture cancellation but got " + authResult);
196+
} catch (CancellationException expected) {
197+
}
198+
assertThat(authResultFuture.isCancelled()).isTrue();
199+
assertThat(callResult.get().getCode()).isEqualTo(Status.Code.UNAVAILABLE);
200+
}
201+
178202
private ListenableFuture<Status> makeCall() {
179203
ClientCall<Empty, Empty> call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT);
180204
ListenableFuture<Empty> responseFuture =

binder/src/test/java/io/grpc/binder/internal/TransportAuthorizationStateTest.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,26 @@ public void checkAuthorization_failedFuture_notCached() throws Exception {
169169
assertThat(authResult2.get()).isEqualTo(Status.OK);
170170
}
171171

172+
@Test
173+
public void notifyTerminatedUnlocked_cancelsPendingAuthFutures() {
174+
ListenableFuture<Status> authResult = authState.checkAuthorization(CODEGEN_METHOD);
175+
assertThat(authResult.isDone()).isFalse();
176+
177+
authState.notifyTerminatedUnlocked();
178+
179+
assertThat(authResult.isCancelled()).isTrue();
180+
}
181+
182+
@Test
183+
public void checkAuthorization_afterTermination_returnsCancelledFuture() {
184+
authState.notifyTerminatedUnlocked();
185+
186+
ListenableFuture<Status> authResult = authState.checkAuthorization(CODEGEN_METHOD);
187+
188+
assertThat(authResult.isCancelled()).isTrue();
189+
assertThat(fakePolicyChecker.statusesToSet).isEmpty(); // fakePolicyChecker never called.
190+
}
191+
172192
private static final class FakeServerPolicyChecker implements ServerPolicyChecker {
173193
final LinkedBlockingQueue<SettableFuture<Status>> statusesToSet = new LinkedBlockingQueue<>();
174194

0 commit comments

Comments
 (0)