/src/CMake/Tests/Fuzzing/cmScriptFuzzer.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 CMake script execution |
6 | | * |
7 | | * This fuzzer executes CMake scripts in script mode (-P). |
8 | | * This exercises the majority of CMake's codebase including: |
9 | | * - All built-in commands |
10 | | * - Variable expansion |
11 | | * - Control flow (if, foreach, while, function, macro) |
12 | | * - String/list/file operations |
13 | | * - Generator expressions |
14 | | * |
15 | | * This is the highest-impact fuzzer for coverage. |
16 | | * |
17 | | * Performance notes: |
18 | | * - Uses memfd_create on Linux for memory-backed file I/O |
19 | | * - Falls back to temp files on other platforms |
20 | | */ |
21 | | |
22 | | #include <cstddef> |
23 | | #include <cstdint> |
24 | | #include <cstdio> |
25 | | #include <cstdlib> |
26 | | #include <string> |
27 | | #include <vector> |
28 | | |
29 | | #include <unistd.h> |
30 | | |
31 | | #include "cmCMakePolicyCommand.h" |
32 | | #include "cmExecutionStatus.h" |
33 | | #include "cmGlobalGenerator.h" |
34 | | #include "cmMakefile.h" |
35 | | #include "cmMessenger.h" |
36 | | #include "cmState.h" |
37 | | #include "cmStateSnapshot.h" |
38 | | #include "cmSystemTools.h" |
39 | | #include "cmake.h" |
40 | | |
41 | | #ifdef __linux__ |
42 | | # include <sys/mman.h> |
43 | | # ifndef MFD_CLOEXEC |
44 | | # define MFD_CLOEXEC 0x0001U |
45 | | # endif |
46 | | #endif |
47 | | |
48 | | static constexpr size_t kMaxInputSize = 256 * 1024; |
49 | | static std::string g_testDir; |
50 | | static std::string g_scriptFile; |
51 | | static bool g_useMemfd = false; |
52 | | |
53 | | extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) |
54 | 2 | { |
55 | 2 | (void)argc; |
56 | 2 | (void)argv; |
57 | | |
58 | | // Suppress output during fuzzing (set once at init) |
59 | 2 | cmSystemTools::SetMessageCallback( |
60 | 3 | [](std::string const&, cmMessageMetadata const&) {}); |
61 | 2 | cmSystemTools::SetStdoutCallback([](std::string const&) {}); |
62 | 2 | cmSystemTools::SetStderrCallback([](std::string const&) {}); |
63 | | |
64 | | // Create unique test directory (even with memfd, scripts can create files) |
65 | 2 | char tmpl[] = "/tmp/cmake_fuzz_script_XXXXXX"; |
66 | 2 | char* dir = mkdtemp(tmpl); |
67 | 2 | if (dir) { |
68 | 2 | g_testDir = dir; |
69 | 2 | } else { |
70 | 0 | g_testDir = "/tmp/cmake_fuzz_script"; |
71 | 0 | cmSystemTools::MakeDirectory(g_testDir); |
72 | 0 | } |
73 | | |
74 | 2 | #ifdef __linux__ |
75 | | // Try to use memfd for better performance |
76 | 2 | int fd = memfd_create("cmake_fuzz", MFD_CLOEXEC); |
77 | 2 | if (fd >= 0) { |
78 | 2 | g_useMemfd = true; |
79 | | // Create path via /proc/self/fd |
80 | 2 | g_scriptFile = "/proc/self/fd/" + std::to_string(fd); |
81 | | // Keep fd open - will be reused |
82 | 2 | } else |
83 | 0 | #endif |
84 | 0 | { |
85 | 0 | g_scriptFile = g_testDir + "/fuzz_script.cmake"; |
86 | 0 | } |
87 | | |
88 | 2 | return 0; |
89 | 2 | } |
90 | | |
91 | | extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) |
92 | 35 | { |
93 | 35 | if (size == 0 || size > kMaxInputSize) { |
94 | 0 | return 0; |
95 | 0 | } |
96 | | |
97 | 35 | #ifdef __linux__ |
98 | 35 | if (g_useMemfd) { |
99 | | // Extract fd from path and write directly |
100 | 35 | int fd = std::atoi(g_scriptFile.c_str() + 14); // "/proc/self/fd/" |
101 | 35 | ftruncate(fd, 0); |
102 | 35 | lseek(fd, 0, SEEK_SET); |
103 | 35 | if (write(fd, data, size) != static_cast<ssize_t>(size)) { |
104 | 0 | return 0; |
105 | 0 | } |
106 | 35 | } else |
107 | 0 | #endif |
108 | 0 | { |
109 | | // Write script to temp file |
110 | 0 | FILE* fp = fopen(g_scriptFile.c_str(), "wb"); |
111 | 0 | if (!fp) |
112 | 0 | return 0; |
113 | 0 | fwrite(data, 1, size, fp); |
114 | 0 | fclose(fp); |
115 | 0 | } |
116 | | |
117 | | // Save CWD in case script uses file(CHDIR) |
118 | 35 | std::string cwd = cmSystemTools::GetCurrentWorkingDirectory(); |
119 | | |
120 | | // Create cmake instance for script mode |
121 | 35 | cmake cm(cmState::Role::Script); |
122 | 35 | cm.SetHomeDirectory(g_testDir); |
123 | 35 | cm.SetHomeOutputDirectory(g_testDir); |
124 | | |
125 | | // Run the script |
126 | 35 | std::vector<std::string> args; |
127 | 35 | args.push_back("cmake"); |
128 | 35 | args.push_back("-P"); |
129 | 35 | args.push_back(g_scriptFile); |
130 | | |
131 | 35 | (void)cm.Run(args, false); |
132 | | |
133 | | // Restore CWD before cleanup (script may have changed it via file(CHDIR)) |
134 | 35 | cmSystemTools::ChangeDirectory(cwd); |
135 | | |
136 | | // Cleanup temp file (memfd doesn't need cleanup) |
137 | 35 | if (!g_useMemfd) { |
138 | 0 | unlink(g_scriptFile.c_str()); |
139 | 0 | } |
140 | | |
141 | | // Clean up any files the script may have created in g_testDir |
142 | | // This prevents disk growth and non-determinism from previous iterations |
143 | 35 | cmSystemTools::RemoveADirectory(g_testDir); |
144 | 35 | cmSystemTools::MakeDirectory(g_testDir); |
145 | | |
146 | 35 | return 0; |
147 | 35 | } |