-
Notifications
You must be signed in to change notification settings - Fork 170
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
Changes from 4 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
|
||
// 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. | ||
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). | ||
end_stream = false; | ||
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); | ||
nareddyt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
orContinue
, and that will probably never change. Our filter should support all 3 being called.There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 thendecodeData(_, _)
somewhere?There was a problem hiding this comment.
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.