diff --git a/test/extensions/filters/http/common/fuzz/BUILD b/test/extensions/filters/http/common/fuzz/BUILD index 929cb14dfec6a..9c42dc89bb6d8 100644 --- a/test/extensions/filters/http/common/fuzz/BUILD +++ b/test/extensions/filters/http/common/fuzz/BUILD @@ -33,6 +33,7 @@ envoy_cc_test_library( deps = [ ":filter_fuzz_proto_cc_proto", "//source/common/config:utility_lib", + "//source/common/http:utility_lib", "//source/common/protobuf:utility_lib", "//source/extensions/filters/http:well_known_names", "//test/fuzz:utility_lib", diff --git a/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_stats b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_stats new file mode 100644 index 0000000000000..10704daac17bb --- /dev/null +++ b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_stats @@ -0,0 +1,47 @@ +config { + name: "envoy.filters.http.grpc_stats" + typed_config: {} +} +data { + headers { + headers { + key: ":method" + value: "POST" + } + headers { + key: ":path" + value: "/bookstore.Bookstore/CreateShelfWithPackageServiceAndMethod" + } + headers { + key: "content-type" + value: "application/grpc" + } + } +} +upstream_data { + headers { + headers { + key: ":status" + value: "200" + } + headers { + key: "content-type" + value: "application/grpc" + } + } + proto_body { + message { + [type.googleapis.com/bookstore.Book] { + id: 16 + title: "Hardy Boys" + } + } + chunk_size: 4 + } + trailers { + headers { + key: "grpc-status" + value: "0" + } + } +} \ No newline at end of file diff --git a/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_decode_encode b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_decode_encode new file mode 100644 index 0000000000000..d1a907e186fc1 --- /dev/null +++ b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_decode_encode @@ -0,0 +1,50 @@ +config { + name: "envoy.filters.http.grpc_json_transcoder" + typed_config: {} +} +data { + headers { + headers { + key: "content-type" + value: "application/json" + } + headers { + key: ":method" + value: "POST" + } + headers { + key: ":path" + value: "/bookstore.Bookstore/CreateShelfWithPackageServiceAndMethod" + } + } + http_body { + data: "{\"theme\": \"Children\"}" + } +} +upstream_data { + headers { + headers { + key: ":status" + value: "200" + } + headers { + key: "content-type" + value: "application/grpc" + } + } + proto_body { + message { + [type.googleapis.com/bookstore.Book] { + id: 16 + title: "Hardy Boys" + } + } + chunk_size: 100 + } + trailers { + headers { + key: "grpc-status" + value: "0" + } + } +} \ No newline at end of file diff --git a/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_proto_data b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_proto_data index 711ea9f66ec5d..3adc75ba874e2 100644 --- a/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_proto_data +++ b/test/extensions/filters/http/common/fuzz/filter_corpus/grpc_transcoding_proto_data @@ -5,10 +5,6 @@ config { data { headers { - headers { - key: "content-type" - value: "application/json" - } headers { key: ":method" value: "POST" @@ -17,6 +13,10 @@ data { key: ":path" value: "/bookstore.Bookstore/CreateShelf" } + headers { + key: "content-type" + value: "application/grpc" + } } proto_body { message { @@ -29,4 +29,10 @@ data { } chunk_size: 3 } + trailers { + headers { + key: "grpc-status" + value: "0" + } + } } \ No newline at end of file diff --git a/test/extensions/filters/http/common/fuzz/filter_fuzz.proto b/test/extensions/filters/http/common/fuzz/filter_fuzz.proto index a97d9dcfd2bbb..20f036684161c 100644 --- a/test/extensions/filters/http/common/fuzz/filter_fuzz.proto +++ b/test/extensions/filters/http/common/fuzz/filter_fuzz.proto @@ -8,5 +8,9 @@ import "envoy/extensions/filters/network/http_connection_manager/v3/http_connect message FilterFuzzTestCase { envoy.extensions.filters.network.http_connection_manager.v3.HttpFilter config = 1; + // Downstream data (named for backwards compatibility). test.fuzz.HttpData data = 2; + + // Upstream data. + test.fuzz.HttpData upstream_data = 3; } diff --git a/test/extensions/filters/http/common/fuzz/filter_fuzz_test.cc b/test/extensions/filters/http/common/fuzz/filter_fuzz_test.cc index edfa89f917c77..7e773b4f13115 100644 --- a/test/extensions/filters/http/common/fuzz/filter_fuzz_test.cc +++ b/test/extensions/filters/http/common/fuzz/filter_fuzz_test.cc @@ -50,7 +50,7 @@ DEFINE_PROTO_FUZZER(const test::extensions::filters::http::FilterFuzzTestCase& i TestUtility::validate(input); // Fuzz filter. static UberFilterFuzzer fuzzer; - fuzzer.fuzz(input.config(), input.data()); + fuzzer.fuzz(input.config(), input.data(), input.upstream_data()); } catch (const ProtoValidationException& e) { ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what()); } diff --git a/test/extensions/filters/http/common/fuzz/uber_filter.cc b/test/extensions/filters/http/common/fuzz/uber_filter.cc index a88cc585a72ef..ae7fbc2d9f924 100644 --- a/test/extensions/filters/http/common/fuzz/uber_filter.cc +++ b/test/extensions/filters/http/common/fuzz/uber_filter.cc @@ -3,6 +3,7 @@ #include "common/config/utility.h" #include "common/config/version_converter.h" #include "common/http/message_impl.h" +#include "common/http/utility.h" #include "common/protobuf/protobuf.h" #include "common/protobuf/utility.h" @@ -13,16 +14,25 @@ namespace Extensions { namespace HttpFilters { UberFilterFuzzer::UberFilterFuzzer() { - // Need to set for both a decoder filter and an encoder/decoder filter. + // This is a decoder filter. ON_CALL(filter_callback_, addStreamDecoderFilter(_)) - .WillByDefault(Invoke([&](std::shared_ptr filter) -> void { - filter_ = filter; - filter_->setDecoderFilterCallbacks(callbacks_); + .WillByDefault(Invoke([&](Http::StreamDecoderFilterSharedPtr filter) -> void { + decoder_filter_ = filter; + decoder_filter_->setDecoderFilterCallbacks(decoder_callbacks_); })); + // This is an encoded filter. + ON_CALL(filter_callback_, addStreamEncoderFilter(_)) + .WillByDefault(Invoke([&](Http::StreamEncoderFilterSharedPtr filter) -> void { + encoder_filter_ = filter; + encoder_filter_->setEncoderFilterCallbacks(encoder_callbacks_); + })); + // This is a decoder and encoder filter. ON_CALL(filter_callback_, addStreamFilter(_)) - .WillByDefault(Invoke([&](std::shared_ptr filter) -> void { - filter_ = filter; - filter_->setDecoderFilterCallbacks(callbacks_); + .WillByDefault(Invoke([&](Http::StreamFilterSharedPtr filter) -> void { + decoder_filter_ = filter; + decoder_filter_->setDecoderFilterCallbacks(decoder_callbacks_); + encoder_filter_ = filter; + encoder_filter_->setEncoderFilterCallbacks(encoder_callbacks_); })); // Set expectations for particular filters that may get fuzzed. perFilterSetup(); @@ -44,28 +54,16 @@ std::vector UberFilterFuzzer::parseHttpData(const test::fuzz::HttpD return data_chunks; } -void UberFilterFuzzer::decode(Http::StreamDecoderFilter* filter, const test::fuzz::HttpData& data) { +template +void UberFilterFuzzer::runData(FilterType* filter, const test::fuzz::HttpData& data) { bool end_stream = false; - - auto headers = Fuzz::fromHeaders(data.headers()); - if (headers.Path() == nullptr) { - headers.setPath("/foo"); - } - if (headers.Method() == nullptr) { - headers.setMethod("GET"); - } - if (headers.Host() == nullptr) { - headers.setHost("foo.com"); - } - if (data.body_case() == test::fuzz::HttpData::BODY_NOT_SET && !data.has_trailers()) { end_stream = true; } - ENVOY_LOG_MISC(debug, "Decoding headers (end_stream={}): {} ", end_stream, - data.headers().DebugString()); - const auto& headersStatus = filter->decodeHeaders(headers, end_stream); + const auto& headersStatus = sendHeaders(filter, data, end_stream); if (headersStatus != Http::FilterHeadersStatus::Continue && headersStatus != Http::FilterHeadersStatus::StopIteration) { + ENVOY_LOG_MISC(debug, "Finished with FilterHeadersStatus: {}", headersStatus); return; } @@ -75,23 +73,90 @@ void UberFilterFuzzer::decode(Http::StreamDecoderFilter* filter, const test::fuz end_stream = true; } Buffer::OwnedImpl buffer(data_chunks[i]); - ENVOY_LOG_MISC(debug, "Decoding data (end_stream={}): {} ", end_stream, buffer.toString()); - if (filter->decodeData(buffer, end_stream) != Http::FilterDataStatus::Continue) { + const auto& dataStatus = sendData(filter, buffer, end_stream); + if (dataStatus != Http::FilterDataStatus::Continue) { + ENVOY_LOG_MISC(debug, "Finished with FilterDataStatus: {}", dataStatus); return; } } if (data.has_trailers()) { - ENVOY_LOG_MISC(debug, "Decoding trailers: {} ", data.trailers().DebugString()); - auto trailers = Fuzz::fromHeaders(data.trailers()); - filter->decodeTrailers(trailers); + sendTrailers(filter, data); + } +} + +template <> +Http::FilterHeadersStatus UberFilterFuzzer::sendHeaders(Http::StreamDecoderFilter* filter, + const test::fuzz::HttpData& data, + bool end_stream) { + request_headers_ = Fuzz::fromHeaders(data.headers()); + if (request_headers_.Path() == nullptr) { + request_headers_.setPath("/foo"); + } + if (request_headers_.Method() == nullptr) { + request_headers_.setMethod("GET"); + } + if (request_headers_.Host() == nullptr) { + request_headers_.setHost("foo.com"); + } + + ENVOY_LOG_MISC(debug, "Decoding headers (end_stream={}):\n{} ", end_stream, request_headers_); + return filter->decodeHeaders(request_headers_, end_stream); +} + +template <> +Http::FilterHeadersStatus UberFilterFuzzer::sendHeaders(Http::StreamEncoderFilter* filter, + const test::fuzz::HttpData& data, + bool end_stream) { + response_headers_ = Fuzz::fromHeaders(data.headers()); + + // Status must be a valid unsigned long. If not set, the utility function below will throw + // an exception on the data path of some filters. This should never happen in production, so catch + // the exception and set to a default value. + try { + (void)Http::Utility::getResponseStatus(response_headers_); + } catch (const Http::CodecClientException& e) { + response_headers_.setStatus(200); } + + ENVOY_LOG_MISC(debug, "Encoding headers (end_stream={}):\n{} ", end_stream, response_headers_); + return filter->encodeHeaders(response_headers_, end_stream); +} + +template <> +Http::FilterDataStatus UberFilterFuzzer::sendData(Http::StreamDecoderFilter* filter, + Buffer::Instance& buffer, bool end_stream) { + ENVOY_LOG_MISC(debug, "Decoding data (end_stream={}): {} ", end_stream, buffer.toString()); + return filter->decodeData(buffer, end_stream); +} + +template <> +Http::FilterDataStatus UberFilterFuzzer::sendData(Http::StreamEncoderFilter* filter, + Buffer::Instance& buffer, bool end_stream) { + ENVOY_LOG_MISC(debug, "Encoding data (end_stream={}): {} ", end_stream, buffer.toString()); + return filter->encodeData(buffer, end_stream); +} + +template <> +void UberFilterFuzzer::sendTrailers(Http::StreamDecoderFilter* filter, + const test::fuzz::HttpData& data) { + request_trailers_ = Fuzz::fromHeaders(data.trailers()); + ENVOY_LOG_MISC(debug, "Decoding trailers:\n{} ", request_trailers_); + filter->decodeTrailers(request_trailers_); +} + +template <> +void UberFilterFuzzer::sendTrailers(Http::StreamEncoderFilter* filter, + const test::fuzz::HttpData& data) { + response_trailers_ = Fuzz::fromHeaders(data.trailers()); + ENVOY_LOG_MISC(debug, "Encoding trailers:\n{} ", response_trailers_); + filter->encodeTrailers(response_trailers_); } void UberFilterFuzzer::fuzz( const envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter& proto_config, - const test::fuzz::HttpData& data) { + const test::fuzz::HttpData& downstream_data, const test::fuzz::HttpData& upstream_data) { try { // Try to create the filter. Exit early if the config is invalid or violates PGV constraints. ENVOY_LOG_MISC(info, "filter name {}", proto_config.name()); @@ -108,15 +173,32 @@ void UberFilterFuzzer::fuzz( return; } - decode(filter_.get(), data); + // Data path should not throw exceptions. + if (decoder_filter_ != nullptr) { + runData(decoder_filter_.get(), downstream_data); + } + if (encoder_filter_ != nullptr) { + runData(encoder_filter_.get(), upstream_data); + } + reset(); } void UberFilterFuzzer::reset() { - if (filter_ != nullptr) { - filter_->onDestroy(); + if (decoder_filter_ != nullptr) { + decoder_filter_->onDestroy(); + } + decoder_filter_.reset(); + + if (encoder_filter_ != nullptr) { + encoder_filter_->onDestroy(); } - filter_.reset(); + encoder_filter_.reset(); + + request_headers_.clear(); + response_headers_.clear(); + request_trailers_.clear(); + response_trailers_.clear(); } } // namespace HttpFilters diff --git a/test/extensions/filters/http/common/fuzz/uber_filter.h b/test/extensions/filters/http/common/fuzz/uber_filter.h index 511c587a6e625..af6b060f6a802 100644 --- a/test/extensions/filters/http/common/fuzz/uber_filter.h +++ b/test/extensions/filters/http/common/fuzz/uber_filter.h @@ -11,10 +11,13 @@ class UberFilterFuzzer { public: UberFilterFuzzer(); - // This creates the filter config and runs the decode methods. + // This creates the filter config and runs the fuzzed data against the filter. void fuzz(const envoy::extensions::filters::network::http_connection_manager::v3::HttpFilter& proto_config, - const test::fuzz::HttpData& data); + const test::fuzz::HttpData& downstream_data, const test::fuzz::HttpData& upstream_data); + + // This executes the filter decoders/encoders with the fuzzed data. + template void runData(FilterType* filter, const test::fuzz::HttpData& data); // For fuzzing proto data, guide the mutator to useful 'Any' types. static void guideAnyProtoType(test::fuzz::HttpData* mutable_data, uint choice); @@ -26,22 +29,47 @@ class UberFilterFuzzer { void cleanFuzzedConfig(absl::string_view filter_name, Protobuf::Message* message); // Parses http or proto body into chunks. - std::vector parseHttpData(const test::fuzz::HttpData& data); + static std::vector parseHttpData(const test::fuzz::HttpData& data); + + // Templated functions to validate and send headers/data/trailers for decoders/encoders. + // General functions are deleted, but templated specializations for encoders/decoders are defined + // in the cc file. + template + Http::FilterHeadersStatus sendHeaders(FilterType* filter, const test::fuzz::HttpData& data, + bool end_stream) = delete; + + template + Http::FilterDataStatus sendData(FilterType* filter, Buffer::Instance& buffer, + bool end_stream) = delete; - // This executes the decode methods to be fuzzed. - void decode(Http::StreamDecoderFilter* filter, const test::fuzz::HttpData& data); + template + void sendTrailers(FilterType* filter, const test::fuzz::HttpData& data) = delete; void reset(); private: NiceMock factory_context_; - NiceMock callbacks_; NiceMock filter_callback_; std::shared_ptr resolver_{std::make_shared()}; - std::shared_ptr filter_; Http::FilterFactoryCb cb_; NiceMock connection_; Network::Address::InstanceConstSharedPtr addr_; + + // Mocked callbacks. + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; + + // Filter constructed from the config. + Http::StreamDecoderFilterSharedPtr decoder_filter_; + Http::StreamEncoderFilterSharedPtr encoder_filter_; + + // Headers/trailers need to be saved for the lifetime of the the filter, + // so save them as member variables. + // TODO(nareddyt): Use for access logging in a followup PR. + Http::TestRequestHeaderMapImpl request_headers_; + Http::TestResponseHeaderMapImpl response_headers_; + Http::TestRequestTrailerMapImpl request_trailers_; + Http::TestResponseTrailerMapImpl response_trailers_; }; } // namespace HttpFilters diff --git a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc index 353eea56f0bee..ad6913bda5f04 100644 --- a/test/extensions/filters/http/common/fuzz/uber_per_filter.cc +++ b/test/extensions/filters/http/common/fuzz/uber_per_filter.cc @@ -83,10 +83,16 @@ void UberFilterFuzzer::perFilterSetup() { addr_ = std::make_shared("1.2.3.4", 1111); ON_CALL(connection_, remoteAddress()).WillByDefault(testing::ReturnRef(addr_)); ON_CALL(connection_, localAddress()).WillByDefault(testing::ReturnRef(addr_)); - ON_CALL(callbacks_, connection()).WillByDefault(testing::Return(&connection_)); - ON_CALL(callbacks_, activeSpan()) + + ON_CALL(decoder_callbacks_, connection()).WillByDefault(testing::Return(&connection_)); + ON_CALL(decoder_callbacks_, activeSpan()) + .WillByDefault(testing::ReturnRef(Tracing::NullSpan::instance())); + decoder_callbacks_.stream_info_.protocol_ = Envoy::Http::Protocol::Http2; + + ON_CALL(encoder_callbacks_, connection()).WillByDefault(testing::Return(&connection_)); + ON_CALL(encoder_callbacks_, activeSpan()) .WillByDefault(testing::ReturnRef(Tracing::NullSpan::instance())); - callbacks_.stream_info_.protocol_ = Envoy::Http::Protocol::Http2; + encoder_callbacks_.stream_info_.protocol_ = Envoy::Http::Protocol::Http2; // Prepare expectations for dynamic forward proxy. ON_CALL(factory_context_.dispatcher_, createDnsResolver(_, _)) diff --git a/test/fuzz/common.proto b/test/fuzz/common.proto index 92df9a1b40219..3c4f62a005e6a 100644 --- a/test/fuzz/common.proto +++ b/test/fuzz/common.proto @@ -28,7 +28,7 @@ message ProtoBody { google.protobuf.Any message = 1 [(validate.rules).any.required = true]; // The size (in bytes) of each buffer when forming the requests. - uint64 chunk_size = 2 [(validate.rules).uint64.gt = 0]; + uint64 chunk_size = 2 [(validate.rules).uint64 = {gt: 0, lt: 8192}]; } message HttpData { diff --git a/test/fuzz/fuzz_runner.cc b/test/fuzz/fuzz_runner.cc index c7cbcdfa08b70..bda9446b39e9c 100644 --- a/test/fuzz/fuzz_runner.cc +++ b/test/fuzz/fuzz_runner.cc @@ -52,7 +52,9 @@ void Runner::setupEnvironment(int argc, char** argv, spdlog::level::level_enum d // For fuzzing, this prevents logging when parsing text-format protos fails, // deprecated fields are used, etc. // https://github.com/protocolbuffers/protobuf/blob/204f99488ce1ef74565239cf3963111ae4c774b7/src/google/protobuf/stubs/logging.h#L223 - ABSL_ATTRIBUTE_UNUSED static auto* log_silencer = new Protobuf::LogSilencer(); + if (log_level_ > spdlog::level::debug) { + ABSL_ATTRIBUTE_UNUSED static auto* log_silencer = new Protobuf::LogSilencer(); + } } } // namespace Fuzz