diff --git a/CMakeLists.txt b/CMakeLists.txt index acb1afbc67..26b4f10730 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,7 @@ option(MATERIALX_BUILD_OIIO "Build OpenImageIO support for MaterialXRender." OFF option(MATERIALX_BUILD_OCIO "Build OpenColorIO support for shader generators." OFF) option(MATERIALX_BUILD_TESTS "Build unit tests." OFF) option(MATERIALX_BUILD_BENCHMARK_TESTS "Build benchmark tests." OFF) +option(MATERIALX_BUILD_FUZZ_TESTS "Build Google FuzzTest fuzz tests for the XML I/O layer." OFF) option(MATERIALX_BUILD_OSOS "Build OSL .oso's of standard library shaders for the OSL Network generator" OFF) option(MATERIALX_BUILD_PERFETTO_TRACING "Build with Perfetto tracing support for performance analysis." OFF) @@ -370,6 +371,14 @@ else() add_compile_options(${DYNAMIC_ANALYSIS_OPTIONS}) add_link_options(${DYNAMIC_ANALYSIS_OPTIONS}) endif() + if(MATERIALX_BUILD_FUZZ_TESTS AND FUZZTEST_FUZZING_MODE) + # Add SanitizerCoverage instrumentation to all MaterialX libraries. + # -fsanitize=fuzzer-no-link injects edge counters and cmp tables without + # linking any fuzzer runtime — FuzzTest's own engine reads those counters + # to guide mutation. + add_compile_options(-fsanitize=fuzzer-no-link) + add_link_options(-fsanitize=fuzzer-no-link) + endif() if(MATERIALX_BUILD_JS) if (CMAKE_BUILD_TYPE MATCHES Debug) add_compile_options(-fexceptions) @@ -596,6 +605,29 @@ if(MATERIALX_BUILD_TESTS) add_subdirectory(source/MaterialXTest) endif() +# Add Google FuzzTest subdirectory +if(MATERIALX_BUILD_FUZZ_TESTS) + include(FetchContent) + FetchContent_Declare( + fuzztest + GIT_REPOSITORY https://github.com/google/fuzztest.git + GIT_TAG main + SYSTEM + ) + + # CMake propagates the current directory's COMPILE_OPTIONS into every + # subdirectory added from this point. Temporarily clear them before fetching + # FuzzTest so that FuzzTest, abseil, re2, and googletest compile with their + # own warning policies rather than MaterialX's -Wshadow/-Wunused-parameter/etc. + # Restore afterwards so source/MaterialXFuzz still gets the full set. + get_directory_property(_mx_saved_compile_options COMPILE_OPTIONS) + set_directory_properties(PROPERTIES COMPILE_OPTIONS "") + FetchContent_MakeAvailable(fuzztest) + set_directory_properties(PROPERTIES COMPILE_OPTIONS "${_mx_saved_compile_options}") + + add_subdirectory(source/MaterialXFuzz) +endif() + if (MATERIALX_BUILD_DOCS) add_subdirectory(documents) endif() diff --git a/documents/DeveloperGuide/MainPage.md b/documents/DeveloperGuide/MainPage.md index 1c6a031bfd..3944c25ed1 100644 --- a/documents/DeveloperGuide/MainPage.md +++ b/documents/DeveloperGuide/MainPage.md @@ -54,6 +54,44 @@ Select the `MATERIALX_BUILD_VIEWER` option to build the MaterialX Viewer. Insta To generate HTML documentation for the MaterialX C++ API, make sure a version of [Doxygen](https://www.doxygen.org/) is on your path, and select the advanced option `MATERIALX_BUILD_DOCS` in CMake. This option will add a target named `MaterialXDocs` to your project, which can be built as an independent step from your development environment. +### Building Fuzz Tests + +Select the `MATERIALX_BUILD_FUZZ_TESTS` option to build the [Google FuzzTest](https://github.com/google/fuzztest) fuzz target for the XML I/O layer. The fuzz target is located in `source/MaterialXFuzz/` and is fetched automatically via CMake's FetchContent. + +**Unit-test mode** (any compiler, no special toolchain required): + +```sh +cmake -B build -DMATERIALX_BUILD_FUZZ_TESTS=ON +cmake --build build --target MaterialXFuzz +./build/bin/MaterialXFuzz +``` + +In this mode the fuzz target runs as a standard GoogleTest case. + +**Coverage-guided fuzzing mode** (requires Clang and libFuzzer): + +```sh +cmake -B build \ + -DMATERIALX_BUILD_FUZZ_TESTS=ON \ + -DFUZZTEST_FUZZING_MODE=ON \ + -DMATERIALX_DYNAMIC_ANALYSIS=ON \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ +cmake --build build --target MaterialXFuzz +./build/bin/MaterialXFuzz --fuzz=MaterialXXmlIoFuzz.ParseXmlString +``` + +Setting `FUZZTEST_FUZZING_MODE=ON` enables SanitizerCoverage instrumentation across all MaterialX libraries, allowing FuzzTest's mutation engine to explore new code paths. Adding `MATERIALX_DYNAMIC_ANALYSIS=ON` further enables AddressSanitizer and UndefinedBehaviorSanitizer for more thorough bug detection. + +To persist the corpus between runs and limit fuzzing duration: + +```sh +./build/bin/MaterialXFuzz \ + --fuzz=MaterialXXmlIoFuzz.ParseXmlString \ + --fuzz_for=300s \ + --corpus_database=./corpus/ +``` + ## Editor Setup MaterialX should work in any editor that supports CMake, or that CMake can generate a project for. diff --git a/source/MaterialXFuzz/CMakeLists.txt b/source/MaterialXFuzz/CMakeLists.txt new file mode 100644 index 0000000000..ad71835496 --- /dev/null +++ b/source/MaterialXFuzz/CMakeLists.txt @@ -0,0 +1,43 @@ +include(GoogleTest) + +add_executable(MaterialXFuzz XmlIo_fuzz.cpp) + +target_link_libraries(MaterialXFuzz PRIVATE + MaterialXFormat + MaterialXCore) + +target_include_directories(MaterialXFuzz PRIVATE + ${CMAKE_SOURCE_DIR}/source) + +# link_fuzztest selects the fuzzing or unit-test backend based on FUZZTEST_FUZZING_MODE. +link_fuzztest(MaterialXFuzz) + +# FuzzTest's CMakeLists.txt calls add_compile_options(-Werror) globally, which propagates here. +# FuzzTest's own headers then trigger -Wshadow/-Wreturn-type during template instantiation. +# Override with -Wno-error so warnings from third-party headers don't fail the build. +if(NOT MSVC) + target_compile_options(MaterialXFuzz PRIVATE + -Wno-error # FuzzTest's CMakeLists propagates -Werror globally + -Wno-return-type # meta.h:167 falls off end without return; GCC 14 hard-errors this + -Wno-shadow # meta.h:67 lambda captures shadow outer parameter + -Wno-sign-compare) # value_mutation_helpers.h:62 int vs size_t comparison + + # FuzzTest compiles abseil with -fsanitize=address by default in fuzzing mode. + # Link the ASan runtime into the final executable to resolve those symbols. + if(FUZZTEST_FUZZING_MODE) + target_link_options(MaterialXFuzz PRIVATE -fsanitize=address) + endif() +endif() + +# Always copy resources to build/bin/resources/ +# add_custom_target(ALL) ensures this runs on every build, not just when the +# binary is relinked. +add_custom_target(MaterialXFuzzResources ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different + ${CMAKE_SOURCE_DIR}/resources ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/resources + COMMENT "Copying resources to build/bin/resources/ for fuzz tests") +add_dependencies(MaterialXFuzz MaterialXFuzzResources) + +gtest_discover_tests(MaterialXFuzz + WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} + DISCOVERY_MODE PRE_TEST) diff --git a/source/MaterialXFuzz/XmlIo_fuzz.cpp b/source/MaterialXFuzz/XmlIo_fuzz.cpp new file mode 100644 index 0000000000..db07c0cf08 --- /dev/null +++ b/source/MaterialXFuzz/XmlIo_fuzz.cpp @@ -0,0 +1,25 @@ +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" +#include +#include + +namespace mx = MaterialX; + +void ParseXmlString(const std::string& xml_string) +{ + mx::DocumentPtr doc = mx::createDocument(); + try + { + mx::readFromXmlString(doc, xml_string); + doc->validate(); + } + catch (const mx::Exception&) + { + // MaterialX exceptions indicate expected parse/validation failures, not bugs. + } +} + +FUZZ_TEST(MaterialXXmlIoFuzz, ParseXmlString) + .WithDomains(fuzztest::Arbitrary().WithMaxSize(1024 * 1024)) + .WithSeeds(fuzztest::ReadFilesFromDirectory( + "resources/Materials/Examples"));