blob: 8add7bcfe05367389ac4a463d0c445f55d51ce80 [file] [log] [blame] [edit]
// Copyright 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 <cstddef>
#include <string>
#include "absl/strings/escaping.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "quiche/balsa/balsa_enums.h"
#include "quiche/balsa/balsa_frame.h"
#include "quiche/balsa/balsa_fuzz_util.h"
#include "quiche/balsa/balsa_headers.h"
#include "quiche/balsa/http_validation_policy.h"
#include "quiche/balsa/simple_buffer.h"
#include "quiche/common/platform/api/quiche_fuzztest.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/platform/api/quiche_test.h"
namespace quiche {
namespace {
struct FuzzParams {
// This string is the input to `BalsaFrame::ProcessInput()`.
std::string input_to_parse;
HttpValidationPolicy http_validation_policy;
size_t max_header_length = 0;
bool is_request = false;
bool request_was_head = false;
bool allow_arbitrary_body = false;
bool allow_reading_until_close_for_request = false;
bool parse_truncated_headers_even_when_headers_too_long = false;
// Used by fuzztest as the "human-readable printer".
template <typename Sink>
friend void AbslStringify(Sink& sink, const FuzzParams& p) {
absl::Format(&sink,
"(\"%s\", http_validation_policy=%v, max_header_length=%v, "
"is_request=%v, request_was_head=%v, allow_arbitrary_body=%v,"
"allow_reading_until_close_for_request=%v, "
"parse_truncated_headers_even_when_headers_too_long=%v)",
absl::CHexEscape(p.input_to_parse), p.http_validation_policy,
p.max_header_length, p.is_request, p.request_was_head,
p.allow_arbitrary_body,
p.allow_reading_until_close_for_request,
p.parse_truncated_headers_even_when_headers_too_long);
}
};
void ConfigureBalsaFrame(const FuzzParams& params, BalsaFrame& out) {
out.set_http_validation_policy(params.http_validation_policy);
out.set_is_request(params.is_request);
out.set_request_was_head(params.request_was_head);
if (params.allow_arbitrary_body) {
out.AllowArbitraryBody();
}
out.set_max_header_length(params.max_header_length);
out.set_allow_reading_until_close_for_request(
params.allow_reading_until_close_for_request);
out.set_parse_truncated_headers_even_when_headers_too_long(
params.parse_truncated_headers_even_when_headers_too_long);
}
// This property test configures `BalsaFrame` with arbitrary parameters before
// asking it to parse an arbitrary input.
//
// Besides testing for crashes, this test also checks idempotency properties of
// header serialization and parsing.
//
// Graphically:
//
// Bytes BalsaFrame BalsaHeaders SimpleBuffer
// │ │ │ │
// a ──────> b ──────────> c1 ──────────> d1
// │ │ c2 <───────────┤
// │ │ ├────────────> d2
//
// The following properties should be true:
// 1. BalsaHeaders c1 and c2 contain the same headers.
// 2. SimpleBuffer d1 and d2 contain the same bytes.
void BalsaFrameParsesArbitraryInput(const FuzzParams& params) {
QUICHE_DVLOG(1) << "Input to parse: "
<< absl::CHexEscape(params.input_to_parse);
BalsaFrame framer;
ConfigureBalsaFrame(params, /*out=*/framer);
BalsaHeaders headers;
framer.set_balsa_headers(&headers);
const size_t num_bytes_consumed = framer.ProcessInput(
params.input_to_parse.data(), params.input_to_parse.size());
const std::string headers_debug_string = headers.DebugString();
QUICHE_DVLOG(1) << "Parsed headers: " << headers_debug_string;
EXPECT_LE(num_bytes_consumed, params.input_to_parse.size());
if (framer.Error() || !framer.MessageFullyRead() || headers.IsEmpty()) {
return;
}
// Serialize `headers` into `simple_buffer`.
SimpleBuffer simple_buffer;
size_t expected_write_buffer_size = headers.GetSizeForWriteBuffer();
headers.WriteHeaderAndEndingToBuffer(&simple_buffer);
absl::string_view readable_region = simple_buffer.GetReadableRegion();
EXPECT_EQ(expected_write_buffer_size,
static_cast<size_t>(simple_buffer.ReadableBytes()));
QUICHE_DVLOG(1) << "Serialized headers: "
<< absl::CHexEscape(readable_region);
// Parse `simple_buffer` into `headers2`.
framer.Reset();
ConfigureBalsaFrame(params, /*out=*/framer);
BalsaHeaders headers2;
framer.set_balsa_headers(&headers2);
const size_t num_bytes_consumed2 =
framer.ProcessInput(readable_region.data(), readable_region.size());
EXPECT_LE(num_bytes_consumed2,
static_cast<size_t>(simple_buffer.ReadableBytes()))
<< "Parsing should not consume more bytes than were serialized.";
// Usually, we should be able to parse our own serialization. One exception to
// the rule is that serializing a header can make it longer, so we will fail
// to parse headers that become longer than `params.max_header_length`.
if (framer.Error()) {
EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::HEADERS_TOO_LONG)
<< "Unexpectedly failed to parse our own serialization. Parse state: "
<< BalsaFrameEnums::ParseStateToString(framer.ParseState())
<< ", error code: "
<< BalsaFrameEnums::ErrorCodeToString(framer.ErrorCode())
<< ", readable_region.size(): " << readable_region.size()
<< ", max_header_length: " << params.max_header_length
<< ", original input: \"" << absl::CHexEscape(params.input_to_parse)
<< "\", serialization: \"" << absl::CHexEscape(readable_region) << "\"";
return;
}
const std::string headers_debug_string2 = headers2.DebugString();
QUICHE_DVLOG(1) << "Re-parsed headers: " << headers_debug_string2;
EXPECT_STREQ(headers_debug_string.c_str(), headers_debug_string2.c_str());
EXPECT_TRUE(framer.MessageFullyRead());
// Serialize `headers2` into `simple_buffer2`.
SimpleBuffer simple_buffer2;
headers2.WriteHeaderAndEndingToBuffer(&simple_buffer2);
QUICHE_DVLOG(1) << "Re-serialized headers: "
<< absl::CHexEscape(simple_buffer2.GetReadableRegion());
EXPECT_EQ(simple_buffer.GetReadableRegion(),
simple_buffer2.GetReadableRegion());
}
FUZZ_TEST(BalsaFrameTest, BalsaFrameParsesArbitraryInput)
.WithDomains(fuzztest::StructOf<FuzzParams>(
fuzztest::Arbitrary<std::string>(), ArbitraryHttpValidationPolicy(),
// When `max_header_length` is zero, `BalsaBuffer::StartOfFirstBlock()`
// hits the QUICHE_BUG named `bug_if_1182_1`. TBD whether this is a real
// bug or whether `max_header_length` should never be zero.
/*max_header_length=*/fuzztest::NonZero<size_t>(),
fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(),
fuzztest::Arbitrary<bool>(), fuzztest::Arbitrary<bool>(),
fuzztest::Arbitrary<bool>()));
// An earlier version of `BalsaFrameParsesArbitraryInput()` believed that the
// number of bytes returned from `frame.ProcessInput()` would be equal to the
// size of the input when `frame.MessageFullyRead()` is true. Now, it only
// checks that the number of bytes is <= the size of the input.
TEST(BalsaFrameTest, RegressionTestFuzzerBugParsingFewerBytesThanSerialized) {
FuzzParams params;
params.input_to_parse = "!\n";
params.max_header_length = 1024;
params.is_request = true;
BalsaFrameParsesArbitraryInput(params);
}
// An earlier version of `BalsaFrameParsesArbitraryInput()` believed that any
// serialization we produced was guaranteed to be parseable. However, it's
// possible for serialization to make the header longer than the max header
// length. In this case, "X\n" serializes to "X\r\n\r\n", which exceeds the max
// length of 2.
TEST(BalsaFrameTest, RegressionTestFuzzerBugHeaderTooLong) {
FuzzParams params;
params.input_to_parse = "X\n";
params.max_header_length = 2;
params.is_request = true;
BalsaFrameParsesArbitraryInput(params);
}
} // namespace
} // namespace quiche