| // 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 |