| // Copyright (c) 2025 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <algorithm> | |
| #include <cstdint> | |
| #include <cstring> | |
| #include <string> | |
| #include <variant> | |
| #include <vector> | |
| #include "absl/functional/overload.h" | |
| #include "absl/strings/string_view.h" | |
| #include "quiche/common/platform/api/quiche_fuzztest.h" | |
| #include "quiche/common/platform/api/quiche_test.h" | |
| #include "quiche/common/quiche_data_reader.h" | |
| #include "quiche/common/quiche_data_writer.h" | |
| #include "quiche/common/quiche_endian.h" | |
| namespace quiche { | |
| namespace { | |
| using fuzztest::Arbitrary; | |
| using fuzztest::ElementOf; | |
| using fuzztest::InRange; | |
| using fuzztest::Map; | |
| using fuzztest::StructOf; | |
| using fuzztest::VariantOf; | |
| using fuzztest::VectorOf; | |
| class MethodFuzzUtil { | |
| public: | |
| struct CallWriteUInt8 { | |
| uint8_t value; | |
| }; | |
| struct CallWriteUInt16 { | |
| uint16_t value; | |
| }; | |
| struct CallWriteUInt32 { | |
| uint32_t value; | |
| }; | |
| struct CallWriteUInt64 { | |
| uint64_t value; | |
| }; | |
| struct CallWriteBytesToUInt64 { | |
| size_t num_bytes; | |
| uint64_t value; | |
| }; | |
| struct CallWriteStringPiece { | |
| std::string value; | |
| }; | |
| struct CallWriteStringPiece16 { | |
| std::string value; | |
| }; | |
| struct CallWriteBytes { | |
| std::string data; | |
| }; | |
| struct CallWriteRepeatedByte { | |
| uint8_t byte; | |
| size_t count; | |
| }; | |
| struct CallWritePadding {}; | |
| struct CallWritePaddingBytes { | |
| size_t count; | |
| }; | |
| struct CallWriteTag { | |
| uint32_t tag; | |
| }; | |
| struct CallWriteVarInt62 { | |
| uint64_t value; | |
| }; | |
| struct CallWriteVarInt62WithForcedLength { | |
| uint64_t value; | |
| QuicheVariableLengthIntegerLength write_length; | |
| }; | |
| struct CallWriteStringPieceVarInt62 { | |
| std::string value; | |
| }; | |
| struct CallSeek { | |
| size_t length; | |
| }; | |
| using CallVariant = | |
| std::variant<CallWriteUInt8, CallWriteUInt16, CallWriteUInt32, | |
| CallWriteUInt64, CallWriteBytesToUInt64, | |
| CallWriteStringPiece, CallWriteStringPiece16, CallWriteBytes, | |
| CallWriteRepeatedByte, CallWritePadding, | |
| CallWritePaddingBytes, CallWriteTag, CallWriteVarInt62, | |
| CallWriteVarInt62WithForcedLength, | |
| CallWriteStringPieceVarInt62, CallSeek>; | |
| static fuzztest::Domain<CallVariant> CallVariantDomain() { | |
| return VariantOf( | |
| Arbitrary<MethodFuzzUtil::CallWriteUInt8>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteUInt16>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteUInt32>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteUInt64>(), | |
| StructOf<MethodFuzzUtil::CallWriteBytesToUInt64>( | |
| /*num_bytes=*/InRange<size_t>(1u, 8u), | |
| /*value=*/Arbitrary<uint64_t>()), | |
| Arbitrary<MethodFuzzUtil::CallWriteStringPiece>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteStringPiece16>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteBytes>(), | |
| StructOf<MethodFuzzUtil::CallWriteRepeatedByte>( | |
| /*byte=*/Arbitrary<uint8_t>(), | |
| /*count=*/InRange<size_t>(0u, 1u << 16u)), | |
| Arbitrary<MethodFuzzUtil::CallWritePadding>(), | |
| StructOf<MethodFuzzUtil::CallWritePaddingBytes>( | |
| InRange<size_t>(0u, 1u << 16u)), | |
| Arbitrary<MethodFuzzUtil::CallWriteTag>(), | |
| Arbitrary<MethodFuzzUtil::CallWriteVarInt62>(), | |
| Map( | |
| [](MethodFuzzUtil::CallWriteVarInt62WithForcedLength call) { | |
| // Increase `call.write_length` if `call.value` wouldn't fit. | |
| // This use of `std::max` is potentially fragile because it | |
| // depends on the comparability of the enum type | |
| // `QuicheVariableLengthIntegerLength`. | |
| call.write_length = | |
| std::max(call.write_length, | |
| QuicheDataWriter::GetVarInt62Len(call.value)); | |
| return call; | |
| }, | |
| StructOf<MethodFuzzUtil::CallWriteVarInt62WithForcedLength>( | |
| InRange<uint64_t>(0u, kVarInt62MaxValue), | |
| ElementOf(kAllQuicheVariableLengthIntegerLengths))), | |
| Arbitrary<MethodFuzzUtil::CallWriteStringPieceVarInt62>(), | |
| Arbitrary<MethodFuzzUtil::CallSeek>()); | |
| } | |
| }; | |
| // Interprets each element of `call_sequence` by calling the appropriate method | |
| // of `QuicheDataWriter`. For each writer call, it also takes a corresponding | |
| // action on a `QuicheDataReader` and makes a best effort to ensure that the | |
| // writer and reader agree. | |
| void WriterAndReaderStayInSync( | |
| size_t buffer_size, quiche::Endianness endianness, | |
| const std::vector<MethodFuzzUtil::CallVariant>& call_sequence) { | |
| std::vector<char> buffer(buffer_size); | |
| QuicheDataWriter writer(buffer.size(), buffer.data(), endianness); | |
| QuicheDataReader reader(buffer.data(), buffer.size(), endianness); | |
| for (const MethodFuzzUtil::CallVariant& call : call_sequence) { | |
| bool write_succeeded = true; | |
| std::visit( | |
| absl::Overload{ | |
| [&](const MethodFuzzUtil::CallWriteUInt8& call) { | |
| if (!writer.WriteUInt8(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint8_t value; | |
| ASSERT_TRUE(reader.ReadUInt8(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteUInt16& call) { | |
| if (!writer.WriteUInt16(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint16_t value; | |
| ASSERT_TRUE(reader.ReadUInt16(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteUInt32& call) { | |
| if (!writer.WriteUInt32(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint32_t value; | |
| ASSERT_TRUE(reader.ReadUInt32(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteUInt64& call) { | |
| if (!writer.WriteUInt64(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint64_t value; | |
| ASSERT_TRUE(reader.ReadUInt64(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteBytesToUInt64& call) { | |
| if (!writer.WriteBytesToUInt64(call.num_bytes, call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| // Ideally, we would test whether `parsed_value` has the expected | |
| // value, but it's difficult to compute the endianness-specific | |
| // least-significant bytes of `call.value` without reimplementing | |
| // a large part of `QuicheDataWriter::WriteBytesToUInt64()`. | |
| uint64_t parsed_value; | |
| ASSERT_TRUE( | |
| reader.ReadBytesToUInt64(call.num_bytes, &parsed_value)); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteStringPiece& call) { | |
| if (!writer.WriteStringPiece(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| absl::string_view value; | |
| ASSERT_TRUE(reader.ReadStringPiece(&value, call.value.length())); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteStringPiece16& call) { | |
| if (!writer.WriteStringPiece16(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| absl::string_view value; | |
| ASSERT_TRUE(reader.ReadStringPiece16(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteBytes& call) { | |
| if (!writer.WriteBytes(call.data.data(), call.data.length())) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| std::string temp(call.data.length(), '\0'); | |
| ASSERT_TRUE(reader.ReadBytes(temp.data(), temp.length())); | |
| ASSERT_EQ(call.data, temp); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteRepeatedByte& call) { | |
| if (!writer.WriteRepeatedByte(call.byte, call.count)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| absl::string_view value; | |
| ASSERT_TRUE(reader.ReadStringPiece(&value, call.count)); | |
| ASSERT_THAT(value, testing::Each(static_cast<char>(call.byte))); | |
| }, | |
| [&](const MethodFuzzUtil::CallWritePadding&) { | |
| size_t remaining = writer.remaining(); | |
| writer.WritePadding(); | |
| absl::string_view value = reader.ReadRemainingPayload(); | |
| ASSERT_EQ(value.length(), remaining); | |
| ASSERT_THAT(value, testing::Each('\0')); | |
| }, | |
| [&](const MethodFuzzUtil::CallWritePaddingBytes& call) { | |
| if (!writer.WritePaddingBytes(call.count)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| absl::string_view value; | |
| ASSERT_TRUE(reader.ReadStringPiece(&value, call.count)); | |
| ASSERT_THAT(value, testing::Each('\0')); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteTag& call) { | |
| if (!writer.WriteTag(call.tag)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint32_t value; | |
| ASSERT_TRUE(reader.ReadTag(&value)); | |
| ASSERT_EQ(call.tag, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteVarInt62& call) { | |
| if (endianness != NETWORK_BYTE_ORDER) { | |
| return; | |
| } | |
| if (!writer.WriteVarInt62(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint64_t value; | |
| ASSERT_TRUE(reader.ReadVarInt62(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteVarInt62WithForcedLength& call) { | |
| if (endianness != NETWORK_BYTE_ORDER) { | |
| return; | |
| } | |
| if (!writer.WriteVarInt62WithForcedLength(call.value, | |
| call.write_length)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| uint64_t value; | |
| ASSERT_TRUE(reader.ReadVarInt62(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallWriteStringPieceVarInt62& call) { | |
| if (endianness != NETWORK_BYTE_ORDER) { | |
| return; | |
| } | |
| if (!writer.WriteStringPieceVarInt62(call.value)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| absl::string_view value; | |
| ASSERT_TRUE(reader.ReadStringPieceVarInt62(&value)); | |
| ASSERT_EQ(call.value, value); | |
| }, | |
| [&](const MethodFuzzUtil::CallSeek& call) { | |
| if (!writer.Seek(call.length)) { | |
| write_succeeded = false; | |
| return; | |
| } | |
| ASSERT_TRUE(reader.Seek(call.length)); | |
| }, | |
| }, | |
| call); | |
| // `QuicheDataWriter` operations do not guarantee atomicity. For instance, | |
| // when `WriteStringPiece16()` fails, it may have successfully written the | |
| // length prefix, but failed to write the payload. As a consequence, after a | |
| // write operation fails, we cannot assume that the reader and writer will | |
| // still be in sync. | |
| if (!write_succeeded) { | |
| break; | |
| } | |
| ASSERT_EQ(writer.remaining(), reader.BytesRemaining()); | |
| ASSERT_EQ(writer.remaining() == 0, reader.IsDoneReading()); | |
| } | |
| } | |
| FUZZ_TEST(QuicheDataWriterFuzzTest, WriterAndReaderStayInSync) | |
| .WithDomains( | |
| /*buffer_size=*/InRange<size_t>(1u, 1024u * 1024u), | |
| /*endianness=*/ | |
| ElementOf({quiche::NETWORK_BYTE_ORDER, quiche::HOST_BYTE_ORDER}), | |
| /*call_sequence=*/VectorOf(MethodFuzzUtil::CallVariantDomain())); | |
| } // namespace | |
| } // namespace quiche |