| /* 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 file(LOCK) command |
| * |
| * The file(LOCK) command manages file locks for synchronization. |
| * This fuzzer tests various lock scenarios and argument combinations. |
| * |
| * Coverage targets: |
| * - Lock acquisition (LOCK) |
| * - Lock release (RELEASE) |
| * - Guard modes (FUNCTION, FILE, PROCESS) |
| * - Timeout handling |
| * - Error paths |
| * |
| * Security focus: |
| * - Symlink handling (CVE for data destruction) |
| * - Path traversal in lock paths |
| * - Race conditions |
| */ |
| |
| #include <cstddef> |
| #include <cstdint> |
| #include <cstdio> |
| #include <cstring> |
| #include <string> |
| |
| #include <unistd.h> |
| |
| #include "cmFileLock.h" |
| #include "cmFileLockResult.h" |
| #include "cmSystemTools.h" |
| |
| // Limit input size |
| static constexpr size_t kMaxInputSize = 4096; |
| |
| // Sandbox directory |
| static std::string g_testDir; |
| |
| extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) |
| { |
| (void)argc; |
| (void)argv; |
| |
| // Create unique test directory |
| char tmpl[] = "/tmp/cmake_fuzz_lock_XXXXXX"; |
| char* dir = mkdtemp(tmpl); |
| if (dir) { |
| g_testDir = dir; |
| } else { |
| g_testDir = "/tmp/cmake_fuzz_lock"; |
| cmSystemTools::MakeDirectory(g_testDir); |
| } |
| |
| return 0; |
| } |
| |
| extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) |
| { |
| if (size < 1 || size > kMaxInputSize) { |
| return 0; |
| } |
| |
| // Use first byte for flags |
| uint8_t flags = data[0]; |
| |
| // Create a test file with known content |
| std::string testFile = g_testDir + "/lock_target.txt"; |
| std::string lockFile = g_testDir + "/test.lock"; |
| char const* testContent = "IMPORTANT DATA - MUST NOT BE TRUNCATED"; |
| |
| { |
| FILE* fp = fopen(testFile.c_str(), "w"); |
| if (!fp) |
| return 0; |
| fputs(testContent, fp); |
| fclose(fp); |
| } |
| |
| // Test different scenarios based on fuzz input |
| cmFileLock lock; |
| |
| // Vary the lock file path based on remaining input |
| std::string lockPath = lockFile; |
| if (size > 1 && (flags & 0x01)) { |
| // Use part of input as filename suffix (sanitized) |
| size_t nameLen = std::min(size - 1, size_t(32)); |
| std::string suffix; |
| for (size_t i = 0; i < nameLen; ++i) { |
| char c = static_cast<char>(data[1 + i]); |
| // Only allow safe filename characters |
| if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || |
| (c >= '0' && c <= '9') || c == '_' || c == '-') { |
| suffix += c; |
| } |
| } |
| if (!suffix.empty()) { |
| lockPath = g_testDir + "/" + suffix + ".lock"; |
| } |
| } |
| |
| // Test symlink scenario (security-critical) |
| if (flags & 0x02) { |
| // Create a symlink to the test file |
| std::string symlinkPath = g_testDir + "/symlink.lock"; |
| unlink(symlinkPath.c_str()); |
| if (symlink(testFile.c_str(), symlinkPath.c_str()) == 0) { |
| lockPath = symlinkPath; |
| } |
| } |
| |
| // Determine timeout - use 0 for fuzzing to avoid blocking |
| // (non-zero timeouts would stall the fuzzer) |
| unsigned long timeout = 0; |
| |
| // Try to acquire lock |
| cmFileLockResult result = lock.Lock(lockPath, timeout); |
| (void)result.IsOk(); |
| |
| // Always try to release |
| (void)lock.Release(); |
| |
| // Security check: Verify test file wasn't truncated |
| { |
| FILE* fp = fopen(testFile.c_str(), "r"); |
| if (fp) { |
| char buffer[256] = { 0 }; |
| size_t bytesRead = fread(buffer, 1, sizeof(buffer) - 1, fp); |
| fclose(fp); |
| |
| if (bytesRead == 0 || strcmp(buffer, testContent) != 0) { |
| // DATA DESTRUCTION DETECTED! |
| fprintf(stderr, "VULNERABILITY: File was truncated or modified!\n"); |
| fprintf(stderr, "Expected: '%s'\n", testContent); |
| fprintf(stderr, "Got: '%s' (%zu bytes)\n", buffer, bytesRead); |
| abort(); |
| } |
| } |
| } |
| |
| // Cleanup |
| unlink(lockPath.c_str()); |
| unlink((g_testDir + "/symlink.lock").c_str()); |
| |
| return 0; |
| } |