Skip to content

Commit

Permalink
[fuzz] add fuzz tests for hpack encoding and decoding (#13315)
Browse files Browse the repository at this point in the history
Signed-off-by: Asra Ali <asraa@google.com>
  • Loading branch information
asraa authored Dec 21, 2020
1 parent 4ba2827 commit e90d674
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 0 deletions.
20 changes: 20 additions & 0 deletions test/common/http/http2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ load(
"envoy_cc_test",
"envoy_cc_test_library",
"envoy_package",
"envoy_proto_library",
)

licenses(["notice"]) # Apache 2
Expand Down Expand Up @@ -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",
],
)

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions test/common/http/http2/hpack_corpus/example

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions test/common/http/http2/hpack_corpus/example_many

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions test/common/http/http2/hpack_fuzz.proto
Original file line number Diff line number Diff line change
@@ -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;
}
155 changes: 155 additions & 0 deletions test/common/http/http2/hpack_fuzz_test.cc
Original file line number Diff line number Diff line change
@@ -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 <algorithm>

#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<nghttp2_nv> createNameValueArray(const test::fuzz::Headers& input) {
const size_t nvlen = input.headers().size();
std::vector<nghttp2_nv> 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<uint8_t*>(reinterpret_cast<const uint8_t*>(header.key().data())),
const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(header.value().data())),
header.key().size(), header.value().size(), flags};
}

return nva;
}

Buffer::OwnedImpl encodeHeaders(nghttp2_hd_deflater* deflater,
const std::vector<nghttp2_nv>& 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<uint8_t*>(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<nghttp2_nv> 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<nghttp2_nv> 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<uint8_t*>(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<uint8_t*>(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<char*>(a.name), a.namelen);
absl::string_view b_str(reinterpret_cast<char*>(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<nghttp2_nv> 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<nghttp2_nv> 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<char*>(input_nv[i].name), input_nv[i].namelen};
absl::string_view out_name = {reinterpret_cast<char*>(output_nv[i].name), output_nv[i].namelen};
absl::string_view in_val = {reinterpret_cast<char*>(input_nv[i].value), input_nv[i].valuelen};
absl::string_view out_val = {reinterpret_cast<char*>(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

0 comments on commit e90d674

Please sign in to comment.