Skip to content

Commit 5dfa0f5

Browse files
authored
Adding an HTTP/2 integration fuzzer (envoyproxy#10321)
Adding an HTTP/2 integration fuzzer that checks different kind of frames handling in both Downstream and Upstream Risk Level: Low - a new test Testing: It is a new fuzz test Signed-off-by: Adi Suissa-Peleg <[email protected]>
1 parent 7ce7a8d commit 5dfa0f5

9 files changed

+707
-4
lines changed

test/common/http/http2/http2_frame.cc

+53-4
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@ const char Http2Frame::Preamble[25] = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
3333
void Http2Frame::setHeader(absl::string_view header) {
3434
ASSERT(header.size() >= HeaderSize);
3535
data_.assign(HeaderSize, 0);
36+
// TODO(adisuissa): memcpy is discouraged as it may be unsafe. This should be
37+
// use a safer memcpy alternative (example: https://abseil.io/tips/93)
3638
memcpy(data_.data(), header.data(), HeaderSize);
3739
data_.resize(HeaderSize + payloadSize());
3840
}
3941

4042
void Http2Frame::setPayload(absl::string_view payload) {
4143
ASSERT(payload.size() >= payloadSize());
44+
ASSERT(data_.capacity() >= HeaderSize + payloadSize());
4245
memcpy(&data_[HeaderSize], payload.data(), payloadSize());
4346
}
4447

@@ -116,6 +119,7 @@ Http2Frame Http2Frame::makePingFrame(absl::string_view data) {
116119
static constexpr size_t kPingPayloadSize = 8;
117120
Http2Frame frame;
118121
frame.buildHeader(Type::Ping, kPingPayloadSize);
122+
ASSERT(frame.data_.capacity() >= HeaderSize + std::min(kPingPayloadSize, data.size()));
119123
if (!data.empty()) {
120124
memcpy(&frame.data_[HeaderSize], data.data(), std::min(kPingPayloadSize, data.size()));
121125
}
@@ -152,8 +156,46 @@ Http2Frame Http2Frame::makePriorityFrame(uint32_t stream_index, uint32_t depende
152156
static constexpr size_t kPriorityPayloadSize = 5;
153157
Http2Frame frame;
154158
frame.buildHeader(Type::Priority, kPriorityPayloadSize, 0, makeRequestStreamId(stream_index));
155-
uint32_t dependent_net = makeRequestStreamId(dependent_index);
156-
memcpy(&frame.data_[HeaderSize], reinterpret_cast<void*>(&dependent_net), sizeof(uint32_t));
159+
const uint32_t dependent_net = makeRequestStreamId(dependent_index);
160+
ASSERT(frame.data_.capacity() >= HeaderSize + sizeof(uint32_t));
161+
memcpy(&frame.data_[HeaderSize], reinterpret_cast<const void*>(&dependent_net), sizeof(uint32_t));
162+
return frame;
163+
}
164+
165+
Http2Frame Http2Frame::makeEmptyPushPromiseFrame(uint32_t stream_index,
166+
uint32_t promised_stream_index,
167+
HeadersFlags flags) {
168+
static constexpr size_t kEmptyPushPromisePayloadSize = 4;
169+
Http2Frame frame;
170+
frame.buildHeader(Type::PushPromise, kEmptyPushPromisePayloadSize, static_cast<uint8_t>(flags),
171+
makeRequestStreamId(stream_index));
172+
const uint32_t promised_stream_id = makeRequestStreamId(promised_stream_index);
173+
ASSERT(frame.data_.capacity() >= HeaderSize + sizeof(uint32_t));
174+
memcpy(&frame.data_[HeaderSize], reinterpret_cast<const void*>(&promised_stream_id),
175+
sizeof(uint32_t));
176+
return frame;
177+
}
178+
179+
Http2Frame Http2Frame::makeResetStreamFrame(uint32_t stream_index, ErrorCode error_code) {
180+
static constexpr size_t kResetStreamPayloadSize = 4;
181+
Http2Frame frame;
182+
frame.buildHeader(Type::RstStream, kResetStreamPayloadSize, 0, makeRequestStreamId(stream_index));
183+
const uint32_t error = static_cast<uint32_t>(error_code);
184+
ASSERT(frame.data_.capacity() >= HeaderSize + sizeof(uint32_t));
185+
memcpy(&frame.data_[HeaderSize], reinterpret_cast<const void*>(&error), sizeof(uint32_t));
186+
return frame;
187+
}
188+
189+
Http2Frame Http2Frame::makeEmptyGoAwayFrame(uint32_t last_stream_index, ErrorCode error_code) {
190+
static constexpr size_t kEmptyGoAwayPayloadSize = 8;
191+
Http2Frame frame;
192+
frame.buildHeader(Type::GoAway, kEmptyGoAwayPayloadSize, 0, makeRequestStreamId(0));
193+
const uint32_t last_stream_id = makeRequestStreamId(last_stream_index);
194+
ASSERT(frame.data_.capacity() >= HeaderSize + 4 + sizeof(uint32_t));
195+
memcpy(&frame.data_[HeaderSize], reinterpret_cast<const void*>(&last_stream_id),
196+
sizeof(uint32_t));
197+
const uint32_t error = static_cast<uint32_t>(error_code);
198+
memcpy(&frame.data_[HeaderSize + 4], reinterpret_cast<const void*>(&error), sizeof(uint32_t));
157199
return frame;
158200
}
159201

@@ -162,8 +204,9 @@ Http2Frame Http2Frame::makeWindowUpdateFrame(uint32_t stream_index, uint32_t inc
162204
Http2Frame frame;
163205
frame.buildHeader(Type::WindowUpdate, kWindowUpdatePayloadSize, 0,
164206
makeRequestStreamId(stream_index));
165-
uint32_t increment_net = htonl(increment);
166-
memcpy(&frame.data_[HeaderSize], reinterpret_cast<void*>(&increment_net), sizeof(uint32_t));
207+
const uint32_t increment_net = htonl(increment);
208+
ASSERT(frame.data_.capacity() >= HeaderSize + sizeof(uint32_t));
209+
memcpy(&frame.data_[HeaderSize], reinterpret_cast<const void*>(&increment_net), sizeof(uint32_t));
167210
return frame;
168211
}
169212

@@ -218,6 +261,12 @@ Http2Frame Http2Frame::makePostRequest(uint32_t stream_index, absl::string_view
218261
return frame;
219262
}
220263

264+
Http2Frame Http2Frame::makeGenericFrame(absl::string_view contents) {
265+
Http2Frame frame;
266+
frame.appendData(contents);
267+
return frame;
268+
}
269+
221270
} // namespace Http2
222271
} // namespace Http
223272
} // namespace Envoy

