Skip to content

Commit 009ff8d

Browse files
committed
fuzz: add bidirectional fragmented transport test
This adds a simulation test, with two V1Transport objects, which send messages to each other, with sending and receiving fragmented into multiple pieces that may be interleaved. It primarily verifies that the sending and receiving side are compatible with each other, plus a few sanity checks.
1 parent fb2c5ed commit 009ff8d

File tree

1 file changed

+241
-0
lines changed

1 file changed

+241
-0
lines changed

Diff for: src/test/fuzz/p2p_transport_serialization.cpp

+241
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include <protocol.h>
1010
#include <test/fuzz/FuzzedDataProvider.h>
1111
#include <test/fuzz/fuzz.h>
12+
#include <test/fuzz/util.h>
13+
#include <test/util/xoroshiro128plusplus.h>
1214
#include <util/chaintype.h>
1315

1416
#include <cassert>
@@ -17,11 +19,19 @@
1719
#include <optional>
1820
#include <vector>
1921

22+
namespace {
23+
24+
std::vector<std::string> g_all_messages;
25+
2026
void initialize_p2p_transport_serialization()
2127
{
2228
SelectParams(ChainType::REGTEST);
29+
g_all_messages = getAllNetMessageTypes();
30+
std::sort(g_all_messages.begin(), g_all_messages.end());
2331
}
2432

33+
} // namespace
34+
2535
FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serialization)
2636
{
2737
// Construct transports for both sides, with dummy NodeIds.
@@ -92,3 +102,234 @@ FUZZ_TARGET(p2p_transport_serialization, .init = initialize_p2p_transport_serial
92102
}
93103
}
94104
}
105+
106+
namespace {
107+
108+
template<typename R>
109+
void SimulationTest(Transport& initiator, Transport& responder, R& rng, FuzzedDataProvider& provider)
110+
{
111+
// Simulation test with two Transport objects, which send messages to each other, with
112+
// sending and receiving fragmented into multiple pieces that may be interleaved. It primarily
113+
// verifies that the sending and receiving side are compatible with each other, plus a few
114+
// sanity checks. It does not attempt to introduce errors in the communicated data.
115+
116+
// Put the transports in an array for by-index access.
117+
const std::array<Transport*, 2> transports = {&initiator, &responder};
118+
119+
// Two vectors representing in-flight bytes. inflight[i] is from transport[i] to transport[!i].
120+
std::array<std::vector<uint8_t>, 2> in_flight;
121+
122+
// Two queues with expected messages. expected[i] is expected to arrive in transport[!i].
123+
std::array<std::deque<CSerializedNetMsg>, 2> expected;
124+
125+
// Vectors with bytes last returned by GetBytesToSend() on transport[i].
126+
std::array<std::vector<uint8_t>, 2> to_send;
127+
128+
// Last returned 'more' values (if still relevant) by transport[i]->GetBytesToSend().
129+
std::array<std::optional<bool>, 2> last_more;
130+
131+
// Whether more bytes to be sent are expected on transport[i].
132+
std::array<std::optional<bool>, 2> expect_more;
133+
134+
// Function to consume a message type.
135+
auto msg_type_fn = [&]() {
136+
uint8_t v = provider.ConsumeIntegral<uint8_t>();
137+
if (v == 0xFF) {
138+
// If v is 0xFF, construct a valid (but possibly unknown) message type from the fuzz
139+
// data.
140+
std::string ret;
141+
while (ret.size() < CMessageHeader::COMMAND_SIZE) {
142+
char c = provider.ConsumeIntegral<char>();
143+
// Match the allowed characters in CMessageHeader::IsCommandValid(). Any other
144+
// character is interpreted as end.
145+
if (c < ' ' || c > 0x7E) break;
146+
ret += c;
147+
}
148+
return ret;
149+
} else {
150+
// Otherwise, use it as index into the list of known messages.
151+
return g_all_messages[v % g_all_messages.size()];
152+
}
153+
};
154+
155+
// Function to construct a CSerializedNetMsg to send.
156+
auto make_msg_fn = [&](bool first) {
157+
CSerializedNetMsg msg;
158+
if (first) {
159+
// Always send a "version" message as first one.
160+
msg.m_type = "version";
161+
} else {
162+
msg.m_type = msg_type_fn();
163+
}
164+
// Determine size of message to send (limited to 75 kB for performance reasons).
165+
size_t size = provider.ConsumeIntegralInRange<uint32_t>(0, 75000);
166+
// Get payload of message from RNG.
167+
msg.data.resize(size);
168+
for (auto& v : msg.data) v = uint8_t(rng());
169+
// Return.
170+
return msg;
171+
};
172+
173+
// The next message to be sent (initially version messages, but will be replaced once sent).
174+
std::array<CSerializedNetMsg, 2> next_msg = {
175+
make_msg_fn(/*first=*/true),
176+
make_msg_fn(/*first=*/true)
177+
};
178+
179+
// Wrapper around transport[i]->GetBytesToSend() that performs sanity checks.
180+
auto bytes_to_send_fn = [&](int side) -> Transport::BytesToSend {
181+
const auto& [bytes, more, msg_type] = transports[side]->GetBytesToSend();
182+
// Compare with expected more.
183+
if (expect_more[side].has_value()) assert(!bytes.empty() == *expect_more[side]);
184+
// Compare with previously reported output.
185+
assert(to_send[side].size() <= bytes.size());
186+
assert(to_send[side] == Span{bytes}.first(to_send[side].size()));
187+
to_send[side].resize(bytes.size());
188+
std::copy(bytes.begin(), bytes.end(), to_send[side].begin());
189+
// Remember 'more' result.
190+
last_more[side] = {more};
191+
// Return.
192+
return {bytes, more, msg_type};
193+
};
194+
195+
// Function to make side send a new message.
196+
auto new_msg_fn = [&](int side) {
197+
// Don't do anything if there are too many unreceived messages already.
198+
if (expected[side].size() >= 16) return;
199+
// Try to send (a copy of) the message in next_msg[side].
200+
CSerializedNetMsg msg = next_msg[side].Copy();
201+
bool queued = transports[side]->SetMessageToSend(msg);
202+
// Update expected more data.
203+
expect_more[side] = std::nullopt;
204+
// Verify consistency of GetBytesToSend after SetMessageToSend
205+
bytes_to_send_fn(/*side=*/side);
206+
if (queued) {
207+
// Remember that this message is now expected by the receiver.
208+
expected[side].emplace_back(std::move(next_msg[side]));
209+
// Construct a new next message to send.
210+
next_msg[side] = make_msg_fn(/*first=*/false);
211+
}
212+
};
213+
214+
// Function to make side send out bytes (if any).
215+
auto send_fn = [&](int side, bool everything = false) {
216+
const auto& [bytes, more, msg_type] = bytes_to_send_fn(/*side=*/side);
217+
// Don't do anything if no bytes to send.
218+
if (bytes.empty()) return false;
219+
size_t send_now = everything ? bytes.size() : provider.ConsumeIntegralInRange<size_t>(0, bytes.size());
220+
if (send_now == 0) return false;
221+
// Add bytes to the in-flight queue, and mark those bytes as consumed.
222+
in_flight[side].insert(in_flight[side].end(), bytes.begin(), bytes.begin() + send_now);
223+
transports[side]->MarkBytesSent(send_now);
224+
// If all to-be-sent bytes were sent, move last_more data to expect_more data.
225+
if (send_now == bytes.size()) {
226+
expect_more[side] = last_more[side];
227+
}
228+
// Remove the bytes from the last reported to-be-sent vector.
229+
assert(to_send[side].size() >= send_now);
230+
to_send[side].erase(to_send[side].begin(), to_send[side].begin() + send_now);
231+
// Verify that GetBytesToSend gives a result consistent with earlier.
232+
bytes_to_send_fn(/*side=*/side);
233+
// Return whether anything was sent.
234+
return send_now > 0;
235+
};
236+
237+
// Function to make !side receive bytes (if any).
238+
auto recv_fn = [&](int side, bool everything = false) {
239+
// Don't do anything if no bytes in flight.
240+
if (in_flight[side].empty()) return false;
241+
// Decide span to receive
242+
size_t to_recv_len = in_flight[side].size();
243+
if (!everything) to_recv_len = provider.ConsumeIntegralInRange<size_t>(0, to_recv_len);
244+
Span<const uint8_t> to_recv = Span{in_flight[side]}.first(to_recv_len);
245+
// Process those bytes
246+
while (!to_recv.empty()) {
247+
size_t old_len = to_recv.size();
248+
bool ret = transports[!side]->ReceivedBytes(to_recv);
249+
// Bytes must always be accepted, as this test does not introduce any errors in
250+
// communication.
251+
assert(ret);
252+
// Clear cached expected 'more' information: if certainly no more data was to be sent
253+
// before, receiving bytes makes this uncertain.
254+
if (expect_more[!side] == false) expect_more[!side] = std::nullopt;
255+
// Verify consistency of GetBytesToSend after ReceivedBytes
256+
bytes_to_send_fn(/*side=*/!side);
257+
bool progress = to_recv.size() < old_len;
258+
if (transports[!side]->ReceivedMessageComplete()) {
259+
bool reject{false};
260+
auto received = transports[!side]->GetReceivedMessage({}, reject);
261+
// Receiving must succeed.
262+
assert(!reject);
263+
// There must be a corresponding expected message.
264+
assert(!expected[side].empty());
265+
// The m_message_size field must be correct.
266+
assert(received.m_message_size == received.m_recv.size());
267+
// The m_type must match what is expected.
268+
assert(received.m_type == expected[side].front().m_type);
269+
// The data must match what is expected.
270+
assert(MakeByteSpan(received.m_recv) == MakeByteSpan(expected[side].front().data));
271+
expected[side].pop_front();
272+
progress = true;
273+
}
274+
// Progress must be made (by processing incoming bytes and/or returning complete
275+
// messages) until all received bytes are processed.
276+
assert(progress);
277+
}
278+
// Remove the processed bytes from the in_flight buffer.
279+
in_flight[side].erase(in_flight[side].begin(), in_flight[side].begin() + to_recv_len);
280+
// Return whether anything was received.
281+
return to_recv_len > 0;
282+
};
283+
284+
// Main loop, interleaving new messages, sends, and receives.
285+
LIMITED_WHILE(provider.remaining_bytes(), 1000) {
286+
CallOneOf(provider,
287+
// (Try to) give the next message to the transport.
288+
[&] { new_msg_fn(/*side=*/0); },
289+
[&] { new_msg_fn(/*side=*/1); },
290+
// (Try to) send some bytes from the transport to the network.
291+
[&] { send_fn(/*side=*/0); },
292+
[&] { send_fn(/*side=*/1); },
293+
// (Try to) receive bytes from the network, converting to messages.
294+
[&] { recv_fn(/*side=*/0); },
295+
[&] { recv_fn(/*side=*/1); }
296+
);
297+
}
298+
299+
// When we're done, perform sends and receives of existing messages to flush anything already
300+
// in flight.
301+
while (true) {
302+
bool any = false;
303+
if (send_fn(/*side=*/0, /*everything=*/true)) any = true;
304+
if (send_fn(/*side=*/1, /*everything=*/true)) any = true;
305+
if (recv_fn(/*side=*/0, /*everything=*/true)) any = true;
306+
if (recv_fn(/*side=*/1, /*everything=*/true)) any = true;
307+
if (!any) break;
308+
}
309+
310+
// Make sure nothing is left in flight.
311+
assert(in_flight[0].empty());
312+
assert(in_flight[1].empty());
313+
314+
// Make sure all expected messages were received.
315+
assert(expected[0].empty());
316+
assert(expected[1].empty());
317+
}
318+
319+
std::unique_ptr<Transport> MakeV1Transport(NodeId nodeid) noexcept
320+
{
321+
return std::make_unique<V1Transport>(nodeid, SER_NETWORK, INIT_PROTO_VERSION);
322+
}
323+
324+
} // namespace
325+
326+
FUZZ_TARGET(p2p_transport_bidirectional, .init = initialize_p2p_transport_serialization)
327+
{
328+
// Test with two V1 transports talking to each other.
329+
FuzzedDataProvider provider{buffer.data(), buffer.size()};
330+
XoRoShiRo128PlusPlus rng(provider.ConsumeIntegral<uint64_t>());
331+
auto t1 = MakeV1Transport(NodeId{0});
332+
auto t2 = MakeV1Transport(NodeId{1});
333+
if (!t1 || !t2) return;
334+
SimulationTest(*t1, *t2, rng, provider);
335+
}

0 commit comments

Comments
 (0)