HTTP/2 METADATA Queue Grows Without Limit
Affected commit: c33d01624d
Sink: source/common/http/filter_manager.cc:1418
Summary
When HTTP/2 METADATA is enabled, Envoy queues each upstream metadata block until response headers are processed. An upstream peer can withhold headers and continuously send metadata, growing worker memory unboundedly until the stream closes or the worker is exhausted.
Detail
At filter_manager.cc:1418-1420, when a filter has not yet processed response headers (!processed_headers_) or has stopped iteration (stoppedAll()), metadata is saved into a per-filter vector:
if (!(*entry)->processed_headers_ || (*entry)->stoppedAll()) {
(*entry)->getSavedResponseMetadata()->emplace_back(std::move(metadata_map_ptr));
return;
}
getSavedResponseMetadata() returns a MetadataMapVector, which is defined in metadata_interface.h:47-49 as:
using VectorMetadataMapPtr = std::vector<MetadataMapPtr>;
class MetadataMapVector : public VectorMetadataMapPtr { ... };
This vector has no aggregate size limit. An upstream peer can withhold response headers (keeping processed_headers_ false) while continuously sending METADATA frames. Each frame appends to the vector, growing worker memory until the stream closes or the worker is exhausted.
Reproduce
#!/bin/bash
set -e
git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-metadata
cd /tmp/envoy-metadata
echo "HEAD: $(git rev-parse HEAD)"
# Verify unbounded vector type
echo "=== MetadataMapVector is an unbounded std::vector ==="
grep -n 'VectorMetadataMapPtr\|class MetadataMapVector' envoy/http/metadata_interface.h
# Verify metadata saved without size check
echo ""
echo "=== Metadata queued without limit (filter_manager.cc:1414-1421) ==="
sed -n '1414,1421p' source/common/http/filter_manager.cc
# Verify saved_response_metadata_ has no size limit
echo ""
echo "=== Per-filter metadata storage (no limit) ==="
grep -n 'saved_response_metadata_\|getSavedResponseMetadata' source/common/http/filter_manager.h
rm -rf /tmp/envoy-metadata
Expected output (line numbers may shift):
HEAD: <resolved-HEAD>
=== MetadataMapVector is an unbounded std::vector ===
47:using VectorMetadataMapPtr = std::vector<MetadataMapPtr>;
49:class MetadataMapVector : public VectorMetadataMapPtr {
=== Metadata queued without limit (filter_manager.cc:1414-1421) ===
// If the filter pointed by entry has stopped for all frame type, stores metadata and returns.
// If the filter pointed by entry hasn't returned from encodeHeaders, stores newly added
// metadata in case encodeHeaders returns StopAllIteration. ...
if (!(*entry)->processed_headers_ || (*entry)->stoppedAll()) {
(*entry)->getSavedResponseMetadata()->emplace_back(std::move(metadata_map_ptr));
return;
}
=== Per-filter metadata storage (no limit) ===
196: MetadataMapVector* getSavedResponseMetadata() {
...
216: std::unique_ptr<MetadataMapVector> saved_response_metadata_{nullptr};
With allow_metadata: true, a malicious upstream sends HTTP/2 METADATA frames before response headers. Each frame is queued in an unbounded std::vector per filter entry. The upstream withholds headers indefinitely while sending metadata, growing worker memory without any cap.
Credit
Zheng Yu @ Depthfirst