From 97c527aeb89bb5f3a41c9da12ed96ea50ae60efd Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Tue, 22 Sep 2026 19:57:28 +0100 Subject: [PATCH] OSS-Fuzz: Add new fuzzer targets Package Info Reader Signed-off-by: Arthur Chan --- Source/cmPackageInfoReader.cxx | 6 + Tests/Fuzzing/CMakeLists.txt | 3 + Tests/Fuzzing/cmPackageInfoReader.dict | 63 ++++++ Tests/Fuzzing/cmPackageInfoReaderFuzzer.cxx | 179 ++++++++++++++++++ .../corpus/cps/DefaultConfigurationsTest.cps | 24 +++ Tests/Fuzzing/corpus/cps/RequiresTest.cps | 49 +++++ Tests/Fuzzing/corpus/cps/TransitiveTest.cps | 11 ++ Tests/Fuzzing/corpus/cps/bad1.cps | 11 ++ Tests/Fuzzing/corpus/cps/badversion2.cps | 8 + Tests/Fuzzing/corpus/cps/customversion.cps | 8 + Tests/Fuzzing/corpus/cps/defs.cps | 27 +++ Tests/Fuzzing/corpus/cps/filesets-named.cps | 49 +++++ Tests/Fuzzing/corpus/cps/foo.cps | 17 ++ Tests/Fuzzing/corpus/cps/foo@default.cps | 13 ++ Tests/Fuzzing/corpus/cps/sample.cps | 8 + 15 files changed, 476 insertions(+) create mode 100644 Tests/Fuzzing/cmPackageInfoReader.dict create mode 100644 Tests/Fuzzing/cmPackageInfoReaderFuzzer.cxx create mode 100644 Tests/Fuzzing/corpus/cps/DefaultConfigurationsTest.cps create mode 100644 Tests/Fuzzing/corpus/cps/RequiresTest.cps create mode 100644 Tests/Fuzzing/corpus/cps/TransitiveTest.cps create mode 100644 Tests/Fuzzing/corpus/cps/bad1.cps create mode 100644 Tests/Fuzzing/corpus/cps/badversion2.cps create mode 100644 Tests/Fuzzing/corpus/cps/customversion.cps create mode 100644 Tests/Fuzzing/corpus/cps/defs.cps create mode 100644 Tests/Fuzzing/corpus/cps/filesets-named.cps create mode 100644 Tests/Fuzzing/corpus/cps/foo.cps create mode 100644 Tests/Fuzzing/corpus/cps/foo@default.cps create mode 100644 Tests/Fuzzing/corpus/cps/sample.cps diff --git a/Source/cmPackageInfoReader.cxx b/Source/cmPackageInfoReader.cxx index abc89d701a..5822d55dd6 100644 --- a/Source/cmPackageInfoReader.cxx +++ b/Source/cmPackageInfoReader.cxx @@ -31,6 +31,12 @@ #include "cmTargetTypes.h" #include "cmValue.h" +// When adding or changing CPS schema versions, component types, languages, or +// branch-selecting attributes, review Tests/Fuzzing/cmPackageInfoReader.dict. +// Add tokens including keys that help mutations reach the new paths; keep the +// dictionary focused on useful parser inputs, not an exhaustive list of CPS +// keywords. + namespace { // Map of CPS language names to CMake language name. Case insensitivity is diff --git a/Tests/Fuzzing/CMakeLists.txt b/Tests/Fuzzing/CMakeLists.txt index d62d1b9059..06392e511d 100644 --- a/Tests/Fuzzing/CMakeLists.txt +++ b/Tests/Fuzzing/CMakeLists.txt @@ -104,3 +104,6 @@ add_fuzzer(cmScriptFuzzer cmScriptFuzzer.cxx) # Fortran dependency scanner fuzzer add_fuzzer(cmFortranParserFuzzer cmFortranParserFuzzer.cxx) + +# CPS (Common Package Specification) package-info reader fuzzer +add_fuzzer(cmPackageInfoReaderFuzzer cmPackageInfoReaderFuzzer.cxx) diff --git a/Tests/Fuzzing/cmPackageInfoReader.dict b/Tests/Fuzzing/cmPackageInfoReader.dict new file mode 100644 index 0000000000..8138d28c8e --- /dev/null +++ b/Tests/Fuzzing/cmPackageInfoReader.dict @@ -0,0 +1,63 @@ +# Dictionary for the CPS (Common Package Specification) package-info reader. +# Keys and values that gate the reader's branches: the accepted schema range, +# the prefix marker, component types, and the per-configuration attributes. + +# Top-level keys +"\"cps_version\"" +"\"name\"" +"\"version\"" +"\"compat_version\"" +"\"version_schema\"" +"\"cps_path\"" +"\"prefix\"" +"\"components\"" +"\"requires\"" +"\"configurations\"" +"\"default_configurations\"" +"\"default_license\"" +"\"license\"" + +# Accepted schema versions (reader requires >= 0.13 and < 0.16) +"\"0.13\"" +"\"0.14\"" +"\"0.15\"" + +# Prefix resolution +"\"@prefix@\"" +"\"@prefix@/cps\"" + +# Version schemas +"\"simple\"" +"\"custom\"" +"\"rpm\"" +"\"dpkg\"" +"\"pep440\"" + +# Component types +"\"type\"" +"\"interface\"" +"\"executable\"" +"\"archive\"" +"\"dylib\"" +"\"module\"" +"\"symbolic\"" + +# Component attributes +"\"location\"" +"\"includes\"" +"\"definitions\"" +"\"compile_flags\"" +"\"link_flags\"" +"\"link_libraries\"" +"\"compile_features\"" +"\"link_features\"" +"\"requires\"" +"\"configurations\"" +"\"file_sets\"" +"\"cxx_modules\"" + +# Languages accepted for per-language properties +"\"c\"" +"\"cpp\"" +"\"cxx\"" +"\"fortran\"" diff --git a/Tests/Fuzzing/cmPackageInfoReaderFuzzer.cxx b/Tests/Fuzzing/cmPackageInfoReaderFuzzer.cxx new file mode 100644 index 0000000000..0708d0880c --- /dev/null +++ b/Tests/Fuzzing/cmPackageInfoReaderFuzzer.cxx @@ -0,0 +1,179 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ + +/* + * Fuzzer for CMake's CPS (Common Package Specification) reader. + * + * cmPackageInfoReader.cxx is 694 lines at 0.00% coverage. It parses the .cps + * JSON files that third-party packages install and that find_package() picks + * up from whatever is on the search path, so the input is supplied by the + * packages found on the machine rather than by the project being built. + * + * cmJSONParserFuzzer already covers jsoncpp itself. What is untested is + * everything the reader does with the decoded value: schema-version gating, + * prefix resolution against the file's own location, the "simple"/pep440 + * version grammar, requirement specs, and -- the bulk of the file -- turning + * each entry under "components" into an imported target, with per- + * configuration properties, link/compile features, definitions, file sets and + * C++ module metadata. + * + * Reaching the target-import half needs a real cmMakefile, so each input gets + * a fresh cmake/cmGlobalGenerator/cmMakefile fixture. That also keeps targets + * created by one input from colliding with the next. + * + * The reader resolves its prefix by matching "cps_path" against the directory + * the file was read from, so the input has to be a real file in a real + * directory: the fixture writes it to /cps/fuzz.cps, mirroring the layout + * the upstream CPS tests use ("cps_path": "@prefix@/cps"). + * + * Both the primary and the appendix read run on every input. An appendix is a + * supplemental file read with an already-parsed package as its parent, which + * skips the schema-version check and inherits the parent's prefix, components + * and default configurations -- a different path through Read() that no + * primary read can reach. + */ + +#include +#include +#include +#include +#include + +#include +#include + +#include "cmExecutionStatus.h" +#include "cmGlobalGenerator.h" +#include "cmMakefile.h" +#include "cmMessenger.h" +#include "cmPackageInfoReader.h" +#include "cmState.h" +#include "cmStateDirectory.h" +#include "cmStateSnapshot.h" +#include "cmSystemTools.h" +#include "cmTargetTypes.h" +#include "cmake.h" + +static constexpr size_t kMaxInputSize = 256 * 1024; + +static std::string g_prefixDir; +static std::string g_cpsFile; + +extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) +{ + (void)argc; + (void)argv; + + cmSystemTools::SetMessageCallback( + [](std::string const&, cmMessageMetadata const&) {}); + cmSystemTools::SetStdoutCallback([](std::string const&) {}); + cmSystemTools::SetStderrCallback([](std::string const&) {}); + + char tmpl[] = "/tmp/cmake_fuzz_cps_XXXXXX"; + char* dir = mkdtemp(tmpl); + if (dir) { + g_prefixDir = dir; + } else { + g_prefixDir = "/tmp/cmake_fuzz_cps"; + } + + cmSystemTools::MakeDirectory(g_prefixDir + "/cps"); + g_cpsFile = g_prefixDir + "/cps/fuzz.cps"; + + return 0; +} + +namespace { + +struct Fixture +{ + cmake CMake{ cmState::Role::Project }; + std::unique_ptr GG; + std::unique_ptr MF; + + Fixture() + { + this->CMake.SetHomeDirectory(g_prefixDir); + this->CMake.SetHomeOutputDirectory(g_prefixDir); + this->GG = cm::make_unique(&this->CMake); + cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot(); + snapshot.GetDirectory().SetCurrentBinary(g_prefixDir); + snapshot.GetDirectory().SetCurrentSource(g_prefixDir); + this->MF = cm::make_unique(this->GG.get(), snapshot); + } +}; + +void Consume(cmPackageInfoReader const& reader) +{ + (void)reader.GetName(); + + cm::optional const version = reader.GetVersion(); + cm::optional const compatVersion = reader.GetCompatVersion(); + (void)reader.ParseVersion(version); + (void)reader.ParseVersion(compatVersion); + + for (cmPackageRequirement const& req : reader.GetRequirements()) { + (void)req.Name; + (void)req.Version; + (void)req.Components; + (void)req.Hints; + } + (void)reader.GetComponentNames(); +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) +{ + if (size == 0 || size > kMaxInputSize) { + return 0; + } + + { + FILE* fp = fopen(g_cpsFile.c_str(), "wb"); + if (!fp) { + return 0; + } + bool const written = fwrite(data, 1, size, fp) == size; + fclose(fp); + if (!written) { + return 0; + } + } + + { + Fixture fx; + std::unique_ptr reader = + cmPackageInfoReader::Read(fx.MF.get(), g_cpsFile); + if (reader) { + Consume(*reader); + + cmExecutionStatus status(*fx.MF); + if (reader->ImportTargets(fx.MF.get(), status, + cm::ImportedTargetScope::Local)) { + reader->ImportTargetConfigurations(fx.MF.get(), status); + } + } + } + + { + Fixture fx; + std::unique_ptr parent = + cmPackageInfoReader::Read(fx.MF.get(), g_cpsFile); + if (parent) { + std::unique_ptr appendix = + cmPackageInfoReader::Read(fx.MF.get(), g_cpsFile, parent.get()); + if (appendix) { + Consume(*appendix); + + cmExecutionStatus status(*fx.MF); + if (appendix->ImportTargets(fx.MF.get(), status, + cm::ImportedTargetScope::Global)) { + appendix->ImportTargetConfigurations(fx.MF.get(), status); + } + } + } + } + + return 0; +} diff --git a/Tests/Fuzzing/corpus/cps/DefaultConfigurationsTest.cps b/Tests/Fuzzing/corpus/cps/DefaultConfigurationsTest.cps new file mode 100644 index 0000000000..0eb39accc0 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/DefaultConfigurationsTest.cps @@ -0,0 +1,24 @@ +{ + "cps_version": "0.13", + "name": "DefaultConfigurationsTest", + "cps_path": "@prefix@/cps", + "configurations": [ "Default" ], + "components": { + "Target1": { + "type": "interface", + "configurations": { + "Test": { + "includes": [ "@prefix@/include" ] + } + } + }, + "Target2": { + "type": "interface", + "configurations": { + "Test": { + "includes": [ "@prefix@/include" ] + } + } + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/RequiresTest.cps b/Tests/Fuzzing/corpus/cps/RequiresTest.cps new file mode 100644 index 0000000000..3a6affb5e5 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/RequiresTest.cps @@ -0,0 +1,49 @@ +{ + "cps_version": "0.14.0", + "name": "RequiresTest", + "cps_path": "@prefix@/cps", + "components": { + "Indirect": { + "type": "interface", + "requires": [ ":Direct" ] + }, + "Direct": { + "type": "interface", + "definitions": { + "*": { + "ANSWER": 42 + } + } + }, + "CompileOnly": { + "type": "interface", + "compile_requires": [ ":BrokenLibrary" ] + }, + "BrokenLibrary": { + "type": "archive", + "location": "@prefix@/lib/does-not-exist.a", + "definitions": { + "*": { + "ANSWER": 42 + } + } + }, + "Private1": { + "type": "dylib" + }, + "Private2": { + "type": "dylib" + }, + "Other": { + "type": "dylib", + "link_libraries": [ "every-config" ], + "dyld_requires": [ ":Private1" ], + "configurations": { + "test": { + "link_libraries": [ "test-config" ], + "dyld_requires": [ ":Private2" ] + } + } + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/TransitiveTest.cps b/Tests/Fuzzing/corpus/cps/TransitiveTest.cps new file mode 100644 index 0000000000..4c5e31e30e --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/TransitiveTest.cps @@ -0,0 +1,11 @@ +{ + "cps_version": "0.13", + "name": "TransitiveTest", + "cps_path": "@prefix@/cps", + "requires": { + "TransitiveDep": { + "components": [ "Target2" ] + } + }, + "components": {} +} diff --git a/Tests/Fuzzing/corpus/cps/bad1.cps b/Tests/Fuzzing/corpus/cps/bad1.cps new file mode 100644 index 0000000000..96666caa78 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/bad1.cps @@ -0,0 +1,11 @@ +{ + "cps_version": "0.13", + "name": "Bad1", + "cps_path": "@prefix@/cps", + "requires": "Broken", + "components": { + "Target": { + "type": "interface" + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/badversion2.cps b/Tests/Fuzzing/corpus/cps/badversion2.cps new file mode 100644 index 0000000000..99e683dbb9 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/badversion2.cps @@ -0,0 +1,8 @@ +{ + "cps_version": "0.13", + "name": "BadVersion2", + "version": "1.1a.0", + "version_schema": "simple", + "cps_path": "@prefix@/cps", + "components": {} +} diff --git a/Tests/Fuzzing/corpus/cps/customversion.cps b/Tests/Fuzzing/corpus/cps/customversion.cps new file mode 100644 index 0000000000..30aa29dd60 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/customversion.cps @@ -0,0 +1,8 @@ +{ + "cps_version": "0.13", + "name": "CustomVersion", + "version": "VII", + "version_schema": "roman", + "cps_path": "@prefix@/cps", + "components": {} +} diff --git a/Tests/Fuzzing/corpus/cps/defs.cps b/Tests/Fuzzing/corpus/cps/defs.cps new file mode 100644 index 0000000000..cb937720ca --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/defs.cps @@ -0,0 +1,27 @@ +{ + "cps_version": "0.13", + "name": "defs", + "cps_path": "@prefix@/cps", + "components": { + "defs": { + "type": "interface", + "definitions": { + "c": { + "ONLY_IN_C": null, + "OVERRIDE1": "1", + "OVERRIDE2": "1" + }, + "cxx": { + "ONLY_IN_CXX": null, + "OVERRIDE1": "2" + }, + "*": { + "NOVALUE": null, + "EMPTYVALUE": "", + "OVERRIDE1": "0", + "OVERRIDE2": "0" + } + } + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/filesets-named.cps b/Tests/Fuzzing/corpus/cps/filesets-named.cps new file mode 100644 index 0000000000..3e8911a4af --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/filesets-named.cps @@ -0,0 +1,49 @@ +{ + "cps_version": "0.15.0", + "name": "FileSets-Named", + "cps_path": "@prefix@/cps", + "components": { + "Target": { + "file_sets": + [ + { + "extensions": + { + "cmake": + { + "name@v1": "foo" + } + }, + "type": "includes", + "root": "@prefix@/include/a", + "files": [ "foo1.h", "foo2.h" ] + }, + { + "extensions": + { + "cmake": + { + "name@v1": "foo" + } + }, + "type": "includes", + "root": "@prefix@/include/b", + "files": [ "foo3.h" ] + }, + { + "extensions": + { + "cmake": + { + "name@v1": "bar" + } + }, + "type": "includes", + "root": "@prefix@/include", + "files": [ "bar.h" ] + } + ], + "type": "interface" + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/foo.cps b/Tests/Fuzzing/corpus/cps/foo.cps new file mode 100644 index 0000000000..7c93e1fb2b --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/foo.cps @@ -0,0 +1,17 @@ +{ + "cps_version": "0.13", + "name": "Foo", + "cps_path": "@prefix@/cps", + "default_configurations": ["default"], + "components": { + "PrefixTest": { + "type": "interface" + }, + "Empty": { + "type": "interface" + }, + "ExecutableTest": { + "type": "executable" + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/foo@default.cps b/Tests/Fuzzing/corpus/cps/foo@default.cps new file mode 100644 index 0000000000..ee5c8204b2 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/foo@default.cps @@ -0,0 +1,13 @@ +{ + "cps_version": "0.13", + "name": "Foo", + "configuration": "default", + "components": { + "PrefixTest": { + "includes": ["@prefix@/include"] + }, + "ExecutableTest": { + "location": "@prefix@/foo" + } + } +} diff --git a/Tests/Fuzzing/corpus/cps/sample.cps b/Tests/Fuzzing/corpus/cps/sample.cps new file mode 100644 index 0000000000..41ec3c34b7 --- /dev/null +++ b/Tests/Fuzzing/corpus/cps/sample.cps @@ -0,0 +1,8 @@ +{ + "cps_version": "0.13", + "name": "Sample", + "version": "2.10.11", + "compat_version": "2.0.0", + "cps_path": "@prefix@/cps", + "components": {} +}