Skip to content

Commit 735b3f3

Browse files
authored
netty: add soft Metadata size limit enforcement. (#11603)
1 parent fe350cf commit 735b3f3

14 files changed

+722
-264
lines changed

netty/src/main/java/io/grpc/netty/AbstractNettyHandler.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler {
4242

4343
private final int initialConnectionWindow;
4444
private final FlowControlPinger flowControlPing;
45-
45+
protected final int maxHeaderListSize;
46+
protected final int softLimitHeaderListSize;
4647
private boolean autoTuneFlowControlOn;
4748
private ChannelHandlerContext ctx;
4849
private boolean initialWindowSent = false;
@@ -58,7 +59,9 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler {
5859
ChannelLogger negotiationLogger,
5960
boolean autoFlowControl,
6061
PingLimiter pingLimiter,
61-
Ticker ticker) {
62+
Ticker ticker,
63+
int maxHeaderListSize,
64+
int softLimitHeaderListSize) {
6265
super(channelUnused, decoder, encoder, initialSettings, negotiationLogger);
6366

6467
// During a graceful shutdown, wait until all streams are closed.
@@ -73,6 +76,8 @@ abstract class AbstractNettyHandler extends GrpcHttp2ConnectionHandler {
7376
}
7477
this.flowControlPing = new FlowControlPinger(pingLimiter);
7578
this.ticker = checkNotNull(ticker, "ticker");
79+
this.maxHeaderListSize = maxHeaderListSize;
80+
this.softLimitHeaderListSize = softLimitHeaderListSize;
7681
}
7782

7883
@Override

netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java

Lines changed: 109 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2<NettyCh
104104
private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL;
105105
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
106106
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
107+
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
107108
private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
108109
private long keepAliveTimeNanos = KEEPALIVE_TIME_NANOS_DISABLED;
109110
private long keepAliveTimeoutNanos = DEFAULT_KEEPALIVE_TIMEOUT_NANOS;
@@ -452,6 +453,40 @@ public NettyChannelBuilder maxHeaderListSize(int maxHeaderListSize) {
452453
public NettyChannelBuilder maxInboundMetadataSize(int bytes) {
453454
checkArgument(bytes > 0, "maxInboundMetadataSize must be > 0");
454455
this.maxHeaderListSize = bytes;
456+
// Clear the soft limit setting, by setting soft limit to maxInboundMetadataSize. The
457+
// maxInboundMetadataSize will take precedence be applied before soft limit check.
458+
this.softLimitHeaderListSize = bytes;
459+
return this;
460+
}
461+
462+
/**
463+
* Sets the size of metadata that clients are advised to not exceed. When a metadata with size
464+
* larger than the soft limit is encountered there will be a probability the RPC will fail. The
465+
* chance of failing increases as the metadata size approaches the hard limit.
466+
* {@code Integer.MAX_VALUE} disables the enforcement. The default is implementation-dependent,
467+
* but is not generally less than 8 KiB and may be unlimited.
468+
*
469+
* <p>This is cumulative size of the metadata. The precise calculation is
470+
* implementation-dependent, but implementations are encouraged to follow the calculation used
471+
* for
472+
* <a href="http://httpwg.org/specs/rfc7540.html#rfc.section.6.5.2">HTTP/2's
473+
* SETTINGS_MAX_HEADER_LIST_SIZE</a>. It sums the bytes from each entry's key and value, plus 32
474+
* bytes of overhead per entry.
475+
*
476+
* @param soft the soft size limit of received metadata
477+
* @param max the hard size limit of received metadata
478+
* @return this
479+
* @throws IllegalArgumentException if soft and/or max is non-positive, or max smaller than
480+
* soft
481+
* @since 1.68.0
482+
*/
483+
@CanIgnoreReturnValue
484+
public NettyChannelBuilder maxInboundMetadataSize(int soft, int max) {
485+
checkArgument(soft > 0, "softLimitHeaderListSize must be > 0");
486+
checkArgument(max > soft,
487+
"maxInboundMetadataSize must be greater than softLimitHeaderListSize");
488+
this.softLimitHeaderListSize = soft;
489+
this.maxHeaderListSize = max;
455490
return this;
456491
}
457492

@@ -573,10 +608,22 @@ ClientTransportFactory buildTransportFactory() {
573608

574609
ProtocolNegotiator negotiator = protocolNegotiatorFactory.newNegotiator();
575610
return new NettyTransportFactory(
576-
negotiator, channelFactory, channelOptions,
577-
eventLoopGroupPool, autoFlowControl, flowControlWindow, maxInboundMessageSize,
578-
maxHeaderListSize, keepAliveTimeNanos, keepAliveTimeoutNanos, keepAliveWithoutCalls,
579-
transportTracerFactory, localSocketPicker, useGetForSafeMethods, transportSocketType);
611+
negotiator,
612+
channelFactory,
613+
channelOptions,
614+
eventLoopGroupPool,
615+
autoFlowControl,
616+
flowControlWindow,
617+
maxInboundMessageSize,
618+
maxHeaderListSize,
619+
softLimitHeaderListSize,
620+
keepAliveTimeNanos,
621+
keepAliveTimeoutNanos,
622+
keepAliveWithoutCalls,
623+
transportTracerFactory,
624+
localSocketPicker,
625+
useGetForSafeMethods,
626+
transportSocketType);
580627
}
581628

582629
@VisibleForTesting
@@ -710,6 +757,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
710757
private final int flowControlWindow;
711758
private final int maxMessageSize;
712759
private final int maxHeaderListSize;
760+
private final int softLimitHeaderListSize;
713761
private final long keepAliveTimeNanos;
714762
private final AtomicBackoff keepAliveBackoff;
715763
private final long keepAliveTimeoutNanos;
@@ -724,11 +772,20 @@ private static final class NettyTransportFactory implements ClientTransportFacto
724772
NettyTransportFactory(
725773
ProtocolNegotiator protocolNegotiator,
726774
ChannelFactory<? extends Channel> channelFactory,
727-
Map<ChannelOption<?>, ?> channelOptions, ObjectPool<? extends EventLoopGroup> groupPool,
728-
boolean autoFlowControl, int flowControlWindow, int maxMessageSize, int maxHeaderListSize,
729-
long keepAliveTimeNanos, long keepAliveTimeoutNanos, boolean keepAliveWithoutCalls,
730-
TransportTracer.Factory transportTracerFactory, LocalSocketPicker localSocketPicker,
731-
boolean useGetForSafeMethods, Class<? extends SocketAddress> transportSocketType) {
775+
Map<ChannelOption<?>, ?> channelOptions,
776+
ObjectPool<? extends EventLoopGroup> groupPool,
777+
boolean autoFlowControl,
778+
int flowControlWindow,
779+
int maxMessageSize,
780+
int maxHeaderListSize,
781+
int softLimitHeaderListSize,
782+
long keepAliveTimeNanos,
783+
long keepAliveTimeoutNanos,
784+
boolean keepAliveWithoutCalls,
785+
TransportTracer.Factory transportTracerFactory,
786+
LocalSocketPicker localSocketPicker,
787+
boolean useGetForSafeMethods,
788+
Class<? extends SocketAddress> transportSocketType) {
732789
this.protocolNegotiator = checkNotNull(protocolNegotiator, "protocolNegotiator");
733790
this.channelFactory = channelFactory;
734791
this.channelOptions = new HashMap<ChannelOption<?>, Object>(channelOptions);
@@ -738,6 +795,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
738795
this.flowControlWindow = flowControlWindow;
739796
this.maxMessageSize = maxMessageSize;
740797
this.maxHeaderListSize = maxHeaderListSize;
798+
this.softLimitHeaderListSize = softLimitHeaderListSize;
741799
this.keepAliveTimeNanos = keepAliveTimeNanos;
742800
this.keepAliveBackoff = new AtomicBackoff("keepalive time nanos", keepAliveTimeNanos);
743801
this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
@@ -774,13 +832,30 @@ public void run() {
774832
};
775833

776834
// TODO(carl-mastrangelo): Pass channelLogger in.
777-
NettyClientTransport transport = new NettyClientTransport(
778-
serverAddress, channelFactory, channelOptions, group,
779-
localNegotiator, autoFlowControl, flowControlWindow,
780-
maxMessageSize, maxHeaderListSize, keepAliveTimeNanosState.get(), keepAliveTimeoutNanos,
781-
keepAliveWithoutCalls, options.getAuthority(), options.getUserAgent(),
782-
tooManyPingsRunnable, transportTracerFactory.create(), options.getEagAttributes(),
783-
localSocketPicker, channelLogger, useGetForSafeMethods, Ticker.systemTicker());
835+
NettyClientTransport transport =
836+
new NettyClientTransport(
837+
serverAddress,
838+
channelFactory,
839+
channelOptions,
840+
group,
841+
localNegotiator,
842+
autoFlowControl,
843+
flowControlWindow,
844+
maxMessageSize,
845+
maxHeaderListSize,
846+
softLimitHeaderListSize,
847+
keepAliveTimeNanosState.get(),
848+
keepAliveTimeoutNanos,
849+
keepAliveWithoutCalls,
850+
options.getAuthority(),
851+
options.getUserAgent(),
852+
tooManyPingsRunnable,
853+
transportTracerFactory.create(),
854+
options.getEagAttributes(),
855+
localSocketPicker,
856+
channelLogger,
857+
useGetForSafeMethods,
858+
Ticker.systemTicker());
784859
return transport;
785860
}
786861

@@ -796,11 +871,24 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch
796871
if (result.error != null) {
797872
return null;
798873
}
799-
ClientTransportFactory factory = new NettyTransportFactory(
800-
result.negotiator.newNegotiator(), channelFactory, channelOptions, groupPool,
801-
autoFlowControl, flowControlWindow, maxMessageSize, maxHeaderListSize, keepAliveTimeNanos,
802-
keepAliveTimeoutNanos, keepAliveWithoutCalls, transportTracerFactory, localSocketPicker,
803-
useGetForSafeMethods, transportSocketType);
874+
ClientTransportFactory factory =
875+
new NettyTransportFactory(
876+
result.negotiator.newNegotiator(),
877+
channelFactory,
878+
channelOptions,
879+
groupPool,
880+
autoFlowControl,
881+
flowControlWindow,
882+
maxMessageSize,
883+
maxHeaderListSize,
884+
softLimitHeaderListSize,
885+
keepAliveTimeNanos,
886+
keepAliveTimeoutNanos,
887+
keepAliveWithoutCalls,
888+
transportTracerFactory,
889+
localSocketPicker,
890+
useGetForSafeMethods,
891+
transportSocketType);
804892
return new SwapChannelCredentialsResult(factory, result.callCredentials);
805893
}
806894

netty/src/main/java/io/grpc/netty/NettyClientHandler.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ static NettyClientHandler newHandler(
142142
boolean autoFlowControl,
143143
int flowControlWindow,
144144
int maxHeaderListSize,
145+
int softLimitHeaderListSize,
145146
Supplier<Stopwatch> stopwatchFactory,
146147
Runnable tooManyPingsRunnable,
147148
TransportTracer transportTracer,
@@ -171,6 +172,7 @@ static NettyClientHandler newHandler(
171172
autoFlowControl,
172173
flowControlWindow,
173174
maxHeaderListSize,
175+
softLimitHeaderListSize,
174176
stopwatchFactory,
175177
tooManyPingsRunnable,
176178
transportTracer,
@@ -190,6 +192,7 @@ static NettyClientHandler newHandler(
190192
boolean autoFlowControl,
191193
int flowControlWindow,
192194
int maxHeaderListSize,
195+
int softLimitHeaderListSize,
193196
Supplier<Stopwatch> stopwatchFactory,
194197
Runnable tooManyPingsRunnable,
195198
TransportTracer transportTracer,
@@ -202,6 +205,8 @@ static NettyClientHandler newHandler(
202205
Preconditions.checkNotNull(lifecycleManager, "lifecycleManager");
203206
Preconditions.checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
204207
Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive");
208+
Preconditions.checkArgument(softLimitHeaderListSize > 0,
209+
"softLimitHeaderListSize must be positive");
205210
Preconditions.checkNotNull(stopwatchFactory, "stopwatchFactory");
206211
Preconditions.checkNotNull(tooManyPingsRunnable, "tooManyPingsRunnable");
207212
Preconditions.checkNotNull(eagAttributes, "eagAttributes");
@@ -247,7 +252,9 @@ static NettyClientHandler newHandler(
247252
authority,
248253
autoFlowControl,
249254
pingCounter,
250-
ticker);
255+
ticker,
256+
maxHeaderListSize,
257+
softLimitHeaderListSize);
251258
}
252259

253260
private NettyClientHandler(
@@ -264,9 +271,20 @@ private NettyClientHandler(
264271
String authority,
265272
boolean autoFlowControl,
266273
PingLimiter pingLimiter,
267-
Ticker ticker) {
268-
super(/* channelUnused= */ null, decoder, encoder, settings,
269-
negotiationLogger, autoFlowControl, pingLimiter, ticker);
274+
Ticker ticker,
275+
int maxHeaderListSize,
276+
int softLimitHeaderListSize) {
277+
super(
278+
/* channelUnused= */ null,
279+
decoder,
280+
encoder,
281+
settings,
282+
negotiationLogger,
283+
autoFlowControl,
284+
pingLimiter,
285+
ticker,
286+
maxHeaderListSize,
287+
softLimitHeaderListSize);
270288
this.lifecycleManager = lifecycleManager;
271289
this.keepAliveManager = keepAliveManager;
272290
this.stopwatchFactory = stopwatchFactory;
@@ -380,6 +398,28 @@ private void onHeadersRead(int streamId, Http2Headers headers, boolean endStream
380398
if (streamId != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) {
381399
NettyClientStream.TransportState stream = clientStream(requireHttp2Stream(streamId));
382400
PerfMark.event("NettyClientHandler.onHeadersRead", stream.tag());
401+
// check metadata size vs soft limit
402+
int h2HeadersSize = Utils.getH2HeadersSize(headers);
403+
boolean shouldFail =
404+
Utils.shouldRejectOnMetadataSizeSoftLimitExceeded(
405+
h2HeadersSize, softLimitHeaderListSize, maxHeaderListSize);
406+
if (shouldFail && endStream) {
407+
stream.transportReportStatus(Status.RESOURCE_EXHAUSTED
408+
.withDescription(
409+
String.format(
410+
"Server Status + Trailers of size %d exceeded Metadata size soft limit: %d",
411+
h2HeadersSize,
412+
softLimitHeaderListSize)), true, new Metadata());
413+
return;
414+
} else if (shouldFail) {
415+
stream.transportReportStatus(Status.RESOURCE_EXHAUSTED
416+
.withDescription(
417+
String.format(
418+
"Server Headers of size %d exceeded Metadata size soft limit: %d",
419+
h2HeadersSize,
420+
softLimitHeaderListSize)), true, new Metadata());
421+
return;
422+
}
383423
stream.transportHeadersReceived(headers, endStream);
384424
}
385425

netty/src/main/java/io/grpc/netty/NettyClientTransport.java

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class NettyClientTransport implements ConnectionClientTransport {
8383
private final int flowControlWindow;
8484
private final int maxMessageSize;
8585
private final int maxHeaderListSize;
86+
private final int softLimitHeaderListSize;
8687
private KeepAliveManager keepAliveManager;
8788
private final long keepAliveTimeNanos;
8889
private final long keepAliveTimeoutNanos;
@@ -106,15 +107,28 @@ class NettyClientTransport implements ConnectionClientTransport {
106107
private final Ticker ticker;
107108

108109
NettyClientTransport(
109-
SocketAddress address, ChannelFactory<? extends Channel> channelFactory,
110-
Map<ChannelOption<?>, ?> channelOptions, EventLoopGroup group,
111-
ProtocolNegotiator negotiator, boolean autoFlowControl, int flowControlWindow,
112-
int maxMessageSize, int maxHeaderListSize,
113-
long keepAliveTimeNanos, long keepAliveTimeoutNanos,
114-
boolean keepAliveWithoutCalls, String authority, @Nullable String userAgent,
115-
Runnable tooManyPingsRunnable, TransportTracer transportTracer, Attributes eagAttributes,
116-
LocalSocketPicker localSocketPicker, ChannelLogger channelLogger,
117-
boolean useGetForSafeMethods, Ticker ticker) {
110+
SocketAddress address,
111+
ChannelFactory<? extends Channel> channelFactory,
112+
Map<ChannelOption<?>, ?> channelOptions,
113+
EventLoopGroup group,
114+
ProtocolNegotiator negotiator,
115+
boolean autoFlowControl,
116+
int flowControlWindow,
117+
int maxMessageSize,
118+
int maxHeaderListSize,
119+
int softLimitHeaderListSize,
120+
long keepAliveTimeNanos,
121+
long keepAliveTimeoutNanos,
122+
boolean keepAliveWithoutCalls,
123+
String authority,
124+
@Nullable String userAgent,
125+
Runnable tooManyPingsRunnable,
126+
TransportTracer transportTracer,
127+
Attributes eagAttributes,
128+
LocalSocketPicker localSocketPicker,
129+
ChannelLogger channelLogger,
130+
boolean useGetForSafeMethods,
131+
Ticker ticker) {
118132

119133
this.negotiator = Preconditions.checkNotNull(negotiator, "negotiator");
120134
this.negotiationScheme = this.negotiator.scheme();
@@ -126,6 +140,7 @@ class NettyClientTransport implements ConnectionClientTransport {
126140
this.flowControlWindow = flowControlWindow;
127141
this.maxMessageSize = maxMessageSize;
128142
this.maxHeaderListSize = maxHeaderListSize;
143+
this.softLimitHeaderListSize = softLimitHeaderListSize;
129144
this.keepAliveTimeNanos = keepAliveTimeNanos;
130145
this.keepAliveTimeoutNanos = keepAliveTimeoutNanos;
131146
this.keepAliveWithoutCalls = keepAliveWithoutCalls;
@@ -220,18 +235,19 @@ public Runnable start(Listener transportListener) {
220235
}
221236

222237
handler = NettyClientHandler.newHandler(
223-
lifecycleManager,
224-
keepAliveManager,
225-
autoFlowControl,
226-
flowControlWindow,
227-
maxHeaderListSize,
228-
GrpcUtil.STOPWATCH_SUPPLIER,
229-
tooManyPingsRunnable,
230-
transportTracer,
231-
eagAttributes,
232-
authorityString,
233-
channelLogger,
234-
ticker);
238+
lifecycleManager,
239+
keepAliveManager,
240+
autoFlowControl,
241+
flowControlWindow,
242+
maxHeaderListSize,
243+
softLimitHeaderListSize,
244+
GrpcUtil.STOPWATCH_SUPPLIER,
245+
tooManyPingsRunnable,
246+
transportTracer,
247+
eagAttributes,
248+
authorityString,
249+
channelLogger,
250+
ticker);
235251

236252
ChannelHandler negotiationHandler = negotiator.newHandler(handler);
237253

0 commit comments

Comments
 (0)