Skip to content

Commit cf64276

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 f8c98c5 commit cf64276

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;
@@ -449,6 +453,13 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
449453
ResponseHeaderMapSharedPtr response_headers_;
450454
ResponseTrailerMapSharedPtr response_trailers_;
451455

456+
// Keep track all the historical headers to avoid potential lifetime issues.
457+
// For example,
458+
// when Envoy processing a response, if we send a local reply, then the local reply
459+
// headers will overwrite the original response and result in the previous response
460+
// being dangling. To avoid this, we store the original headers.
461+
std::vector<std::shared_ptr<HeaderMap>> overwritten_headers_;
462+
452463
// Note: The FM must outlive the above headers, as they are possibly accessed during filter
453464
// destruction.
454465
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
@@ -250,6 +250,23 @@ class LuaIntegrationTest : public UpstreamDownstreamIntegrationTest {
250250
expectResponseBodyRewrite(code, true, enable_wrap_body);
251251
}
252252

253+
IntegrationStreamDecoderPtr initializeAndSendRequest(const std::string& code) {
254+
initializeFilter(code);
255+
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
256+
Http::TestRequestHeaderMapImpl request_headers{{":method", "POST"},
257+
{":path", "/test/long/url"},
258+
{":scheme", "http"},
259+
{":authority", "foo.lyft.com"},
260+
{"x-forwarded-for", "10.0.0.1"}};
261+
262+
auto encoder_decoder = codec_client_->startRequest(request_headers);
263+
Buffer::OwnedImpl request_data("done");
264+
encoder_decoder.first.encodeData(request_data, true);
265+
waitForNextUpstreamRequest();
266+
267+
return std::move(encoder_decoder.second);
268+
}
269+
253270
void cleanup() {
254271
codec_client_->close();
255272
if (fake_lua_connection_ != nullptr) {
@@ -1276,6 +1293,41 @@ name: lua
12761293
testRewriteResponse(FILTER_AND_CODE);
12771294
}
12781295

1296+
// Rewrite response buffer to a huge body.
1297+
TEST_P(LuaIntegrationTest, RewriteResponseToHugeBody) {
1298+
const std::string FILTER_AND_CODE =
1299+
R"EOF(
1300+
name: lua
1301+
typed_config:
1302+
"@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
1303+
default_source_code:
1304+
inline_string: |
1305+
function envoy_on_response(response_handle)
1306+
-- Default HTTP2 body buffer limit is 16MB for now. To set
1307+
-- a 16MB+ body to ensure both HTTP1 and HTTP2 will hit the limit.
1308+
local huge_body = string.rep("a", 1024 * 1024 * 16 + 1) -- 16MB + 1
1309+
local content_length = response_handle:body():setBytes(huge_body)
1310+
response_handle:logTrace(content_length)
1311+
end
1312+
)EOF";
1313+
1314+
auto response = initializeAndSendRequest(FILTER_AND_CODE);
1315+
1316+
Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}, {"foo", "bar"}};
1317+
upstream_request_->encodeHeaders(response_headers, false);
1318+
Buffer::OwnedImpl response_data1("good");
1319+
upstream_request_->encodeData(response_data1, false);
1320+
Buffer::OwnedImpl response_data2("bye");
1321+
upstream_request_->encodeData(response_data2, true);
1322+
1323+
ASSERT_TRUE(response->waitForEndStream());
1324+
ASSERT_TRUE(response->complete());
1325+
1326+
EXPECT_EQ(response->headers().getStatusValue(), "500");
1327+
1328+
cleanup();
1329+
}
1330+
12791331
// Rewrite response buffer, without original upstream response body
12801332
// and always wrap body.
12811333
TEST_P(LuaIntegrationTest, RewriteResponseBufferWithoutUpstreamBody) {

0 commit comments

Comments
 (0)