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 2 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",
],
)
212 changes: 212 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,212 @@
#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) {
// Setup downstream request.
auto downstream_headers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestRequestHeaderMapImpl>(
input.downstream_request().headers());
auto downstream_body =
Buffer::OwnedImpl(input.downstream_request().data().Get(0));
auto downstream_trailers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestRequestTrailerMapImpl>(
input.downstream_request().trailers());
// TODO(b/146671523): Uncomment this when implemented upstream.
// stream_info.addBytesReceived(downstream_body.length());

// Downstream functions under test.
filter.decodeHeaders(downstream_headers, false);
filter.decodeData(downstream_body, false);
filter.decodeData(downstream_body, true);
filter.decodeTrailers(downstream_trailers);

// Setup upstream response.
auto upstream_headers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseHeaderMapImpl>(
input.upstream_response().headers());
auto upstream_body =
Buffer::OwnedImpl(input.upstream_response().data().Get(0));
auto upstream_trailers =
Envoy::Fuzz::fromHeaders<Envoy::Http::TestResponseTrailerMapImpl>(
input.upstream_response().trailers());
// TODO(b/146671523): Uncomment this when implemented upstream.
// stream_info.addBytesSent(upstream_body.length());

// Upstream functions under test.
filter.encodeHeaders(upstream_headers, false);
filter.encodeData(upstream_body, false);
filter.encodeData(upstream_body, true);
filter.encodeTrailers(upstream_trailers);

// Report function under test.
filter.log(&downstream_headers, &upstream_headers, nullptr, 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 only need 1 requirement in the config, others will just add noise.
if (input.config().requirements_size() != 1) {
throw ProtoValidationException("requirements", input);
}
// We only expect 1 buffer in the body to simplify setup.
if (input.downstream_request().data().size() != 1) {
throw ProtoValidationException("downstream data", input);
}
if (input.upstream_response().data().size() != 1) {
throw ProtoValidationException("upstream data", input);
}
for (auto& sidestream_response : input.sidestream_response()) {
if (sidestream_response.data().size() != 1) {
throw ProtoValidationException("sidestream data", input);
}
}
// There should be at least 1 sidestream response, otherwise no point.
if (input.sidestream_response().size() < 1) {
throw ProtoValidationException("num sidestream", input);
}

// Setup mocks.
NiceMock<MockFactoryContext> context;
NiceMock<Http::MockAsyncClientRequest> response(
&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([&response, &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));
msg->body() =
std::make_unique<Buffer::OwnedImpl>(response_data.data().Get(0));

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

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

Choose a reason for hiding this comment

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

Instead of setting expectation (slow-down), can you set mock_decoder_callbacks.stream_info_ = stream_info, the expectation to return stream_info_ is already made in initialization. Does this speed-up?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately, this didn't work due to mocks vs fakes.

src/envoy/http/service_control/filter_fuzz_test.cc:182:41: error: no match for 'operator=' (operand types are 'testing::NiceMock<Envoy::StreamInfo::MockStreamInfo>' and 'Envoy::TestStreamInfo')
  182 |   mock_decoder_callbacks.stream_info_ = stream_info;

Copy link

Choose a reason for hiding this comment

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

Ahhhh. Gotcha. Didn't realize stream_info_ was a mock. Boo.

I think this is fine as is as a start, and you can iterat for speed ups. I think it'll be possible to make some of the mocks and expectations static and updating/cleaning up per-run, but it's a good task to iterate on.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, could you share an example of this in Envoy? I'm not familiar with how this will speed up execution.

Yup, I can iterate on this next week. I'd like to get this merged first so OSS-Fuzz will pick it up and display some initial fuzz stats.

Copy link

Choose a reason for hiding this comment

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

Sounds like a plan!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As discussed offline: envoyproxy/envoy#8397

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());
}

} catch (const ProtoValidationException& e) {
ENVOY_LOG_MISC(debug, "Controlled proto validation failure: {}", 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