Skip to content

Commit d51ec50

Browse files
committed
binder: Cancel pending authorizations upon transport termination
Fixes #12929
1 parent 84fecb7 commit d51ec50

5 files changed

Lines changed: 94 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: 18 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.assertThrows;
2324
import static org.robolectric.Shadows.shadowOf;
2425

2526
import android.app.Application;
@@ -49,6 +50,10 @@
4950
import io.grpc.stub.ServerCalls;
5051
import java.io.IOException;
5152
import java.util.concurrent.ArrayBlockingQueue;
53+
import java.util.concurrent.BlockingQueue;
54+
import java.util.concurrent.CancellationException;
55+
import java.util.concurrent.Future;
56+
import java.util.concurrent.TimeoutException;
5257
import org.junit.After;
5358
import org.junit.Before;
5459
import org.junit.Test;
@@ -209,6 +214,19 @@ public void testAsyncServerSecurityPolicy_allowed_returnsOkStatus() throws Excep
209214
assertThat(awaitResult(status).getCode()).isEqualTo(Status.Code.OK);
210215
}
211216

217+
@Test
218+
public void testAsyncServerSecurityPolicy_shutdownNow_cancelsAuthFutures() throws Exception {
219+
ListenableFuture<Status> callResult = makeCall();
220+
SettableFuture<Status> authResultFuture = awaitNext(statusesToSet);
221+
222+
channel.shutdownNow();
223+
boolean terminationResult = channel.awaitTermination(10, SECONDS);
224+
assertThat(terminationResult).isTrue();
225+
226+
assertThrows(CancellationException.class, () -> awaitResult(authResultFuture));
227+
assertThat(awaitResult(callResult).getCode()).isEqualTo(Status.Code.UNAVAILABLE);
228+
}
229+
212230
private ListenableFuture<Status> makeCall() {
213231
ClientCall<Empty, Empty> call = channel.newCall(getMethodDescriptor(), CallOptions.DEFAULT);
214232
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
@@ -183,7 +183,25 @@ public void checkAuthorization_failedFuture_notCached() throws Exception {
183183
assertThat(authResult2.get()).isEqualTo(Status.OK);
184184
}
185185

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

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

188206
@Test
189207
public void checkAuthorization_synchronousException_doesNotLeaveStrandedFuture()

0 commit comments

Comments
 (0)