Skip to content

Commit 1c1a86b

Browse files
committed
Prevent stuck connections after local reply in proxy-wasm
This change fixes envoyproxy#28826. Some additional discussions for context can be found in proxy-wasm/proxy-wasm-cpp-host#423. The issue reported in envoyproxy#28826 happens when proxy-wasm plugin calls proxy_send_local_response during the HTTP request proessing and HTTP response processing. This happens because in attempt to mitigate a use-after-free issue (see envoyproxy#23049) we added logic to proxy-wasm that avoids calling sendLocalReply multiple times. So now when proxy-wasm plugin calls proxy_send_local_response only the first call will result in sendLocalReply, while all subsequent calls will get ignored. At the same time, when proxy-wasm plugins call proxy_send_local_response, because it's used to report an error in the plugin, proxy-wasm also stops iteration. During HTTP request processing this leads to the following chain of events: 1. During request proxy-wasm plugin calls proxy_send_local_response 2. proxy_send_local_response calls sendLocalReply, which schedules the local reply to be processed later through the filter chain 3. Request processing filter chain gets aborted and Envoy sends the previous created local reply though the filter chain 4. Proxy-wasm plugin gets called to process the response it generated and it calls proxy_send_local_response 5. proxy_send_local_response **does not** call sendLocalReply, because proxy-wasm prevents multiple calls to sendLocalReply currently 6. proxy-wasm stops iteration So in the end the filter chain iteration is stopped for the response and because proxy_send_local_respose does not actually call sendLocalReply we don't send another locally generated response either. I think we can do slightly better and close the connection in this case. This change includes the following parts: 1. Partial rollback of envoyproxy#23049 2. Tests covering this case and some other using the actual FilterManager. The most important question is why rolling back envoyproxy#23049 now is safe? The reason why it's safe, is that since introduction of prepareLocalReplyViaFilterChain in envoyproxy#24367, calling sendLocalReply multiple times is safe - that PR basically address the issue in a generic way for all the plugins, so a proxy-wasm specific fix is not needed anymore. On top of being safe, there are additional benefits to making this change: 1. We don't end up with a stuck connection in case of errors, which is slightly better 2. We remove a failure mode from proxy_send_local_response that was introduced in envoyproxy#23049 - which is good, because proxy-wasm plugins don't have a good fallback when proxy_send_local_response is failing. Finally, why replace the current mocks with a real FilterManager? Mock implementation of sendLocalReply works fine for tests that just need to assert that sendLocalReply gets called. However, in this case we rely on the fact that it's safe to call sendLocalReply multiple times and it will do the right thing and we want to assert that the connection will get closed in the end - that cannot be tested by just checking that the sendLocalReply gets called or by relying on a simplistic mock implementation of sendLocalReply. Signed-off-by: Mikhail Krinkin <krinkin.m.u@gmail.com>
1 parent 742a3b0 commit 1c1a86b

6 files changed

Lines changed: 181 additions & 53 deletions

File tree

source/extensions/common/wasm/context.cc

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,12 +1595,6 @@ void Context::failStream(WasmStreamType stream_type) {
15951595
WasmResult Context::sendLocalResponse(uint32_t response_code, std::string_view body_text,
15961596
Pairs additional_headers, uint32_t grpc_status,
15971597
std::string_view details) {
1598-
// This flag is used to avoid calling sendLocalReply() twice, even if wasm code has this
1599-
// logic. We can't reuse "local_reply_sent_" here because it can't avoid calling nested
1600-
// sendLocalReply() during encodeHeaders().
1601-
if (local_reply_hold_) {
1602-
return WasmResult::BadArgument;
1603-
}
16041598
// "additional_headers" is a collection of string_views. These will no longer
16051599
// be valid when "modify_headers" is finally called below, so we must
16061600
// make copies of all the headers.
@@ -1625,11 +1619,6 @@ WasmResult Context::sendLocalResponse(uint32_t response_code, std::string_view b
16251619
modify_headers = std::move(modify_headers), grpc_status,
16261620
details = StringUtil::replaceAllEmptySpace(
16271621
absl::string_view(details.data(), details.size()))] {
1628-
// When the wasm vm fails, failStream() is called if the plugin is fail-closed, we need
1629-
// this flag to avoid calling sendLocalReply() twice.
1630-
if (local_reply_sent_) {
1631-
return;
1632-
}
16331622
// C++, Rust and other SDKs use -1 (InvalidCode) as the default value if gRPC code is not set,
16341623
// which should be mapped to nullopt in Envoy to prevent it from sending a grpc-status trailer
16351624
// at all.
@@ -1640,10 +1629,8 @@ WasmResult Context::sendLocalResponse(uint32_t response_code, std::string_view b
16401629
}
16411630
decoder_callbacks_->sendLocalReply(static_cast<Envoy::Http::Code>(response_code), body_text,
16421631
modify_headers, grpc_status_code, details);
1643-
local_reply_sent_ = true;
16441632
});
16451633
}
1646-
local_reply_hold_ = true;
16471634
return WasmResult::Ok;
16481635
}
16491636

