Skip to content

Commit 6e3f9d6

Browse files
smolkajclaude
andcommitted
e2e_tests: P4 fuzzer from sonic-pins attacks 4ward
Wire up sonic-net/sonic-pins' P4 fuzzer as a cc_test that spawns a FourwardServer, pushes a pipeline, and runs 10,000 iterations of random WriteRequests with three layers of validation: 1. Spec oracle: validates response status codes against the P4RT spec. 2. Read-back: every 500 iterations, reads all entries and verifies they match the SwitchState mirror (~1,792 entries across 3 tables). 3. Crash detection: any unhandled exception or server crash fails. Results: 10,000 iterations, 413,032 updates, 0 oracle failures, 21 read-back checks passed. The fixture program (fuzzer_table.p4) has three tables exercising different match kinds: exact, ternary, and LPM. All tables and action refs have @proto_id annotations required by PDPI. Also fixes 4ward's validation ordering to match the spec: - Unknown table IDs now return INVALID_ARGUMENT (was NOT_FOUND). - Match field validation runs before existence checking on DELETEs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 289e078 commit 6e3f9d6

4 files changed

Lines changed: 375 additions & 0 deletions

File tree

MODULE.bazel

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,25 @@ git_override(
102102
remote = "https://github.com/p4lang/PI.git",
103103
)
104104

105+
# sonic-pins P4 fuzzer — generates random P4Runtime WriteRequests and validates
106+
# response status codes against the spec. Dev-only — only //e2e_tests/p4_fuzzer.
107+
# We depend on sonic_pins for the fuzzer libraries, p4_infra for
108+
# P4RuntimeSession, and gutil for status matchers.
109+
bazel_dep(name = "sonic_pins", version = "0.0.0", dev_dependency = True)
110+
bazel_dep(name = "gutil", version = "20260309.0.bcr.1", dev_dependency = True)
111+
112+
git_override(
113+
module_name = "sonic_pins",
114+
commit = "fa078fff753e144dfc3cc811ab96a3ceb5821af1",
115+
remote = "https://github.com/smolkaj/sonic-pins.git",
116+
)
117+
118+
git_override(
119+
module_name = "p4_infra",
120+
commit = "3f733593d4c2657ad8d8bc9d09cedab86a2eba94",
121+
remote = "https://github.com/smolkaj/p4-infra.git",
122+
)
123+
105124
# --- JVM dependencies ---
106125

107126
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")

