Fail loudly when FUZZ_TEST is used with custom main() without InitFuzzTest PiperOrigin-RevId: 998932557
diff --git a/cmake/FuzzTestHelpers.cmake b/cmake/FuzzTestHelpers.cmake index 2869a77..354e241 100644 --- a/cmake/FuzzTestHelpers.cmake +++ b/cmake/FuzzTestHelpers.cmake
@@ -433,7 +433,7 @@ set(generated_include_dir "${generated_target_dir}") if (NOT ${FLATBUFFERS_GENERATE_HEADERS_INCLUDE_PREFIX} STREQUAL "") set(generated_include_dir "${generated_include_dir}/${FLATBUFFERS_GENERATE_HEADERS_INCLUDE_PREFIX}") - list(APPEND FLATBUFFERS_GENERATE_HEADERS_FLAGS + list(APPEND FLATBUFFERS_GENERATE_HEADERS_FLAGS "--include-prefix" ${FLATBUFFERS_GENERATE_HEADERS_INCLUDE_PREFIX}) endif()
diff --git a/e2e_tests/BUILD b/e2e_tests/BUILD index e58abc9..b392ee6 100644 --- a/e2e_tests/BUILD +++ b/e2e_tests/BUILD
@@ -48,6 +48,7 @@ "@com_google_fuzztest//centipede:centipede_uninstrumented", "@com_google_fuzztest//e2e_tests/testdata:data", "@com_google_fuzztest//e2e_tests/testdata:dynamically_registered_fuzz_tests.stripped", + "@com_google_fuzztest//e2e_tests/testdata:fuzz_test_without_init_fuzztest.stripped", "@com_google_fuzztest//e2e_tests/testdata:fuzz_tests_for_functional_testing.stripped", "@com_google_fuzztest//e2e_tests/testdata:fuzz_tests_with_invalid_seeds.stripped", "@com_google_fuzztest//e2e_tests/testdata:llvm_fuzzer_with_custom_mutator.stripped",
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc index 943eb72..c3263b2 100644 --- a/e2e_tests/functional_test.cc +++ b/e2e_tests/functional_test.cc
@@ -730,6 +730,16 @@ Optional(IsXmlWithExactlyFuzzTestsHavingFuzzTestProperty())); } +TEST_F(UnitTestModeTest, FailsLoudlyWhenInitFuzzTestIsNotCalled) { + auto [status, std_out, std_err] = + RunWithExactFuzzerFlags("*", "testdata/fuzz_test_without_init_fuzztest"); + + EXPECT_THAT(status, Ne(ExitCode(0))); + EXPECT_THAT_LOG( + std_out, HasSubstr("FUZZ_TEST(MySuite, MyFuzzTest) was registered, but " + "InitFuzzTest was never called in main().")); +} + // Tests for the FuzzTest command line interface. class GenericCommandLineInterfaceTest : public ::testing::Test { protected:
diff --git a/e2e_tests/testdata/BUILD b/e2e_tests/testdata/BUILD index 0db3a80..bc41e92 100644 --- a/e2e_tests/testdata/BUILD +++ b/e2e_tests/testdata/BUILD
@@ -138,6 +138,16 @@ ) cc_binary( + name = "fuzz_test_without_init_fuzztest", + testonly = 1, + srcs = ["fuzz_test_without_init_fuzztest.cc"], + deps = [ + "@com_google_fuzztest//fuzztest", + "@googletest//:gtest", + ], +) + +cc_binary( name = "fuzz_tests_for_corpus_database_testing", testonly = 1, srcs = ["fuzz_tests_for_corpus_database_testing.cc"],
diff --git a/e2e_tests/testdata/CMakeLists.txt b/e2e_tests/testdata/CMakeLists.txt index ec93a80..c1b7eb5 100644 --- a/e2e_tests/testdata/CMakeLists.txt +++ b/e2e_tests/testdata/CMakeLists.txt
@@ -116,3 +116,19 @@ PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/_main/e2e_tests/testdata" ) + +add_executable( + fuzz_test_without_init_fuzztest.stripped + fuzz_test_without_init_fuzztest.cc +) +target_link_libraries( + fuzz_test_without_init_fuzztest.stripped + PRIVATE + fuzztest::fuzztest + GTest::gtest +) +set_target_properties( + fuzz_test_without_init_fuzztest.stripped + PROPERTIES RUNTIME_OUTPUT_DIRECTORY + "${CMAKE_BINARY_DIR}/_main/e2e_tests/testdata" +)
diff --git a/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc b/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc new file mode 100644 index 0000000..8f95aed --- /dev/null +++ b/e2e_tests/testdata/fuzz_test_without_init_fuzztest.cc
@@ -0,0 +1,28 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gtest/gtest.h" +#include "./fuzztest/fuzztest.h" + +namespace { + +void MyFuzzTest(int) {} +FUZZ_TEST(MySuite, MyFuzzTest); + +} // namespace + +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}
diff --git a/fuzztest/BUILD b/fuzztest/BUILD index ab25066..f6fb89e 100644 --- a/fuzztest/BUILD +++ b/fuzztest/BUILD
@@ -55,10 +55,13 @@ cc_library( name = "fuzztest", + testonly = True, hdrs = ["fuzztest.h"], deps = [ ":domain", ":fuzztest_macros", + # TODO(b/570019271): Remove dependency once bug is fixed. + "@com_google_fuzztest//fuzztest/internal:googletest_adaptor", ], ) @@ -161,7 +164,6 @@ testonly = True, srcs = ["llvm_fuzzer_wrapper.cc"], deps = [ - ":fuzztest", ":fuzztest_macros", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/base:no_destructor",
diff --git a/fuzztest/CMakeLists.txt b/fuzztest/CMakeLists.txt index c42d529..a06867a 100644 --- a/fuzztest/CMakeLists.txt +++ b/fuzztest/CMakeLists.txt
@@ -92,6 +92,7 @@ DEPS fuzztest::domain fuzztest::fuzztest_macros + fuzztest::googletest_adaptor ) fuzztest_cc_library(
diff --git a/fuzztest/init_fuzztest.cc b/fuzztest/init_fuzztest.cc index 74c1bc5..5b09d54 100644 --- a/fuzztest/init_fuzztest.cc +++ b/fuzztest/init_fuzztest.cc
@@ -447,6 +447,8 @@ void InitFuzzTest(int* argc, char*** argv, std::string_view binary_id) { auto& runtime = internal::Runtime::instance(); + runtime.SetInitFuzzTestCalled(); + runtime.SetArgs(argc, argv); const bool is_listing = absl::GetFlag(FUZZTEST_FLAG(list_fuzz_tests)); if (is_listing) { for (const auto& name : ListRegisteredTests()) { @@ -494,7 +496,9 @@ internal::Configuration configuration = CreateConfigurationsFromFlags(derived_binary_id); configuration.reproduction_command_template = reproduction_command_template; - internal::RegisterFuzzTestsAsGoogleTests(argc, argv, configuration); + runtime.SetInitConfiguration(configuration); + internal::RegisterSeparateRegressionTestsForEachCrashingInput(configuration); + internal::RegisterGoogleTestEventListener(); const bool is_fuzzing_or_replaying = (fuzzing_time_limit || replay_corpus_time_limit);
diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD index 1219ada..27f9e67 100644 --- a/fuzztest/internal/BUILD +++ b/fuzztest/internal/BUILD
@@ -383,6 +383,7 @@ "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/functional:function_ref", "@abseil-cpp//absl/strings:string_view", + "@com_google_fuzztest//common:logging", ] + select({ "@com_google_fuzztest//fuzztest:use_centipede": [":centipede_adaptor"], "//conditions:default": [],
diff --git a/fuzztest/internal/CMakeLists.txt b/fuzztest/internal/CMakeLists.txt index 1ccc8a3..4f90993 100644 --- a/fuzztest/internal/CMakeLists.txt +++ b/fuzztest/internal/CMakeLists.txt
@@ -352,6 +352,7 @@ SRCS "registry.cc" DEPS + fuzztest::common_logging fuzztest::compatibility_mode fuzztest::fixture_driver fuzztest::registration
diff --git a/fuzztest/internal/googletest_adaptor.cc b/fuzztest/internal/googletest_adaptor.cc index d36d498..44f9f90 100644 --- a/fuzztest/internal/googletest_adaptor.cc +++ b/fuzztest/internal/googletest_adaptor.cc
@@ -2,6 +2,7 @@ #include "./fuzztest/internal/googletest_adaptor.h" #include <cstdlib> +#include <optional> #include <string> #include <type_traits> #include <utility> @@ -25,12 +26,13 @@ namespace fuzztest::internal { -std::vector<std::string> GTest_TestAdaptor::GetFuzzTestsInCurrentShard() const { +std::vector<std::string> GTest_TestAdaptor::GetFuzzTestsInCurrentShard( + const Configuration& configuration) const { std::vector<std::string> result; for (const auto* test : GetRegisteredTests()) { if (!test->should_run()) continue; if (test->is_in_another_shard()) continue; - for (const auto& fuzztest : configuration_.fuzz_tests) { + for (const auto& fuzztest : configuration.fuzz_tests) { if (fuzztest == absl::StrCat(test->test_suite_name(), ".", test->name())) { result.push_back(fuzztest); @@ -43,13 +45,12 @@ namespace { template <typename T> -void RegisterFuzzTestAsGTest(int* argc, char*** argv, FuzzTest& test, - const Configuration& configuration, - absl::string_view crashing_input_path = "") { - auto fixture_factory = [argc, argv, &test, - configuration = configuration]() mutable -> T* { - return new ::fuzztest::internal::GTest_TestAdaptor( - test, argc, argv, std::move(configuration)); +void RegisterFuzzTestAsGTest( + FuzzTest& test, std::optional<Configuration> configuration = std::nullopt, + absl::string_view crashing_input_path = "") { + auto fixture_factory = [&test, + configuration = std::move(configuration)]() -> T* { + return new ::fuzztest::internal::GTest_TestAdaptor(test, configuration); }; if (crashing_input_path.empty()) { ::testing::RegisterTest(test.suite_name().c_str(), test.test_name().c_str(), @@ -72,9 +73,8 @@ } template <typename T> -void RegisterSeparateRegressionTestForEachCrashingInput( - int* argc, char*** argv, FuzzTest& test, - const Configuration& configuration) { +void RegisterRegressionTestsForTest(FuzzTest& test, + const Configuration& configuration) { if (!configuration.reproduce_findings_as_separate_tests) return; #ifdef FUZZTEST_USE_CENTIPEDE const std::vector<std::string> crash_inputs = @@ -87,31 +87,33 @@ for (const std::string& input : crash_inputs) { Configuration updated_configuration = configuration; updated_configuration.crashing_input_to_reproduce = input; - RegisterFuzzTestAsGTest<T>(argc, argv, test, updated_configuration, input); + RegisterFuzzTestAsGTest<T>(test, updated_configuration, input); } } -template <typename T> -void RegisterTests(int* argc, char*** argv, FuzzTest& test, - const Configuration& configuration) { - RegisterFuzzTestAsGTest<T>(argc, argv, test, configuration); - RegisterSeparateRegressionTestForEachCrashingInput<T>(argc, argv, test, - configuration); -} - } // namespace -void RegisterFuzzTestsAsGoogleTests(int* argc, char*** argv, - const Configuration& configuration) { +void RegisterFuzzTestAsGoogleTest(FuzzTest& test) { + if (test.uses_fixture()) { + RegisterFuzzTestAsGTest<::fuzztest::internal::GTest_TestAdaptor>(test); + } else { + RegisterFuzzTestAsGTest<::testing::Test>(test); + } +} + +void RegisterSeparateRegressionTestsForEachCrashingInput( + const Configuration& configuration) { ::fuzztest::internal::ForEachTest([&](auto& test) { if (test.uses_fixture()) { - RegisterTests<::fuzztest::internal::GTest_TestAdaptor>(argc, argv, test, - configuration); + RegisterRegressionTestsForTest<::fuzztest::internal::GTest_TestAdaptor>( + test, configuration); } else { - RegisterTests<::testing::Test>(argc, argv, test, configuration); + RegisterRegressionTestsForTest<::testing::Test>(test, configuration); } }); +} +void RegisterGoogleTestEventListener() { ::testing::UnitTest::GetInstance()->listeners().Append( new ::fuzztest::internal::GTest_EventListener< ::testing::EmptyTestEventListener, ::testing::TestPartResult>());
diff --git a/fuzztest/internal/googletest_adaptor.h b/fuzztest/internal/googletest_adaptor.h index 39ffcab..0303d75 100644 --- a/fuzztest/internal/googletest_adaptor.h +++ b/fuzztest/internal/googletest_adaptor.h
@@ -16,6 +16,7 @@ #define FUZZTEST_FUZZTEST_GOOGLETEST_ADAPTOR_H_ #include <cstdlib> +#include <optional> #include <string> #include <utility> #include <vector> @@ -30,23 +31,33 @@ class GTest_TestAdaptor : public ::testing::Test { public: - explicit GTest_TestAdaptor(FuzzTest& test, int* argc, char*** argv, - Configuration configuration) - : test_(test), - argc_(argc), - argv_(argv), - configuration_(std::move(configuration)) {} + explicit GTest_TestAdaptor( + FuzzTest& test, std::optional<Configuration> configuration = std::nullopt) + : test_(test), configuration_(std::move(configuration)) {} void TestBody() override { + if (!Runtime::instance().init_fuzztest_called()) { + ADD_FAILURE() + << "FUZZ_TEST(" << test_.suite_name() << ", " << test_.test_name() + << ") was registered, but InitFuzzTest was never called in main(). " + << "If you are using a custom main(), please call " + << "fuzztest::InitFuzzTest(&argc, &argv)" + << " before RUN_ALL_TESTS()."; + return; + } RecordProperty("fuzz_test", "true"); auto test = test_.make(); - configuration_.fuzz_tests_in_current_shard = GetFuzzTestsInCurrentShard(); + Configuration configuration = + configuration_.has_value() ? *configuration_ + : Runtime::instance().init_configuration(); + configuration.fuzz_tests_in_current_shard = + GetFuzzTestsInCurrentShard(configuration); // We replay a reproducer in the same process to help debugging when // (1) we're replaying a single reproducer and (2) we're running locally. const bool running_locally = !std::getenv("FUZZTEST_RUNNING_UNDER_CI"); - configuration_.replay_in_single_process = - configuration_.crashing_input_to_reproduce.has_value() && + configuration.replay_in_single_process = + configuration.crashing_input_to_reproduce.has_value() && testing::UnitTest::GetInstance()->test_to_run_count() == 1 && running_locally; if (Runtime::instance().run_mode() == RunMode::kUnitTest) { @@ -54,8 +65,8 @@ // bugs, i.e., run multiple tests that lead to a crash. #if defined(GTEST_HAS_DEATH_TEST) && !defined(FUZZTEST_USE_CENTIPEDE) const bool needs_subprocess = - configuration_.crashing_input_to_reproduce.has_value() && - (!configuration_.replay_in_single_process || + configuration.crashing_input_to_reproduce.has_value() && + (!configuration.replay_in_single_process || // EXPECT_EXIT is required in the death-test subprocess, but in // the subprocess there's only one test to run. testing::internal::InDeathTestChild()); @@ -63,7 +74,7 @@ const bool needs_subprocess = false; #endif if (needs_subprocess) { - configuration_.preprocess_crash_reproducing = [] { + configuration.preprocess_crash_reproducing = [] { // EXPECT_EXIT disables event forwarding in gtest and as a result, // EXPECT/ASSERT-s are disabled. Here, we overwrite this option. testing::UnitTest::GetInstance()->listeners().SuppressEventForwarding( @@ -76,7 +87,7 @@ // test below fails without terminating the process. #ifdef GTEST_HAS_DEATH_TEST EXPECT_EXIT( - (test->RunInUnitTestMode(configuration_), + (test->RunInUnitTestMode(configuration), void( R"( FuzzTest failure! Please see 'actual message' below for the crash report. )"), std::exit(0)), @@ -85,7 +96,7 @@ EXPECT_TRUE(false) << "Death test is not supported."; #endif } else { - EXPECT_TRUE(test->RunInUnitTestMode(configuration_) || + EXPECT_TRUE(test->RunInUnitTestMode(configuration) || Runtime::instance().skipping_requested()) << "Failure(s) found in the unit-test mode - please see the test " "log for more details."; @@ -93,7 +104,9 @@ } else { // TODO(b/245753736): Consider using `tolerate_failure` when FuzzTest can // tolerate crashes in fuzzing mode. - EXPECT_TRUE(test->RunInFuzzingMode(argc_, argv_, configuration_) || + EXPECT_TRUE(test->RunInFuzzingMode(Runtime::instance().argc(), + Runtime::instance().argv(), + configuration) || Runtime::instance().skipping_requested()) << "Failure(s) found in the fuzzing mode - please see the test log " "for more details."; @@ -113,12 +126,11 @@ } private: - std::vector<std::string> GetFuzzTestsInCurrentShard() const; + std::vector<std::string> GetFuzzTestsInCurrentShard( + const Configuration& configuration) const; FuzzTest& test_; - int* argc_; - char*** argv_; - Configuration configuration_; + std::optional<Configuration> configuration_; }; template <typename Base, typename TestPartResult> @@ -142,9 +154,14 @@ } }; -// Registers FUZZ_TEST as GoogleTest TEST-s. -void RegisterFuzzTestsAsGoogleTests(int* argc, char*** argv, - const Configuration& configuration); +// Registers a separate GoogleTest TEST for each crashing input of each +// FUZZ_TEST when `configuration.reproduce_findings_as_separate_tests` is true. +void RegisterSeparateRegressionTestsForEachCrashingInput( + const Configuration& configuration); + +// Registers a GoogleTest event listener that reports assertion failures to +// FuzzTest. +void RegisterGoogleTestEventListener(); // Set listing mode validator for GoogleTest to check that fuzz test listing was // properly handled.
diff --git a/fuzztest/internal/registry.cc b/fuzztest/internal/registry.cc index ac695f0..563f684 100644 --- a/fuzztest/internal/registry.cc +++ b/fuzztest/internal/registry.cc
@@ -21,6 +21,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/functional/function_ref.h" #include "absl/strings/string_view.h" +#include "./common/logging.h" #include "./fuzztest/internal/registration.h" #include "./fuzztest/internal/runtime.h" @@ -57,8 +58,10 @@ for (auto& t : Regs()) func(t); } -void RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory) { - Regs().emplace_back(std::move(test_info), std::move(factory)); +FuzzTest& RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory) { + FUZZTEST_CHECK(!Runtime::instance().init_fuzztest_called()) + << "RegisterFuzzTest() must be called before InitFuzzTest()."; + return Regs().emplace_back(std::move(test_info), std::move(factory)); } void RegisterSetUpTearDownTestSuiteFunctions(
diff --git a/fuzztest/internal/registry.h b/fuzztest/internal/registry.h index 0e8a446..77acabc 100644 --- a/fuzztest/internal/registry.h +++ b/fuzztest/internal/registry.h
@@ -34,7 +34,10 @@ namespace fuzztest { namespace internal { -void RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory); +FuzzTest& RegisterImpl(BasicTestInfo test_info, FuzzTestFuzzerFactory factory); + +// TODO(b/570019271): Remove declaration once bug is fixed. +void RegisterFuzzTestAsGoogleTest(FuzzTest& test); void ForEachTest(absl::FunctionRef<void(FuzzTest&)> func); @@ -61,8 +64,8 @@ &Fixture::TearDownTestSuite); } BasicTestInfo test_info = reg.test_info_; - RegisterImpl(std::move(test_info), - GetFuzzTestFuzzerFactory(std::move(reg))); + RegisterFuzzTestAsGoogleTest(RegisterImpl( + std::move(test_info), GetFuzzTestFuzzerFactory(std::move(reg)))); return *this; } @@ -122,8 +125,7 @@ // fuzz test registration by calling functions `GetRegistration()` and // `GetRegistrationWithFixture()`. // -// Make sure to only call this function before calling -// `RegisterFuzzTestsAsGoogleTests()`. +// Make sure to only call this function before calling `InitFuzzTest()`. template <typename Registration> void RegisterFuzzTest(Registration&& reg) { ::fuzztest::internal::RegistrationToken{} = std::forward<Registration>(reg);
diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h index 1e0cbdd..38d16a9 100644 --- a/fuzztest/internal/runtime.h +++ b/fuzztest/internal/runtime.h
@@ -163,6 +163,28 @@ void SetRunMode(RunMode run_mode) { run_mode_ = run_mode; } RunMode run_mode() const { return run_mode_; } + // Records that `InitFuzzTest` has been called. + void SetInitFuzzTestCalled() { init_fuzztest_called_ = true; } + bool init_fuzztest_called() const { return init_fuzztest_called_; } + + // Sets the command-line arguments passed to `InitFuzzTest`. `argc()` and + // `argv()` are only valid after `InitFuzzTest` has been called. + void SetArgs(int* argc, char*** argv) { + argc_ = argc; + argv_ = argv; + } + int* argc() const { return argc_; } + char*** argv() const { return argv_; } + + // Sets the initial configuration created by `InitFuzzTest`. + // `init_configuration()` is only valid after `InitFuzzTest` has been called. + void SetInitConfiguration(Configuration configuration) { + init_configuration_ = std::move(configuration); + } + const Configuration& init_configuration() const { + return init_configuration_; + } + // Enables the crash reporter. // REQUIRES: `SetCurrentTest()` has been called with non-null arguments. void EnableReporter(const RuntimeStats* stats, absl::Time (*clock_fn)()) { @@ -265,6 +287,11 @@ const RuntimeStats* stats_ = nullptr; absl::Time (*clock_fn_)() = nullptr; + bool init_fuzztest_called_ = false; + int* argc_ = nullptr; + char*** argv_ = nullptr; + Configuration init_configuration_; + // We use a simple custom spinlock instead of absl::Mutex to reduce // dependencies and avoid potential issues with code instrumentation. class ABSL_LOCKABLE Spinlock {
diff --git a/fuzztest/llvm_fuzzer_wrapper.cc b/fuzztest/llvm_fuzzer_wrapper.cc index 95e3d7c..ccaead0 100644 --- a/fuzztest/llvm_fuzzer_wrapper.cc +++ b/fuzztest/llvm_fuzzer_wrapper.cc
@@ -16,7 +16,6 @@ #include "absl/random/random.h" #include "absl/synchronization/mutex.h" #include "./common/logging.h" -#include "./fuzztest/fuzztest.h" #include "./fuzztest/fuzztest_macros.h" #include "./fuzztest/internal/domains/arbitrary_impl.h" #include "./fuzztest/internal/domains/container_of_impl.h"