diff --git a/test/common/upstream/BUILD b/test/common/upstream/BUILD index b6b19139a7b55..d274a2be8867b 100644 --- a/test/common/upstream/BUILD +++ b/test/common/upstream/BUILD @@ -242,6 +242,55 @@ envoy_cc_test( ], ) +envoy_cc_test_library( + name = "load_balancer_fuzz_lib", + srcs = ["load_balancer_fuzz_base.cc"], + hdrs = ["load_balancer_fuzz_base.h"], + deps = [ + ":load_balancer_fuzz_proto_cc_proto", + ":utility_lib", + "//source/common/upstream:load_balancer_lib", + "//test/mocks:common_lib", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/upstream:cluster_info_mocks", + "//test/mocks/upstream:host_set_mocks", + "//test/mocks/upstream:load_balancer_context_mock", + "//test/mocks/upstream:priority_set_mocks", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + ], +) + +envoy_proto_library( + name = "load_balancer_fuzz_proto", + srcs = ["load_balancer_fuzz.proto"], + deps = [ + "//test/fuzz:common_proto", + "@envoy_api//envoy/config/cluster/v3:pkg", + ], +) + +envoy_proto_library( + name = "least_request_load_balancer_fuzz_proto", + srcs = ["least_request_load_balancer_fuzz.proto"], + deps = [ + "//test/common/upstream:load_balancer_fuzz_proto", + "@envoy_api//envoy/config/cluster/v3:pkg", + ], +) + +envoy_cc_fuzz_test( + name = "least_request_load_balancer_fuzz_test", + srcs = ["least_request_load_balancer_fuzz_test.cc"], + corpus = "//test/common/upstream:least_request_load_balancer_corpus", + deps = [ + ":least_request_load_balancer_fuzz_proto_cc_proto", + ":load_balancer_fuzz_lib", + ":load_balancer_fuzz_proto_cc_proto", + ":utility_lib", + "//test/fuzz:utility_lib", + ], +) + envoy_cc_test( name = "load_balancer_simulation_test", srcs = ["load_balancer_simulation_test.cc"], diff --git a/test/common/upstream/least_request_load_balancer_corpus/least_request-Normal b/test/common/upstream/least_request_load_balancer_corpus/least_request-Normal new file mode 100644 index 0000000000000..4d03261c1f438 --- /dev/null +++ b/test/common/upstream/least_request_load_balancer_corpus/least_request-Normal @@ -0,0 +1,34 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + failover_host_set: false + num_healthy_hosts: 2 + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +num_hosts_in_priority_set: 2 +num_hosts_in_failover_set: 0 +bytestring_for_random_calls: 0 +bytestring_for_random_calls: 2 +bytestring_for_random_calls: 3 +} +least_request_lb_config { + +} diff --git a/test/common/upstream/least_request_load_balancer_corpus/least_request-SingleHost b/test/common/upstream/least_request_load_balancer_corpus/least_request-SingleHost new file mode 100644 index 0000000000000..794017cf62c5f --- /dev/null +++ b/test/common/upstream/least_request_load_balancer_corpus/least_request-SingleHost @@ -0,0 +1,29 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + failover_host_set: false + num_healthy_hosts: 1 + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +num_hosts_in_priority_set: 1 +num_hosts_in_failover_set: 0 +bytestring_for_random_calls: 0 +bytestring_for_random_calls: 2 +bytestring_for_random_calls: 3 +} +least_request_lb_config { + +} diff --git a/test/common/upstream/least_request_load_balancer_corpus/least_request_NoHosts b/test/common/upstream/least_request_load_balancer_corpus/least_request_NoHosts new file mode 100644 index 0000000000000..991aff91b2dcb --- /dev/null +++ b/test/common/upstream/least_request_load_balancer_corpus/least_request_NoHosts @@ -0,0 +1,21 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +num_hosts_in_priority_set: 0 +num_hosts_in_failover_set: 0 +bytestring_for_random_calls: 0 +} +least_request_lb_config { + +} diff --git a/test/common/upstream/least_request_load_balancer_fuzz.proto b/test/common/upstream/least_request_load_balancer_fuzz.proto new file mode 100644 index 0000000000000..74bb94a46f6bb --- /dev/null +++ b/test/common/upstream/least_request_load_balancer_fuzz.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package test.common.upstream; + +import "validate/validate.proto"; +import "test/common/upstream/load_balancer_fuzz.proto"; +import "envoy/config/cluster/v3/cluster.proto"; + +//Least request load balancing takes in a more specific least request proto config, TODO: and also stats config in terms of weights +message LeastRequestLoadBalancerTestCase { + LoadBalancerTestCase load_balancer_test_case = 1 [(validate.rules).message.required = true]; + //Specific least_request_config isn't required, can even be a nullptr in constructor + envoy.config.cluster.v3.Cluster.LeastRequestLbConfig least_request_lb_config = 2; +} diff --git a/test/common/upstream/least_request_load_balancer_fuzz_test.cc b/test/common/upstream/least_request_load_balancer_fuzz_test.cc new file mode 100644 index 0000000000000..3af1acaab84ee --- /dev/null +++ b/test/common/upstream/least_request_load_balancer_fuzz_test.cc @@ -0,0 +1,37 @@ +#include + +#include "test/common/upstream/least_request_load_balancer_fuzz.pb.validate.h" +#include "test/common/upstream/load_balancer_fuzz_base.h" +#include "test/fuzz/fuzz_runner.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Upstream { + +DEFINE_PROTO_FUZZER(const test::common::upstream::LeastRequestLoadBalancerTestCase input) { + try { + TestUtility::validate(input); + } catch (const ProtoValidationException& e) { + ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what()); + return; + } + + std::unique_ptr load_balancer_fuzz = + std::make_unique(); + load_balancer_fuzz->initializeLbComponents(input.load_balancer_test_case()); + + try { + load_balancer_fuzz->lb_ = std::make_unique( + load_balancer_fuzz->priority_set_, nullptr, load_balancer_fuzz->stats_, + load_balancer_fuzz->runtime_, load_balancer_fuzz->random_, + input.load_balancer_test_case().common_lb_config(), input.least_request_lb_config()); + } catch (EnvoyException& e) { + ENVOY_LOG_MISC(debug, "EnvoyException; {}", e.what()); + return; + } + + load_balancer_fuzz->replay(input.load_balancer_test_case().actions()); +} + +} // namespace Upstream +} // namespace Envoy diff --git a/test/common/upstream/load_balancer_fuzz.proto b/test/common/upstream/load_balancer_fuzz.proto new file mode 100644 index 0000000000000..84b53ab93fd05 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package test.common.upstream; + +import "validate/validate.proto"; +import "envoy/config/cluster/v3/cluster.proto"; +import "google/protobuf/empty.proto"; + +message UpdateHealthFlags { + bool failover_host_set = 1; + uint32 num_healthy_hosts = 2; + uint32 num_degraded_hosts = 3; + uint32 num_excluded_hosts = 4; +} + +message LbAction { + oneof action_selector { + option (validate.required) = true; + UpdateHealthFlags update_health_flags = 1; + google.protobuf.Empty prefetch = 2; + google.protobuf.Empty choose_host = 3; + } +} + +//This message represents what LoadBalancerFuzzBase will interact with, performing setup of host sets and calling into load balancers. +//The logic that this message represents and the base class for load balancing fuzzing will be logic that maps to all types of load balancing +//and can be modularly used at the highest level for each load balancer. +message LoadBalancerTestCase { + envoy.config.cluster.v3.Cluster.CommonLbConfig common_lb_config = 1 + [(validate.rules).message.required = true]; + repeated LbAction actions = 2; + //This is capped at the max port value on an ip address - 80 + uint32 num_hosts_in_priority_set = 3 [(validate.rules).uint32.lt = 65455]; + uint32 num_hosts_in_failover_set = 4 [(validate.rules).uint32.lt = 65455]; + repeated uint64 bytestring_for_random_calls = 5 + [(validate.rules).repeated = {min_items: 1, max_items: 50}]; +} diff --git a/test/common/upstream/load_balancer_fuzz_base.cc b/test/common/upstream/load_balancer_fuzz_base.cc new file mode 100644 index 0000000000000..cef0e286aa508 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz_base.cc @@ -0,0 +1,115 @@ +#include "test/common/upstream/load_balancer_fuzz_base.h" + +#include "test/common/upstream/utility.h" + +namespace Envoy { + +namespace Random { +uint64_t FakeRandomGenerator::random() { + uint8_t index_of_data = counter % bytestring_.size(); + ++counter; + ENVOY_LOG_MISC(trace, "random() returned: {}", bytestring_.at(index_of_data)); + return bytestring_.at(index_of_data); +} +} // namespace Random + +namespace Upstream { + +// Anonymous namespace for helper functions +namespace { +std::vector +constructByteVectorForRandom(const Protobuf::RepeatedField& byteString) { + std::vector byteVector; + for (int i = 0; i < byteString.size(); ++i) { + byteVector.push_back(byteString.at(i)); + } + return byteVector; +} +} // namespace + +void LoadBalancerFuzzBase::initializeFixedHostSets(uint32_t num_hosts_in_priority_set, + uint32_t num_hosts_in_failover_set) { + int port = 80; + for (uint32_t i = 0; i < num_hosts_in_priority_set; ++i) { + host_set_.hosts_.push_back(makeTestHost(info_, "tcp://127.0.0.1:" + std::to_string(port))); + ++port; + } + for (uint32_t i = 0; i < num_hosts_in_failover_set; ++i) { + failover_host_set_.hosts_.push_back( + makeTestHost(info_, "tcp://127.0.0.1:" + std::to_string(port))); + ++port; + } + // TODO: More than two hosts? +} + +// Initializes random and fixed host sets +void LoadBalancerFuzzBase::initializeLbComponents( + test::common::upstream::LoadBalancerTestCase input) { + random_.bytestring_ = constructByteVectorForRandom(input.bytestring_for_random_calls()); + initializeFixedHostSets(input.num_hosts_in_priority_set(), input.num_hosts_in_failover_set()); +} + +// Updating host sets is shared amongst all the load balancer tests. Since logically, we're just +// setting the mock priority set to have certain values, and all load balancers interface with host +// sets and their health statuses, this action maps to all load balancers. +void LoadBalancerFuzzBase::updateHealthFlagsForAHostSet(bool failover_host_set, + uint32_t num_healthy_hosts, + uint32_t num_degraded_hosts, + uint32_t num_excluded_hosts) { + MockHostSet& host_set = *priority_set_.getMockHostSet(int(failover_host_set)); + uint32_t i = 0; + for (; i < num_healthy_hosts && i < host_set.hosts_.size(); ++i) { + host_set.healthy_hosts_.push_back(host_set.hosts_[i]); + } + for (; i < (num_healthy_hosts + num_degraded_hosts) && i < host_set.hosts_.size(); ++i) { + host_set.degraded_hosts_.push_back(host_set.hosts_[i]); + } + + for (; i < (num_healthy_hosts + num_degraded_hosts + num_excluded_hosts) && + i < host_set.hosts_.size(); + ++i) { + host_set.excluded_hosts_.push_back(host_set.hosts_[i]); + } + + host_set.runCallbacks({}, {}); +} + +void LoadBalancerFuzzBase::prefetch() { + // TODO: context, could generate it in proto action + lb_->peekAnotherHost(nullptr); +} + +void LoadBalancerFuzzBase::chooseHost() { + // TODO: context, could generate it in proto action + lb_->chooseHost(nullptr); +} + +void LoadBalancerFuzzBase::replay( + const Protobuf::RepeatedPtrField& actions) { + constexpr auto max_actions = 64; + for (int i = 0; i < std::min(max_actions, actions.size()); ++i) { + const auto& event = actions.at(i); + ENVOY_LOG_MISC(trace, "Action: {}", event.DebugString()); + switch (event.action_selector_case()) { + case test::common::upstream::LbAction::kUpdateHealthFlags: { + updateHealthFlagsForAHostSet(event.update_health_flags().failover_host_set(), + event.update_health_flags().num_degraded_hosts(), + event.update_health_flags().num_excluded_hosts()); + break; + } + case test::common::upstream::LbAction::kPrefetch: { + prefetch(); + break; + } + case test::common::upstream::LbAction::kChooseHost: { + chooseHost(); + break; + } + default: + break; + } + } +} + +} // namespace Upstream +} // namespace Envoy diff --git a/test/common/upstream/load_balancer_fuzz_base.h b/test/common/upstream/load_balancer_fuzz_base.h new file mode 100644 index 0000000000000..5fba94c393cf4 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz_base.h @@ -0,0 +1,74 @@ +#include "envoy/config/cluster/v3/cluster.pb.h" + +#include "common/upstream/load_balancer_impl.h" + +#include "test/common/upstream/load_balancer_fuzz.pb.validate.h" +#include "test/mocks/common.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/upstream/cluster_info.h" +#include "test/mocks/upstream/host_set.h" +#include "test/mocks/upstream/load_balancer_context.h" +#include "test/mocks/upstream/priority_set.h" + +namespace Envoy { + +namespace Random { +class FakeRandomGenerator : public RandomGenerator { +public: + FakeRandomGenerator() = default; + ~FakeRandomGenerator() override = default; + + // RandomGenerator + uint64_t random() override; // returns a part of the generated bytestring + std::string uuid() override { return ""; } + uint32_t counter = 0; // Will get modded against byte string length for byte string index + std::vector bytestring_; // String of bytes that returns on random() calls +}; +} // namespace Random + +namespace Upstream { + +// This class implements replay logic, and also handles the initial setup of static host sets and +// the subsequent updates to those sets. +class LoadBalancerFuzzBase { +public: + LoadBalancerFuzzBase() + : stats_(ClusterInfoImpl::generateStats(stats_store_)){ + + }; + + // Untrusted upstreams don't have the ability to change the host set size, so keep it constant + // over the fuzz iteration. + void initializeFixedHostSets(uint32_t num_hosts_in_priority_set, + uint32_t num_hosts_in_failover_set); + + // Initializes load balancer components shared amongst every load balancer, random_, and + // priority_set_ + void initializeLbComponents(test::common::upstream::LoadBalancerTestCase input); + void updateHealthFlagsForAHostSet(bool failover_host_set, uint32_t num_healthy_hosts, + uint32_t num_degraded_hosts = 0, + uint32_t num_excluded_hosts = 0); + // These two actions have a lot of logic attached to them. However, all the logic that the load + // balancer needs to run its algorithm is already encapsulated within the load balancer. Thus, + // once the load balancer is constructed, all this class has to do is call lb_->peekAnotherHost() + // and lb_->chooseHost(). + void prefetch(); + void chooseHost(); + ~LoadBalancerFuzzBase() = default; + void replay(const Protobuf::RepeatedPtrField& actions); + + // These public objects shared amongst all types of load balancers will be used to construct load + // balancers in specific load balancer fuzz classes + Stats::IsolatedStoreImpl stats_store_; + ClusterStats stats_; + NiceMock runtime_; + Random::FakeRandomGenerator random_; + NiceMock priority_set_; + MockHostSet& host_set_ = *priority_set_.getMockHostSet(0); + MockHostSet& failover_host_set_ = *priority_set_.getMockHostSet(1); + std::shared_ptr info_{new NiceMock()}; + std::unique_ptr lb_; +}; + +} // namespace Upstream +} // namespace Envoy diff --git a/test/common/upstream/random_load_balancer_corpus/random_NoHosts b/test/common/upstream/random_load_balancer_corpus/random_NoHosts new file mode 100644 index 0000000000000..d75b02137df96 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_NoHosts @@ -0,0 +1,18 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +num_hosts_in_priority_set: 0 +num_hosts_in_failover_set: 0 +bytestring_for_random_calls: 0 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_Normal b/test/common/upstream/random_load_balancer_corpus/random_Normal new file mode 100644 index 0000000000000..725f4fc14a20e --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_Normal @@ -0,0 +1,35 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + failover_host_set: false + num_healthy_hosts: 2 + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +num_hosts_in_priority_set: 2 +num_hosts_in_failover_set: 0 +bytestring_for_random_calls: 2 +bytestring_for_random_calls: 3 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb b/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb new file mode 100644 index 0000000000000..543b4422a730b --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb @@ -0,0 +1,33 @@ +load_balancer_test_case { +common_lb_config { + healthy_panic_threshold { + value: 2.12199579096527e-314 + } + locality_weighted_lb_config { + } +} +actions { + choose_host { + } +} +actions { + prefetch { + } +} +actions { + prefetch { + } +} +actions { + choose_host { + } +} +actions { + choose_host { + } +} +num_hosts_in_priority_set: 2 +num_hosts_in_failover_set: 9007199258945536 +bytestring_for_random_calls: 2 +bytestring_for_random_calls: 3 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_largest-port-value b/test/common/upstream/random_load_balancer_corpus/random_largest-port-value new file mode 100644 index 0000000000000..7df28b458063c --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_largest-port-value @@ -0,0 +1,29 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + choose_host { + } +} +actions { + prefetch { + } +} +actions { + prefetch { + } +} +actions { + choose_host { + } +} +actions { + choose_host { + } +} +num_hosts_in_priority_set: 65455 +num_hosts_in_failover_set: 65455 +bytestring_for_random_calls: 2 +bytestring_for_random_calls: 3 +}