source/extensions/common/wasm/context.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,6 @@ class Context : public proxy_wasm::ContextBase,
431431
bool buffering_response_body_ = false;
432432
bool end_of_stream_ = false;
433433
bool local_reply_sent_ = false;
434-
bool local_reply_hold_ = false;
435434
ProtobufWkt::Struct temporary_metadata_;
436435

437436
// MB: must be a node-type map as we take persistent references to the entries.

test/extensions/common/wasm/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ envoy_cc_test(
5555
"//test/extensions/common/wasm/test_data:test_context_cpp_plugin",
5656
"//test/extensions/common/wasm/test_data:test_cpp_plugin",
5757
"//test/extensions/common/wasm/test_data:test_restriction_cpp_plugin",
58+
"//test/mocks/local_reply:local_reply_mocks",
5859
"//test/mocks/server:server_mocks",
5960
"//test/test_common:environment_lib",
6061
"//test/test_common:registry_lib",

test/extensions/common/wasm/test_data/test_context_cpp.cc

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,22 +94,59 @@ FilterDataStatus DupReplyContext::onRequestBody(size_t, bool) {
9494
return FilterDataStatus::Continue;
9595
}
9696

97-
class PanicReplyContext : public Context {
97+
class LocalReplyInRequestAndResponseContext : public Context {
9898
public:
99-
explicit PanicReplyContext(uint32_t id, RootContext* root) : Context(id, root) {}
99+
explicit LocalReplyInRequestAndResponseContext(uint32_t id, RootContext* root) : Context(id, root) {}
100+
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
101+
FilterHeadersStatus onResponseHeaders(uint32_t, bool) override;
102+
private:
103+
EnvoyRootContext* root() { return static_cast<EnvoyRootContext*>(Context::root()); }
104+
};
105+
106+
FilterHeadersStatus LocalReplyInRequestAndResponseContext::onRequestHeaders(uint32_t, bool) {
107+
sendLocalResponse(200, "ok", "body", {});
108+
return FilterHeadersStatus::Continue;
109+
}
110+
111+
FilterHeadersStatus LocalReplyInRequestAndResponseContext::onResponseHeaders(uint32_t, bool) {
112+
sendLocalResponse(200, "ok", "body", {});
113+
return FilterHeadersStatus::Continue;
114+
}
115+
116+
class PanicInRequestContext : public Context {
117+
public:
118+
explicit PanicInRequestContext(uint32_t id, RootContext* root) : Context(id, root) {}
100119
FilterDataStatus onRequestBody(size_t body_buffer_length, bool end_of_stream) override;
101120

102121
private:
103122
EnvoyRootContext* root() { return static_cast<EnvoyRootContext*>(Context::root()); }
104123
};
105124

106-
FilterDataStatus PanicReplyContext::onRequestBody(size_t, bool) {
107-
sendLocalResponse(200, "not send", "body", {});
108-
int* badptr = nullptr;
109-
*badptr = 0; // NOLINT(clang-analyzer-core.NullDereference)
125+
FilterDataStatus PanicInRequestContext::onRequestBody(size_t, bool) {
126+
abort();
110127
return FilterDataStatus::Continue;
111128
}
112129

130+
class PanicInResponseContext : public Context {
131+
public:
132+
explicit PanicInResponseContext(uint32_t id, RootContext* root) : Context(id, root) {}
133+
FilterHeadersStatus onResponseHeaders(uint32_t, bool) override;
134+
FilterHeadersStatus onRequestHeaders(uint32_t, bool) override;
135+
136+
private:
137+
EnvoyRootContext* root() { return static_cast<EnvoyRootContext*>(Context::root()); }
138+
};
139+
140+
FilterHeadersStatus PanicInResponseContext::onRequestHeaders(uint32_t, bool) {
141+
sendLocalResponse(200, "ok", "body", {});
142+
return FilterHeadersStatus::Continue;
143+
}
144+
145+
FilterHeadersStatus PanicInResponseContext::onResponseHeaders(uint32_t, bool) {
146+
abort();
147+
return FilterHeadersStatus::Continue;
148+
}
149+
113150
class InvalidGrpcStatusReplyContext : public Context {
114151
public:
115152
explicit InvalidGrpcStatusReplyContext(uint32_t id, RootContext* root) : Context(id, root) {}
@@ -127,9 +164,15 @@ FilterDataStatus InvalidGrpcStatusReplyContext::onRequestBody(size_t size, bool)
127164
static RegisterContextFactory register_DupReplyContext(CONTEXT_FACTORY(DupReplyContext),
128165
ROOT_FACTORY(EnvoyRootContext),
129166
"send local reply twice");
130-
static RegisterContextFactory register_PanicReplyContext(CONTEXT_FACTORY(PanicReplyContext),
167+
static RegisterContextFactory register_LocalReplyInRequestAndResponseContext(CONTEXT_FACTORY(LocalReplyInRequestAndResponseContext),
168+
ROOT_FACTORY(EnvoyRootContext),
169+
"local reply in request and response");
170+
static RegisterContextFactory register_PanicInRequestContext(CONTEXT_FACTORY(PanicInRequestContext),
171+
ROOT_FACTORY(EnvoyRootContext),
172+
"panic during request processing");
173+
static RegisterContextFactory register_PanicInResponseContext(CONTEXT_FACTORY(PanicInResponseContext),
131174
ROOT_FACTORY(EnvoyRootContext),
132-
"panic after sending local reply");
175+
"panic during response processing");
133176

134177
static RegisterContextFactory register_InvalidGrpcStatusReplyContext(CONTEXT_FACTORY(InvalidGrpcStatusReplyContext),
135178
ROOT_FACTORY(EnvoyRootContext),

test/extensions/common/wasm/wasm_test.cc

Lines changed: 125 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
#include "envoy/http/filter.h"
2+
#include "envoy/http/filter_factory.h"
13
#include "envoy/server/lifecycle_notifier.h"
24

35
#include "source/common/common/hex.h"
46
#include "source/common/event/dispatcher_impl.h"
7+
#include "source/common/http/filter_manager.h"
58
#include "source/common/stats/isolated_store_impl.h"
69
#include "source/extensions/common/wasm/wasm.h"
710

811
#include "test/extensions/common/wasm/wasm_runtime.h"
12+
#include "test/mocks/local_reply/mocks.h"
913
#include "test/mocks/server/mocks.h"
1014
#include "test/mocks/stats/mocks.h"
1115
#include "test/mocks/upstream/mocks.h"
@@ -1310,7 +1314,6 @@ class WasmCommonContextTest : public Common::Wasm::WasmHttpFilterTestBase<
13101314
return new TestContext(wasm, plugin);
13111315
});
13121316
}
1313-
13141317
void setupContext() { setupFilterBase<TestContext>(); }
13151318

13161319
TestContext& rootContext() { return *static_cast<TestContext*>(root_context_); }
@@ -1392,43 +1395,19 @@ TEST_P(WasmCommonContextTest, EmptyContext) {
13921395
root_context_->validateConfiguration("", plugin_);
13931396
}
13941397

1395-
// test that we don't send the local reply twice, even though it's specified in the wasm code
1396-
TEST_P(WasmCommonContextTest, DuplicateLocalReply) {
1397-
std::string code;
1398-
if (std::get<0>(GetParam()) != "null") {
1399-
code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat(
1400-
"{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm")));
1401-
} else {
1402-
// The name of the Null VM plugin.
1403-
code = "CommonWasmTestContextCpp";
1404-
}
1405-
EXPECT_FALSE(code.empty());
1406-
1407-
setup(code, "context", "send local reply twice");
1408-
setupContext();
1409-
EXPECT_CALL(decoder_callbacks_, encodeHeaders_(_, _))
1410-
.WillOnce([this](Http::ResponseHeaderMap&, bool) { context().onResponseHeaders(0, false); });
1411-
EXPECT_CALL(decoder_callbacks_,
1412-
sendLocalReply(Envoy::Http::Code::OK, testing::Eq("body"), _, _, testing::Eq("ok")));
1413-
1414-
// Create in-VM context.
1415-
context().onCreate();
1416-
EXPECT_EQ(proxy_wasm::FilterDataStatus::StopIterationNoBuffer, context().onRequestBody(0, false));
1417-
}
1418-
14191398
// test that we don't send the local reply twice when the wasm code panics
14201399
TEST_P(WasmCommonContextTest, LocalReplyWhenPanic) {
14211400
std::string code;
14221401
if (std::get<0>(GetParam()) != "null") {
14231402
code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat(
14241403
"{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm")));
14251404
} else {
1426-
// no need test the Null VM plugin.
1405+
// Let's not cause crashes in Null VM
14271406
return;
14281407
}
14291408
EXPECT_FALSE(code.empty());
14301409

