| // 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; |
| |
| bool InProcessFuzzer::UseSingleProcessMode() { |
| return true; |
| } |
| |
| 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) |
| } |
| |
| bool InProcessFuzzer::InMergeMode() const { |
| for (const auto& arg : libfuzzer_command_line_) { |
| if (arg.starts_with("-merge")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| 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)) { |
| } |
| |
| 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 new FuzzerChromeMainDelegate(); |
| } |
| #endif |
| |
| private: |
| std::unique_ptr<InProcessFuzzer> fuzzer_; |
| 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; |
| auto res = LLVMFuzzerRunDriver(&argc, &argv2, fuzz_callback); |
| if (exit_after_fuzz_case_) { |
| LOG(INFO) << "Early exit requested - exiting after LLVMFuzzerRunDriver."; |
| exit(0); |
| } |
| // Performing exit here allows us to skip the browser shutdown phase thus |
| // avoid potential hangs. This is the behaviour already observed in libfuzzer |
| // which performs exits within LLVMFuzzerRunDriver. It is important to use the |
| // LLVMFuzzerRunDriver return value for the exit value since this helps |
| // centipede determine which input crashed when rerunning inputs one-by-one. |
| exit(res); |
| } |
| |
| 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); |
| } |
| |
| /// Used only for child processes (e.g. renderers etc.) |
| class ChildProcessTestLauncherDelegate : public content::TestLauncherDelegate { |
| public: |
| ChildProcessTestLauncherDelegate() = default; |
| int RunTestSuite(int argc, char** argv) override { |
| LOG(FATAL) << "Trying to run tests in child"; |
| } |
| #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 new FuzzerChromeMainDelegate(); |
| } |
| #endif |
| }; |
| |
| // 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); |
| |
| // 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. |
| |
| if (base::CommandLine::ForCurrentProcess()->argv().size() > 1) { |
| if (base::StartsWith(base::CommandLine::ForCurrentProcess()->argv()[1], |
| FILE_PATH_LITERAL("--"))) { |
| // 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. |
| // Ensure we don't create the InProcessFuzzer in this branch as it |
| // initializes unwelcome things in its base class constructors. |
| ChildProcessTestLauncherDelegate delegate; |
| return LaunchChromeTests(1, &delegate, argc, argv); |
| } |
| } |
| |
| // We are the outermost process, let's run the fuzzer. |
| std::unique_ptr<InProcessFuzzer> fuzzer = |
| g_in_process_fuzzer_factory->CreateInProcessFuzzer(); |
| #if BUILDFLAG(IS_WIN) |
| // Convert std::wstring (Windows command lines) to std::string |
| // (as needed by libfuzzer). |
| std::vector<std::string> libfuzzer_arguments; |
| auto wide_argv = base::CommandLine::ForCurrentProcess()->argv(); |
| for (auto arg : wide_argv) { |
| libfuzzer_arguments.push_back(base::SysWideToUTF8(arg)); |
| } |
| #else |
| std::vector<std::string> 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")); |
| if (fuzzer->UseSingleProcessMode()) { |
| chromium_arguments.push_back(FILE_PATH_LITERAL("--single-process")); |
| chromium_arguments.push_back( |
| FILE_PATH_LITERAL("--disable-crashpad-for-testing")); |
| } else { |
| #if BUILDFLAG(IS_CENTIPEDE) |
| // Static destructors in centipede runner code will open |
| // shared memory handles if they find the runner flags lurking in this |
| // environment variable. If child processes do this, they'll |
| // unfortunately clobber valid information from the main process |
| // that it's trying to pass back to the external centipede executor. |
| // Fortunately, this variable is only read during centipede's |
| // static initializers, so we can now safely clear it in our parent |
| // (browser) process in order to ensure it's unset for all children. |
| // TODO(crbug.com/383356867) - stop depending on centipede internals |
| // like this |
| unsetenv("CENTIPEDE_RUNNER_FLAGS"); |
| #endif |
| } |
| chromium_arguments.push_back(FILE_PATH_LITERAL("--no-zygote")); |
| chromium_arguments.push_back(FILE_PATH_LITERAL("--no-sandbox")); |
| chromium_arguments.push_back(FILE_PATH_LITERAL("--disable-gpu")); |
| chromium_arguments.push_back( |
| FILE_PATH_LITERAL("--enable-unsafe-swiftshader")); |
| #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( |
| std::move(fuzzer), std::move(libfuzzer_arguments)); |
| return LaunchChromeTests(1, &fuzzer_launcher_delegate, argc, argv); |
| } |