/src/CMake/Tests/Fuzzing/cmCMakePresetsFuzzer.cxx
Line | Count | Source |
1 | | /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
2 | | file LICENSE.rst or https://cmake.org/licensing for details. */ |
3 | | |
4 | | /* |
5 | | * Fuzzer for CMakePresets.json / CMakeUserPresets.json handling |
6 | | * |
7 | | * cmJSONParserFuzzer already covers the jsoncpp syntax layer. This fuzzer |
8 | | * targets the layer above it, cmCMakePresetsGraph, which turns a parsed |
9 | | * document into a resolved preset graph. |
10 | | * |
11 | | * That code runs before any build logic does: `cmake --list-presets` reads |
12 | | * these files without configuring anything, and IDEs parse them to populate |
13 | | * their UI as soon as a folder is opened. The input is therefore untrusted |
14 | | * data rather than the trusted, executable build code that CMakeLists.txt is. |
15 | | * |
16 | | * Coverage targets: |
17 | | * - schema validation of configure/build/test/package/workflow presets |
18 | | * - "include" chains between preset files, and their cycle detection |
19 | | * - "inherits" resolution, and its cycle detection |
20 | | * - macro expansion ($env{}, $penv{}, ${sourceDir}, ...) |
21 | | * - version gating and the project/user file interaction |
22 | | */ |
23 | | |
24 | | #include <cstddef> |
25 | | #include <cstdint> |
26 | | #include <cstdio> |
27 | | #include <cstdlib> |
28 | | #include <initializer_list> |
29 | | #include <string> |
30 | | |
31 | | #include <unistd.h> |
32 | | |
33 | | #include "cmCMakePresetsGraph.h" |
34 | | #include "cmMessageMetadata.h" |
35 | | #include "cmSystemTools.h" |
36 | | |
37 | | static constexpr size_t kMaxInputSize = 64 * 1024; |
38 | | |
39 | | static std::string g_testDir; |
40 | | static std::string g_projectFile; |
41 | | static std::string g_userFile; |
42 | | static std::string g_explicitFile; |
43 | | static std::string g_includedFile; |
44 | | |
45 | | extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) |
46 | 10 | { |
47 | 10 | (void)argc; |
48 | 10 | (void)argv; |
49 | | |
50 | | // Suppress output during fuzzing (set once at init) |
51 | 10 | cmSystemTools::SetMessageCallback( |
52 | 10 | [](std::string const&, cmMessageMetadata const&) {}); |
53 | 10 | cmSystemTools::SetStdoutCallback([](std::string const&) {}); |
54 | 10 | cmSystemTools::SetStderrCallback([](std::string const&) {}); |
55 | | |
56 | 10 | char tmpl[] = "/tmp/cmake_fuzz_presets_XXXXXX"; |
57 | 10 | char* dir = mkdtemp(tmpl); |
58 | 10 | if (dir) { |
59 | 10 | g_testDir = dir; |
60 | 10 | } else { |
61 | 0 | g_testDir = "/tmp/cmake_fuzz_presets"; |
62 | 0 | cmSystemTools::MakeDirectory(g_testDir); |
63 | 0 | } |
64 | | |
65 | 10 | g_projectFile = g_testDir + "/CMakePresets.json"; |
66 | 10 | g_userFile = g_testDir + "/CMakeUserPresets.json"; |
67 | 10 | g_explicitFile = g_testDir + "/explicit.json"; |
68 | 10 | g_includedFile = g_testDir + "/included.json"; |
69 | | |
70 | 10 | return 0; |
71 | 10 | } |
72 | | |
73 | | namespace { |
74 | | |
75 | | // Which root file is on disk decides which reader runs, so the layout *is* the |
76 | | // mode -- see the comment in RunOneLayout(). |
77 | | enum class RootLayout |
78 | | { |
79 | | Project, |
80 | | User, |
81 | | Explicit, |
82 | | }; |
83 | | |
84 | | bool WriteWholeFile(std::string const& path, uint8_t const* data, size_t size) |
85 | 45.0k | { |
86 | 45.0k | FILE* fp = fopen(path.c_str(), "wb"); |
87 | 45.0k | if (!fp) { |
88 | 0 | return false; |
89 | 0 | } |
90 | 45.0k | bool const ok = fwrite(data, 1, size, fp) == size; |
91 | 45.0k | fclose(fp); |
92 | 45.0k | return ok; |
93 | 45.0k | } |
94 | | |
95 | | // Run the whole pipeline once, for one root layout, from a clean directory. |
96 | | void RunOneLayout(RootLayout layout, uint8_t const* data, size_t size) |
97 | 33.7k | { |
98 | 33.7k | unlink(g_projectFile.c_str()); |
99 | 33.7k | unlink(g_userFile.c_str()); |
100 | 33.7k | unlink(g_explicitFile.c_str()); |
101 | | |
102 | 33.7k | std::string presetsFileArg; |
103 | 33.7k | switch (layout) { |
104 | 11.2k | case RootLayout::Project: |
105 | 11.2k | if (!WriteWholeFile(g_projectFile, data, size)) { |
106 | 0 | return; |
107 | 0 | } |
108 | 11.2k | break; |
109 | 11.2k | case RootLayout::User: |
110 | 11.2k | if (!WriteWholeFile(g_userFile, data, size)) { |
111 | 0 | return; |
112 | 0 | } |
113 | 11.2k | break; |
114 | 11.2k | case RootLayout::Explicit: |
115 | 11.2k | if (!WriteWholeFile(g_explicitFile, data, size)) { |
116 | 0 | return; |
117 | 0 | } |
118 | 11.2k | presetsFileArg = g_explicitFile; |
119 | 11.2k | break; |
120 | 33.7k | } |
121 | | |
122 | | // A fresh graph per layout: ClearPresets() empties the preset maps but |
123 | | // leaves "errors" and "parseState" behind, so a reused graph would carry |
124 | | // diagnostic state from one layout into the next. |
125 | 33.7k | cmCMakePresetsGraph graph; |
126 | 33.7k | if (!graph.ReadProjectPresets(g_testDir, presetsFileArg)) { |
127 | 27.0k | return; |
128 | 27.0k | } |
129 | | |
130 | | // Walking the graph reaches the inheritance and macro-expansion results, not |
131 | | // just the parse that produced them. |
132 | 21.4k | for (auto const& it : graph.ConfigurePresets) { |
133 | 21.4k | (void)graph.GetGeneratorForPreset(it.first); |
134 | 21.4k | (void)it.second.Expanded.has_value(); |
135 | 21.4k | } |
136 | 6.74k | for (auto const& it : graph.BuildPresets) { |
137 | 279 | (void)graph.GetGeneratorForPreset(it.first); |
138 | 279 | (void)it.second.Expanded.has_value(); |
139 | 279 | } |
140 | 6.74k | for (auto const& it : graph.TestPresets) { |
141 | 234 | (void)graph.GetGeneratorForPreset(it.first); |
142 | 234 | (void)it.second.Expanded.has_value(); |
143 | 234 | } |
144 | 6.74k | for (auto const& it : graph.PackagePresets) { |
145 | 42 | (void)it.second.Expanded.has_value(); |
146 | 42 | } |
147 | 6.74k | for (auto const& it : graph.WorkflowPresets) { |
148 | 39 | (void)it.second.Expanded.has_value(); |
149 | 39 | } |
150 | 6.74k | } |
151 | | |
152 | | } |
153 | | |
154 | | extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) |
155 | 11.2k | { |
156 | 11.2k | if (size == 0 || size > kMaxInputSize) { |
157 | 7 | return 0; |
158 | 7 | } |
159 | | |
160 | 11.2k | if (!WriteWholeFile(g_includedFile, data, size)) { |
161 | 0 | return 0; |
162 | 0 | } |
163 | | |
164 | 11.2k | for (RootLayout layout : |
165 | 33.7k | { RootLayout::Project, RootLayout::User, RootLayout::Explicit }) { |
166 | 33.7k | RunOneLayout(layout, data, size); |
167 | 33.7k | } |
168 | | |
169 | 11.2k | return 0; |
170 | 11.2k | } |