blob: f15ccfbdb90b692b8d3a50417609fe7f31e0000f [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stdint.h>
#include <utility>
#include "base/at_exit.h"
#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/files/scoped_temp_dir.h"
#include "base/i18n/icu_util.h"
#include "base/test/test_switches.h"
#include "base/test/test_timeouts.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "content/browser/code_cache/generated_code_cache_context.h" // [nogncheck]
#include "content/browser/renderer_host/code_cache_host_impl.h" // [nogncheck]
#include "content/browser/storage_partition_impl_map.h" // [nogncheck]
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_content_client_initializer.h"
#include "content/test/fuzzer/code_cache_host_mojolpm_fuzzer.pb.h"
#include "mojo/core/embedder/embedder.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "third_party/blink/public/mojom/loader/code_cache.mojom-mojolpm.h"
#include "third_party/libprotobuf-mutator/src/src/libfuzzer/libfuzzer_macro.h"
#include "url/origin.h"
using url::Origin;
const char* cmdline[] = {"code_cache_host_mojolpm_fuzzer", nullptr};
// Global environment needed to run the interface being tested.
//
// This will be created once, before fuzzing starts, and will be shared between
// all testcases. It is created on the main thread.
//
// At a minimum, we should always be able to set up the command line, i18n and
// mojo, and create the thread on which the fuzzer will be run. We want to avoid
// (as much as is reasonable) any state being preserved between testcases.
//
// For CodeCacheHost, we can also safely re-use a single BrowserTaskEnvironment
// and the TestContentClientInitializer between testcases. We try to create an
// environment that matches the real browser process as much as possible, so we
// use real platform threads in the task environment.
class ContentFuzzerEnvironment {
public:
ContentFuzzerEnvironment()
: fuzzer_thread_("fuzzer_thread"),
task_environment_(
(base::CommandLine::Init(1, cmdline),
TestTimeouts::Initialize(),
base::test::TaskEnvironment::MainThreadType::DEFAULT),
base::test::TaskEnvironment::ThreadPoolExecutionMode::ASYNC,
base::test::TaskEnvironment::ThreadingMode::MULTIPLE_THREADS,
content::BrowserTaskEnvironment::REAL_IO_THREAD) {
logging::SetMinLogLevel(logging::LOG_FATAL);
mojo::core::Init();
base::i18n::InitializeICU();
fuzzer_thread_.StartAndWaitForTesting();
}
scoped_refptr<base::SequencedTaskRunner> fuzzer_task_runner() {
return fuzzer_thread_.task_runner();
}
private:
base::AtExitManager at_exit_manager_;
base::Thread fuzzer_thread_;
content::BrowserTaskEnvironment task_environment_;
content::TestContentClientInitializer content_client_initializer_;
};
ContentFuzzerEnvironment& GetEnvironment() {
static base::NoDestructor<ContentFuzzerEnvironment> environment;
return *environment;
}
scoped_refptr<base::SequencedTaskRunner> GetFuzzerTaskRunner() {
return GetEnvironment().fuzzer_task_runner();
}
// Per-testcase state needed to run the interface being tested.
//
// The lifetime of this is scoped to a single testcase, and it is created and
// destroyed from the fuzzer sequence.
//
// For CodeCacheHost, this needs the basic common Browser process state provided
// by TestBrowserContext, and to set up the cache storage that will provide the
// storage backing for the code cache.
//
// Since the Browser process will host one CodeCacheHostImpl per
// RenderProcessHost, we emulate this by allowing the fuzzer to create (and
// destroy) multiple CodeCacheHostImpl instances.
class CodeCacheHostTestcase {
public:
explicit CodeCacheHostTestcase(
const content::fuzzing::code_cache_host::proto::Testcase& testcase);
// Returns true once either all of the actions in the testcase have been
// performed, or the per-testcase action limit has been exceeded.
//
// This should only be called from the fuzzer sequence.
bool IsFinished();
// If there are still actions remaining in the testcase, this will perform the
// next sequence of actions before returning.
//
// If IsFinished() would return true, then calling this function is a no-op.
//
// This should only be called from the fuzzer sequence.
void NextAction();
void SetUp();
void TearDown();
private:
using Action = content::fuzzing::code_cache_host::proto::Action;
void SetUpOnUIThread();
void TearDownOnUIThread();
// Used by AddCodeCacheHost to create and bind CodeCacheHostImpl on the UI
// thread.
void AddCodeCacheHostImpl(
uint32_t id,
int renderer_id,
const Origin& origin,
mojo::PendingReceiver<::blink::mojom::CodeCacheHost>&& receiver);
// Create and bind a new instance for fuzzing. This needs to make sure that
// the new instance has been created and bound on the correct sequence before
// returning.
void AddCodeCacheHost(
uint32_t id,
int renderer_id,
content::fuzzing::code_cache_host::proto::NewCodeCacheHostAction::OriginId
origin_id);
// The proto message describing the test actions to perform.
const content::fuzzing::code_cache_host::proto::Testcase& testcase_;
// This set of origins should cover all of the origin types which have special
// handling in CodeCacheHostImpl, and give us two distinct "normal" origins,
// which should be enough to exercise all of the code.
const Origin origin_a_;
const Origin origin_b_;
const Origin origin_opaque_;
const Origin origin_empty_;
// Apply a reasonable upper-bound on testcase complexity to avoid timeouts.
const int max_action_count_ = 512;
// Apply a reasonable upper-bound on maximum size of action that we will
// deserialize. (This is deliberately slightly larger than max mojo message
// size)
const size_t max_action_size_ = 300 * 1024 * 1024;
// Count of total actions performed in this testcase.
int action_count_ = 0;
// The index of the next sequence of actions to execute.
int next_sequence_idx_ = 0;
// Prerequisite state.
std::unique_ptr<content::TestBrowserContext> browser_context_;
std::unique_ptr<content::CacheStorageControlWrapper>
cache_storage_control_wrapper_;
scoped_refptr<content::GeneratedCodeCacheContext>
generated_code_cache_context_;
// Mapping from renderer id to CodeCacheHostImpl instances being fuzzed.
// Access only from UI thread.
std::map<int, std::unique_ptr<content::CodeCacheHostImpl>> code_cache_hosts_;
SEQUENCE_CHECKER(sequence_checker_);
};
CodeCacheHostTestcase::CodeCacheHostTestcase(
const content::fuzzing::code_cache_host::proto::Testcase& testcase)
: testcase_(testcase),
origin_a_(url::Origin::Create(GURL("http://aaa.com/"))),
origin_b_(url::Origin::Create(GURL("http://bbb.com/"))),
origin_opaque_(url::Origin::Create(GURL("opaque"))),
origin_empty_(url::Origin::Create(GURL("file://this_becomes_empty"))) {
// CodeCacheHostTestcase is created on the main thread, but the actions that
// we want to validate the sequencing of take place on the fuzzer sequence.
DETACH_FROM_SEQUENCE(sequence_checker_);
}
void CodeCacheHostTestcase::SetUp() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
content::GetUIThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&CodeCacheHostTestcase::SetUpOnUIThread,
base::Unretained(this)),
run_loop.QuitClosure());
run_loop.Run();
}
void CodeCacheHostTestcase::SetUpOnUIThread() {
browser_context_ = std::make_unique<content::TestBrowserContext>();
cache_storage_control_wrapper_ =
std::make_unique<content::CacheStorageControlWrapper>(
content::GetIOThreadTaskRunner({}), browser_context_->GetPath(),
browser_context_->GetSpecialStoragePolicy(),
/*quota_manager_proxy=*/nullptr,
/*blob_storage_context=*/mojo::NullRemote());
generated_code_cache_context_ =
base::MakeRefCounted<content::GeneratedCodeCacheContext>();
generated_code_cache_context_->Initialize(browser_context_->GetPath(), 65536);
}
void CodeCacheHostTestcase::TearDown() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
content::GetUIThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&CodeCacheHostTestcase::TearDownOnUIThread,
base::Unretained(this)),
run_loop.QuitClosure());
run_loop.Run();
}
void CodeCacheHostTestcase::TearDownOnUIThread() {
code_cache_hosts_.clear();
generated_code_cache_context_.reset();
cache_storage_control_wrapper_.reset();
browser_context_.reset();
}
bool CodeCacheHostTestcase::IsFinished() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return next_sequence_idx_ >= testcase_.sequence_indexes_size();
}
void CodeCacheHostTestcase::NextAction() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (next_sequence_idx_ < testcase_.sequence_indexes_size()) {
auto sequence_idx = testcase_.sequence_indexes(next_sequence_idx_++);
const auto& sequence =
testcase_.sequences(sequence_idx % testcase_.sequences_size());
for (auto action_idx : sequence.action_indexes()) {
if (!testcase_.actions_size() || ++action_count_ > max_action_count_) {
return;
}
const auto& action =
testcase_.actions(action_idx % testcase_.actions_size());
if (action.ByteSizeLong() > max_action_size_) {
return;
}
switch (action.action_case()) {
case Action::kNewCodeCacheHost: {
AddCodeCacheHost(action.new_code_cache_host().id(),
action.new_code_cache_host().render_process_id(),
action.new_code_cache_host().origin_id());
} break;
case Action::kRunThread: {
if (action.run_thread().id()) {
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
} else {
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, run_loop.QuitClosure());
run_loop.Run();
}
} break;
case Action::kCodeCacheHostRemoteAction: {
mojolpm::HandleRemoteAction(action.code_cache_host_remote_action());
} break;
case content::fuzzing::code_cache_host::proto::Action::ACTION_NOT_SET:
break;
}
}
}
}
void CodeCacheHostTestcase::AddCodeCacheHostImpl(
uint32_t id,
int renderer_id,
const Origin& origin,
mojo::PendingReceiver<::blink::mojom::CodeCacheHost>&& receiver) {
auto code_cache_host = std::make_unique<content::CodeCacheHostImpl>(
renderer_id, /*render_process_host_impl=*/nullptr,
generated_code_cache_context_, std::move(receiver));
code_cache_host->SetCacheStorageControlForTesting(
cache_storage_control_wrapper_.get());
code_cache_hosts_[renderer_id] = std::move(code_cache_host);
}
void CodeCacheHostTestcase::AddCodeCacheHost(
uint32_t id,
int renderer_id,
content::fuzzing::code_cache_host::proto::NewCodeCacheHostAction::OriginId
origin_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
mojo::Remote<::blink::mojom::CodeCacheHost> remote;
auto receiver = remote.BindNewPipeAndPassReceiver();
const Origin* origin = &origin_a_;
if (origin_id == 1) {
origin = &origin_b_;
} else if (origin_id == 2) {
origin = &origin_opaque_;
} else if (origin_id == 3) {
origin = &origin_empty_;
}
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
content::GetUIThreadTaskRunner({})->PostTaskAndReply(
FROM_HERE,
base::BindOnce(&CodeCacheHostTestcase::AddCodeCacheHostImpl,
base::Unretained(this), id, renderer_id, *origin,
std::move(receiver)),
run_loop.QuitClosure());
run_loop.Run();
mojolpm::GetContext()->AddInstance(id, std::move(remote));
}
// Helper function to keep scheduling fuzzer actions on the current runloop
// until the testcase has completed, and then quit the runloop.
void NextAction(CodeCacheHostTestcase* testcase,
base::RepeatingClosure quit_closure) {
if (!testcase->IsFinished()) {
testcase->NextAction();
GetFuzzerTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(NextAction, base::Unretained(testcase),
std::move(quit_closure)));
} else {
GetFuzzerTaskRunner()->PostTask(FROM_HERE, std::move(quit_closure));
}
}
// Helper function to setup and run the testcase, since we need to do that from
// the fuzzer sequence rather than the main thread.
void RunTestcase(CodeCacheHostTestcase* testcase) {
mojo::Message message;
auto dispatch_context =
std::make_unique<mojo::internal::MessageDispatchContext>(&message);
testcase->SetUp();
mojolpm::GetContext()->StartTestcase();
base::RunLoop fuzzer_run_loop(base::RunLoop::Type::kNestableTasksAllowed);
GetFuzzerTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(NextAction, base::Unretained(testcase),
fuzzer_run_loop.QuitClosure()));
fuzzer_run_loop.Run();
mojolpm::GetContext()->EndTestcase();
testcase->TearDown();
}
DEFINE_BINARY_PROTO_FUZZER(
const content::fuzzing::code_cache_host::proto::Testcase& proto_testcase) {
if (!proto_testcase.actions_size() || !proto_testcase.sequences_size() ||
!proto_testcase.sequence_indexes_size()) {
return;
}
// Make sure that the environment is initialized before we do anything else.
GetEnvironment();
CodeCacheHostTestcase testcase(proto_testcase);
base::RunLoop ui_run_loop(base::RunLoop::Type::kNestableTasksAllowed);
// Unretained is safe here, because ui_run_loop has to finish before testcase
// goes out of scope.
GetFuzzerTaskRunner()->PostTaskAndReply(
FROM_HERE, base::BindOnce(RunTestcase, base::Unretained(&testcase)),
ui_run_loop.QuitClosure());
ui_run_loop.Run();
}