|
| 1 | +// Copyright 2026 The 4ward Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +// Runs the sonic-pins P4 fuzzer against a 4ward P4Runtime server. |
| 5 | +// |
| 6 | +// Three layers of validation: |
| 7 | +// 1. Spec oracle: validates response status codes against P4Runtime spec. |
| 8 | +// 2. Read-back: periodically reads all entries and verifies they match the |
| 9 | +// SwitchState mirror. |
| 10 | +// 3. Crash detection: any unhandled exception or server crash is a failure. |
| 11 | + |
| 12 | +#include <fstream> |
| 13 | +#include <memory> |
| 14 | +#include <string> |
| 15 | +#include <vector> |
| 16 | + |
| 17 | +#include "absl/log/log.h" |
| 18 | +#include "absl/random/random.h" |
| 19 | +#include "absl/strings/str_cat.h" |
| 20 | +#include "absl/types/span.h" |
| 21 | +#include "fourward_cc/fourward_server.h" |
| 22 | +#include "google/rpc/code.pb.h" |
| 23 | +#include "grpcpp/security/credentials.h" |
| 24 | +#include "gtest/gtest.h" |
| 25 | +#include "gutil/status_matchers.h" |
| 26 | +#include "lib/p4rt/p4rt_port.h" |
| 27 | +#include "p4/v1/p4runtime.pb.h" |
| 28 | +#include "p4_fuzzer/annotation_util.h" |
| 29 | +#include "p4_fuzzer/fuzz_util.h" |
| 30 | +#include "p4_fuzzer/fuzzer.pb.h" |
| 31 | +#include "p4_fuzzer/fuzzer_config.h" |
| 32 | +#include "p4_fuzzer/oracle_util.h" |
| 33 | +#include "p4_fuzzer/switch_state.h" |
| 34 | +#include "p4_infra/p4_runtime/p4_runtime_session.h" |
| 35 | +#include "p4_infra/p4_runtime/p4_runtime_session_extras.h" |
| 36 | +#include "tools/cpp/runfiles/runfiles.h" |
| 37 | + |
| 38 | +namespace fourward { |
| 39 | +namespace { |
| 40 | + |
| 41 | +using ::bazel::tools::cpp::runfiles::Runfiles; |
| 42 | +using ::p4_fuzzer::AnnotatedWriteRequest; |
| 43 | +using ::p4_fuzzer::FuzzWriteRequest; |
| 44 | +using ::p4_fuzzer::FuzzerConfig; |
| 45 | +using ::p4_fuzzer::RemoveAnnotations; |
| 46 | +using ::p4_fuzzer::SwitchState; |
| 47 | +using ::p4_fuzzer::WriteRequestOracle; |
| 48 | +using ::p4_runtime::P4RuntimeSession; |
| 49 | + |
| 50 | +constexpr int kFuzzerIterations = 10000; |
| 51 | +constexpr int kReadBackInterval = 500; |
| 52 | + |
| 53 | +p4::v1::ForwardingPipelineConfig LoadPipeline() { |
| 54 | + std::string error; |
| 55 | + std::unique_ptr<Runfiles> runfiles(Runfiles::Create("", &error)); |
| 56 | + CHECK(runfiles != nullptr) << error; |
| 57 | + std::string path = runfiles->Rlocation(PIPELINE_RLOCATION); |
| 58 | + std::ifstream file(path, std::ios::binary); |
| 59 | + CHECK(file.good()) << "cannot open: " << path; |
| 60 | + std::string content((std::istreambuf_iterator<char>(file)), |
| 61 | + std::istreambuf_iterator<char>()); |
| 62 | + p4::v1::ForwardingPipelineConfig config; |
| 63 | + CHECK(config.ParseFromString(content)) << "failed to parse pipeline"; |
| 64 | + return config; |
| 65 | +} |
| 66 | + |
| 67 | +bool ShouldSkipUpdate(const p4_fuzzer::AnnotatedUpdate& update) { |
| 68 | + // MODIFY: oracle crashes (upstream b/126750297). |
| 69 | + if (update.pi().type() == p4::v1::Update::MODIFY) return true; |
| 70 | + // Mutated DELETEs: oracle validates action/match fields on DELETEs, but |
| 71 | + // the spec says DELETE only needs the key (§9.1). 4ward correctly skips |
| 72 | + // action validation on DELETE; the oracle incorrectly flags this. |
| 73 | + if (update.pi().type() == p4::v1::Update::DELETE && |
| 74 | + update.mutations_size() > 0) |
| 75 | + return true; |
| 76 | + return false; |
| 77 | +} |
| 78 | + |
| 79 | +TEST(P4FuzzerTest, FuzzWriteRequestsAgainstFourward) { |
| 80 | + ASSERT_OK_AND_ASSIGN(FourwardServer server, FourwardServer::Start()); |
| 81 | + |
| 82 | + p4_runtime::P4RuntimeSessionOptionalArgs session_args; |
| 83 | + session_args.role = ""; |
| 84 | + ASSERT_OK_AND_ASSIGN( |
| 85 | + auto session, |
| 86 | + P4RuntimeSession::Create(server.Address(), |
| 87 | + grpc::InsecureChannelCredentials(), |
| 88 | + server.DeviceId(), session_args)); |
| 89 | + |
| 90 | + auto pipeline = LoadPipeline(); |
| 91 | + ASSERT_OK(p4_runtime::SetMetadataAndSetForwardingPipelineConfig( |
| 92 | + session.get(), |
| 93 | + p4::v1::SetForwardingPipelineConfigRequest::VERIFY_AND_COMMIT, |
| 94 | + pipeline)); |
| 95 | + |
| 96 | + p4_fuzzer::ConfigParams params; |
| 97 | + params.ports = |
| 98 | + pins_test::P4rtPortId::MakeVectorFromOpenConfigEncodings({1, 2, 3}); |
| 99 | + params.role = ""; |
| 100 | + params.mutate_update_probability = 0.1; |
| 101 | + ASSERT_OK_AND_ASSIGN(auto config, |
| 102 | + FuzzerConfig::Create(pipeline.p4info(), params)); |
| 103 | + SwitchState switch_state(config.GetIrP4Info()); |
| 104 | + absl::BitGen gen; |
| 105 | + |
| 106 | + int num_updates = 0; |
| 107 | + int num_oracle_failures = 0; |
| 108 | + int num_readback_checks = 0; |
| 109 | + |
| 110 | + for (int i = 0; i < kFuzzerIterations; ++i) { |
| 111 | + if (i % 1000 == 0) LOG(INFO) << "Fuzzer iteration " << i; |
| 112 | + |
| 113 | + AnnotatedWriteRequest annotated_request = |
| 114 | + FuzzWriteRequest(&gen, config, switch_state); |
| 115 | + |
| 116 | + // Filter updates the oracle can't handle correctly. |
| 117 | + AnnotatedWriteRequest filtered_request; |
| 118 | + for (const auto& update : annotated_request.updates()) { |
| 119 | + if (!ShouldSkipUpdate(update)) { |
| 120 | + *filtered_request.add_updates() = update; |
| 121 | + } |
| 122 | + } |
| 123 | + annotated_request = filtered_request; |
| 124 | + |
| 125 | + p4::v1::WriteRequest request = RemoveAnnotations(annotated_request); |
| 126 | + if (request.updates_size() == 0) continue; |
| 127 | + num_updates += request.updates_size(); |
| 128 | + |
| 129 | + ASSERT_OK_AND_ASSIGN( |
| 130 | + auto response, |
| 131 | + p4_runtime::SendPiUpdatesAndReturnPerUpdateStatus(*session, |
| 132 | + request.updates())); |
| 133 | + ASSERT_TRUE(response.has_rpc_response()) |
| 134 | + << "RPC-level error: " << response.DebugString(); |
| 135 | + ASSERT_EQ(response.rpc_response().statuses().size(), |
| 136 | + request.updates_size()); |
| 137 | + |
| 138 | + // Layer 1: spec oracle. |
| 139 | + std::vector<pdpi::IrUpdateStatus> statuses( |
| 140 | + response.rpc_response().statuses().begin(), |
| 141 | + response.rpc_response().statuses().end()); |
| 142 | + auto problems = WriteRequestOracle(config.GetIrP4Info(), annotated_request, |
| 143 | + absl::MakeSpan(statuses), switch_state); |
| 144 | + if (problems.has_value()) { |
| 145 | + num_oracle_failures++; |
| 146 | + for (const std::string& problem : *problems) { |
| 147 | + ADD_FAILURE() << "Oracle failure at iteration " << i << ": " << problem; |
| 148 | + } |
| 149 | + } |
| 150 | + |
| 151 | + // Update switch state with successful writes. |
| 152 | + for (int j = 0; j < request.updates_size(); ++j) { |
| 153 | + if (statuses[j].code() == google::rpc::Code::OK) { |
| 154 | + ASSERT_OK(switch_state.ApplyUpdate(request.updates(j))); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + // Layer 2: periodic read-back verification. |
| 159 | + if ((i + 1) % kReadBackInterval == 0) { |
| 160 | + num_readback_checks++; |
| 161 | + ASSERT_OK_AND_ASSIGN( |
| 162 | + auto entries, p4_runtime::ReadPiTableEntriesSorted(*session)); |
| 163 | + auto readback_status = switch_state.AssertEntriesAreEqualToState(entries); |
| 164 | + ASSERT_OK(readback_status) |
| 165 | + << "Read-back mismatch at iteration " << i << ": " |
| 166 | + << readback_status.message(); |
| 167 | + LOG(INFO) << "Read-back check " << num_readback_checks |
| 168 | + << " passed (" << entries.size() << " entries)."; |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + // Final read-back check. |
| 173 | + num_readback_checks++; |
| 174 | + ASSERT_OK_AND_ASSIGN( |
| 175 | + auto final_entries, p4_runtime::ReadPiTableEntriesSorted(*session)); |
| 176 | + ASSERT_OK(switch_state.AssertEntriesAreEqualToState(final_entries)) |
| 177 | + << "Final read-back mismatch."; |
| 178 | + |
| 179 | + LOG(INFO) << "Fuzzer complete: " << kFuzzerIterations << " iterations, " |
| 180 | + << num_updates << " updates, " << num_oracle_failures |
| 181 | + << " oracle failures, " << num_readback_checks |
| 182 | + << " read-back checks passed."; |
| 183 | + EXPECT_EQ(num_oracle_failures, 0); |
| 184 | +} |
| 185 | + |
| 186 | +} // namespace |
| 187 | +} // namespace fourward |
0 commit comments