1431-
setup(code, "context", "panic after sending local reply");
1410+
setup(code, "context", "panic during request processing");
14321411
setupContext();
14331412
// In the case of VM failure, failStream is called, so we need to make sure that we don't send the
14341413
// local reply twice.
@@ -1492,6 +1471,125 @@ TEST_P(WasmCommonContextTest, ProcessValidGRPCStatusCodeAsEmptyInLocalReply) {
14921471
EXPECT_EQ(proxy_wasm::FilterDataStatus::StopIterationNoBuffer, context().onRequestBody(1, false));
14931472
}
14941473

1474+
class WasmLocalReplyTest : public WasmCommonContextTest {
1475+
public:
1476+
WasmLocalReplyTest() = default;
1477+
1478+
void setup(const std::string& code, std::string vm_configuration, std::string root_id = "") {
1479+
WasmCommonContextTest::setup(code, vm_configuration, root_id);
1480+
filter_manager_ = std::make_unique<Http::DownstreamFilterManager>(
1481+
filter_manager_callbacks_, dispatcher_, connection_, 0, nullptr, true, 10000,
1482+
filter_factory_, local_reply_, protocol_, time_source_, filter_state_, overload_manager_);
1483+
request_headers_ = Http::RequestHeaderMapPtr{
1484+
new Http::TestRequestHeaderMapImpl{{":path", "/"}, {":method", "GET"}}};
1485+
request_data_ = Envoy::Buffer::OwnedImpl("body");
1486+
}
1487+
1488+
Http::StreamFilterSharedPtr filter() { return context_; }
1489+
1490+
Http::FilterFactoryCb createWasmFilter() {
1491+
return [this](Http::FilterChainFactoryCallbacks& callbacks) {
1492+
callbacks.addStreamFilter(filter());
1493+
};
1494+
}
1495+
1496+
void setupContext() {
1497+
WasmCommonContextTest::setupContext();
1498+
ON_CALL(filter_factory_, createFilterChain(_))
1499+
.WillByDefault(Invoke([this](Http::FilterChainManager& manager) -> bool {
1500+
auto factory = createWasmFilter();
1501+
manager.applyFilterFactoryCb({}, factory);
1502+
return true;
1503+
}));
1504+
ON_CALL(filter_manager_callbacks_, requestHeaders())
1505+
.WillByDefault(Return(makeOptRef(*request_headers_)));
1506+
filter_manager_->createFilterChain();
1507+
filter_manager_->requestHeadersInitialized();
1508+
}
1509+
1510+
std::unique_ptr<Http::FilterManager> filter_manager_;
1511+
NiceMock<Http::MockFilterManagerCallbacks> filter_manager_callbacks_;
1512+
NiceMock<Event::MockDispatcher> dispatcher_;
1513+
NiceMock<Network::MockConnection> connection_;
1514+
NiceMock<Envoy::Http::MockFilterChainFactory> filter_factory_;
1515+
NiceMock<LocalReply::MockLocalReply> local_reply_;
1516+
Http::Protocol protocol_{Http::Protocol::Http2};
1517+
NiceMock<MockTimeSystem> time_source_;
1518+
StreamInfo::FilterStateSharedPtr filter_state_ =
1519+
std::make_shared<StreamInfo::FilterStateImpl>(StreamInfo::FilterState::LifeSpan::Connection);
1520+
NiceMock<Server::MockOverloadManager> overload_manager_;
1521+
Http::RequestHeaderMapPtr request_headers_;
1522+
Envoy::Buffer::OwnedImpl request_data_;
1523+
};
1524+
1525+
INSTANTIATE_TEST_SUITE_P(Runtimes, WasmLocalReplyTest,
1526+
Envoy::Extensions::Common::Wasm::runtime_and_cpp_values);
1527+
1528+
TEST_P(WasmLocalReplyTest, DuplicateLocalReply) {
1529+
std::string code;
1530+
if (std::get<0>(GetParam()) != "null") {
1531+
code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat(
1532+
"{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm")));
1533+
} else {
1534+
// Skip the Null plugin
1535+
return;
1536+
}
1537+
EXPECT_FALSE(code.empty());
1538+
1539+
setup(code, "context", "send local reply twice");
1540+
setupContext();
1541+
1542+
// Even if sendLocalReply is called multiple times it should only generate a single
1543+
// response to the client, so encodeHeaders should only be called once
1544+
EXPECT_CALL(filter_manager_callbacks_, encodeHeaders(_, _));
1545+
EXPECT_CALL(filter_manager_callbacks_, endStream());
1546+
filter_manager_->decodeHeaders(*request_headers_, false);
1547+
filter_manager_->decodeData(request_data_, false);
1548+
filter_manager_->destroyFilters();
1549+
}
1550+
1551+
TEST_P(WasmLocalReplyTest, LocalReplyInRequestAndResponse) {
1552+
std::string code;
1553+
if (std::get<0>(GetParam()) != "null") {
1554+
code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat(
1555+
"{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm")));
1556+
} else {
1557+
code = "CommonWasmTestContextCpp";
1558+
}
1559+
EXPECT_FALSE(code.empty());
1560+
1561+
setup(code, "context", "local reply in request and response");
1562+
setupContext();
1563+
1564+
EXPECT_CALL(filter_manager_callbacks_, encodeHeaders(_, _));
1565+
EXPECT_CALL(filter_manager_callbacks_, endStream());
1566+
filter_manager_->decodeHeaders(*request_headers_, false);
1567+
filter_manager_->decodeData(request_data_, false);
1568+
filter_manager_->destroyFilters();
1569+
}
1570+
1571+
TEST_P(WasmLocalReplyTest, PanicDuringResponse) {
1572+
std::string code;
1573+
if (std::get<0>(GetParam()) != "null") {
1574+
code = TestEnvironment::readFileToStringForTest(TestEnvironment::substitute(absl::StrCat(
1575+
"{{ test_rundir }}/test/extensions/common/wasm/test_data/test_context_cpp.wasm")));
1576+
} else {
1577+
// Let's not cause crashes in Null VM
1578+
return;
1579+
}
1580+
EXPECT_FALSE(code.empty());
1581+
1582+
setup(code, "context", "panic during response processing");
1583+
setupContext();
1584+
1585+
EXPECT_CALL(filter_manager_callbacks_, encodeHeaders(_, _));
1586+
EXPECT_CALL(filter_manager_callbacks_, endStream());
1587+
1588+
filter_manager_->decodeHeaders(*request_headers_, false);
1589+
filter_manager_->decodeData(request_data_, false);
1590+
filter_manager_->destroyFilters();
1591+
}
1592+
14951593
} // namespace Wasm
14961594
} // namespace Common
14971595
} // namespace Extensions

