From c11f45d6748b07dbe20fea363731920fa96d2101 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 01/34] Pre-factor out the guts of the BinderClientTransport handshake --- .../grpc/binder/internal/BinderTransport.java | 123 ++++++++++++------ 1 file changed, 86 insertions(+), 37 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index d87cfb74044..7fefde7720b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -22,6 +22,7 @@ import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; @@ -32,6 +33,7 @@ import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ticker; import com.google.common.base.Verify; @@ -559,6 +561,27 @@ final void handleAcknowledgedBytes(long numBytes) { } } + /** + * An abstraction of the client handshake, used to transition off a problematic legacy approach. + */ + interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server 'endpointBinder'. + */ + @GuardedBy("this") + @MainThread + void onBound(IBinder endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @GuardedBy("this") + @BinderThread + void handleSetupTransport(IBinder serverBinder); + } + /** Concrete client-side transport implementation. */ @ThreadSafe @Internal @@ -576,6 +599,7 @@ public static final class BinderClientTransport extends BinderTransport private final long readyTimeoutMillis; private final PingTracker pingTracker; private final boolean preAuthorizeServer; + private final ClientHandshake handshakeImpl; @Nullable private ManagedClientTransport.Listener clientTransportListener; @@ -618,6 +642,7 @@ public BinderClientTransport( Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); this.preAuthorizeServer = preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + this.handshakeImpl = new LegacyClientHandshake(); numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); @@ -641,9 +666,8 @@ void releaseExecutors() { } @Override - public synchronized void onBound(IBinder binder) { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + public synchronized void onBound(IBinder endpointBinder) { + handshakeImpl.onBound(endpointBinder); } @Override @@ -826,8 +850,6 @@ void notifyTerminated() { @Override @GuardedBy("this") protected void handleSetupTransport(Parcel parcel) { - int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); if (inState(TransportState.SETUP)) { int version = parcel.readInt(); IBinder binder = parcel.readStrongBinder(); @@ -838,21 +860,7 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handleAuthResult(binder, result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); + handshakeImpl.handleSetupTransport(binder); } } } @@ -863,29 +871,70 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; + class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(IBinder binder) { + sendSetupTransaction( + binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(IBinder binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); + } + } + + @Override + public void onFailure(Throwable t) { + BinderClientTransport.this.handleAuthResult(t); + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(IBinder binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + reportReady(); } } } } } + @GuardedBy("this") + private void reportReady() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + private synchronized void handleAuthResult(Throwable t) { shutdownInternal( Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); From 8486ac1c2e4a7ff4c57f3601688bdb17e6ed7c2d Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:23:34 -0700 Subject: [PATCH 02/34] don't rename binder --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 7fefde7720b..ba02e597714 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -666,8 +666,8 @@ void releaseExecutors() { } @Override - public synchronized void onBound(IBinder endpointBinder) { - handshakeImpl.onBound(endpointBinder); + public synchronized void onBound(IBinder binder) { + handshakeImpl.onBound(binder); } @Override From efd855f8183df7ccb1f88d266c05c64e7ff59e55 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 03/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index ba02e597714..f1b1509d9e5 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -22,7 +22,6 @@ import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 7bbfd423bf420dd8418620c395bcf78548c5070e Mon Sep 17 00:00:00 2001 From: John Cormie Date: Thu, 21 Aug 2025 16:08:58 -0700 Subject: [PATCH 04/34] binder: Move BinderTransport's inner classes to the top level (#12303) BinderTransport.java was getting too long and deeply nested. This is a pure refactor with no behavior changes. # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../internal/BinderClientTransportTest.java | 7 +- .../binder/internal/BinderTransportTest.java | 3 +- .../internal/ActiveTransportTracker.java | 4 +- .../internal/BinderClientTransport.java | 441 ++++++++++++++ .../BinderClientTransportFactory.java | 4 +- .../io/grpc/binder/internal/BinderServer.java | 4 +- .../internal/BinderServerTransport.java | 126 ++++ .../grpc/binder/internal/BinderTransport.java | 560 +----------------- .../java/io/grpc/binder/internal/Inbound.java | 5 +- .../internal/BinderServerTransportTest.java | 4 +- .../BinderClientTransportBuilder.java | 6 +- 11 files changed, 587 insertions(+), 577 deletions(-) create mode 100644 binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java create mode 100644 binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java index fe2fd587453..0038a054854 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderClientTransportTest.java @@ -100,7 +100,7 @@ public final class BinderClientTransportTest { .build(); AndroidComponentAddress serverAddress; - BinderTransport.BinderClientTransport transport; + BinderClientTransport transport; BlockingSecurityPolicy blockingSecurityPolicy = new BlockingSecurityPolicy(); private final ObjectPool executorServicePool = @@ -178,7 +178,7 @@ public BinderClientTransportBuilder setPreAuthorizeServer(boolean preAuthorizeSe return this; } - public BinderTransport.BinderClientTransport build() { + public BinderClientTransport build() { return factoryBuilder .buildClientTransportFactory() .newClientTransport(serverAddress, new ClientTransportOptions(), null); @@ -502,8 +502,7 @@ public void testAsyncSecurityPolicyCancelledUponExternalTermination() throws Exc } private static void startAndAwaitReady( - BinderTransport.BinderClientTransport transport, TestTransportListener transportListener) - throws Exception { + BinderClientTransport transport, TestTransportListener transportListener) throws Exception { transport.start(transportListener).run(); transportListener.awaitReady(); } diff --git a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java index fc9a383d572..7932cabde89 100644 --- a/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/internal/BinderTransportTest.java @@ -106,8 +106,7 @@ protected ManagedClientTransport newClientTransport(InternalServer server) { options.setEagAttributes(eagAttrs()); options.setChannelLogger(transportLogger()); - return new BinderTransport.BinderClientTransport( - builder.buildClientTransportFactory(), addr, options); + return new BinderClientTransport(builder.buildClientTransportFactory(), addr, options); } @Test diff --git a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java index 2bfa9fea4cb..01505bfd509 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java +++ b/binder/src/main/java/io/grpc/binder/internal/ActiveTransportTracker.java @@ -11,8 +11,8 @@ import io.grpc.internal.ServerTransportListener; /** - * Tracks which {@link BinderTransport.BinderServerTransport} are currently active and allows - * invoking a {@link Runnable} only once all transports are terminated. + * Tracks which {@link BinderServerTransport} are currently active and allows invoking a {@link + * Runnable} only once all transports are terminated. */ final class ActiveTransportTracker implements ServerListener { private final ServerListener delegate; diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java new file mode 100644 index 00000000000..95bd531aa41 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -0,0 +1,441 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Process; +import com.google.common.base.Ticker; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.errorprone.annotations.CheckReturnValue; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Attributes; +import io.grpc.CallOptions; +import io.grpc.ClientStreamTracer; +import io.grpc.Grpc; +import io.grpc.Internal; +import io.grpc.InternalLogId; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.SecurityLevel; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.binder.AndroidComponentAddress; +import io.grpc.binder.AsyncSecurityPolicy; +import io.grpc.binder.InboundParcelablePolicy; +import io.grpc.binder.SecurityPolicy; +import io.grpc.internal.ClientStream; +import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; +import io.grpc.internal.ConnectionClientTransport; +import io.grpc.internal.FailingClientStream; +import io.grpc.internal.GrpcAttributes; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.StatsTraceContext; +import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; +import javax.annotation.concurrent.ThreadSafe; + +/** Concrete client-side transport implementation. */ +@ThreadSafe +@Internal +public final class BinderClientTransport extends BinderTransport + implements ConnectionClientTransport, Bindable.Observer { + + private final ObjectPool offloadExecutorPool; + private final Executor offloadExecutor; + private final SecurityPolicy securityPolicy; + private final Bindable serviceBinding; + + /** Number of ongoing calls which keep this transport "in-use". */ + private final AtomicInteger numInUseStreams; + + private final long readyTimeoutMillis; + private final PingTracker pingTracker; + private final boolean preAuthorizeServer; + + @Nullable private ManagedClientTransport.Listener clientTransportListener; + + @GuardedBy("this") + private int latestCallId = FIRST_CALL_ID; + + @GuardedBy("this") + private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. + + @GuardedBy("this") + @Nullable + private ListenableFuture authResultFuture; // null before we check auth. + + @GuardedBy("this") + @Nullable + private ListenableFuture preAuthResultFuture; // null before we pre-auth. + + /** + * Constructs a new transport instance. + * + * @param factory parameters common to all a Channel's transports + * @param targetAddress the fully resolved and load-balanced server address + * @param options other parameters that can vary as transports come and go within a Channel + */ + public BinderClientTransport( + BinderClientTransportFactory factory, + AndroidComponentAddress targetAddress, + ClientTransportOptions options) { + super( + factory.scheduledExecutorPool, + buildClientAttributes( + options.getEagAttributes(), + factory.sourceContext, + targetAddress, + factory.inboundParcelablePolicy), + factory.binderDecorator, + buildLogId(factory.sourceContext, targetAddress)); + this.offloadExecutorPool = factory.offloadExecutorPool; + this.securityPolicy = factory.securityPolicy; + this.offloadExecutor = offloadExecutorPool.getObject(); + this.readyTimeoutMillis = factory.readyTimeoutMillis; + Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); + this.preAuthorizeServer = + preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + numInUseStreams = new AtomicInteger(); + pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); + + serviceBinding = + new ServiceBinding( + factory.mainThreadExecutor, + factory.sourceContext, + factory.channelCredentials, + targetAddress.asBindIntent(), + targetAddress.getTargetUser() != null + ? targetAddress.getTargetUser() + : factory.defaultTargetUserHandle, + factory.bindServiceFlags.toInteger(), + this); + } + + @Override + void releaseExecutors() { + super.releaseExecutors(); + offloadExecutorPool.returnObject(offloadExecutor); + } + + @Override + public synchronized void onBound(IBinder binder) { + sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + } + + @Override + public synchronized void onUnbound(Status reason) { + shutdownInternal(reason, true); + } + + @CheckReturnValue + @Override + public synchronized Runnable start(Listener clientTransportListener) { + this.clientTransportListener = checkNotNull(clientTransportListener); + return () -> { + synchronized (BinderClientTransport.this) { + if (inState(TransportState.NOT_STARTED)) { + setState(TransportState.SETUP); + try { + if (preAuthorizeServer) { + preAuthorize(serviceBinding.resolve()); + } else { + serviceBinding.bind(); + } + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } + if (readyTimeoutMillis >= 0) { + readyTimeoutFuture = + getScheduledExecutorService() + .schedule( + BinderClientTransport.this::onReadyTimeout, + readyTimeoutMillis, + MILLISECONDS); + } + } + } + }; + } + + @GuardedBy("this") + private void preAuthorize(ServiceInfo serviceInfo) { + // It's unlikely, but the identity/existence of this Service could change by the time we + // actually connect. It doesn't matter though, because: + // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer + // will eventually retry using a new transport instance that will see the Service's new state. + // - If pre-auth succeeds (but would fail against the server's new state), we might give an + // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy + // check later in handshake. Pre-auth remains effective at mitigating abuse because malware + // can't typically control the exact timing of its installation. + preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + Futures.addCallback( + preAuthResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handlePreAuthResult(result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + + private synchronized void handlePreAuthResult(Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else { + serviceBinding.bind(); + } + } + } + + private synchronized void onReadyTimeout() { + if (inState(TransportState.SETUP)) { + readyTimeoutFuture = null; + shutdownInternal( + Status.DEADLINE_EXCEEDED.withDescription( + "Connect timeout " + readyTimeoutMillis + "ms lapsed"), + true); + } + } + + @Override + public synchronized ClientStream newStream( + final MethodDescriptor method, + final Metadata headers, + final CallOptions callOptions, + ClientStreamTracer[] tracers) { + if (!inState(TransportState.READY)) { + return newFailingClientStream( + isShutdown() + ? shutdownStatus + : Status.INTERNAL.withDescription("newStream() before transportReady()"), + attributes, + headers, + tracers); + } + + int callId = latestCallId++; + if (latestCallId == LAST_CALL_ID) { + latestCallId = FIRST_CALL_ID; + } + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + Inbound.ClientInbound inbound = + new Inbound.ClientInbound( + this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions)); + if (ongoingCalls.putIfAbsent(callId, inbound) != null) { + Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); + shutdownInternal(failure, true); + return newFailingClientStream(failure, attributes, headers, tracers); + } else { + if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { + clientTransportListener.transportInUse(true); + } + Outbound.ClientOutbound outbound = + new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); + if (method.getType().clientSendsOneMessage()) { + return new SingleMessageClientStream(inbound, outbound, attributes); + } else { + return new MultiMessageClientStream(inbound, outbound, attributes); + } + } + } + + @Override + protected void unregisterInbound(Inbound inbound) { + if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) { + clientTransportListener.transportInUse(false); + } + super.unregisterInbound(inbound); + } + + @Override + public void ping(final PingCallback callback, Executor executor) { + pingTracker.startPing(callback, executor); + } + + @Override + public synchronized void shutdown(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, true); + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + clientTransportListener.transportShutdown(status); + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (numInUseStreams.getAndSet(0) > 0) { + clientTransportListener.transportInUse(false); + } + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + if (preAuthResultFuture != null) { + preAuthResultFuture.cancel(false); // No effect if already complete. + } + if (authResultFuture != null) { + authResultFuture.cancel(false); // No effect if already complete. + } + serviceBinding.unbind(); + clientTransportListener.transportTerminated(); + } + + @Override + @GuardedBy("this") + protected void handleSetupTransport(Parcel parcel) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + if (inState(TransportState.SETUP)) { + int version = parcel.readInt(); + IBinder binder = parcel.readStrongBinder(); + if (version != WIRE_FORMAT_VERSION) { + shutdownInternal(Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); + } else if (binder == null) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); + } else { + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handleAuthResult(binder, result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + + private synchronized void handleAuthResult(IBinder binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + } + } + } + + private synchronized void handleAuthResult(Throwable t) { + shutdownInternal( + Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); + } + + @GuardedBy("this") + @Override + protected void handlePingResponse(Parcel parcel) { + pingTracker.onPingResponse(parcel.readInt()); + } + + private static ClientStream newFailingClientStream( + Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + statsTraceContext.clientOutboundHeaders(); + return new FailingClientStream(failure, tracers); + } + + private static InternalLogId buildLogId( + Context sourceContext, AndroidComponentAddress targetAddress) { + return InternalLogId.allocate( + BinderClientTransport.class, + sourceContext.getClass().getSimpleName() + "->" + targetAddress); + } + + private static Attributes buildClientAttributes( + Attributes eagAttrs, + Context sourceContext, + AndroidComponentAddress targetAddress, + InboundParcelablePolicy inboundParcelablePolicy) { + return Attributes.newBuilder() + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now. + .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs) + .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext)) + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress) + .set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy) + .build(); + } + + private static Attributes setSecurityAttrs(Attributes attributes, int uid) { + return attributes.toBuilder() + .set(REMOTE_UID, uid) + .set( + GrpcAttributes.ATTR_SECURITY_LEVEL, + uid == Process.myUid() + ? SecurityLevel.PRIVACY_AND_INTEGRITY + : SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this. + .build(); + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 85a0ddd35b7..3f51452c90c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -83,12 +83,12 @@ private BinderClientTransportFactory(Builder builder) { } @Override - public BinderTransport.BinderClientTransport newClientTransport( + public BinderClientTransport newClientTransport( SocketAddress addr, ClientTransportOptions options, ChannelLogger channelLogger) { if (closed) { throw new IllegalStateException("The transport factory is closed."); } - return new BinderTransport.BinderClientTransport(this, (AndroidComponentAddress) addr, options); + return new BinderClientTransport(this, (AndroidComponentAddress) addr, options); } @Override diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java index 6b8347390b9..fca8e3d88e1 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderServer.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServer.java @@ -178,8 +178,8 @@ public synchronized boolean handleTransaction(int code, Parcel parcel) { serverPolicyChecker, checkNotNull(executor, "Not started?")); // Create a new transport and let our listener know about it. - BinderTransport.BinderServerTransport transport = - new BinderTransport.BinderServerTransport( + BinderServerTransport transport = + new BinderServerTransport( executorServicePool, attrsBuilder.build(), streamTracerFactories, diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java new file mode 100644 index 00000000000..1c345249735 --- /dev/null +++ b/binder/src/main/java/io/grpc/binder/internal/BinderServerTransport.java @@ -0,0 +1,126 @@ +/* + * Copyright 2020 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.grpc.binder.internal; + +import android.os.IBinder; +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.grpc.Attributes; +import io.grpc.Grpc; +import io.grpc.Internal; +import io.grpc.InternalLogId; +import io.grpc.Metadata; +import io.grpc.ServerStreamTracer; +import io.grpc.Status; +import io.grpc.internal.ObjectPool; +import io.grpc.internal.ServerStream; +import io.grpc.internal.ServerTransport; +import io.grpc.internal.ServerTransportListener; +import io.grpc.internal.StatsTraceContext; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import javax.annotation.Nullable; + +/** Concrete server-side transport implementation. */ +@Internal +public final class BinderServerTransport extends BinderTransport implements ServerTransport { + + private final List streamTracerFactories; + @Nullable private ServerTransportListener serverTransportListener; + + /** + * Constructs a new transport instance. + * + * @param binderDecorator used to decorate 'callbackBinder', for fault injection. + */ + public BinderServerTransport( + ObjectPool executorServicePool, + Attributes attributes, + List streamTracerFactories, + OneWayBinderProxy.Decorator binderDecorator, + IBinder callbackBinder) { + super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); + this.streamTracerFactories = streamTracerFactories; + // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. + setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); + } + + public synchronized void setServerTransportListener( + ServerTransportListener serverTransportListener) { + this.serverTransportListener = serverTransportListener; + if (isShutdown()) { + setState(TransportState.SHUTDOWN_TERMINATED); + notifyTerminated(); + releaseExecutors(); + } else { + sendSetupTransaction(); + // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a callback + // it triggers), could have shut us down. + if (!isShutdown()) { + setState(TransportState.READY); + attributes = serverTransportListener.transportReady(attributes); + } + } + } + + StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { + return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); + } + + synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { + if (isShutdown()) { + return Status.UNAVAILABLE.withDescription("transport is shutdown"); + } else { + serverTransportListener.streamCreated(stream, methodName, headers); + return Status.OK; + } + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + // Nothing to do. + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (serverTransportListener != null) { + serverTransportListener.transportTerminated(); + } + } + + @Override + public synchronized void shutdown() { + shutdownInternal(Status.OK, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + shutdownInternal(reason, true); + } + + @Override + @Nullable + @GuardedBy("this") + protected Inbound createInbound(int callId) { + return new Inbound.ServerInbound(this, attributes, callId); + } + + private static InternalLogId buildLogId(Attributes attributes) { + return InternalLogId.allocate( + BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); + } +} diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index f1b1509d9e5..3c724f4ddf8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -19,68 +19,33 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; -import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; -import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.content.Context; -import android.content.pm.ServiceInfo; -import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; -import android.os.Process; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Ticker; import com.google.common.base.Verify; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.errorprone.annotations.CheckReturnValue; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; -import io.grpc.CallOptions; -import io.grpc.ClientStreamTracer; -import io.grpc.Grpc; import io.grpc.Internal; import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalLogId; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.SecurityLevel; -import io.grpc.ServerStreamTracer; import io.grpc.Status; import io.grpc.StatusException; -import io.grpc.binder.AndroidComponentAddress; -import io.grpc.binder.AsyncSecurityPolicy; import io.grpc.binder.InboundParcelablePolicy; -import io.grpc.binder.SecurityPolicy; -import io.grpc.internal.ClientStream; -import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; -import io.grpc.internal.ConnectionClientTransport; -import io.grpc.internal.FailingClientStream; -import io.grpc.internal.GrpcAttributes; -import io.grpc.internal.GrpcUtil; -import io.grpc.internal.ManagedClientTransport; import io.grpc.internal.ObjectPool; -import io.grpc.internal.ServerStream; -import io.grpc.internal.ServerTransport; -import io.grpc.internal.ServerTransportListener; -import io.grpc.internal.StatsTraceContext; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.Nullable; @@ -170,10 +135,10 @@ public abstract class BinderTransport implements IBinder.DeathRecipient { private static final int RESERVED_TRANSACTIONS = 1000; /** The first call ID we can use. */ - private static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS; + static final int FIRST_CALL_ID = IBinder.FIRST_CALL_TRANSACTION + RESERVED_TRANSACTIONS; /** The last call ID we can use. */ - private static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION; + static final int LAST_CALL_ID = IBinder.LAST_CALL_TRANSACTION; /** The states of this transport. */ protected enum TransportState { @@ -219,7 +184,7 @@ protected enum TransportState { // Only read/written on @BinderThread. private long acknowledgedIncomingBytes; - private BinderTransport( + protected BinderTransport( ObjectPool executorServicePool, Attributes attributes, OneWayBinderProxy.Decorator binderDecorator, @@ -560,525 +525,6 @@ final void handleAcknowledgedBytes(long numBytes) { } } - /** - * An abstraction of the client handshake, used to transition off a problematic legacy approach. - */ - interface ClientHandshake { - /** - * Notifies the implementation that the binding has succeeded and we are now connected to the - * server 'endpointBinder'. - */ - @GuardedBy("this") - @MainThread - void onBound(IBinder endpointBinder); - - /** - * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a - * server that can be reached at 'serverBinder'. - */ - @GuardedBy("this") - @BinderThread - void handleSetupTransport(IBinder serverBinder); - } - - /** Concrete client-side transport implementation. */ - @ThreadSafe - @Internal - public static final class BinderClientTransport extends BinderTransport - implements ConnectionClientTransport, Bindable.Observer { - - private final ObjectPool offloadExecutorPool; - private final Executor offloadExecutor; - private final SecurityPolicy securityPolicy; - private final Bindable serviceBinding; - - /** Number of ongoing calls which keep this transport "in-use". */ - private final AtomicInteger numInUseStreams; - - private final long readyTimeoutMillis; - private final PingTracker pingTracker; - private final boolean preAuthorizeServer; - private final ClientHandshake handshakeImpl; - - @Nullable private ManagedClientTransport.Listener clientTransportListener; - - @GuardedBy("this") - private int latestCallId = FIRST_CALL_ID; - - @GuardedBy("this") - private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. - @GuardedBy("this") - @Nullable private ListenableFuture authResultFuture; // null before we check auth. - - @GuardedBy("this") - @Nullable - private ListenableFuture preAuthResultFuture; // null before we pre-auth. - - /** - * Constructs a new transport instance. - * - * @param factory parameters common to all a Channel's transports - * @param targetAddress the fully resolved and load-balanced server address - * @param options other parameters that can vary as transports come and go within a Channel - */ - public BinderClientTransport( - BinderClientTransportFactory factory, - AndroidComponentAddress targetAddress, - ClientTransportOptions options) { - super( - factory.scheduledExecutorPool, - buildClientAttributes( - options.getEagAttributes(), - factory.sourceContext, - targetAddress, - factory.inboundParcelablePolicy), - factory.binderDecorator, - buildLogId(factory.sourceContext, targetAddress)); - this.offloadExecutorPool = factory.offloadExecutorPool; - this.securityPolicy = factory.securityPolicy; - this.offloadExecutor = offloadExecutorPool.getObject(); - this.readyTimeoutMillis = factory.readyTimeoutMillis; - Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); - this.preAuthorizeServer = - preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; - this.handshakeImpl = new LegacyClientHandshake(); - numInUseStreams = new AtomicInteger(); - pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - - serviceBinding = - new ServiceBinding( - factory.mainThreadExecutor, - factory.sourceContext, - factory.channelCredentials, - targetAddress.asBindIntent(), - targetAddress.getTargetUser() != null - ? targetAddress.getTargetUser() - : factory.defaultTargetUserHandle, - factory.bindServiceFlags.toInteger(), - this); - } - - @Override - void releaseExecutors() { - super.releaseExecutors(); - offloadExecutorPool.returnObject(offloadExecutor); - } - - @Override - public synchronized void onBound(IBinder binder) { - handshakeImpl.onBound(binder); - } - - @Override - public synchronized void onUnbound(Status reason) { - shutdownInternal(reason, true); - } - - @CheckReturnValue - @Override - public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) { - this.clientTransportListener = checkNotNull(clientTransportListener); - return () -> { - synchronized (BinderClientTransport.this) { - if (inState(TransportState.NOT_STARTED)) { - setState(TransportState.SETUP); - try { - if (preAuthorizeServer) { - preAuthorize(serviceBinding.resolve()); - } else { - serviceBinding.bind(); - } - } catch (StatusException e) { - shutdownInternal(e.getStatus(), true); - return; - } - if (readyTimeoutMillis >= 0) { - readyTimeoutFuture = - getScheduledExecutorService() - .schedule( - BinderClientTransport.this::onReadyTimeout, - readyTimeoutMillis, - MILLISECONDS); - } - } - } - }; - } - - @GuardedBy("this") - private void preAuthorize(ServiceInfo serviceInfo) { - // It's unlikely, but the identity/existence of this Service could change by the time we - // actually connect. It doesn't matter though, because: - // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer - // will eventually retry using a new transport instance that will see the Service's new state. - // - If pre-auth succeeds (but would fail against the server's new state), we might give an - // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy - // check later in handshake. Pre-auth remains effective at mitigating abuse because malware - // can't typically control the exact timing of its installation. - preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); - Futures.addCallback( - preAuthResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handlePreAuthResult(result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); - } - - private synchronized void handlePreAuthResult(Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else { - serviceBinding.bind(); - } - } - } - - private synchronized void onReadyTimeout() { - if (inState(TransportState.SETUP)) { - readyTimeoutFuture = null; - shutdownInternal( - Status.DEADLINE_EXCEEDED.withDescription( - "Connect timeout " + readyTimeoutMillis + "ms lapsed"), - true); - } - } - - @Override - public synchronized ClientStream newStream( - final MethodDescriptor method, - final Metadata headers, - final CallOptions callOptions, - ClientStreamTracer[] tracers) { - if (!inState(TransportState.READY)) { - return newFailingClientStream( - isShutdown() - ? shutdownStatus - : Status.INTERNAL.withDescription("newStream() before transportReady()"), - attributes, - headers, - tracers); - } - - int callId = latestCallId++; - if (latestCallId == LAST_CALL_ID) { - latestCallId = FIRST_CALL_ID; - } - StatsTraceContext statsTraceContext = - StatsTraceContext.newClientContext(tracers, attributes, headers); - Inbound.ClientInbound inbound = - new Inbound.ClientInbound( - this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions)); - if (ongoingCalls.putIfAbsent(callId, inbound) != null) { - Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); - shutdownInternal(failure, true); - return newFailingClientStream(failure, attributes, headers, tracers); - } else { - if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { - clientTransportListener.transportInUse(true); - } - Outbound.ClientOutbound outbound = - new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); - if (method.getType().clientSendsOneMessage()) { - return new SingleMessageClientStream(inbound, outbound, attributes); - } else { - return new MultiMessageClientStream(inbound, outbound, attributes); - } - } - } - - @Override - protected void unregisterInbound(Inbound inbound) { - if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) { - clientTransportListener.transportInUse(false); - } - super.unregisterInbound(inbound); - } - - @Override - public void ping(final PingCallback callback, Executor executor) { - pingTracker.startPing(callback, executor); - } - - @Override - public synchronized void shutdown(Status reason) { - checkNotNull(reason, "reason"); - shutdownInternal(reason, false); - } - - @Override - public synchronized void shutdownNow(Status reason) { - checkNotNull(reason, "reason"); - shutdownInternal(reason, true); - } - - @Override - @GuardedBy("this") - void notifyShutdown(Status status) { - clientTransportListener.transportShutdown(status); - } - - @Override - @GuardedBy("this") - void notifyTerminated() { - if (numInUseStreams.getAndSet(0) > 0) { - clientTransportListener.transportInUse(false); - } - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - if (preAuthResultFuture != null) { - preAuthResultFuture.cancel(false); // No effect if already complete. - } - if (authResultFuture != null) { - authResultFuture.cancel(false); // No effect if already complete. - } - serviceBinding.unbind(); - clientTransportListener.transportTerminated(); - } - - @Override - @GuardedBy("this") - protected void handleSetupTransport(Parcel parcel) { - if (inState(TransportState.SETUP)) { - int version = parcel.readInt(); - IBinder binder = parcel.readStrongBinder(); - if (version != WIRE_FORMAT_VERSION) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); - } else if (binder == null) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); - } else { - handshakeImpl.handleSetupTransport(binder); - } - } - } - - private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { - return (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); - } - - class LegacyClientHandshake implements ClientHandshake { - @Override - @MainThread - @GuardedBy("BinderClientTransport.this") - public void onBound(IBinder binder) { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); - } - - @Override - @BinderThread - @GuardedBy("BinderClientTransport.this") - public void handleSetupTransport(IBinder binder) { - int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - synchronized (BinderClientTransport.this) { - handleAuthResult(binder, result); - } - } - - @Override - public void onFailure(Throwable t) { - BinderClientTransport.this.handleAuthResult(t); - } - }, - offloadExecutor); - } - - @GuardedBy("BinderClientTransport.this") - private void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - reportReady(); - } - } - } - } - } - - @GuardedBy("this") - private void reportReady() { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - } - - private synchronized void handleAuthResult(Throwable t) { - shutdownInternal( - Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); - } - - @GuardedBy("this") - @Override - protected void handlePingResponse(Parcel parcel) { - pingTracker.onPingResponse(parcel.readInt()); - } - - private static ClientStream newFailingClientStream( - Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { - StatsTraceContext statsTraceContext = - StatsTraceContext.newClientContext(tracers, attributes, headers); - statsTraceContext.clientOutboundHeaders(); - return new FailingClientStream(failure, tracers); - } - - private static InternalLogId buildLogId( - Context sourceContext, AndroidComponentAddress targetAddress) { - return InternalLogId.allocate( - BinderClientTransport.class, - sourceContext.getClass().getSimpleName() + "->" + targetAddress); - } - - private static Attributes buildClientAttributes( - Attributes eagAttrs, - Context sourceContext, - AndroidComponentAddress targetAddress, - InboundParcelablePolicy inboundParcelablePolicy) { - return Attributes.newBuilder() - .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now. - .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs) - .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext)) - .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress) - .set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy) - .build(); - } - - private static Attributes setSecurityAttrs(Attributes attributes, int uid) { - return attributes.toBuilder() - .set(REMOTE_UID, uid) - .set( - GrpcAttributes.ATTR_SECURITY_LEVEL, - uid == Process.myUid() - ? SecurityLevel.PRIVACY_AND_INTEGRITY - : SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this. - .build(); - } - } - - /** Concrete server-side transport implementation. */ - @Internal - public static final class BinderServerTransport extends BinderTransport - implements ServerTransport { - - private final List streamTracerFactories; - @Nullable private ServerTransportListener serverTransportListener; - - /** - * Constructs a new transport instance. - * - * @param binderDecorator used to decorate 'callbackBinder', for fault injection. - */ - public BinderServerTransport( - ObjectPool executorServicePool, - Attributes attributes, - List streamTracerFactories, - OneWayBinderProxy.Decorator binderDecorator, - IBinder callbackBinder) { - super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); - this.streamTracerFactories = streamTracerFactories; - // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. - setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); - } - - public synchronized void setServerTransportListener( - ServerTransportListener serverTransportListener) { - this.serverTransportListener = serverTransportListener; - if (isShutdown()) { - setState(TransportState.SHUTDOWN_TERMINATED); - notifyTerminated(); - releaseExecutors(); - } else { - sendSetupTransaction(); - // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a - // callback it triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = serverTransportListener.transportReady(attributes); - } - } - } - - StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { - return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); - } - - synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { - if (isShutdown()) { - return Status.UNAVAILABLE.withDescription("transport is shutdown"); - } else { - serverTransportListener.streamCreated(stream, methodName, headers); - return Status.OK; - } - } - - @Override - @GuardedBy("this") - void notifyShutdown(Status status) { - // Nothing to do. - } - - @Override - @GuardedBy("this") - void notifyTerminated() { - if (serverTransportListener != null) { - serverTransportListener.transportTerminated(); - } - } - - @Override - public synchronized void shutdown() { - shutdownInternal(Status.OK, false); - } - - @Override - public synchronized void shutdownNow(Status reason) { - shutdownInternal(reason, true); - } - - @Override - @Nullable - @GuardedBy("this") - protected Inbound createInbound(int callId) { - return new Inbound.ServerInbound(this, attributes, callId); - } - - private static InternalLogId buildLogId(Attributes attributes) { - return InternalLogId.allocate( - BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); - } - } - private static void checkTransition(TransportState current, TransportState next) { switch (next) { case SETUP: diff --git a/binder/src/main/java/io/grpc/binder/internal/Inbound.java b/binder/src/main/java/io/grpc/binder/internal/Inbound.java index 50654297c74..9b9dfeef5ce 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Inbound.java +++ b/binder/src/main/java/io/grpc/binder/internal/Inbound.java @@ -610,10 +610,9 @@ protected void deliverCloseAbnormal(Status status) { // Server-side inbound transactions. static final class ServerInbound extends Inbound { - private final BinderTransport.BinderServerTransport serverTransport; + private final BinderServerTransport serverTransport; - ServerInbound( - BinderTransport.BinderServerTransport transport, Attributes attributes, int callId) { + ServerInbound(BinderServerTransport transport, Attributes attributes, int callId) { super(transport, attributes, callId); this.serverTransport = transport; } diff --git a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java index d47106d1d35..e7e73e6d4b0 100644 --- a/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/BinderServerTransportTest.java @@ -56,12 +56,12 @@ public final class BinderServerTransportTest { @Mock IBinder mockBinder; - BinderTransport.BinderServerTransport transport; + BinderServerTransport transport; @Before public void setUp() throws Exception { transport = - new BinderTransport.BinderServerTransport( + new BinderServerTransport( new FixedObjectPool<>(executorService), Attributes.EMPTY, ImmutableList.of(), diff --git a/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java index e98daeec3ed..f732ff64663 100644 --- a/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java +++ b/binder/src/testFixtures/java/io/grpc/binder/internal/BinderClientTransportBuilder.java @@ -24,8 +24,8 @@ import java.net.SocketAddress; /** - * Helps unit tests create {@link BinderTransport.BinderClientTransport} instances without having to - * mention irrelevant details (go/tott/719). + * Helps unit tests create {@link BinderClientTransport} instances without having to mention + * irrelevant details (go/tott/719). */ public class BinderClientTransportBuilder { private BinderClientTransportFactory factory; @@ -54,7 +54,7 @@ public BinderClientTransportBuilder setFactory(BinderClientTransportFactory fact return this; } - public BinderTransport.BinderClientTransport build() { + public BinderClientTransport build() { return factory.newClientTransport( checkNotNull(serverAddress), checkNotNull(options), checkNotNull(channelLogger)); } From 1e1a69b9bd5893066d2f301b1387a2c8ed1dc74f Mon Sep 17 00:00:00 2001 From: John Cormie Date: Wed, 27 Aug 2025 16:13:47 -0700 Subject: [PATCH 05/34] Implement new handshake --- .../io/grpc/binder/BinderChannelBuilder.java | 24 +++ .../io/grpc/binder/internal/Bindable.java | 13 +- .../internal/BinderClientTransport.java | 154 ++++++++++++++++-- .../BinderClientTransportFactory.java | 9 + .../grpc/binder/internal/ServiceBinding.java | 25 ++- .../RobolectricBinderTransportTest.java | 25 ++- .../binder/internal/ServiceBindingTest.java | 11 +- 7 files changed, 233 insertions(+), 28 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 18928339fbd..c36f0f9728b 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -308,6 +308,30 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { return this; } + /** + * Specifies how and when to authorize the server. + * + *

The legacy client handshake uses the UID of the server process that sent the SETUP_TRANSPORT + * transaction for SecurityPolicy authorization. This is problematic for Android isolated + * processes which run as a completely different UID with none of the hosting app's privs. The new + * handshake checks the server *app*'s UID instead, which allows connections to Services hosted in + * isolated processes like normal. + * + *

In order to learn the UID of the server *process*, the legacy handshake must send its + * secret client Binder to the server before authorizing it. This problematic because it allows a + * malicious man-in-the-middle server to forge responses. + * + *

The default value of this property is true but it will become false in a future release. + * Clients that require a particular behavior should configure it explicitly using this method + * rather than relying on the default. + * + * @return this + */ + public BinderChannelBuilder useLegacyHandshake(boolean legacyHandshake) { + transportFactoryBuilder.setUseLegacyHandshake(legacyHandshake); + return this; + } + @Override public BinderChannelBuilder idleTimeout(long value, TimeUnit unit) { checkState( diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index ae0c7284faf..77676240699 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -16,6 +16,7 @@ package io.grpc.binder.internal; +import android.content.ComponentName; import android.content.pm.ServiceInfo; import android.os.IBinder; import androidx.annotation.AnyThread; @@ -35,7 +36,7 @@ interface Observer { /** We're now bound to the service. Only called once, and only if the binding succeeded. */ @MainThread - void onBound(IBinder binder); + void onBound(ComponentName serviceName, IBinder binder); /** * We've disconnected from (or failed to bind to) the service. This will only be called once, @@ -60,6 +61,16 @@ interface Observer { @AnyThread ServiceInfo resolve() throws StatusException; + /** + * Fetches details about a *connected* remote Service from PackageManager. + * + *

Use the 'serviceName' result from the Observer#onBound callback. The difference + * + * @throws StatusException UNIMPLEMENTED if 'serviceName' isn't found + */ + @AnyThread + ServiceInfo getServiceInfo(ComponentName serviceName) throws StatusException; + /** * Attempt to bind with the remote service. * diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 95bd531aa41..17b1cbb683f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -19,12 +19,15 @@ import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; import android.os.IBinder; import android.os.Parcel; import android.os.Process; +import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.base.Ticker; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -78,6 +81,7 @@ public final class BinderClientTransport extends BinderTransport private final long readyTimeoutMillis; private final PingTracker pingTracker; private final boolean preAuthorizeServer; + private final ClientHandshake handshakeImpl; @Nullable private ManagedClientTransport.Listener clientTransportListener; @@ -124,7 +128,8 @@ public BinderClientTransport( preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - + this.handshakeImpl = + factory.useLegacyHandshake ? new LegacyClientHandshake() : new NewClientHandshake(); serviceBinding = new ServiceBinding( factory.mainThreadExecutor, @@ -145,8 +150,8 @@ void releaseExecutors() { } @Override - public synchronized void onBound(IBinder binder) { - sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + public synchronized void onBound(ComponentName serviceName, IBinder binder) { + handshakeImpl.onBound(serviceName, binder); } @Override @@ -329,8 +334,6 @@ void notifyTerminated() { @Override @GuardedBy("this") protected void handleSetupTransport(Parcel parcel) { - int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); if (inState(TransportState.SETUP)) { int version = parcel.readInt(); IBinder binder = parcel.readStrongBinder(); @@ -340,30 +343,134 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { + handshakeImpl.handleSetupTransport(binder); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + class LegacyClientHandshake extends ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(ComponentName unused, IBinder binder) { + sendSetupTransaction( + binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(IBinder binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); authResultFuture = checkServerAuthorizationAsync(remoteUid); Futures.addCallback( authResultFuture, new FutureCallback() { @Override public void onSuccess(Status result) { - handleAuthResult(binder, result); + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); + } } @Override public void onFailure(Throwable t) { - handleAuthResult(t); + BinderClientTransport.this.handleAuthResult(t); } }, offloadExecutor); } + + @GuardedBy("this") + private void handleAuthResult(IBinder binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + reportReady(); + } + } + } + } } - } - private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { - return (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); - } + class NewClientHandshake extends ClientHandshake { + @Override + @GuardedBy("BinderClientTransport.this") + public void onBound(ComponentName serviceName, IBinder endpointBinder) { + ServiceInfo serviceInfo; + try { + serviceInfo = serviceBinding.getServiceInfo(serviceName); + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } + attributes = setSecurityAttrs(attributes, serviceInfo.applicationInfo.uid); + authResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(endpointBinder, result); + } + } + + @Override + public void onFailure(Throwable t) { + BinderClientTransport.this.handleAuthResult(t); + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(IBinder endpointBinder, Status result) { + if (inState(TransportState.SETUP)) { + if (!result.isOk()) { + shutdownInternal(result, true); + } else { + sendSetupTransaction( + binderDecorator.decorate(OneWayBinderProxy.wrap(endpointBinder, offloadExecutor))); + } + } + } + + @Override + public void handleSetupTransport(IBinder binder) { + if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + reportReady(); + } + } + } + + @GuardedBy("this") + private void reportReady() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } private synchronized void handleAuthResult(IBinder binder, Status authorization) { if (inState(TransportState.SETUP)) { @@ -399,6 +506,27 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + /** + * An abstraction of the client handshake, used to transition off a problematic legacy approach. + */ + abstract class ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server 'endpointBinder'. + */ + @GuardedBy("BinderClientTransport.this") + @MainThread + abstract void onBound(ComponentName unused, IBinder endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @GuardedBy("BinderClientTransport.this") + @BinderThread + abstract void handleSetupTransport(IBinder serverBinder); + } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 3f51452c90c..90c435a7c5d 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -56,6 +56,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final OneWayBinderProxy.Decorator binderDecorator; final long readyTimeoutMillis; final boolean preAuthorizeServers; // TODO(jdcormie): Default to true. + final boolean useLegacyHandshake; ScheduledExecutorService executorService; Executor offloadExecutor; @@ -77,6 +78,7 @@ private BinderClientTransportFactory(Builder builder) { binderDecorator = checkNotNull(builder.binderDecorator); readyTimeoutMillis = builder.readyTimeoutMillis; preAuthorizeServers = builder.preAuthorizeServers; + useLegacyHandshake = builder.useLegacyHandshake; executorService = scheduledExecutorPool.getObject(); offloadExecutor = offloadExecutorPool.getObject(); @@ -131,6 +133,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; long readyTimeoutMillis = 60_000; boolean preAuthorizeServers; + boolean useLegacyHandshake = true; // TODO(jdcormie): Default to false. @Override public BinderClientTransportFactory buildClientTransportFactory() { @@ -229,5 +232,11 @@ public Builder setPreAuthorizeServers(boolean preAuthorizeServers) { this.preAuthorizeServers = preAuthorizeServers; return this; } + + /** Specifies which version of the client handshake to use. */ + public Builder setUseLegacyHandshake(boolean useLegacyHandshake) { + this.useLegacyHandshake = useLegacyHandshake; + return this; + } } } diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 42d69d27a2e..685b9c604fe 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Build; @@ -131,11 +132,11 @@ public String methodName() { } @MainThread - private void notifyBound(IBinder binder) { + private void notifyBound(ComponentName serviceName, IBinder binder) { if (reportedState == State.NOT_BINDING) { reportedState = State.BOUND; logger.log(Level.FINEST, "notify bound - notifying"); - observer.onBound(binder); + observer.onBound(serviceName, binder); } } @@ -336,6 +337,24 @@ private void clearReferences() { sourceContext = null; } + @Override + public ServiceInfo getServiceInfo(ComponentName serviceName) throws StatusException { + try { + Context targetUserContext = + targetUserHandle != null + ? SystemApis.createContextAsUser(sourceContext, targetUserHandle, 0) + : sourceContext; + return targetUserContext.getPackageManager().getServiceInfo(serviceName, 0); + } catch (NameNotFoundException e) { + throw Status.UNIMPLEMENTED.withCause(e).withDescription("impossible race?").asException(); + } catch (ReflectiveOperationException e) { + throw Status.INTERNAL + .withCause(e) + .withDescription("cross user requires SDK>=R") + .asException(); + } + } + @Override @MainThread public void onServiceConnected(ComponentName className, IBinder binder) { @@ -349,7 +368,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { if (bound) { // We call notify directly because we know we're on the main thread already. // (every millisecond counts in this path). - notifyBound(binder); + notifyBound(className, binder); } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 7275c47d51c..16965dee2b1 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -98,11 +98,19 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest private int nextServerAddress; - @Parameter public boolean preAuthServersParam; - - @Parameters(name = "preAuthServersParam={0}") - public static ImmutableList data() { - return ImmutableList.of(true, false); + @Parameter(value = 0) + public boolean preAuthServersParam; + + @Parameter(value = 1) + public boolean useLegacyHandshake; + + @Parameters(name = "preAuthServersParam={0};useLegacyHandshake={1}") + public static ImmutableList data() { + return ImmutableList.of( + new Object[] {false, false}, + new Object[] {false, true}, + new Object[] {true, false}, + new Object[] {true, true}); } @Override @@ -168,6 +176,7 @@ protected InternalServer newServer( BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() { return new BinderClientTransportFactory.Builder() .setPreAuthorizeServers(preAuthServersParam) + .setUseLegacyHandshake(useLegacyHandshake) .setSourceContext(application) .setScheduledExecutorPool(executorServicePool) .setOffloadExecutorPool(offloadExecutorPool); @@ -228,7 +237,11 @@ public void clientAuthorizesServerUidsInOrder() throws Exception { } AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); - assertThat(authRequest.uid).isEqualTo(11111); + if (useLegacyHandshake) { + assertThat(authRequest.uid).isEqualTo(11111); + } else { + assertThat(authRequest.uid).isEqualTo(22222); + } verify(mockClientTransportListener, never()).transportReady(); authRequest.setResult(Status.OK); diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 2079b0eed2c..35686731df8 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -112,6 +112,7 @@ public void testBind() throws Exception { assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); assertThat(observer.gotBoundEvent).isTrue(); assertThat(observer.binder).isSameInstanceAs(mockBinder); + assertThat(observer.serviceName).isEqualTo(serviceComponent); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); } @@ -380,8 +381,8 @@ public void testBindWithDeviceAdmin() throws Exception { allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0); binding = newBuilder() - .setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0)) .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) + .setTargetComponent(serviceComponent) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) .build(); shadowOf(getMainLooper()).idle(); @@ -392,6 +393,7 @@ public void testBindWithDeviceAdmin() throws Exception { assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); assertThat(observer.gotBoundEvent).isTrue(); assertThat(observer.binder).isSameInstanceAs(mockBinder); + assertThat(observer.serviceName).isEqualTo(serviceComponent); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); } @@ -413,10 +415,7 @@ private static void allowBindDeviceAdminForUser( ShadowDevicePolicyManager devicePolicyManager = shadowOf(context.getSystemService(DevicePolicyManager.class)); devicePolicyManager.setDeviceOwner(admin); - devicePolicyManager.setBindDeviceAdminTargetUsers( - Arrays.asList(UserHandle.getUserHandleForUid(userId))); shadowOf((DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE)); - devicePolicyManager.setDeviceOwner(admin); devicePolicyManager.setBindDeviceAdminTargetUsers(Arrays.asList(generateUserHandle(userId))); } @@ -434,16 +433,18 @@ private class TestObserver implements Bindable.Observer { public boolean gotBoundEvent; public IBinder binder; + public ComponentName serviceName; public boolean gotUnboundEvent; public Status unboundReason; @Override - public void onBound(IBinder binder) { + public void onBound(ComponentName serviceName, IBinder binder) { assertThat(gotBoundEvent).isFalse(); assertNoLockHeld(); gotBoundEvent = true; this.binder = binder; + this.serviceName = serviceName; } @Override From 80eee5816434b5fb6eef8373b5890ae6998ca3ba Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 06/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0fe131a0728..39779f7bd7c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -20,12 +20,16 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.util.concurrent.ListenableFuture; From 643144c17912e9bab54a1e02fe1929b7d47647b8 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 07/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 39779f7bd7c..0c9259b91db 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From aedc3bc8e3b7fe132f55f6147c1225056ac4cc4c Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 08/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0c9259b91db..39779f7bd7c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 53bb3c4242da164d46fcfd21d626cd25c2068314 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Fri, 29 Aug 2025 10:37:46 +0100 Subject: [PATCH 09/34] use the new getContextForTargetUser() --- .../grpc/binder/internal/ServiceBinding.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index b7f16be12ab..fce400574d4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -288,8 +288,14 @@ public ServiceInfo resolve() throws StatusException { return resolveInfo.serviceInfo; } + // The returned Context must be *retained* for any ongoing operations, e.g. registerReceiver(). private Context getContextForTargetUser(String purpose) throws StatusException { - checkState(sourceContext != null, "Already unbound!"); + // This call races against unbind(). + Context sourceContext = this.sourceContext; + if (sourceContext == null) { + throw Status.UNAVAILABLE.withDescription("Already unbound!").asException(); + } + try { return targetUserHandle == null ? sourceContext @@ -309,18 +315,11 @@ private void clearReferences() { @Override public ServiceInfo getServiceInfo(ComponentName serviceName) throws StatusException { try { - Context targetUserContext = - targetUserHandle != null - ? SystemApis.createContextAsUser(sourceContext, targetUserHandle, 0) - : sourceContext; - return targetUserContext.getPackageManager().getServiceInfo(serviceName, 0); + return getContextForTargetUser("cross-user v2 handshake") + .getPackageManager() + .getServiceInfo(serviceName, /* flags= */ 0); } catch (NameNotFoundException e) { throw Status.UNIMPLEMENTED.withCause(e).withDescription("impossible race?").asException(); - } catch (ReflectiveOperationException e) { - throw Status.INTERNAL - .withCause(e) - .withDescription("cross user requires SDK>=R") - .asException(); } } From b7eb448264b77647db58e8d9e6a16a3637fd11b8 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Fri, 29 Aug 2025 23:09:03 +0100 Subject: [PATCH 10/34] keep ComponentName private --- .../io/grpc/binder/internal/Bindable.java | 18 +- .../internal/BinderClientTransport.java | 193 ++++++++++-------- .../grpc/binder/internal/ServiceBinding.java | 19 +- .../binder/internal/ServiceBindingTest.java | 9 +- 4 files changed, 133 insertions(+), 106 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index 77676240699..c28301204b5 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -16,7 +16,6 @@ package io.grpc.binder.internal; -import android.content.ComponentName; import android.content.pm.ServiceInfo; import android.os.IBinder; import androidx.annotation.AnyThread; @@ -36,7 +35,7 @@ interface Observer { /** We're now bound to the service. Only called once, and only if the binding succeeded. */ @MainThread - void onBound(ComponentName serviceName, IBinder binder); + void onBound(IBinder binder); /** * We've disconnected from (or failed to bind to) the service. This will only be called once, @@ -62,22 +61,21 @@ interface Observer { ServiceInfo resolve() throws StatusException; /** - * Fetches details about a *connected* remote Service from PackageManager. - * - *

Use the 'serviceName' result from the Observer#onBound callback. The difference + * Attempt to bind with the remote service. * - * @throws StatusException UNIMPLEMENTED if 'serviceName' isn't found + *

Calling this multiple times or after {@link #unbind()} has no effect. */ @AnyThread - ServiceInfo getServiceInfo(ComponentName serviceName) throws StatusException; + void bind(); /** - * Attempt to bind with the remote service. + * Asks PackageManager for details about the remote Service we're connected to. * - *

Calling this multiple times or after {@link #unbind()} has no effect. + * @throws StatusException UNIMPLEMENTED if the connected service isn't found + * @throws IllegalStateException if Observer#onBound() has not "happened-before" this call */ @AnyThread - void bind(); + ServiceInfo getConnectedServiceInfo() throws StatusException; /** * Unbind from the remote service if connected. diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 17b1cbb683f..8026f864813 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -19,7 +19,6 @@ import static io.grpc.binder.ApiConstants.PRE_AUTH_SERVER_OVERRIDE; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; @@ -129,7 +128,7 @@ public BinderClientTransport( numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); this.handshakeImpl = - factory.useLegacyHandshake ? new LegacyClientHandshake() : new NewClientHandshake(); + factory.useLegacyHandshake ? new LegacyClientHandshake() : new NewClientHandshake(); serviceBinding = new ServiceBinding( factory.mainThreadExecutor, @@ -150,8 +149,9 @@ void releaseExecutors() { } @Override - public synchronized void onBound(ComponentName serviceName, IBinder binder) { - handshakeImpl.onBound(serviceName, binder); + public synchronized void onBound(IBinder binder) { + handshakeImpl.onBound( + binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); } @Override @@ -343,7 +343,9 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - handshakeImpl.handleSetupTransport(binder); + OneWayBinderProxy binderProxy = + binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor)); + handshakeImpl.handleSetupTransport(binderProxy); } } } @@ -353,70 +355,86 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - class LegacyClientHandshake extends ClientHandshake { - @Override - @MainThread - @GuardedBy("BinderClientTransport.this") - public void onBound(ComponentName unused, IBinder binder) { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); - } - @Override - @BinderThread - @GuardedBy("BinderClientTransport.this") - public void handleSetupTransport(IBinder binder) { - int remoteUid = Binder.getCallingUid(); - attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - synchronized (BinderClientTransport.this) { - handleAuthResult(binder, result); - } - } + class LegacyClientHandshake extends ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); + } - @Override - public void onFailure(Throwable t) { - BinderClientTransport.this.handleAuthResult(t); + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(OneWayBinderProxy binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); } - }, - offloadExecutor); - } + } - @GuardedBy("this") - private void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - reportReady(); + @Override + public void onFailure(Throwable t) { + BinderClientTransport.this.handleAuthResult(t); } + }, + offloadExecutor); + } + + @GuardedBy("this") + private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(binder)) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + reportReady(); } } } } + } - class NewClientHandshake extends ClientHandshake { - @Override - @GuardedBy("BinderClientTransport.this") - public void onBound(ComponentName serviceName, IBinder endpointBinder) { - ServiceInfo serviceInfo; - try { - serviceInfo = serviceBinding.getServiceInfo(serviceName); - } catch (StatusException e) { - shutdownInternal(e.getStatus(), true); - return; - } + class NewClientHandshake extends ClientHandshake { + @Override + @GuardedBy("BinderClientTransport.this") + public void onBound(OneWayBinderProxy endpointBinder) { + Futures.addCallback( + Futures.submit(serviceBinding::getConnectedServiceInfo, offloadExecutor), + new FutureCallback() { + @Override + public void onSuccess(ServiceInfo result) { + synchronized (BinderClientTransport.this) { + onConnectedServiceInfo(endpointBinder, result); + } + } + + @Override + public void onFailure(Throwable t) { + synchronized (BinderClientTransport.this) { + shutdownInternal(Status.fromThrowable(t), true); + } + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void onConnectedServiceInfo(OneWayBinderProxy endpointBinder, ServiceInfo serviceInfo) { + if (inState(TransportState.SETUP)) { attributes = setSecurityAttrs(attributes, serviceInfo.applicationInfo.uid); authResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); @@ -437,40 +455,41 @@ public void onFailure(Throwable t) { }, offloadExecutor); } + } - @GuardedBy("BinderClientTransport.this") - private void handleAuthResult(IBinder endpointBinder, Status result) { - if (inState(TransportState.SETUP)) { - if (!result.isOk()) { - shutdownInternal(result, true); - } else { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(endpointBinder, offloadExecutor))); - } - } - } - - @Override - public void handleSetupTransport(IBinder binder) { - if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(OneWayBinderProxy endpointBinder, Status authResult) { + if (inState(TransportState.SETUP)) { + if (!authResult.isOk()) { + shutdownInternal(authResult, true); } else { - reportReady(); + sendSetupTransaction(endpointBinder); } } } - @GuardedBy("this") - private void reportReady() { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; + @Override + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(OneWayBinderProxy binder) { + if (!setOutgoingBinder(binder)) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + reportReady(); } } + } + + @GuardedBy("this") + private void reportReady() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } private synchronized void handleAuthResult(IBinder binder, Status authorization) { if (inState(TransportState.SETUP)) { @@ -516,7 +535,7 @@ abstract class ClientHandshake { */ @GuardedBy("BinderClientTransport.this") @MainThread - abstract void onBound(ComponentName unused, IBinder endpointBinder); + abstract void onBound(OneWayBinderProxy endpointBinder); /** * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a @@ -524,7 +543,7 @@ abstract class ClientHandshake { */ @GuardedBy("BinderClientTransport.this") @BinderThread - abstract void handleSetupTransport(IBinder serverBinder); + abstract void handleSetupTransport(OneWayBinderProxy binder); } private static ClientStream newFailingClientStream( diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index fce400574d4..9410fbdcfc8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -103,6 +103,9 @@ public String methodName() { private State reportedState; // Only used on the main thread. + @GuardedBy("this") + private ComponentName connectedServiceName; + @AnyThread ServiceBinding( Executor mainThreadExecutor, @@ -128,11 +131,11 @@ public String methodName() { } @MainThread - private void notifyBound(ComponentName serviceName, IBinder binder) { + private void notifyBound(IBinder binder) { if (reportedState == State.NOT_BINDING) { reportedState = State.BOUND; logger.log(Level.FINEST, "notify bound - notifying"); - observer.onBound(serviceName, binder); + observer.onBound(binder); } } @@ -313,16 +316,21 @@ private void clearReferences() { } @Override - public ServiceInfo getServiceInfo(ComponentName serviceName) throws StatusException { + public ServiceInfo getConnectedServiceInfo() throws StatusException { try { return getContextForTargetUser("cross-user v2 handshake") .getPackageManager() - .getServiceInfo(serviceName, /* flags= */ 0); + .getServiceInfo(getConnectedServiceName(), /* flags= */ 0); } catch (NameNotFoundException e) { throw Status.UNIMPLEMENTED.withCause(e).withDescription("impossible race?").asException(); } } + private synchronized ComponentName getConnectedServiceName() { + checkState(connectedServiceName != null, "onBound() not yet called!"); + return connectedServiceName; + } + @Override @MainThread public void onServiceConnected(ComponentName className, IBinder binder) { @@ -330,13 +338,14 @@ public void onServiceConnected(ComponentName className, IBinder binder) { synchronized (this) { if (state == State.BINDING) { state = State.BOUND; + connectedServiceName = className; bound = true; } } if (bound) { // We call notify directly because we know we're on the main thread already. // (every millisecond counts in this path). - notifyBound(className, binder); + notifyBound(binder); } } diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index f44521cb1f3..0464c098720 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -112,7 +112,6 @@ public void testBind() throws Exception { assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); assertThat(observer.gotBoundEvent).isTrue(); assertThat(observer.binder).isSameInstanceAs(mockBinder); - assertThat(observer.serviceName).isEqualTo(serviceComponent); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); } @@ -402,9 +401,12 @@ public void testBindWithDeviceAdmin() throws Exception { assertThat(shadowApplication.getBoundServiceConnections()).isNotEmpty(); assertThat(observer.gotBoundEvent).isTrue(); assertThat(observer.binder).isSameInstanceAs(mockBinder); - assertThat(observer.serviceName).isEqualTo(serviceComponent); assertThat(observer.gotUnboundEvent).isFalse(); assertThat(binding.isSourceContextCleared()).isFalse(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); } private void assertNoLockHeld() { @@ -448,12 +450,11 @@ private class TestObserver implements Bindable.Observer { public Status unboundReason; @Override - public void onBound(ComponentName serviceName, IBinder binder) { + public void onBound(IBinder binder) { assertThat(gotBoundEvent).isFalse(); assertNoLockHeld(); gotBoundEvent = true; this.binder = binder; - this.serviceName = serviceName; } @Override From c208ab3cfdeb5799db4e1ba39bba42a704240d47 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 00:03:33 +0100 Subject: [PATCH 11/34] factor out the decorate() and wrap() calls common to all handshakes --- .../internal/BinderClientTransport.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index d77e16ed5ce..d4f384a1fe4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -150,7 +150,9 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - handshake.onBound(binder); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + binderProxy = binderDecorator.decorate(binderProxy); + handshake.onBound(binderProxy); } @Override @@ -342,7 +344,8 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - handshake.handleSetupTransport(binder); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + handshake.handleSetupTransport(binderProxy); } } } @@ -357,15 +360,14 @@ class LegacyClientHandshake implements ClientHandshake { @Override @MainThread @GuardedBy("BinderClientTransport.this") - public void onBound(IBinder binder) { - sendSetupTransaction( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); } @Override @BinderThread @GuardedBy("BinderClientTransport.this") - public void handleSetupTransport(IBinder binder) { + public void handleSetupTransport(OneWayBinderProxy binder) { int remoteUid = Binder.getCallingUid(); attributes = setSecurityAttrs(attributes, remoteUid); authResultFuture = checkServerAuthorizationAsync(remoteUid); @@ -388,11 +390,11 @@ public void onFailure(Throwable t) { } @GuardedBy("BinderClientTransport.this") - private void handleAuthResult(IBinder binder, Status authorization) { + private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { if (inState(TransportState.SETUP)) { if (!authorization.isOk()) { shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + } else if (!setOutgoingBinder(binder)) { shutdownInternal( Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); } else { @@ -438,7 +440,7 @@ interface ClientHandshake { */ @GuardedBy("this") @MainThread - void onBound(IBinder endpointBinder); + void onBound(OneWayBinderProxy endpointBinder); /** * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a @@ -446,7 +448,7 @@ interface ClientHandshake { */ @GuardedBy("this") @BinderThread - void handleSetupTransport(IBinder serverBinder); + void handleSetupTransport(OneWayBinderProxy serverBinder); } private static ClientStream newFailingClientStream( From 6ae3230c7e1b0ac328cec7887981399774889085 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 00:25:36 +0100 Subject: [PATCH 12/34] undo some stuff --- .../internal/BinderClientTransport.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 5b9401cb83f..cf25b55171b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -73,6 +73,7 @@ public final class BinderClientTransport extends BinderTransport private final Executor offloadExecutor; private final SecurityPolicy securityPolicy; private final Bindable serviceBinding; + private final ClientHandshake handshakeImpl; /** Number of ongoing calls which keep this transport "in-use". */ private final AtomicInteger numInUseStreams; @@ -80,7 +81,6 @@ public final class BinderClientTransport extends BinderTransport private final long readyTimeoutMillis; private final PingTracker pingTracker; private final boolean preAuthorizeServer; - private final ClientHandshake handshakeImpl; @Nullable private ManagedClientTransport.Listener clientTransportListener; @@ -150,8 +150,9 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - handshakeImpl.onBound( - binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + binderProxy = binderDecorator.decorate(binderProxy); + handshakeImpl.onBound(binderProxy); } @Override @@ -388,7 +389,7 @@ public void onFailure(Throwable t) { offloadExecutor); } - @GuardedBy("this") + @GuardedBy("BinderClientTransport.this") private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { if (inState(TransportState.SETUP)) { if (!authorization.isOk()) { @@ -400,7 +401,7 @@ private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { // Check state again, since a failure inside setOutgoingBinder (or a callback it // triggers), could have shut us down. if (!isShutdown()) { - reportReady(); + onHandshakeComplete(); } } } @@ -469,18 +470,18 @@ private void handleAuthResult(OneWayBinderProxy endpointBinder, Status authResul @Override @GuardedBy("BinderClientTransport.this") - public void handleSetupTransport(OneWayBinderProxy binder) { - if (!setOutgoingBinder(binder)) { + public void handleSetupTransport(OneWayBinderProxy serverBinder) { + if (!setOutgoingBinder(serverBinder)) { shutdownInternal( Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); } else { - reportReady(); + onHandshakeComplete(); } } } @GuardedBy("this") - private void reportReady() { + private void onHandshakeComplete() { setState(TransportState.READY); attributes = clientTransportListener.filterTransport(attributes); clientTransportListener.transportReady(); @@ -542,7 +543,7 @@ abstract class ClientHandshake { */ @GuardedBy("BinderClientTransport.this") @BinderThread - abstract void handleSetupTransport(OneWayBinderProxy binder); + abstract void handleSetupTransport(OneWayBinderProxy serverBinder); } private static ClientStream newFailingClientStream( From 57466ad93bb05499f47164fc33a44ccb6d81c9bc Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 00:27:15 +0100 Subject: [PATCH 13/34] handshakeimpl --- .../io/grpc/binder/internal/BinderClientTransport.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index cf25b55171b..52c21e2fd8c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -73,7 +73,7 @@ public final class BinderClientTransport extends BinderTransport private final Executor offloadExecutor; private final SecurityPolicy securityPolicy; private final Bindable serviceBinding; - private final ClientHandshake handshakeImpl; + private final ClientHandshake handshake; /** Number of ongoing calls which keep this transport "in-use". */ private final AtomicInteger numInUseStreams; @@ -127,7 +127,7 @@ public BinderClientTransport( preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - this.handshakeImpl = + this.handshake = factory.useLegacyHandshake ? new LegacyClientHandshake() : new NewClientHandshake(); serviceBinding = new ServiceBinding( @@ -152,7 +152,7 @@ void releaseExecutors() { public synchronized void onBound(IBinder binder) { OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); binderProxy = binderDecorator.decorate(binderProxy); - handshakeImpl.onBound(binderProxy); + handshake.onBound(binderProxy); } @Override @@ -345,7 +345,7 @@ protected void handleSetupTransport(Parcel parcel) { Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); - handshakeImpl.handleSetupTransport(binderProxy); + handshake.handleSetupTransport(binderProxy); } } } From c1ba7bfef1e8bf683580dd6f565b7b36c427c3bf Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 10:03:32 +0100 Subject: [PATCH 14/34] vestigial --- .../internal/BinderClientTransport.java | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 52c21e2fd8c..a15f6159376 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -491,29 +491,6 @@ private void onHandshakeComplete() { } } - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; - } - } - } - } - } - private synchronized void handleAuthResult(Throwable t) { shutdownInternal( Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); From 118d03e1c49fcc3581f497213989f0047f356157 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 16:23:18 +0100 Subject: [PATCH 15/34] expand javadoc --- .../io/grpc/binder/internal/Bindable.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/Bindable.java b/binder/src/main/java/io/grpc/binder/internal/Bindable.java index c28301204b5..59a2502de2b 100644 --- a/binder/src/main/java/io/grpc/binder/internal/Bindable.java +++ b/binder/src/main/java/io/grpc/binder/internal/Bindable.java @@ -54,8 +54,11 @@ interface Observer { * before giving them a chance to run. However, note that the identity/existence of the resolved * Service can change between the time this method returns and the time you actually bind/connect * to it. For example, suppose the target package gets uninstalled or upgraded right after this - * method returns. In {@link Observer#onBound}, you should verify that the server you resolved is - * the same one you connected to. + * method returns. + * + *

Compare with {@link #getConnectedServiceInfo()}, which can only be called after {@link + * Observer#onBound(IBinder)} but can be used to learn about the service you actually connected + * to. */ @AnyThread ServiceInfo resolve() throws StatusException; @@ -69,10 +72,16 @@ interface Observer { void bind(); /** - * Asks PackageManager for details about the remote Service we're connected to. + * Asks PackageManager for details about the remote Service we *actually* connected to. + * + *

Can only be called after {@link Observer#onBound}. + * + *

Compare with {@link #resolve()}, which reports which service would be selected as of now but + * *without* connecting. * - * @throws StatusException UNIMPLEMENTED if the connected service isn't found - * @throws IllegalStateException if Observer#onBound() has not "happened-before" this call + * @throws StatusException UNIMPLEMENTED if the connected service isn't found (an {@link + * Observer#onUnbound} callback has likely already happened or is on its way!) + * @throws IllegalStateException if {@link Observer#onBound} has not "happened-before" this call */ @AnyThread ServiceInfo getConnectedServiceInfo() throws StatusException; From a5a6057343de6907a30c4b03eef7d1e4257fc91c Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 16/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 6c89c56ffd4..b3987e1ee1f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,12 +21,16 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.util.concurrent.ListenableFuture; From 0400204fad0a1180920c4b383200a11ecd0dd6f2 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 17/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../grpc/binder/internal/BinderTransport.java | 520 ++++++++++++++++++ 1 file changed, 520 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b3987e1ee1f..d4a04e063b0 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,6 +21,7 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; @@ -550,6 +551,525 @@ final void handleAcknowledgedBytes(long numBytes) { } } + /** + * An abstraction of the client handshake, used to transition off a problematic legacy approach. + */ + interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server 'endpointBinder'. + */ + @GuardedBy("this") + @MainThread + void onBound(IBinder endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @GuardedBy("this") + @BinderThread + void handleSetupTransport(IBinder serverBinder); + } + + /** Concrete client-side transport implementation. */ + @ThreadSafe + @Internal + public static final class BinderClientTransport extends BinderTransport + implements ConnectionClientTransport, Bindable.Observer { + + private final ObjectPool offloadExecutorPool; + private final Executor offloadExecutor; + private final SecurityPolicy securityPolicy; + private final Bindable serviceBinding; + + /** Number of ongoing calls which keep this transport "in-use". */ + private final AtomicInteger numInUseStreams; + + private final long readyTimeoutMillis; + private final PingTracker pingTracker; + private final boolean preAuthorizeServer; + private final ClientHandshake handshakeImpl; + + @Nullable private ManagedClientTransport.Listener clientTransportListener; + + @GuardedBy("this") + private int latestCallId = FIRST_CALL_ID; + + @GuardedBy("this") + private ScheduledFuture readyTimeoutFuture; // != null iff timeout scheduled. + @GuardedBy("this") + @Nullable private ListenableFuture authResultFuture; // null before we check auth. + + @GuardedBy("this") + @Nullable + private ListenableFuture preAuthResultFuture; // null before we pre-auth. + + /** + * Constructs a new transport instance. + * + * @param factory parameters common to all a Channel's transports + * @param targetAddress the fully resolved and load-balanced server address + * @param options other parameters that can vary as transports come and go within a Channel + */ + public BinderClientTransport( + BinderClientTransportFactory factory, + AndroidComponentAddress targetAddress, + ClientTransportOptions options) { + super( + factory.scheduledExecutorPool, + buildClientAttributes( + options.getEagAttributes(), + factory.sourceContext, + targetAddress, + factory.inboundParcelablePolicy), + factory.binderDecorator, + buildLogId(factory.sourceContext, targetAddress)); + this.offloadExecutorPool = factory.offloadExecutorPool; + this.securityPolicy = factory.securityPolicy; + this.offloadExecutor = offloadExecutorPool.getObject(); + this.readyTimeoutMillis = factory.readyTimeoutMillis; + Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); + this.preAuthorizeServer = + preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + this.handshakeImpl = new LegacyClientHandshake(); + numInUseStreams = new AtomicInteger(); + pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); + + serviceBinding = + new ServiceBinding( + factory.mainThreadExecutor, + factory.sourceContext, + factory.channelCredentials, + targetAddress.asBindIntent(), + targetAddress.getTargetUser() != null + ? targetAddress.getTargetUser() + : factory.defaultTargetUserHandle, + factory.bindServiceFlags.toInteger(), + this); + } + + @Override + void releaseExecutors() { + super.releaseExecutors(); + offloadExecutorPool.returnObject(offloadExecutor); + } + + @Override + public synchronized void onBound(IBinder endpointBinder) { + handshakeImpl.onBound(endpointBinder); + } + + @Override + public synchronized void onUnbound(Status reason) { + shutdownInternal(reason, true); + } + + @CheckReturnValue + @Override + public synchronized Runnable start(ManagedClientTransport.Listener clientTransportListener) { + this.clientTransportListener = checkNotNull(clientTransportListener); + return () -> { + synchronized (BinderClientTransport.this) { + if (inState(TransportState.NOT_STARTED)) { + setState(TransportState.SETUP); + try { + if (preAuthorizeServer) { + preAuthorize(serviceBinding.resolve()); + } else { + serviceBinding.bind(); + } + } catch (StatusException e) { + shutdownInternal(e.getStatus(), true); + return; + } + if (readyTimeoutMillis >= 0) { + readyTimeoutFuture = + getScheduledExecutorService() + .schedule( + BinderClientTransport.this::onReadyTimeout, + readyTimeoutMillis, + MILLISECONDS); + } + } + } + }; + } + + @GuardedBy("this") + private void preAuthorize(ServiceInfo serviceInfo) { + // It's unlikely, but the identity/existence of this Service could change by the time we + // actually connect. It doesn't matter though, because: + // - If pre-auth fails (but would succeed against the server's new state), the grpc-core layer + // will eventually retry using a new transport instance that will see the Service's new state. + // - If pre-auth succeeds (but would fail against the server's new state), we might give an + // unauthorized server a chance to run, but the connection will still fail by SecurityPolicy + // check later in handshake. Pre-auth remains effective at mitigating abuse because malware + // can't typically control the exact timing of its installation. + preAuthResultFuture = checkServerAuthorizationAsync(serviceInfo.applicationInfo.uid); + Futures.addCallback( + preAuthResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + handlePreAuthResult(result); + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + + private synchronized void handlePreAuthResult(Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else { + serviceBinding.bind(); + } + } + } + + private synchronized void onReadyTimeout() { + if (inState(TransportState.SETUP)) { + readyTimeoutFuture = null; + shutdownInternal( + Status.DEADLINE_EXCEEDED.withDescription( + "Connect timeout " + readyTimeoutMillis + "ms lapsed"), + true); + } + } + + @Override + public synchronized ClientStream newStream( + final MethodDescriptor method, + final Metadata headers, + final CallOptions callOptions, + ClientStreamTracer[] tracers) { + if (!inState(TransportState.READY)) { + return newFailingClientStream( + isShutdown() + ? shutdownStatus + : Status.INTERNAL.withDescription("newStream() before transportReady()"), + attributes, + headers, + tracers); + } + + int callId = latestCallId++; + if (latestCallId == LAST_CALL_ID) { + latestCallId = FIRST_CALL_ID; + } + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + Inbound.ClientInbound inbound = + new Inbound.ClientInbound( + this, attributes, callId, GrpcUtil.shouldBeCountedForInUse(callOptions)); + if (ongoingCalls.putIfAbsent(callId, inbound) != null) { + Status failure = Status.INTERNAL.withDescription("Clashing call IDs"); + shutdownInternal(failure, true); + return newFailingClientStream(failure, attributes, headers, tracers); + } else { + if (inbound.countsForInUse() && numInUseStreams.getAndIncrement() == 0) { + clientTransportListener.transportInUse(true); + } + Outbound.ClientOutbound outbound = + new Outbound.ClientOutbound(this, callId, method, headers, statsTraceContext); + if (method.getType().clientSendsOneMessage()) { + return new SingleMessageClientStream(inbound, outbound, attributes); + } else { + return new MultiMessageClientStream(inbound, outbound, attributes); + } + } + } + + @Override + protected void unregisterInbound(Inbound inbound) { + if (inbound.countsForInUse() && numInUseStreams.decrementAndGet() == 0) { + clientTransportListener.transportInUse(false); + } + super.unregisterInbound(inbound); + } + + @Override + public void ping(final PingCallback callback, Executor executor) { + pingTracker.startPing(callback, executor); + } + + @Override + public synchronized void shutdown(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + checkNotNull(reason, "reason"); + shutdownInternal(reason, true); + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + clientTransportListener.transportShutdown(status); + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (numInUseStreams.getAndSet(0) > 0) { + clientTransportListener.transportInUse(false); + } + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + if (preAuthResultFuture != null) { + preAuthResultFuture.cancel(false); // No effect if already complete. + } + if (authResultFuture != null) { + authResultFuture.cancel(false); // No effect if already complete. + } + serviceBinding.unbind(); + clientTransportListener.transportTerminated(); + } + + @Override + @GuardedBy("this") + protected void handleSetupTransport(Parcel parcel) { + if (inState(TransportState.SETUP)) { + int version = parcel.readInt(); + IBinder binder = parcel.readStrongBinder(); + if (version != WIRE_FORMAT_VERSION) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Wire format version mismatch"), true); + } else if (binder == null) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); + } else { + handshakeImpl.handleSetupTransport(binder); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + + class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(IBinder binder) { + sendSetupTransaction( + binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(IBinder binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); + } + } + + @Override + public void onFailure(Throwable t) { + BinderClientTransport.this.handleAuthResult(t); + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(IBinder binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + reportReady(); + } + } + } + } + } + + @GuardedBy("this") + private void reportReady() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + + private synchronized void handleAuthResult(Throwable t) { + shutdownInternal( + Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); + } + + @GuardedBy("this") + @Override + protected void handlePingResponse(Parcel parcel) { + pingTracker.onPingResponse(parcel.readInt()); + } + + private static ClientStream newFailingClientStream( + Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { + StatsTraceContext statsTraceContext = + StatsTraceContext.newClientContext(tracers, attributes, headers); + statsTraceContext.clientOutboundHeaders(); + return new FailingClientStream(failure, tracers); + } + + private static InternalLogId buildLogId( + Context sourceContext, AndroidComponentAddress targetAddress) { + return InternalLogId.allocate( + BinderClientTransport.class, + sourceContext.getClass().getSimpleName() + "->" + targetAddress); + } + + private static Attributes buildClientAttributes( + Attributes eagAttrs, + Context sourceContext, + AndroidComponentAddress targetAddress, + InboundParcelablePolicy inboundParcelablePolicy) { + return Attributes.newBuilder() + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.NONE) // Trust noone for now. + .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs) + .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, AndroidComponentAddress.forContext(sourceContext)) + .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, targetAddress) + .set(INBOUND_PARCELABLE_POLICY, inboundParcelablePolicy) + .build(); + } + + private static Attributes setSecurityAttrs(Attributes attributes, int uid) { + return attributes.toBuilder() + .set(REMOTE_UID, uid) + .set( + GrpcAttributes.ATTR_SECURITY_LEVEL, + uid == Process.myUid() + ? SecurityLevel.PRIVACY_AND_INTEGRITY + : SecurityLevel.INTEGRITY) // TODO: Have the SecrityPolicy decide this. + .build(); + } + } + + /** Concrete server-side transport implementation. */ + @Internal + public static final class BinderServerTransport extends BinderTransport + implements ServerTransport { + + private final List streamTracerFactories; + @Nullable private ServerTransportListener serverTransportListener; + + /** + * Constructs a new transport instance. + * + * @param binderDecorator used to decorate 'callbackBinder', for fault injection. + */ + public BinderServerTransport( + ObjectPool executorServicePool, + Attributes attributes, + List streamTracerFactories, + OneWayBinderProxy.Decorator binderDecorator, + IBinder callbackBinder) { + super(executorServicePool, attributes, binderDecorator, buildLogId(attributes)); + this.streamTracerFactories = streamTracerFactories; + // TODO(jdcormie): Plumb in the Server's executor() and use it here instead. + setOutgoingBinder(OneWayBinderProxy.wrap(callbackBinder, getScheduledExecutorService())); + } + + public synchronized void setServerTransportListener( + ServerTransportListener serverTransportListener) { + this.serverTransportListener = serverTransportListener; + if (isShutdown()) { + setState(TransportState.SHUTDOWN_TERMINATED); + notifyTerminated(); + releaseExecutors(); + } else { + sendSetupTransaction(); + // Check we're not shutdown again, since a failure inside sendSetupTransaction (or a + // callback it triggers), could have shut us down. + if (!isShutdown()) { + setState(TransportState.READY); + attributes = serverTransportListener.transportReady(attributes); + } + } + } + + StatsTraceContext createStatsTraceContext(String methodName, Metadata headers) { + return StatsTraceContext.newServerContext(streamTracerFactories, methodName, headers); + } + + synchronized Status startStream(ServerStream stream, String methodName, Metadata headers) { + if (isShutdown()) { + return Status.UNAVAILABLE.withDescription("transport is shutdown"); + } else { + serverTransportListener.streamCreated(stream, methodName, headers); + return Status.OK; + } + } + + @Override + @GuardedBy("this") + void notifyShutdown(Status status) { + // Nothing to do. + } + + @Override + @GuardedBy("this") + void notifyTerminated() { + if (serverTransportListener != null) { + serverTransportListener.transportTerminated(); + } + } + + @Override + public synchronized void shutdown() { + shutdownInternal(Status.OK, false); + } + + @Override + public synchronized void shutdownNow(Status reason) { + shutdownInternal(reason, true); + } + + @Override + @Nullable + @GuardedBy("this") + protected Inbound createInbound(int callId) { + return new Inbound.ServerInbound(this, attributes, callId); + } + + private static InternalLogId buildLogId(Attributes attributes) { + return InternalLogId.allocate( + BinderServerTransport.class, "from " + attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)); + } + } + private static void checkTransition(TransportState current, TransportState next) { switch (next) { case SETUP: From 1bb363cac7e6fedb583c5acc7751947ce083d538 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:23:34 -0700 Subject: [PATCH 18/34] don't rename binder --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index d4a04e063b0..f7a928e4875 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -656,8 +656,8 @@ void releaseExecutors() { } @Override - public synchronized void onBound(IBinder endpointBinder) { - handshakeImpl.onBound(endpointBinder); + public synchronized void onBound(IBinder binder) { + handshakeImpl.onBound(binder); } @Override From d77c6f58f20fea75a1e87854f9c06b7b61488206 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 19/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index f7a928e4875..7286d44dad8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,7 +21,6 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 1cfb40bd3dd8127f21e6c840a908a9a39dbdba00 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 20/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 7286d44dad8..f7a928e4875 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,6 +21,7 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From beb8f6e94cbde15ba42be29e5af83840551d0eeb Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 21/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index f7a928e4875..7286d44dad8 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,7 +21,6 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 3baa4b95f00e6bd48192e38e1db290a84adb7f02 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 00:03:33 +0100 Subject: [PATCH 22/34] factor out the decorate() and wrap() calls common to all handshakes --- .../internal/BinderClientTransport.java | 113 +++++++++++++----- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 144ad56eec3..84a75f599e4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -146,7 +146,9 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + binderProxy = binderDecorator.decorate(binderProxy); + handshake.onBound(binderProxy); } @Override @@ -339,14 +341,39 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - restrictIncomingBinderToCallsFrom(remoteUid); - attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + handshake.handleSetupTransport(binderProxy); + } + } + } + + private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { + return (securityPolicy instanceof AsyncSecurityPolicy) + ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) + : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); + } + + class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(OneWayBinderProxy binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { handleAuthResult(binder, result); } @@ -360,35 +387,36 @@ public void onFailure(Throwable t) { } } - private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { - return (securityPolicy instanceof AsyncSecurityPolicy) - ? ((AsyncSecurityPolicy) securityPolicy).checkAuthorizationAsync(remoteUid) - : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); - } - - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(binder)) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + onHandshakeComplete(); } } } } } + @GuardedBy("this") + private void onHandshakeComplete() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + private synchronized void handleAuthResult(Throwable t) { shutdownInternal( Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); @@ -400,6 +428,27 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + /** + * An abstraction of the client handshake, used to transition off a problematic legacy approach. + */ + interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server 'endpointBinder'. + */ + @GuardedBy("this") + @MainThread + void onBound(OneWayBinderProxy endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @GuardedBy("this") + @BinderThread + void handleSetupTransport(OneWayBinderProxy serverBinder); + } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = From 24e3ba1f40d99739a81775bb16310cdc6e17be64 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 23/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 6c89c56ffd4..b3987e1ee1f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,12 +21,16 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.Context; +import android.content.pm.ServiceInfo; +import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.util.concurrent.ListenableFuture; From 9ff81d9f460f1fdf933f1d2cb824f1627c0cecec Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 24/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b3987e1ee1f..b4d4502b23e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,6 +21,7 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 72024dfbffea2ede29746f6e1dda9125bc8ac5de Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 25/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b4d4502b23e..b3987e1ee1f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,7 +21,6 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 0e2e9bc920ead7a0863db699700436c39b83a703 Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:21:15 -0700 Subject: [PATCH 26/34] Pre-factor out the guts of the BinderClientTransport handshake # Conflicts: # binder/src/main/java/io/grpc/binder/internal/BinderTransport.java --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 + 1 file changed, 1 insertion(+) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b3987e1ee1f..b4d4502b23e 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,6 +21,7 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From b60b721c53aa62cb7072ca67fdda4dbb4f10f79f Mon Sep 17 00:00:00 2001 From: John Cormie Date: Tue, 19 Aug 2025 17:31:34 -0700 Subject: [PATCH 27/34] ComponentName --- .../src/main/java/io/grpc/binder/internal/BinderTransport.java | 1 - 1 file changed, 1 deletion(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b4d4502b23e..b3987e1ee1f 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,7 +21,6 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; -import android.content.ComponentName; import android.content.Context; import android.content.pm.ServiceInfo; import android.os.Binder; From 883e47128de3b1fbf46581cb0a6d9a59661aa1cd Mon Sep 17 00:00:00 2001 From: jdcormie Date: Sat, 30 Aug 2025 00:03:33 +0100 Subject: [PATCH 28/34] factor out the decorate() and wrap() calls common to all handshakes --- .../internal/BinderClientTransport.java | 122 +++++++++++++----- 1 file changed, 87 insertions(+), 35 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 144ad56eec3..16a4f60ef02 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -25,6 +25,10 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Process; + +import androidx.annotation.BinderThread; +import androidx.annotation.MainThread; + import com.google.common.base.Ticker; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -146,7 +150,9 @@ void releaseExecutors() { @Override public synchronized void onBound(IBinder binder) { - sendSetupTransaction(binderDecorator.decorate(OneWayBinderProxy.wrap(binder, offloadExecutor))); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + binderProxy = binderDecorator.decorate(binderProxy); + handshake.onBound(binderProxy); } @Override @@ -339,23 +345,8 @@ protected void handleSetupTransport(Parcel parcel) { shutdownInternal( Status.UNAVAILABLE.withDescription("Malformed SETUP_TRANSPORT data"), true); } else { - restrictIncomingBinderToCallsFrom(remoteUid); - attributes = setSecurityAttrs(attributes, remoteUid); - authResultFuture = checkServerAuthorizationAsync(remoteUid); - Futures.addCallback( - authResultFuture, - new FutureCallback() { - @Override - public void onSuccess(Status result) { - handleAuthResult(binder, result); - } - - @Override - public void onFailure(Throwable t) { - handleAuthResult(t); - } - }, - offloadExecutor); + OneWayBinderProxy binderProxy = OneWayBinderProxy.wrap(binder, offloadExecutor); + handshake.handleSetupTransport(binderProxy); } } } @@ -366,29 +357,69 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - private synchronized void handleAuthResult(IBinder binder, Status authorization) { - if (inState(TransportState.SETUP)) { - if (!authorization.isOk()) { - shutdownInternal(authorization, true); - } else if (!setOutgoingBinder(OneWayBinderProxy.wrap(binder, offloadExecutor))) { - shutdownInternal( - Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); - } else { - // Check state again, since a failure inside setOutgoingBinder (or a callback it - // triggers), could have shut us down. - if (!isShutdown()) { - setState(TransportState.READY); - attributes = clientTransportListener.filterTransport(attributes); - clientTransportListener.transportReady(); - if (readyTimeoutFuture != null) { - readyTimeoutFuture.cancel(false); - readyTimeoutFuture = null; + class LegacyClientHandshake implements ClientHandshake { + @Override + @MainThread + @GuardedBy("BinderClientTransport.this") + public void onBound(OneWayBinderProxy binder) { + sendSetupTransaction(binder); + } + + @Override + @BinderThread + @GuardedBy("BinderClientTransport.this") + public void handleSetupTransport(OneWayBinderProxy binder) { + int remoteUid = Binder.getCallingUid(); + attributes = setSecurityAttrs(attributes, remoteUid); + authResultFuture = checkServerAuthorizationAsync(remoteUid); + Futures.addCallback( + authResultFuture, + new FutureCallback() { + @Override + public void onSuccess(Status result) { + synchronized (BinderClientTransport.this) { + handleAuthResult(binder, result); + } + } + + @Override + public void onFailure(Throwable t) { + handleAuthResult(t); + } + }, + offloadExecutor); + } + + @GuardedBy("BinderClientTransport.this") + private void handleAuthResult(OneWayBinderProxy binder, Status authorization) { + if (inState(TransportState.SETUP)) { + if (!authorization.isOk()) { + shutdownInternal(authorization, true); + } else if (!setOutgoingBinder(binder)) { + shutdownInternal( + Status.UNAVAILABLE.withDescription("Failed to observe outgoing binder"), true); + } else { + // Check state again, since a failure inside setOutgoingBinder (or a callback it + // triggers), could have shut us down. + if (!isShutdown()) { + onHandshakeComplete(); } } } } } + @GuardedBy("this") + private void onHandshakeComplete() { + setState(TransportState.READY); + attributes = clientTransportListener.filterTransport(attributes); + clientTransportListener.transportReady(); + if (readyTimeoutFuture != null) { + readyTimeoutFuture.cancel(false); + readyTimeoutFuture = null; + } + } + private synchronized void handleAuthResult(Throwable t) { shutdownInternal( Status.INTERNAL.withDescription("Could not evaluate SecurityPolicy").withCause(t), true); @@ -400,6 +431,27 @@ protected void handlePingResponse(Parcel parcel) { pingTracker.onPingResponse(parcel.readInt()); } + /** + * An abstraction of the client handshake, used to transition off a problematic legacy approach. + */ + interface ClientHandshake { + /** + * Notifies the implementation that the binding has succeeded and we are now connected to the + * server 'endpointBinder'. + */ + @GuardedBy("this") + @MainThread + void onBound(OneWayBinderProxy endpointBinder); + + /** + * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a + * server that can be reached at 'serverBinder'. + */ + @GuardedBy("this") + @BinderThread + void handleSetupTransport(OneWayBinderProxy serverBinder); + } + private static ClientStream newFailingClientStream( Status failure, Attributes attributes, Metadata headers, ClientStreamTracer[] tracers) { StatsTraceContext statsTraceContext = From 25df7e8edb46a62966b3b92f440dcef19d628481 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Tue, 16 Sep 2025 09:45:27 -0700 Subject: [PATCH 29/34] fix imports --- .../main/java/io/grpc/binder/internal/BinderTransport.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index b3987e1ee1f..6c89c56ffd4 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -21,16 +21,12 @@ import static com.google.common.util.concurrent.Futures.immediateFuture; import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; -import android.content.Context; -import android.content.pm.ServiceInfo; -import android.os.Binder; import android.os.DeadObjectException; import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.TransactionTooLargeException; import androidx.annotation.BinderThread; -import androidx.annotation.MainThread; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Verify; import com.google.common.util.concurrent.ListenableFuture; From 208633559c2a1fe5008ce3dd3d5393b4acfa768e Mon Sep 17 00:00:00 2001 From: jdcormie Date: Tue, 16 Sep 2025 11:30:54 -0700 Subject: [PATCH 30/34] interface -> abstract class --- .../grpc/binder/internal/BinderClientTransport.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 8798eba39aa..3976ef206a7 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -356,7 +356,7 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - class LegacyClientHandshake implements ClientHandshake { + class LegacyClientHandshake extends ClientHandshake { @Override @MainThread @GuardedBy("BinderClientTransport.this") @@ -434,22 +434,22 @@ protected void handlePingResponse(Parcel parcel) { /** * An abstraction of the client handshake, used to transition off a problematic legacy approach. */ - interface ClientHandshake { + abstract class ClientHandshake { /** * Notifies the implementation that the binding has succeeded and we are now connected to the * server 'endpointBinder'. */ - @GuardedBy("this") + @GuardedBy("BinderClientTransport.this") @MainThread - void onBound(OneWayBinderProxy endpointBinder); + abstract void onBound(OneWayBinderProxy endpointBinder); /** * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a * server that can be reached at 'serverBinder'. */ - @GuardedBy("this") + @GuardedBy("BinderClientTransport.this") @BinderThread - void handleSetupTransport(OneWayBinderProxy serverBinder); + abstract void handleSetupTransport(OneWayBinderProxy serverBinder); } private static ClientStream newFailingClientStream( From d53fe91df568fd7e6e8ca0a2ede4a414ca61cf89 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Tue, 16 Sep 2025 11:41:36 -0700 Subject: [PATCH 31/34] sync comments --- .../java/io/grpc/binder/internal/BinderClientTransport.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index 3976ef206a7..d6d5f8c977c 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -432,12 +432,14 @@ protected void handlePingResponse(Parcel parcel) { } /** - * An abstraction of the client handshake, used to transition off a problematic legacy approach. + * A base for all implementations of the client handshake. + * + *

Supports a clean migration away from the legacy approach, one client at a time. */ abstract class ClientHandshake { /** * Notifies the implementation that the binding has succeeded and we are now connected to the - * server 'endpointBinder'. + * server's "endpoint" which can be reached at 'endpointBinder'. */ @GuardedBy("BinderClientTransport.this") @MainThread From 77ff0194566563764f6f855a14cafffb817383e2 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Tue, 16 Sep 2025 20:59:28 -0700 Subject: [PATCH 32/34] merge --- .github/workflows/testing.yml | 2 +- MODULE.bazel | 6 +- alts/build.gradle | 1 - android-interop-testing/build.gradle | 2 - .../src/main/AndroidManifest.xml | 7 +- .../integrationtest/TesterActivity.java | 2 +- .../src/main/res/layout/activity_tester.xml | 1 + .../src/main/res/values/strings.xml | 1 + .../java/io/grpc/EquivalentAddressGroup.java | 5 + .../LongUpDownCounterMetricInstrument.java | 32 +++ .../io/grpc/MetricInstrumentRegistry.java | 41 ++++ api/src/main/java/io/grpc/MetricRecorder.java | 25 ++- api/src/main/java/io/grpc/MetricSink.java | 18 +- .../main/java/io/grpc/StatusException.java | 2 + .../java/io/grpc/StatusRuntimeException.java | 2 + authz/build.gradle | 1 - benchmarks/build.gradle | 1 - binder/src/main/AndroidManifest.xml | 13 +- .../io/grpc/binder/BinderChannelBuilder.java | 33 +-- .../internal/BinderClientTransport.java | 11 +- .../BinderClientTransportFactory.java | 10 +- .../grpc/binder/internal/BinderTransport.java | 30 ++- .../binder/internal/LeakSafeOneWayBinder.java | 16 +- .../grpc/binder/internal/ServiceBinding.java | 17 +- .../binder/internal/TransactionUtils.java | 24 +++ .../RobolectricBinderTransportTest.java | 86 +++++++- .../binder/internal/ServiceBindingTest.java | 31 ++- .../binder/internal/TransactionUtilsTest.java | 70 +++++++ buildscripts/kokoro/unix.sh | 5 + buildscripts/make_dependencies.sh | 8 +- compiler/build.gradle | 12 +- compiler/check-artifact.sh | 20 +- .../src/java_plugin/cpp/java_generator.cpp | 32 ++- core/build.gradle | 2 +- .../grpc/internal/AbstractClientStream.java | 3 +- .../grpc/internal/AbstractServerStream.java | 3 +- .../java/io/grpc/internal/AbstractStream.java | 5 +- .../io/grpc/internal/InternalSubchannel.java | 55 ++++- .../main/java/io/grpc/internal/JsonUtil.java | 6 +- .../io/grpc/internal/ManagedChannelImpl.java | 9 +- .../io/grpc/internal/MetricRecorderImpl.java | 29 ++- .../internal/PickFirstLeafLoadBalancer.java | 2 +- .../io/grpc/internal/SubchannelMetrics.java | 189 ++++++++++++++++++ .../grpc/internal/InternalSubchannelTest.java | 154 +++++++++++++- .../grpc/internal/MetricRecorderImplTest.java | 33 ++- cronet/build.gradle | 1 + examples/android/clientcache/app/build.gradle | 1 - examples/android/clientcache/settings.gradle | 16 ++ examples/android/helloworld/app/build.gradle | 1 - examples/android/helloworld/settings.gradle | 16 ++ examples/android/routeguide/app/build.gradle | 1 - examples/android/routeguide/settings.gradle | 16 ++ examples/android/strictmode/app/build.gradle | 1 - examples/android/strictmode/settings.gradle | 16 ++ examples/build.gradle | 1 - examples/example-alts/build.gradle | 1 - examples/example-alts/settings.gradle | 14 ++ examples/example-debug/build.gradle | 1 - examples/example-debug/pom.xml | 6 - examples/example-debug/settings.gradle | 16 ++ examples/example-dualstack/build.gradle | 1 - examples/example-dualstack/pom.xml | 6 - examples/example-dualstack/settings.gradle | 14 ++ examples/example-gauth/build.gradle | 1 - examples/example-gauth/pom.xml | 6 - examples/example-gauth/settings.gradle | 14 ++ .../build.gradle | 1 - .../settings.gradle | 16 ++ .../example-gcp-observability/build.gradle | 1 - .../example-gcp-observability/settings.gradle | 16 ++ examples/example-hostname/build.gradle | 1 - examples/example-hostname/pom.xml | 6 - examples/example-hostname/settings.gradle | 16 ++ examples/example-jwt-auth/build.gradle | 2 - examples/example-jwt-auth/pom.xml | 6 - examples/example-jwt-auth/settings.gradle | 14 ++ examples/example-oauth/build.gradle | 2 - examples/example-oauth/pom.xml | 6 - examples/example-oauth/settings.gradle | 14 ++ examples/example-opentelemetry/build.gradle | 1 - .../example-opentelemetry/settings.gradle | 16 ++ examples/example-orca/build.gradle | 2 - examples/example-orca/settings.gradle | 16 ++ examples/example-reflection/build.gradle | 2 - examples/example-reflection/settings.gradle | 16 ++ examples/example-servlet/build.gradle | 3 +- examples/example-servlet/settings.gradle | 14 ++ examples/example-tls/build.gradle | 1 - examples/example-tls/pom.xml | 6 - examples/example-tls/settings.gradle | 14 ++ examples/example-xds/build.gradle | 1 - examples/example-xds/settings.gradle | 16 ++ examples/pom.xml | 6 - examples/settings.gradle | 14 ++ gradle/libs.versions.toml | 17 +- grpclb/build.gradle | 1 - .../AnonymousInProcessSocketAddress.java | 7 + interop-testing/build.gradle | 1 - istio-interop-testing/build.gradle | 2 - lint.xml | 5 + .../netty/GrpcHttp2ConnectionHandler.java | 1 + .../io/grpc/netty/NettyServerHandler.java | 19 +- .../grpc/netty/NettyClientTransportTest.java | 71 ++++++- .../io/grpc/netty/NettyServerHandlerTest.java | 4 +- .../io/grpc/okhttp/OkHttpServerTransport.java | 14 +- .../okhttp/OkHttpServerTransportTest.java | 19 +- .../OpenTelemetryMetricSink.java | 23 +++ .../internal/OpenTelemetryConstants.java | 6 + .../OpenTelemetryMetricSinkTest.java | 68 ++++++- repositories.bzl | 6 +- rls/build.gradle | 1 - s2a/build.gradle | 2 +- services/build.gradle | 2 - servlet/build.gradle | 3 +- servlet/jakarta/build.gradle | 3 +- .../io/grpc/servlet/JettyTransportTest.java | 1 + .../java/io/grpc/servlet/GrpcServlet.java | 1 + .../java/io/grpc/servlet/ServletAdapter.java | 22 +- .../io/grpc/servlet/ServletServerBuilder.java | 19 +- .../servlet/ServletServerBuilderTest.java | 2 +- .../io/grpc/servlet/TomcatTransportTest.java | 4 +- .../grpc/servlet/UndertowTransportTest.java | 4 +- settings.gradle | 13 ++ .../main/java/io/grpc/stub/ClientCalls.java | 6 +- testing-proto/build.gradle | 2 - .../util/AdvancedTlsX509TrustManager.java | 4 + .../io/grpc/util/MultiChildLoadBalancer.java | 2 + .../util/AdvancedTlsX509TrustManagerTest.java | 11 + xds/build.gradle | 1 - .../io/grpc/xds/ClusterImplLoadBalancer.java | 2 +- .../grpc/xds/ClusterResolverLoadBalancer.java | 4 +- .../main/java/io/grpc/xds/MessagePrinter.java | 2 + .../io/grpc/xds/WrrLocalityLoadBalancer.java | 2 +- .../main/java/io/grpc/xds/XdsAttributes.java | 7 - .../grpc/xds/ClusterImplLoadBalancerTest.java | 2 +- .../grpc/xds/GrpcXdsClientImplTestBase.java | 63 +++--- .../grpc/xds/WrrLocalityLoadBalancerTest.java | 2 +- 137 files changed, 1615 insertions(+), 272 deletions(-) create mode 100644 api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java create mode 100644 binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java create mode 100644 core/src/main/java/io/grpc/internal/SubchannelMetrics.java diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 0f099cbcac7..4fe75b0be78 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - jre: [8, 11, 17] + jre: [8, 11, 17, 21] fail-fast: false # Should swap to true if we grow a large matrix steps: diff --git a/MODULE.bazel b/MODULE.bazel index 7d49c4e4b49..4465f46e6c5 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,9 +15,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.30.0", + "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.3.1-android", + "com.google.guava:guava:33.4.8-android", "com.google.re2j:re2j:1.8", "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", @@ -41,7 +41,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.checkerframework:checker-qual:3.12.0", + "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END diff --git a/alts/build.gradle b/alts/build.gradle index 3e472d9cea6..fe2e27784fc 100644 --- a/alts/build.gradle +++ b/alts/build.gradle @@ -22,7 +22,6 @@ dependencies { libraries.guava.jre, // JRE required by protobuf-java-util from grpclb libraries.google.auth.oauth2Http def nettyDependency = implementation project(':grpc-netty') - compileOnly libraries.javax.annotation shadow configurations.implementation.getDependencies().minus(nettyDependency) shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') diff --git a/android-interop-testing/build.gradle b/android-interop-testing/build.gradle index 72b6ac0a302..17551465f05 100644 --- a/android-interop-testing/build.gradle +++ b/android-interop-testing/build.gradle @@ -83,8 +83,6 @@ dependencies { exclude group: 'com.google.guava' } - compileOnly libraries.javax.annotation - androidTestImplementation 'androidx.test.ext:junit:1.1.3', 'androidx.test:runner:1.4.0' } diff --git a/android-interop-testing/src/main/AndroidManifest.xml b/android-interop-testing/src/main/AndroidManifest.xml index 35f3ee33a2b..da7ccef5b1d 100644 --- a/android-interop-testing/src/main/AndroidManifest.xml +++ b/android-interop-testing/src/main/AndroidManifest.xml @@ -5,7 +5,9 @@ - + + android:exported="true"> diff --git a/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java index fb5b35c42d5..17c7e24cbfa 100644 --- a/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java +++ b/android-interop-testing/src/main/java/io/grpc/android/integrationtest/TesterActivity.java @@ -121,7 +121,7 @@ private void startTest(String testCase) { ((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow( hostEdit.getWindowToken(), 0); enableButtons(false); - resultText.setText("Testing..."); + resultText.setText(R.string.testing_message); String host = hostEdit.getText().toString(); String portStr = portEdit.getText().toString(); diff --git a/android-interop-testing/src/main/res/layout/activity_tester.xml b/android-interop-testing/src/main/res/layout/activity_tester.xml index e25bd1bb6f6..042da6437c0 100644 --- a/android-interop-testing/src/main/res/layout/activity_tester.xml +++ b/android-interop-testing/src/main/res/layout/activity_tester.xml @@ -16,6 +16,7 @@ android:layout_weight="2" android:layout_width="0dp" android:layout_height="wrap_content" + android:inputType="text" android:hint="Enter Host" /> gRPC Integration Test + Testing… diff --git a/api/src/main/java/io/grpc/EquivalentAddressGroup.java b/api/src/main/java/io/grpc/EquivalentAddressGroup.java index 4b3db006684..bf8a864902c 100644 --- a/api/src/main/java/io/grpc/EquivalentAddressGroup.java +++ b/api/src/main/java/io/grpc/EquivalentAddressGroup.java @@ -50,6 +50,11 @@ public final class EquivalentAddressGroup { @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138") public static final Attributes.Key ATTR_AUTHORITY_OVERRIDE = Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE"); + /** + * The name of the locality that this EquivalentAddressGroup is in. + */ + public static final Attributes.Key ATTR_LOCALITY_NAME = + Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY"); private final List addrs; private final Attributes attrs; diff --git a/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java new file mode 100644 index 00000000000..07e099cde5d --- /dev/null +++ b/api/src/main/java/io/grpc/LongUpDownCounterMetricInstrument.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc; + +import java.util.List; + +/** + * Represents a long-valued up down counter metric instrument. + */ +@Internal +public final class LongUpDownCounterMetricInstrument extends PartialMetricInstrument { + public LongUpDownCounterMetricInstrument(int index, String name, String description, String unit, + List requiredLabelKeys, + List optionalLabelKeys, + boolean enableByDefault) { + super(index, name, description, unit, requiredLabelKeys, optionalLabelKeys, enableByDefault); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java index 1b33ed17a71..ce0f8f1b5cb 100644 --- a/api/src/main/java/io/grpc/MetricInstrumentRegistry.java +++ b/api/src/main/java/io/grpc/MetricInstrumentRegistry.java @@ -144,6 +144,47 @@ public LongCounterMetricInstrument registerLongCounter(String name, } } + /** + * Registers a new Long Up Down Counter metric instrument. + * + * @param name the name of the metric + * @param description a description of the metric + * @param unit the unit of measurement for the metric + * @param requiredLabelKeys a list of required label keys + * @param optionalLabelKeys a list of optional label keys + * @param enableByDefault whether the metric should be enabled by default + * @return the newly created LongUpDownCounterMetricInstrument + * @throws IllegalStateException if a metric with the same name already exists + */ + public LongUpDownCounterMetricInstrument registerLongUpDownCounter(String name, + String description, + String unit, + List requiredLabelKeys, + List optionalLabelKeys, + boolean enableByDefault) { + checkArgument(!Strings.isNullOrEmpty(name), "missing metric name"); + checkNotNull(description, "description"); + checkNotNull(unit, "unit"); + checkNotNull(requiredLabelKeys, "requiredLabelKeys"); + checkNotNull(optionalLabelKeys, "optionalLabelKeys"); + synchronized (lock) { + if (registeredMetricNames.contains(name)) { + throw new IllegalStateException("Metric with name " + name + " already exists"); + } + int index = nextAvailableMetricIndex; + if (index + 1 == metricInstruments.length) { + resizeMetricInstruments(); + } + LongUpDownCounterMetricInstrument instrument = new LongUpDownCounterMetricInstrument( + index, name, description, unit, requiredLabelKeys, optionalLabelKeys, + enableByDefault); + metricInstruments[index] = instrument; + registeredMetricNames.add(name); + nextAvailableMetricIndex += 1; + return instrument; + } + } + /** * Registers a new Double Histogram metric instrument. * diff --git a/api/src/main/java/io/grpc/MetricRecorder.java b/api/src/main/java/io/grpc/MetricRecorder.java index d418dcbf590..897c28011cd 100644 --- a/api/src/main/java/io/grpc/MetricRecorder.java +++ b/api/src/main/java/io/grpc/MetricRecorder.java @@ -50,7 +50,7 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do * Adds a value for a long valued counter metric instrument. * * @param metricInstrument The counter metric instrument to add the value against. - * @param value The value to add. + * @param value The value to add. MUST be non-negative. * @param requiredLabelValues A list of required label values for the metric. * @param optionalLabelValues A list of additional, optional label values for the metric. */ @@ -66,6 +66,29 @@ default void addLongCounter(LongCounterMetricInstrument metricInstrument, long v metricInstrument.getOptionalLabelKeys().size()); } + /** + * Adds a value for a long valued up down counter metric instrument. + * + * @param metricInstrument The counter metric instrument to add the value against. + * @param value The value to add. May be positive, negative or zero. + * @param requiredLabelValues A list of required label values for the metric. + * @param optionalLabelValues A list of additional, optional label values for the metric. + */ + default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, + long value, + List requiredLabelValues, + List optionalLabelValues) { + checkArgument(requiredLabelValues != null + && requiredLabelValues.size() == metricInstrument.getRequiredLabelKeys().size(), + "Incorrect number of required labels provided. Expected: %s", + metricInstrument.getRequiredLabelKeys().size()); + checkArgument(optionalLabelValues != null + && optionalLabelValues.size() == metricInstrument.getOptionalLabelKeys().size(), + "Incorrect number of optional labels provided. Expected: %s", + metricInstrument.getOptionalLabelKeys().size()); + } + + /** * Records a value for a double-precision histogram metric instrument. * diff --git a/api/src/main/java/io/grpc/MetricSink.java b/api/src/main/java/io/grpc/MetricSink.java index 0f56b1acb73..ce5d3822520 100644 --- a/api/src/main/java/io/grpc/MetricSink.java +++ b/api/src/main/java/io/grpc/MetricSink.java @@ -65,12 +65,26 @@ default void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, do * Adds a value for a long valued counter metric associated with specified metric instrument. * * @param metricInstrument The counter metric instrument identifies metric measure to add. - * @param value The value to record. + * @param value The value to record. MUST be non-negative. * @param requiredLabelValues A list of required label values for the metric. * @param optionalLabelValues A list of additional, optional label values for the metric. */ default void addLongCounter(LongCounterMetricInstrument metricInstrument, long value, - List requiredLabelValues, List optionalLabelValues) { + List requiredLabelValues, List optionalLabelValues) { + } + + /** + * Adds a value for a long valued up down counter metric associated with specified metric + * instrument. + * + * @param metricInstrument The counter metric instrument identifies metric measure to add. + * @param value The value to record. May be positive, negative or zero. + * @param requiredLabelValues A list of required label values for the metric. + * @param optionalLabelValues A list of additional, optional label values for the metric. + */ + default void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { } /** diff --git a/api/src/main/java/io/grpc/StatusException.java b/api/src/main/java/io/grpc/StatusException.java index f9416bf72e3..2a235c3aaaf 100644 --- a/api/src/main/java/io/grpc/StatusException.java +++ b/api/src/main/java/io/grpc/StatusException.java @@ -25,7 +25,9 @@ */ public class StatusException extends Exception { private static final long serialVersionUID = -660954903976144640L; + @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913 private final Status status; + @SuppressWarnings("serial") private final Metadata trailers; /** diff --git a/api/src/main/java/io/grpc/StatusRuntimeException.java b/api/src/main/java/io/grpc/StatusRuntimeException.java index dd22d6b2486..ebcc2f0d671 100644 --- a/api/src/main/java/io/grpc/StatusRuntimeException.java +++ b/api/src/main/java/io/grpc/StatusRuntimeException.java @@ -26,7 +26,9 @@ public class StatusRuntimeException extends RuntimeException { private static final long serialVersionUID = 1950934672280720624L; + @SuppressWarnings("serial") // https://github.com/grpc/grpc-java/issues/1913 private final Status status; + @SuppressWarnings("serial") private final Metadata trailers; /** diff --git a/authz/build.gradle b/authz/build.gradle index b72088bfbaa..4b02b01aa29 100644 --- a/authz/build.gradle +++ b/authz/build.gradle @@ -15,7 +15,6 @@ dependencies { libraries.guava.jre // JRE required by transitive protobuf-java-util annotationProcessor libraries.auto.value - compileOnly libraries.javax.annotation testImplementation project(':grpc-testing'), project(':grpc-testing-proto'), diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index bf043106050..88b26397e78 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -38,7 +38,6 @@ dependencies { classifier = "linux-x86_64" } } - compileOnly libraries.javax.annotation testImplementation libraries.junit, libraries.mockito.core diff --git a/binder/src/main/AndroidManifest.xml b/binder/src/main/AndroidManifest.xml index a30cbbdd6fa..239c3b39b38 100644 --- a/binder/src/main/AndroidManifest.xml +++ b/binder/src/main/AndroidManifest.xml @@ -1,2 +1,11 @@ - - + + + + + + + + + + + \ No newline at end of file diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index c36f0f9728b..6d656aaf560 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -309,26 +309,33 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { } /** - * Specifies how and when to authorize the server. + * Specifies how and when to authorize a server against this channel's {@link SecurityPolicy}. * - *

The legacy client handshake uses the UID of the server process that sent the SETUP_TRANSPORT - * transaction for SecurityPolicy authorization. This is problematic for Android isolated - * processes which run as a completely different UID with none of the hosting app's privs. The new - * handshake checks the server *app*'s UID instead, which allows connections to Services hosted in - * isolated processes like normal. + *

The legacy authorization strategy considers the UID of the server *process* we connect to. + * This is problematic for services using the `android:isolatedProcess` feature which runs them + * under a different UID and without any of the privileges of the hosting app. The new and + * improved strategy uses the server *app's* UID instead, which lets clients authorize all types + * of servers in the same way, isolated or not. * - *

In order to learn the UID of the server *process*, the legacy handshake must send its - * secret client Binder to the server before authorizing it. This problematic because it allows a - * malicious man-in-the-middle server to forge responses. + *

The new and improved authorization strategy performs SecurityPolicy checks earlier the + * handshake, which allows subsequent transactions over the connection to proceed securely without + * further UID checks. + * + *

The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. * *

The default value of this property is true but it will become false in a future release. - * Clients that require a particular behavior should configure it explicitly using this method - * rather than relying on the default. + * Clients that require a particular authorization strategy should configure it explicitly using + * this method rather than relying on the default. Eventually support for the legacy behavior will + * be removed. + * + *

If moving to the new authorization strategy causes a robolectric test to fail, ensure your + * fake service component is registered with `ShadowPackageManager` using `addOrUpdateService()`. * * @return this */ - public BinderChannelBuilder useLegacyHandshake(boolean legacyHandshake) { - transportFactoryBuilder.setUseLegacyHandshake(legacyHandshake); + public BinderChannelBuilder useLegacyAuthStrategy(boolean useLegacyAuthStrategy) { + transportFactoryBuilder.setUseLegacyAuthStrategy(useLegacyAuthStrategy); return this; } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index a15f6159376..c264ab24329 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -125,10 +125,10 @@ public BinderClientTransport( Boolean preAuthServerOverride = options.getEagAttributes().get(PRE_AUTH_SERVER_OVERRIDE); this.preAuthorizeServer = preAuthServerOverride != null ? preAuthServerOverride : factory.preAuthorizeServers; + this.handshake = + factory.useLegacyAuthStrategy ? new LegacyClientHandshake() : new NewClientHandshake(); numInUseStreams = new AtomicInteger(); pingTracker = new PingTracker(Ticker.systemTicker(), (id) -> sendPing(id)); - this.handshake = - factory.useLegacyHandshake ? new LegacyClientHandshake() : new NewClientHandshake(); serviceBinding = new ServiceBinding( factory.mainThreadExecutor, @@ -369,6 +369,7 @@ public void onBound(OneWayBinderProxy binder) { @GuardedBy("BinderClientTransport.this") public void handleSetupTransport(OneWayBinderProxy binder) { int remoteUid = Binder.getCallingUid(); + restrictIncomingBinderToCallsFrom(remoteUid); attributes = setSecurityAttrs(attributes, remoteUid); authResultFuture = checkServerAuthorizationAsync(remoteUid); Futures.addCallback( @@ -503,12 +504,14 @@ protected void handlePingResponse(Parcel parcel) { } /** - * An abstraction of the client handshake, used to transition off a problematic legacy approach. + * A base for all implementations of the client handshake. + * + *

Supports a clean migration away from the legacy approach, one client at a time. */ abstract class ClientHandshake { /** * Notifies the implementation that the binding has succeeded and we are now connected to the - * server 'endpointBinder'. + * server's "endpoint" which can be reached at 'endpointBinder'. */ @GuardedBy("BinderClientTransport.this") @MainThread diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java index 90c435a7c5d..a22453fd601 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransportFactory.java @@ -56,7 +56,7 @@ public final class BinderClientTransportFactory implements ClientTransportFactor final OneWayBinderProxy.Decorator binderDecorator; final long readyTimeoutMillis; final boolean preAuthorizeServers; // TODO(jdcormie): Default to true. - final boolean useLegacyHandshake; + final boolean useLegacyAuthStrategy; ScheduledExecutorService executorService; Executor offloadExecutor; @@ -78,7 +78,7 @@ private BinderClientTransportFactory(Builder builder) { binderDecorator = checkNotNull(builder.binderDecorator); readyTimeoutMillis = builder.readyTimeoutMillis; preAuthorizeServers = builder.preAuthorizeServers; - useLegacyHandshake = builder.useLegacyHandshake; + useLegacyAuthStrategy = builder.useLegacyAuthStrategy; executorService = scheduledExecutorPool.getObject(); offloadExecutor = offloadExecutorPool.getObject(); @@ -133,7 +133,7 @@ public static final class Builder implements ClientTransportFactoryBuilder { OneWayBinderProxy.Decorator binderDecorator = OneWayBinderProxy.IDENTITY_DECORATOR; long readyTimeoutMillis = 60_000; boolean preAuthorizeServers; - boolean useLegacyHandshake = true; // TODO(jdcormie): Default to false. + boolean useLegacyAuthStrategy = true; // TODO(jdcormie): Default to false. @Override public BinderClientTransportFactory buildClientTransportFactory() { @@ -234,8 +234,8 @@ public Builder setPreAuthorizeServers(boolean preAuthorizeServers) { } /** Specifies which version of the client handshake to use. */ - public Builder setUseLegacyHandshake(boolean useLegacyHandshake) { - this.useLegacyHandshake = useLegacyHandshake; + public Builder setUseLegacyAuthStrategy(boolean useLegacyAuthStrategy) { + this.useLegacyAuthStrategy = useLegacyAuthStrategy; return this; } } diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java index 0fe131a0728..a7ca5b25b24 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderTransport.java @@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.util.concurrent.Futures.immediateFuture; +import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; import android.os.DeadObjectException; import android.os.IBinder; @@ -31,12 +32,15 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.Attributes; +import io.grpc.Grpc; import io.grpc.Internal; +import io.grpc.InternalChannelz; import io.grpc.InternalChannelz.SocketStats; import io.grpc.InternalLogId; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.binder.InboundParcelablePolicy; +import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler; import io.grpc.internal.ObjectPool; import java.util.ArrayList; import java.util.Iterator; @@ -153,6 +157,7 @@ protected enum TransportState { private final ObjectPool executorServicePool; private final ScheduledExecutorService scheduledExecutorService; private final InternalLogId logId; + @GuardedBy("this") private final LeakSafeOneWayBinder incomingBinder; protected final ConcurrentHashMap> ongoingCalls; @@ -205,7 +210,15 @@ public final ScheduledExecutorService getScheduledExecutorService() { // Override in child class. public final ListenableFuture getStats() { - return immediateFuture(null); + Attributes attributes = getAttributes(); + return immediateFuture( + new InternalChannelz.SocketStats( + /* data= */ null, // TODO: Keep track of these stats with TransportTracer or similar. + /* local= */ attributes.get(Grpc.TRANSPORT_ATTR_LOCAL_ADDR), + /* remote= */ attributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR), + // TODO: SocketOptions are meaningless for binder but we're still forced to provide one. + new InternalChannelz.SocketOptions.Builder().build(), + /* security= */ null)); } // Override in child class. @@ -466,6 +479,15 @@ private boolean handleTransactionInternal(int code, Parcel parcel) { } } + @BinderThread + @GuardedBy("this") + protected void restrictIncomingBinderToCallsFrom(int allowedCallingUid) { + TransactionHandler currentHandler = incomingBinder.getHandler(); + if (currentHandler != null) { + incomingBinder.setHandler(newCallerFilteringHandler(allowedCallingUid, currentHandler)); + } + } + @Nullable @GuardedBy("this") protected Inbound createInbound(int callId) { @@ -551,6 +573,12 @@ Map> getOngoingCalls() { return ongoingCalls; } + @VisibleForTesting + @SuppressWarnings("GuardedBy") + LeakSafeOneWayBinder getIncomingBinderForTesting() { + return this.incomingBinder; + } + private static Status statusFromRemoteException(RemoteException e) { if (e instanceof DeadObjectException || e instanceof TransactionTooLargeException) { // These are to be expected from time to time and can simply be retried. diff --git a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java index e7837b520f8..c36bc7d5bd3 100644 --- a/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java +++ b/binder/src/main/java/io/grpc/binder/internal/LeakSafeOneWayBinder.java @@ -73,7 +73,21 @@ public void detach() { setHandler(null); } - /** Replaces the current {@link TransactionHandler} with `handler`. */ + /** Returns the current {@link TransactionHandler} or null if already detached. */ + public @Nullable TransactionHandler getHandler() { + return handler; + } + + /** + * Replaces the current {@link TransactionHandler} with `handler`. + * + *

{@link TransactionHandler} mutations race against incoming transactions except in the + * special case where the caller is already handling an incoming transaction on this same {@link + * LeakSafeOneWayBinder} instance. In that case, mutations are safe and the provided 'handler' is + * guaranteed to be used for the very next transaction. This follows from the one-at-a-time + * property of one-way Binder transactions as explained by {@link + * TransactionHandler#handleTransaction}. + */ public void setHandler(@Nullable TransactionHandler handler) { this.handler = handler; } diff --git a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java index 9410fbdcfc8..4b6bf7d06fb 100644 --- a/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java +++ b/binder/src/main/java/io/grpc/binder/internal/ServiceBinding.java @@ -25,7 +25,6 @@ import android.content.Intent; import android.content.ServiceConnection; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Build; @@ -291,14 +290,8 @@ public ServiceInfo resolve() throws StatusException { return resolveInfo.serviceInfo; } - // The returned Context must be *retained* for any ongoing operations, e.g. registerReceiver(). private Context getContextForTargetUser(String purpose) throws StatusException { - // This call races against unbind(). - Context sourceContext = this.sourceContext; - if (sourceContext == null) { - throw Status.UNAVAILABLE.withDescription("Already unbound!").asException(); - } - + checkState(sourceContext != null, "Already unbound!"); try { return targetUserHandle == null ? sourceContext @@ -315,14 +308,18 @@ private void clearReferences() { sourceContext = null; } + @AnyThread @Override public ServiceInfo getConnectedServiceInfo() throws StatusException { try { return getContextForTargetUser("cross-user v2 handshake") .getPackageManager() .getServiceInfo(getConnectedServiceName(), /* flags= */ 0); - } catch (NameNotFoundException e) { - throw Status.UNIMPLEMENTED.withCause(e).withDescription("impossible race?").asException(); + } catch (PackageManager.NameNotFoundException e) { + throw Status.UNIMPLEMENTED + .withCause(e) + .withDescription("connected remote service was uninstalled/disabled during handshake") + .asException(); } } diff --git a/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java b/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java index c962554d125..2777a78d4ac 100644 --- a/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java +++ b/binder/src/main/java/io/grpc/binder/internal/TransactionUtils.java @@ -16,9 +16,13 @@ package io.grpc.binder.internal; +import android.os.Binder; import android.os.Parcel; import io.grpc.MethodDescriptor.MethodType; import io.grpc.Status; +import java.util.logging.Level; +import java.util.logging.Logger; +import io.grpc.binder.internal.LeakSafeOneWayBinder.TransactionHandler; import javax.annotation.Nullable; /** Constants and helpers for managing inbound / outbound transactions. */ @@ -99,4 +103,24 @@ static void fillInFlags(Parcel parcel, int flags) { parcel.writeInt(flags); parcel.setDataPosition(pos); } + + /** + * Decorates the given {@link TransactionHandler} with a wrapper that only forwards transactions + * from the given `allowedCallingUid`. + */ + static TransactionHandler newCallerFilteringHandler( + int allowedCallingUid, TransactionHandler wrapped) { + final Logger logger = Logger.getLogger(TransactionUtils.class.getName()); + return new TransactionHandler() { + @Override + public boolean handleTransaction(int code, Parcel data) { + int callingUid = Binder.getCallingUid(); + if (callingUid != allowedCallingUid) { + logger.log(Level.WARNING, "dropped txn from " + callingUid + " !=" + allowedCallingUid); + return false; + } + return wrapped.handleTransaction(code, data); + } + }; + } } diff --git a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java index 75be03d6145..de78b9e5311 100644 --- a/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/RobolectricBinderTransportTest.java @@ -16,12 +16,19 @@ package io.grpc.binder.internal; +import static android.os.IBinder.FLAG_ONEWAY; import static android.os.Process.myUid; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static io.grpc.binder.internal.BinderTransport.REMOTE_UID; import static io.grpc.binder.internal.BinderTransport.SETUP_TRANSPORT; +import static io.grpc.binder.internal.BinderTransport.SHUTDOWN_TRANSPORT; import static io.grpc.binder.internal.BinderTransport.WIRE_FORMAT_VERSION; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.junit.Assume.assumeThat; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; @@ -39,6 +46,7 @@ import androidx.test.core.content.pm.PackageInfoBuilder; import com.google.common.collect.ImmutableList; import io.grpc.Attributes; +import io.grpc.InternalChannelz.SocketStats; import io.grpc.ServerStreamTracer; import io.grpc.Status; import io.grpc.binder.AndroidComponentAddress; @@ -47,11 +55,13 @@ import io.grpc.binder.SecurityPolicies; import io.grpc.binder.internal.SettableAsyncSecurityPolicy.AuthRequest; import io.grpc.internal.AbstractTransportTest; +import io.grpc.internal.ClientTransport; import io.grpc.internal.ClientTransportFactory.ClientTransportOptions; import io.grpc.internal.ConnectionClientTransport; import io.grpc.internal.GrpcUtil; import io.grpc.internal.InternalServer; import io.grpc.internal.ManagedClientTransport; +import io.grpc.internal.MockServerTransportListener; import io.grpc.internal.ObjectPool; import io.grpc.internal.SharedResourcePool; import java.util.List; @@ -62,6 +72,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -100,6 +112,9 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest @Mock AsyncSecurityPolicy mockClientSecurityPolicy; + @Captor + ArgumentCaptor statusCaptor; + ApplicationInfo serverAppInfo; PackageInfo serverPkgInfo; ServiceInfo serviceInfo; @@ -110,9 +125,9 @@ public final class RobolectricBinderTransportTest extends AbstractTransportTest public boolean preAuthServersParam; @Parameter(value = 1) - public boolean useLegacyHandshake; + public boolean useLegacyAuthStrategy; - @Parameters(name = "preAuthServersParam={0};useLegacyHandshake={1}") + @Parameters(name = "preAuthServersParam={0};useLegacyAuthStrategy={1}") public static ImmutableList data() { return ImmutableList.of( new Object[] {false, false}, @@ -184,7 +199,7 @@ protected InternalServer newServer( BinderClientTransportFactory.Builder newClientTransportFactoryBuilder() { return new BinderClientTransportFactory.Builder() .setPreAuthorizeServers(preAuthServersParam) - .setUseLegacyHandshake(useLegacyHandshake) + .setUseLegacyAuthStrategy(useLegacyAuthStrategy) .setSourceContext(application) .setScheduledExecutorPool(executorServicePool) .setOffloadExecutorPool(offloadExecutorPool); @@ -245,7 +260,7 @@ public void clientAuthorizesServerUidsInOrder() throws Exception { } AuthRequest authRequest = securityPolicy.takeNextAuthRequest(TIMEOUT_MS, MILLISECONDS); - if (useLegacyHandshake) { + if (useLegacyAuthStrategy) { assertThat(authRequest.uid).isEqualTo(11111); } else { assertThat(authRequest.uid).isEqualTo(22222); @@ -314,13 +329,70 @@ public void clientIgnoresDuplicateSetupTransaction() throws Exception { } assertThat(((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID)) - .isEqualTo(myUid()); + .isEqualTo(myUid()); + } + + @Test + public void clientIgnoresTransactionFromNonServerUids() throws Exception { + server.start(serverListener); + + // This test is not applicable to the new auth strategy which keeps the client Binder a secret. + assumeTrue(useLegacyAuthStrategy); + + client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + + int serverUid = ((ConnectionClientTransport) client).getAttributes().get(REMOTE_UID); + int someOtherUid = 1 + serverUid; + sendShutdownTransportTransactionAsUid(client, someOtherUid); + + // Demonstrate that the transport is still working and that shutdown transaction was ignored. + ClientTransport.PingCallback mockPingCallback = mock(ClientTransport.PingCallback.class); + client.ping(mockPingCallback, directExecutor()); + verify(mockPingCallback, timeout(TIMEOUT_MS)).onSuccess(anyLong()); + + // Try again as the expected uid to demonstrate that this wasn't ignored for some other reason. + sendShutdownTransportTransactionAsUid(client, serverUid); + + verify(mockClientTransportListener, timeout(TIMEOUT_MS)) + .transportShutdown(statusCaptor.capture()); + assertThat(statusCaptor.getValue().getCode()).isEqualTo(Status.Code.UNAVAILABLE); + assertThat(statusCaptor.getValue().getDescription()).contains("shutdown"); + } + + static void sendShutdownTransportTransactionAsUid(ClientTransport client, int sendingUid) { + int originalUid = Binder.getCallingUid(); + try { + ShadowBinder.setCallingUid(sendingUid); + ((BinderClientTransport) client) + .getIncomingBinderForTesting() + .onTransact(SHUTDOWN_TRANSPORT, null, null, FLAG_ONEWAY); + } finally { + ShadowBinder.setCallingUid(originalUid); + } } @Test - @Ignore("See BinderTransportTest#socketStats.") @Override - public void socketStats() {} + // We don't quite pass the official/abstract version of this test yet because + // today's binder client and server transports have different ideas of each others' address. + // TODO(#12347): Remove this @Override once this difference is resolved. + public void socketStats() throws Exception { + server.start(serverListener); + ManagedClientTransport client = newClientTransport(server); + startTransport(client, mockClientTransportListener); + + SocketStats clientSocketStats = client.getStats().get(); + assertThat(clientSocketStats.local).isInstanceOf(AndroidComponentAddress.class); + assertThat(((AndroidComponentAddress) clientSocketStats.remote).getPackage()) + .isEqualTo(((AndroidComponentAddress) server.getListenSocketAddress()).getPackage()); + + MockServerTransportListener serverTransportListener = + serverListener.takeListenerOrFail(TIMEOUT_MS, MILLISECONDS); + SocketStats serverSocketStats = serverTransportListener.transport.getStats().get(); + assertThat(serverSocketStats.local).isEqualTo(server.getListenSocketAddress()); + assertThat(serverSocketStats.remote).isEqualTo(new BoundClientAddress(myUid())); + } @Test @Ignore("See BinderTransportTest#flowControlPushBack") diff --git a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java index 0464c098720..b9a745e1521 100644 --- a/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java +++ b/binder/src/test/java/io/grpc/binder/internal/ServiceBindingTest.java @@ -116,6 +116,32 @@ public void testBind() throws Exception { assertThat(binding.isSourceContextCleared()).isFalse(); } + @Test + public void testGetConnectedServiceInfo() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + + ServiceInfo serviceInfo = binding.getConnectedServiceInfo(); + assertThat(serviceInfo.name).isEqualTo(serviceComponent.getClassName()); + assertThat(serviceInfo.packageName).isEqualTo(serviceComponent.getPackageName()); + } + + @Test + public void testGetConnectedServiceInfoThrows() throws Exception { + binding = newBuilder().setTargetComponent(serviceComponent).build(); + binding.bind(); + shadowOf(getMainLooper()).idle(); + + assertThat(observer.gotBoundEvent).isTrue(); + shadowOf(appContext.getPackageManager()).removeService(serviceComponent); + + StatusException se = assertThrows(StatusException.class, binding::getConnectedServiceInfo); + assertThat(se.getStatus().getCode()).isEqualTo(Code.UNIMPLEMENTED); + } + @Test public void testBindingIntent() throws Exception { shadowApplication.setComponentNameAndServiceForBindService(null, null); @@ -389,6 +415,7 @@ public void testBindWithDeviceAdmin() throws Exception { allowBindDeviceAdminForUser(appContext, adminComponent, /* userId= */ 0); binding = newBuilder() + .setTargetUserHandle(UserHandle.getUserHandleForUid(/* uid= */ 0)) .setTargetUserHandle(generateUserHandle(/* userId= */ 0)) .setTargetComponent(serviceComponent) .setChannelCredentials(BinderChannelCredentials.forDevicePolicyAdmin(adminComponent)) @@ -426,7 +453,10 @@ private static void allowBindDeviceAdminForUser( ShadowDevicePolicyManager devicePolicyManager = shadowOf(context.getSystemService(DevicePolicyManager.class)); devicePolicyManager.setDeviceOwner(admin); + devicePolicyManager.setBindDeviceAdminTargetUsers( + Arrays.asList(UserHandle.getUserHandleForUid(userId))); shadowOf((DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE)); + devicePolicyManager.setDeviceOwner(admin); devicePolicyManager.setBindDeviceAdminTargetUsers(Arrays.asList(generateUserHandle(userId))); } @@ -444,7 +474,6 @@ private class TestObserver implements Bindable.Observer { public boolean gotBoundEvent; public IBinder binder; - public ComponentName serviceName; public boolean gotUnboundEvent; public Status unboundReason; diff --git a/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java b/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java new file mode 100644 index 00000000000..44a3ce3ef26 --- /dev/null +++ b/binder/src/test/java/io/grpc/binder/internal/TransactionUtilsTest.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.binder.internal; + +import static com.google.common.truth.Truth.assertThat; +import static io.grpc.binder.internal.TransactionUtils.newCallerFilteringHandler; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.Binder; +import android.os.Parcel; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowBinder; + +@RunWith(RobolectricTestRunner.class) +public final class TransactionUtilsTest { + + @Rule public MockitoRule mocks = MockitoJUnit.rule(); + + @Mock LeakSafeOneWayBinder.TransactionHandler mockHandler; + + @Test + public void shouldIgnoreTransactionFromWrongUid() { + Parcel p = Parcel.obtain(); + int originalUid = Binder.getCallingUid(); + try { + when(mockHandler.handleTransaction(eq(1234), same(p))).thenReturn(true); + LeakSafeOneWayBinder.TransactionHandler uid100OnlyHandler = + newCallerFilteringHandler(1000, mockHandler); + + ShadowBinder.setCallingUid(9999); + boolean result = uid100OnlyHandler.handleTransaction(1234, p); + assertThat(result).isFalse(); + verify(mockHandler, never()).handleTransaction(anyInt(), any()); + + ShadowBinder.setCallingUid(1000); + result = uid100OnlyHandler.handleTransaction(1234, p); + assertThat(result).isTrue(); + verify(mockHandler).handleTransaction(1234, p); + } finally { + ShadowBinder.setCallingUid(originalUid); + p.recycle(); + } + } +} diff --git a/buildscripts/kokoro/unix.sh b/buildscripts/kokoro/unix.sh index e65825cac01..455d9c07199 100755 --- a/buildscripts/kokoro/unix.sh +++ b/buildscripts/kokoro/unix.sh @@ -39,6 +39,11 @@ ARCH="$ARCH" buildscripts/make_dependencies.sh # Set properties via flags, do not pollute gradle.properties GRADLE_FLAGS="${GRADLE_FLAGS:-}" GRADLE_FLAGS+=" -PtargetArch=$ARCH" + +# For universal binaries on macOS, signal Gradle to use universal flags. +if [[ "$(uname -s)" == "Darwin" ]]; then + GRADLE_FLAGS+=" -PbuildUniversal=true" +fi GRADLE_FLAGS+=" -Pcheckstyle.ignoreFailures=false" GRADLE_FLAGS+=" -PfailOnWarnings=true" GRADLE_FLAGS+=" -PerrorProne=true" diff --git a/buildscripts/make_dependencies.sh b/buildscripts/make_dependencies.sh index e5d2450afe7..73cb54b7b68 100755 --- a/buildscripts/make_dependencies.sh +++ b/buildscripts/make_dependencies.sh @@ -41,7 +41,13 @@ else mkdir "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" pushd "$DOWNLOAD_DIR/protobuf-${PROTOBUF_VERSION}/build" # install here so we don't need sudo - if [[ "$ARCH" == x86* ]]; then + if [[ "$(uname -s)" == "Darwin" ]]; then + cmake .. \ + -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ + -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \ + -B. || exit 1 + elif [[ "$ARCH" == x86* ]]; then CFLAGS=-m${ARCH#*_} CXXFLAGS=-m${ARCH#*_} cmake .. \ -DCMAKE_CXX_STANDARD=14 -Dprotobuf_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" -DABSL_INTERNAL_AT_LEAST_CXX17=0 \ diff --git a/compiler/build.gradle b/compiler/build.gradle index 6d832ecd56b..c5acccebae7 100644 --- a/compiler/build.gradle +++ b/compiler/build.gradle @@ -103,6 +103,12 @@ model { cppCompiler.args "--std=c++14" addEnvArgs("CXXFLAGS", cppCompiler.args) addEnvArgs("CPPFLAGS", cppCompiler.args) + if (project.hasProperty('buildUniversal') && + project.getProperty('buildUniversal').toBoolean() && + osdetector.os == "osx") { + cppCompiler.args "-arch", "arm64", "-arch", "x86_64" + linker.args "-arch", "arm64", "-arch", "x86_64" + } if (osdetector.os == "osx") { cppCompiler.args "-mmacosx-version-min=10.7", "-stdlib=libc++" linker.args "-framework", "CoreFoundation" @@ -147,11 +153,9 @@ sourceSets { dependencies { testImplementation project(':grpc-protobuf'), - project(':grpc-stub'), - libraries.javax.annotation + project(':grpc-stub') testLiteImplementation project(':grpc-protobuf-lite'), - project(':grpc-stub'), - libraries.javax.annotation + project(':grpc-stub') } tasks.named("compileTestJava").configure { diff --git a/compiler/check-artifact.sh b/compiler/check-artifact.sh index 12d7709a2a8..83b41f50282 100755 --- a/compiler/check-artifact.sh +++ b/compiler/check-artifact.sh @@ -86,17 +86,17 @@ checkArch () fi fi elif [[ "$OS" == osx ]]; then - format="$(file -b "$1" | grep -o "[^ ]*$")" - echo Format=$format - if [[ "$ARCH" == x86_32 ]]; then - assertEq "$format" "i386" $LINENO - elif [[ "$ARCH" == x86_64 ]]; then - assertEq "$format" "x86_64" $LINENO - elif [[ "$ARCH" == aarch_64 ]]; then - assertEq "$format" "arm64" $LINENO - else - fail "Unsupported arch: $ARCH" + # For macOS, we now build a universal binary. We check that both + # required architectures are present. + format="$(lipo -archs "$1")" + echo "Architectures found: $format" + if ! echo "$format" | grep -q "x86_64"; then + fail "Universal binary is missing x86_64 architecture." + fi + if ! echo "$format" | grep -q "arm64"; then + fail "Universal binary is missing arm64 architecture." fi + echo "Universal binary check successful." else fail "Unsupported system: $OS" fi diff --git a/compiler/src/java_plugin/cpp/java_generator.cpp b/compiler/src/java_plugin/cpp/java_generator.cpp index 4cee7999402..659d7ccca47 100644 --- a/compiler/src/java_plugin/cpp/java_generator.cpp +++ b/compiler/src/java_plugin/cpp/java_generator.cpp @@ -143,11 +143,24 @@ static std::set java_keywords = { "false", }; +// Methods on java.lang.Object that take no arguments. +static std::set java_object_methods = { + "clone", + "finalize", + "getClass", + "hashCode", + "notify", + "notifyAll", + "toString", + "wait", +}; + // Adjust a method name prefix identifier to follow the JavaBean spec: // - decapitalize the first letter // - remove embedded underscores & capitalize the following letter -// Finally, if the result is a reserved java keyword, append an underscore. -static std::string MixedLower(std::string word) { +// Finally, if the result is a reserved java keyword or an Object method, +// append an underscore. +static std::string MixedLower(std::string word, bool mangle_object_methods = false) { std::string w; w += tolower(word[0]); bool after_underscore = false; @@ -159,7 +172,9 @@ static std::string MixedLower(std::string word) { after_underscore = false; } } - if (java_keywords.find(w) != java_keywords.end()) { + if (java_keywords.find(w) != java_keywords.end() || + (mangle_object_methods && + java_object_methods.find(w) != java_object_methods.end())) { return w + "_"; } return w; @@ -180,8 +195,9 @@ static std::string ToAllUpperCase(std::string word) { return w; } -static inline std::string LowerMethodName(const MethodDescriptor* method) { - return MixedLower(std::string(method->name())); +static inline std::string LowerMethodName(const MethodDescriptor* method, + bool mangle_object_methods = false) { + return MixedLower(std::string(method->name()), mangle_object_methods); } static inline std::string MethodPropertiesFieldName(const MethodDescriptor* method) { @@ -676,10 +692,12 @@ static void PrintStub( const MethodDescriptor* method = service->method(i); (*vars)["input_type"] = MessageFullJavaName(method->input_type()); (*vars)["output_type"] = MessageFullJavaName(method->output_type()); - (*vars)["lower_method_name"] = LowerMethodName(method); - (*vars)["method_method_name"] = MethodPropertiesGetterName(method); bool client_streaming = method->client_streaming(); bool server_streaming = method->server_streaming(); + bool mangle_object_methods = (call_type == BLOCKING_V2_CALL && client_streaming) + || (call_type == BLOCKING_CALL && client_streaming && server_streaming); + (*vars)["lower_method_name"] = LowerMethodName(method, mangle_object_methods); + (*vars)["method_method_name"] = MethodPropertiesGetterName(method); if (call_type == BLOCKING_CALL && client_streaming) { // Blocking client interface with client streaming is not available diff --git a/core/build.gradle b/core/build.gradle index 2fac9ddba04..b320f326b41 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,6 +1,6 @@ buildscript { dependencies { - classpath 'com.google.guava:guava:30.0-android' + classpath 'com.google.guava:guava:33.4.8-android' } } diff --git a/core/src/main/java/io/grpc/internal/AbstractClientStream.java b/core/src/main/java/io/grpc/internal/AbstractClientStream.java index 9718f8c5171..14fd5888147 100644 --- a/core/src/main/java/io/grpc/internal/AbstractClientStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractClientStream.java @@ -101,6 +101,7 @@ void writeFrame( */ private volatile boolean cancelled; + @SuppressWarnings("this-escape") protected AbstractClientStream( WritableBufferAllocator bufferAllocator, StatsTraceContext statsTraceCtx, @@ -113,7 +114,7 @@ protected AbstractClientStream( this.shouldBeCountedForInUse = GrpcUtil.shouldBeCountedForInUse(callOptions); this.useGet = useGet; if (!useGet) { - framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); + this.framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); this.headers = headers; } else { framer = new GetFramer(headers, statsTraceCtx); diff --git a/core/src/main/java/io/grpc/internal/AbstractServerStream.java b/core/src/main/java/io/grpc/internal/AbstractServerStream.java index a535330f4b1..c468cba978a 100644 --- a/core/src/main/java/io/grpc/internal/AbstractServerStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractServerStream.java @@ -75,10 +75,11 @@ protected interface Sink { private boolean outboundClosed; private boolean headersSent; + @SuppressWarnings("this-escape") protected AbstractServerStream( WritableBufferAllocator bufferAllocator, StatsTraceContext statsTraceCtx) { this.statsTraceCtx = Preconditions.checkNotNull(statsTraceCtx, "statsTraceCtx"); - framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); + this.framer = new MessageFramer(this, bufferAllocator, statsTraceCtx); } @Override diff --git a/core/src/main/java/io/grpc/internal/AbstractStream.java b/core/src/main/java/io/grpc/internal/AbstractStream.java index 46cdab7ef28..9f5fb035dab 100644 --- a/core/src/main/java/io/grpc/internal/AbstractStream.java +++ b/core/src/main/java/io/grpc/internal/AbstractStream.java @@ -163,20 +163,21 @@ public abstract static class TransportState @GuardedBy("onReadyLock") private int onReadyThreshold; + @SuppressWarnings("this-escape") protected TransportState( int maxMessageSize, StatsTraceContext statsTraceCtx, TransportTracer transportTracer) { this.statsTraceCtx = checkNotNull(statsTraceCtx, "statsTraceCtx"); this.transportTracer = checkNotNull(transportTracer, "transportTracer"); - rawDeframer = new MessageDeframer( + this.rawDeframer = new MessageDeframer( this, Codec.Identity.NONE, maxMessageSize, statsTraceCtx, transportTracer); // TODO(#7168): use MigratingThreadDeframer when enabling retry doesn't break. - deframer = rawDeframer; + deframer = this.rawDeframer; onReadyThreshold = DEFAULT_ONREADY_THRESHOLD; } diff --git a/core/src/main/java/io/grpc/internal/InternalSubchannel.java b/core/src/main/java/io/grpc/internal/InternalSubchannel.java index a27e46eaf60..649843c5c03 100644 --- a/core/src/main/java/io/grpc/internal/InternalSubchannel.java +++ b/core/src/main/java/io/grpc/internal/InternalSubchannel.java @@ -48,6 +48,9 @@ import io.grpc.LoadBalancer; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.MetricRecorder; +import io.grpc.NameResolver; +import io.grpc.SecurityLevel; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.SynchronizationContext.ScheduledHandle; @@ -160,6 +163,8 @@ protected void handleNotInUse() { private Status shutdownReason; private volatile Attributes connectedAddressAttributes; + private final SubchannelMetrics subchannelMetrics; + private final String target; InternalSubchannel(LoadBalancer.CreateSubchannelArgs args, String authority, String userAgent, BackoffPolicy.Provider backoffPolicyProvider, @@ -168,7 +173,9 @@ protected void handleNotInUse() { Supplier stopwatchSupplier, SynchronizationContext syncContext, Callback callback, InternalChannelz channelz, CallTracer callsTracer, ChannelTracer channelTracer, InternalLogId logId, - ChannelLogger channelLogger, List transportFilters) { + ChannelLogger channelLogger, List transportFilters, + String target, + MetricRecorder metricRecorder) { List addressGroups = args.getAddresses(); Preconditions.checkNotNull(addressGroups, "addressGroups"); Preconditions.checkArgument(!addressGroups.isEmpty(), "addressGroups is empty"); @@ -192,6 +199,8 @@ protected void handleNotInUse() { this.channelLogger = Preconditions.checkNotNull(channelLogger, "channelLogger"); this.transportFilters = transportFilters; this.reconnectDisabled = args.getOption(LoadBalancer.DISABLE_SUBCHANNEL_RECONNECT_KEY); + this.target = target; + this.subchannelMetrics = new SubchannelMetrics(metricRecorder); } ChannelLogger getChannelLogger() { @@ -593,6 +602,13 @@ public void run() { pendingTransport = null; connectedAddressAttributes = addressIndex.getCurrentEagAttributes(); gotoNonErrorState(READY); + subchannelMetrics.recordConnectionAttemptSucceeded(/* target= */ target, + /* backendService= */ getAttributeOrDefault( + addressIndex.getCurrentEagAttributes(), NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME), + /* securityLevel= */ extractSecurityLevel(addressIndex.getCurrentEagAttributes() + .get(GrpcAttributes.ATTR_SECURITY_LEVEL))); } } }); @@ -618,11 +634,25 @@ public void run() { activeTransport = null; addressIndex.reset(); gotoNonErrorState(IDLE); + subchannelMetrics.recordDisconnection(/* target= */ target, + /* backendService= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME), + /* disconnectError= */ SubchannelMetrics.DisconnectError.UNKNOWN + .getErrorString(null), + /* securityLevel= */ extractSecurityLevel(addressIndex.getCurrentEagAttributes() + .get(GrpcAttributes.ATTR_SECURITY_LEVEL))); } else if (pendingTransport == transport) { + subchannelMetrics.recordConnectionAttemptFailed(/* target= */ target, + /* backendService= */getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + NameResolver.ATTR_BACKEND_SERVICE), + /* locality= */ getAttributeOrDefault(addressIndex.getCurrentEagAttributes(), + EquivalentAddressGroup.ATTR_LOCALITY_NAME)); Preconditions.checkState(state.getState() == CONNECTING, "Expected state is CONNECTING, actual state is %s", state.getState()); addressIndex.increment(); - // Continue reconnect if there are still addresses to try. + // Continue to reconnect if there are still addresses to try. if (!addressIndex.isValid()) { pendingTransport = null; addressIndex.reset(); @@ -658,6 +688,27 @@ public void run() { } }); } + + private String extractSecurityLevel(SecurityLevel securityLevel) { + if (securityLevel == null) { + return "none"; + } + switch (securityLevel) { + case NONE: + return "none"; + case INTEGRITY: + return "integrity_only"; + case PRIVACY_AND_INTEGRITY: + return "privacy_and_integrity"; + default: + throw new IllegalArgumentException("Unknown SecurityLevel: " + securityLevel); + } + } + + private String getAttributeOrDefault(Attributes attributes, Attributes.Key key) { + String value = attributes.get(key); + return value == null ? "" : value; + } } // All methods are called in syncContext diff --git a/core/src/main/java/io/grpc/internal/JsonUtil.java b/core/src/main/java/io/grpc/internal/JsonUtil.java index 44cb22abda5..a0d5eef8660 100644 --- a/core/src/main/java/io/grpc/internal/JsonUtil.java +++ b/core/src/main/java/io/grpc/internal/JsonUtil.java @@ -356,7 +356,7 @@ private static int parseNanos(String value) throws ParseException { return result; } - private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + private static final int NANOS_PER_SECOND = 1_000_000_000; /** * Copy of {@link com.google.protobuf.util.Durations#normalizedDuration}. @@ -368,11 +368,11 @@ private static long normalizedDuration(long seconds, int nanos) { nanos %= NANOS_PER_SECOND; } if (seconds > 0 && nanos < 0) { - nanos += NANOS_PER_SECOND; // no overflow since nanos is negative (and we're adding) + nanos += NANOS_PER_SECOND; // no overflow— nanos is negative (and we're adding) seconds--; // no overflow since seconds is positive (and we're decrementing) } if (seconds < 0 && nanos > 0) { - nanos -= NANOS_PER_SECOND; // no overflow since nanos is positive (and we're subtracting) + nanos -= NANOS_PER_SECOND; // no overflow— nanos is positive (and we're subtracting) seconds++; // no overflow since seconds is negative (and we're incrementing) } if (!durationIsValid(seconds, nanos)) { diff --git a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java index 16b8adbd347..78c5181502f 100644 --- a/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java +++ b/core/src/main/java/io/grpc/internal/ManagedChannelImpl.java @@ -415,7 +415,7 @@ void exitIdleMode() { LbHelperImpl lbHelper = new LbHelperImpl(); lbHelper.lb = loadBalancerFactory.newLoadBalancer(lbHelper); // Delay setting lbHelper until fully initialized, since loadBalancerFactory is user code and - // may throw. We don't want to confuse our state, even if we will enter panic mode. + // may throw. We don't want to confuse our state, even if we enter panic mode. this.lbHelper = lbHelper; channelStateManager.gotoState(CONNECTING); @@ -1464,7 +1464,9 @@ void onStateChange(InternalSubchannel is, ConnectivityStateInfo newState) { subchannelTracer, subchannelLogId, subchannelLogger, - transportFilters); + transportFilters, + target, + lbHelper.getMetricRecorder()); oobChannelTracer.reportEvent(new ChannelTrace.Event.Builder() .setDescription("Child Subchannel created") .setSeverity(ChannelTrace.Event.Severity.CT_INFO) @@ -1895,7 +1897,8 @@ void onNotInUse(InternalSubchannel is) { subchannelTracer, subchannelLogId, subchannelLogger, - transportFilters); + transportFilters, target, + lbHelper.getMetricRecorder()); channelTracer.reportEvent(new ChannelTrace.Event.Builder() .setDescription("Child Subchannel started") diff --git a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java index 452b1c5df07..ded9d5ce589 100644 --- a/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java +++ b/core/src/main/java/io/grpc/internal/MetricRecorderImpl.java @@ -26,6 +26,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricInstrumentRegistry; import io.grpc.MetricRecorder; @@ -82,7 +83,7 @@ public void addDoubleCounter(DoubleCounterMetricInstrument metricInstrument, dou * Records a long counter value. * * @param metricInstrument the {@link LongCounterMetricInstrument} to record. - * @param value the value to record. + * @param value the value to record. Must be non-negative. * @param requiredLabelValues the required label values for the metric. * @param optionalLabelValues the optional label values for the metric. */ @@ -103,6 +104,32 @@ public void addLongCounter(LongCounterMetricInstrument metricInstrument, long va } } + /** + * Adds a long up down counter value. + * + * @param metricInstrument the {@link io.grpc.LongUpDownCounterMetricInstrument} to record. + * @param value the value to record. May be positive, negative or zero. + * @param requiredLabelValues the required label values for the metric. + * @param optionalLabelValues the optional label values for the metric. + */ + @Override + public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { + MetricRecorder.super.addLongUpDownCounter(metricInstrument, value, requiredLabelValues, + optionalLabelValues); + for (MetricSink sink : metricSinks) { + int measuresSize = sink.getMeasuresSize(); + if (measuresSize <= metricInstrument.getIndex()) { + // Measures may need updating in two cases: + // 1. When the sink is initially created with an empty list of measures. + // 2. When new metric instruments are registered, requiring the sink to accommodate them. + sink.updateMeasures(registry.getMetricInstruments()); + } + sink.addLongUpDownCounter(metricInstrument, value, requiredLabelValues, optionalLabelValues); + } + } + /** * Records a double histogram value. * diff --git a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java index bbc144ea775..ebe329ca591 100644 --- a/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java +++ b/core/src/main/java/io/grpc/internal/PickFirstLeafLoadBalancer.java @@ -92,7 +92,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { return Status.FAILED_PRECONDITION.withDescription("Already shut down"); } - // Cache whether or not this is a petiole policy, which is based off of an address attribute + // Check whether this is a petiole policy, which is based off of an address attribute Boolean isPetiolePolicy = resolvedAddresses.getAttributes().get(IS_PETIOLE_POLICY); this.notAPetiolePolicy = isPetiolePolicy == null || !isPetiolePolicy; diff --git a/core/src/main/java/io/grpc/internal/SubchannelMetrics.java b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java new file mode 100644 index 00000000000..8921f13ebe6 --- /dev/null +++ b/core/src/main/java/io/grpc/internal/SubchannelMetrics.java @@ -0,0 +1,189 @@ +/* + * Copyright 2025 The gRPC Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.grpc.internal; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import io.grpc.LongCounterMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; +import io.grpc.MetricInstrumentRegistry; +import io.grpc.MetricRecorder; +import javax.annotation.Nullable; + +final class SubchannelMetrics { + + private static final LongCounterMetricInstrument disconnections; + private static final LongCounterMetricInstrument connectionAttemptsSucceeded; + private static final LongCounterMetricInstrument connectionAttemptsFailed; + private static final LongUpDownCounterMetricInstrument openConnections; + private final MetricRecorder metricRecorder; + + public SubchannelMetrics(MetricRecorder metricRecorder) { + this.metricRecorder = metricRecorder; + } + + static { + MetricInstrumentRegistry metricInstrumentRegistry + = MetricInstrumentRegistry.getDefaultRegistry(); + disconnections = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.disconnections", + "EXPERIMENTAL. Number of times the selected subchannel becomes disconnected", + "{disconnection}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality", "grpc.disconnect_error"), + false + ); + + connectionAttemptsSucceeded = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.connection_attempts_succeeded", + "EXPERIMENTAL. Number of successful connection attempts", + "{attempt}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + + connectionAttemptsFailed = metricInstrumentRegistry.registerLongCounter( + "grpc.subchannel.connection_attempts_failed", + "EXPERIMENTAL. Number of failed connection attempts", + "{attempt}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + + openConnections = metricInstrumentRegistry.registerLongUpDownCounter( + "grpc.subchannel.open_connections", + "EXPERIMENTAL. Number of open connections.", + "{connection}", + Lists.newArrayList("grpc.target"), + Lists.newArrayList("grpc.security_level", "grpc.lb.backend_service", "grpc.lb.locality"), + false + ); + } + + public void recordConnectionAttemptSucceeded(String target, String backendService, + String locality, String securityLevel) { + metricRecorder + .addLongCounter(connectionAttemptsSucceeded, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality)); + metricRecorder + .addLongUpDownCounter(openConnections, 1, + ImmutableList.of(target), + ImmutableList.of(securityLevel, backendService, locality)); + } + + public void recordConnectionAttemptFailed(String target, String backendService, String locality) { + metricRecorder + .addLongCounter(connectionAttemptsFailed, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality)); + } + + public void recordDisconnection(String target, String backendService, String locality, + String disconnectError, String securityLevel) { + metricRecorder + .addLongCounter(disconnections, 1, + ImmutableList.of(target), + ImmutableList.of(backendService, locality, disconnectError)); + metricRecorder + .addLongUpDownCounter(openConnections, -1, + ImmutableList.of(target), + ImmutableList.of(securityLevel, backendService, locality)); + } + + /** + * Represents the reason for a subchannel failure. + */ + public enum DisconnectError { + + /** + * Represents an HTTP/2 GOAWAY frame. The specific error code + * (e.g., "NO_ERROR", "PROTOCOL_ERROR") should be handled separately + * as it is a dynamic part of the error. + * See RFC 9113 for error codes: https://www.rfc-editor.org/rfc/rfc9113.html#name-error-codes + */ + GOAWAY("goaway"), + + /** + * The subchannel was shut down for various reasons like parent channel shutdown, + * idleness, or load balancing policy changes. + */ + SUBCHANNEL_SHUTDOWN("subchannel shutdown"), + + /** + * Connection was reset (e.g., ECONNRESET, WSAECONNERESET). + */ + CONNECTION_RESET("connection reset"), + + /** + * Connection timed out (e.g., ETIMEDOUT, WSAETIMEDOUT), including closures + * from gRPC keepalives. + */ + CONNECTION_TIMED_OUT("connection timed out"), + + /** + * Connection was aborted (e.g., ECONNABORTED, WSAECONNABORTED). + */ + CONNECTION_ABORTED("connection aborted"), + + /** + * Any socket error not covered by other specific disconnect errors. + */ + SOCKET_ERROR("socket error"), + + /** + * A catch-all for any other unclassified reason. + */ + UNKNOWN("unknown"); + + private final String errorTag; + + /** + * Private constructor to associate a description with each enum constant. + * + * @param errorTag The detailed explanation of the error. + */ + DisconnectError(String errorTag) { + this.errorTag = errorTag; + } + + /** + * Gets the error string suitable for use as a metric tag. + * + *

If the reason is {@code GOAWAY}, this method requires the specific + * HTTP/2 error code to create the complete tag (e.g., "goaway PROTOCOL_ERROR"). + * For all other reasons, the parameter is ignored.

+ * + * @param goawayErrorCode The specific HTTP/2 error code. This is only + * used if the reason is GOAWAY and should not be null in that case. + * @return The formatted error string. + */ + public String getErrorString(@Nullable String goawayErrorCode) { + if (this == GOAWAY) { + if (goawayErrorCode == null || goawayErrorCode.isEmpty()) { + // Return the base tag if the code is missing, or consider throwing an exception + // throw new IllegalArgumentException("goawayErrorCode is required for GOAWAY reason."); + return this.errorTag; + } + return this.errorTag + " " + goawayErrorCode; + } + return this.errorTag; + } + } +} diff --git a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java index bed722f5f3a..4ac5fbac362 100644 --- a/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java +++ b/core/src/test/java/io/grpc/internal/InternalSubchannelTest.java @@ -29,10 +29,13 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.delegatesTo; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -48,6 +51,10 @@ import io.grpc.InternalLogId; import io.grpc.InternalWithLogId; import io.grpc.LoadBalancer; +import io.grpc.MetricInstrument; +import io.grpc.MetricRecorder; +import io.grpc.NameResolver; +import io.grpc.SecurityLevel; import io.grpc.Status; import io.grpc.SynchronizationContext; import io.grpc.internal.InternalSubchannel.CallTracingTransport; @@ -68,6 +75,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -81,6 +89,9 @@ public class InternalSubchannelTest { public final MockitoRule mocks = MockitoJUnit.rule(); private static final String AUTHORITY = "fakeauthority"; + private static final String BACKEND_SERVICE = "ice-cream-factory-service"; + private static final String LOCALITY = "mars-olympus-mons-datacenter"; + private static final SecurityLevel SECURITY_LEVEL = SecurityLevel.PRIVACY_AND_INTEGRITY; private static final String USER_AGENT = "mosaic"; private static final ConnectivityStateInfo UNAVAILABLE_STATE = ConnectivityStateInfo.forTransientFailure(Status.UNAVAILABLE); @@ -108,6 +119,10 @@ public void uncaughtException(Thread t, Throwable e) { @Mock private BackoffPolicy.Provider mockBackoffPolicyProvider; @Mock private ClientTransportFactory mockTransportFactory; + @Mock private BackoffPolicy mockBackoffPolicy; + private MetricRecorder mockMetricRecorder = mock(MetricRecorder.class, + delegatesTo(new MetricRecorderImpl())); + private final LinkedList callbackInvokes = new LinkedList<>(); private final InternalSubchannel.Callback mockInternalSubchannelCallback = new InternalSubchannel.Callback() { @@ -1446,7 +1461,136 @@ private void createInternalSubchannel(boolean reconnectDisabled, subchannelTracer, logId, new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), - Collections.emptyList()); + Collections.emptyList(), + "", + new MetricRecorder() { + } + ); + } + + @Test + public void subchannelStateChanges_triggersAttemptFailedMetric() { + // 1. Setup: Standard subchannel initialization + when(mockBackoffPolicyProvider.get()).thenReturn(mockBackoffPolicy); + SocketAddress addr = mock(SocketAddress.class); + Attributes eagAttributes = Attributes.newBuilder() + .set(NameResolver.ATTR_BACKEND_SERVICE, BACKEND_SERVICE) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, LOCALITY) + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SECURITY_LEVEL) + .build(); + List addressGroups = + Arrays.asList(new EquivalentAddressGroup(Arrays.asList(addr), eagAttributes)); + InternalLogId logId = InternalLogId.allocate("Subchannel", /*details=*/ AUTHORITY); + ChannelTracer subchannelTracer = new ChannelTracer(logId, 10, + fakeClock.getTimeProvider().currentTimeNanos(), "Subchannel"); + LoadBalancer.CreateSubchannelArgs createSubchannelArgs = + LoadBalancer.CreateSubchannelArgs.newBuilder().setAddresses(addressGroups).build(); + internalSubchannel = new InternalSubchannel( + createSubchannelArgs, AUTHORITY, USER_AGENT, mockBackoffPolicyProvider, + mockTransportFactory, fakeClock.getScheduledExecutorService(), + fakeClock.getStopwatchSupplier(), syncContext, mockInternalSubchannelCallback, channelz, + CallTracer.getDefaultFactory().create(), subchannelTracer, logId, + new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), + Collections.emptyList(), AUTHORITY, mockMetricRecorder + ); + + // --- Action: Simulate the "connecting to failed" transition --- + // a. Initiate the connection attempt. The subchannel is now CONNECTING. + internalSubchannel.obtainActiveTransport(); + MockClientTransportInfo transportInfo = transports.poll(); + assertNotNull("A connection attempt should have been made", transportInfo); + + // b. Fail the transport before it can signal `transportReady()`. + transportInfo.listener.transportShutdown( + Status.INTERNAL.withDescription("Simulated connect failure")); + fakeClock.runDueTasks(); // Process the failure event + + // --- Verification --- + // a. Verify that the "connection_attempts_failed" metric was recorded exactly once. + verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.connection_attempts_failed"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY)) + ); + + // b. Verify no other metrics were recorded. This confirms it wasn't incorrectly + // logged as a success, disconnection, or open connection. + verifyNoMoreInteractions(mockMetricRecorder); + } + + @Test + public void subchannelStateChanges_triggersSuccessAndDisconnectMetrics() { + // 1. Mock the backoff policy (needed for subchannel creation) + when(mockBackoffPolicyProvider.get()).thenReturn(mockBackoffPolicy); + + // 2. Setup Subchannel with attributes + SocketAddress addr = mock(SocketAddress.class); + Attributes eagAttributes = Attributes.newBuilder() + .set(NameResolver.ATTR_BACKEND_SERVICE, BACKEND_SERVICE) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, LOCALITY) + .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SECURITY_LEVEL) + .build(); + List addressGroups = + Arrays.asList(new EquivalentAddressGroup(Arrays.asList(addr), eagAttributes)); + createInternalSubchannel(new EquivalentAddressGroup(addr)); + InternalLogId logId = InternalLogId.allocate("Subchannel", /*details=*/ AUTHORITY); + ChannelTracer subchannelTracer = new ChannelTracer(logId, 10, + fakeClock.getTimeProvider().currentTimeNanos(), "Subchannel"); + LoadBalancer.CreateSubchannelArgs createSubchannelArgs = + LoadBalancer.CreateSubchannelArgs.newBuilder().setAddresses(addressGroups).build(); + internalSubchannel = new InternalSubchannel( + createSubchannelArgs, AUTHORITY, USER_AGENT, mockBackoffPolicyProvider, + mockTransportFactory, fakeClock.getScheduledExecutorService(), + fakeClock.getStopwatchSupplier(), syncContext, mockInternalSubchannelCallback, channelz, + CallTracer.getDefaultFactory().create(), subchannelTracer, logId, + new ChannelLoggerImpl(subchannelTracer, fakeClock.getTimeProvider()), + Collections.emptyList(), AUTHORITY, mockMetricRecorder + ); + + // --- Action: Successful connection --- + internalSubchannel.obtainActiveTransport(); + MockClientTransportInfo transportInfo = transports.poll(); + assertNotNull(transportInfo); + transportInfo.listener.transportReady(); + fakeClock.runDueTasks(); // Process the successful connection + + // --- Action: Transport is shut down --- + transportInfo.listener.transportShutdown(Status.UNAVAILABLE.withDescription("unknown")); + fakeClock.runDueTasks(); // Process the shutdown + + // --- Verification --- + InOrder inOrder = inOrder(mockMetricRecorder); + + // Verify successful connection metrics + inOrder.verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.connection_attempts_succeeded"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY)) + ); + inOrder.verify(mockMetricRecorder).addLongUpDownCounter( + eqMetricInstrumentName("grpc.subchannel.open_connections"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList("privacy_and_integrity", BACKEND_SERVICE, LOCALITY)) + ); + + // Verify disconnection metrics + inOrder.verify(mockMetricRecorder).addLongCounter( + eqMetricInstrumentName("grpc.subchannel.disconnections"), + eq(1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList(BACKEND_SERVICE, LOCALITY, "unknown")) + ); + inOrder.verify(mockMetricRecorder).addLongUpDownCounter( + eqMetricInstrumentName("grpc.subchannel.open_connections"), + eq(-1L), + eq(Arrays.asList(AUTHORITY)), + eq(Arrays.asList("privacy_and_integrity", BACKEND_SERVICE, LOCALITY)) + ); + + inOrder.verifyNoMoreInteractions(); } private void assertNoCallbackInvoke() { @@ -1459,5 +1603,13 @@ private void assertExactCallbackInvokes(String ... expectedInvokes) { callbackInvokes.clear(); } + static class MetricRecorderImpl implements MetricRecorder { + } + + @SuppressWarnings("TypeParameterUnusedInFormals") + private T eqMetricInstrumentName(String name) { + return argThat(instrument -> instrument.getName().equals(name)); + } + private static class FakeSocketAddress extends SocketAddress {} } diff --git a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java index 08f34a267f9..33bf9bb41e2 100644 --- a/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java +++ b/core/src/test/java/io/grpc/internal/MetricRecorderImplTest.java @@ -32,6 +32,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrumentRegistry; import io.grpc.MetricInstrumentRegistryAccessor; import io.grpc.MetricRecorder; @@ -79,6 +80,9 @@ public class MetricRecorderImplTest { private final LongGaugeMetricInstrument longGaugeInstrument = registry.registerLongGauge("gauge0", DESCRIPTION, UNIT, REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED); + private final LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + registry.registerLongUpDownCounter("upDownCounter0", DESCRIPTION, UNIT, + REQUIRED_LABEL_KEYS, OPTIONAL_LABEL_KEYS, ENABLED); private MetricRecorder recorder; @Before @@ -88,7 +92,7 @@ public void setUp() { @Test public void addCounter() { - when(mockSink.getMeasuresSize()).thenReturn(4); + when(mockSink.getMeasuresSize()).thenReturn(6); recorder.addDoubleCounter(doubleCounterInstrument, 1.0, REQUIRED_LABEL_VALUES, OPTIONAL_LABEL_VALUES); @@ -100,6 +104,12 @@ public void addCounter() { verify(mockSink, times(2)).addLongCounter(eq(longCounterInstrument), eq(1L), eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, -10, REQUIRED_LABEL_VALUES, + OPTIONAL_LABEL_VALUES); + verify(mockSink, times(2)) + .addLongUpDownCounter(eq(longUpDownCounterInstrument), eq(-10L), + eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); + verify(mockSink, never()).updateMeasures(registry.getMetricInstruments()); } @@ -190,6 +200,13 @@ public void newRegisteredMetricUpdateMeasures() { verify(mockSink, times(2)) .registerBatchCallback(any(Runnable.class), eq(longGaugeInstrument)); registration.close(); + + // Long UpDown Counter + recorder.addLongUpDownCounter(longUpDownCounterInstrument, -10, REQUIRED_LABEL_VALUES, + OPTIONAL_LABEL_VALUES); + verify(mockSink, times(12)).updateMeasures(anyList()); + verify(mockSink, times(2)).addLongUpDownCounter(eq(longUpDownCounterInstrument), eq(-10L), + eq(REQUIRED_LABEL_VALUES), eq(OPTIONAL_LABEL_VALUES)); } @Test(expected = IllegalArgumentException.class) @@ -208,6 +225,13 @@ public void addLongCounterMismatchedRequiredLabelValues() { OPTIONAL_LABEL_VALUES); } + @Test(expected = IllegalArgumentException.class) + public void addLongUpDownCounterMismatchedRequiredLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(6); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, 1, ImmutableList.of(), + OPTIONAL_LABEL_VALUES); + } + @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedRequiredLabelValues() { when(mockSink.getMeasuresSize()).thenReturn(4); @@ -260,6 +284,13 @@ public void addLongCounterMismatchedOptionalLabelValues() { ImmutableList.of()); } + @Test(expected = IllegalArgumentException.class) + public void addLongUpDownCounterMismatchedOptionalLabelValues() { + when(mockSink.getMeasuresSize()).thenReturn(6); + recorder.addLongUpDownCounter(longUpDownCounterInstrument, 1, REQUIRED_LABEL_VALUES, + ImmutableList.of()); + } + @Test(expected = IllegalArgumentException.class) public void recordDoubleHistogramMismatchedOptionalLabelValues() { when(mockSink.getMeasuresSize()).thenReturn(4); diff --git a/cronet/build.gradle b/cronet/build.gradle index 0715b4129bf..d6d773a97e4 100644 --- a/cronet/build.gradle +++ b/cronet/build.gradle @@ -46,6 +46,7 @@ dependencies { libraries.cronet.api implementation project(':grpc-core') implementation libraries.guava + implementation 'org.checkerframework:checker-qual:3.49.5' testImplementation project(':grpc-testing') testImplementation libraries.cronet.embedded diff --git a/examples/android/clientcache/app/build.gradle b/examples/android/clientcache/app/build.gradle index a9716ea1f62..8048eba73ab 100644 --- a/examples/android/clientcache/app/build.gradle +++ b/examples/android/clientcache/app/build.gradle @@ -57,7 +57,6 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' testImplementation 'junit:junit:4.13.2' testImplementation 'com.google.truth:truth:1.1.5' diff --git a/examples/android/clientcache/settings.gradle b/examples/android/clientcache/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/clientcache/settings.gradle +++ b/examples/android/clientcache/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/helloworld/app/build.gradle b/examples/android/helloworld/app/build.gradle index 2fd2d2fa950..69519c8e4ab 100644 --- a/examples/android/helloworld/app/build.gradle +++ b/examples/android/helloworld/app/build.gradle @@ -55,5 +55,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/helloworld/settings.gradle b/examples/android/helloworld/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/helloworld/settings.gradle +++ b/examples/android/helloworld/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/routeguide/app/build.gradle b/examples/android/routeguide/app/build.gradle index 371e277bdc6..a09de35e994 100644 --- a/examples/android/routeguide/app/build.gradle +++ b/examples/android/routeguide/app/build.gradle @@ -55,5 +55,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/routeguide/settings.gradle b/examples/android/routeguide/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/routeguide/settings.gradle +++ b/examples/android/routeguide/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/android/strictmode/app/build.gradle b/examples/android/strictmode/app/build.gradle index 02327041d34..c0447a42af1 100644 --- a/examples/android/strictmode/app/build.gradle +++ b/examples/android/strictmode/app/build.gradle @@ -56,5 +56,4 @@ dependencies { implementation 'io.grpc:grpc-okhttp:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-protobuf-lite:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION implementation 'io.grpc:grpc-stub:1.76.0-SNAPSHOT' // CURRENT_GRPC_VERSION - implementation 'org.apache.tomcat:annotations-api:6.0.53' } diff --git a/examples/android/strictmode/settings.gradle b/examples/android/strictmode/settings.gradle index e7b4def49cb..6208d70e838 100644 --- a/examples/android/strictmode/settings.gradle +++ b/examples/android/strictmode/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + include ':app' diff --git a/examples/build.gradle b/examples/build.gradle index 507b87df4db..ddd8e7b3a65 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" // examples/advanced need this for JsonFormat implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}" diff --git a/examples/example-alts/build.gradle b/examples/example-alts/build.gradle index 33ea02a5875..9f86adb0aeb 100644 --- a/examples/example-alts/build.gradle +++ b/examples/example-alts/build.gradle @@ -27,7 +27,6 @@ def protocVersion = '3.25.8' dependencies { // grpc-alts transitively depends on grpc-netty-shaded, grpc-protobuf, and grpc-stub implementation "io.grpc:grpc-alts:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" } protobuf { diff --git a/examples/example-alts/settings.gradle b/examples/example-alts/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-alts/settings.gradle +++ b/examples/example-alts/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-debug/build.gradle b/examples/example-debug/build.gradle index ed01bbb2636..3dc873e179b 100644 --- a/examples/example-debug/build.gradle +++ b/examples/example-debug/build.gradle @@ -30,7 +30,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation 'junit:junit:4.13.2' diff --git a/examples/example-debug/pom.xml b/examples/example-debug/pom.xml index 90ce766d8a9..c4fce2d793b 100644 --- a/examples/example-debug/pom.xml +++ b/examples/example-debug/pom.xml @@ -44,12 +44,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-debug/settings.gradle b/examples/example-debug/settings.gradle index 3700c983b6c..48c08629ca9 100644 --- a/examples/example-debug/settings.gradle +++ b/examples/example-debug/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-debug' diff --git a/examples/example-dualstack/build.gradle b/examples/example-dualstack/build.gradle index fa9db23f987..5d8c30b2127 100644 --- a/examples/example-dualstack/build.gradle +++ b/examples/example-dualstack/build.gradle @@ -31,7 +31,6 @@ dependencies { implementation "io.grpc:grpc-netty:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" } protobuf { diff --git a/examples/example-dualstack/pom.xml b/examples/example-dualstack/pom.xml index b70a3eca3d8..b65beb84103 100644 --- a/examples/example-dualstack/pom.xml +++ b/examples/example-dualstack/pom.xml @@ -48,12 +48,6 @@ io.grpc grpc-netty - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-dualstack/settings.gradle b/examples/example-dualstack/settings.gradle index 49a762696f7..160d5134334 100644 --- a/examples/example-dualstack/settings.gradle +++ b/examples/example-dualstack/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-gauth/build.gradle b/examples/example-gauth/build.gradle index 52d945196fa..befc2f7c0c7 100644 --- a/examples/example-gauth/build.gradle +++ b/examples/example-gauth/build.gradle @@ -30,7 +30,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-auth:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" implementation "com.google.api.grpc:grpc-google-cloud-pubsub-v1:0.1.24" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" diff --git a/examples/example-gauth/pom.xml b/examples/example-gauth/pom.xml index 68a98c526a5..98bbebd114a 100644 --- a/examples/example-gauth/pom.xml +++ b/examples/example-gauth/pom.xml @@ -49,12 +49,6 @@ io.grpc grpc-auth - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-gauth/settings.gradle b/examples/example-gauth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-gauth/settings.gradle +++ b/examples/example-gauth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-gcp-csm-observability/build.gradle b/examples/example-gcp-csm-observability/build.gradle index 816ef9e6742..abb2dd8a220 100644 --- a/examples/example-gcp-csm-observability/build.gradle +++ b/examples/example-gcp-csm-observability/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation "io.opentelemetry:opentelemetry-sdk:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-sdk-metrics:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-prometheus:${openTelemetryPrometheusVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-xds:${grpcVersion}" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-gcp-csm-observability/settings.gradle b/examples/example-gcp-csm-observability/settings.gradle index 6b7615117d6..44e6f340ede 100644 --- a/examples/example-gcp-csm-observability/settings.gradle +++ b/examples/example-gcp-csm-observability/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-gcp-csm-observability' diff --git a/examples/example-gcp-observability/build.gradle b/examples/example-gcp-observability/build.gradle index 432dceb6730..08e00294452 100644 --- a/examples/example-gcp-observability/build.gradle +++ b/examples/example-gcp-observability/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-gcp-observability:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-gcp-observability/settings.gradle b/examples/example-gcp-observability/settings.gradle index 1e4ba3812eb..39efc20a459 100644 --- a/examples/example-gcp-observability/settings.gradle +++ b/examples/example-gcp-observability/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-gcp-observability' diff --git a/examples/example-hostname/build.gradle b/examples/example-hostname/build.gradle index 7345d873e4f..94a85e8f8ab 100644 --- a/examples/example-hostname/build.gradle +++ b/examples/example-hostname/build.gradle @@ -28,7 +28,6 @@ dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-services:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation 'junit:junit:4.13.2' diff --git a/examples/example-hostname/pom.xml b/examples/example-hostname/pom.xml index 00209657e1d..e93b36e39d1 100644 --- a/examples/example-hostname/pom.xml +++ b/examples/example-hostname/pom.xml @@ -44,12 +44,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-hostname/settings.gradle b/examples/example-hostname/settings.gradle index aa159eb0946..5bd641b3fc1 100644 --- a/examples/example-hostname/settings.gradle +++ b/examples/example-hostname/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'hostname' diff --git a/examples/example-jwt-auth/build.gradle b/examples/example-jwt-auth/build.gradle index b14040ad58f..28df68e4fc7 100644 --- a/examples/example-jwt-auth/build.gradle +++ b/examples/example-jwt-auth/build.gradle @@ -31,8 +31,6 @@ dependencies { implementation "io.jsonwebtoken:jjwt:0.9.1" implementation "javax.xml.bind:jaxb-api:2.3.1" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation "io.grpc:grpc-testing:${grpcVersion}" diff --git a/examples/example-jwt-auth/pom.xml b/examples/example-jwt-auth/pom.xml index 8dce2a71032..4834cfa09ef 100644 --- a/examples/example-jwt-auth/pom.xml +++ b/examples/example-jwt-auth/pom.xml @@ -57,12 +57,6 @@ jaxb-api 2.3.1 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-jwt-auth/settings.gradle b/examples/example-jwt-auth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-jwt-auth/settings.gradle +++ b/examples/example-jwt-auth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-oauth/build.gradle b/examples/example-oauth/build.gradle index 521d8b082ce..05a450dcc8d 100644 --- a/examples/example-oauth/build.gradle +++ b/examples/example-oauth/build.gradle @@ -31,8 +31,6 @@ dependencies { implementation "io.grpc:grpc-auth:${grpcVersion}" implementation "com.google.auth:google-auth-library-oauth2-http:1.23.0" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" testImplementation "io.grpc:grpc-testing:${grpcVersion}" diff --git a/examples/example-oauth/pom.xml b/examples/example-oauth/pom.xml index 2907d062053..a2f61e38467 100644 --- a/examples/example-oauth/pom.xml +++ b/examples/example-oauth/pom.xml @@ -62,12 +62,6 @@ google-auth-library-oauth2-http 1.23.0 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/example-oauth/settings.gradle b/examples/example-oauth/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-oauth/settings.gradle +++ b/examples/example-oauth/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-opentelemetry/build.gradle b/examples/example-opentelemetry/build.gradle index 482f5766bee..edcca46480e 100644 --- a/examples/example-opentelemetry/build.gradle +++ b/examples/example-opentelemetry/build.gradle @@ -34,7 +34,6 @@ dependencies { implementation "io.opentelemetry:opentelemetry-sdk-metrics:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-logging:${openTelemetryVersion}" implementation "io.opentelemetry:opentelemetry-exporter-prometheus:${openTelemetryPrometheusVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-opentelemetry/settings.gradle b/examples/example-opentelemetry/settings.gradle index ff7ea3fc2be..26e3bea044b 100644 --- a/examples/example-opentelemetry/settings.gradle +++ b/examples/example-opentelemetry/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-opentelemetry' diff --git a/examples/example-orca/build.gradle b/examples/example-orca/build.gradle index 2716adf7de1..f21c89ee0a4 100644 --- a/examples/example-orca/build.gradle +++ b/examples/example-orca/build.gradle @@ -24,8 +24,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-xds:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - } protobuf { diff --git a/examples/example-orca/settings.gradle b/examples/example-orca/settings.gradle index 3c62dc663ce..12536c0ca8d 100644 --- a/examples/example-orca/settings.gradle +++ b/examples/example-orca/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-orca' diff --git a/examples/example-reflection/build.gradle b/examples/example-reflection/build.gradle index 2b48cb30b5f..a5bda680ef4 100644 --- a/examples/example-reflection/build.gradle +++ b/examples/example-reflection/build.gradle @@ -24,8 +24,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-netty-shaded:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" - } protobuf { diff --git a/examples/example-reflection/settings.gradle b/examples/example-reflection/settings.gradle index dccb973085e..28e44b77905 100644 --- a/examples/example-reflection/settings.gradle +++ b/examples/example-reflection/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-reflection' diff --git a/examples/example-servlet/build.gradle b/examples/example-servlet/build.gradle index bbdb65349ca..d0f475f1833 100644 --- a/examples/example-servlet/build.gradle +++ b/examples/example-servlet/build.gradle @@ -23,8 +23,7 @@ dependencies { "io.grpc:grpc-servlet:${grpcVersion}", "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "javax.servlet:javax.servlet-api:4.0.1", - "org.apache.tomcat:annotations-api:6.0.53" + compileOnly "javax.servlet:javax.servlet-api:4.0.1" } protobuf { diff --git a/examples/example-servlet/settings.gradle b/examples/example-servlet/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-servlet/settings.gradle +++ b/examples/example-servlet/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-tls/build.gradle b/examples/example-tls/build.gradle index aeb769a479b..0b3914febf3 100644 --- a/examples/example-tls/build.gradle +++ b/examples/example-tls/build.gradle @@ -27,7 +27,6 @@ def protocVersion = '3.25.8' dependencies { implementation "io.grpc:grpc-protobuf:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-tls/pom.xml b/examples/example-tls/pom.xml index 575400c3608..3932f4c32f4 100644 --- a/examples/example-tls/pom.xml +++ b/examples/example-tls/pom.xml @@ -40,12 +40,6 @@ io.grpc grpc-stub - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-netty-shaded diff --git a/examples/example-tls/settings.gradle b/examples/example-tls/settings.gradle index c665ff96674..6bd0f0cdc2d 100644 --- a/examples/example-tls/settings.gradle +++ b/examples/example-tls/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/examples/example-xds/build.gradle b/examples/example-xds/build.gradle index 6c78db3513c..3914aa3fea0 100644 --- a/examples/example-xds/build.gradle +++ b/examples/example-xds/build.gradle @@ -29,7 +29,6 @@ dependencies { implementation "io.grpc:grpc-services:${grpcVersion}" implementation "io.grpc:grpc-stub:${grpcVersion}" implementation "io.grpc:grpc-xds:${grpcVersion}" - compileOnly "org.apache.tomcat:annotations-api:6.0.53" runtimeOnly "io.grpc:grpc-netty-shaded:${grpcVersion}" } diff --git a/examples/example-xds/settings.gradle b/examples/example-xds/settings.gradle index 878f1f23ae3..4197fa6760d 100644 --- a/examples/example-xds/settings.gradle +++ b/examples/example-xds/settings.gradle @@ -1 +1,17 @@ +pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } +} + rootProject.name = 'example-xds' diff --git a/examples/pom.xml b/examples/pom.xml index f81346c913c..4da1eb14f90 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -60,12 +60,6 @@ j2objc-annotations 3.0.0 - - org.apache.tomcat - annotations-api - 6.0.53 - provided - io.grpc grpc-testing diff --git a/examples/settings.gradle b/examples/settings.gradle index dadd24b1c0f..4d39e8b45ba 100644 --- a/examples/settings.gradle +++ b/examples/settings.gradle @@ -1,4 +1,18 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } + repositories { gradlePluginPortal() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a374ba5aa73..cb5dce02843 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,11 +31,7 @@ commons-math3 = "org.apache.commons:commons-math3:3.6.1" conscrypt = "org.conscrypt:conscrypt-openjdk-uber:2.5.2" cronet-api = "org.chromium.net:cronet-api:119.6045.31" cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31" -# error-prone 2.31.0+ blocked on https://github.com/grpc/grpc-java/issues/10152 -# It breaks Bazel (ArrayIndexOutOfBoundsException in turbine) and Dexing ("D8: -# java.lang.NullPointerException"). We can trivially upgrade the Bazel CI to -# 6.3.0+ (https://github.com/bazelbuild/bazel/issues/18743). -errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.30.0" +errorprone-annotations = "com.google.errorprone:error_prone_annotations:2.36.0" # error-prone 2.32.0+ require Java 17+ errorprone-core = "com.google.errorprone:error_prone_core:2.31.0" google-api-protos = "com.google.api.grpc:proto-google-common-protos:2.59.2" @@ -47,19 +43,16 @@ google-auth-oauth2Http = "com.google.auth:google-auth-library-oauth2-http:1.24.1 google-cloud-logging = "com.google.cloud:google-cloud-logging:3.23.1" # 2.12.1 requires error_prone_annotations:2.36.0 but we are stuck with 2.30.0 gson = "com.google.code.gson:gson:2.11.0" -# 33.4.0 requires com.google.errorprone:error_prone_annotations:2.36.0 but we are stuck with 2.30.0 (see above) -guava = "com.google.guava:guava:33.3.1-android" +# 33.4.8 requires com.google.errorprone:error_prone_annotations:2.36.0 +guava = "com.google.guava:guava:33.4.8-android" guava-betaChecker = "com.google.guava:guava-beta-checker:1.0" -guava-testlib = "com.google.guava:guava-testlib:33.3.1-android" +guava-testlib = "com.google.guava:guava-testlib:33.4.8-android" # JRE version is needed for projects where its a transitive dependency, f.e. gcp-observability. # May be different from the -android version. -guava-jre = "com.google.guava:guava:33.3.1-jre" +guava-jre = "com.google.guava:guava:33.4.8-jre" hdrhistogram = "org.hdrhistogram:HdrHistogram:2.2.2" # 6.0.0+ use java.lang.Deprecated forRemoval and since from Java 9 jakarta-servlet-api = "jakarta.servlet:jakarta.servlet-api:5.0.0" -# Using javax.annotation is fine as it is part of the JDK, we don't want to depend on J2EE -# where it is relocated to as org.apache.tomcat:tomcat-annotations-api. See issue #9179. -javax-annotation = "org.apache.tomcat:annotations-api:6.0.53" javax-servlet-api = "javax.servlet:javax.servlet-api:4.0.1" # 12.0.0+ require Java 17+ jetty-client = "org.eclipse.jetty:jetty-client:11.0.24" diff --git a/grpclb/build.gradle b/grpclb/build.gradle index 3f67181372b..f543e0d71fc 100644 --- a/grpclb/build.gradle +++ b/grpclb/build.gradle @@ -23,7 +23,6 @@ dependencies { libraries.protobuf.java, libraries.protobuf.java.util runtimeOnly libraries.errorprone.annotations - compileOnly libraries.javax.annotation testImplementation libraries.truth, project(':grpc-inprocess'), testFixtures(project(':grpc-core')) diff --git a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java index 089a9f12b02..c458857d70b 100644 --- a/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java +++ b/inprocess/src/main/java/io/grpc/inprocess/AnonymousInProcessSocketAddress.java @@ -21,6 +21,8 @@ import com.google.errorprone.annotations.concurrent.GuardedBy; import io.grpc.ExperimentalApi; import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectOutputStream; import java.net.SocketAddress; import javax.annotation.Nullable; @@ -34,8 +36,13 @@ public final class AnonymousInProcessSocketAddress extends SocketAddress { @Nullable @GuardedBy("this") + @SuppressWarnings("serial") private InProcessServer server; + private void writeObject(ObjectOutputStream out) throws IOException { + throw new NotSerializableException("AnonymousInProcessSocketAddress is not serializable"); + } + /** Creates a new AnonymousInProcessSocketAddress. */ public AnonymousInProcessSocketAddress() { } diff --git a/interop-testing/build.gradle b/interop-testing/build.gradle index 97e7c69533a..5160759460c 100644 --- a/interop-testing/build.gradle +++ b/interop-testing/build.gradle @@ -31,7 +31,6 @@ dependencies { project(':grpc-stub'), project(':grpc-protobuf'), libraries.junit - compileOnly libraries.javax.annotation // TODO(sergiitk): replace with com.google.cloud:google-cloud-logging // Used instead of google-cloud-logging because it's failing // due to a circular dependency on grpc. diff --git a/istio-interop-testing/build.gradle b/istio-interop-testing/build.gradle index 4550f0ea202..083d8fcb9bf 100644 --- a/istio-interop-testing/build.gradle +++ b/istio-interop-testing/build.gradle @@ -18,8 +18,6 @@ dependencies { project(':grpc-testing'), project(':grpc-xds') - compileOnly libraries.javax.annotation - runtimeOnly libraries.netty.tcnative, libraries.netty.tcnative.classes testImplementation testFixtures(project(':grpc-api')), diff --git a/lint.xml b/lint.xml index 93e2f603108..5b35a8d151b 100644 --- a/lint.xml +++ b/lint.xml @@ -5,4 +5,9 @@ Remove after AGP upgrade. --> + + + diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java index 3b8c595a12e..a463cf01d95 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2ConnectionHandler.java @@ -68,6 +68,7 @@ public abstract class GrpcHttp2ConnectionHandler extends Http2ConnectionHandler usingPre4_1_111_Netty = identifiedOldVersion; } + @SuppressWarnings("this-escape") protected GrpcHttp2ConnectionHandler( ChannelPromise channelUnused, Http2ConnectionDecoder decoder, diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 48f1aae91a1..036fde55e2c 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -60,6 +60,7 @@ import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Connection; @@ -68,8 +69,10 @@ import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; +import io.netty.handler.codec.http2.EmptyHttp2Headers; import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2ConnectionAdapter; import io.netty.handler.codec.http2.Http2ConnectionDecoder; @@ -83,6 +86,7 @@ import io.netty.handler.codec.http2.Http2FrameWriter; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersDecoder; +import io.netty.handler.codec.http2.Http2HeadersEncoder; import io.netty.handler.codec.http2.Http2InboundFrameLogger; import io.netty.handler.codec.http2.Http2LifecycleManager; import io.netty.handler.codec.http2.Http2OutboundFrameLogger; @@ -177,8 +181,10 @@ static NettyServerHandler newHandler( Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new Http2InboundFrameLogger( new DefaultHttp2FrameReader(headersDecoder), frameLogger); + Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder( + Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE); Http2FrameWriter frameWriter = - new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(), frameLogger); + new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger); return newHandler( channelUnused, frameReader, @@ -480,8 +486,10 @@ private void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers } if (!HTTP_METHOD.contentEquals(headers.method())) { + Http2Headers extraHeaders = new DefaultHttp2Headers(); + extraHeaders.add(HttpHeaderNames.ALLOW, HTTP_METHOD); respondWithHttpError(ctx, streamId, 405, Status.Code.INTERNAL, - String.format("Method '%s' is not supported", headers.method())); + String.format("Method '%s' is not supported", headers.method()), extraHeaders); return; } @@ -869,6 +877,12 @@ public boolean visit(Http2Stream stream) throws Http2Exception { private void respondWithHttpError( ChannelHandlerContext ctx, int streamId, int code, Status.Code statusCode, String msg) { + respondWithHttpError(ctx, streamId, code, statusCode, msg, EmptyHttp2Headers.INSTANCE); + } + + private void respondWithHttpError( + ChannelHandlerContext ctx, int streamId, int code, Status.Code statusCode, String msg, + Http2Headers extraHeaders) { Metadata metadata = new Metadata(); metadata.put(InternalStatus.CODE_KEY, statusCode.toStatus()); metadata.put(InternalStatus.MESSAGE_KEY, msg); @@ -880,6 +894,7 @@ private void respondWithHttpError( for (int i = 0; i < serialized.length; i += 2) { headers.add(new AsciiString(serialized[i], false), new AsciiString(serialized[i + 1], false)); } + headers.add(extraHeaders); encoder().writeHeaders(ctx, streamId, headers, 0, false, ctx.newPromise()); ByteBuf msgBuf = ByteBufUtil.writeUtf8(ctx.alloc(), msg); encoder().writeData(ctx, streamId, msgBuf, 0, true, ctx.newPromise()); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index 65683dd8396..55abe29e93a 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -151,6 +151,9 @@ public class NettyClientTransportTest { private static final SslContext SSL_CONTEXT = createSslContext(); + @SuppressWarnings("InlineMeInliner") // Requires Java 11 + private static final String LONG_STRING_OF_A = Strings.repeat("a", 128); + @Mock private ManagedClientTransport.Listener clientTransportListener; @@ -624,9 +627,6 @@ public void maxHeaderListSizeShouldBeEnforcedOnClient() throws Exception { @Test public void huffmanCodingShouldNotBePerformed() throws Exception { - @SuppressWarnings("InlineMeInliner") // Requires Java 11 - String longStringOfA = Strings.repeat("a", 128); - negotiator = ProtocolNegotiators.serverPlaintext(); startServer(); @@ -637,7 +637,7 @@ public void huffmanCodingShouldNotBePerformed() throws Exception { Metadata headers = new Metadata(); headers.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), - longStringOfA); + LONG_STRING_OF_A); callMeMaybe(transport.start(clientTransportListener)); verify(clientTransportListener, timeout(5000)).transportReady(); @@ -649,7 +649,7 @@ public void huffmanCodingShouldNotBePerformed() throws Exception { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof ByteBuf) { - if (((ByteBuf) msg).toString(StandardCharsets.UTF_8).contains(longStringOfA)) { + if (((ByteBuf) msg).toString(StandardCharsets.UTF_8).contains(LONG_STRING_OF_A)) { foundExpectedHeaderBytes.set(true); } } @@ -664,6 +664,47 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) } } + @Test + public void huffmanCodingShouldNotBePerformedOnServer() throws Exception { + negotiator = ProtocolNegotiators.serverPlaintext(); + + Metadata responseHeaders = new Metadata(); + responseHeaders.put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), + LONG_STRING_OF_A); + + startServer(new EchoServerListener(responseHeaders)); + + NettyClientTransport transport = newTransport(ProtocolNegotiators.plaintext(), + DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, null, false, + TimeUnit.SECONDS.toNanos(10L), TimeUnit.SECONDS.toNanos(1L), + new ReflectiveChannelFactory<>(NioSocketChannel.class), group); + + callMeMaybe(transport.start(clientTransportListener)); + verify(clientTransportListener, timeout(5000)).transportReady(); + + AtomicBoolean foundExpectedHeaderBytes = new AtomicBoolean(false); + + // Add a handler to the client pipeline to inspect server's response + transport.channel().pipeline().addFirst(new ChannelDuplexHandler() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof ByteBuf) { + String data = ((ByteBuf) msg).toString(StandardCharsets.UTF_8); + if (data.contains(LONG_STRING_OF_A)) { + foundExpectedHeaderBytes.set(true); + } + } + super.channelRead(ctx, msg); + } + }); + + new Rpc(transport).halfClose().waitForResponse(); + + if (!foundExpectedHeaderBytes.get()) { + fail("expected to find UTF-8 encoded 'a's in the response header sent by the server"); + } + } + @Test public void maxHeaderListSizeShouldBeEnforcedOnServer() throws Exception { startServer(100, 1); @@ -1115,7 +1156,16 @@ private void startServer() throws IOException { startServer(100, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE); } + private void startServer(ServerListener serverListener) throws IOException { + startServer(100, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, serverListener); + } + private void startServer(int maxStreamsPerConnection, int maxHeaderListSize) throws IOException { + startServer(maxStreamsPerConnection, maxHeaderListSize, serverListener); + } + + private void startServer(int maxStreamsPerConnection, int maxHeaderListSize, + ServerListener serverListener) throws IOException { server = new NettyServer( TestUtils.testServerAddresses(new InetSocketAddress(0)), @@ -1283,6 +1333,15 @@ private final class EchoServerListener implements ServerListener { final List transports = new ArrayList<>(); final List streamListeners = Collections.synchronizedList(new ArrayList()); + Metadata responseHeaders; + + public EchoServerListener() { + this(new Metadata()); + } + + public EchoServerListener(Metadata responseHeaders) { + this.responseHeaders = responseHeaders; + } @Override public ServerTransportListener transportCreated(final ServerTransport transport) { @@ -1292,7 +1351,7 @@ public ServerTransportListener transportCreated(final ServerTransport transport) public void streamCreated(ServerStream stream, String method, Metadata headers) { EchoServerStreamListener listener = new EchoServerStreamListener(stream, headers); stream.setListener(listener); - stream.writeHeaders(new Metadata(), true); + stream.writeHeaders(responseHeaders, true); stream.request(1); streamListeners.add(listener); } diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 28217937adc..0d5a9bab176 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -78,6 +78,7 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Error; @@ -542,7 +543,8 @@ public void headersWithInvalidMethodShouldFail() throws Exception { .set(InternalStatus.CODE_KEY.name(), String.valueOf(Code.INTERNAL.value())) .set(InternalStatus.MESSAGE_KEY.name(), "Method 'FAKE' is not supported") .status("" + 405) - .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8"); + .set(CONTENT_TYPE_HEADER, "text/plain; charset=utf-8") + .set(HttpHeaderNames.ALLOW, HTTP_METHOD); verifyWrite() .writeHeaders( diff --git a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java index cc52bee85eb..b744bca3116 100644 --- a/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java +++ b/okhttp/src/main/java/io/grpc/okhttp/OkHttpServerTransport.java @@ -20,6 +20,7 @@ import static io.grpc.okhttp.OkHttpServerBuilder.MAX_CONNECTION_IDLE_NANOS_DISABLED; import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.errorprone.annotations.concurrent.GuardedBy; @@ -52,6 +53,7 @@ import java.io.IOException; import java.net.Socket; import java.net.SocketException; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -91,6 +93,7 @@ final class OkHttpServerTransport implements ServerTransport, private static final ByteString TE_TRAILERS = ByteString.encodeUtf8("trailers"); private static final ByteString CONTENT_TYPE = ByteString.encodeUtf8("content-type"); private static final ByteString CONTENT_LENGTH = ByteString.encodeUtf8("content-length"); + private static final ByteString ALLOW = ByteString.encodeUtf8("allow"); private final Config config; private final Variant variant = new Http2(); @@ -772,8 +775,9 @@ public void headers(boolean outFinished, } if (!POST_METHOD.equals(httpMethod)) { + List
extraHeaders = Lists.newArrayList(new Header(ALLOW, POST_METHOD)); respondWithHttpError(streamId, inFinished, 405, Status.Code.INTERNAL, - "HTTP Method is not supported: " + asciiString(httpMethod)); + "HTTP Method is not supported: " + asciiString(httpMethod), extraHeaders); return; } @@ -1066,11 +1070,19 @@ private void streamError(int streamId, ErrorCode errorCode, String reason) { private void respondWithHttpError( int streamId, boolean inFinished, int httpCode, Status.Code statusCode, String msg) { + respondWithHttpError(streamId, inFinished, httpCode, statusCode, msg, + Collections.emptyList()); + } + + private void respondWithHttpError( + int streamId, boolean inFinished, int httpCode, Status.Code statusCode, String msg, + List
extraHeaders) { Metadata metadata = new Metadata(); metadata.put(InternalStatus.CODE_KEY, statusCode.toStatus()); metadata.put(InternalStatus.MESSAGE_KEY, msg); List
headers = Headers.createHttpResponseHeaders(httpCode, "text/plain; charset=utf-8", metadata); + headers.addAll(extraHeaders); Buffer data = new Buffer().writeUtf8(msg); synchronized (lock) { diff --git a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java index d64d314d7d8..4d2744dc9c7 100644 --- a/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java +++ b/okhttp/src/test/java/io/grpc/okhttp/OkHttpServerTransportTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import com.google.common.collect.Lists; import com.google.common.io.ByteStreams; import io.grpc.Attributes; import io.grpc.InternalChannelz.SocketStats; @@ -62,6 +63,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Deque; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -919,8 +921,9 @@ public void httpGet_failsWith405() throws Exception { CONTENT_TYPE_HEADER, TE_HEADER)); clientFrameWriter.flush(); - - verifyHttpError(1, 405, Status.Code.INTERNAL, "HTTP Method is not supported: GET"); + List
extraHeaders = Lists.newArrayList(new Header("allow", "POST")); + verifyHttpError(1, 405, Status.Code.INTERNAL, "HTTP Method is not supported: GET", + extraHeaders); shutdownAndTerminate(/*lastStreamId=*/ 1); } @@ -976,7 +979,8 @@ public void httpErrorsAdhereToFlowControl() throws Exception { new Header(":status", "405"), new Header("content-type", "text/plain; charset=utf-8"), new Header("grpc-status", "" + Status.Code.INTERNAL.value()), - new Header("grpc-message", errorDescription)); + new Header("grpc-message", errorDescription), + new Header("allow", "POST")); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) .headers(false, false, 1, -1, responseHeaders, HeadersMode.HTTP_20_HEADERS); @@ -1398,11 +1402,18 @@ private void pingPong() throws IOException { private void verifyHttpError( int streamId, int httpCode, Status.Code grpcCode, String errorDescription) throws Exception { - List
responseHeaders = Arrays.asList( + verifyHttpError(streamId, httpCode, grpcCode, errorDescription, Collections.emptyList()); + } + + private void verifyHttpError( + int streamId, int httpCode, Status.Code grpcCode, String errorDescription, + List
extraHeaders) throws Exception { + List
responseHeaders = Lists.newArrayList( new Header(":status", "" + httpCode), new Header("content-type", "text/plain; charset=utf-8"), new Header("grpc-status", "" + grpcCode.value()), new Header("grpc-message", errorDescription)); + responseHeaders.addAll(extraHeaders); assertThat(clientFrameReader.nextFrame(clientFramesRead)).isTrue(); verify(clientFramesRead) .headers(false, false, streamId, -1, responseHeaders, HeadersMode.HTTP_20_HEADERS); diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java index 8f612804436..fd8af7f998f 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/OpenTelemetryMetricSink.java @@ -27,6 +27,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricSink; import io.opentelemetry.api.common.Attributes; @@ -36,6 +37,7 @@ import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.api.metrics.ObservableMeasurement; @@ -117,6 +119,22 @@ public void addLongCounter(LongCounterMetricInstrument metricInstrument, long va counter.add(value, attributes); } + @Override + public void addLongUpDownCounter(LongUpDownCounterMetricInstrument metricInstrument, long value, + List requiredLabelValues, + List optionalLabelValues) { + MeasuresData instrumentData = measures.get(metricInstrument.getIndex()); + if (instrumentData == null) { + // Disabled metric + return; + } + Attributes attributes = createAttributes(metricInstrument.getRequiredLabelKeys(), + metricInstrument.getOptionalLabelKeys(), requiredLabelValues, optionalLabelValues, + instrumentData.getOptionalLabelsBitSet()); + LongUpDownCounter counter = (LongUpDownCounter) instrumentData.getMeasure(); + counter.add(value, attributes); + } + @Override public void recordDoubleHistogram(DoubleHistogramMetricInstrument metricInstrument, double value, List requiredLabelValues, List optionalLabelValues) { @@ -256,6 +274,11 @@ public void updateMeasures(List instruments) { .setDescription(description) .ofLongs() .buildObserver(); + } else if (instrument instanceof LongUpDownCounterMetricInstrument) { + openTelemetryMeasure = openTelemetryMeter.upDownCounterBuilder(name) + .setUnit(unit) + .setDescription(description) + .build(); } else { logger.log(Level.FINE, "Unsupported metric instrument type : {0}", instrument); openTelemetryMeasure = null; diff --git a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java index 5214804d369..ef21903c8e7 100644 --- a/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java +++ b/opentelemetry/src/main/java/io/grpc/opentelemetry/internal/OpenTelemetryConstants.java @@ -36,6 +36,12 @@ public final class OpenTelemetryConstants { public static final AttributeKey BACKEND_SERVICE_KEY = AttributeKey.stringKey("grpc.lb.backend_service"); + public static final AttributeKey DISCONNECT_ERROR_KEY = + AttributeKey.stringKey("grpc.disconnect_error"); + + public static final AttributeKey SECURITY_LEVEL_KEY = + AttributeKey.stringKey("grpc.security_level"); + public static final List LATENCY_BUCKETS = ImmutableList.of( 0d, 0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d, diff --git a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java index c538da55dcb..cced4de3cb4 100644 --- a/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java +++ b/opentelemetry/src/test/java/io/grpc/opentelemetry/OpenTelemetryMetricSinkTest.java @@ -24,6 +24,7 @@ import io.grpc.LongCounterMetricInstrument; import io.grpc.LongGaugeMetricInstrument; import io.grpc.LongHistogramMetricInstrument; +import io.grpc.LongUpDownCounterMetricInstrument; import io.grpc.MetricInstrument; import io.grpc.MetricSink; import io.grpc.opentelemetry.internal.OpenTelemetryConstants; @@ -144,16 +145,25 @@ public void addCounter_enabledMetric() { "Number of client calls started", "count", Collections.emptyList(), Collections.emptyList(), true); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(2, "active_carrier_pigeons", + "Active Carrier Pigeons", "pigeons", + Collections.emptyList(), + Collections.emptyList(), true); + // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, Collections.emptyList()); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(longCounterInstrument, doubleCounterInstrument)); + sink.updateMeasures(Arrays.asList(longCounterInstrument, doubleCounterInstrument, + longUpDownCounterInstrument)); sink.addLongCounter(longCounterInstrument, 123L, Collections.emptyList(), Collections.emptyList()); sink.addDoubleCounter(doubleCounterInstrument, 12.0, Collections.emptyList(), Collections.emptyList()); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -3L, Collections.emptyList(), + Collections.emptyList()); assertThat(openTelemetryTesting.getMetrics()) .satisfiesExactlyInAnyOrder( @@ -184,7 +194,21 @@ public void addCounter_enabledMetric() { .hasPointsSatisfying( point -> point - .hasValue(12.0D)))); + .hasValue(12.0D))), + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("active_carrier_pigeons") + .hasDescription("Active Carrier Pigeons") + .hasUnit("pigeons") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasValue(-3L)))); } @Test @@ -192,18 +216,27 @@ public void addCounter_disabledMetric() { // set up sink with disabled metric Map enabledMetrics = new HashMap<>(); enabledMetrics.put("client_latency", false); + enabledMetrics.put("active_carrier_pigeons", false); LongCounterMetricInstrument instrument = new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", Collections.emptyList(), Collections.emptyList(), true); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(1, "active_carrier_pigeons", + "Active Carrier Pigeons", "pigeons", + Collections.emptyList(), + Collections.emptyList(), false); + // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, true, Collections.emptyList()); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(instrument)); + sink.updateMeasures(Arrays.asList(instrument, longUpDownCounterInstrument)); sink.addLongCounter(instrument, 123L, Collections.emptyList(), Collections.emptyList()); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -13L, Collections.emptyList(), + Collections.emptyList()); assertThat(openTelemetryTesting.getMetrics()).isEmpty(); } @@ -377,6 +410,7 @@ public void registerBatchCallback_bothEnabledAndDisabled() { public void recordLabels() { Map enabledMetrics = new HashMap<>(); enabledMetrics.put("client_latency", true); + enabledMetrics.put("ghosts_in_the_wire", true); List optionalLabels = Arrays.asList("optional_label_key_2"); @@ -384,16 +418,24 @@ public void recordLabels() { new LongCounterMetricInstrument(0, "client_latency", "Client latency", "s", ImmutableList.of("required_label_key_1", "required_label_key_2"), ImmutableList.of("optional_label_key_1", "optional_label_key_2"), false); + LongUpDownCounterMetricInstrument longUpDownCounterInstrument = + new LongUpDownCounterMetricInstrument(1, "ghosts_in_the_wire", + "Number of Ghosts Haunting the Wire", "{ghosts}", + ImmutableList.of("required_label_key_1", "required_label_key_2"), + ImmutableList.of("optional_label_key_1", "optional_label_key_2"), false); // Create sink sink = new OpenTelemetryMetricSink(testMeter, enabledMetrics, false, optionalLabels); // Invoke updateMeasures - sink.updateMeasures(Arrays.asList(longCounterInstrument)); + sink.updateMeasures(Arrays.asList(longCounterInstrument, longUpDownCounterInstrument)); sink.addLongCounter(longCounterInstrument, 123L, ImmutableList.of("required_label_value_1", "required_label_value_2"), ImmutableList.of("optional_label_value_1", "optional_label_value_2")); + sink.addLongUpDownCounter(longUpDownCounterInstrument, -400L, + ImmutableList.of("required_label_value_1", "required_label_value_2"), + ImmutableList.of("optional_label_value_1", "optional_label_value_2")); io.opentelemetry.api.common.Attributes expectedAtrributes = io.opentelemetry.api.common.Attributes.of( @@ -417,6 +459,22 @@ public void recordLabels() { point -> point .hasAttributes(expectedAtrributes) - .hasValue(123L)))); + .hasValue(123L))), + metric -> + assertThat(metric) + .hasInstrumentationScope(InstrumentationScopeInfo.create( + OpenTelemetryConstants.INSTRUMENTATION_SCOPE)) + .hasName("ghosts_in_the_wire") + .hasDescription("Number of Ghosts Haunting the Wire") + .hasUnit("{ghosts}") + .hasLongSumSatisfying( + longSum -> + longSum + .hasPointsSatisfying( + point -> + point + .hasAttributes(expectedAtrributes) + .hasValue(-400L)))); + } } diff --git a/repositories.bzl b/repositories.bzl index 47609ae7671..4b9d0327b66 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -19,9 +19,9 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "com.google.auto.value:auto-value:1.11.0", "com.google.code.findbugs:jsr305:3.0.2", "com.google.code.gson:gson:2.11.0", - "com.google.errorprone:error_prone_annotations:2.30.0", + "com.google.errorprone:error_prone_annotations:2.36.0", "com.google.guava:failureaccess:1.0.1", - "com.google.guava:guava:33.3.1-android", + "com.google.guava:guava:33.4.8-android", "com.google.re2j:re2j:1.8", "com.google.s2a.proto.v2:s2a-proto:0.1.2", "com.google.truth:truth:1.4.2", @@ -45,7 +45,7 @@ IO_GRPC_GRPC_JAVA_ARTIFACTS = [ "io.opencensus:opencensus-contrib-grpc-metrics:0.31.0", "io.perfmark:perfmark-api:0.27.0", "junit:junit:4.13.2", - "org.checkerframework:checker-qual:3.12.0", + "org.checkerframework:checker-qual:3.49.5", "org.codehaus.mojo:animal-sniffer-annotations:1.24", ] # GRPC_DEPS_END diff --git a/rls/build.gradle b/rls/build.gradle index 1193ab3d4bc..10b1d5fc371 100644 --- a/rls/build.gradle +++ b/rls/build.gradle @@ -22,7 +22,6 @@ dependencies { libraries.auto.value.annotations, libraries.guava annotationProcessor libraries.auto.value - compileOnly libraries.javax.annotation testImplementation libraries.truth, project(':grpc-grpclb'), project(':grpc-inprocess'), diff --git a/s2a/build.gradle b/s2a/build.gradle index 1e48e2bb297..c46993ec9c8 100644 --- a/s2a/build.gradle +++ b/s2a/build.gradle @@ -12,6 +12,7 @@ description = "gRPC: S2A" dependencies { implementation libraries.s2a.proto + implementation 'org.checkerframework:checker-qual:3.49.5' api project(':grpc-api') implementation project(':grpc-stub'), @@ -20,7 +21,6 @@ dependencies { libraries.protobuf.java, libraries.guava.jre // JRE required by protobuf-java-util from grpclb def nettyDependency = implementation project(':grpc-netty') - compileOnly libraries.javax.annotation shadow configurations.implementation.getDependencies().minus(nettyDependency) shadow project(path: ':grpc-netty-shaded', configuration: 'shadow') diff --git a/services/build.gradle b/services/build.gradle index 758f2a5c899..c30e1ba53bd 100644 --- a/services/build.gradle +++ b/services/build.gradle @@ -32,13 +32,11 @@ dependencies { runtimeOnly libraries.errorprone.annotations, libraries.gson // to fix checkUpperBoundDeps error here - compileOnly libraries.javax.annotation testImplementation project(':grpc-testing'), project(':grpc-inprocess'), libraries.netty.transport.epoll, // for DomainSocketAddress testFixtures(project(':grpc-core')), testFixtures(project(':grpc-api')) - testCompileOnly libraries.javax.annotation signature (libraries.signature.java) { artifact { extension = "signature" diff --git a/servlet/build.gradle b/servlet/build.gradle index 7f9cd04a57c..1367a72ab44 100644 --- a/servlet/build.gradle +++ b/servlet/build.gradle @@ -34,8 +34,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api') - compileOnly libraries.javax.servlet.api, - libraries.javax.annotation // java 9, 10 needs it + compileOnly libraries.javax.servlet.api implementation project(':grpc-core'), libraries.guava diff --git a/servlet/jakarta/build.gradle b/servlet/jakarta/build.gradle index 456b2b75e3e..bcd904ccaee 100644 --- a/servlet/jakarta/build.gradle +++ b/servlet/jakarta/build.gradle @@ -85,8 +85,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-api') - compileOnly libraries.jakarta.servlet.api, - libraries.javax.annotation + compileOnly libraries.jakarta.servlet.api implementation project(':grpc-util'), project(':grpc-core'), diff --git a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java index e9cb391ea08..c896c7a23ea 100644 --- a/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java +++ b/servlet/src/jettyTest/java/io/grpc/servlet/JettyTransportTest.java @@ -69,6 +69,7 @@ public void start(ServerListener listener) throws IOException { listener.transportCreated(new ServletServerBuilder.ServerTransportImpl(scheduler)); ServletAdapter adapter = new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); diff --git a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java index f68ed083506..8c1eb858ad1 100644 --- a/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java +++ b/servlet/src/main/java/io/grpc/servlet/GrpcServlet.java @@ -37,6 +37,7 @@ public class GrpcServlet extends HttpServlet { private static final long serialVersionUID = 1L; + @SuppressWarnings("serial") private final ServletAdapter servletAdapter; GrpcServlet(ServletAdapter servletAdapter) { diff --git a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java index 4bfe8949776..668e82425cb 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletAdapter.java @@ -22,6 +22,7 @@ import static java.util.logging.Level.FINE; import static java.util.logging.Level.FINEST; +import com.google.common.annotations.VisibleForTesting; import com.google.common.io.BaseEncoding; import io.grpc.Attributes; import io.grpc.ExperimentalApi; @@ -45,6 +46,7 @@ import java.util.Enumeration; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.logging.Logger; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; @@ -72,18 +74,23 @@ public final class ServletAdapter { static final Logger logger = Logger.getLogger(ServletAdapter.class.getName()); + static final Function DEFAULT_METHOD_NAME_RESOLVER = + req -> req.getRequestURI().substring(1); // remove the leading "/" private final ServerTransportListener transportListener; private final List streamTracerFactories; + private final Function methodNameResolver; private final int maxInboundMessageSize; private final Attributes attributes; ServletAdapter( ServerTransportListener transportListener, List streamTracerFactories, + Function methodNameResolver, int maxInboundMessageSize) { this.transportListener = transportListener; this.streamTracerFactories = streamTracerFactories; + this.methodNameResolver = methodNameResolver; this.maxInboundMessageSize = maxInboundMessageSize; attributes = transportListener.transportReady(Attributes.EMPTY); } @@ -119,7 +126,7 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx AsyncContext asyncCtx = req.startAsync(req, resp); - String method = req.getRequestURI().substring(1); // remove the leading "/" + String method = methodNameResolver.apply(req); Metadata headers = getHeaders(req); if (logger.isLoggable(FINEST)) { @@ -128,10 +135,9 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx } Long timeoutNanos = headers.get(TIMEOUT_KEY); - if (timeoutNanos == null) { - timeoutNanos = 0L; - } - asyncCtx.setTimeout(TimeUnit.NANOSECONDS.toMillis(timeoutNanos)); + asyncCtx.setTimeout(timeoutNanos != null + ? TimeUnit.NANOSECONDS.toMillis(timeoutNanos) + ASYNC_TIMEOUT_SAFETY_MARGIN + : 0); StatsTraceContext statsTraceCtx = StatsTraceContext.newServerContext(streamTracerFactories, method, headers); @@ -158,6 +164,12 @@ public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOEx asyncCtx.addListener(new GrpcAsyncListener(stream, logId)); } + /** + * Deadlines are managed via Context, servlet async timeout is not supposed to happen. + */ + @VisibleForTesting + static final long ASYNC_TIMEOUT_SAFETY_MARGIN = 5_000; + // This method must use Enumeration and its members, since that is the only way to read headers // from the servlet api. @SuppressWarnings("JdkObsolete") diff --git a/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java b/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java index 72c4383d273..aee25de01ad 100644 --- a/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java +++ b/servlet/src/main/java/io/grpc/servlet/ServletServerBuilder.java @@ -49,8 +49,10 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; +import javax.servlet.http.HttpServletRequest; /** * Builder to build a gRPC server that can run as a servlet. This is for advanced custom settings. @@ -64,6 +66,8 @@ @NotThreadSafe public final class ServletServerBuilder extends ForwardingServerBuilder { List streamTracerFactories; + private Function methodNameResolver = + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER; int maxInboundMessageSize = DEFAULT_MAX_MESSAGE_SIZE; private final ServerImplBuilder serverImplBuilder; @@ -98,7 +102,8 @@ public Server build() { * Creates a {@link ServletAdapter}. */ public ServletAdapter buildServletAdapter() { - return new ServletAdapter(buildAndStart(), streamTracerFactories, maxInboundMessageSize); + return new ServletAdapter(buildAndStart(), streamTracerFactories, methodNameResolver, + maxInboundMessageSize); } /** @@ -176,6 +181,18 @@ public ServletServerBuilder useTransportSecurity(File certChain, File privateKey throw new UnsupportedOperationException("TLS should be configured by the servlet container"); } + /** + * Specifies how to determine gRPC method name from servlet request. + * + *

The default strategy is using {@link HttpServletRequest#getRequestURI()} without the leading + * slash.

+ */ + public ServletServerBuilder methodNameResolver( + Function methodResolver) { + this.methodNameResolver = checkNotNull(methodResolver); + return this; + } + @Override public ServletServerBuilder maxInboundMessageSize(int bytes) { checkArgument(bytes >= 0, "bytes must be >= 0"); diff --git a/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java b/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java index d571cfd45d5..7a8c5b91f25 100644 --- a/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java +++ b/servlet/src/test/java/io/grpc/servlet/ServletServerBuilderTest.java @@ -80,7 +80,7 @@ public void scheduledExecutorService() throws Exception { ServletAdapter servletAdapter = serverBuilder.buildServletAdapter(); servletAdapter.doPost(request, response); - verify(asyncContext).setTimeout(1); + verify(asyncContext).setTimeout(1 + ServletAdapter.ASYNC_TIMEOUT_SAFETY_MARGIN); // The following just verifies that scheduler is populated to the transport. // It doesn't matter what tasks (such as handshake timeout and request deadline) are actually diff --git a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java index 262036883a9..2171c6eb2df 100644 --- a/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java +++ b/servlet/src/tomcatTest/java/io/grpc/servlet/TomcatTransportTest.java @@ -81,7 +81,9 @@ public void start(ServerListener listener) throws IOException { ServerTransportListener serverTransportListener = listener.transportCreated(new ServerTransportImpl(scheduler)); ServletAdapter adapter = - new ServletAdapter(serverTransportListener, streamTracerFactories, Integer.MAX_VALUE); + new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, + Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); tomcatServer = new Tomcat(); diff --git a/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java b/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java index e14c11985de..ef897c87d70 100644 --- a/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java +++ b/servlet/src/undertowTest/java/io/grpc/servlet/UndertowTransportTest.java @@ -100,7 +100,9 @@ public void start(ServerListener listener) throws IOException { ServerTransportListener serverTransportListener = listener.transportCreated(new ServerTransportImpl(scheduler)); ServletAdapter adapter = - new ServletAdapter(serverTransportListener, streamTracerFactories, Integer.MAX_VALUE); + new ServletAdapter(serverTransportListener, streamTracerFactories, + ServletAdapter.DEFAULT_METHOD_NAME_RESOLVER, + Integer.MAX_VALUE); GrpcServlet grpcServlet = new GrpcServlet(adapter); InstanceFactory instanceFactory = () -> new ImmediateInstanceHandle<>(grpcServlet); diff --git a/settings.gradle b/settings.gradle index 22a49f0c3be..f4df1105090 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,17 @@ pluginManagement { + // https://issuetracker.google.com/issues/342522142#comment8 + // use D8/R8 8.0.44 or 8.1.44 with AGP 7.4 if needed. + buildscript { + repositories { + mavenCentral() + maven { + url = uri("https://storage.googleapis.com/r8-releases/raw") + } + } + dependencies { + classpath("com.android.tools:r8:8.1.44") + } + } plugins { // https://developer.android.com/build/releases/gradle-plugin // 8+ has many changes: https://github.com/grpc/grpc-java/issues/10152 diff --git a/stub/src/main/java/io/grpc/stub/ClientCalls.java b/stub/src/main/java/io/grpc/stub/ClientCalls.java index e5a94f4d864..8cd31ea9cca 100644 --- a/stub/src/main/java/io/grpc/stub/ClientCalls.java +++ b/stub/src/main/java/io/grpc/stub/ClientCalls.java @@ -931,7 +931,7 @@ public void waitAndDrainWithTimeout(boolean waitForever, long end, } while ((runnable = poll()) != null); // Wake everything up now that we've done something and they can check in their outer loop // if they can continue or need to wait again. - signallAll(); + signalAll(); } } @@ -949,11 +949,11 @@ public void drain() throws InterruptedException { } if (didWork) { - signallAll(); + signalAll(); } } - private void signallAll() { + private void signalAll() { waiterLock.lock(); try { waiterCondition.signalAll(); diff --git a/testing-proto/build.gradle b/testing-proto/build.gradle index a34392b26d2..ee602bc5135 100644 --- a/testing-proto/build.gradle +++ b/testing-proto/build.gradle @@ -17,9 +17,7 @@ tasks.named("jar").configure { dependencies { api project(':grpc-protobuf'), project(':grpc-stub') - compileOnly libraries.javax.annotation testImplementation libraries.truth - testRuntimeOnly libraries.javax.annotation signature (libraries.signature.java) { artifact { extension = "signature" diff --git a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java index b4b9b25d1de..0739fa3d453 100644 --- a/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java +++ b/util/src/main/java/io/grpc/util/AdvancedTlsX509TrustManager.java @@ -339,6 +339,10 @@ public void run() { private long readAndUpdate(File trustCertFile, long oldTime) throws IOException, GeneralSecurityException { long newTime = checkNotNull(trustCertFile, "trustCertFile").lastModified(); + if (newTime == 0) { + throw new IOException( + "Certificate file not found or not readable: " + trustCertFile.getAbsolutePath()); + } if (newTime == oldTime) { return oldTime; } diff --git a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java index 2a93ef964f7..acc186e3be6 100644 --- a/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java +++ b/util/src/main/java/io/grpc/util/MultiChildLoadBalancer.java @@ -268,6 +268,8 @@ public class ChildLbState { private ConnectivityState currentState; private SubchannelPicker currentPicker = new FixedResultPicker(PickResult.withNoResult()); + @SuppressWarnings("this-escape") + // TODO(okshiva): Fix 'this-escape' from the constructor before making the API public. public ChildLbState(Object key, LoadBalancer.Factory policyFactory) { this.key = key; this.lb = policyFactory.newLoadBalancer(createChildHelper()); diff --git a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java index 228dbf5ea5b..b9803b03570 100644 --- a/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java +++ b/util/src/test/java/io/grpc/util/AdvancedTlsX509TrustManagerTest.java @@ -142,6 +142,17 @@ record -> record.getMessage().contains("Default value of ")); } } + @Test + public void missingFile_throwsFileNotFoundException() throws Exception { + AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder().build(); + File nonExistentFile = new File("missing_cert.pem"); + Exception thrown = + assertThrows(Exception.class, () -> trustManager.updateTrustCredentials(nonExistentFile)); + assertNotNull(thrown); + assertEquals(thrown.getMessage(), + "Certificate file not found or not readable: " + nonExistentFile.getAbsolutePath()); + } + @Test public void clientTrustedWithSocketTest() throws Exception { AdvancedTlsX509TrustManager trustManager = AdvancedTlsX509TrustManager.newBuilder() diff --git a/xds/build.gradle b/xds/build.gradle index 72dea373097..8394fe12f6b 100644 --- a/xds/build.gradle +++ b/xds/build.gradle @@ -41,7 +41,6 @@ configurations { } dependencies { - thirdpartyCompileOnly libraries.javax.annotation thirdpartyImplementation project(':grpc-protobuf'), project(':grpc-stub') compileOnly sourceSets.thirdparty.output diff --git a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java index 034cdee0815..fba66e2e8d7 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterImplLoadBalancer.java @@ -305,7 +305,7 @@ private List withAdditionalAttributes( private ClusterLocality createClusterLocalityFromAttributes(Attributes addressAttributes) { Locality locality = addressAttributes.get(XdsAttributes.ATTR_LOCALITY); - String localityName = addressAttributes.get(XdsAttributes.ATTR_LOCALITY_NAME); + String localityName = addressAttributes.get(EquivalentAddressGroup.ATTR_LOCALITY_NAME); // Endpoint addresses resolved by ClusterResolverLoadBalancer should always contain // attributes with its locality, including endpoints in LOGICAL_DNS clusters. diff --git a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java index e333c46750c..f57cada52e9 100644 --- a/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/ClusterResolverLoadBalancer.java @@ -408,7 +408,7 @@ public void run() { Attributes attr = endpoint.eag().getAttributes().toBuilder() .set(XdsAttributes.ATTR_LOCALITY, locality) - .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) .set(XdsAttributes.ATTR_LOCALITY_WEIGHT, localityLbInfo.localityWeight()) .set(XdsAttributes.ATTR_SERVER_WEIGHT, weight) @@ -659,7 +659,7 @@ public Status onResult2(final ResolutionResult resolutionResult) { String localityName = localityName(LOGICAL_DNS_CLUSTER_LOCALITY); Attributes attr = eag.getAttributes().toBuilder() .set(XdsAttributes.ATTR_LOCALITY, LOGICAL_DNS_CLUSTER_LOCALITY) - .set(XdsAttributes.ATTR_LOCALITY_NAME, localityName) + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, localityName) .set(XdsAttributes.ATTR_ADDRESS_NAME, dnsHostName) .build(); eag = new EquivalentAddressGroup(eag.getAddresses(), attr); diff --git a/xds/src/main/java/io/grpc/xds/MessagePrinter.java b/xds/src/main/java/io/grpc/xds/MessagePrinter.java index db15e961204..d6fdaa81dd7 100644 --- a/xds/src/main/java/io/grpc/xds/MessagePrinter.java +++ b/xds/src/main/java/io/grpc/xds/MessagePrinter.java @@ -37,6 +37,7 @@ import io.envoyproxy.envoy.extensions.load_balancing_policies.wrr_locality.v3.WrrLocality; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext; import io.envoyproxy.envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext; +import io.envoyproxy.envoy.service.discovery.v3.Resource; import io.grpc.xds.client.MessagePrettyPrinter; /** @@ -55,6 +56,7 @@ private static class LazyHolder { private static JsonFormat.Printer newPrinter() { TypeRegistry.Builder registry = TypeRegistry.newBuilder() + .add(Resource.getDescriptor()) .add(Listener.getDescriptor()) .add(HttpConnectionManager.getDescriptor()) .add(HTTPFault.getDescriptor()) diff --git a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java index ab1abb1da15..1a12412f923 100644 --- a/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java +++ b/xds/src/main/java/io/grpc/xds/WrrLocalityLoadBalancer.java @@ -74,7 +74,7 @@ public Status acceptResolvedAddresses(ResolvedAddresses resolvedAddresses) { Map localityWeights = new HashMap<>(); for (EquivalentAddressGroup eag : resolvedAddresses.getAddresses()) { Attributes eagAttrs = eag.getAttributes(); - String locality = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_NAME); + String locality = eagAttrs.get(EquivalentAddressGroup.ATTR_LOCALITY_NAME); Integer localityWeight = eagAttrs.get(XdsAttributes.ATTR_LOCALITY_WEIGHT); if (locality == null) { diff --git a/xds/src/main/java/io/grpc/xds/XdsAttributes.java b/xds/src/main/java/io/grpc/xds/XdsAttributes.java index 4a64fdb1453..2e165201e5f 100644 --- a/xds/src/main/java/io/grpc/xds/XdsAttributes.java +++ b/xds/src/main/java/io/grpc/xds/XdsAttributes.java @@ -81,13 +81,6 @@ final class XdsAttributes { static final Attributes.Key ATTR_LOCALITY = Attributes.Key.create("io.grpc.xds.XdsAttributes.locality"); - /** - * The name of the locality that this EquivalentAddressGroup is in. - */ - @EquivalentAddressGroup.Attr - static final Attributes.Key ATTR_LOCALITY_NAME = - Attributes.Key.create("io.grpc.xds.XdsAttributes.localityName"); - /** * Endpoint weight for load balancing purposes. */ diff --git a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java index 7df0630b779..c5e3f80f170 100644 --- a/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/ClusterImplLoadBalancerTest.java @@ -1017,7 +1017,7 @@ public String toString() { Attributes.Builder attributes = Attributes.newBuilder() .set(XdsAttributes.ATTR_LOCALITY, locality) // Unique but arbitrary string - .set(XdsAttributes.ATTR_LOCALITY_NAME, locality.toString()); + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, locality.toString()); if (authorityHostname != null) { attributes.set(XdsAttributes.ATTR_ADDRESS_NAME, authorityHostname); } diff --git a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java index 19266b0d289..9ff19c6d1b0 100644 --- a/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java +++ b/xds/src/test/java/io/grpc/xds/GrpcXdsClientImplTestBase.java @@ -220,7 +220,7 @@ public boolean shouldAccept(Runnable command) { protected final Queue loadReportCalls = new ArrayDeque<>(); protected final AtomicBoolean adsEnded = new AtomicBoolean(true); protected final AtomicBoolean lrsEnded = new AtomicBoolean(true); - private final MessageFactory mf = createMessageFactory(); + protected MessageFactory mf; private static final long TIME_INCREMENT = TimeUnit.SECONDS.toNanos(1); /** Fake time provider increments time TIME_INCREMENT each call. */ @@ -234,37 +234,22 @@ public long currentTimeNanos() { private static final int VHOST_SIZE = 2; // LDS test resources. - private final Any testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(LDS_RESOURCE, - mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); - private final Any testListenerRds = - Any.pack(mf.buildListenerWithApiListenerForRds(LDS_RESOURCE, RDS_RESOURCE)); + private Any testListenerVhosts; + private Any testListenerRds; // RDS test resources. - private final Any testRouteConfig = - Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(VHOST_SIZE))); + private Any testRouteConfig; // CDS test resources. - private final Any testClusterRoundRobin = - Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, - null, false, null, "envoy.transport_sockets.tls", null, null - )); + private Any testClusterRoundRobin; // EDS test resources. - private final Message lbEndpointHealthy = - mf.buildLocalityLbEndpoints("region1", "zone1", "subzone1", - mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2, "endpoint-host-name"), 1, 0); + private Message lbEndpointHealthy; // Locality with 0 endpoints - private final Message lbEndpointEmpty = - mf.buildLocalityLbEndpoints("region3", "zone3", "subzone3", - ImmutableList.of(), 2, 1); + private Message lbEndpointEmpty; // Locality with 0-weight endpoint - private final Message lbEndpointZeroWeight = - mf.buildLocalityLbEndpoints("region4", "zone4", "subzone4", - mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5, "endpoint-host-name"), 0, 2); - private final Any testClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, - ImmutableList.of(lbEndpointHealthy, lbEndpointEmpty, lbEndpointZeroWeight), - ImmutableList.of(mf.buildDropOverload("lb", 200), mf.buildDropOverload("throttle", 1000)))); - + private Message lbEndpointZeroWeight; + private Any testClusterLoadAssignment; @Captor private ArgumentCaptor ldsUpdateCaptor; @Captor @@ -304,8 +289,8 @@ public long currentTimeNanos() { private boolean originalEnableLeastRequest; private Server xdsServer; private final String serverName = InProcessServerBuilder.generateName(); - private final BindableService adsService = createAdsService(); - private final BindableService lrsService = createLrsService(); + private BindableService adsService; + private BindableService lrsService; private XdsTransportFactory xdsTransportFactory = new XdsTransportFactory() { @Override @@ -333,6 +318,32 @@ public XdsTransport create(ServerInfo serverInfo) { @Before public void setUp() throws IOException { + mf = createMessageFactory(); + testListenerVhosts = Any.pack(mf.buildListenerWithApiListener(LDS_RESOURCE, + mf.buildRouteConfiguration("do not care", mf.buildOpaqueVirtualHosts(VHOST_SIZE)))); + testListenerRds = + Any.pack(mf.buildListenerWithApiListenerForRds(LDS_RESOURCE, RDS_RESOURCE)); + testRouteConfig = + Any.pack(mf.buildRouteConfiguration(RDS_RESOURCE, mf.buildOpaqueVirtualHosts(VHOST_SIZE))); + testClusterRoundRobin = + Any.pack(mf.buildEdsCluster(CDS_RESOURCE, null, "round_robin", null, + null, false, null, "envoy.transport_sockets.tls", null, null + )); + lbEndpointHealthy = + mf.buildLocalityLbEndpoints("region1", "zone1", "subzone1", + mf.buildLbEndpoint("192.168.0.1", 8080, "healthy", 2, "endpoint-host-name"), 1, 0); + lbEndpointEmpty = + mf.buildLocalityLbEndpoints("region3", "zone3", "subzone3", + ImmutableList.of(), 2, 1); + lbEndpointZeroWeight = + mf.buildLocalityLbEndpoints("region4", "zone4", "subzone4", + mf.buildLbEndpoint("192.168.142.5", 80, "unknown", 5, "endpoint-host-name"), 0, 2); + testClusterLoadAssignment = Any.pack(mf.buildClusterLoadAssignment(EDS_RESOURCE, + ImmutableList.of(lbEndpointHealthy, lbEndpointEmpty, lbEndpointZeroWeight), + ImmutableList.of(mf.buildDropOverload("lb", 200), mf.buildDropOverload("throttle", 1000)))); + adsService = createAdsService(); + lrsService = createLrsService(); + when(backoffPolicyProvider.get()).thenReturn(backoffPolicy1, backoffPolicy2); when(backoffPolicy1.nextBackoffNanos()).thenReturn(10L, 100L); when(backoffPolicy2.nextBackoffNanos()).thenReturn(20L, 200L); diff --git a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java index b6a5d8dbf73..584c32738c5 100644 --- a/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java +++ b/xds/src/test/java/io/grpc/xds/WrrLocalityLoadBalancerTest.java @@ -254,7 +254,7 @@ public String toString() { } Attributes.Builder attrBuilder = Attributes.newBuilder() - .set(XdsAttributes.ATTR_LOCALITY_NAME, locality); + .set(EquivalentAddressGroup.ATTR_LOCALITY_NAME, locality); if (localityWeight != null) { attrBuilder.set(XdsAttributes.ATTR_LOCALITY_WEIGHT, localityWeight); } From 01f46c784f36ccedc2540e58014836d9820b26ea Mon Sep 17 00:00:00 2001 From: jdcormie Date: Thu, 2 Oct 2025 10:43:56 -0700 Subject: [PATCH 33/34] address review comments Make ClientHandshake an interface Make the impl private/final --- .../binder/internal/BinderClientTransport.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java index d6d5f8c977c..c7d954db1cc 100644 --- a/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java +++ b/binder/src/main/java/io/grpc/binder/internal/BinderClientTransport.java @@ -73,6 +73,8 @@ public final class BinderClientTransport extends BinderTransport private final Executor offloadExecutor; private final SecurityPolicy securityPolicy; private final Bindable serviceBinding; + + @GuardedBy("this") private final ClientHandshake handshake; /** Number of ongoing calls which keep this transport "in-use". */ @@ -356,17 +358,17 @@ private ListenableFuture checkServerAuthorizationAsync(int remoteUid) { : Futures.submit(() -> securityPolicy.checkAuthorization(remoteUid), offloadExecutor); } - class LegacyClientHandshake extends ClientHandshake { + private final class LegacyClientHandshake implements ClientHandshake { @Override @MainThread - @GuardedBy("BinderClientTransport.this") + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. public void onBound(OneWayBinderProxy binder) { sendSetupTransaction(binder); } @Override @BinderThread - @GuardedBy("BinderClientTransport.this") + @GuardedBy("BinderClientTransport.this") // By way of @GuardedBy("this") `handshake` member. public void handleSetupTransport(OneWayBinderProxy binder) { int remoteUid = Binder.getCallingUid(); restrictIncomingBinderToCallsFrom(remoteUid); @@ -436,22 +438,20 @@ protected void handlePingResponse(Parcel parcel) { * *

Supports a clean migration away from the legacy approach, one client at a time. */ - abstract class ClientHandshake { + private interface ClientHandshake { /** * Notifies the implementation that the binding has succeeded and we are now connected to the * server's "endpoint" which can be reached at 'endpointBinder'. */ - @GuardedBy("BinderClientTransport.this") @MainThread - abstract void onBound(OneWayBinderProxy endpointBinder); + void onBound(OneWayBinderProxy endpointBinder); /** * Notifies the implementation that we've received a valid SETUP_TRANSPORT transaction from a * server that can be reached at 'serverBinder'. */ - @GuardedBy("BinderClientTransport.this") @BinderThread - abstract void handleSetupTransport(OneWayBinderProxy serverBinder); + void handleSetupTransport(OneWayBinderProxy serverBinder); } private static ClientStream newFailingClientStream( From 1cb32594db7b2400895a50fce6073132ff3f6c08 Mon Sep 17 00:00:00 2001 From: jdcormie Date: Thu, 2 Oct 2025 12:13:42 -0700 Subject: [PATCH 34/34] Use two named useXXX() BinderChannelBuilder methods instead of a bool --- .../grpc/binder/BinderChannelSmokeTest.java | 29 +++++++--- .../io/grpc/binder/BinderChannelBuilder.java | 55 ++++++++++++++----- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java index e3a8c58bf88..4e3cfcf0d05 100644 --- a/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java +++ b/binder/src/androidTest/java/io/grpc/binder/BinderChannelSmokeTest.java @@ -97,6 +97,7 @@ public final class BinderChannelSmokeTest { .setType(MethodDescriptor.MethodType.BIDI_STREAMING) .build(); + AndroidComponentAddress serverAddress; ManagedChannel channel; AtomicReference headersCapture = new AtomicReference<>(); AtomicReference clientUidCapture = new AtomicReference<>(); @@ -134,7 +135,7 @@ public void setUp() throws Exception { TestUtils.recordRequestHeadersInterceptor(headersCapture), PeerUids.newPeerIdentifyingServerInterceptor()); - AndroidComponentAddress serverAddress = HostServices.allocateService(appContext); + serverAddress = HostServices.allocateService(appContext); HostServices.configureService( serverAddress, HostServices.serviceParamsBuilder() @@ -149,13 +150,15 @@ public void setUp() throws Exception { .build()) .build()); - channel = - BinderChannelBuilder.forAddress(serverAddress, appContext) + channel = newBinderChannelBuilder().build(); + } + + BinderChannelBuilder newBinderChannelBuilder() { + return BinderChannelBuilder.forAddress(serverAddress, appContext) .inboundParcelablePolicy( - InboundParcelablePolicy.newBuilder() - .setAcceptParcelableMetadataValues(true) - .build()) - .build(); + InboundParcelablePolicy.newBuilder() + .setAcceptParcelableMetadataValues(true) + .build()); } @After @@ -185,6 +188,18 @@ public void testBasicCall() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); } + @Test + public void testBasicCallWithLegacyAuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useLegacyAuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + + @Test + public void testBasicCallWithV2AuthStrategy() throws Exception { + channel = newBinderChannelBuilder().useV2AuthStrategy().build(); + assertThat(doCall("Hello").get()).isEqualTo("Hello"); + } + @Test public void testPeerUidIsRecorded() throws Exception { assertThat(doCall("Hello").get()).isEqualTo("Hello"); diff --git a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java index 6d656aaf560..1a68725d823 100644 --- a/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java +++ b/binder/src/main/java/io/grpc/binder/BinderChannelBuilder.java @@ -309,33 +309,58 @@ public BinderChannelBuilder preAuthorizeServers(boolean preAuthorize) { } /** - * Specifies how and when to authorize a server against this channel's {@link SecurityPolicy}. + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. * - *

The legacy authorization strategy considers the UID of the server *process* we connect to. - * This is problematic for services using the `android:isolatedProcess` feature which runs them - * under a different UID and without any of the privileges of the hosting app. The new and - * improved strategy uses the server *app's* UID instead, which lets clients authorize all types - * of servers in the same way, isolated or not. + *

This method selects the original "legacy" authorization strategy, which is no longer + * preferred for two reasons. First, the legacy strategy considers the UID of the server *process* + * we connect to. This is problematic for services using the `android:isolatedProcess` attribute, + * which runs them under a different UID and without any of the privileges of the hosting app. + * Second, the legacy authorization strategy performs SecurityPolicy checks later in the + * handshake, which means the calling UID must be rechecked on every subsequent transaction. For + * these reasons, prefer {@link #useV2AuthStrategy()} instead. * - *

The new and improved authorization strategy performs SecurityPolicy checks earlier the + *

The server does not know which authorization strategy a client is using. Both strategies + * work with all versions of the grpc-binder server. + * + *

The default authorization strategy is unspecified. Clients that require the legacy strategy + * should configure it explicitly using this method. Eventually support for the legacy strategy + * will be removed. + * + * @return this + */ + public BinderChannelBuilder useLegacyAuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(true); + return this; + } + + /** + * Specifies how and when to authorize a server against this Channel's {@link SecurityPolicy}. + * + *

This method selects the v2 authorization strategy. It improves on {@link + * #useLegacyAuthStrategy()}, by considering the UID of the server *app* we connect to, rather + * than the server *process*. This allows clients to connect to services using the + * `android:isolatedProcess` attribute, which runs them under a different ephemeral UID and + * without any of the privileges of the hosting app. + * + *

Furthermore, the v2 authorization strategy performs SecurityPolicy checks earlier the * handshake, which allows subsequent transactions over the connection to proceed securely without - * further UID checks. + * further UID checks. For these reasons, clients should prefer the v2 strategy. * *

The server does not know which authorization strategy a client is using. Both strategies * work with all versions of the grpc-binder server. * - *

The default value of this property is true but it will become false in a future release. - * Clients that require a particular authorization strategy should configure it explicitly using - * this method rather than relying on the default. Eventually support for the legacy behavior will - * be removed. + *

The default authorization strategy is unspecified. Clients that require the v2 strategy + * should configure it explicitly using this method. Eventually support for the legacy strategy + * will be removed. * *

If moving to the new authorization strategy causes a robolectric test to fail, ensure your - * fake service component is registered with `ShadowPackageManager` using `addOrUpdateService()`. + * fake Service component is registered with `ShadowPackageManager` using `addOrUpdateService()`. * * @return this */ - public BinderChannelBuilder useLegacyAuthStrategy(boolean useLegacyAuthStrategy) { - transportFactoryBuilder.setUseLegacyAuthStrategy(useLegacyAuthStrategy); + @ExperimentalApi("https://github.com/grpc/grpc-java/issues/12397") + public BinderChannelBuilder useV2AuthStrategy() { + transportFactoryBuilder.setUseLegacyAuthStrategy(false); return this; }