blob: b642dacc6f9a3d0c609a9163696b89ba8366c751 [file] [log] [blame]
// 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