Skip to content

Commit 85fa39d

Browse files
wbpcodephlax
andcommitted
lua: fix a bug where setting a big response body in lua will result in crash (envoyproxy#41571)
Commit Message: lua: fix a bug where setting a big response body in lua will result in crash Additional Description: At the response phase, when the lua script rewrite the response body to a huge body, the wartermark callback will be triggered and result a direct local response. The direct local response headers will override the original response headers and result in all references of original response headers be dangling. But note, because the lua didn't aware the he wartermark callback and will still try to update the content-length of the original response headers, so, finally it result in a crash. Risk Level: low. Testing: unit, integration. Docs Changes: n/a. Release Notes: added. Platform Specific Features: n/a. --------- Signed-off-by: WangBaiping <[email protected]> Signed-off-by: code <[email protected]> Co-authored-by: phlax <[email protected]>
1 parent c094bdd commit 85fa39d

File tree

5 files changed

+79
-3
lines changed

5 files changed

+79
-3
lines changed

changelogs/current.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ minor_behavior_changes:
88

99
bug_fixes:
1010
# *Changes expected to improve the state of the world and are unlikely to have negative effects*
11+
- area: lua
12+
change: |
13+
Fix a bug where Lua filters may result in Envoy crashes when setting response body to a
14+
larger payload (greater than the body buffer limit).
1115
1216
removed_config_or_runtime:
1317
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`

source/common/http/conn_manager_impl.h

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,11 +250,15 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
250250
informational_headers_ = std::move(informational_headers);
251251
}
252252
void setResponseHeaders(Http::ResponseHeaderMapPtr&& response_headers) override {
253-
// We'll overwrite the headers in the case where we fail the stream after upstream headers
254-
// have begun filter processing but before they have been sent downstream.
253+
if (response_headers_ != nullptr) {
254+
overwritten_headers_.emplace_back(std::move(response_headers_));
255+
}
255256
response_headers_ = std::move(response_headers);
256257
}
257258
void setResponseTrailers(Http::ResponseTrailerMapPtr&& response_trailers) override {
259+
if (response_trailers_ != nullptr) {
260+
overwritten_headers_.emplace_back(std::move(response_trailers_));
261+
}
258262
response_trailers_ = std::move(response_trailers);
259263
}
260264
void chargeStats(const ResponseHeaderMap& headers) override;
@@ -448,6 +452,13 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
448452
ResponseHeaderMapSharedPtr response_headers_;
449453
ResponseTrailerMapSharedPtr response_trailers_;
450454

455+
// Keep track all the historical headers to avoid potential lifetime issues.
456+
// For example,
457+
// when Envoy processing a response, if we send a local reply, then the local reply
458+
// headers will overwrite the original response and result in the previous response
459+
// being dangling. To avoid this, we store the original headers.
460+
std::vector<std::shared_ptr<HeaderMap>> overwritten_headers_;
461+
451462
// Note: The FM must outlive the above headers, as they are possibly accessed during filter
452463
// destruction.
453464
DownstreamFilterManager filter_manager_;

source/extensions/filters/common/lua/wrappers.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ int BufferWrapper::luaGetBytes(lua_State* state) {
6666

6767
int BufferWrapper::luaSetBytes(lua_State* state) {
6868
data_.drain(data_.length());
69+
ASSERT(data_.length() == 0); // Defensive check.
6970
absl::string_view bytes = getStringViewFromLuaString(state, 2);
71+
headers_.setContentLength(bytes.size());
7072
data_.add(bytes);
71-
headers_.setContentLength(data_.length());
7273
lua_pushnumber(state, data_.length());
7374
return 1;
7475
}

test/common/http/conn_manager_impl_test_2.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,8 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18201820
// Start the response without processing the request headers through all
18211821
// filters.
18221822
ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
1823+
ResponseHeaderMap* original_response_headers = response_headers.get();
1824+
18231825
EXPECT_CALL(*encoder_filters_[1], encodeHeaders(_, false))
18241826
.WillOnce(Return(FilterHeadersStatus::StopIteration));
18251827
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
@@ -1837,6 +1839,8 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18371839
// The 500 goes directly to the encoder.
18381840
EXPECT_CALL(response_encoder_, encodeHeaders(_, false))
18391841
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> FilterHeadersStatus {
1842+
// The new headers should overwrite the original headers.
1843+
EXPECT_NE(&headers, original_response_headers);
18401844
// Make sure this is a 500
18411845
EXPECT_EQ("500", headers.getStatusValue());
18421846
// Make sure Envoy standard sanitization has been applied.
@@ -1849,6 +1853,10 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18491853
decoder_filters_[0]->callbacks_->encodeData(fake_response, false);
18501854
EXPECT_EQ("Internal Server Error", response_body);
18511855

1856+
// The active stream will keep the overwritten headers alive to avoid potential lifetime issues.
1857+
// Ensure the original headers are still valid.
1858+
EXPECT_EQ(original_response_headers->getStatusValue(), "200");
1859+
18521860
EXPECT_EQ(1U, stats_.named_.rs_too_large_.value());
18531861
}
18541862

test/extensions/filters/http/lua/lua_integration_test.cc

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,23 @@ class LuaIntegrationTest : public UpstreamDownstreamIntegrationTest {
263263
expectResponseBodyRewrite(code, true, enable_wrap_body);
264264
}
265265

266+
IntegrationStreamDecoderPtr initializeAndSendRequest(const std::string& code) {
267+
initializeFilter(code);
268+
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
269+
Http::TestRequestHeaderMapImpl request_headers{{":method", "POST"},
270+
{":path", "/test/long/url"},
271+
{":scheme", "http"},
272+
{":authority", "foo.lyft.com"},
273+
{"x-forwarded-for", "10.0.0.1"}};
274+
275+
auto encoder_decoder = codec_client_->startRequest(request_headers);
276+
Buffer::OwnedImpl request_data("done");
277+
encoder_decoder.first.encodeData(request_data, true);
278+
waitForNextUpstreamRequest();
279+
280+
return std::move(encoder_decoder.second);
281+
}
282+
266283
void cleanup() {
267284
codec_client_->close();
268285
if (fake_lua_connection_ != nullptr) {
@@ -1411,6 +1428,41 @@ name: lua
14111428
testRewriteResponse(FILTER_AND_CODE);
14121429
}
14131430

1431+
// Rewrite response buffer to a huge body.
1432+
TEST_P(LuaIntegrationTest, RewriteResponseToHugeBody) {
1433+
const std::string FILTER_AND_CODE =
1434+
R"EOF(
1435+
name: lua
1436+
typed_config:
1437+
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
1438+
default_source_code:
1439+
inline_string: |
1440+
function envoy_on_response(response_handle)
1441+
-- Default HTTP2 body buffer limit is 16MB for now. To set
1442+
-- a 16MB+ body to ensure both HTTP1 and HTTP2 will hit the limit.
1443+
local huge_body = string.rep("a", 1024 * 1024 * 16 + 1) -- 16MB + 1
1444+
local content_length = response_handle:body():setBytes(huge_body)
1445+
response_handle:logTrace(content_length)
1446+
end
1447+
)EOF";
1448+
1449+
auto response = initializeAndSendRequest(FILTER_AND_CODE);
1450+
1451+
Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, {"foo", "bar"}};
1452+
upstream_request_->encodeHeaders(response_headers, false);
1453+
Buffer::OwnedImpl response_data1("good");
1454+
upstream_request_->encodeData(response_data1, false);
1455+
Buffer::OwnedImpl response_data2("bye");
1456+
upstream_request_->encodeData(response_data2, true);
1457+
1458+
ASSERT_TRUE(response->waitForEndStream());
1459+
ASSERT_TRUE(response->complete());
1460+
1461+
EXPECT_EQ(response->headers().getStatusValue(), "500");
1462+
1463+
cleanup();
1464+
}
1465+
14141466
// Rewrite response buffer, without original upstream response body
14151467
// and always wrap body.
14161468
TEST_P(LuaIntegrationTest, RewriteResponseBufferWithoutUpstreamBody) {

0 commit comments

Comments
 (0)