GCC Code Coverage Report


Directory: cvmfs/
File: cvmfs/cvmfs_suid_helper.cc
Date: 2025-06-22 02:36:02
Exec Total Coverage
Lines: 0 191 0.0%
Branches: 0 131 0.0%

Line Branch Exec Source
1 /**
2 * This file is part of the CernVM File System.
3 *
4 * Runs mount/umount commands and removes scratch space on behalf of a
5 * repository owner. Server-only utility.
6 *
7 * This binary does not use the cvmfs infrastructure code to stay lean.
8 */
9
10 // clang-format off
11 #include <sys/xattr.h> // NOLINT
12 // clang-format on
13
14 #include <dirent.h>
15 #include <errno.h>
16 #include <sys/stat.h>
17 #include <sys/wait.h>
18 #include <unistd.h>
19
20 #include <cstdio>
21 #include <cstdlib>
22 #include <cstring>
23 #include <string>
24
25 #include "cvmfs_suid_util.h"
26 #include "sanitizer.h"
27 #include "util/platform.h"
28
29 using namespace std; // NOLINT
30
31 const char *kSpoolArea = "/var/spool/cvmfs";
32
33 enum RemountType {
34 kRemountRdonly,
35 kRemountRw
36 };
37
38 static void GetCredentials(uid_t *calling_uid, uid_t *effective_uid) {
39 *calling_uid = getuid();
40 *effective_uid = geteuid();
41 }
42
43 static void ExecAsRoot(const char *binary, const char *arg1, const char *arg2,
44 const char *arg3, const char *arg4) {
45 char *argv[] = {strdup(binary),
46 arg1 ? strdup(arg1) : NULL,
47 arg2 ? strdup(arg2) : NULL,
48 arg3 ? strdup(arg3) : NULL,
49 arg4 ? strdup(arg4) : NULL,
50 NULL};
51 char *environ[] = {NULL};
52
53 const int retval = setuid(0);
54 if (retval != 0) {
55 fprintf(stderr, "failed to gain root privileges (%d)\n", errno);
56 exit(1);
57 }
58
59 execve(binary, argv, environ);
60 fprintf(stderr, "failed to run %s... (%d)\n", binary, errno);
61 exit(1);
62 }
63
64 static void ForkAndExecAsRoot(const char *binary, const char *arg1,
65 const char *arg2, const char *arg3,
66 const char *arg4) {
67 const pid_t child = fork();
68 if (child == -1) {
69 fprintf(stderr, "failed to fork %s... (%d)\n", binary, errno);
70 exit(1);
71 } else if (child == 0) {
72 ExecAsRoot(binary, arg1, arg2, arg3, arg4);
73 } else {
74 int wstatus;
75 waitpid(child, &wstatus, 0);
76 if (WIFSIGNALED(wstatus)) {
77 exit(128 + WTERMSIG(wstatus));
78 } else if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus)) {
79 exit(WEXITSTATUS(wstatus));
80 }
81 }
82 }
83
84 static void Remount(const string &path, const RemountType how) {
85 string remount_option = "remount,";
86 switch (how) {
87 case kRemountRw:
88 remount_option += "rw";
89 break;
90 case kRemountRdonly:
91 remount_option += "ro";
92 break;
93 default:
94 fprintf(stderr, "internal error\n");
95 exit(1);
96 }
97
98 // Note: the device name "dev" doesn't matter. It won't change from the
99 // "overlayfs_<fqrn>" name used when originally mounting the path. It is
100 // a dummy argument to appease the mount command.
101 ExecAsRoot("/bin/mount", "-o", remount_option.c_str(), "dev", path.c_str());
102 }
103
104 static void Mount(const string &path) {
105 platform_stat64 info;
106 const int retval = platform_stat("/bin/systemctl", &info);
107 if (retval == 0) {
108 string systemd_unit = cvmfs_suid::EscapeSystemdUnit(path);
109 // On newer versions of systemd, the mount unit is based on the fully
110 // resolved path (discovered on Ubuntu 18.04, test 539)
111 if (!cvmfs_suid::PathExists(string("/run/systemd/generator/")
112 + systemd_unit)) {
113 const string resolved_path = cvmfs_suid::ResolvePath(path);
114 if (resolved_path.empty()) {
115 fprintf(stderr, "cannot resolve %s\n", path.c_str());
116 exit(1);
117 }
118 systemd_unit = cvmfs_suid::EscapeSystemdUnit(resolved_path);
119 }
120 ForkAndExecAsRoot("/bin/systemctl", "restart", systemd_unit.c_str(), NULL,
121 NULL);
122 ExecAsRoot("/bin/systemctl", "reset-failed", systemd_unit.c_str(), NULL,
123 NULL);
124 } else {
125 ExecAsRoot("/bin/mount", path.c_str(), NULL, NULL, NULL);
126 }
127 }
128
129 static void Umount(const string &path) {
130 ExecAsRoot("/bin/umount", path.c_str(), NULL, NULL, NULL);
131 }
132
133 static void LazyUmount(const string &path) {
134 ExecAsRoot("/bin/umount", "-l", path.c_str(), NULL, NULL);
135 }
136
137 static void KillCvmfs(const string &fqrn) {
138 // prevent exploitation like:
139 // fqrn = ../../../../usr/home/file_with_xattr_user.pid
140 if (fqrn.find("/") != string::npos || fqrn.find("\\") != string::npos) {
141 exit(1);
142 }
143 string pid;
144 const string mountpoint = string(kSpoolArea) + "/" + fqrn + "/rdonly";
145 const bool retval = platform_getxattr(mountpoint.c_str(), "user.pid", &pid);
146 if (!retval || pid.empty())
147 exit(1);
148 const sanitizer::PositiveIntegerSanitizer pid_sanitizer;
149 if (!pid_sanitizer.IsValid(pid))
150 exit(1);
151 ExecAsRoot("/bin/kill", "-9", pid.c_str(), NULL, NULL);
152 }
153
154 class ScopedWorkingDirectory {
155 public:
156 explicit ScopedWorkingDirectory(const string &path)
157 : previous_path_(GetCurrentWorkingDirectory()), directory_handle_(NULL) {
158 ChangeDirectory(path);
159 directory_handle_ = opendir(".");
160 }
161
162 ~ScopedWorkingDirectory() {
163 if (directory_handle_ != NULL) {
164 closedir(directory_handle_);
165 }
166 ChangeDirectory(previous_path_);
167 }
168
169 operator bool() const { return directory_handle_ != NULL; }
170
171 struct DirectoryEntry {
172 bool is_directory;
173 string name;
174 };
175
176 bool NextDirectoryEntry(DirectoryEntry *entry) {
177 platform_dirent64 *dirent;
178 while ((dirent = platform_readdir(directory_handle_)) != NULL
179 && IsDotEntry(dirent)) {
180 }
181 if (dirent == NULL) {
182 return false;
183 }
184
185 platform_stat64 info;
186 if (platform_lstat(dirent->d_name, &info) != 0) {
187 return false;
188 }
189
190 entry->is_directory = S_ISDIR(info.st_mode);
191 entry->name = dirent->d_name;
192 return true;
193 }
194
195 protected:
196 string GetCurrentWorkingDirectory() {
197 char path[PATH_MAX];
198 const char *cwd = getcwd(path, PATH_MAX);
199 assert(cwd == path);
200 return string(cwd);
201 }
202
203 void ChangeDirectory(const string &path) {
204 const int retval = chdir(path.c_str());
205 assert(retval == 0);
206 }
207
208 bool IsDotEntry(const platform_dirent64 *dirent) {
209 return (strcmp(dirent->d_name, ".") == 0)
210 || (strcmp(dirent->d_name, "..") == 0);
211 }
212
213 private:
214 const string previous_path_;
215 DIR *directory_handle_;
216 };
217
218 static bool ClearDirectory(const string &path) {
219 ScopedWorkingDirectory swd(path);
220 if (!swd) {
221 return false;
222 }
223
224 bool success = true;
225 ScopedWorkingDirectory::DirectoryEntry dirent;
226 while (success && swd.NextDirectoryEntry(&dirent)) {
227 success = (dirent.is_directory) ? ClearDirectory(dirent.name)
228 && (rmdir(dirent.name.c_str()) == 0)
229 : (unlink(dirent.name.c_str()) == 0);
230 }
231
232 return success;
233 }
234
235 static int CleanupDirectory(const string &path) {
236 if (!ClearDirectory(path)) {
237 fprintf(stderr, "failed to clear %s\n", path.c_str());
238 return 1;
239 }
240 return 0;
241 }
242
243 static int DoSynchronousScratchCleanup(const string &fqrn) {
244 const string scratch = string(kSpoolArea) + "/" + fqrn + "/scratch/current";
245 return CleanupDirectory(scratch);
246 }
247
248 static int DoAsynchronousScratchCleanup(const string &fqrn) {
249 const string wastebin = string(kSpoolArea) + "/" + fqrn + "/scratch/wastebin";
250
251 // double-fork to daemonize the process and redirect I/O to /dev/null
252 pid_t pid;
253 int statloc;
254 if ((pid = fork()) == 0) {
255 int retval = setsid();
256 assert(retval != -1);
257 if ((pid = fork()) == 0) {
258 const int null_read = open("/dev/null", O_RDONLY);
259 const int null_write = open("/dev/null", O_WRONLY);
260 assert((null_read >= 0) && (null_write >= 0));
261 retval = dup2(null_read, 0);
262 assert(retval == 0);
263 retval = dup2(null_write, 1);
264 assert(retval == 1);
265 retval = dup2(null_write, 2);
266 assert(retval == 2);
267 close(null_read);
268 close(null_write);
269 } else {
270 assert(pid > 0);
271 _exit(0);
272 }
273 } else {
274 assert(pid > 0);
275 waitpid(pid, &statloc, 0);
276 _exit(0);
277 }
278
279 return CleanupDirectory(wastebin);
280 }
281
282 static void Usage(const string &exe, FILE *output) {
283 fprintf(output,
284 "Usage: %s lock|open|rw_mount|rw_umount|rdonly_mount|rdonly_umount|"
285 "clear_scratch|clear_scratch_async|kill_cvmfs <fqrn>\n"
286 "Example: %s rw_umount atlas.cern.ch\n"
287 "This binary is typically called by cvmfs_server.\n",
288 exe.c_str(), exe.c_str());
289 }
290
291
292 int main(int argc, char *argv[]) {
293 umask(077);
294 int retval;
295
296 // Figure out real and effective uid
297 uid_t calling_uid, effective_uid, repository_uid;
298 GetCredentials(&calling_uid, &effective_uid);
299 if (effective_uid != 0) {
300 fprintf(stderr, "Needs to run as root\n");
301 return 1;
302 }
303
304 // Arguments
305 if (argc != 3) {
306 Usage(argv[0], stderr);
307 return 1;
308 }
309 const string command = argv[1];
310 const string fqrn = argv[2];
311
312 // Verify if repository exists
313 platform_stat64 info;
314 retval = platform_lstat((string(kSpoolArea) + "/" + fqrn).c_str(), &info);
315 if (retval != 0) {
316 fprintf(stderr, "unknown repository: %s\n", fqrn.c_str());
317 return 1;
318 }
319 repository_uid = info.st_uid;
320
321 // Verify if caller uid matches
322 if ((calling_uid != 0) && (calling_uid != repository_uid)) {
323 fprintf(stderr, "called as %d, repository owned by %d\n", calling_uid,
324 repository_uid);
325 return 1;
326 }
327
328 if (command == "lock") {
329 Remount("/cvmfs/" + fqrn, kRemountRdonly);
330 } else if (command == "open") {
331 Remount("/cvmfs/" + fqrn, kRemountRw);
332 } else if (command == "kill_cvmfs") {
333 KillCvmfs(fqrn);
334 } else if (command == "rw_mount") {
335 Mount("/cvmfs/" + fqrn);
336 } else if (command == "rw_umount") {
337 Umount("/cvmfs/" + fqrn);
338 } else if (command == "rw_lazy_umount") {
339 LazyUmount("/cvmfs/" + fqrn);
340 } else if (command == "rdonly_mount") {
341 Mount(string(kSpoolArea) + "/" + fqrn + "/rdonly");
342 } else if (command == "rdonly_umount") {
343 Umount(string(kSpoolArea) + "/" + fqrn + "/rdonly");
344 } else if (command == "rdonly_lazy_umount") {
345 LazyUmount(string(kSpoolArea) + "/" + fqrn + "/rdonly");
346 } else if (command == "clear_scratch") {
347 return DoSynchronousScratchCleanup(fqrn);
348 } else if (command == "clear_scratch_async") {
349 return DoAsynchronousScratchCleanup(fqrn);
350 } else {
351 Usage(argv[0], stderr);
352 return 1;
353 }
354
355 return 0;
356 }
357