Skip to content

Commit 21328cf

Browse files
committed
binder: Cancel pending authorizations upon transport termination
Fixes #12929
1 parent f5dc95d commit 21328cf

5 files changed

Lines changed: 96 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: 49 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.
@@ -174,6 +192,8 @@ static final class TransportAuthorizationState {
174192
private final ConcurrentHashMap<String, ListenableFuture<Status>> serviceAuthorization;
175193
private final Executor executor;
176194

195+
private volatile boolean isTerminated;
196+
177197
/**
178198
* @param executor used for calling into the application. Must outlive the transport.
179199
*/
@@ -204,11 +224,20 @@ ListenableFuture<Status> checkAuthorization(MethodDescriptor<?, ?> method) {
204224
return nonCancellationPropagating(checkThenActRaceWinner);
205225
}
206226

207-
try {
208-
newPendingAuthResult.setFuture(
209-
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName));
210-
} catch (Exception e) { // Not just RuntimeException! Handle the "sneaky" checked case too.
211-
newPendingAuthResult.setException(e);
227+
// We only check isTerminated *after* the new future is visible to other threads in
228+
// serviceAuthorization. In case of a race with a simultaneous call to notifyTerminated(),
229+
// better to harmlessly cancel the new future twice rather than not cancel it at all.
230+
if (!isTerminated) {
231+
// If notifyTerminated() already cancelled newPendingAuthResult, setFuture() will forward
232+
// that cancellation to its argument so it's safe to ignore the return value here.
233+
try {
234+
newPendingAuthResult.setFuture(
235+
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName));
236+
} catch (Exception e) { // Not just RuntimeException! Handle the "sneaky" checked case too.
237+
newPendingAuthResult.setException(e);
238+
}
239+
} else {
240+
newPendingAuthResult.cancel(false);
212241
}
213242

214243
Futures.addCallback(
@@ -236,6 +265,21 @@ public void onFailure(Throwable t) {
236265
return nonCancellationPropagating(newPendingAuthResult);
237266
}
238267

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

241285
/**

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.common.truth.Truth.assertThat;
2121
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
2222
import static java.util.concurrent.TimeUnit.SECONDS;
23+
import static org.junit.Assert.fail;
2324
import static org.robolectric.Shadows.shadowOf;
2425

2526
import android.app.Application;
@@ -49,6 +50,7 @@
4950
import io.grpc.stub.ServerCalls;
5051
import java.io.IOException;
5152
import java.util.concurrent.ArrayBlockingQueue;
53+
import java.util.concurrent.CancellationException;
5254
import org.junit.After;
5355
import org.junit.Before;
5456
import org.junit.Test;
@@ -209,6 +211,24 @@ public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Excep
209211
assertThat(awaitResult(status).getCode()).isEqualTo(Status.Code.OK);
210212
}
211213

214+
@Test
215+
public void testAsyncServerSecurityPolicy_shutdownNow_cancelsAuthFutures() throws Exception {
216+
ListenableFuture<Status> callResult = makeCall();
217+
SettableFuture<Status> authResultFuture = awaitNext(statusesToSet);
218+
219+
channel.shutdownNow();
220+
boolean terminationResult = channel.awaitTermination(10, SECONDS);
221+
assertThat(terminationResult).isTrue();
222+
223+
try {
224+
Status authResult = awaitResult(authResultFuture);
225+
fail("Expected authResultFuture cancellation but got " + authResult);
226+
} catch (CancellationException expected) {
227+
}
228+
assertThat(authResultFuture.isCancelled()).isTrue();
229+
assertThat(awaitResult(callResult).getCode()).isEqualTo(Status.Code.UNAVAILABLE);
230+
}
231+
212232
private ListenableFuture<Status> makeCall() {
213233
ClientCall<Empty, Empty> call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT);
214234
ListenableFuture<Empty> responseFuture =

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,25 @@ public void checkAuthorization_failedFuture_notCached() throws Exception {
182182
assertThat(authResult2.get()).isEqualTo(Status.OK);
183183
}
184184

185+
@Test
186+
public void notifyTerminatedUnlocked_cancelsPendingAuthFutures() {
187+
ListenableFuture<Status> authResult = authState.checkAuthorization(CODEGEN_METHOD);
188+
assertThat(authResult.isDone()).isFalse();
189+
190+
authState.notifyTerminatedUnlocked();
191+
192+
assertThat(authResult.isCancelled()).isTrue();
193+
}
185194

195+
@Test
196+
public void checkAuthorization_afterTermination_returnsCancelledFuture() {
197+
authState.notifyTerminatedUnlocked();
198+
199+
ListenableFuture<Status> authResult = authState.checkAuthorization(CODEGEN_METHOD);
200+
201+
assertThat(authResult.isCancelled()).isTrue();
202+
assertThat(fakePolicyChecker.statusesToSet).isEmpty(); // fakePolicyChecker never called.
203+
}
186204

187205
@Test
188206
public void checkAuthorization_synchronousException_doesNotLeaveStrandedFuture()

0 commit comments

Comments
 (0)