Skip to content

Commit 9d66819

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 9d04c60 commit 9d66819

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
@@ -255,11 +255,15 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
255255
informational_headers_ = std::move(informational_headers);
256256
}
257257
void setResponseHeaders(Http::ResponseHeaderMapPtr&& response_headers) override {
258-
// We'll overwrite the headers in the case where we fail the stream after upstream headers
259-
// have begun filter processing but before they have been sent downstream.
258+
if (response_headers_ != nullptr) {
259+
overwritten_headers_.emplace_back(std::move(response_headers_));
260+
}
260261
response_headers_ = std::move(response_headers);
261262
}
262263
void setResponseTrailers(Http::ResponseTrailerMapPtr&& response_trailers) override {
264+
if (response_trailers_ != nullptr) {
265+
overwritten_headers_.emplace_back(std::move(response_trailers_));
266+
}
263267
response_trailers_ = std::move(response_trailers);
264268
}
265269
void chargeStats(const ResponseHeaderMap& headers) override;
@@ -452,6 +456,13 @@ class ConnectionManagerImpl : Logger::Loggable<Logger::Id::http>,
452456
ResponseHeaderMapSharedPtr response_headers_;
453457
ResponseTrailerMapSharedPtr response_trailers_;
454458

459+
// Keep track all the historical headers to avoid potential lifetime issues.
460+
// For example,
461+
// when Envoy processing a response, if we send a local reply, then the local reply
462+
// headers will overwrite the original response and result in the previous response
463+
// being dangling. To avoid this, we store the original headers.
464+
std::vector<std::shared_ptr<HeaderMap>> overwritten_headers_;
465+
455466
// Note: The FM must outlive the above headers, as they are possibly accessed during filter
456467
// destruction.
457468
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
@@ -1800,6 +1800,8 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18001800
// Start the response without processing the request headers through all
18011801
// filters.
18021802
ResponseHeaderMapPtr response_headers{new TestResponseHeaderMapImpl{{":status", "200"}}};
1803+
ResponseHeaderMap* original_response_headers = response_headers.get();
1804+
18031805
EXPECT_CALL(*encoder_filters_[1], encodeHeaders(_, false))
18041806
.WillOnce(Return(FilterHeadersStatus::StopIteration));
18051807
decoder_filters_[0]->callbacks_->streamInfo().setResponseCodeDetails("");
@@ -1817,6 +1819,8 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18171819
// The 500 goes directly to the encoder.
18181820
EXPECT_CALL(response_encoder_, encodeHeaders(_, false))
18191821
.WillOnce(Invoke([&](const ResponseHeaderMap& headers, bool) -> FilterHeadersStatus {
1822+
// The new headers should overwrite the original headers.
1823+
EXPECT_NE(&headers, original_response_headers);
18201824
// Make sure this is a 500
18211825
EXPECT_EQ("500", headers.getStatusValue());
18221826
// Make sure Envoy standard sanitization has been applied.
@@ -1829,6 +1833,10 @@ TEST_F(HttpConnectionManagerImplTest, HitResponseBufferLimitsBeforeHeaders) {
18291833
decoder_filters_[0]->callbacks_->encodeData(fake_response, false);
18301834
EXPECT_EQ("Internal Server Error", response_body);
18311835

1836+
// The active stream will keep the overwritten headers alive to avoid potential lifetime issues.
1837+
// Ensure the original headers are still valid.
1838+
EXPECT_EQ(original_response_headers->getStatusValue(), "200");
1839+
18321840
EXPECT_EQ(1U, stats_.named_.rs_too_large_.value());
18331841
}
18341842

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

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

249+
IntegrationStreamDecoderPtr initializeAndSendRequest(const std::string& code) {
250+
initializeFilter(code);
251+
codec_client_ = makeHttpConnection(makeClientConnection(lookupPort("http")));
252+
Http::TestRequestHeaderMapImpl request_headers{{":method", "POST"},
253+
{":path", "/test/long/url"},
254+
{":scheme", "http"},
255+
{":authority", "foo.lyft.com"},
256+
{"x-forwarded-for", "10.0.0.1"}};
257+
258+
auto encoder_decoder = codec_client_->startRequest(request_headers);
259+
Buffer::OwnedImpl request_data("done");
260+
encoder_decoder.first.encodeData(request_data, true);
261+
waitForNextUpstreamRequest();
262+
263+
return std::move(encoder_decoder.second);
264+
}
265+
249266
void cleanup() {
250267
codec_client_->close();
251268
if (fake_lua_connection_ != nullptr) {
@@ -1272,6 +1289,41 @@ name: lua
12721289
testRewriteResponse(FILTER_AND_CODE);
12731290
}
12741291

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

0 commit comments

Comments
 (0)