From 90436b31dd270cfaf2a01357c1a19012485b84eb Mon Sep 17 00:00:00 2001 From: jonathan-dilorenzo Date: Wed, 3 Jan 2024 14:59:33 -0800 Subject: [PATCH] [fuzzer] Add ability to fuzz multicast group entries and entries that refer to multicast group entries. PiperOrigin-RevId: 595512074 --- p4_fuzzer/BUILD.bazel | 4 + p4_fuzzer/fuzz_util.cc | 204 ++++++++++++++---- p4_fuzzer/fuzz_util.h | 12 ++ p4_fuzzer/fuzz_util_test.cc | 194 +++++++++++++++++ p4_fuzzer/switch_state.cc | 198 ++++++++++++++++- p4_fuzzer/switch_state.h | 31 ++- ...assert_entry_equality_test.expected.output | 5 + ...state_assert_entry_equality_test_runner.cc | 27 ++- p4_fuzzer/switch_state_test.cc | 86 ++++++-- 9 files changed, 693 insertions(+), 68 deletions(-) diff --git a/p4_fuzzer/BUILD.bazel b/p4_fuzzer/BUILD.bazel index 429358e3..6ae2b42b 100644 --- a/p4_fuzzer/BUILD.bazel +++ b/p4_fuzzer/BUILD.bazel @@ -120,11 +120,13 @@ cc_library( "//gutil:collections", "//gutil:status", "//lib/p4rt:p4rt_port", + "//p4_pdpi:built_ins", "//p4_pdpi:entity_keys", "//p4_pdpi:ir_cc_proto", "//p4_pdpi:references", "//p4_pdpi/internal:ordered_map", "//p4_pdpi/netaddr:ipv6_address", + "//p4_pdpi/string_encodings:byte_string", "//p4_pdpi/utils:ir", "@com_github_google_glog//:glog", "@com_github_p4lang_p4_constraints//p4_constraints/backend:constraint_info", @@ -163,6 +165,8 @@ cc_test( "//gutil:collections", "//gutil:proto_matchers", "//gutil:status_matchers", + "//gutil:testing", + "//p4_pdpi:built_ins", "//p4_pdpi:ir_cc_proto", "@com_github_google_glog//:glog", "@com_github_p4lang_p4runtime//:p4info_cc_proto", diff --git a/p4_fuzzer/fuzz_util.cc b/p4_fuzzer/fuzz_util.cc index ba933d1c..a0181bbf 100644 --- a/p4_fuzzer/fuzz_util.cc +++ b/p4_fuzzer/fuzz_util.cc @@ -35,7 +35,6 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" -#include "absl/strings/substitute.h" #include "absl/types/span.h" #include "google/protobuf/repeated_field.h" #include "google/protobuf/repeated_ptr_field.h" @@ -50,23 +49,25 @@ #include "p4_fuzzer/fuzzer_config.h" #include "p4_fuzzer/mutation.h" #include "p4_fuzzer/switch_state.h" +#include "p4_pdpi/built_ins.h" #include "p4_pdpi/entity_keys.h" #include "p4_pdpi/internal/ordered_map.h" #include "p4_pdpi/ir.pb.h" #include "p4_pdpi/netaddr/ipv6_address.h" #include "p4_pdpi/references.h" +#include "p4_pdpi/string_encodings/byte_string.h" #include "p4_pdpi/utils/ir.h" -#include "p4_pdpi/entity_keys.h" namespace p4_fuzzer { using ::absl::gntohll; using ::absl::Uniform; -using ::pdpi::TableEntryKey; using ::p4::v1::Action; using ::p4::v1::Entity; using ::p4::v1::FieldMatch; +using ::p4::v1::MulticastGroupEntry; +using ::p4::v1::Replica; using ::p4::v1::TableEntry; using ::p4::v1::Update; using ::pdpi::IrTableDefinition; @@ -233,39 +234,66 @@ CreateReferenceMapping( const google::protobuf::RepeatedPtrField& references) { absl::flat_hash_map reference_map; for (const IrTableReference& reference : references) { - if (reference.destination_table().has_p4_table()) { - if (reference.destination_table().p4_table().table_name() == - "router_interface_table" && - reference.source_table().p4_table().table_name() != - "neighbor_table") { - // TODO: b/317404235 - Remove once a less "baked-in" way of masking this - // is found. - // This is a mask that is consistent with `CheckReferenceAssumptions`, - // which is used when creating the `FuzzerConfig`. - continue; - } + Entity referenced_entity; + switch (reference.destination_table().table_case()) { + case pdpi::IrTable::kP4Table: { + if (reference.destination_table().p4_table().table_name() == + "router_interface_table" && + reference.source_table().p4_table().table_name() != + "neighbor_table") { + // TODO: b/317404235 - Remove once a less "baked-in" way of masking + // this is found. This is a mask that is consistent with + // `CheckReferenceAssumptions`, which is used when creating the + // `FuzzerConfig`. + continue; + } + + if (switch_state.IsTableEmpty( + reference.destination_table().p4_table().table_id())) { + return gutil::FailedPreconditionErrorBuilder() + << "Table with id " + << reference.destination_table().p4_table().table_id() + << " is empty. Cannot currently generate references to table."; + } - if (switch_state.IsTableEmpty( - reference.destination_table().p4_table().table_id())) { - return gutil::FailedPreconditionErrorBuilder() - << "Table with id " - << reference.destination_table().p4_table().table_id() - << " is empty. Cannot currently generate references to table."; + *referenced_entity.mutable_table_entry() = UniformValueFromMap( + gen, switch_state.GetTableEntries( + reference.destination_table().p4_table().table_id())); + break; } + case pdpi::IrTable::kBuiltInTable: { + if (reference.destination_table().built_in_table() != + pdpi::BUILT_IN_TABLE_MULTICAST_GROUP_TABLE) { + return gutil::UnimplementedErrorBuilder() + << "Only built-in destination table of type " + "BUILT_IN_TABLE_MULTICAST_GROUP_TABLE is supported, got: " + << reference.DebugString(); + } - Entity referenced_entity; - *referenced_entity.mutable_table_entry() = UniformValueFromMap( - gen, switch_state.GetTableEntries( - reference.destination_table().p4_table().table_id())); - ASSIGN_OR_RETURN( - absl::flat_hash_set concrete_references, - pdpi::PossibleIncomingConcreteTableReferences(reference, - referenced_entity)); - for (const pdpi::ConcreteTableReference& concrete_reference : - concrete_references) { - for (const auto& field : concrete_reference.fields) { - reference_map[field.source_field] = field.value; + if (switch_state.GetMulticastGroupEntries().empty()) { + return gutil::FailedPreconditionErrorBuilder() + << "Multicast group table is empty. Cannot currently " + "generate references."; } + *referenced_entity.mutable_packet_replication_engine_entry() + ->mutable_multicast_group_entry() = + UniformValueFromMap(gen, switch_state.GetMulticastGroupEntries()); + break; + } + default: { + return gutil::InvalidArgumentErrorBuilder() + << "Destination table type in reference info is not known: " + << reference.DebugString(); + } + } + ASSIGN_OR_RETURN( + absl::flat_hash_set concrete_references, + pdpi::PossibleIncomingConcreteTableReferences(reference, + referenced_entity)); + for (const pdpi::ConcreteTableReference& concrete_reference : + concrete_references) { + for (const auto& field : concrete_reference.fields) { + reference_map[field.source_field] = field.value; } } } @@ -731,6 +759,7 @@ const std::vector AllValidActions( for (const auto& action : table.entry_actions()) { // Skip deprecated, unused, and disallowed actions. if (pdpi::IsElementDeprecated(action.action().preamble().annotations()) || + action.action().is_unsupported() || IsDisabledForFuzzing(config, action.action().preamble().name())) continue; actions.push_back(action); @@ -1057,7 +1086,6 @@ absl::StatusOr FuzzAction( return action; } - // Gets a set of actions with a skewed distribution of weights, which add up to // at most the max_group_size of the action profile by repeatedly sampling a // uniform weight from 1 to the maximum possible weight remaining. We could @@ -1082,8 +1110,8 @@ absl::StatusOr FuzzActionProfileActionSet( ? action_profile.max_group_size() : kActionProfileActionSetMaxCardinality; int number_of_actions = Uniform( - absl::IntervalClosedClosed, *gen, - config.no_empty_action_profile_groups ? 1 : 0, max_number_of_actions); + absl::IntervalClosedClosed, *gen, + config.no_empty_action_profile_groups ? 1 : 0, max_number_of_actions); // Get the max member weight from the P4Info if it is set. int max_member_weight = @@ -1139,10 +1167,10 @@ absl::StatusOr FuzzActionProfileActionSet( // We want to randomly select some number of actions up to our max // cardinality; however, we can't have more actions than the amount of // weight we support since every action must have weight >= 1. - int number_of_actions = Uniform( - absl::IntervalClosedClosed, *gen, - config.no_empty_action_profile_groups ? 1 : 0, - std::min(unallocated_weight, kActionProfileActionSetMaxCardinality)); + int number_of_actions = Uniform( + absl::IntervalClosedClosed, *gen, + config.no_empty_action_profile_groups ? 1 : 0, + std::min(unallocated_weight, kActionProfileActionSetMaxCardinality)); for (int i = 0; i < number_of_actions; i++) { // Since each action must have at least weight 1, we need to take the @@ -1192,6 +1220,104 @@ absl::StatusOr FuzzAction( return result; } +// Fuzzes `Replica` for a packet replication engine entry (multicast +// group/clone session). `packet_replication_engine_definition` provides +// outgoing references. +absl::StatusOr FuzzReplica(absl::BitGen* gen, + const FuzzerConfig& config, + const SwitchState& switch_state, + const pdpi::IrBuiltInTableDefinition& + packet_replication_engine_definition) { + Replica replica; + + // NOTE: The ability to fuzz references for actions and match fields + // independently is based on an assumption enforced in + // `CheckReferenceAssumptions` when constructing `FuzzerConfig`. Should that + // assumption ever be removed, this code should be updated. + absl::flat_hash_map reference_map; + ASSIGN_OR_RETURN( + reference_map, + CreateReferenceMapping( + gen, switch_state, + packet_replication_engine_definition.outgoing_references())); + + ASSIGN_OR_RETURN( + std::string port_param, + pdpi::IrBuiltInParameterToString( + pdpi::IrBuiltInParameter::BUILT_IN_PARAMETER_REPLICA_PORT)); + if (auto it = reference_map.find(port_param); it != reference_map.end()) { + replica.set_port(it->second); + } else { + replica.set_port(FuzzPort(gen, config).GetP4rtEncoding()); + } + + // Inherited from v1model, see `standard_metadata_t.egress_rid`. + // https://github.com/p4lang/p4c/blob/main/p4include/v1model.p4 + constexpr int kReplicaInstanceBitwidth = 16; + + ASSIGN_OR_RETURN( + std::string instance_param, + pdpi::IrBuiltInParameterToString( + pdpi::IrBuiltInParameter::BUILT_IN_PARAMETER_REPLICA_INSTANCE)); + if (auto it = reference_map.find(instance_param); it != reference_map.end()) { + ASSIGN_OR_RETURN( + auto instance, + pdpi::ByteStringToBitset(it->second)); + replica.set_instance(instance.to_ullong()); + } else { + replica.set_instance(FuzzUint64(gen, kReplicaInstanceBitwidth)); + } + + return replica; +} + +absl::StatusOr FuzzValidMulticastGroupEntry( + absl::BitGen* gen, const FuzzerConfig& config, + const SwitchState& switch_state, + const pdpi::IrBuiltInTableDefinition& multicast_group_table_info) { + if (multicast_group_table_info.built_in_table() != + pdpi::BUILT_IN_TABLE_MULTICAST_GROUP_TABLE) { + return gutil::InvalidArgumentErrorBuilder() + << "Built-in definition does not belong to multicast group table." + << multicast_group_table_info.DebugString(); + } + + MulticastGroupEntry entry; + + // Inherited from v1model , see `standard_metadata_t.mcast_grp`. + // https://github.com/p4lang/p4c/blob/main/p4include/v1model.p4 + constexpr int kMulticastGroupIdWidth = 16; + + // NOTE: References from `multicast_group_id` are not implemented since there + // is no current use case, but an implementation should exist for general + // purposes. + while (entry.multicast_group_id() == 0) { + entry.set_multicast_group_id(FuzzUint64(gen, kMulticastGroupIdWidth)); + } + + // Fuzz 0-64 unique replicas + int replica_fuzz_iterations = FuzzUint64(gen, /*bits=*/6); + absl::flat_hash_set> unique_replicas; + // Depending on references, the number of unique replicas can be capped so + // replicas generated can be less than `replica_fuzz_iterations`. + for (int i = 0; i < replica_fuzz_iterations; ++i) { + // Generate replica. + ASSIGN_OR_RETURN( + Replica replica, + FuzzReplica(gen, config, switch_state, multicast_group_table_info), + _.SetPrepend() << "while fuzzing action: "); + + // Within a given multicast entry, replicas must be unique. + if (unique_replicas + .insert(std::make_pair(replica.instance(), replica.port())) + .second) { + *entry.add_replicas() = std::move(replica); + } + } + + return entry; +}; + // TODO: Optional fields with @refers_to will not be properly // fuzzed if they refer to fields that currently have no existing entry. // Fuzzing fails for this, but it should simply omit the optional. Thankfully, diff --git a/p4_fuzzer/fuzz_util.h b/p4_fuzzer/fuzz_util.h index 9268ca2a..74d3bf6b 100644 --- a/p4_fuzzer/fuzz_util.h +++ b/p4_fuzzer/fuzz_util.h @@ -242,6 +242,18 @@ absl::StatusOr FuzzValidTableEntry( absl::BitGen* gen, const FuzzerConfig& config, const SwitchState& switch_state, const uint32_t table_id); +// Randomly generates a multicast group entry that conforms to the given +// `multicast_group_table_info`. May fail if a reference to another table is +// required. +// ASSUMPTIONS regarding `multicast_group_table_info`: +// 1) `IrBuiltInTableDefinition` has `built_in_table` set to +// `BUILT_IN_TABLE_MULTICAST_GROUP_TABLE`. +// 2) Contains outgoing and incoming references for multicast group table. +absl::StatusOr FuzzValidMulticastGroupEntry( + absl::BitGen* gen, const FuzzerConfig& config, + const SwitchState& switch_state, + const pdpi::IrBuiltInTableDefinition& multicast_group_table_info); + // Randomly generates a set of valid table entries that, when installed in order // to an empty switch state, all install correctly. std::vector ValidForwardingEntries( diff --git a/p4_fuzzer/fuzz_util_test.cc b/p4_fuzzer/fuzz_util_test.cc index 187fbe63..24b2d95a 100644 --- a/p4_fuzzer/fuzz_util_test.cc +++ b/p4_fuzzer/fuzz_util_test.cc @@ -32,11 +32,13 @@ #include "gutil/collections.h" #include "gutil/proto_matchers.h" #include "gutil/status_matchers.h" +#include "gutil/testing.h" #include "p4/config/v1/p4info.pb.h" #include "p4/v1/p4runtime.pb.h" #include "p4_fuzzer/fuzzer.pb.h" #include "p4_fuzzer/fuzzer_config.h" #include "p4_fuzzer/test_utils.h" +#include "p4_pdpi/built_ins.h" #include "p4_pdpi/ir.pb.h" namespace p4_fuzzer { @@ -204,6 +206,162 @@ TEST(FuzzUtilTest, FuzzWriteRequestAreReproducibleWithState) { } } +// This test uses behavior specific to the main.p4 program. In main.p4, +// `refers_to_multicast_by_action_table` is a table that uses an action whose +// parameter refers to multicast group id. +TEST(FuzzUtilTest, FuzzTableFailsWhenNoMulticastReferenceIsAvailable) { + FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); + + // Generate entry referring to multicast and fail due to no references. + pdpi::IrTableDefinition multicast_dependent_definition = + gutil::FindOrDie(fuzzer_state.config.GetIrP4Info().tables_by_name(), + "refers_to_multicast_by_action_table"); + absl::BitGen gen; + EXPECT_THAT( + FuzzValidTableEntry(&gen, fuzzer_state.config, fuzzer_state.switch_state, + multicast_dependent_definition), + gutil::StatusIs(absl::StatusCode::kFailedPrecondition)); +} + +// This test uses behavior specific to the main.p4 program. In main.p4, +// `refers_to_multicast_by_action_table` is a table that uses an action whose +// parameter refers to multicast group id. +TEST(FuzzUtilTest, FuzzTableRespectsMulticastReferences) { + FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); + + // Store multicast group entry being referenced. + ASSERT_OK(fuzzer_state.switch_state.ApplyUpdate( + gutil::ParseProtoOrDie(R"pb( + type: INSERT + entity { + packet_replication_engine_entry { + multicast_group_entry { multicast_group_id: 0x86 } + } + } + )pb"))); + + // Generate entry referring to multicast and ensure the action references + // multicast group id. + pdpi::IrTableDefinition multicast_dependent_definition = + gutil::FindOrDie(fuzzer_state.config.GetIrP4Info().tables_by_name(), + "refers_to_multicast_by_action_table"); + absl::BitGen gen; + EXPECT_THAT( + FuzzValidTableEntry(&gen, fuzzer_state.config, fuzzer_state.switch_state, + multicast_dependent_definition), + IsOkAndHolds(Partially(EqualsProto(R"pb( + action { + action { + # Action id for `refers_to_multicast_action`. + action_id: 18598416 + params { param_id: 1 value: "\x86" } + } + } + )pb")))); +} + +// This test uses behavior specific to the main.p4 program. In main.p4, +// built-in multicast group table replicas refer to fields in +// `referenced_by_multicast_replica_table`. +TEST(FuzzUtilTest, FuzzMulticastRespectsReplicaReferences) { + FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); + + // Store table entry being referenced. + ASSERT_OK(fuzzer_state.switch_state.ApplyUpdate( + gutil::ParseProtoOrDie(R"pb( + type: INSERT + entity { + table_entry { + table_id: 49197097 + # Port + match { + field_id: 1 + exact { value: "sample_port" } + } + # Instance + match { + field_id: 2 + exact { value: "\x86" } + } + action { action { action_id: 16777221 } } + } + } + )pb"))); + + pdpi::IrBuiltInTableDefinition multicast_defintion = + gutil::FindOrDie(fuzzer_state.config.GetIrP4Info().built_in_tables(), + pdpi::GetMulticastGroupTableName()); + + absl::BitGen gen; + p4::v1::MulticastGroupEntry multicast_entry; + // Fuzz until multicast group entry has the one replica. + while (multicast_entry.replicas().empty()) { + ASSERT_OK_AND_ASSIGN(multicast_entry, + FuzzValidMulticastGroupEntry(&gen, fuzzer_state.config, + fuzzer_state.switch_state, + multicast_defintion)); + } + + // Multicast group cannot be 0. + EXPECT_NE(multicast_entry.multicast_group_id(), 0); + + // Ensure the one replica references values in table entry. + ASSERT_EQ(multicast_entry.replicas_size(), 1); + EXPECT_THAT(multicast_entry, Partially(EqualsProto(R"pb( + replicas { instance: 0x86 port: "sample_port" } + )pb"))); +} + +// This test uses behavior specific to the main.p4 program. In main.p4, +// built-in multicast group table replicas refer to fields in +// `referenced_by_multicast_replica_table`. +TEST(FuzzUtilTest, FuzzMulticastAreReproducibleWithState) { + FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); + + pdpi::IrTableDefinition multicast_dependency_definition = + gutil::FindOrDie(fuzzer_state.config.GetIrP4Info().tables_by_name(), + "referenced_by_multicast_replica_table"); + + absl::BitGen init_gen; + + // Generate up to 50 random table entries that can be referenced by multicast. + for (int i = 0; i < 50; ++i) { + p4::v1::Update update; + update.set_type(p4::v1::Update::INSERT); + ASSERT_OK_AND_ASSIGN(*update.mutable_entity()->mutable_table_entry(), + FuzzValidTableEntry(&init_gen, fuzzer_state.config, + fuzzer_state.switch_state, + multicast_dependency_definition)); + ASSERT_OK(fuzzer_state.switch_state.ApplyUpdate(update)); + } + + LOG(INFO) << "State size = " + << fuzzer_state.switch_state.GetNumTableEntries(); + + // Use the same sequence seed for both generators. + absl::SeedSeq seed; + absl::BitGen gen_0(seed); + absl::BitGen gen_1(seed); + pdpi::IrBuiltInTableDefinition multicast_defintion = + gutil::FindOrDie(fuzzer_state.config.GetIrP4Info().built_in_tables(), + pdpi::GetMulticastGroupTableName()); + + // Create 50 instances and verify that they are identical. + for (int i = 0; i < 20; ++i) { + ASSERT_OK_AND_ASSIGN(p4::v1::MulticastGroupEntry entry0, + FuzzValidMulticastGroupEntry( + &gen_0, fuzzer_state.config, + fuzzer_state.switch_state, multicast_defintion)); + + ASSERT_OK_AND_ASSIGN(p4::v1::MulticastGroupEntry entry1, + FuzzValidMulticastGroupEntry( + &gen_1, fuzzer_state.config, + fuzzer_state.switch_state, multicast_defintion)); + + EXPECT_THAT(entry0, EqualsProto(entry1)); + } +} + // Test that FuzzActionProfileActionSet correctly generates an ActionProfile // Action Set of acceptable weights and size (derived from max_group_size and // kActionProfileActionSetMaxCardinality). @@ -399,6 +557,42 @@ TEST(FuzzUtilTest, FuzzWriteRequestRespectsDisallowList) { } } +TEST(FuzzUtilTest, FuzzValidTableEntryRespectsDisallowList) { + FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); + fuzzer_state.config.disabled_fully_qualified_names = { + "ingress.ternary_table.ipv6_upper_64_bits", + "ingress.ternary_table.normal", + "ingress.ternary_table.mac", + "ingress.ternary_table.unsupported_field", + }; + + ASSERT_OK_AND_ASSIGN( + const pdpi::IrTableDefinition& ternary_table, + gutil::FindOrStatus(fuzzer_state.config.GetIrP4Info().tables_by_name(), + "ternary_table")); + + absl::flat_hash_set disallowed_ids; + for (const auto& path : fuzzer_state.config.disabled_fully_qualified_names) { + std::vector parts = absl::StrSplit(path, '.'); + ASSERT_OK_AND_ASSIGN( + const pdpi::IrMatchFieldDefinition& match, + gutil::FindOrStatus(ternary_table.match_fields_by_name(), + parts[parts.size() - 1])); + disallowed_ids.insert(match.match_field().id()); + } + + for (int i = 0; i < 1000; i++) { + ASSERT_OK_AND_ASSIGN( + p4::v1::TableEntry entry, + FuzzValidTableEntry(&fuzzer_state.gen, fuzzer_state.config, + fuzzer_state.switch_state, + ternary_table.preamble().id())); + for (const auto& match : entry.match()) { + EXPECT_THAT(match.field_id(), Not(AnyOfArray(disallowed_ids))); + } + } +} + TEST(FuzzUtilTest, FuzzActionRespectsDisallowList) { FuzzerTestState fuzzer_state = ConstructStandardFuzzerTestState(); ASSERT_OK_AND_ASSIGN( diff --git a/p4_fuzzer/switch_state.cc b/p4_fuzzer/switch_state.cc index 61236594..fe8f413f 100644 --- a/p4_fuzzer/switch_state.cc +++ b/p4_fuzzer/switch_state.cc @@ -29,6 +29,7 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" #include "absl/strings/string_view.h" +#include "absl/types/span.h" #include "glog/logging.h" #include "google/protobuf/util/message_differencer.h" #include "gutil/collections.h" @@ -51,8 +52,11 @@ namespace { using ::gutil::FindOrDie; using ::gutil::FindPtrOrStatus; using ::gutil::PrintTextProto; +using ::p4::v1::Entity; +using ::p4::v1::MulticastGroupEntry; using ::p4::v1::TableEntry; using ::p4::v1::Update; +using ::pdpi::IrEntity; using ::pdpi::IrP4Info; using ::pdpi::IrTableEntry; @@ -111,6 +115,7 @@ absl::StatusOr ReasonActionProfileCanAccommodateTableEntry( // If action weight is 0 or less, or if it is greater than the // max_member_weight (if non-zero) for SumOfMembers semantics, the server // MUST return an InvalidArgumentError. + // Ref: http://screen/6TucRSmmLEytHQK if (action.weight() <= 0) { return absl::InvalidArgumentError(absl::StrFormat( "The new entry attempts to program a member with weight %d, which is " @@ -130,6 +135,7 @@ absl::StatusOr ReasonActionProfileCanAccommodateTableEntry( if (action_profile.has_sum_of_members()) { // If the table entry has too many actions then the current table resources // do not matter. The server must return an InvalidArgumentError. + // Ref: http://screen/ANhyq7q6jQHry2W if (needed_resources.actions > action_profile.max_group_size() && action_profile.max_group_size() != 0) { return absl::InvalidArgumentError(absl::StrFormat( @@ -149,6 +155,7 @@ absl::StatusOr ReasonActionProfileCanAccommodateTableEntry( } else { // If the table entry has too much weight then the current table resources // do not matter. The server must return an InvalidArgumentError. + // Ref: http://screen/axLMH8UBcGE6GQD if (needed_resources.total_weight > action_profile.max_group_size() && action_profile.max_group_size() != 0) { return absl::InvalidArgumentError(absl::StrFormat( @@ -173,6 +180,29 @@ absl::StatusOr ReasonActionProfileCanAccommodateTableEntry( } // namespace +// Returns the canonical form of `entity` according to the P4 Runtime Spec +// https://s3-us-west-2.amazonaws.com/p4runtime/ci/main/P4Runtime-Spec.html#sec-bytestrings. +// TODO: Canonical form is achieved by performing an IR roundtrip +// translation. This ties correctness to IR functionality. Local +// canonicalization would be preferred. +absl::StatusOr CanonicalizeEntity(const IrP4Info& info, + const Entity& entity, bool key_only) { + auto pdpi_options = pdpi::TranslationOptions{ + .key_only = key_only, + }; + // IR->PI translation includes canonicalization so a PI->IR->PI translation is + // performed for canonicalization. + ASSIGN_OR_RETURN(IrEntity ir_entity, + pdpi::PiEntityToIr(info, entity, pdpi_options), + _ << "Could not canonicalize: PiToIr Error\n" + << entity.DebugString()); + ASSIGN_OR_RETURN(Entity canonical_entity, + pdpi::IrEntityToPi(info, ir_entity, pdpi_options), + _ << "Could not canonicalize: IrToPi Error\n" + << entity.DebugString()); + return canonical_entity; +} + absl::StatusOr CanonicalizeTableEntry(const IrP4Info& info, const TableEntry& entry, bool key_only) { @@ -208,6 +238,8 @@ void SwitchState::ClearTableEntries() { unordered_tables_[table_id] = UnorderedTableEntries(); current_resource_statistics_[table_id] = ResourceStatistics(); } + ordered_multicast_entries_ = {}; + unordered_multicast_entries_ = {}; current_entries_ = 0; } @@ -331,6 +363,17 @@ std::optional SwitchState::GetTableEntry( return std::nullopt; } +std::optional SwitchState::GetMulticastGroupEntry( + const MulticastGroupEntry& entry) const { + if (auto table_iter = + unordered_multicast_entries_.find(entry.multicast_group_id()); + table_iter != unordered_multicast_entries_.end()) { + auto [table_key, table_entry] = *table_iter; + return table_entry; + } + return std::nullopt; +} + absl::StatusOr SwitchState::GetPeakResourceStatistics( int table_id) const { if (!peak_resource_statistics_.contains(table_id)) { @@ -418,7 +461,116 @@ absl::Status SwitchState::UpdateResourceStatistics(const TableEntry& entry, return absl::OkStatus(); } +absl::Status SwitchState::ApplyMulticastUpdate(const Update& update) { + ASSIGN_OR_RETURN( + Entity canonical_entity, + CanonicalizeEntity(ir_p4info_, update.entity(), + /*key_only=*/update.type() == Update::DELETE)); + const p4::v1::MulticastGroupEntry& multicast_group_entry = + canonical_entity.packet_replication_engine_entry() + .multicast_group_entry(); + int multicast_group_id = multicast_group_entry.multicast_group_id(); + switch (update.type()) { + case Update::INSERT: { + auto [ordered_iter, ordered_not_present] = + ordered_multicast_entries_.insert( + /*value=*/{multicast_group_id, multicast_group_entry}); + + auto [unordered_iter, unordered_not_present] = + unordered_multicast_entries_.insert( + /*value=*/{multicast_group_id, multicast_group_entry}); + + if (ordered_not_present != unordered_not_present) { + return gutil::InternalErrorBuilder() + << "Ordered Table and Unordered Table out of sync. Entry " + << (ordered_not_present ? "not present" : "present") + << " in Ordered Table but " + << (unordered_not_present ? "not present" : "present") + << " in Unordered Table.\n" + << "Offending Entry Update\n" + << update.DebugString(); + } + + if (!ordered_not_present) { + return gutil::InvalidArgumentErrorBuilder() + << "Cannot install the same table entry multiple times. Update: " + << update.DebugString(); + } + + break; + } + case Update::DELETE: { + int ordered_entries_erased = + ordered_multicast_entries_.erase(multicast_group_id); + int unordered_entries_erased = + unordered_multicast_entries_.erase(multicast_group_id); + + if (ordered_entries_erased != unordered_entries_erased) { + return gutil::InternalErrorBuilder() + << "Ordered Table and Unordered Table out of sync. Entry " + << (ordered_entries_erased == 0 ? "not present" : "present") + << " in Ordered Table but " + << (unordered_entries_erased == 0 ? "not present" : "present") + << " in Unordered Table.\n" + << "Offending Update\n" + << update.DebugString(); + } + + if (ordered_entries_erased != 1) { + return gutil::InvalidArgumentErrorBuilder() + << "Cannot erase non-existent table entries. Update: " + << update.DebugString(); + } + + break; + } + + case Update::MODIFY: { + auto [ordered_iter, ordered_not_present] = + ordered_multicast_entries_.insert_or_assign( + /*k=*/multicast_group_id, + /*obj=*/multicast_group_entry); + + auto [unordered_iter, unordered_not_present] = + unordered_multicast_entries_ + .insert_or_assign(/*k=*/ + multicast_group_id, + /*obj=*/multicast_group_entry); + + if (ordered_not_present != unordered_not_present) { + return gutil::InternalErrorBuilder() + << "Ordered Table and Unordered Table out of sync. Entry " + << (ordered_not_present ? "not present" : "present") + << " in Ordered Table but " + << (unordered_not_present ? "not present" : "present") + << " in Unordered Table.\n" + << "Offending Update\n" + << update.DebugString(); + } + + if (ordered_not_present) { + return gutil::InvalidArgumentErrorBuilder() + << "Cannot modify a non-existing update. Update: " + << update.DebugString(); + } + + break; + } + + default: + LOG(FATAL) << "Update of unsupported type: " // Crash OK + << update.DebugString(); + } + return absl::OkStatus(); +} + absl::Status SwitchState::ApplyUpdate(const Update& update) { + if (update.entity() + .packet_replication_engine_entry() + .has_multicast_group_entry()) { + return ApplyMulticastUpdate(update); + } + const int table_id = update.entity().table_entry().table_id(); auto& ordered_table = FindOrDie(ordered_tables_, table_id); @@ -528,14 +680,14 @@ absl::Status SwitchState::ApplyUpdate(const Update& update) { return absl::OkStatus(); } -absl::Status SwitchState::SetTableEntries( - absl::Span table_entries) { +absl::Status SwitchState::SetEntities( + absl::Span entities) { ClearTableEntries(); p4::v1::Update update; update.set_type(p4::v1::Update::INSERT); - for (const p4::v1::TableEntry& entry : table_entries) { - *update.mutable_entity()->mutable_table_entry() = entry; + for (const Entity& entity : entities) { + *update.mutable_entity() = entity; RETURN_IF_ERROR(ApplyUpdate(update)); } @@ -624,6 +776,11 @@ std::string SwitchState::SwitchStateSummary() const { } } } + + absl::StrAppendFormat(&res, "\n % 12d% 16s% 18s %s", + ordered_multicast_entries_.size(), "N/A", "N/A", + "builtin::multicast_group_table"); + return absl::StrFormat( "State(\n % 12s% 16s% 18s table_name\n % 12d% 16d% 18s total " "number of table entries%s\n * marks tables where max size >= " @@ -683,6 +840,39 @@ absl::Status SwitchState::CheckConsistency() const { } } } + + if (unordered_multicast_entries_.size() != + ordered_multicast_entries_.size()) { + return absl::InternalError(absl::StrFormat( + "Number of ordered multicast group entries differs from number of " + "unordered multicast group entries. Ordered Entries: %d Unordered " + "Entries: %d", + ordered_multicast_entries_.size(), + unordered_multicast_entries_.size())); + } + + // Ensure that every `table_entry` in an ordered table has a corresponding + // `table_entry` in the unordered table. + for (const auto& [multicast_group_id, ordered_entry] : + ordered_multicast_entries_) { + std::optional unordered_entry = + GetMulticastGroupEntry(ordered_entry); + if (unordered_entry == std::nullopt) { + return absl::InternalError(absl::StrFormat( + "Ordered multicast group entry %s is missing corresponding " + "unordered multicast group entry", + ordered_entry.DebugString())); + } + + google::protobuf::util::MessageDifferencer differ; + if (!gutil::ProtoEqual(ordered_entry, *unordered_entry, differ)) { + return absl::InternalError(absl::StrFormat( + "Ordered entry differs from unordered entry\n " + "Ordered entry: %s Unordered Entry: %s", + ordered_entry.DebugString(), unordered_entry->DebugString())); + } + } + return absl::OkStatus(); } diff --git a/p4_fuzzer/switch_state.h b/p4_fuzzer/switch_state.h index b38a5986..977424b4 100644 --- a/p4_fuzzer/switch_state.h +++ b/p4_fuzzer/switch_state.h @@ -24,7 +24,6 @@ #include "absl/container/btree_map.h" #include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/types/optional.h" @@ -129,11 +128,27 @@ class SwitchState { // Returns the total number of table entries in all tables. int64_t GetNumTableEntries() const; + // Returns the number of multicast group entries. + int64_t GetNumMulticastEntries() const { + return ordered_multicast_entries_.size(); + }; + // Returns the current state of a table entry (or nullopt if it is not // present). Only the uniquely identifying fields of entry are considered. std::optional GetTableEntry( const p4::v1::TableEntry& entry) const; + // Returns the current state of a multicast group entry (or nullopt if it is + // not present). Only multicast_group_id is considered when retrieving entry. + std::optional GetMulticastGroupEntry( + const p4::v1::MulticastGroupEntry& entry) const; + + // Returns all multicast group entries. + const absl::btree_map& + GetMulticastGroupEntries() const { + return ordered_multicast_entries_; + } + // Returns the list of all non-const table IDs in the underlying P4 program. const std::vector AllTableIds() const; @@ -152,9 +167,8 @@ class SwitchState { // Returns max number of entries seen on the switch. int GetMaxEntriesSeen() const { return peak_entries_seen_; } - // Updates all tables to match the given set of table entries. - absl::Status SetTableEntries( - absl::Span table_entries); + // Updates all tables to match the given set of entities. + absl::Status SetEntities(absl::Span entities); // Clears all table entries. void ClearTableEntries(); @@ -205,6 +219,15 @@ class SwitchState { // Tracks peak resource usage of entire switch. int peak_entries_seen_ = 0; + // Internal overload used to apply multicast updates. + absl::Status ApplyMulticastUpdate(const p4::v1::Update& update); + // Multicast group entries are keyed by their multicast group id. + // Btree copy used for deterministic ordering. + absl::btree_map ordered_multicast_entries_; + // Copy of above used for fast lookups. + absl::flat_hash_map + unordered_multicast_entries_; + pdpi::IrP4Info ir_p4info_; }; diff --git a/p4_fuzzer/switch_state_assert_entry_equality_test.expected.output b/p4_fuzzer/switch_state_assert_entry_equality_test.expected.output index 3331d70e..f654c824 100644 --- a/p4_fuzzer/switch_state_assert_entry_equality_test.expected.output +++ b/p4_fuzzer/switch_state_assert_entry_equality_test.expected.output @@ -214,6 +214,7 @@ State( 0 0 1024 referenced_by_multicast_replica_table 0 0 1024 referring_by_match_field_table 0 0 1024 golden_test_friendly_table + 0 N/A N/A builtin::multicast_group_table * marks tables where max size >= guaranteed size. ) @@ -263,6 +264,7 @@ State( 0 0 1024 referenced_by_multicast_replica_table 0 0 1024 referring_by_match_field_table 0 0 1024 golden_test_friendly_table + 0 N/A N/A builtin::multicast_group_table * marks tables where max size >= guaranteed size. ) @@ -312,6 +314,7 @@ State( 0 0 1024 referenced_by_multicast_replica_table 0 0 1024 referring_by_match_field_table 0 0 1024 golden_test_friendly_table + 0 N/A N/A builtin::multicast_group_table * marks tables where max size >= guaranteed size. ) @@ -361,6 +364,7 @@ State( 0 0 1024 referenced_by_multicast_replica_table 0 0 1024 referring_by_match_field_table 0 0 1024 golden_test_friendly_table + 0 N/A N/A builtin::multicast_group_table * marks tables where max size >= guaranteed size. ) @@ -410,5 +414,6 @@ State( 0 0 1024 referenced_by_multicast_replica_table 0 0 1024 referring_by_match_field_table 0 0 1024 golden_test_friendly_table + 0 N/A N/A builtin::multicast_group_table * marks tables where max size >= guaranteed size. ) diff --git a/p4_fuzzer/switch_state_assert_entry_equality_test_runner.cc b/p4_fuzzer/switch_state_assert_entry_equality_test_runner.cc index 0dfc877e..7af79b2e 100644 --- a/p4_fuzzer/switch_state_assert_entry_equality_test_runner.cc +++ b/p4_fuzzer/switch_state_assert_entry_equality_test_runner.cc @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "absl/status/status.h" @@ -11,6 +12,7 @@ #include "absl/types/span.h" #include "google/protobuf/util/message_differencer.h" #include "gutil/collections.h" +#include "gutil/status.h" #include "gutil/testing.h" #include "p4/config/v1/p4info.pb.h" #include "p4/v1/p4runtime.pb.h" @@ -25,6 +27,7 @@ namespace { using ::p4::config::v1::P4Info; using ::p4::config::v1::Preamble; using ::p4::config::v1::Table; +using ::p4::v1::Entity; using ::p4::v1::TableEntry; using ::pdpi::CreateIrP4Info; using ::pdpi::IrP4Info; @@ -199,14 +202,16 @@ std::vector SwitchStateSummaryTestCases() { }); } - // Exceeding max capacities for WCMP tables with SUM_OF_WEIGHTS and - // SUM_OF_MEMBERS size semantics. + // Exceeding max capacities for WCMP tables with SumOfWeights and + // SumOfMembers size semantics. { IrP4Info ir_info_sum_of_weights = pdpi::GetTestIrP4Info(); IrP4Info ir_info_sum_of_members = pdpi::GetTestIrP4Info(); for (auto& [_, action_profile] : *ir_info_sum_of_members.mutable_action_profiles_by_id()) { - action_profile.mutable_action_profile()->mutable_sum_of_members()->set_max_member_weight(4096); + action_profile.mutable_action_profile() + ->mutable_sum_of_members() + ->set_max_member_weight(4096); } // Relevant constants. @@ -314,6 +319,18 @@ absl::StatusOr> IrToPiVector( return pi_entries; } +// TODO: b/316926338 - Remove once test is refactored to use entities. +std::vector PiEntriesToEntities(std::vector pi_entries) { + std::vector pi_entities; + pi_entities.reserve(pi_entries.size()); + for (const auto& pi_entry : pi_entries) { + Entity entity; + *entity.mutable_table_entry() = pi_entry; + pi_entities.push_back(std::move(entity)); + } + return pi_entities; +} + absl::Status main() { IrP4Info ir_info = GetIrP4Info(); SwitchState state(ir_info); @@ -326,7 +343,7 @@ absl::Status main() { ASSIGN_OR_RETURN(std::vector pi_switch_entries, IrToPiVector(test.switch_entries, ir_info)); - RETURN_IF_ERROR(state.SetTableEntries(pi_fuzzer_entries)); + RETURN_IF_ERROR(state.SetEntities(PiEntriesToEntities(pi_fuzzer_entries))); RETURN_IF_ERROR(state.CheckConsistency()); std::cout << "#########################################################\n" @@ -351,7 +368,7 @@ absl::Status main() { state = SwitchState(test.ir_info); ASSIGN_OR_RETURN(std::vector pi_entries, IrToPiVector(test.entries, test.ir_info)); - RETURN_IF_ERROR(state.SetTableEntries(pi_entries)); + RETURN_IF_ERROR(state.SetEntities(PiEntriesToEntities(pi_entries))); RETURN_IF_ERROR(state.CheckConsistency()); if (test.delete_entries) { state.ClearTableEntries(); diff --git a/p4_fuzzer/switch_state_test.cc b/p4_fuzzer/switch_state_test.cc index f514f3b4..8166de2b 100644 --- a/p4_fuzzer/switch_state_test.cc +++ b/p4_fuzzer/switch_state_test.cc @@ -14,6 +14,7 @@ #include "p4_fuzzer/switch_state.h" #include +#include #include #include "absl/container/flat_hash_set.h" @@ -49,6 +50,7 @@ using ::p4::config::v1::P4Info; using ::p4::config::v1::Preamble; using ::p4::config::v1::Table; using ::p4::v1::ActionProfileAction; +using ::p4::v1::MulticastGroupEntry; using ::p4::v1::TableEntry; using ::p4::v1::Update; using ::pdpi::CreateIrP4Info; @@ -146,6 +148,40 @@ TEST(SwitchStateTest, RuleInsert) { EXPECT_TRUE(state.AllTablesEmpty()); } +TEST(SwitchStateTest, MulticastInsertWorks) { + SwitchState state(GetIrP4Info()); + + Update update = gutil::ParseProtoOrDie(R"pb( + type: INSERT + entity { + packet_replication_engine_entry { + multicast_group_entry { + multicast_group_id: 1 + replicas { port: "some-port" } + } + } + } + )pb"); + ASSERT_OK(state.ApplyUpdate(update)); + + // TODO: b/316926338 - Uncomment once multicast is treated as just another + // table in switch state. + // EXPECT_FALSE(state.AllTablesEmpty()); + + EXPECT_EQ(state.GetMulticastGroupEntries().size(), 1); + + MulticastGroupEntry& entry = *update.mutable_entity() + ->mutable_packet_replication_engine_entry() + ->mutable_multicast_group_entry(); + ASSERT_TRUE(state.GetMulticastGroupEntry(entry) != std::nullopt); + EXPECT_THAT(*state.GetMulticastGroupEntry(entry), EqualsProto(entry)); + + EXPECT_OK(state.CheckConsistency()); + + state.ClearTableEntries(); + EXPECT_TRUE(state.AllTablesEmpty()); +} + TEST(SwitchStateTest, ClearTableEntriesPreservesP4Info) { const IrP4Info p4info = pdpi::GetTestIrP4Info(); SwitchState state(p4info); @@ -409,35 +445,53 @@ TEST(SwitchStateTest, ASSERT_OK(state.CheckConsistency()); } -TEST(SwitchStateTest, SetTableEntriesSetsTableEntries) { +TEST(SwitchStateTest, SetEntitiesSetsEntities) { SwitchState state(GetIrP4Info()); EXPECT_TRUE(state.AllTablesEmpty()); // Call SetTableEntries and ensure it indeed populates the correct tables. - std::vector entries; - entries.emplace_back() = // Entry #1 in table 1. - gutil::ParseProtoOrDie( + std::vector entities; + + entities.emplace_back() = // Entry #1 in multicast table. + gutil::ParseProtoOrDie( + R"pb( + packet_replication_engine_entry { + multicast_group_entry { + multicast_group_id: 7 + replicas { instance: 1 port: "some_port" } + replicas { instance: 2 port: "some_port" } + replicas { instance: 1 port: "some_other_port" } + } + } + )pb"); + entities.emplace_back() = // Entry #1 in table 1. + gutil::ParseProtoOrDie( absl::Substitute(R"pb( - table_id: $0 - match { - field_id: 1 - exact { value: "\378\"" } + table_entry { + table_id: $0 + match { + field_id: 1 + exact { value: "\378\"" } + } } )pb", kSpamTableId)); - entries.emplace_back().set_table_id(kEggTableId); // Entry #1 in table 2. - entries.emplace_back() = // Entry #2 in table 1. - gutil::ParseProtoOrDie( + entities.emplace_back().mutable_table_entry()->set_table_id( + kEggTableId); // Entry #1 in table 2. + entities.emplace_back() = // Entry #2 in table 1. + gutil::ParseProtoOrDie( absl::Substitute(R"pb( - table_id: $0 - match { - field_id: 1 - exact { value: "\377\"" } + table_entry { + table_id: $0 + match { + field_id: 1 + exact { value: "\377\"" } + } } )pb", kSpamTableId)); - ASSERT_OK(state.SetTableEntries(entries)) + ASSERT_OK(state.SetEntities(entities)) << " with the following P4Info:\n " << state.GetIrP4Info().DebugString(); EXPECT_EQ(state.GetNumTableEntries(kSpamTableId), 2); EXPECT_EQ(state.GetNumTableEntries(kEggTableId), 1);