test/test_common/wasm_base.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ template <typename Base = testing::Test> class WasmHttpFilterTestBase : public W
142142
auto wasm = WasmTestBase<Base>::wasm_ ? WasmTestBase<Base>::wasm_->wasm().get() : nullptr;
143143
int root_context_id = wasm ? wasm->getRootContext(WasmTestBase<Base>::plugin_, false)->id() : 0;
144144
context_ =
145-
std::make_unique<TestFilter>(wasm, root_context_id, WasmTestBase<Base>::plugin_handle_);
145+
std::make_shared<TestFilter>(wasm, root_context_id, WasmTestBase<Base>::plugin_handle_);
146146
context_->setDecoderFilterCallbacks(decoder_callbacks_);
147147
context_->setEncoderFilterCallbacks(encoder_callbacks_);
148148
}
149149

150-
std::unique_ptr<Context> context_;
150+
std::shared_ptr<Context> context_;
151151
NiceMock<Http::MockStreamDecoderFilterCallbacks> decoder_callbacks_;
152152
NiceMock<Http::MockStreamEncoderFilterCallbacks> encoder_callbacks_;
153153
NiceMock<Envoy::StreamInfo::MockStreamInfo> request_stream_info_;
@@ -160,12 +160,12 @@ class WasmNetworkFilterTestBase : public WasmTestBase<Base> {
160160
auto wasm = WasmTestBase<Base>::wasm_ ? WasmTestBase<Base>::wasm_->wasm().get() : nullptr;
161161
int root_context_id = wasm ? wasm->getRootContext(WasmTestBase<Base>::plugin_, false)->id() : 0;
162162
context_ =
163-
std::make_unique<TestFilter>(wasm, root_context_id, WasmTestBase<Base>::plugin_handle_);
163+
std::make_shared<TestFilter>(wasm, root_context_id, WasmTestBase<Base>::plugin_handle_);
164164
context_->initializeReadFilterCallbacks(read_filter_callbacks_);
165165
context_->initializeWriteFilterCallbacks(write_filter_callbacks_);
166166
}
167167

168-
std::unique_ptr<Context> context_;
168+
std::shared_ptr<Context> context_;
169169
NiceMock<Network::MockReadFilterCallbacks> read_filter_callbacks_;
170170
NiceMock<Network::MockWriteFilterCallbacks> write_filter_callbacks_;
171171
};

0 commit comments

Comments
 (0)