Skip to content

Add rudimentary fuzz test for service control filter #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/envoy/http/service_control/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SERVICE_CONTROL_VISIBILITY = [
"//src/envoy/http/service_control:__subpackages__",
"//src/go:__subpackages__",
"//tests/utils:__subpackages__",
"//tests/fuzz/structured_inputs:__subpackages__",
]

package(default_visibility = SERVICE_CONTROL_VISIBILITY)
Expand Down
3 changes: 2 additions & 1 deletion src/api_proxy/auth/auth_token_fuzz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ namespace auth {
namespace fuzz {

DEFINE_PROTO_FUZZER(const tests::fuzz::protos::AuthTokenInput& input) {
char* token = get_auth_token(input.secret().c_str(), input.audience().c_str());
char* token =
get_auth_token(input.secret().c_str(), input.audience().c_str());
if (token != nullptr) {
grpc_free(token);
}
Expand Down
18 changes: 18 additions & 0 deletions src/envoy/http/service_control/BUILD
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
load(
"@envoy//bazel:envoy_build_system.bzl",
"envoy_cc_fuzz_test",
"envoy_cc_library",
"envoy_cc_test",
)
Expand Down Expand Up @@ -261,3 +262,20 @@ envoy_cc_test(
"@envoy//test/test_common:utility_lib",
],
)

envoy_cc_fuzz_test(
name = "service_control_filter_fuzz_test",
srcs = ["filter_fuzz_test.cc"],
corpus = "//tests/fuzz/corpus:service_control_filter_corpus",
repository = "@envoy",
deps = [
":filter_config_lib",
":filter_lib",
"//src/envoy/utils:filter_state_utils_lib",
"//tests/fuzz/structured_inputs:service_control_filter_proto_cc_proto",
"@envoy//test/fuzz:utility_lib",
"@envoy//test/mocks/init:init_mocks",
"@envoy//test/mocks/server:server_mocks",
"@envoy//test/test_common:utility_lib",
],
)
222 changes: 222 additions & 0 deletions src/envoy/http/service_control/filter_fuzz_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
#include "google/protobuf/text_format.h"
#include "test/fuzz/fuzz_runner.h"
#include "test/fuzz/utility.h"
#include "test/mocks/http/mocks.h"
#include "test/mocks/server/mocks.h"

#include "api/envoy/http/service_control/config.pb.validate.h"
#include "src/envoy/http/service_control/filter.h"
#include "src/envoy/http/service_control/filter_config.h"
#include "src/envoy/utils/filter_state_utils.h"
#include "tests/fuzz/structured_inputs/service_control_filter.pb.validate.h"

#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include <fstream>
#include <stdexcept>
#include <string>

namespace filter_api = ::google::api::envoy::http::service_control;
namespace sc_api = ::google::api::servicecontrol::v1;
using ::Envoy::Server::Configuration::MockFactoryContext;
using ::testing::MockFunction;
using ::testing::Return;
using ::testing::ReturnRef;

namespace Envoy {
namespace Extensions {
namespace HttpFilters {
namespace ServiceControl {
namespace Fuzz {

void doTest(ServiceControlFilter& filter, TestStreamInfo& stream_info,
const tests::fuzz::protos::ServiceControlFilterInput& input) {
// Decode headers.
bool end_stream = false;
auto downstream_headers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestRequestHeaderMapImpl>(
input.downstream_request().headers());
if (input.downstream_request().data().size() == 0 &&
!input.downstream_request().has_trailers()) {
end_stream = true;
}
filter.decodeHeaders(downstream_headers, end_stream);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then similarly, is there any reason to consider the filter status in case there's a reason to stop decoding?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this filter, we only ever StopIteration or Continue, and that will probably never change. Our filter should support all 3 being called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to make sure end_stream is correct? Sometime, due to bug, end_stream may not called correctly. we still need to fuzz these data flow.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying it's possible to execute decodeHeaders(_, true) and then decodeData(_, _) somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@qiwzhang I think it's reasonable to expect end_stream is set correctly, this is part of the Envoy filter API. If it's set incorrectly, some upstream filters would not function. I don't think this will ever happen, but we can fuzz it if you believe it can.


// Decode body (if needed).
for (int i = 0; i < input.downstream_request().data().size(); i++) {
if (i == input.downstream_request().data().size() - 1 &&
!input.downstream_request().has_trailers()) {
end_stream = true;
}
Buffer::OwnedImpl buffer(input.downstream_request().data().Get(i));
filter.decodeData(buffer, end_stream);
}

// Decode trailers (if needed).
auto downstream_trailers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestRequestTrailerMapImpl>(
input.downstream_request().trailers());
if (input.downstream_request().has_trailers()) {
filter.decodeTrailers(downstream_trailers);
}

// Encode headers.
end_stream = false;
auto upstream_headers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseHeaderMapImpl>(
input.upstream_response().headers());
if (input.upstream_response().data().size() == 0 &&
!input.upstream_response().has_trailers()) {
end_stream = true;
}
filter.encodeHeaders(upstream_headers, end_stream);

// Encode body (if needed).
for (int i = 0; i < input.upstream_response().data().size(); i++) {
if (i == input.upstream_response().data().size() - 1 &&
!input.upstream_response().has_trailers()) {
end_stream = true;
}
Buffer::OwnedImpl buffer(input.upstream_response().data().Get(i));
filter.encodeData(buffer, end_stream);
}

// Encode trailers (if needed).
auto upstream_trailers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseTrailerMapImpl>(
input.upstream_response().trailers());
if (input.upstream_response().has_trailers()) {
filter.encodeTrailers(upstream_trailers);
}

// Access log (report).
filter.log(&downstream_headers, &upstream_headers, &upstream_trailers,
stream_info);
}

DEFINE_PROTO_FUZZER(
const tests::fuzz::protos::ServiceControlFilterInput& input) {
ENVOY_LOG_MISC(trace, "{}", input.DebugString());

try {
TestUtility::validate(input);

// Validate nested protos with stricter requirements for the fuzz test.
// We need at least 1 requirement in the config to match a selector.
if (input.config().requirements_size() < 1) {
throw ProtoValidationException("Need at least 1 requirement", input);
}
} catch (const ProtoValidationException& e) {
ENVOY_LOG_MISC(debug, "Controlled proto validation failure: {}", e.what());
return;
}

// Setup mocks.
NiceMock<MockFactoryContext> context;
NiceMock<Http::MockAsyncClientRequest> request(
&context.cluster_manager_.async_client_);
NiceMock<Envoy::Http::MockStreamDecoderFilterCallbacks>
mock_decoder_callbacks;
NiceMock<Envoy::Http::MockStreamEncoderFilterCallbacks>
mock_encoder_callbacks;

// Return a fake span.
EXPECT_CALL(mock_decoder_callbacks, activeSpan())
.WillRepeatedly(ReturnRef(Envoy::Tracing::NullSpan::instance()));

// Callback for token subscriber to start.
Envoy::Event::TimerCb onReadyCallback;
EXPECT_CALL(context.dispatcher_, createTimer_(_))
.WillRepeatedly(
Invoke([&onReadyCallback](const Envoy::Event::TimerCb& cb) {
ENVOY_LOG_MISC(trace, "Mocking dispatcher createTimer");
onReadyCallback = cb;
return new NiceMock<Event::MockTimer>();
}));

// Mock the http async client.
int resp_num = 0;
EXPECT_CALL(context.cluster_manager_.async_client_, send_(_, _, _))
.WillRepeatedly(Invoke([&request, &input, &resp_num](
const Envoy::Http::RequestMessagePtr&,
Envoy::Http::AsyncClient::Callbacks& callback,
const Envoy::Http::AsyncClient::
RequestOptions&) {
// FIXME(nareddyt): For now, just increment the counter for
// response numbers.
auto& response_data = input.sidestream_response().Get(
resp_num++ % input.sidestream_response().size());

// Create the response message.
auto headers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseHeaderMapImpl>(
response_data.headers());
auto headers_ptr =
std::make_unique<Envoy::Http::TestResponseHeaderMapImpl>(headers);
auto trailers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseTrailerMapImpl>(
response_data.trailers());
auto trailers_ptr =
std::make_unique<Envoy::Http::TestResponseTrailerMapImpl>(trailers);

auto msg = std::make_unique<Envoy::Http::ResponseMessageImpl>(
std::move(headers_ptr));
msg->trailers(std::move(trailers_ptr));
if (response_data.data_size() > 0) {
// FIXME(nareddyt): For now, just grab 1 data item from the
// proto.
msg->body() =
std::make_unique<Buffer::OwnedImpl>(response_data.data().Get(0));
} else {
msg->body() = std::make_unique<Buffer::OwnedImpl>();
}

// Callback.
callback.onSuccess(std::move(msg));
return &request;
}));

// Fuzz the stream info.
TestStreamInfo stream_info = Envoy::Fuzz::fromStreamInfo(input.stream_info());
EXPECT_CALL(mock_decoder_callbacks, streamInfo())
.WillRepeatedly(ReturnRef(stream_info));
EXPECT_CALL(mock_encoder_callbacks, streamInfo())
.WillRepeatedly(ReturnRef(stream_info));

try {
// Create filter config.
ServiceControlFilterConfig filter_config(input.config(), "fuzz-test-stats",
context);

// Set the operation name to match an endpoint that requires API keys
// and has configured metric costs.
// This ensures both CHECK and QUOTA are called.
Utils::setStringFilterState(
*stream_info.filter_state_, Utils::kOperation,
input.config().requirements(0).operation_name());

// Create filter.
ServiceControlFilter filter(filter_config.stats(),
filter_config.handler_factory());
filter.setDecoderFilterCallbacks(mock_decoder_callbacks);
filter.setEncoderFilterCallbacks(mock_encoder_callbacks);

if (onReadyCallback != nullptr) {
// Filter config is valid enough to start the token subscriber.
onReadyCallback();
}

// Run data against the filter.
doTest(filter, stream_info, input);

} catch (const EnvoyException& e) {
ENVOY_LOG_MISC(debug, "Controlled envoy exception: {}", e.what());
}
}

} // namespace Fuzz
} // namespace ServiceControl
} // namespace HttpFilters
} // namespace Extensions
} // namespace Envoy
50 changes: 29 additions & 21 deletions src/envoy/http/service_control/http_call.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,32 +72,40 @@ class HttpCallImpl : public HttpCall,
// HTTP async receive methods
void onSuccess(Http::ResponseMessagePtr&& response) override {
ENVOY_LOG(trace, "{}", __func__);
const uint64_t status_code =
Http::Utility::getResponseStatus(response->headers());

request_span_->setTag(Tracing::Tags::get().HttpStatusCode,
std::to_string(status_code));
request_span_->finishSpan();

std::string body;
if (response->body()) {
const auto len = response->body()->length();
body = std::string(static_cast<char*>(response->body()->linearize(len)),
len);
}
if (status_code == enumToInt(Http::Code::OK)) {
ENVOY_LOG(debug, "http call [uri = {}]: success with body {}", uri_,
body);
on_done_(Status::OK, body);
} else {
if (attemptRetry(status_code)) {
return;
}
try {
const uint64_t status_code =
Http::Utility::getResponseStatus(response->headers());

ENVOY_LOG(debug, "http call response status code: {}, body: {}",
status_code, body);
request_span_->setTag(Tracing::Tags::get().HttpStatusCode,
std::to_string(status_code));
request_span_->finishSpan();

if (response->body()) {
const auto len = response->body()->length();
body = std::string(static_cast<char*>(response->body()->linearize(len)),
len);
}
if (status_code == enumToInt(Http::Code::OK)) {
ENVOY_LOG(debug, "http call [uri = {}]: success with body {}", uri_,
body);
on_done_(Status::OK, body);
} else {
if (attemptRetry(status_code)) {
return;
}

ENVOY_LOG(debug, "http call response status code: {}, body: {}",
status_code, body);
on_done_(Status(Code::INTERNAL, "Failed to call service control"),
body);
}
} catch (const EnvoyException& e) {
ENVOY_LOG(debug, "http call invalid status");
on_done_(Status(Code::INTERNAL, "Failed to call service control"), body);
}

reset();
deferredDelete();
}
Expand Down
8 changes: 8 additions & 0 deletions tests/fuzz/corpus/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,11 @@ filegroup(
"auth_token/**",
]),
)

filegroup(
name = "service_control_filter_corpus",
testonly = 1,
srcs = glob([
"service_control_filter/**",
]),
)
Loading