Skip to content

Commit a8200d0

Browse files
jdcormieAgraVator
authored andcommitted
binder: Only accept post-setup transactions from the server UID we actually authorize. (#12359)
1 parent ebe4c48 commit a8200d0

File tree

6 files changed

+174
-1
lines changed

6 files changed

+174
-1
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ protected void handleSetupTransport(Parcel parcel) {
340340
shutdownInternal(
341341
Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true);
342342
} else {
343+
restrictIncomingBinderToCallsFrom(remoteUid);
343344
attributes = setSecurityAttrs(attributes, remoteUid);
344345
authResultFuture = checkServerAuthorizationAsync(remoteUid);
345346
Futures.addCallback(

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.common.base.Preconditions.checkNotNull;
2020
import static com.google.common.base.Preconditions.checkState;
2121
import static com.google.common.util.concurrent.Futures.immediateFuture;
22+
import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler;
2223

2324
import android.os.DeadObjectException;
2425
import android.os.IBinder;
@@ -39,6 +40,7 @@
3940
import io.grpc.Status;
4041
import io.grpc.StatusException;
4142
import io.grpc.binder.InboundParcelablePolicy;
43+
import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler;
4244
import io.grpc.internal.ObjectPool;
4345
import java.util.ArrayList;
4446
import java.util.Iterator;
@@ -155,6 +157,7 @@ protected enum TransportState {
155157
private final ObjectPool<ScheduledExecutorService> executorServicePool;
156158
private final ScheduledExecutorService scheduledExecutorService;
157159
private final InternalLogId logId;
160+
@GuardedBy("this")
158161
private final LeakSafeOneWayBinder incomingBinder;
159162

160163
protected final ConcurrentHashMap<Integer, Inbound<?>> ongoingCalls;
@@ -476,6 +479,15 @@ private boolean handleTransactionInternal(int code, Parcel parcel) {
476479
}
477480
}
478481

482+
@BinderThread
483+
@GuardedBy("this")
484+
protected void restrictIncomingBinderToCallsFrom(int allowedCallingUid) {
485+
TransactionHandler currentHandler = incomingBinder.getHandler();
486+
if (currentHandler != null) {
487+
incomingBinder.setHandler(newCallerFilteringHandler(allowedCallingUid, currentHandler));
488+
}
489+
}
490+
479491
@Nullable
480492
@GuardedBy("this")
481493
protected Inbound<?> createInbound(int callId) {
@@ -561,6 +573,11 @@ Map<Integer, Inbound<?>> getOngoingCalls() {
561573
return ongoingCalls;
562574
}
563575

576+
@VisibleForTesting
577+
LeakSafeOneWayBinder getIncomingBinderForTesting() {
578+
return this.incomingBinder;
579+
}
580+
564581
private static Status statusFromRemoteException(RemoteException e) {
565582
if (e instanceof DeadObjectException || e instanceof TransactionTooLargeException) {
566583
// These are to be expected from time to time and can simply be retried.

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,21 @@ public void detach() {
7373
setHandler(null);
7474
}
7575

76-
/** Replaces the current {@link TransactionHandler} with `handler`. */
76+
/** Returns the current {@link TransactionHandler} or null if already detached. */
77+
public @Nullable TransactionHandler getHandler() {
78+
return handler;
79+
}
80+
81+
/**
82+
* Replaces the current {@link TransactionHandler} with `handler`.
83+
*
84+
* <p>{@link TransactionHandler} mutations race against incoming transactions except in the
85+
* special case where the caller is already handling an incoming transaction on this same {@link
86+
* LeakSafeOneWayBinder} instance. In that case, mutations are safe and the provided 'handler' is
87+
* guaranteed to be used for the very next transaction. This follows from the one-at-a-time
88+
* property of one-way Binder transactions as explained by {@link
89+
* TransactionHandler#handleTransaction}.
90+
*/
7791
public void setHandler(@Nullable TransactionHandler handler) {
7892
this.handler = handler;
7993
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,13 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import android.os.Binder;
1920
import android.os.Parcel;
2021
import io.grpc.MethodDescriptor.MethodType;
2122
import io.grpc.Status;
23+
import java.util.logging.Level;
24+
import java.util.logging.Logger;
25+
import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler;
2226
import javax.annotation.Nullable;
2327

2428
/** Constants and helpers for managing inbound / outbound transactions. */
@@ -99,4 +103,24 @@ static void fillInFlags(Parcel parcel, int flags) {
99103
parcel.writeInt(flags);
100104
parcel.setDataPosition(pos);
101105
}
106+
107+
/**
108+
* Decorates the given {@link TransactionHandler} with a wrapper that only forwards transactions
109+
* from the given `allowedCallingUid`.
110+
*/
111+
static TransactionHandler newCallerFilteringHandler(
112+
int allowedCallingUid, TransactionHandler wrapped) {
113+
final Logger logger = Logger.getLogger(TransactionUtils.class.getName());
114+
return new TransactionHandler() {
115+
@Override
116+
public boolean handleTransaction(int code, Parcel data) {
117+
int callingUid = Binder.getCallingUid();
118+
if (callingUid != allowedCallingUid) {
119+
logger.log(Level.WARNING, "dropped txn from " + callingUid + " !=" + allowedCallingUid);
120+
return false;
121+
}
122+
return wrapped.handleTransaction(code, data);
123+
}
124+
};
125+
}
102126
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616

1717
package io.grpc.binder.internal;
1818

19+
import static android.os.IBinder.FLAG_ONEWAY;
1920
import static android.os.Process.myUid;
2021
import static com.google.common.truth.Truth.assertThat;
22+
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
2123
import static io.grpc.binder.internal.BinderTransport.REMOTE_UID;
2224
import static io.grpc.binder.internal.BinderTransport.SETUP_TRANSPORT;
25+
import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT;
2326
import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION;
2427
import static java.util.concurrent.TimeUnit.MILLISECONDS;
28+
import static org.mockito.ArgumentMatchers.anyLong;
29+
import static org.mockito.Mockito.mock;
2530
import static org.mockito.Mockito.never;
2631
import static org.mockito.Mockito.timeout;
2732
import static org.mockito.Mockito.verify;
@@ -48,6 +53,7 @@
4853
import io.grpc.binder.SecurityPolicies;
4954
import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest;
5055
import io.grpc.internal.AbstractTransportTest;
56+
import io.grpc.internal.ClientTransport;
5157
import io.grpc.internal.ClientTransportFactory.ClientTransportOptions;
5258
import io.grpc.internal.ConnectionClientTransport;
5359
import io.grpc.internal.GrpcUtil;
@@ -64,6 +70,8 @@
6470
import org.junit.Rule;
6571
import org.junit.Test;
6672
import org.junit.runner.RunWith;
73+
import org.mockito.ArgumentCaptor;
74+
import org.mockito.Captor;
6775
import org.mockito.Mock;
6876
import org.mockito.junit.MockitoJUnit;
6977
import org.mockito.junit.MockitoRule;
@@ -102,6 +110,9 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest
102110

103111
@Mock AsyncSecurityPolicy mockClientSecurityPolicy;
104112

113+
@Captor
114+
ArgumentCaptor<Status> statusCaptor;
115+
105116
ApplicationInfo serverAppInfo;
106117
PackageInfo serverPkgInfo;
107118
ServiceInfo serviceInfo;
@@ -306,6 +317,42 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception {
306317
.isEqualTo(myUid());
307318
}
308319

320+
@Test
321+
public void clientIgnoresTransactionFromNonServerUids() throws Exception {
322+
server.start(serverListener);
323+
client = newClientTransport(server);
324+
startTransport(client, mockClientTransportListener);
325+
326+
int serverUid = ((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID);
327+
int someOtherUid = 1 + serverUid;
328+
sendShutdownTransportTransactionAsUid(client, someOtherUid);
329+
330+
// Demonstrate that the transport is still working and that shutdown transaction was ignored.
331+
ClientTransport.PingCallback mockPingCallback = mock(ClientTransport.PingCallback.class);
332+
client.ping(mockPingCallback, directExecutor());
333+
verify(mockPingCallback, timeout(TIMEOUT_MS)).onSuccess(anyLong());
334+
335+
// Try again as the expected uid to demonstrate that this wasn't ignored for some other reason.
336+
sendShutdownTransportTransactionAsUid(client, serverUid);
337+
338+
verify(mockClientTransportListener, timeout(TIMEOUT_MS))
339+
.transportShutdown(statusCaptor.capture());
340+
assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.UNAVAILABLE);
341+
assertThat(statusCaptor.getValue().getDescription()).contains("shutdown");
342+
}
343+
344+
static void sendShutdownTransportTransactionAsUid(ClientTransport client, int sendingUid) {
345+
int originalUid = Binder.getCallingUid();
346+
try {
347+
ShadowBinder.setCallingUid(sendingUid);
348+
((BinderClientTransport) client)
349+
.getIncomingBinderForTesting()
350+
.onTransact(SHUTDOWN_TRANSPORT, null, null, FLAG_ONEWAY);
351+
} finally {
352+
ShadowBinder.setCallingUid(originalUid);
353+
}
354+
}
355+
309356
@Test
310357
@Override
311358
// We don't quite pass the official/abstract version of this test yet because
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.internal;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler;
21+
import static org.mockito.ArgumentMatchers.any;
22+
import static org.mockito.ArgumentMatchers.anyInt;
23+
import static org.mockito.ArgumentMatchers.eq;
24+
import static org.mockito.ArgumentMatchers.same;
25+
import static org.mockito.Mockito.never;
26+
import static org.mockito.Mockito.verify;
27+
import static org.mockito.Mockito.when;
28+
29+
import android.os.Binder;
30+
import android.os.Parcel;
31+
import org.junit.Rule;
32+
import org.junit.Test;
33+
import org.junit.runner.RunWith;
34+
import org.mockito.Mock;
35+
import org.mockito.junit.MockitoJUnit;
36+
import org.mockito.junit.MockitoRule;
37+
import org.robolectric.RobolectricTestRunner;
38+
import org.robolectric.shadows.ShadowBinder;
39+
40+
@RunWith(RobolectricTestRunner.class)
41+
public final class TransactionUtilsTest {
42+
43+
@Rule public MockitoRule mocks = MockitoJUnit.rule();
44+
45+
@Mock LeakSafeOneWayBinder.TransactionHandler mockHandler;
46+
47+
@Test
48+
public void shouldIgnoreTransactionFromWrongUid() {
49+
Parcel p = Parcel.obtain();
50+
int originalUid = Binder.getCallingUid();
51+
try {
52+
when(mockHandler.handleTransaction(eq(1234), same(p))).thenReturn(true);
53+
LeakSafeOneWayBinder.TransactionHandler uid100OnlyHandler =
54+
newCallerFilteringHandler(1000, mockHandler);
55+
56+
ShadowBinder.setCallingUid(9999);
57+
boolean result = uid100OnlyHandler.handleTransaction(1234, p);
58+
assertThat(result).isFalse();
59+
verify(mockHandler, never()).handleTransaction(anyInt(), any());
60+
61+
ShadowBinder.setCallingUid(1000);
62+
result = uid100OnlyHandler.handleTransaction(1234, p);
63+
assertThat(result).isTrue();
64+
verify(mockHandler).handleTransaction(1234, p);
65+
} finally {
66+
ShadowBinder.setCallingUid(originalUid);
67+
p.recycle();
68+
}
69+
}
70+
}

0 commit comments

Comments
 (0)