diff --git a/test/extensions/filters/network/common/fuzz/BUILD b/test/extensions/filters/network/common/fuzz/BUILD index f8d38307d569a..8f54f57e5de81 100644 --- a/test/extensions/filters/network/common/fuzz/BUILD +++ b/test/extensions/filters/network/common/fuzz/BUILD @@ -23,6 +23,15 @@ envoy_proto_library( ], ) +envoy_proto_library( + name = "network_writefilter_fuzz_proto", + srcs = ["network_writefilter_fuzz.proto"], + deps = [ + "//test/fuzz:common_proto", + "@envoy_api//envoy/config/listener/v3:pkg", + ], +) + envoy_cc_test_library( name = "uber_readfilter_lib", srcs = [ @@ -59,3 +68,38 @@ envoy_cc_fuzz_test( "//test/config:utility_lib", ] + envoy_all_network_filters(), ) + +envoy_cc_test_library( + name = "uber_writefilter_lib", + srcs = [ + "uber_per_writefilter.cc", + "uber_writefilter.cc", + ], + hdrs = ["uber_writefilter.h"], + deps = [ + ":network_writefilter_fuzz_proto_cc_proto", + "//source/common/config:utility_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:utility_lib", + "//test/extensions/filters/network/common/fuzz/utils:network_filter_fuzzer_fakes_lib", + "//test/fuzz:utility_lib", + "//test/mocks/network:network_mocks", + ], +) + +envoy_cc_fuzz_test( + name = "network_writefilter_fuzz_test", + srcs = ["network_writefilter_fuzz_test.cc"], + corpus = "network_writefilter_corpus", + # All Envoy network filters must be linked to the test in order for the fuzzer to pick + # these up via the NamedNetworkFilterConfigFactory. + deps = [ + ":uber_writefilter_lib", + "//source/common/config:utility_lib", + "//source/extensions/filters/network/kafka:kafka_broker_config_lib", + "//source/extensions/filters/network/mongo_proxy:config", + "//source/extensions/filters/network/mysql_proxy:config", + "//source/extensions/filters/network/zookeeper_proxy:config", + "//test/config:utility_lib", + ], +) diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/kafka_broker_1 b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/kafka_broker_1 new file mode 100644 index 0000000000000..a20c58dd2d4a1 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/kafka_broker_1 @@ -0,0 +1,110 @@ +config { + name: "envoy.filters.network.kafka_broker" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.network.kafka_broker.v3.KafkaBroker" + value: "\n}\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177\177" + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + advance_time { + milliseconds: 268435 + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + data: "\312\312\312\312\312\312\312\312\312\312\312\312\315\312\312\312\312\312\312\312\312\312\312" + end_stream: true + } +} +actions { + on_write { + data: "-" + } +} +actions { + on_write { + data: "\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312\312" + end_stream: true + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + data: "-" + } +} +actions { + on_write { + data: "-" + } +} +actions { + on_write { + data: "\n\002\315\265" + } +} +actions { + on_write { + end_stream: true + } +} +actions { + on_write { + data: "\020\000\000\000" + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + data: "p" + } +} +actions { + on_write { + data: "-" + } +} +actions { + on_write { + data: "-" + end_stream: true + } +} +actions { + on_write { + } +} diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mongodb_proxy_1 b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mongodb_proxy_1 new file mode 100644 index 0000000000000..20a344f8fe351 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mongodb_proxy_1 @@ -0,0 +1,107 @@ +config { + name: "envoy.filters.network.mongo_proxy" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.network.mongo_proxy.v3.MongoProxy" + value: "\n\001\\\032\007\"\003\010\200t*\000 \001" + } +} +actions { + on_write { + data: "]\000" + } +} +actions { + on_write { + data: "\004\000" + end_stream: true + } +} +actions { + advance_time { + milliseconds: 14848 + } +} +actions { + on_write { + data: "\004\000" + end_stream: true + } +} +actions { + advance_time { + milliseconds: 14848 + } +} +actions { + on_write { + data: "\004\000\001\000\000\000\000\000\000\001" + end_stream: true + } +} +actions { + on_write { + data: "<" + end_stream: true + } +} +actions { + on_write { + data: "\004\000" + } +} +actions { + on_write { + data: "\004\000" + } +} +actions { + advance_time { + milliseconds: 14848 + } +} +actions { + on_write { + data: "type.googleapis.com/envoy.extensions.filters.network.mongo_proxy.v3.MongoProxy" + end_stream: true + } +} +actions { + on_write { + data: "\004\000" + end_stream: true + } +} +actions { + on_write { + data: "\004\000" + } +} +actions { + on_write { + data: "pH\037\000 `\000\000" + end_stream: true + } +} +actions { + on_write { + data: "\004\000" + end_stream: true + } +} +actions { + advance_time { + milliseconds: 14848 + } +} +actions { + on_write { + data: "=" + end_stream: true + } +} +actions { + on_write { + data: "\004\000" + end_stream: true + } +} diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mysql_proxy_1 b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mysql_proxy_1 new file mode 100644 index 0000000000000..f58ad110b8b9d --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/mysql_proxy_1 @@ -0,0 +1,86 @@ +config { + name: "envoy.filters.network.mysql_proxy" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.network.mysql_proxy.v3.MySQLProxy" + value: "\n\006#\336\215\302\246\001" + } +} +actions { + on_write { + data: "\031\031\031\031" + } +} +actions { + on_write { + data: "\031\031\031\031\031\031\031\031" + end_stream: true + } +} +actions { + on_write { + data: "3" + } +} +actions { + on_write { + data: "#" + } +} +actions { + on_write { + data: "#" + end_stream: true + } +} +actions { + on_write { + data: "3" + } +} +actions { + on_write { + data: "#" + end_stream: true + } +} +actions { + on_write { + data: "#" + } +} +actions { + on_write { + data: "#" + } +} +actions { + on_write { + data: "\031\031\031\031\031\031\031\031" + end_stream: true + } +} +actions { + on_write { + end_stream: true + } +} +actions { + on_write { + end_stream: true + } +} +actions { + on_write { + data: "3" + } +} +actions { + on_write { + end_stream: true + } +} +actions { + on_write { + data: "3" + } +} diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_1 b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_1 new file mode 100644 index 0000000000000..2e2e6c1bfb8dc --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_1 @@ -0,0 +1,17 @@ +config { + name: "envoy.filters.network.zookeeper_proxy" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.network.zookeeper_proxy.v3.ZooKeeperProxy" + value: "\nVtype.googleapis.com/envoy.extensions.filters.network.zookeeper_proxy.v3.ZooKeeperProxy\032\000" + } +} +actions { + on_write { + data: "\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030c.googlers.com\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030\030" + } +} +actions { + on_write { + } +} + diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_assert_failure_onwrite b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_assert_failure_onwrite new file mode 100644 index 0000000000000..ae270c6fe26cc --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_corpus/zookeeper_proxy_assert_failure_onwrite @@ -0,0 +1,12 @@ +config { + name: "envoy.filters.network.zookeeper_proxy" + typed_config { + type_url: "type.googleapis.com/envoy.extensions.filters.network.zookeeper_proxy.v3.ZooKeeperProxy" + value: "\nVtype.googleapis.com/envoy.extensions.filters.network.zookeeper_proxy.v3.ZooKeeperProxy\022\001!\032\006\010\377\376\377\317\017" + } +} +actions { + on_write { + data: "\030\030\030\030\030\030\030\030" + } +} diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.proto b/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.proto new file mode 100644 index 0000000000000..77de32b5858f8 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.proto @@ -0,0 +1,31 @@ +syntax = "proto3"; + +package test.extensions.filters.network; +import "validate/validate.proto"; +import "envoy/config/listener/v3/listener_components.proto"; + +message OnWrite { + bytes data = 1; + bool end_stream = 2; +} + +message AdvanceTime { + // Advance the system time by (0,24] hours. + uint32 milliseconds = 1 [(validate.rules).uint32 = {gt: 0 lt: 86400000}]; +} + +message WriteAction { + oneof action_selector { + option (validate.required) = true; + // Call onWrite() + OnWrite on_write = 2; + // Advance time_source_ + AdvanceTime advance_time = 3; + } +} + +message FilterFuzzTestCase { + // This is actually a protobuf type for the config of network filters. + envoy.config.listener.v3.Filter config = 1; + repeated WriteAction actions = 2; +} diff --git a/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz_test.cc b/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz_test.cc new file mode 100644 index 0000000000000..702cb4078db46 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz_test.cc @@ -0,0 +1,58 @@ +#include "common/config/utility.h" +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/well_known_names.h" + +#include "test/config/utility.h" +#include "test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.pb.validate.h" +#include "test/extensions/filters/network/common/fuzz/uber_writefilter.h" +#include "test/fuzz/fuzz_runner.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +DEFINE_PROTO_FUZZER(const test::extensions::filters::network::FilterFuzzTestCase& input) { + ABSL_ATTRIBUTE_UNUSED static PostProcessorRegistration reg = { + [](test::extensions::filters::network::FilterFuzzTestCase* input, unsigned int seed) { + // This post-processor mutation is applied only when libprotobuf-mutator + // calls mutate on an input, and *not* during fuzz target execution. + // Replaying a corpus through the fuzzer will not be affected by the + // post-processor mutation. + + // TODO(jianwendong): consider using a factory to store the names of all + // writeFilters. + static const auto filter_names = UberWriteFilterFuzzer::filterNames(); + static const auto factories = Registry::FactoryRegistry< + Server::Configuration::NamedNetworkFilterConfigFactory>::factories(); + // Choose a valid filter name. + if (std::find(filter_names.begin(), filter_names.end(), input->config().name()) == + std::end(filter_names)) { + absl::string_view filter_name = filter_names[seed % filter_names.size()]; + input->mutable_config()->set_name(std::string(filter_name)); + } + // Set the corresponding type_url for Any. + auto& factory = factories.at(input->config().name()); + input->mutable_config()->mutable_typed_config()->set_type_url( + absl::StrCat("type.googleapis.com/", + factory->createEmptyConfigProto()->GetDescriptor()->full_name())); + }}; + try { + TestUtility::validate(input); + // Check the filter's name in case some filters are not supported yet. + // TODO(jianwendong): remove this if block when we have a factory for writeFilters. + static const auto filter_names = UberWriteFilterFuzzer::filterNames(); + if (std::find(filter_names.begin(), filter_names.end(), input.config().name()) == + std::end(filter_names)) { + ENVOY_LOG_MISC(debug, "Test case with unsupported filter type: {}", input.config().name()); + return; + } + static UberWriteFilterFuzzer fuzzer; + fuzzer.fuzz(input.config(), input.actions()); + } catch (const ProtoValidationException& e) { + ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what()); + } +} + +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/network/common/fuzz/uber_per_writefilter.cc b/test/extensions/filters/network/common/fuzz/uber_per_writefilter.cc new file mode 100644 index 0000000000000..911caa250c522 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/uber_per_writefilter.cc @@ -0,0 +1,35 @@ +#include "extensions/filters/network/common/utility.h" +#include "extensions/filters/network/well_known_names.h" + +#include "test/extensions/filters/network/common/fuzz/uber_writefilter.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +std::vector UberWriteFilterFuzzer::filterNames() { + // These filters have already been covered by this fuzzer. + // Will extend to cover other network filters one by one. + static std::vector filter_names; + if (filter_names.empty()) { + const auto factories = Registry::FactoryRegistry< + Server::Configuration::NamedNetworkFilterConfigFactory>::factories(); + const std::vector supported_filter_names = { + NetworkFilterNames::get().ZooKeeperProxy, NetworkFilterNames::get().KafkaBroker, + NetworkFilterNames::get().MongoProxy, NetworkFilterNames::get().MySQLProxy + // TODO(jianwendong) Add "NetworkFilterNames::get().Postgres" after it supports untrusted + // data. + }; + for (auto& filter_name : supported_filter_names) { + if (factories.contains(filter_name)) { + filter_names.push_back(filter_name); + } else { + ENVOY_LOG_MISC(debug, "Filter name not found in the factory: {}", filter_name); + } + } + } + return filter_names; +} + +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/common/fuzz/uber_writefilter.cc b/test/extensions/filters/network/common/fuzz/uber_writefilter.cc new file mode 100644 index 0000000000000..517429a1dd4bd --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/uber_writefilter.cc @@ -0,0 +1,123 @@ +#include "test/extensions/filters/network/common/fuzz/uber_writefilter.h" + +#include "common/config/utility.h" +#include "common/config/version_converter.h" + +using testing::_; +using testing::Return; + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +void UberWriteFilterFuzzer::reset() { + // Reset the state of dependencies so that a new fuzz input starts in a clean state. + + // Close the connection to make sure the filter's callback is set to nullptr. + write_filter_callbacks_->connection_.raiseEvent(Network::ConnectionEvent::LocalClose); + // Clear the filter's raw pointer stored inside the connection_ and reset the connection_'s state. + write_filter_callbacks_->connection_.callbacks_.clear(); + write_filter_callbacks_->connection_.bytes_sent_callbacks_.clear(); + write_filter_callbacks_->connection_.state_ = Network::Connection::State::Open; + // Clear the pointers inside the mock_dispatcher + Event::MockDispatcher& mock_dispatcher = + dynamic_cast(write_filter_callbacks_->connection_.dispatcher_); + mock_dispatcher.clearDeferredDeleteList(); + write_filter_.reset(); +} + +void UberWriteFilterFuzzer::fuzzerSetup() { + // Setup process when this fuzzer object is constructed. + // For a static fuzzer, this will only be executed once. + + // Get the pointer of write_filter when the write_filter is being added to connection_. + write_filter_callbacks_ = std::make_shared>(); + read_filter_callbacks_ = std::make_shared>(); + ON_CALL(write_filter_callbacks_->connection_, addWriteFilter(_)) + .WillByDefault(Invoke([&](Network::WriteFilterSharedPtr write_filter) -> void { + write_filter->initializeWriteFilterCallbacks(*write_filter_callbacks_); + write_filter_ = write_filter; + })); + ON_CALL(write_filter_callbacks_->connection_, addFilter(_)) + .WillByDefault(Invoke([&](Network::FilterSharedPtr filter) -> void { + filter->initializeReadFilterCallbacks(*read_filter_callbacks_); + filter->initializeWriteFilterCallbacks(*write_filter_callbacks_); + write_filter_ = filter; + })); + factory_context_.prepareSimulatedSystemTime(); + + // Set featureEnabled for mongo_proxy + ON_CALL(factory_context_.runtime_loader_.snapshot_, featureEnabled("mongo.proxy_enabled", 100)) + .WillByDefault(Return(true)); + ON_CALL(factory_context_.runtime_loader_.snapshot_, + featureEnabled("mongo.connection_logging_enabled", 100)) + .WillByDefault(Return(true)); + ON_CALL(factory_context_.runtime_loader_.snapshot_, featureEnabled("mongo.logging_enabled", 100)) + .WillByDefault(Return(true)); + + // Set featureEnabled for thrift_proxy + ON_CALL(factory_context_.runtime_loader_.snapshot_, + featureEnabled("ratelimit.thrift_filter_enabled", 100)) + .WillByDefault(Return(true)); + ON_CALL(factory_context_.runtime_loader_.snapshot_, + featureEnabled("ratelimit.thrift_filter_enforcing", 100)) + .WillByDefault(Return(true)); + ON_CALL(factory_context_.runtime_loader_.snapshot_, + featureEnabled("ratelimit.test_key.thrift_filter_enabled", 100)) + .WillByDefault(Return(true)); +} + +UberWriteFilterFuzzer::UberWriteFilterFuzzer() + : time_source_(factory_context_.simulatedTimeSystem()) { + fuzzerSetup(); +} + +void UberWriteFilterFuzzer::fuzz( + const envoy::config::listener::v3::Filter& proto_config, + const Protobuf::RepeatedPtrField<::test::extensions::filters::network::WriteAction>& actions) { + try { + // Try to create the filter callback(cb_). Exit early if the config is invalid or violates PGV + // constraints. + const std::string& filter_name = proto_config.name(); + ENVOY_LOG_MISC(debug, "filter name {}", filter_name); + auto& factory = Config::Utility::getAndCheckFactoryByName< + Server::Configuration::NamedNetworkFilterConfigFactory>(filter_name); + ProtobufTypes::MessagePtr message = Config::Utility::translateToFactoryConfig( + proto_config, factory_context_.messageValidationVisitor(), factory); + ENVOY_LOG_MISC(debug, "Config content after decoded: {}", message->DebugString()); + cb_ = factory.createFilterFactoryFromProto(*message, factory_context_); + // Add filter to connection_. + cb_(write_filter_callbacks_->connection_); + } catch (const EnvoyException& e) { + ENVOY_LOG_MISC(debug, "Controlled exception in filter setup {}", e.what()); + return; + } + for (const auto& action : actions) { + ENVOY_LOG_MISC(debug, "action {}", action.DebugString()); + switch (action.action_selector_case()) { + case test::extensions::filters::network::WriteAction::kOnWrite: { + ASSERT(write_filter_ != nullptr); + Buffer::OwnedImpl buffer(action.on_write().data()); + write_filter_->onWrite(buffer, action.on_write().end_stream()); + + break; + } + case test::extensions::filters::network::WriteAction::kAdvanceTime: { + time_source_.advanceTimeAsync( + std::chrono::milliseconds(action.advance_time().milliseconds())); + factory_context_.dispatcher().run(Event::Dispatcher::RunType::NonBlock); + break; + } + default: { + // Unhandled actions. + ENVOY_LOG_MISC(debug, "Action support is missing for:\n{}", action.DebugString()); + PANIC("A case is missing for an action"); + } + } + } + + reset(); +} + +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/common/fuzz/uber_writefilter.h b/test/extensions/filters/network/common/fuzz/uber_writefilter.h new file mode 100644 index 0000000000000..9f6c34eb60e93 --- /dev/null +++ b/test/extensions/filters/network/common/fuzz/uber_writefilter.h @@ -0,0 +1,40 @@ +#include "envoy/network/filter.h" + +#include "common/protobuf/protobuf.h" + +#include "test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.pb.validate.h" +#include "test/extensions/filters/network/common/fuzz/utils/fakes.h" +#include "test/mocks/network/mocks.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { + +class UberWriteFilterFuzzer { +public: + UberWriteFilterFuzzer(); + // This creates the filter config and runs the fuzzed data against the filter. + void fuzz( + const envoy::config::listener::v3::Filter& proto_config, + const Protobuf::RepeatedPtrField<::test::extensions::filters::network::WriteAction>& actions); + // Get the name of filters which has been covered by this fuzzer. + static std::vector filterNames(); + +protected: + // Set-up filter specific mock expectations in constructor. + void fuzzerSetup(); + // Reset the states of the mock objects. + void reset(); + +private: + Server::Configuration::FakeFactoryContext factory_context_; + Event::SimulatedTimeSystem& time_source_; + Network::WriteFilterSharedPtr write_filter_; + Network::FilterFactoryCb cb_; + std::shared_ptr> write_filter_callbacks_; + std::shared_ptr> read_filter_callbacks_; +}; + +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy