Skip to content

Commit 84fecb7

Browse files
committed
binder: Tighten up simultaneous auth handling fixing #10669
Include uncached methods in the deduping logic too.
1 parent eb4c0ea commit 84fecb7

2 files changed

Lines changed: 130 additions & 41 deletions

File tree

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

Lines changed: 48 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import static com.google.common.util.concurrent.Futures.nonCancellationPropagating;
20+
1921
import com.google.common.annotations.VisibleForTesting;
2022
import com.google.common.util.concurrent.FutureCallback;
2123
import com.google.common.util.concurrent.Futures;
2224
import com.google.common.util.concurrent.ListenableFuture;
2325
import com.google.common.util.concurrent.MoreExecutors;
26+
import com.google.common.util.concurrent.SettableFuture;
2427
import com.google.errorprone.annotations.CheckReturnValue;
2528
import io.grpc.Attributes;
2629
import io.grpc.Internal;
@@ -167,6 +170,7 @@ private static Status statusFromFailedAuthorizationFuture(Throwable cause) {
167170
static final class TransportAuthorizationState {
168171
private final int uid;
169172
private final ServerPolicyChecker serverPolicyChecker;
173+
// Holds *all* pending policy check futures and *certain* complete ones that we want to cache.
170174
private final ConcurrentHashMap<String, ListenableFuture<Status>> serviceAuthorization;
171175
private final Executor executor;
172176

@@ -185,44 +189,53 @@ static final class TransportAuthorizationState {
185189
@CheckReturnValue
186190
ListenableFuture<Status> checkAuthorization(MethodDescriptor<?, ?> method) {
187191
String serviceName = method.getServiceName();
188-
// Only cache decisions if the method can be sampled for tracing,
189-
// which is true for all generated methods. Otherwise, programmatically
190-
// created methods could cause this cache to grow unbounded.
191-
boolean useCache = method.isSampledToLocalTracing();
192-
if (useCache) {
193-
@Nullable ListenableFuture<Status> authorization = serviceAuthorization.get(serviceName);
194-
if (authorization != null) {
195-
// Authorization check exists and is a pending or successful future (even if for a
196-
// failed authorization).
197-
return authorization;
198-
}
192+
@Nullable
193+
ListenableFuture<Status> pendingOrCachedAuthResult = serviceAuthorization.get(serviceName);
194+
if (pendingOrCachedAuthResult != null) {
195+
return nonCancellationPropagating(pendingOrCachedAuthResult);
199196
}
200-
// Under high load, this may trigger a large number of concurrent authorization checks that
201-
// perform essentially the same work and have the potential of exhausting the resources they
202-
// depend on. This was a non-issue in the past with synchronous policy checks due to the
203-
// fixed-size nature of the thread pool this method runs under.
204-
//
205-
// TODO(10669): evaluate if there should be at most a single pending authorization check per
206-
// (uid, serviceName) pair at any given time.
207-
ListenableFuture<Status> authorization =
208-
serverPolicyChecker.checkAuthorizationForServiceAsync(uid, serviceName);
209-
if (useCache) {
210-
serviceAuthorization.putIfAbsent(serviceName, authorization);
211-
Futures.addCallback(
212-
authorization,
213-
new FutureCallback<Status>() {
214-
@Override
215-
public void onSuccess(Status result) {}
216-
217-
@Override
218-
public void onFailure(Throwable t) {
219-
serviceAuthorization.remove(serviceName, authorization);
220-
}
221-
},
222-
MoreExecutors.directExecutor());
197+
198+
SettableFuture<Status> newPendingAuthResult = SettableFuture.create();
199+
ListenableFuture<Status> checkThenActRaceWinner =
200+
serviceAuthorization.putIfAbsent(serviceName, newPendingAuthResult);
201+
if (checkThenActRaceWinner != null) {
202+
// Another thread running this method must have also just saw no entry for serviceName, then
203+
// beat us to calling putIfAbsent(). We can only track one check at a time so share theirs.
204+
return nonCancellationPropagating(checkThenActRaceWinner);
205+
}
206+
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);
223212
}
224-
return authorization;
213+
214+
Futures.addCallback(
215+
newPendingAuthResult,
216+
new FutureCallback<Status>() {
217+
@Override
218+
public void onSuccess(Status result) {
219+
// Auth checks can be expensive so we want to cache the results. But programmatically
220+
// created service names could cause the cache to grow without bound. Conservatively,
221+
// we only cache results for codegen service names as there can't be too many of them.
222+
if (!method.isSampledToLocalTracing()) {
223+
serviceAuthorization.remove(serviceName, newPendingAuthResult);
224+
}
225+
}
226+
227+
@Override
228+
public void onFailure(Throwable t) {
229+
// Not simply a non-OK auth result but a failure to return any decision at all. Never
230+
// cache these so that if the caller retries, we'll retry the auth check as well.
231+
serviceAuthorization.remove(serviceName, newPendingAuthResult);
232+
}
233+
},
234+
MoreExecutors.directExecutor());
235+
236+
return nonCancellationPropagating(newPendingAuthResult);
225237
}
238+
226239
}
227240

228241
/**

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

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,38 +82,88 @@ public void tearDown() throws Exception {
8282
}
8383

8484
@Test
85-
public void checkAuthorization_doesNotCacheNonCodegenMethods() throws Exception {
85+
public void checkAuthorization_deduplicatesSimultaneousNonCodegenMethods() throws Exception {
8686
ListenableFuture<Status> authResult1 = authState.checkAuthorization(NONCODEGEN_METHOD);
8787
assertThat(authResult1.isDone()).isFalse();
8888

89+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(NONCODEGEN_METHOD);
90+
assertThat(authResult2.isDone()).isFalse();
91+
92+
// The fake policy checker was only invoked ONCE thanks to deduplication.
93+
// Completing that single underlying auth check satisfies both futures.
8994
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
95+
9096
assertThat(authResult1.get()).isEqualTo(Status.OK);
97+
assertThat(authResult2.get()).isEqualTo(Status.OK);
9198
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
9299

93100
// Because it's a non-codegen method, the auth result should not be cached.
94-
ListenableFuture<Status> authResult2 = authState.checkAuthorization(NONCODEGEN_METHOD);
95-
assertThat(authResult2.isDone()).isFalse();
101+
ListenableFuture<Status> authResult3 = authState.checkAuthorization(NONCODEGEN_METHOD);
102+
assertThat(authResult3.isDone()).isFalse();
96103

97104
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.PERMISSION_DENIED);
98-
assertThat(authResult2.get()).isEqualTo(Status.PERMISSION_DENIED);
105+
assertThat(authResult3.get()).isEqualTo(Status.PERMISSION_DENIED);
99106
}
100107

101108
@Test
102-
public void checkAuthorization_cachesCodegenMethods() throws Exception {
109+
public void checkAuthorization_cachesSimultaneousCodegenMethods() throws Exception {
103110
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
104111
assertThat(authResult1.isDone()).isFalse();
105112

113+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
114+
assertThat(authResult2.isDone()).isFalse();
115+
116+
// The fake policy checker was only invoked ONCE thanks to deduplication.
117+
// Completing that single underlying auth check satisfies both futures.
106118
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
119+
107120
assertThat(authResult1.get()).isEqualTo(Status.OK);
121+
assertThat(authResult2.get()).isEqualTo(Status.OK);
108122
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
109123

110124
// Because it's a codegen method, the auth result should be cached for the life of the object.
125+
ListenableFuture<Status> authResult3 = authState.checkAuthorization(CODEGEN_METHOD);
126+
assertThat(authResult3.isDone()).isTrue();
127+
assertThat(authResult3.get()).isEqualTo(Status.OK);
128+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
129+
}
130+
131+
@Test
132+
public void checkAuthorization_cachesPermissionDeniedForCodegenMethods() throws Exception {
133+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
134+
assertThat(authResult1.isDone()).isFalse();
135+
136+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.PERMISSION_DENIED);
137+
138+
assertThat(authResult1.get()).isEqualTo(Status.PERMISSION_DENIED);
139+
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
140+
141+
// Because it's a codegen method, the non-OK status should be cached for the life of the object.
111142
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
112143
assertThat(authResult2.isDone()).isTrue();
113-
assertThat(authResult2.get()).isEqualTo(Status.OK);
144+
assertThat(authResult2.get()).isEqualTo(Status.PERMISSION_DENIED);
114145
assertThat(fakePolicyChecker.statusesToSet).isEmpty();
115146
}
116147

148+
@Test
149+
public void checkAuthorization_cancellation_doesNotPropagateToUnderlyingCheck() throws Exception {
150+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
151+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
152+
153+
// Cancel the first future.
154+
authResult1.cancel(true);
155+
156+
assertThat(authResult1.isCancelled()).isTrue();
157+
// The second future should NOT be cancelled, because the cancellation shouldn't propagate
158+
// to the underlying shared future.
159+
assertThat(authResult2.isCancelled()).isFalse();
160+
161+
// Completing the underlying auth check satisfies the non-cancelled future.
162+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
163+
164+
assertThat(authResult2.get()).isEqualTo(Status.OK);
165+
}
166+
117167
@Test
118168
public void checkAuthorization_failedFuture_notCached() throws Exception {
119169
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
@@ -133,11 +183,37 @@ public void checkAuthorization_failedFuture_notCached() throws Exception {
133183
assertThat(authResult2.get()).isEqualTo(Status.OK);
134184
}
135185

186+
187+
188+
@Test
189+
public void checkAuthorization_synchronousException_doesNotLeaveStrandedFuture()
190+
throws Exception {
191+
IllegalStateException syncException = new IllegalStateException("ouch");
192+
fakePolicyChecker.syncExceptionsToThrow.add(syncException);
193+
194+
// The synchronous exception is safely returned as a failed future.
195+
ListenableFuture<Status> authResult1 = authState.checkAuthorization(CODEGEN_METHOD);
196+
ExecutionException ee = assertThrows(ExecutionException.class, authResult1::get);
197+
assertThat(ee).hasCauseThat().isSameInstanceAs(syncException);
198+
199+
// Ensure the failed check can be retried.
200+
ListenableFuture<Status> authResult2 = authState.checkAuthorization(CODEGEN_METHOD);
201+
assertThat(authResult2.isDone()).isFalse();
202+
203+
fakePolicyChecker.takeNextAuthRequestOrDie().set(Status.OK);
204+
assertThat(authResult2.get()).isEqualTo(Status.OK);
205+
}
206+
136207
private static final class FakeServerPolicyChecker implements ServerPolicyChecker {
208+
final LinkedBlockingQueue<RuntimeException> syncExceptionsToThrow = new LinkedBlockingQueue<>();
137209
final LinkedBlockingQueue<SettableFuture<Status>> statusesToSet = new LinkedBlockingQueue<>();
138210

139211
@Override
140212
public ListenableFuture<Status> checkAuthorizationForServiceAsync(int uid, String serviceName) {
213+
RuntimeException syncException = syncExceptionsToThrow.poll();
214+
if (syncException != null) {
215+
throw syncException;
216+
}
141217
SettableFuture<Status> pendingResult = SettableFuture.create();
142218
statusesToSet.add(pendingResult);
143219
return pendingResult;

0 commit comments

Comments
 (0)