From 661919cdc177b84a6003b0f24b880aef386c6ee9 Mon Sep 17 00:00:00 2001 From: hidingmyname Date: Mon, 2 Nov 2026 20:38:07 +0800 Subject: [PATCH] xds: Validate ring_hash requestHashHeader when parsing the config gRFC A76 requires the config to be rejected if request_hash_header is not a valid header name or ends in -bin. The provider accepted any string, and an invalid name only failed later in RingHashLoadBalancer.acceptResolvedAddresses(), where Metadata.Key.of() throws inside the SynchronizationContext and panics the channel. Validate the name in RingHashLoadBalancerProvider with the same Metadata.Key rules and return an UNAVAILABLE config error if it is invalid. The stored name is now the lower-cased form used by Metadata.Key. Fixes #13151 --- .../xds/RingHashLoadBalancerProvider.java | 11 ++++ .../xds/RingHashLoadBalancerProviderTest.java | 50 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java index bb4f8de5a5f..f265b1ba001 100644 --- a/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java +++ b/xds/src/main/java/io/grpc/xds/RingHashLoadBalancerProvider.java @@ -22,6 +22,7 @@ import io.grpc.LoadBalancer; import io.grpc.LoadBalancer.Helper; import io.grpc.LoadBalancerProvider; +import io.grpc.Metadata; import io.grpc.NameResolver.ConfigOrError; import io.grpc.Status; import io.grpc.internal.GrpcUtil; @@ -106,6 +107,16 @@ private ConfigOrError parseLoadBalancingPolicyConfigInternal( return ConfigOrError.fromError(Status.UNAVAILABLE.withDescription( "Invalid 'minRingSize'/'maxRingSize'")); } + if (!requestHashHeader.isEmpty()) { + // gRFC A76: reject header names that are invalid or refer to a binary (-bin) header. + try { + requestHashHeader = + Metadata.Key.of(requestHashHeader, Metadata.ASCII_STRING_MARSHALLER).name(); + } catch (IllegalArgumentException e) { + return ConfigOrError.fromError(Status.UNAVAILABLE.withDescription( + "Invalid 'requestHashHeader': " + requestHashHeader).withCause(e)); + } + } return ConfigOrError.fromConfig( new RingHashConfig(minRingSize, maxRingSize, requestHashHeader)); } diff --git a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java index 66c9c5c537e..adc26b982da 100644 --- a/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java +++ b/xds/src/test/java/io/grpc/xds/RingHashLoadBalancerProviderTest.java @@ -281,6 +281,56 @@ public void parseLoadBalancingConfig_requestHashHeaderUnsetWhenEnvVarSet_useDefa } } + @Test + public void parseLoadBalancingConfig_requestHashHeaderWithBinSuffix_rejected() + throws IOException { + System.setProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY, "true"); + try { + String lbConfig = "{\"requestHashHeader\" : \"dummy-hash-bin\"}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(configOrError.getError().getDescription()) + .isEqualTo("Invalid 'requestHashHeader': dummy-hash-bin"); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY); + } + } + + @Test + public void parseLoadBalancingConfig_requestHashHeaderWithInvalidCharacters_rejected() + throws IOException { + System.setProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY, "true"); + try { + String lbConfig = "{\"requestHashHeader\" : \"dummy!hash\"}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getError()).isNotNull(); + assertThat(configOrError.getError().getCode()).isEqualTo(Code.UNAVAILABLE); + assertThat(configOrError.getError().getDescription()) + .isEqualTo("Invalid 'requestHashHeader': dummy!hash"); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY); + } + } + + @Test + public void parseLoadBalancingConfig_requestHashHeaderMixedCase_lowerCased() + throws IOException { + System.setProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY, "true"); + try { + String lbConfig = "{\"requestHashHeader\" : \"Dummy-Hash\"}"; + ConfigOrError configOrError = + provider.parseLoadBalancingPolicyConfig(parseJsonObject(lbConfig)); + assertThat(configOrError.getConfig()).isNotNull(); + RingHashConfig config = (RingHashConfig) configOrError.getConfig(); + assertThat(config.requestHashHeader).isEqualTo("dummy-hash"); + } finally { + System.clearProperty(GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY); + } + } + @SuppressWarnings("unchecked") private static Map parseJsonObject(String json) throws IOException { return (Map) JsonParser.parse(json);