Skip to content

Commit 5acb1d0

Browse files
authored
ext_proc filter: Increase fuzz coverage (#17636)
Fuzz test sends generated downstream requests and replies to ext_proc ProcessingRequest messages with generated ProcessingResponse messages. Currently uses the autonomous upstream for upstream responses. Risk Level: low Testing: Adds fuzz test Signed-off-by: Isaac Polinsky <polinsky@google.com>
1 parent bbef2aa commit 5acb1d0

File tree

5 files changed

+943
-0
lines changed

5 files changed

+943
-0
lines changed

test/extensions/filters/http/ext_proc/BUILD

+42
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
load(
22
"//bazel:envoy_build_system.bzl",
3+
"envoy_cc_fuzz_test",
34
"envoy_package",
45
)
56
load(
@@ -162,3 +163,44 @@ envoy_extension_cc_test_library(
162163
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
163164
],
164165
)
166+
167+
envoy_extension_cc_test_library(
168+
name = "ext_proc_grpc_fuzz_lib",
169+
srcs = ["ext_proc_grpc_fuzz_helper.cc"],
170+
hdrs = ["ext_proc_grpc_fuzz_helper.h"],
171+
extension_names = ["envoy.filters.http.ext_proc"],
172+
deps = [
173+
"//source/common/common:thread_lib",
174+
"//source/common/grpc:common_lib",
175+
"//test/common/http:common_lib",
176+
"//test/fuzz:fuzz_runner_lib",
177+
"//test/fuzz:utility_lib",
178+
"//test/integration:http_integration_lib",
179+
"//test/test_common:utility_lib",
180+
"@com_github_grpc_grpc//:grpc++",
181+
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
182+
"@envoy_api//envoy/extensions/filters/http/ext_proc/v3alpha:pkg_cc_proto",
183+
"@envoy_api//envoy/service/ext_proc/v3alpha:pkg_cc_proto",
184+
"@envoy_api//envoy/type/v3:pkg_cc_proto",
185+
],
186+
)
187+
188+
envoy_cc_fuzz_test(
189+
name = "ext_proc_grpc_fuzz_test",
190+
srcs = ["ext_proc_grpc_fuzz.cc"],
191+
corpus = "ext_proc_grpc_corpus",
192+
deps = [
193+
":ext_proc_grpc_fuzz_lib",
194+
":test_processor_lib",
195+
"//source/common/network:address_lib",
196+
"//source/extensions/filters/http/ext_proc:config",
197+
"//test/common/http:common_lib",
198+
"//test/fuzz:utility_lib",
199+
"//test/integration:http_integration_lib",
200+
"//test/test_common:utility_lib",
201+
"@envoy_api//envoy/config/core/v3:pkg_cc_proto",
202+
"@envoy_api//envoy/extensions/filters/http/ext_proc/v3alpha:pkg_cc_proto",
203+
"@envoy_api//envoy/service/ext_proc/v3alpha:pkg_cc_proto",
204+
"@envoy_api//envoy/type/v3:pkg_cc_proto",
205+
],
206+
)

test/extensions/filters/http/ext_proc/ext_proc_grpc_corpus/test

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
// TODO(ikepolinsky): Major action items to improve this fuzzer
2+
// 1. Move external process from separate thread to have test all in one thread
3+
// - Explore using fake gRPC client for this
4+
// 2. Implement sending trailers from downstream and mutating headers/trailers
5+
// in the external process.
6+
// 3. Use an upstream that sends varying responses (also with trailers)
7+
// 4. Explore performance optimizations:
8+
// - Threads and fake gRPC client above might help
9+
// - Local testing had almost 800k inline 8 bit counters resulting in ~3
10+
// exec/s. How far can we reduce the number of counters?
11+
// - At the loss of reproducibility use a persistent envoy
12+
// 5. Protobuf fuzzing would greatly increase crash test case readability
13+
// - How will this impact speed?
14+
// - Can it be done on single thread as well?
15+
// 6. Restructure to inherit common functions between ExtProcIntegrationTest
16+
// and this class. This involves adding a new ExtProcIntegrationBase class
17+
// common to both.
18+
// 7. Remove locks after crash is addressed by separate issue
19+
20+
#include "envoy/config/core/v3/base.pb.h"
21+
#include "envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.pb.h"
22+
#include "envoy/service/ext_proc/v3alpha/external_processor.pb.h"
23+
#include "envoy/type/v3/http_status.pb.h"
24+
25+
#include "source/common/network/address_impl.h"
26+
27+
#include "test/common/http/common.h"
28+
#include "test/extensions/filters/http/ext_proc/ext_proc_grpc_fuzz_helper.h"
29+
#include "test/extensions/filters/http/ext_proc/test_processor.h"
30+
#include "test/fuzz/fuzz_runner.h"
31+
#include "test/integration/http_integration.h"
32+
#include "test/test_common/utility.h"
33+
34+
namespace Envoy {
35+
namespace Extensions {
36+
namespace HttpFilters {
37+
namespace ExternalProcessing {
38+
39+
using envoy::extensions::filters::http::ext_proc::v3alpha::ProcessingMode;
40+
using envoy::service::ext_proc::v3alpha::ProcessingRequest;
41+
using envoy::service::ext_proc::v3alpha::ProcessingResponse;
42+
43+
// The buffer size for the listeners
44+
static const uint32_t BufferSize = 100000;
45+
46+
// These tests exercise the ext_proc filter through Envoy's integration test
47+
// environment by configuring an instance of the Envoy server and driving it
48+
// through the mock network stack.
49+
50+
class ExtProcIntegrationFuzz : public HttpIntegrationTest,
51+
public Grpc::BaseGrpcClientIntegrationParamTest {
52+
public:
53+
ExtProcIntegrationFuzz(Network::Address::IpVersion ip_version, Grpc::ClientType client_type)
54+
: HttpIntegrationTest(Http::CodecType::HTTP2, ip_version) {
55+
ip_version_ = ip_version;
56+
client_type_ = client_type;
57+
}
58+
59+
void tearDown() {
60+
cleanupUpstreamAndDownstream();
61+
test_processor_.shutdown();
62+
}
63+
64+
Network::Address::IpVersion ipVersion() const override { return ip_version_; }
65+
Grpc::ClientType clientType() const override { return client_type_; }
66+
67+
void initializeFuzzer(bool autonomous_upstream) {
68+
autonomous_upstream_ = autonomous_upstream;
69+
autonomous_allow_incomplete_streams_ = true;
70+
initializeConfig();
71+
HttpIntegrationTest::initialize();
72+
}
73+
74+
void initializeConfig() {
75+
config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) {
76+
// Create a cluster for our gRPC server pointing to the address that is running the gRPC
77+
// server.
78+
auto* processor_cluster = bootstrap.mutable_static_resources()->add_clusters();
79+
processor_cluster->set_name("ext_proc_server");
80+
processor_cluster->mutable_load_assignment()->set_cluster_name("ext_proc_server");
81+
auto* address = processor_cluster->mutable_load_assignment()
82+
->add_endpoints()
83+
->add_lb_endpoints()
84+
->mutable_endpoint()
85+
->mutable_address()
86+
->mutable_socket_address();
87+
address->set_address(Network::Test::getLoopbackAddressString(ipVersion()));
88+
address->set_port_value(test_processor_.port());
89+
90+
// Ensure "HTTP2 with no prior knowledge." Necessary for gRPC.
91+
ConfigHelper::setHttp2(
92+
*(bootstrap.mutable_static_resources()->mutable_clusters()->Mutable(0)));
93+
ConfigHelper::setHttp2(*processor_cluster);
94+
95+
// Make sure both flavors of gRPC client use the right address.
96+
if (ipVersion() == Network::Address::IpVersion::v4) {
97+
const auto addr = std::make_shared<Network::Address::Ipv4Instance>(
98+
Network::Test::getLoopbackAddressString(ipVersion()), test_processor_.port());
99+
setGrpcService(*proto_config_.mutable_grpc_service(), "ext_proc_server", addr);
100+
} else {
101+
const auto addr = std::make_shared<Network::Address::Ipv6Instance>(
102+
Network::Test::getLoopbackAddressString(ipVersion()), test_processor_.port());
103+
setGrpcService(*proto_config_.mutable_grpc_service(), "ext_proc_server", addr);
104+
}
105+
106+
// Merge the filter.
107+
envoy::config::listener::v3::Filter ext_proc_filter;
108+
ext_proc_filter.set_name("envoy.filters.http.ext_proc");
109+
ext_proc_filter.mutable_typed_config()->PackFrom(proto_config_);
110+
config_helper_.addFilter(MessageUtil::getJsonStringFromMessageOrDie(ext_proc_filter));
111+
});
112+
113+
// Make sure that we have control over when buffers will fill up.
114+
config_helper_.setBufferLimits(BufferSize, BufferSize);
115+
116+
setUpstreamProtocol(Http::CodecType::HTTP2);
117+
setDownstreamProtocol(Http::CodecType::HTTP2);
118+
}
119+
120+
IntegrationStreamDecoderPtr sendDownstreamRequest(
121+
absl::optional<std::function<void(Http::HeaderMap& headers)>> modify_headers,
122+
absl::string_view http_method = "GET") {
123+
auto conn = makeClientConnection(lookupPort("http"));
124+
codec_client_ = makeHttpConnection(std::move(conn));
125+
Http::TestRequestHeaderMapImpl headers{{":method", std::string(http_method)}};
126+
if (modify_headers) {
127+
(*modify_headers)(headers);
128+
}
129+
HttpTestUtility::addDefaultHeaders(headers, false);
130+
return codec_client_->makeHeaderOnlyRequest(headers);
131+
}
132+
133+
IntegrationStreamDecoderPtr sendDownstreamRequestWithBody(
134+
absl::string_view body,
135+
absl::optional<std::function<void(Http::HeaderMap& headers)>> modify_headers,
136+
absl::string_view http_method = "POST") {
137+
auto conn = makeClientConnection(lookupPort("http"));
138+
codec_client_ = makeHttpConnection(std::move(conn));
139+
Http::TestRequestHeaderMapImpl headers{{":method", std::string(http_method)}};
140+
HttpTestUtility::addDefaultHeaders(headers, false);
141+
if (modify_headers) {
142+
(*modify_headers)(headers);
143+
}
144+
return codec_client_->makeRequestWithBody(headers, std::string(body));
145+
}
146+
147+
IntegrationStreamDecoderPtr sendDownstreamRequestWithChunks(
148+
FuzzedDataProvider* fdp, ExtProcFuzzHelper* fh,
149+
absl::optional<std::function<void(Http::HeaderMap& headers)>> modify_headers,
150+
absl::string_view http_method = "POST") {
151+
auto conn = makeClientConnection(lookupPort("http"));
152+
codec_client_ = makeHttpConnection(std::move(conn));
153+
Http::TestRequestHeaderMapImpl headers{{":method", std::string(http_method)}};
154+
HttpTestUtility::addDefaultHeaders(headers, false);
155+
if (modify_headers) {
156+
(*modify_headers)(headers);
157+
}
158+
auto encoder_decoder = codec_client_->startRequest(headers);
159+
IntegrationStreamDecoderPtr response = std::move(encoder_decoder.second);
160+
auto& encoder = encoder_decoder.first;
161+
162+
const uint32_t num_chunks =
163+
fdp->ConsumeIntegralInRange<uint32_t>(0, ExtProcFuzzMaxStreamChunks);
164+
for (uint32_t i = 0; i < num_chunks; i++) {
165+
// TODO(ikepolinsky): open issue for this crash and remove locks once
166+
// fixed.
167+
// If proxy closes connection before body is fully sent it causes a
168+
// crash. To address this, the external processor sets a flag to
169+
// signal when it has generated an immediate response which will close
170+
// the connection in the future. We check this flag, which is protected
171+
// by a lock, before sending a chunk. If the flag is set, we don't attempt
172+
// to send more data, regardless of whether or not the
173+
// codec_client connection is still open. There are no locks protecting
174+
// the codec_client connection and cannot trust that it's safe to send
175+
// another chunk
176+
fh->immediate_resp_lock_.lock();
177+
if (fh->immediate_resp_sent_) {
178+
ENVOY_LOG_MISC(trace, "Proxy closed connection, returning early");
179+
fh->immediate_resp_lock_.unlock();
180+
return response;
181+
}
182+
const uint32_t data_size = fdp->ConsumeIntegralInRange<uint32_t>(0, ExtProcFuzzMaxDataSize);
183+
ENVOY_LOG_MISC(trace, "Sending chunk of {} bytes", data_size);
184+
codec_client_->sendData(encoder, data_size, false);
185+
fh->immediate_resp_lock_.unlock();
186+
}
187+
188+
// See comment above
189+
fh->immediate_resp_lock_.lock();
190+
if (!fh->immediate_resp_sent_) {
191+
ENVOY_LOG_MISC(trace, "Sending empty chunk to close stream");
192+
Buffer::OwnedImpl empty_chunk;
193+
codec_client_->sendData(encoder, empty_chunk, true);
194+
}
195+
fh->immediate_resp_lock_.unlock();
196+
return response;
197+
}
198+
199+
IntegrationStreamDecoderPtr randomDownstreamRequest(FuzzedDataProvider* fdp,
200+
ExtProcFuzzHelper* fh) {
201+
// From the external processor's view each of these requests
202+
// are handled the same way. They only differ in what the server should
203+
// send back to the client.
204+
// TODO(ikepolinsky): add random flag for sending trailers with a request
205+
// using HttpIntegration::sendTrailers()
206+
switch (fdp->ConsumeEnum<HttpMethod>()) {
207+
case HttpMethod::GET:
208+
ENVOY_LOG_MISC(trace, "Sending GET request");
209+
return sendDownstreamRequest(absl::nullopt);
210+
case HttpMethod::POST:
211+
if (fdp->ConsumeBool()) {
212+
ENVOY_LOG_MISC(trace, "Sending POST request with body");
213+
const uint32_t data_size = fdp->ConsumeIntegralInRange<uint32_t>(0, ExtProcFuzzMaxDataSize);
214+
const std::string data = std::string(data_size, 'a');
215+
return sendDownstreamRequestWithBody(data, absl::nullopt);
216+
} else {
217+
ENVOY_LOG_MISC(trace, "Sending POST request with chunked body");
218+
return sendDownstreamRequestWithChunks(fdp, fh, absl::nullopt);
219+
}
220+
default:
221+
RELEASE_ASSERT(false, "Unhandled HttpMethod");
222+
}
223+
}
224+
225+
envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor proto_config_{};
226+
TestProcessor test_processor_;
227+
Network::Address::IpVersion ip_version_;
228+
Grpc::ClientType client_type_;
229+
};
230+
231+
DEFINE_FUZZER(const uint8_t* buf, size_t len) {
232+
// Split the buffer into two buffers with at least 1 byte
233+
if (len < 2) {
234+
return;
235+
}
236+
237+
// External Process and downstream are on different threads so they should
238+
// have separate data providers
239+
const size_t downstream_buf_len = len / 2;
240+
const size_t ext_proc_buf_len = len - downstream_buf_len;
241+
242+
// downstream buf starts at 0, ext_prob buf starts at buf[downstream_buf_len]
243+
FuzzedDataProvider downstream_provider(buf, downstream_buf_len);
244+
FuzzedDataProvider ext_proc_provider(&buf[downstream_buf_len], ext_proc_buf_len);
245+
246+
// Get IP and gRPC version from environment
247+
ExtProcIntegrationFuzz fuzzer(TestEnvironment::getIpVersionsForTest()[0],
248+
TestEnvironment::getsGrpcVersionsForTest()[0]);
249+
ExtProcFuzzHelper fuzz_helper(&ext_proc_provider);
250+
251+
// This starts an external processor in a separate thread. This allows for the
252+
// external process to consume messages in a loop without blocking the fuzz
253+
// target from receiving the response.
254+
fuzzer.test_processor_.start(
255+
[&fuzz_helper](grpc::ServerReaderWriter<ProcessingResponse, ProcessingRequest>* stream) {
256+
while (true) {
257+
ProcessingRequest req;
258+
if (!stream->Read(&req)) {
259+
return grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "expected message");
260+
}
261+
262+
fuzz_helper.logRequest(&req);
263+
264+
// The following blocks generate random data for the 9 fields of the
265+
// ProcessingResponse gRPC message
266+
267+
// 1 - 7. Randomize response
268+
// If true, immediately close the connection with a random Grpc Status.
269+
// Otherwise randomize the response
270+
ProcessingResponse resp;
271+
if (fuzz_helper.provider_->ConsumeBool()) {
272+
ENVOY_LOG_MISC(trace, "Immediately Closing gRPC connection");
273+
return fuzz_helper.randomGrpcStatusWithMessage();
274+
} else {
275+
ENVOY_LOG_MISC(trace, "Generating Random ProcessingResponse");
276+
fuzz_helper.randomizeResponse(&resp, &req);
277+
}
278+
279+
// 8. Randomize dynamic_metadata
280+
// TODO(ikepolinsky): ext_proc does not support dynamic_metadata
281+
282+
// 9. Randomize mode_override
283+
if (fuzz_helper.provider_->ConsumeBool()) {
284+
ENVOY_LOG_MISC(trace, "Generating Random ProcessingMode Override");
285+
ProcessingMode* msg = resp.mutable_mode_override();
286+
fuzz_helper.randomizeOverrideResponse(msg);
287+
}
288+
289+
ENVOY_LOG_MISC(trace, "Response generated, writing to stream.");
290+
stream->Write(resp);
291+
}
292+
293+
return grpc::Status::OK;
294+
});
295+
296+
ENVOY_LOG_MISC(trace, "External Process started.");
297+
298+
fuzzer.initializeFuzzer(true);
299+
ENVOY_LOG_MISC(trace, "Fuzzer initialized");
300+
301+
const auto response = fuzzer.randomDownstreamRequest(&downstream_provider, &fuzz_helper);
302+
303+
// For fuzz testing we don't care about the response code, only that
304+
// the stream ended in some graceful manner
305+
ENVOY_LOG_MISC(trace, "Waiting for response.");
306+
if (response->waitForEndStream(std::chrono::milliseconds(200))) {
307+
ENVOY_LOG_MISC(trace, "Response received.");
308+
} else {
309+
// TODO(ikepolinsky): investigate if there is anyway around this.
310+
// Waiting too long for a fuzz case to fail will drastically
311+
// reduce executions/second.
312+
ENVOY_LOG_MISC(trace, "Response timed out.");
313+
}
314+
fuzzer.tearDown();
315+
}
316+
317+
} // namespace ExternalProcessing
318+
} // namespace HttpFilters
319+
} // namespace Extensions
320+
} // namespace Envoy

0 commit comments

Comments
 (0)