// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/fuzzing/in_process_fuzzer.h"
#include <vector>
#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/functional/callback_forward.h"
#include "base/strings/string_util.h"
#include "base/test/bind.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "chrome/renderer/chrome_content_renderer_client.h"
#include "chrome/test/base/chrome_test_launcher.h"
#include "chrome/test/fuzzing/in_process_fuzzer_buildflags.h"
#include "content/public/app/content_main.h"
#include "content/public/renderer/render_frame.h"
#include "content/public/renderer/render_frame_observer.h"
#include "content/public/test/test_launcher.h"
#include "third_party/blink/public/web/web_testing_support.h"
#if BUILDFLAG(IS_WIN)
#include "base/strings/sys_string_conversions.h"
#endif // BUILDFLAG(IS_WIN)
// This is provided within libfuzzer, and documented, but is not its headers.
extern "C" int LLVMFuzzerRunDriver(int* argc,
char*** argv,
int (*UserCb)(const uint8_t* Data,
size_t Size));
namespace {
std::string_view RunLoopTimeoutBehaviorToString(
RunLoopTimeoutBehavior behavior) {
switch (behavior) {
case RunLoopTimeoutBehavior::kDefault:
return "kDefault";
case RunLoopTimeoutBehavior::kContinue:
return "kContinue";
case RunLoopTimeoutBehavior::kDeclareInfiniteLoop:
return "kDeclareInfiniteLoop";
}
return "kUnknown";
}
void LogRunLoopTimeoutCallback(RunLoopTimeoutBehavior behavior) {
LOG(INFO) << "Custom RunLoop timeout callback triggered ("
<< RunLoopTimeoutBehaviorToString(behavior) << ").";
}
} // namespace
InProcessFuzzerFactoryBase* g_in_process_fuzzer_factory;
InProcessFuzzer::InProcessFuzzer(InProcessFuzzerOptions options)
: options_(options) {}
InProcessFuzzer::~InProcessFuzzer() = default;
base::CommandLine::StringVector
InProcessFuzzer::GetChromiumCommandLineArguments() {
base::CommandLine::StringVector empty;
return empty;
}
void InProcessFuzzer::SetUp() {
// Overrides the default 60s run loop timeout set by `BrowserTestBase`. See
// https://source.chromium.org/chromium/chromium/src/+/main:content/public/test/browser_test_base.cc?q=ScopedRunLoopTimeout.
// All of the fuzzing engines that we use are having timeouts features, and
// this timeout can vary depending on the number of tested testcases. We must
// let the engines handle timeouts, and set the maximum here.
base::test::ScopedRunLoopTimeout scoped_timeout(FROM_HERE,
base::TimeDelta::Max());
switch (options_.run_loop_timeout_behavior) {
case RunLoopTimeoutBehavior::kContinue:
KeepRunningOnTimeout();
break;
case RunLoopTimeoutBehavior::kDeclareInfiniteLoop:
DeclareInfiniteLoopOnTimeout();
break;
case RunLoopTimeoutBehavior::kDefault:
break;
}
// Note that browser tests are being launched by the `SetUp` method.
InProcessBrowserTest::SetUp();
}
void InProcessFuzzer::KeepRunningOnTimeout() {
base::test::ScopedRunLoopTimeout::SetTimeoutCallbackForTesting(
std::make_unique<base::test::ScopedRunLoopTimeout::TimeoutCallback>(
base::IgnoreArgs<const base::Location&,
base::RepeatingCallback<std::string()>,
const base::Location&>(base::BindRepeating(
&LogRunLoopTimeoutCallback, RunLoopTimeoutBehavior::kContinue))));
}
void InProcessFuzzer::DeclareInfiniteLoopOnTimeout() {
base::test::ScopedRunLoopTimeout::SetTimeoutCallbackForTesting(
std::make_unique<base::test::ScopedRunLoopTimeout::TimeoutCallback>(
base::IgnoreArgs<const base::Location&,
base::RepeatingCallback<std::string()>,
const base::Location&>(
base::BindRepeating(&InProcessFuzzer::DeclareInfiniteLoop,
base::Unretained(this)))
.Then(base::BindRepeating(
&LogRunLoopTimeoutCallback,
RunLoopTimeoutBehavior::kDeclareInfiniteLoop))));
}
void InProcessFuzzer::Run(
const std::vector<std::string>& libfuzzer_command_line) {
libfuzzer_command_line_ = libfuzzer_command_line;
SetUp();
TearDown();
}
void InProcessFuzzer::SetUpOnMainThread() {
InProcessBrowserTest::SetUpOnMainThread();
// All of the engines that are being used to run those fuzzers are handling
// process interruption. In case we let Chrome handle those signals itself,
// we end up exiting the fuzzing process, and the engine records the last
// run as a crash since it cannot not determine the reason why the process
// terminated.
#if BUILDFLAG(IS_POSIX)
signal(SIGTERM, SIG_DFL);
signal(SIGINT, SIG_DFL);
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
// In case we're being built with a memory tool (asan, msan...), we should
// let it handle this signal so that we get better reporting.
// As of now, since both in-process stack traces and the crashpad handler are
// being disabled, this is the only signal that we need to reset since it's
// being set in
// https://source.chromium.org/chromium/chromium/src/+/main:content/public/test/browser_test_base.cc?q=SignalHandler
signal(SIGSEGV, SIG_DFL);
#endif // BUILDFLAG(MEMORY_TOOL_REPLACES_ALLOCATOR)
#endif // BUILDFLAG(IS_POSIX)
}
InProcessFuzzer* g_test;
// The following three classes are only meant to inject `internals` into JS.
// This object is needed by our IPC based fuzzers, and could also be needed by
// other in the future.
class InternalsObjectFrameInjector : public content::RenderFrameObserver {
public:
explicit InternalsObjectFrameInjector(content::RenderFrame* render_frame)
: content::RenderFrameObserver(render_frame) {}
void DidClearWindowObject() override {
blink::WebLocalFrame* frame = render_frame()->GetWebFrame();
blink::WebTestingSupport::InjectInternalsObject(frame);
}
void OnDestruct() override { delete this; }
};
class InternalsObjectRendererInjector : public ChromeContentRendererClient {
public:
void RenderFrameCreated(content::RenderFrame* render_frame) override {
new InternalsObjectFrameInjector(render_frame);
}
};
class FuzzerChromeMainDelegate : public ChromeTestChromeMainDelegate {
public:
FuzzerChromeMainDelegate() = default;
content::ContentRendererClient* CreateContentRendererClient() override {
return new InternalsObjectRendererInjector();
}
};
class FuzzerTestLauncherDelegate : public content::TestLauncherDelegate {
public:
FuzzerTestLauncherDelegate(std::unique_ptr<InProcessFuzzer>&& fuzzer,
std::vector<std::string>&& libfuzzer_arguments)
: fuzzer_(std::move(fuzzer)),
libfuzzer_arguments_(std::move(libfuzzer_arguments)) {
content_main_delegate_ = std::make_unique<FuzzerChromeMainDelegate>();
}
int RunTestSuite(int argc, char** argv) override {
fuzzer_->Run(libfuzzer_arguments_);
return 0;
}
#if !BUILDFLAG(IS_ANDROID)
// Android browser tests set the ContentMainDelegate itself for the test
// harness to use, and do not go through ContentMain() in TestLauncher.
content::ContentMainDelegate* CreateContentMainDelegate() override {
return &*content_main_delegate_;
}
#endif
private:
std::unique_ptr<InProcessFuzzer> fuzzer_;
std::unique_ptr<content::ContentMainDelegate> content_main_delegate_;
std::vector<std::string> libfuzzer_arguments_;
};
int fuzz_callback(const uint8_t* data, size_t size) {
return g_test->DoFuzz(data, size);
}
void InProcessFuzzer::RunTestOnMainThread() {
std::vector<char*> argv;
for (const auto& arg : libfuzzer_command_line_) {
argv.push_back((char*)arg.data());
}
argv.push_back(nullptr);
int argc = argv.size() - 1;
char** argv2 = argv.data();
g_test = this;
base::IgnoreResult(LLVMFuzzerRunDriver(&argc, &argv2, fuzz_callback));
if (exit_after_fuzz_case_) {
LOG(INFO) << "Early exit requested - exiting after LLVMFuzzerRunDriver.";
exit(0);
}
g_test = nullptr;
}
int InProcessFuzzer::DoFuzz(const uint8_t* data, size_t size) {
// We actually exit before running the *next* fuzz case to give an opportunity
// to return the return value to the fuzzing engine.
if (exit_after_fuzz_case_) {
LOG(INFO) << "Early exit requested - exiting after fuzz case.";
exit(0);
}
std::optional<base::test::ScopedRunLoopTimeout> scoped_timeout;
if (options_.run_loop_timeout) {
scoped_timeout.emplace(FROM_HERE, *options_.run_loop_timeout);
}
return Fuzz(data, size);
}
// Main function for running in process fuzz tests.
// This aims to replicate //chrome browser tests as much as possible; we want
// the whole browser environment to be available for this sort of test in as
// realistic a fashion as possible.
int main(int argc, char** argv) {
base::AtExitManager atexit_manager;
base::CommandLine::Init(argc, argv);
std::unique_ptr<InProcessFuzzer> fuzzer =
g_in_process_fuzzer_factory->CreateInProcessFuzzer();
// Oh dear, you've got to the part of the code relating to command lines.
// I'm sorry.
// Here are our constraints:
// * Both libfuzzer/centipede and Chromium expect a full command line
// * We set the format of neither command line
// * Chromium will launch other Chromium processes, giving them a command
// line.
// * The centipede runner will launch our fuzzer, giving it a command line.
// So, at this point, we have to figure out heuristics for what's up.
// Are we the original fuzzer process, in which case we pass the CLI to
// libfuzzer/centipede, and ask for a suitable Chromium command line from
// our fuzz test? Or, are we a child Chromium process which has been
// launched from a previous Chromium process? Well, dear reader, there are
// no telltail arguments guaranteed to be on either, so we're going to
// use a heuristic. If the first argument starts with --, we're assuming
// we're a Chromium child.
bool we_are_probably_a_chromium_child_process = false;
if (base::CommandLine::ForCurrentProcess()->argv().size() > 1) {
if (base::StartsWith(base::CommandLine::ForCurrentProcess()->argv()[1],
FILE_PATH_LITERAL("--"))) {
we_are_probably_a_chromium_child_process = true;
}
}
std::vector<std::string> libfuzzer_arguments;
if (we_are_probably_a_chromium_child_process) {
// If we're a Chromium child, we don't alter the command-line,
// and in fact the libfuzzer code will never run, so we don't need to
// pass any arguments through to libfuzzer.
} else {
#if BUILDFLAG(IS_WIN)
// Convert std::wstring (Windows command lines) to std::string
// (as needed by libfuzzer).
auto wide_argv = base::CommandLine::ForCurrentProcess()->argv();
for (auto arg : wide_argv) {
libfuzzer_arguments.push_back(base::SysWideToUTF8(arg));
}
#else
libfuzzer_arguments = base::CommandLine::ForCurrentProcess()->argv();
#endif // BUILDFLAG(IS_WIN)
base::CommandLine::StringType executable_name =
base::CommandLine::ForCurrentProcess()->argv().at(0);
base::CommandLine::StringVector chromium_arguments =
fuzzer->GetChromiumCommandLineArguments();
chromium_arguments.insert(chromium_arguments.begin(), executable_name);
chromium_arguments.push_back(FILE_PATH_LITERAL("--single-process-tests"));
chromium_arguments.push_back(FILE_PATH_LITERAL("--single-process"));
chromium_arguments.push_back(FILE_PATH_LITERAL("--no-sandbox"));
chromium_arguments.push_back(FILE_PATH_LITERAL("--no-zygote"));
chromium_arguments.push_back(FILE_PATH_LITERAL("--disable-gpu"));
chromium_arguments.push_back(
FILE_PATH_LITERAL("--disable-crashpad-for-testing"));
#if defined(MEMORY_TOOL_REPLACES_ALLOCATOR)
// We disable in-process stack trace handling in case we're using memory
// tools so that we get better reporting on what happened in case of
// SIGSEGV.
chromium_arguments.push_back(
FILE_PATH_LITERAL("--disable-in-process-stack-traces"));
#endif
base::CommandLine::ForCurrentProcess()->InitFromArgv(chromium_arguments);
// Various bits of setup are done by base::TestSuite::Initialize.
// As we're not a functional test suite, most of those things are not
// necessary, but at least this is:
TestTimeouts::Initialize();
}
FuzzerTestLauncherDelegate* fuzzer_launcher_delegate =
new FuzzerTestLauncherDelegate(std::move(fuzzer),
std::move(libfuzzer_arguments));
return LaunchChromeTests(1, fuzzer_launcher_delegate, argc, argv);
}