cmake/Tests/Fuzzing/cmScriptFuzzer.cxx
Leslie P. Polzer c602a9824c Tests/Fuzzing: Add cmScriptFuzzer
Fuzz the CMake script interpreter.
Tests execution of cmake -P scripts.
2026-01-20 14:06:36 -05:00

148 lines
3.8 KiB
C++

/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
file LICENSE.rst or https://cmake.org/licensing for details. */
/*
* Fuzzer for CMake script execution
*
* This fuzzer executes CMake scripts in script mode (-P).
* This exercises the majority of CMake's codebase including:
* - All built-in commands
* - Variable expansion
* - Control flow (if, foreach, while, function, macro)
* - String/list/file operations
* - Generator expressions
*
* This is the highest-impact fuzzer for coverage.
*
* Performance notes:
* - Uses memfd_create on Linux for memory-backed file I/O
* - Falls back to temp files on other platforms
*/
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
#include <unistd.h>
#include "cmCMakePolicyCommand.h"
#include "cmExecutionStatus.h"
#include "cmGlobalGenerator.h"
#include "cmMakefile.h"
#include "cmMessenger.h"
#include "cmState.h"
#include "cmStateSnapshot.h"
#include "cmSystemTools.h"
#include "cmake.h"
#ifdef __linux__
# include <sys/mman.h>
# ifndef MFD_CLOEXEC
# define MFD_CLOEXEC 0x0001U
# endif
#endif
static constexpr size_t kMaxInputSize = 256 * 1024;
static std::string g_testDir;
static std::string g_scriptFile;
static bool g_useMemfd = false;
extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv)
{
(void)argc;
(void)argv;
// Suppress output during fuzzing (set once at init)
cmSystemTools::SetMessageCallback(
[](std::string const&, cmMessageMetadata const&) {});
cmSystemTools::SetStdoutCallback([](std::string const&) {});
cmSystemTools::SetStderrCallback([](std::string const&) {});
// Create unique test directory (even with memfd, scripts can create files)
char tmpl[] = "/tmp/cmake_fuzz_script_XXXXXX";
char* dir = mkdtemp(tmpl);
if (dir) {
g_testDir = dir;
} else {
g_testDir = "/tmp/cmake_fuzz_script";
cmSystemTools::MakeDirectory(g_testDir);
}
#ifdef __linux__
// Try to use memfd for better performance
int fd = memfd_create("cmake_fuzz", MFD_CLOEXEC);
if (fd >= 0) {
g_useMemfd = true;
// Create path via /proc/self/fd
g_scriptFile = "/proc/self/fd/" + std::to_string(fd);
// Keep fd open - will be reused
} else
#endif
{
g_scriptFile = g_testDir + "/fuzz_script.cmake";
}
return 0;
}
extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size)
{
if (size == 0 || size > kMaxInputSize) {
return 0;
}
#ifdef __linux__
if (g_useMemfd) {
// Extract fd from path and write directly
int fd = std::atoi(g_scriptFile.c_str() + 14); // "/proc/self/fd/"
ftruncate(fd, 0);
lseek(fd, 0, SEEK_SET);
if (write(fd, data, size) != static_cast<ssize_t>(size)) {
return 0;
}
} else
#endif
{
// Write script to temp file
FILE* fp = fopen(g_scriptFile.c_str(), "wb");
if (!fp)
return 0;
fwrite(data, 1, size, fp);
fclose(fp);
}
// Save CWD in case script uses file(CHDIR)
std::string cwd = cmSystemTools::GetCurrentWorkingDirectory();
// Create cmake instance for script mode
cmake cm(cmState::Role::Script);
cm.SetHomeDirectory(g_testDir);
cm.SetHomeOutputDirectory(g_testDir);
// Run the script
std::vector<std::string> args;
args.push_back("cmake");
args.push_back("-P");
args.push_back(g_scriptFile);
(void)cm.Run(args, false);
// Restore CWD before cleanup (script may have changed it via file(CHDIR))
cmSystemTools::ChangeDirectory(cwd);
// Cleanup temp file (memfd doesn't need cleanup)
if (!g_useMemfd) {
unlink(g_scriptFile.c_str());
}
// Clean up any files the script may have created in g_testDir
// This prevents disk growth and non-determinism from previous iterations
cmSystemTools::RemoveADirectory(g_testDir);
cmSystemTools::MakeDirectory(g_testDir);
return 0;
}