GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/cvmfs_fsck.cc
Date: 2026-10-11 02:40:02
Exec Total Coverage
Lines: 0 159 0.0%
Branches: 0 126 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 *
4 * This tool checks a cvmfs cache directory for consistency.
5 * If necessary, the managed cache db is removed so that
6 * it will be rebuilt on next mount.
7 */
8
9 #define _FILE_OFFSET_BITS 64
10
11
12 #include <dirent.h>
13 #include <errno.h>
14 #include <fcntl.h>
15 #include <pthread.h>
16 #include <stdint.h>
17 #include <sys/stat.h>
18 #include <unistd.h>
19
20 #include <atomic>
21 #include <cstdio>
22 #include <cstdlib>
23 #include <cstring>
24 #include <string>
25
26 #include "compression/compression.h"
27 #include "crypto/hash.h"
28 #include "util/logging.h"
29 #include "util/platform.h"
30 #include "util/posix.h"
31 #include "util/smalloc.h"
32
33 using namespace std; // NOLINT
34
35 enum Errors {
36 kErrorOk = 0,
37 kErrorFixed = 1,
38 kErrorReboot = 2,
39 kErrorUnfixed = 4,
40 kErrorOperational = 8,
41 kErrorUsage = 16,
42 };
43
44 string *g_cache_dir;
45 std::atomic<int32_t> g_num_files;
46 std::atomic<int32_t> g_num_err_fixed;
47 std::atomic<int32_t> g_num_err_unfixed;
48 std::atomic<int32_t> g_num_err_operational;
49 std::atomic<int32_t> g_num_tmp_catalog;
50 /**
51 * Traversal of the file system tree is serialized.
52 */
53 pthread_mutex_t g_lock_traverse = PTHREAD_MUTEX_INITIALIZER;
54 DIR *g_DIRP_current = NULL;
55 int g_num_dirs = -1; /**< Number of cache directories already examined. */
56 string *g_current_dir; /**< Current cache sub directory */
57
58 int g_num_threads = 1;
59 bool g_fix_errors = false;
60 bool g_verbose = false;
61 std::atomic<int32_t> g_force_rebuild;
62 std::atomic<int32_t> g_modified_cache;
63
64
65 ✗ static void Usage() {
66 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
67 "CernVM File System consistency checker, version %s\n\n"
68 "This tool checks a cvmfs cache directory for consistency.\n"
69 "If necessary, the managed cache db is removed so that\n"
70 "it will be rebuilt on next mount.\n\n"
71 "Usage: cvmfs_fsck [-v] [-p] [-f] [-j #threads] <cache directory>\n"
72 "Options:\n"
73 " -v verbose output\n"
74 " -p try to fix automatically\n"
75 " -f force rebuild of managed cache db on next mount\n"
76 " -j number of concurrent integrity check worker threads\n",
77 CVMFS_VERSION);
78 }
79
80
81 ✗ static bool GetNextFile(string *relative_path, string *hash_name) {
82 ✗ platform_dirent64 *d = NULL;
83
84 ✗ const MutexLockGuard m(&g_lock_traverse);
85 ✗ get_next_file_again:
86 ✗ while (g_DIRP_current && ((d = platform_readdir(g_DIRP_current)) != NULL)) {
87 ✗ const string name = d->d_name;
88 ✗ if ((name == ".") || (name == ".."))
89 ✗ continue;
90
91 platform_stat64 info;
92 ✗ *relative_path = *g_current_dir + "/" + name;
93 ✗ *hash_name = *g_current_dir + name;
94 ✗ const string path = *g_cache_dir + "/" + *relative_path;
95 ✗ if (platform_lstat(relative_path->c_str(), &info) != 0) {
96 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Warning: failed to stat() %s (%d)",
97 path.c_str(), errno);
98 ✗ continue;
99 }
100
101 ✗ if (!S_ISREG(info.st_mode)) {
102 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Warning: %s is not a regular file",
103 path.c_str());
104 ✗ continue;
105 }
106
107 ✗ break;
108 }
109
110 ✗ if (!d) {
111 ✗ if (g_DIRP_current) {
112 ✗ closedir(g_DIRP_current);
113 ✗ g_DIRP_current = NULL;
114 }
115 ✗ g_num_dirs++;
116 ✗ if (g_num_dirs < 256) {
117 char hex[3];
118 ✗ snprintf(hex, sizeof(hex), "%02x", g_num_dirs);
119 ✗ *g_current_dir = string(hex, 2);
120
121 ✗ if (g_verbose)
122 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Entering %s", g_current_dir->c_str());
123 ✗ if ((g_DIRP_current = opendir(hex)) == NULL) {
124 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
125 "Invalid cache directory, %s/%s does not exist",
126 g_cache_dir->c_str(), g_current_dir->c_str());
127 ✗ exit(kErrorUnfixed);
128 }
129 ✗ goto get_next_file_again;
130 }
131 }
132
133 ✗ return d != NULL;
134 }
135
136
137 ✗ static void *MainCheck(void *data __attribute__((unused))) {
138 ✗ string relative_path;
139 ✗ string hash_name;
140
141 ✗ while (GetNextFile(&relative_path, &hash_name)) {
142 ✗ const string path = *g_cache_dir + "/" + relative_path;
143
144 ✗ const int n = g_num_files.fetch_add(1);
145 ✗ if (g_verbose)
146 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Checking file %s", path.c_str());
147 ✗ if (!g_verbose && ((n % 1000) == 0))
148 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, ".");
149
150 ✗ if (relative_path[relative_path.length() - 1] == 'T') {
151 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
152 "Warning: temporary file catalog %s found", path.c_str());
153 g_num_tmp_catalog.fetch_add(1);
154 ✗ if (g_fix_errors) {
155 ✗ if (unlink(relative_path.c_str()) == 0) {
156 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Fix: %s unlinked", path.c_str());
157 g_num_err_fixed.fetch_add(1);
158 } else {
159 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Error: failed to unlink %s",
160 path.c_str());
161 g_num_err_unfixed.fetch_add(1);
162 }
163 }
164 ✗ continue;
165 }
166
167 ✗ const int fd_src = open(relative_path.c_str(), O_RDONLY);
168 ✗ if (fd_src < 0) {
169 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Error: cannot open %s", path.c_str());
170 g_num_err_operational.fetch_add(1);
171 ✗ continue;
172 }
173 // Don't thrash kernel buffers
174 ✗ platform_disable_kcache(fd_src);
175
176 // Compress every file and calculate SHA-1 of stream
177 ✗ const shash::Any expected_hash = shash::MkFromHexPtr(
178 shash::HexPtr(hash_name));
179 ✗ shash::Any hash(expected_hash.algorithm);
180 ✗ if (!zlib::CompressFd2Null(fd_src, &hash)) {
181 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Error: could not compress %s",
182 path.c_str());
183 g_num_err_operational.fetch_add(1);
184 } else {
185 ✗ if (hash != expected_hash) {
186 // If the hashes don't match, try hashing the uncompressed file
187 ✗ if (!shash::HashFile(relative_path, &hash)) {
188 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Error: could not hash %s",
189 path.c_str());
190 g_num_err_operational.fetch_add(1);
191 }
192 ✗ if (hash != expected_hash) {
193 ✗ if (g_fix_errors) {
194 ✗ const string quarantaine_path = "./quarantaine/" + hash_name;
195 ✗ bool fixed = false;
196 ✗ if (rename(relative_path.c_str(), quarantaine_path.c_str()) == 0) {
197 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
198 "Fix: %s is corrupted, moved to quarantaine folder",
199 path.c_str());
200 ✗ fixed = true;
201 } else {
202 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
203 "Warning: failed to move %s into quarantaine folder",
204 path.c_str());
205 ✗ if (unlink(relative_path.c_str()) == 0) {
206 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
207 "Fix: %s is corrupted, file unlinked", path.c_str());
208 ✗ fixed = true;
209 } else {
210 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
211 "Error: %s is corrupted, could not unlink",
212 path.c_str());
213 }
214 }
215
216 ✗ if (fixed) {
217 g_num_err_fixed.fetch_add(1);
218
219 // Changes made, we have to rebuild the managed cache db
220 ✗ int32_t expected_val = 0;
221 g_force_rebuild.compare_exchange_strong(expected_val, 1);
222 ✗ expected_val = 0;
223 g_modified_cache.compare_exchange_strong(expected_val, 1);
224 } else {
225 g_num_err_unfixed.fetch_add(1);
226 }
227 ✗ } else {
228 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
229 "Error: %s has compressed checksum %s, "
230 "delete this file from cache directory!",
231 path.c_str(), hash.ToString().c_str());
232 g_num_err_unfixed.fetch_add(1);
233 }
234 }
235 }
236 }
237 ✗ close(fd_src);
238 }
239
240 ✗ return NULL;
241 }
242
243
244 ✗ int main(int argc, char **argv) {
245 g_force_rebuild.store(0);
246 g_modified_cache.store(0);
247 ✗ g_current_dir = new string();
248
249 int c;
250 ✗ while ((c = getopt(argc, argv, "hvpfj:")) != -1) {
251 ✗ switch (c) {
252 ✗ case 'h':
253 ✗ Usage();
254 ✗ return kErrorOk;
255 ✗ case 'v':
256 ✗ g_verbose = true;
257 ✗ break;
258 ✗ case 'p':
259 ✗ g_fix_errors = true;
260 ✗ break;
261 ✗ case 'f': {
262 ✗ int32_t expected_val = 0;
263 g_force_rebuild.compare_exchange_strong(expected_val, 1);
264 ✗ } break;
265 ✗ case 'j':
266 ✗ g_num_threads = atoi(optarg);
267 ✗ if (g_num_threads < 1) {
268 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
269 "There is at least one worker thread required");
270 ✗ return kErrorUsage;
271 }
272 ✗ break;
273 ✗ case '?':
274 default:
275 ✗ Usage();
276 ✗ return kErrorUsage;
277 }
278 }
279
280 // Switch to cache directory
281 ✗ if (optind >= argc) {
282 ✗ Usage();
283 ✗ return kErrorUsage;
284 }
285 ✗ g_cache_dir = new string(MakeCanonicalPath(argv[optind]));
286 ✗ if (chdir(g_cache_dir->c_str()) != 0) {
287 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Could not chdir to %s",
288 g_cache_dir->c_str());
289 ✗ return kErrorOperational;
290 }
291
292 // Check if txn directory is empty
293 DIR *dirp_txn;
294 ✗ if ((dirp_txn = opendir("txn")) == NULL) {
295 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
296 "Invalid cache directory, %s/txn does not exist",
297 g_cache_dir->c_str());
298 ✗ return kErrorOperational;
299 }
300 platform_dirent64 *d;
301 ✗ while ((d = platform_readdir(dirp_txn)) != NULL) {
302 ✗ const string name = d->d_name;
303 ✗ if ((name == ".") || (name == ".."))
304 ✗ continue;
305
306 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
307 "Warning: temporary directory %s/txn is not empty\n"
308 "If this repository is currently _not_ mounted, "
309 "you can remove its contents",
310 g_cache_dir->c_str());
311 ✗ break;
312 }
313 ✗ closedir(dirp_txn);
314
315 // Run workers to recalculate checksums
316 g_num_files.store(0);
317 g_num_err_fixed.store(0);
318 g_num_err_unfixed.store(0);
319 g_num_err_operational.store(0);
320 g_num_tmp_catalog.store(0);
321 pthread_t *workers = reinterpret_cast<pthread_t *>(
322 ✗ smalloc(g_num_threads * sizeof(pthread_t)));
323 ✗ if (!g_verbose)
324 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "Verifying: ");
325 ✗ for (int i = 0; i < g_num_threads; ++i) {
326 ✗ if (g_verbose)
327 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Starting worker %d", i + 1);
328 ✗ if (pthread_create(&workers[i], NULL, MainCheck, NULL) != 0) {
329 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Fatal: could not create worker thread");
330 ✗ return kErrorOperational;
331 }
332 }
333 ✗ for (int i = g_num_threads - 1; i >= 0; --i) {
334 ✗ pthread_join(workers[i], NULL);
335 ✗ if (g_verbose)
336 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Stopping worker %d", i + 1);
337 }
338 ✗ free(workers);
339 ✗ if (!g_verbose)
340 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "\n");
341 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Verified %d files", g_num_files.load());
342
343 ✗ if (g_num_tmp_catalog.load() > 0)
344 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Temporary file catalogs were found.");
345
346 ✗ if (g_force_rebuild.load()) {
347 ✗ if (unlink("cachedb") == 0) {
348 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
349 "Fix: managed cache db unlinked, will be rebuilt on next mount");
350 g_num_err_fixed.fetch_add(1);
351 } else {
352 ✗ if (errno != ENOENT) {
353 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
354 "Error: could not unlink managed cache database (%d)", errno);
355 g_num_err_unfixed.fetch_add(1);
356 }
357 }
358 }
359
360 ✗ if (g_modified_cache.load()) {
361 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
362 "\n"
363 "WARNING: There might by corrupted files in the kernel buffers.\n"
364 "Remount CernVM-FS or run 'echo 3 > /proc/sys/vm/drop_caches'"
365 "\n\n");
366 }
367
368 ✗ int retval = 0;
369 ✗ if (g_num_err_fixed.load() > 0)
370 ✗ retval |= kErrorFixed;
371 ✗ if (g_num_err_unfixed.load() > 0)
372 ✗ retval |= kErrorUnfixed;
373 ✗ if (g_num_err_operational.load() > 0)
374 ✗ retval |= kErrorOperational;
375
376 ✗ return retval;
377 }
378
379