Skip to content

Commit 599a0a1

Browse files
authored
binder: Let the server know when the client fails to authorize it. (#12445)
Avoids waiting for the handshake timeout on the server side in this case. I also add test coverage for the `!setOutgoingBinder()` case to make sure it works in the new location. My ulterior motive for this change is simplifying the client handshake code in preparation for #12398 -- An (impossible) !isShutdown() clause goes away for easy to understand reasons and I'll no longer have to pass the server's binder as an arg from async function to function in two separate handshake impls. Fixes #12438
1 parent 91f3f4d commit 599a0a1

File tree

4 files changed

+124
-17
lines changed

4 files changed

+124
-17
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import io.grpc.Status.Code;
4040
import io.grpc.binder.AndroidComponentAddress;
4141
import io.grpc.binder.BinderServerBuilder;
42+
import io.grpc.binder.FakeDeadBinder;
4243
import io.grpc.binder.HostServices;
4344
import io.grpc.binder.SecurityPolicy;
4445
import io.grpc.binder.internal.OneWayBinderProxies.BlackHoleOneWayBinderProxy;
@@ -358,6 +359,20 @@ public void testTxnFailurePostSetup() throws Exception {
358359
assertThat(streamStatus.getCause()).isSameInstanceAs(doe);
359360
}
360361

362+
@Test
363+
public void testServerBinderDeadOnArrival() throws Exception {
364+
BlockingBinderDecorator<OneWayBinderProxy> decorator = new BlockingBinderDecorator<>();
365+
transport = new BinderClientTransportBuilder().setBinderDecorator(decorator).build();
366+
transport.start(transportListener).run();
367+
decorator.putNextResult(decorator.takeNextRequest()); // Server's "Endpoint" Binder.
368+
OneWayBinderProxy unusedServerBinder = decorator.takeNextRequest();
369+
decorator.putNextResult(
370+
OneWayBinderProxy.wrap(new FakeDeadBinder(), offloadServicePool.getObject()));
371+
Status clientStatus = transportListener.awaitShutdown();
372+
assertThat(clientStatus.getCode()).isEqualTo(Code.UNAVAILABLE);
373+
assertThat(clientStatus.getDescription()).contains("Failed to observe outgoing binder");
374+
}
375+
361376
@Test
362377
public void testBlackHoleEndpointConnectTimeout() throws Exception {
363378
BlockingBinderDecorator<BlackHoleOneWayBinderProxy> decorator = new BlockingBinderDecorator<>();

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

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ public final class BinderClientTransport extends BinderTransport
8787
@GuardedBy("this")
8888
private ScheduledFuture<?> readyTimeoutFuture; // != null iff timeout scheduled.
8989

90-
9190
/**
9291
* Constructs a new transport instance.
9392
*
@@ -324,6 +323,9 @@ protected void handleSetupTransport(Parcel parcel) {
324323
} else if (binder == null) {
325324
shutdownInternal(
326325
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
326+
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
327+
shutdownInternal(
328+
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
327329
} else {
328330
restrictIncomingBinderToCallsFrom(remoteUid);
329331
attributes = setSecurityAttrs(attributes, remoteUid);
@@ -334,7 +336,7 @@ protected void handleSetupTransport(Parcel parcel) {
334336
new FutureCallback<Status>() {
335337
@Override
336338
public void onSuccess(Status result) {
337-
handleAuthResult(binder, result);
339+
handleAuthResult(result);
338340
}
339341

340342
@Override
@@ -353,24 +355,17 @@ private ListenableFuture<Status> checkServerAuthorizationAsync(int remoteUid) {
353355
: Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor);
354356
}
355357

356-
private synchronized void handleAuthResult(IBinder binder, Status authorization) {
358+
private synchronized void handleAuthResult(Status authorization) {
357359
if (inState(TransportState.SETUP)) {
358360
if (!authorization.isOk()) {
359361
shutdownInternal(authorization, true);
360-
} else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) {
361-
shutdownInternal(
362-
Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true);
363362
} else {
364-
// Check state again, since a failure inside setOutgoingBinder (or a callback it
365-
// triggers), could have shut us down.
366-
if (!isShutdown()) {
367-
setState(TransportState.READY);
368-
attributes = clientTransportListener.filterTransport(attributes);
369-
clientTransportListener.transportReady();
370-
if (readyTimeoutFuture != null) {
371-
readyTimeoutFuture.cancel(false);
372-
readyTimeoutFuture = null;
373-
}
363+
setState(TransportState.READY);
364+
attributes = clientTransportListener.filterTransport(attributes);
365+
clientTransportListener.transportReady();
366+
if (readyTimeoutFuture != null) {
367+
readyTimeoutFuture.cancel(false);
368+
readyTimeoutFuture = null;
374369
}
375370
}
376371
}
@@ -387,7 +382,6 @@ protected void handlePingResponse(Parcel parcel) {
387382
pingTracker.onPingResponse(parcel.readInt());
388383
}
389384

390-
391385
private static ClientStream newFailingClientStream(
392386
Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) {
393387
StatsTraceContext statsTraceContext =

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import androidx.test.core.content.pm.ApplicationInfoBuilder;
4444
import androidx.test.core.content.pm.PackageInfoBuilder;
4545
import com.google.common.collect.ImmutableList;
46+
import com.google.common.truth.TruthJUnit;
4647
import io.grpc.Attributes;
4748
import io.grpc.InternalChannelz.SocketStats;
4849
import io.grpc.ServerStreamTracer;
@@ -353,6 +354,29 @@ static void sendShutdownTransportTransactionAsUid(ClientTransport client, int se
353354
}
354355
}
355356

357+
@Test
358+
public void clientReportsAuthzErrorToServer() throws Exception {
359+
server.start(serverListener);
360+
client =
361+
newClientTransportBuilder()
362+
.setFactory(
363+
newClientTransportFactoryBuilder()
364+
.setSecurityPolicy(SecurityPolicies.permissionDenied("test"))
365+
.buildClientTransportFactory())
366+
.build();
367+
runIfNotNull(client.start(mockClientTransportListener));
368+
verify(mockClientTransportListener, timeout(TIMEOUT_MS))
369+
.transportShutdown(statusCaptor.capture());
370+
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.PERMISSION_DENIED);
371+
372+
TruthJUnit.assume().that(preAuthServersParam).isFalse();
373+
374+
MockServerTransportListener serverTransportListener =
375+
serverListener.takeListenerOrFail(TIMEOUT_MS, MILLISECONDS);
376+
serverTransportListener.waitForTermination(TIMEOUT_MS, MILLISECONDS);
377+
assertThat(serverTransportListener.isTerminated()).isTrue();
378+
}
379+
356380
@Test
357381
@Override
358382
// We don't quite pass the official/abstract version of this test yet because
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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;
18+
19+
import android.os.DeadObjectException;
20+
import android.os.IBinder;
21+
import android.os.IInterface;
22+
import android.os.Parcel;
23+
import android.os.RemoteException;
24+
import java.io.FileDescriptor;
25+
26+
/** An {@link IBinder} that behaves as if its hosting process has died, for testing. */
27+
public class FakeDeadBinder implements IBinder {
28+
@Override
29+
public boolean isBinderAlive() {
30+
return false;
31+
}
32+
33+
@Override
34+
public IInterface queryLocalInterface(String descriptor) {
35+
return null;
36+
}
37+
38+
@Override
39+
public String getInterfaceDescriptor() throws RemoteException {
40+
throw new DeadObjectException();
41+
}
42+
43+
@Override
44+
public boolean pingBinder() {
45+
return false;
46+
}
47+
48+
@Override
49+
public void dump(FileDescriptor fd, String[] args) throws RemoteException {
50+
throw new DeadObjectException();
51+
}
52+
53+
@Override
54+
public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException {
55+
throw new DeadObjectException();
56+
}
57+
58+
@Override
59+
public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
60+
throw new DeadObjectException();
61+
}
62+
63+
@Override
64+
public void linkToDeath(DeathRecipient r, int flags) throws RemoteException {
65+
throw new DeadObjectException();
66+
}
67+
68+
@Override
69+
public boolean unlinkToDeath(DeathRecipient deathRecipient, int flags) {
70+
// No need to check whether 'deathRecipient' was ever actually passed to linkToDeath(): Per our
71+
// API contract, if "the IBinder has already died" we never throw and always return false.
72+
return false;
73+
}
74+
}

0 commit comments

Comments
 (0)