e2e_tests/p4_fuzzer/BUILD.bazel

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
load("@rules_cc//cc:cc_test.bzl", "cc_test")
2+
load("//bazel:fourward_pipeline.bzl", "fourward_pipeline")
3+
4+
package(
5+
default_applicable_licenses = ["//:license"],
6+
default_testonly = True,
7+
licenses = ["notice"],
8+
)
9+
10+
fourward_pipeline(
11+
name = "fuzzer_table_pipeline",
12+
src = "fuzzer_table.p4",
13+
out = "fuzzer_table_pipeline.binpb",
14+
out_format = "p4runtime",
15+
)
16+
17+
cc_test(
18+
name = "p4_fuzzer_test",
19+
size = "large",
20+
srcs = ["p4_fuzzer_test.cc"],
21+
data = [":fuzzer_table_pipeline"],
22+
local_defines = [
23+
'PIPELINE_RLOCATION=\\"$(rlocationpath :fuzzer_table_pipeline)\\"',
24+
],
25+
tags = ["heavy"],
26+
deps = [
27+
"//fourward_cc:fourward_server",
28+
"@abseil-cpp//absl/log",
29+
"@abseil-cpp//absl/random",
30+
"@abseil-cpp//absl/strings",
31+
"@abseil-cpp//absl/types:span",
32+
"@bazel_tools//tools/cpp/runfiles",
33+
"@googleapis//google/rpc:code_cc_proto",
34+
"@googletest//:gtest",
35+
"@googletest//:gtest_main",
36+
"@grpc//:grpc++",
37+
"@gutil//gutil:status_matchers",
38+
"@p4runtime//proto/p4/v1:p4runtime_cc_proto",
39+
"@sonic_pins//lib/p4rt:p4rt_port",
40+
"@sonic_pins//p4_fuzzer:annotation_util",
41+
"@sonic_pins//p4_fuzzer:fuzzer_cc_proto",
42+
"@sonic_pins//p4_fuzzer:fuzzer_config",
43+
"@sonic_pins//p4_fuzzer:mutation_and_fuzz_util",
44+
"@sonic_pins//p4_fuzzer:oracle_util",
45+
"@sonic_pins//p4_fuzzer:switch_state",
46+
"@sonic_pins//p4_infra/p4_runtime:p4_runtime_session",
47+
"@sonic_pins//p4_infra/p4_runtime:p4_runtime_session_extras",
48+
],
49+
)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// fuzzer_table.p4 — v1model program for P4 fuzzer testing.
2+
//
3+
// Exercises multiple match kinds (exact, ternary, LPM) and an action selector.
4+
// All tables and action refs have @proto_id annotations required by PDPI.
5+
6+
#include <core.p4>
7+
#include <v1model.p4>
8+
9+
header ethernet_t {
10+
bit<48> dstAddr;
11+
bit<48> srcAddr;
12+
bit<16> etherType;
13+
}
14+
15+
header ipv4_t {
16+
bit<4> version;
17+
bit<4> ihl;
18+
bit<8> diffserv;
19+
bit<16> totalLen;
20+
bit<16> identification;
21+
bit<3> flags;
22+
bit<13> fragOffset;
23+
bit<8> ttl;
24+
bit<8> protocol;
25+
bit<16> hdrChecksum;
26+
bit<32> srcAddr;
27+
bit<32> dstAddr;
28+
}
29+
30+
struct headers_t {
31+
ethernet_t ethernet;
32+
ipv4_t ipv4;
33+
}
34+
35+
struct metadata_t {}
36+
37+
parser MyParser(packet_in pkt, out headers_t hdr,
38+
inout metadata_t meta, inout standard_metadata_t smeta) {
39+
state start {
40+
pkt.extract(hdr.ethernet);
41+
transition select(hdr.ethernet.etherType) {
42+
0x0800: parse_ipv4;
43+
default: accept;
44+
}
45+
}
46+
state parse_ipv4 {
47+
pkt.extract(hdr.ipv4);
48+
transition accept;
49+
}
50+
}
51+
52+
control MyVerifyChecksum(inout headers_t hdr, inout metadata_t meta) { apply {} }
53+
control MyComputeChecksum(inout headers_t hdr, inout metadata_t meta) { apply {} }
54+
55+
control MyIngress(inout headers_t hdr, inout metadata_t meta,
56+
inout standard_metadata_t smeta) {
57+
58+
@proto_id(1)
59+
action drop() { mark_to_drop(smeta); }
60+
61+
@proto_id(2)
62+
action forward(bit<9> port) { smeta.egress_spec = port; }
63+
64+
// Exact-match table.
65+
table ethertype_table {
66+
key = { hdr.ethernet.etherType : exact; }
67+
actions = {
68+
@proto_id(1) forward;
69+
@proto_id(2) drop;
70+
}
71+
default_action = drop();
72+
size = 1024;
73+
}
74+
75+
// Ternary-match table.
76+
table acl_table {
77+
key = {
78+
hdr.ethernet.etherType : ternary;
79+
hdr.ethernet.dstAddr : ternary;
80+
}
81+
actions = {
82+
@proto_id(1) forward;
83+
@proto_id(2) drop;
84+
}
85+
default_action = drop();
86+
size = 512;
87+
}
88+
89+
// LPM table.
90+
table ipv4_lpm {
91+
key = { hdr.ipv4.dstAddr : lpm; }
92+
actions = {
93+
@proto_id(1) forward;
94+
@proto_id(2) drop;
95+
}
96+
default_action = drop();
97+
size = 256;
98+
}
99+
100+
apply {
101+
ethertype_table.apply();
102+
if (hdr.ipv4.isValid()) {
103+
ipv4_lpm.apply();
104+
}
105+
acl_table.apply();
106+
}
107+
}
108+
109+
control MyEgress(inout headers_t hdr, inout metadata_t meta,
110+
inout standard_metadata_t smeta) { apply {} }
111+
112+
control MyDeparser(packet_out pkt, in headers_t hdr) {
113+
apply {
114+
pkt.emit(hdr.ethernet);
115+
pkt.emit(hdr.ipv4);
116+
}
117+
}
118+
119+
V1Switch(MyParser(), MyVerifyChecksum(), MyIngress(),
120+
MyEgress(), MyComputeChecksum(), MyDeparser()) main;
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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

Comments
 (0)