/src/CMake/Tests/Fuzzing/cmFileLockFuzzer.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's file(LOCK) command |
6 | | * |
7 | | * The file(LOCK) command manages file locks for synchronization. |
8 | | * This fuzzer tests various lock scenarios and argument combinations. |
9 | | * |
10 | | * Coverage targets: |
11 | | * - Lock acquisition (LOCK) |
12 | | * - Lock release (RELEASE) |
13 | | * - Guard modes (FUNCTION, FILE, PROCESS) |
14 | | * - Timeout handling |
15 | | * - Error paths |
16 | | * |
17 | | * Security focus: |
18 | | * - Symlink handling (CVE for data destruction) |
19 | | * - Path traversal in lock paths |
20 | | * - Race conditions |
21 | | */ |
22 | | |
23 | | #include <cstddef> |
24 | | #include <cstdint> |
25 | | #include <cstdio> |
26 | | #include <cstring> |
27 | | #include <string> |
28 | | |
29 | | #include <unistd.h> |
30 | | |
31 | | #include "cmFileLock.h" |
32 | | #include "cmFileLockResult.h" |
33 | | #include "cmSystemTools.h" |
34 | | |
35 | | // Limit input size |
36 | | static constexpr size_t kMaxInputSize = 4096; |
37 | | |
38 | | // Sandbox directory |
39 | | static std::string g_testDir; |
40 | | |
41 | | extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) |
42 | 6 | { |
43 | 6 | (void)argc; |
44 | 6 | (void)argv; |
45 | | |
46 | | // Create unique test directory |
47 | 6 | char tmpl[] = "/tmp/cmake_fuzz_lock_XXXXXX"; |
48 | 6 | char* dir = mkdtemp(tmpl); |
49 | 6 | if (dir) { |
50 | 6 | g_testDir = dir; |
51 | 6 | } else { |
52 | 0 | g_testDir = "/tmp/cmake_fuzz_lock"; |
53 | 0 | cmSystemTools::MakeDirectory(g_testDir); |
54 | 0 | } |
55 | | |
56 | 6 | return 0; |
57 | 6 | } |
58 | | |
59 | | extern "C" int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size) |
60 | 152 | { |
61 | 152 | if (size < 1 || size > kMaxInputSize) { |
62 | 18 | return 0; |
63 | 18 | } |
64 | | |
65 | | // Use first byte for flags |
66 | 134 | uint8_t flags = data[0]; |
67 | | |
68 | | // Create a test file with known content |
69 | 134 | std::string testFile = g_testDir + "/lock_target.txt"; |
70 | 134 | std::string lockFile = g_testDir + "/test.lock"; |
71 | 134 | char const* testContent = "IMPORTANT DATA - MUST NOT BE TRUNCATED"; |
72 | | |
73 | 134 | { |
74 | 134 | FILE* fp = fopen(testFile.c_str(), "w"); |
75 | 134 | if (!fp) |
76 | 0 | return 0; |
77 | 134 | fputs(testContent, fp); |
78 | 134 | fclose(fp); |
79 | 134 | } |
80 | | |
81 | | // Test different scenarios based on fuzz input |
82 | 0 | cmFileLock lock; |
83 | | |
84 | | // Vary the lock file path based on remaining input |
85 | 134 | std::string lockPath = lockFile; |
86 | 134 | if (size > 1 && (flags & 0x01)) { |
87 | | // Use part of input as filename suffix (sanitized) |
88 | 130 | size_t nameLen = std::min(size - 1, size_t(32)); |
89 | 130 | std::string suffix; |
90 | 1.93k | for (size_t i = 0; i < nameLen; ++i) { |
91 | 1.80k | char c = static_cast<char>(data[1 + i]); |
92 | | // Only allow safe filename characters |
93 | 1.80k | if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || |
94 | 1.30k | (c >= '0' && c <= '9') || c == '_' || c == '-') { |
95 | 1.01k | suffix += c; |
96 | 1.01k | } |
97 | 1.80k | } |
98 | 130 | if (!suffix.empty()) { |
99 | 95 | lockPath = g_testDir + "/" + suffix + ".lock"; |
100 | 95 | } |
101 | 130 | } |
102 | | |
103 | | // Test symlink scenario (security-critical) |
104 | 134 | if (flags & 0x02) { |
105 | | // Create a symlink to the test file |
106 | 69 | std::string symlinkPath = g_testDir + "/symlink.lock"; |
107 | 69 | unlink(symlinkPath.c_str()); |
108 | 69 | if (symlink(testFile.c_str(), symlinkPath.c_str()) == 0) { |
109 | 69 | lockPath = symlinkPath; |
110 | 69 | } |
111 | 69 | } |
112 | | |
113 | | // Determine timeout - use 0 for fuzzing to avoid blocking |
114 | | // (non-zero timeouts would stall the fuzzer) |
115 | 134 | unsigned long timeout = 0; |
116 | | |
117 | | // Try to acquire lock |
118 | 134 | cmFileLockResult result = lock.Lock(lockPath, timeout); |
119 | 134 | (void)result.IsOk(); |
120 | | |
121 | | // Always try to release |
122 | 134 | (void)lock.Release(); |
123 | | |
124 | | // Security check: Verify test file wasn't truncated |
125 | 134 | { |
126 | 134 | FILE* fp = fopen(testFile.c_str(), "r"); |
127 | 134 | if (fp) { |
128 | 134 | char buffer[256] = { 0 }; |
129 | 134 | size_t bytesRead = fread(buffer, 1, sizeof(buffer) - 1, fp); |
130 | 134 | fclose(fp); |
131 | | |
132 | 134 | if (bytesRead == 0 || strcmp(buffer, testContent) != 0) { |
133 | | // DATA DESTRUCTION DETECTED! |
134 | 0 | fprintf(stderr, "VULNERABILITY: File was truncated or modified!\n"); |
135 | 0 | fprintf(stderr, "Expected: '%s'\n", testContent); |
136 | 0 | fprintf(stderr, "Got: '%s' (%zu bytes)\n", buffer, bytesRead); |
137 | 0 | abort(); |
138 | 0 | } |
139 | 134 | } |
140 | 134 | } |
141 | | |
142 | | // Cleanup |
143 | 134 | unlink(lockPath.c_str()); |
144 | 134 | unlink((g_testDir + "/symlink.lock").c_str()); |
145 | | |
146 | 134 | return 0; |
147 | 134 | } |