test/common/http/http2/http2_frame.h

+30
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,23 @@ class Http2Frame {
6666
Host = 38,
6767
};
6868

69+
enum class ErrorCode : uint8_t {
70+
NoError = 0,
71+
ProtocolError,
72+
InternalError,
73+
FlowControlError,
74+
SettingsTimeout,
75+
StreamClosed,
76+
FrameSizeError,
77+
RefusedStream,
78+
Cancel,
79+
CompressionError,
80+
ConnectError,
81+
EnhanceYourCalm,
82+
InadequateSecurity,
83+
Http11Required
84+
};
85+
6986
enum class ResponseStatus { Unknown, Ok, NotFound };
7087

7188
// Methods for creating HTTP2 frames
@@ -77,6 +94,12 @@ class Http2Frame {
7794
HeadersFlags flags = HeadersFlags::None);
7895
static Http2Frame makeEmptyDataFrame(uint32_t stream_index, DataFlags flags = DataFlags::None);
7996
static Http2Frame makePriorityFrame(uint32_t stream_index, uint32_t dependent_index);
97+
98+
static Http2Frame makeEmptyPushPromiseFrame(uint32_t stream_index, uint32_t promised_stream_index,
99+
HeadersFlags flags = HeadersFlags::None);
100+
static Http2Frame makeResetStreamFrame(uint32_t stream_index, ErrorCode error_code);
101+
static Http2Frame makeEmptyGoAwayFrame(uint32_t last_stream_index, ErrorCode error_code);
102+
80103
static Http2Frame makeWindowUpdateFrame(uint32_t stream_index, uint32_t increment);
81104
static Http2Frame makeMalformedRequest(uint32_t stream_index);
82105
static Http2Frame makeMalformedRequestWithZerolenHeader(uint32_t stream_index,
@@ -86,6 +109,13 @@ class Http2Frame {
86109
absl::string_view path);
87110
static Http2Frame makePostRequest(uint32_t stream_index, absl::string_view host,
88111
absl::string_view path);
112+
/**
113+
* Creates a frame with the given contents. This frame can be
114+
* malformed/invalid depending on the given contents.
115+
* @param contents the contents of the newly created frame.
116+
* @return an Http2Frame that is comprised of the given contents.
117+
*/
118+
static Http2Frame makeGenericFrame(absl::string_view contents);
89119

90120
Type type() const { return static_cast<Type>(data_[3]); }
91121
ResponseStatus responseStatus() const;

test/integration/BUILD

+68
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ envoy_proto_library(
107107
srcs = [":capture_fuzz.proto"],
108108
)
109109

110+
envoy_proto_library(
111+
name = "h2_capture_fuzz_proto",
112+
srcs = [":h2_capture_fuzz.proto"],
113+
)
114+
110115
envoy_cc_test(
111116
name = "cds_integration_test",
112117
srcs = ["cds_integration_test.cc"],
@@ -1110,6 +1115,69 @@ envoy_cc_fuzz_test(
11101115
],
11111116
)
11121117

1118+
H2_FUZZ_LIB_DEPS = [
1119+
":h2_capture_fuzz_proto_cc_proto",
1120+
":http_integration_lib",
1121+
"//source/common/common:assert_lib",
1122+
"//source/common/common:logger_lib",
1123+
"//test/common/http/http2:http2_frame",
1124+
"//test/fuzz:fuzz_runner_lib",
1125+
"//test/fuzz:utility_lib",
1126+
"//test/integration:integration_lib",
1127+
"//test/test_common:environment_lib",
1128+
]
1129+
1130+
envoy_cc_test_library(
1131+
name = "h2_fuzz_lib",
1132+
srcs = ["h2_fuzz.cc"],
1133+
hdrs = ["h2_fuzz.h"],
1134+
deps = H2_FUZZ_LIB_DEPS,
1135+
)
1136+
1137+
envoy_cc_test_library(
1138+
name = "h2_fuzz_persistent_lib",
1139+
srcs = ["h2_fuzz.cc"],
1140+
hdrs = ["h2_fuzz.h"],
1141+
copts = ["-DPERSISTENT_FUZZER"],
1142+
deps = H2_FUZZ_LIB_DEPS,
1143+
)
1144+
1145+
envoy_cc_fuzz_test(
1146+
name = "h2_capture_fuzz_test",
1147+
srcs = ["h2_capture_fuzz_test.cc"],
1148+
corpus = "h2_corpus",
1149+
deps = [":h2_fuzz_lib"],
1150+
)
1151+
1152+
envoy_cc_fuzz_test(
1153+
name = "h2_capture_persistent_fuzz_test",
1154+
srcs = ["h2_capture_fuzz_test.cc"],
1155+
copts = ["-DPERSISTENT_FUZZER"],
1156+
corpus = "h2_corpus",
1157+
deps = [":h2_fuzz_persistent_lib"],
1158+
)
1159+
1160+
envoy_cc_fuzz_test(
1161+
name = "h2_capture_direct_response_fuzz_test",
1162+
srcs = ["h2_capture_direct_response_fuzz_test.cc"],
1163+
corpus = "h2_corpus",
1164+
deps = [
1165+
":h2_fuzz_lib",
1166+
"@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto",
1167+
],
1168+
)
1169+
1170+
envoy_cc_fuzz_test(
1171+
name = "h2_capture_direct_response_persistent_fuzz_test",
1172+
srcs = ["h2_capture_direct_response_fuzz_test.cc"],
1173+
copts = ["-DPERSISTENT_FUZZER"],
1174+
corpus = "h2_corpus",
1175+
deps = [
1176+
":h2_fuzz_persistent_lib",
1177+
"@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto",
1178+
],
1179+
)
1180+
11131181
envoy_cc_test(
11141182
name = "scoped_rds_integration_test",
11151183
srcs = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h"
2+
3+
#include "test/integration/h2_fuzz.h"
4+
5+
namespace Envoy {
6+
7+
void H2FuzzIntegrationTest::initialize() {
8+
const std::string body = "Response body";
9+
const std::string file_path = TestEnvironment::writeStringToFileForTest("test_envoy", body);
10+
const std::string prefix("/");
11+
const Http::Code status(Http::Code::OK);
12+
13+
setDownstreamProtocol(Http::CodecClient::Type::HTTP2);
14+
setUpstreamProtocol(FakeHttpConnection::Type::HTTP2);
15+
16+
config_helper_.addConfigModifier(
17+
[&file_path, &prefix](
18+
envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
19+
hcm) -> void {
20+
auto* route_config = hcm.mutable_route_config();
21+
// adding direct response mode to the default route
22+
auto* default_route =
23+
hcm.mutable_route_config()->mutable_virtual_hosts(0)->mutable_routes(0);
24+
default_route->mutable_match()->set_prefix(prefix);
25+
default_route->mutable_direct_response()->set_status(static_cast<uint32_t>(status));
26+
default_route->mutable_direct_response()->mutable_body()->set_filename(file_path);
27+
// adding headers to the default route
28+
auto* header_value_option = route_config->mutable_response_headers_to_add()->Add();
29+
header_value_option->mutable_header()->set_value("direct-response-enabled");
30+
header_value_option->mutable_header()->set_key("x-direct-response-header");
31+
});
32+
HttpIntegrationTest::initialize();
33+
}
34+
35+
DEFINE_PROTO_FUZZER(const test::integration::H2CaptureFuzzTestCase& input) {
36+
RELEASE_ASSERT(!TestEnvironment::getIpVersionsForTest().empty(), "");
37+
const auto ip_version = TestEnvironment::getIpVersionsForTest()[0];
38+
PERSISTENT_FUZZ_VAR H2FuzzIntegrationTest h2_fuzz_integration_test(ip_version);
39+
h2_fuzz_integration_test.replay(input, true);
40+
}
41+
42+
} // namespace Envoy

0 commit comments

Comments
 (0)