GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/cvmfs_fsck.cc
Date: 2026-10-04 02:40:33
Exec Total Coverage
Lines: 0 177 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 <cstdio>
21 #include <cstdlib>
22 #include <cstring>
23 #include <string>
24
25 #include "compression/compression.h"
26 #include "crypto/hash.h"
27 #include "util/atomic.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 atomic_int32 g_num_files;
46 atomic_int32 g_num_err_fixed;
47 atomic_int32 g_num_err_unfixed;
48 atomic_int32 g_num_err_operational;
49 atomic_int32 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 atomic_int32 g_force_rebuild;
62 atomic_int32 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 = atomic_xadd32(&g_num_files, 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 ✗ atomic_inc32(&g_num_tmp_catalog);
154 ✗ if (g_fix_errors) {
155 ✗ if (unlink(relative_path.c_str()) == 0) {
156 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Fix: %s unlinked", path.c_str());
157 ✗ atomic_inc32(&g_num_err_fixed);
158 } else {
159 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Error: failed to unlink %s",
160 path.c_str());
161 ✗ atomic_inc32(&g_num_err_unfixed);
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 ✗ atomic_inc32(&g_num_err_operational);
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 ✗ atomic_inc32(&g_num_err_operational);
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 ✗ atomic_inc32(&g_num_err_operational);
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 ✗ atomic_inc32(&g_num_err_fixed);
218
219 // Changes made, we have to rebuild the managed cache db
220 ✗ atomic_cas32(&g_force_rebuild, 0, 1);
221 ✗ atomic_cas32(&g_modified_cache, 0, 1);
222 } else {
223 ✗ atomic_inc32(&g_num_err_unfixed);
224 }
225 ✗ } else {
226 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
227 "Error: %s has compressed checksum %s, "
228 "delete this file from cache directory!",
229 path.c_str(), hash.ToString().c_str());
230 ✗ atomic_inc32(&g_num_err_unfixed);
231 }
232 }
233 }
234 }
235 ✗ close(fd_src);
236 }
237
238 ✗ return NULL;
239 }
240
241
242 ✗ int main(int argc, char **argv) {
243 ✗ atomic_init32(&g_force_rebuild);
244 ✗ atomic_init32(&g_modified_cache);
245 ✗ g_current_dir = new string();
246
247 int c;
248 ✗ while ((c = getopt(argc, argv, "hvpfj:")) != -1) {
249 ✗ switch (c) {
250 ✗ case 'h':
251 ✗ Usage();
252 ✗ return kErrorOk;
253 ✗ case 'v':
254 ✗ g_verbose = true;
255 ✗ break;
256 ✗ case 'p':
257 ✗ g_fix_errors = true;
258 ✗ break;
259 ✗ case 'f':
260 ✗ atomic_cas32(&g_force_rebuild, 0, 1);
261 ✗ break;
262 ✗ case 'j':
263 ✗ g_num_threads = atoi(optarg);
264 ✗ if (g_num_threads < 1) {
265 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
266 "There is at least one worker thread required");
267 ✗ return kErrorUsage;
268 }
269 ✗ break;
270 ✗ case '?':
271 default:
272 ✗ Usage();
273 ✗ return kErrorUsage;
274 }
275 }
276
277 // Switch to cache directory
278 ✗ if (optind >= argc) {
279 ✗ Usage();
280 ✗ return kErrorUsage;
281 }
282 ✗ g_cache_dir = new string(MakeCanonicalPath(argv[optind]));
283 ✗ if (chdir(g_cache_dir->c_str()) != 0) {
284 ✗ LogCvmfs(kLogCvmfs, kLogStderr, "Could not chdir to %s",
285 g_cache_dir->c_str());
286 ✗ return kErrorOperational;
287 }
288
289 // Check if txn directory is empty
290 DIR *dirp_txn;
291 ✗ if ((dirp_txn = opendir("txn")) == NULL) {
292 ✗ LogCvmfs(kLogCvmfs, kLogStderr,
293 "Invalid cache directory, %s/txn does not exist",
294 g_cache_dir->c_str());
295 ✗ return kErrorOperational;
296 }
297 platform_dirent64 *d;
298 ✗ while ((d = platform_readdir(dirp_txn)) != NULL) {
299 ✗ const string name = d->d_name;
300 ✗ if ((name == ".") || (name == ".."))
301 ✗ continue;
302
303 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
304 "Warning: temporary directory %s/txn is not empty\n"
305 "If this repository is currently _not_ mounted, "
306 "you can remove its contents",
307 g_cache_dir->c_str());
308 ✗ break;
309 }
310 ✗ closedir(dirp_txn);
311
312 // Run workers to recalculate checksums
313 ✗ atomic_init32(&g_num_files);
314 ✗ atomic_init32(&g_num_err_fixed);
315 ✗ atomic_init32(&g_num_err_unfixed);
316 ✗ atomic_init32(&g_num_err_operational);
317 ✗ atomic_init32(&g_num_tmp_catalog);
318 pthread_t *workers = reinterpret_cast<pthread_t *>(
319 ✗ smalloc(g_num_threads * sizeof(pthread_t)));
320 ✗ if (!g_verbose)
321 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "Verifying: ");
322 ✗ for (int i = 0; i < g_num_threads; ++i) {
323 ✗ if (g_verbose)
324 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Starting worker %d", i + 1);
325 ✗ if (pthread_create(&workers[i], NULL, MainCheck, NULL) != 0) {
326 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Fatal: could not create worker thread");
327 ✗ return kErrorOperational;
328 }
329 }
330 ✗ for (int i = g_num_threads - 1; i >= 0; --i) {
331 ✗ pthread_join(workers[i], NULL);
332 ✗ if (g_verbose)
333 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Stopping worker %d", i + 1);
334 }
335 ✗ free(workers);
336 ✗ if (!g_verbose)
337 ✗ LogCvmfs(kLogCvmfs, kLogStdout | kLogNoLinebreak, "\n");
338 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Verified %d files",
339 atomic_read32(&g_num_files));
340
341 ✗ if (atomic_read32(&g_num_tmp_catalog) > 0)
342 ✗ LogCvmfs(kLogCvmfs, kLogStdout, "Temporary file catalogs were found.");
343
344 ✗ if (atomic_read32(&g_force_rebuild)) {
345 ✗ if (unlink("cachedb") == 0) {
346 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
347 "Fix: managed cache db unlinked, will be rebuilt on next mount");
348 ✗ atomic_inc32(&g_num_err_fixed);
349 } else {
350 ✗ if (errno != ENOENT) {
351 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
352 "Error: could not unlink managed cache database (%d)", errno);
353 ✗ atomic_inc32(&g_num_err_unfixed);
354 }
355 }
356 }
357
358 ✗ if (atomic_read32(&g_modified_cache)) {
359 ✗ LogCvmfs(kLogCvmfs, kLogStdout,
360 "\n"
361 "WARNING: There might by corrupted files in the kernel buffers.\n"
362 "Remount CernVM-FS or run 'echo 3 > /proc/sys/vm/drop_caches'"
363 "\n\n");
364 }
365
366 ✗ int retval = 0;
367 ✗ if (atomic_read32(&g_num_err_fixed) > 0)
368 ✗ retval |= kErrorFixed;
369 ✗ if (atomic_read32(&g_num_err_unfixed) > 0)
370 ✗ retval |= kErrorUnfixed;
371 ✗ if (atomic_read32(&g_num_err_operational) > 0)
372 ✗ retval |= kErrorOperational;
373
374 ✗ return retval;
375 }
376