diff --git a/test/common/http/http2/BUILD b/test/common/http/http2/BUILD index 8dca77e677b5..9c319e500896 100644 --- a/test/common/http/http2/BUILD +++ b/test/common/http/http2/BUILD @@ -4,6 +4,7 @@ load( "envoy_cc_test", "envoy_cc_test_library", "envoy_package", + "envoy_proto_library", ) licenses(["notice"]) # Apache 2 @@ -177,3 +178,22 @@ envoy_cc_fuzz_test( "//test/common/http/http2:codec_impl_test_util", ], ) + +envoy_proto_library( + name = "hpack_fuzz_proto", + srcs = ["hpack_fuzz.proto"], + deps = ["//test/fuzz:common_proto"], +) + +envoy_cc_fuzz_test( + name = "hpack_fuzz_test", + srcs = ["hpack_fuzz_test.cc"], + corpus = "hpack_corpus", + external_deps = [ + "nghttp2", + ], + deps = [ + ":hpack_fuzz_proto_cc_proto", + "//test/test_common:utility_lib", + ], +) diff --git a/test/common/http/http2/hpack_corpus/crash-52ef0a2d4d861941325ba57fde63d2aa700f43cf b/test/common/http/http2/hpack_corpus/crash-52ef0a2d4d861941325ba57fde63d2aa700f43cf new file mode 100644 index 000000000000..c64b2e0619af --- /dev/null +++ b/test/common/http/http2/hpack_corpus/crash-52ef0a2d4d861941325ba57fde63d2aa700f43cf @@ -0,0 +1,3 @@ +headers { +} +end_headers: true diff --git a/test/common/http/http2/hpack_corpus/example b/test/common/http/http2/hpack_corpus/example new file mode 100644 index 000000000000..4c0e0912324d --- /dev/null +++ b/test/common/http/http2/hpack_corpus/example @@ -0,0 +1,14 @@ +headers { + headers { + key: ":method" + value: "GET" + } + headers { + key: ":path" + value: "/" + } + headers { + key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } +} \ No newline at end of file diff --git a/test/common/http/http2/hpack_corpus/example_many b/test/common/http/http2/hpack_corpus/example_many new file mode 100644 index 000000000000..efc059ff8e00 --- /dev/null +++ b/test/common/http/http2/hpack_corpus/example_many @@ -0,0 +1,30 @@ +headers { + headers { + key: ":method" + value: "GET" + } + headers { + key: ":path" + value: "/" + } + headers { + key: ":scheme" + value: "http" + } + headers { + key: ":host" + value: "foo" + } + headers { + key: "x-envoy" + value: "one_value" + } + headers { + key: "x-envoy" + value: "another" + } + headers { + key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + value: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } +} \ No newline at end of file diff --git a/test/common/http/http2/hpack_fuzz.proto b/test/common/http/http2/hpack_fuzz.proto new file mode 100644 index 000000000000..1e91c39ea596 --- /dev/null +++ b/test/common/http/http2/hpack_fuzz.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package test.common.http.http2; + +import "test/fuzz/common.proto"; + +import "validate/validate.proto"; + +// Structured input for hpack_fuzz_test. + +message HpackTestCase { + test.fuzz.Headers headers = 1 [(validate.rules).message.required = true]; + bool end_headers = 2; +} \ No newline at end of file diff --git a/test/common/http/http2/hpack_fuzz_test.cc b/test/common/http/http2/hpack_fuzz_test.cc new file mode 100644 index 000000000000..6cab23df21f4 --- /dev/null +++ b/test/common/http/http2/hpack_fuzz_test.cc @@ -0,0 +1,155 @@ +// Fuzzer for HPACK encoding and decoding. +// TODO(asraa): Speed up by using raw byte input and separators rather than protobuf input. + +#include + +#include "test/common/http/http2/hpack_fuzz.pb.validate.h" +#include "test/fuzz/fuzz_runner.h" +#include "test/test_common/utility.h" + +#include "absl/container/fixed_array.h" +#include "nghttp2/nghttp2.h" + +namespace Envoy { +namespace Http { +namespace Http2 { +namespace { + +// Dynamic Header Table Size +constexpr int kHeaderTableSize = 4096; + +std::vector createNameValueArray(const test::fuzz::Headers& input) { + const size_t nvlen = input.headers().size(); + std::vector nva(nvlen); + int i = 0; + for (const auto& header : input.headers()) { + // TODO(asraa): Consider adding flags in fuzzed input. + const uint8_t flags = 0; + nva[i++] = {const_cast(reinterpret_cast(header.key().data())), + const_cast(reinterpret_cast(header.value().data())), + header.key().size(), header.value().size(), flags}; + } + + return nva; +} + +Buffer::OwnedImpl encodeHeaders(nghttp2_hd_deflater* deflater, + const std::vector& input_nv) { + // Estimate the upper bound + const size_t buflen = nghttp2_hd_deflate_bound(deflater, input_nv.data(), input_nv.size()); + + Buffer::RawSlice iovec; + Buffer::OwnedImpl payload; + payload.reserve(buflen, &iovec, 1); + ASSERT(iovec.len_ >= buflen); + + // Encode using nghttp2 + uint8_t* buf = reinterpret_cast(iovec.mem_); + ASSERT(input_nv.data() != nullptr); + const ssize_t result = + nghttp2_hd_deflate_hd(deflater, buf, buflen, input_nv.data(), input_nv.size()); + ASSERT(result >= 0, absl::StrCat("Failed to decode with result ", result)); + + iovec.len_ = result; + payload.commit(&iovec, 1); + + return payload; +} + +std::vector decodeHeaders(nghttp2_hd_inflater* inflater, + const Buffer::OwnedImpl& payload, bool end_headers) { + // Decode using nghttp2 + Buffer::RawSliceVector slices = payload.getRawSlices(); + const int num_slices = slices.size(); + ASSERT(num_slices == 1, absl::StrCat("number of slices ", num_slices)); + + std::vector decoded_headers; + int inflate_flags = 0; + nghttp2_nv decoded_nv; + while (slices[0].len_ > 0) { + ssize_t result = nghttp2_hd_inflate_hd2(inflater, &decoded_nv, &inflate_flags, + reinterpret_cast(slices[0].mem_), + slices[0].len_, end_headers); + // Decoding should not fail and data should not be left in slice. + ASSERT(result >= 0); + + slices[0].mem_ = reinterpret_cast(slices[0].mem_) + result; + slices[0].len_ -= result; + + if (inflate_flags & NGHTTP2_HD_INFLATE_EMIT) { + // One header key value pair has been successfully decoded. + decoded_headers.push_back(decoded_nv); + } + } + + if (end_headers) { + nghttp2_hd_inflate_end_headers(inflater); + } + + return decoded_headers; +} + +struct NvComparator { + inline bool operator()(const nghttp2_nv& a, const nghttp2_nv& b) { + absl::string_view a_str(reinterpret_cast(a.name), a.namelen); + absl::string_view b_str(reinterpret_cast(b.name), b.namelen); + return a_str.compare(b_str); + } +}; + +DEFINE_PROTO_FUZZER(const test::common::http::http2::HpackTestCase& input) { + // Validate headers. + try { + TestUtility::validate(input); + } catch (const EnvoyException& e) { + ENVOY_LOG_MISC(trace, "EnvoyException: {}", e.what()); + return; + } + + // Create name value pairs from headers. + std::vector input_nv = createNameValueArray(input.headers()); + // Skip encoding empty headers. nghttp2 will throw a nullptr error on runtime if it receives a + // nullptr input. + if (!input_nv.data()) { + return; + } + + // Create Deflater and Inflater + nghttp2_hd_deflater* deflater = nullptr; + int rc = nghttp2_hd_deflate_new(&deflater, kHeaderTableSize); + ASSERT(rc == 0); + nghttp2_hd_inflater* inflater = nullptr; + rc = nghttp2_hd_inflate_new(&inflater); + ASSERT(rc == 0); + + // Encode headers with nghttp2. + const Buffer::OwnedImpl payload = encodeHeaders(deflater, input_nv); + ASSERT(!payload.getRawSlices().empty()); + + // Decode headers with nghttp2 + std::vector output_nv = decodeHeaders(inflater, payload, input.end_headers()); + + // Verify that decoded == encoded. + ASSERT(input_nv.size() == output_nv.size()); + std::sort(input_nv.begin(), input_nv.end(), NvComparator()); + std::sort(output_nv.begin(), output_nv.end(), NvComparator()); + for (size_t i = 0; i < input_nv.size(); i++) { + absl::string_view in_name = {reinterpret_cast(input_nv[i].name), input_nv[i].namelen}; + absl::string_view out_name = {reinterpret_cast(output_nv[i].name), output_nv[i].namelen}; + absl::string_view in_val = {reinterpret_cast(input_nv[i].value), input_nv[i].valuelen}; + absl::string_view out_val = {reinterpret_cast(output_nv[i].value), + output_nv[i].valuelen}; + ASSERT(in_name == out_name); + ASSERT(in_val == out_val); + } + + // Delete inflater + nghttp2_hd_inflate_del(inflater); + // Delete deflater. + nghttp2_hd_deflate_del(deflater); +} + +} // namespace +} // namespace Http2 +} // namespace Http +} // namespace Envoy