xref: /freebsd/contrib/kyua/utils/fs/operations.cpp (revision 8a7df6c58d549bd2eaa5cad30b2ef93c3eca3c18)
1 // Copyright 2010 The Kyua Authors.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 // * Redistributions of source code must retain the above copyright
9 //   notice, this list of conditions and the following disclaimer.
10 // * Redistributions in binary form must reproduce the above copyright
11 //   notice, this list of conditions and the following disclaimer in the
12 //   documentation and/or other materials provided with the distribution.
13 // * Neither the name of Google Inc. nor the names of its contributors
14 //   may be used to endorse or promote products derived from this software
15 //   without specific prior written permission.
16 //
17 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 
29 #include "utils/fs/operations.hpp"
30 
31 #if defined(HAVE_CONFIG_H)
32 #   include "config.h"
33 #endif
34 
35 extern "C" {
36 #include <sys/param.h>
37 #if defined(HAVE_SYS_MOUNT_H)
38 #   include <sys/mount.h>
39 #endif
40 #include <sys/stat.h>
41 #if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS)
42 #   include <sys/statvfs.h>
43 #endif
44 #if defined(HAVE_SYS_VFS_H)
45 #   include <sys/vfs.h>
46 #endif
47 #include <sys/wait.h>
48 
49 #include <unistd.h>
50 }
51 
52 #include <cerrno>
53 #include <cstdlib>
54 #include <cstring>
55 #include <fstream>
56 #include <iostream>
57 #include <sstream>
58 #include <string>
59 
60 #include "utils/auto_array.ipp"
61 #include "utils/defs.hpp"
62 #include "utils/env.hpp"
63 #include "utils/format/macros.hpp"
64 #include "utils/fs/directory.hpp"
65 #include "utils/fs/exceptions.hpp"
66 #include "utils/fs/path.hpp"
67 #include "utils/logging/macros.hpp"
68 #include "utils/optional.ipp"
69 #include "utils/sanity.hpp"
70 #include "utils/units.hpp"
71 
72 namespace fs = utils::fs;
73 namespace units = utils::units;
74 
75 using utils::optional;
76 
77 
78 namespace {
79 
80 
81 /// Operating systems recognized by the code below.
82 enum os_type {
83     os_unsupported = 0,
84     os_freebsd,
85     os_linux,
86     os_netbsd,
87     os_sunos,
88 };
89 
90 
91 /// The current operating system.
92 static enum os_type current_os =
93 #if defined(__FreeBSD__)
94     os_freebsd
95 #elif defined(__linux__)
96     os_linux
97 #elif defined(__NetBSD__)
98     os_netbsd
99 #elif defined(__SunOS__)
100     os_sunos
101 #else
102     os_unsupported
103 #endif
104     ;
105 
106 
107 /// Error code returned by subprocess to indicate a controlled failure.
108 const int exit_known_error = 123;
109 
110 
111 static void run_mount_tmpfs [[noreturn]] (const fs::path&, const uint64_t);
112 
113 
114 /// Executes 'mount -t tmpfs' (or a similar variant).
115 ///
116 /// This function must be called from a subprocess as it never returns.
117 ///
118 /// \param mount_point Location on which to mount a tmpfs.
119 /// \param size The size of the tmpfs to mount.  If 0, use unlimited.
120 static void
121 run_mount_tmpfs(const fs::path& mount_point, const uint64_t size)
122 {
123     const char* mount_args[16];
124     std::string size_arg;
125 
126     std::size_t last = 0;
127     switch (current_os) {
128     case os_freebsd:
129         mount_args[last++] = "mount";
130         mount_args[last++] = "-ttmpfs";
131         if (size > 0) {
132             size_arg = F("-osize=%s") % size;
133             mount_args[last++] = size_arg.c_str();
134         }
135         mount_args[last++] = "tmpfs";
136         mount_args[last++] = mount_point.c_str();
137         break;
138 
139     case os_linux:
140         mount_args[last++] = "mount";
141         mount_args[last++] = "-ttmpfs";
142         if (size > 0) {
143             size_arg = F("-osize=%s") % size;
144             mount_args[last++] = size_arg.c_str();
145         }
146         mount_args[last++] = "tmpfs";
147         mount_args[last++] = mount_point.c_str();
148         break;
149 
150     case os_netbsd:
151         mount_args[last++] = "mount";
152         mount_args[last++] = "-ttmpfs";
153         if (size > 0) {
154             size_arg = F("-o-s%s") % size;
155             mount_args[last++] = size_arg.c_str();
156         }
157         mount_args[last++] = "tmpfs";
158         mount_args[last++] = mount_point.c_str();
159         break;
160 
161     case os_sunos:
162         mount_args[last++] = "mount";
163         mount_args[last++] = "-Ftmpfs";
164         if (size > 0) {
165             size_arg = F("-o-s%s") % size;
166             mount_args[last++] = size_arg.c_str();
167         }
168         mount_args[last++] = "tmpfs";
169         mount_args[last++] = mount_point.c_str();
170         break;
171 
172     default:
173         std::cerr << "Don't know how to mount a temporary file system in this "
174             "host operating system\n";
175         std::exit(exit_known_error);
176     }
177     mount_args[last] = NULL;
178 
179     const char** arg;
180     std::cout << "Mounting tmpfs onto " << mount_point << " with:";
181     for (arg = &mount_args[0]; *arg != NULL; arg++)
182         std::cout << " " << *arg;
183     std::cout << "\n";
184 
185     const int ret = ::execvp(mount_args[0],
186                              UTILS_UNCONST(char* const, mount_args));
187     INV(ret == -1);
188     std::cerr << "Failed to exec " << mount_args[0] << "\n";
189     std::exit(EXIT_FAILURE);
190 }
191 
192 
193 /// Unmounts a file system using unmount(2) or umount(2).
194 ///
195 /// This is a thin wrapper around the filesystem implementation for the actual
196 /// underlying system call which implements filesystem unmounting function.
197 ///
198 /// \param mount_point The file system to unmount.
199 ///
200 /// \throw fs::system_error The unmount operation fails.
201 static void
202 unmount_impl(const fs::path& mount_point)
203 {
204 
205 #if defined(HAVE_UNMOUNT)
206     if (::unmount(mount_point.c_str(), 0) == 0)
207         return;
208 #elif defined(HAVE_UMOUNT)
209     if (::umount(mount_point.c_str()) == 0)
210         return;
211 #endif
212     const int original_errno = errno;
213     throw fs::system_error(F("unmount(%s) failed") % mount_point,
214                            original_errno);
215 }
216 
217 /// Stats a file, without following links.
218 ///
219 /// \param path The file to stat.
220 ///
221 /// \return The stat structure on success.
222 ///
223 /// \throw system_error An error on failure.
224 static struct ::stat
225 safe_stat(const fs::path& path)
226 {
227     struct ::stat sb;
228     if (::lstat(path.c_str(), &sb) == -1) {
229         const int original_errno = errno;
230         throw fs::system_error(F("Cannot get information about %s") % path,
231                                original_errno);
232     }
233     return sb;
234 }
235 
236 
237 }  // anonymous namespace
238 
239 
240 /// Copies a file.
241 ///
242 /// \param source The file to copy.
243 /// \param target The destination of the new copy; must be a file name, not a
244 ///     directory.
245 ///
246 /// \throw error If there is a problem copying the file.
247 void
248 fs::copy(const fs::path& source, const fs::path& target)
249 {
250     std::ifstream input(source.c_str());
251     if (!input)
252         throw error(F("Cannot open copy source %s") % source);
253 
254     std::ofstream output(target.c_str());
255     if (!output)
256         throw error(F("Cannot create copy target %s") % target);
257 
258     char buffer[1024];
259     while (input.good()) {
260         input.read(buffer, sizeof(buffer));
261         if (input.good() || input.eof())
262             output.write(buffer, input.gcount());
263     }
264     if (!input.good() && !input.eof())
265         throw error(F("Error while reading input file %s") % source);
266 }
267 
268 
269 /// Queries the path to the current directory.
270 ///
271 /// \return The path to the current directory.
272 ///
273 /// \throw fs::error If there is a problem querying the current directory.
274 fs::path
275 fs::current_path(void)
276 {
277     char* cwd;
278 #if defined(HAVE_GETCWD_DYN)
279     cwd = ::getcwd(NULL, 0);
280 #else
281     cwd = ::getcwd(NULL, MAXPATHLEN);
282 #endif
283     if (cwd == NULL) {
284         const int original_errno = errno;
285         throw fs::system_error(F("Failed to get current working directory"),
286                                original_errno);
287     }
288 
289     try {
290         const fs::path result(cwd);
291         std::free(cwd);
292         return result;
293     } catch (...) {
294         std::free(cwd);
295         throw;
296     }
297 }
298 
299 
300 /// Checks if a file exists.
301 ///
302 /// Be aware that this is racy in the same way as access(2) is.
303 ///
304 /// \param path The file to check the existance of.
305 ///
306 /// \return True if the file exists; false otherwise.
307 bool
308 fs::exists(const fs::path& path)
309 {
310     return ::access(path.c_str(), F_OK) == 0;
311 }
312 
313 
314 /// Locates a file in the PATH.
315 ///
316 /// \param name The file to locate.
317 ///
318 /// \return The path to the located file or none if it was not found.  The
319 /// returned path is always absolute.
320 optional< fs::path >
321 fs::find_in_path(const char* name)
322 {
323     const optional< std::string > current_path = utils::getenv("PATH");
324     if (!current_path || current_path.get().empty())
325         return none;
326 
327     std::istringstream path_input(current_path.get() + ":");
328     std::string path_component;
329     while (std::getline(path_input, path_component, ':').good()) {
330         const fs::path candidate = path_component.empty() ?
331             fs::path(name) : (fs::path(path_component) / name);
332         if (exists(candidate)) {
333             if (candidate.is_absolute())
334                 return utils::make_optional(candidate);
335             else
336                 return utils::make_optional(candidate.to_absolute());
337         }
338     }
339     return none;
340 }
341 
342 
343 /// Calculates the free space in a given file system.
344 ///
345 /// \param path Path to a file in the file system for which to check the free
346 ///     disk space.
347 ///
348 /// \return The amount of free space usable by a non-root user.
349 ///
350 /// \throw system_error If the call to statfs(2) fails.
351 utils::units::bytes
352 fs::free_disk_space(const fs::path& path)
353 {
354 #if defined(HAVE_STATVFS)
355     struct ::statvfs buf;
356     if (::statvfs(path.c_str(), &buf) == -1) {
357         const int original_errno = errno;
358         throw fs::system_error(F("Failed to stat file system for %s") % path,
359                                original_errno);
360     }
361     return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail);
362 #elif defined(HAVE_STATFS)
363     struct ::statfs buf;
364     if (::statfs(path.c_str(), &buf) == -1) {
365         const int original_errno = errno;
366         throw fs::system_error(F("Failed to stat file system for %s") % path,
367                                original_errno);
368     }
369     return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail);
370 #else
371 #   error "Don't know how to query free disk space"
372 #endif
373 }
374 
375 
376 /// Checks if the given path is a directory or not.
377 ///
378 /// \return True if the path is a directory; false otherwise.
379 bool
380 fs::is_directory(const fs::path& path)
381 {
382     const struct ::stat sb = safe_stat(path);
383     return S_ISDIR(sb.st_mode);
384 }
385 
386 
387 /// Creates a directory.
388 ///
389 /// \param dir The path to the directory to create.
390 /// \param mode The permissions for the new directory.
391 ///
392 /// \throw system_error If the call to mkdir(2) fails.
393 void
394 fs::mkdir(const fs::path& dir, const int mode)
395 {
396     if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) {
397         const int original_errno = errno;
398         throw fs::system_error(F("Failed to create directory %s") % dir,
399                                original_errno);
400     }
401 }
402 
403 
404 /// Creates a directory and any missing parents.
405 ///
406 /// This is separate from the fs::mkdir function to clearly differentiate the
407 /// libc wrapper from the more complex algorithm implemented here.
408 ///
409 /// \param dir The path to the directory to create.
410 /// \param mode The permissions for the new directories.
411 ///
412 /// \throw system_error If any call to mkdir(2) fails.
413 void
414 fs::mkdir_p(const fs::path& dir, const int mode)
415 {
416     try {
417         fs::mkdir(dir, mode);
418     } catch (const fs::system_error& e) {
419         if (e.original_errno() == ENOENT) {
420             fs::mkdir_p(dir.branch_path(), mode);
421             fs::mkdir(dir, mode);
422         } else if (e.original_errno() != EEXIST)
423             throw e;
424     }
425 }
426 
427 
428 /// Creates a temporary directory that is world readable/accessible.
429 ///
430 /// The temporary directory is created using mkdtemp(3) using the provided
431 /// template.  This should be most likely used in conjunction with
432 /// fs::auto_directory.
433 ///
434 /// The temporary directory is given read and execute permissions to everyone
435 /// and thus should not be used to protect data that may be subject to snooping.
436 /// This goes together with the assumption that this function is used to create
437 /// temporary directories for test cases, and that those test cases may
438 /// sometimes be executed as an unprivileged user.  In those cases, we need to
439 /// support two different things:
440 ///
441 /// - Allow the unprivileged code to write to files in the work directory by
442 ///   name (e.g. to write the results file, whose name is provided by the
443 ///   monitor code running as root).  This requires us to grant search
444 ///   permissions.
445 ///
446 /// - Allow the test cases themselves to call getcwd(3) at any point.  At least
447 ///   on NetBSD 7.x, getcwd(3) requires both read and search permissions on all
448 ///   path components leading to the current directory.  This requires us to
449 ///   grant both read and search permissions.
450 ///
451 /// TODO(jmmv): A cleaner way to support this would be for the test executor to
452 /// create two work directory hierarchies directly rooted under TMPDIR: one for
453 /// root and one for the unprivileged user.  However, that requires more
454 /// bookkeeping for no real gain, because we are not really trying to protect
455 /// the data within our temporary directories against attacks.
456 ///
457 /// \param path_template The template for the temporary path, which is a
458 ///     basename that is created within the TMPDIR.  Must contain the XXXXXX
459 ///     pattern, which is atomically replaced by a random unique string.
460 ///
461 /// \return The generated path for the temporary directory.
462 ///
463 /// \throw fs::system_error If the call to mkdtemp(3) fails.
464 fs::path
465 fs::mkdtemp_public(const std::string& path_template)
466 {
467     PRE(path_template.find("XXXXXX") != std::string::npos);
468 
469     const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp"));
470     const fs::path full_template = tmpdir / path_template;
471 
472     utils::auto_array< char > buf(new char[full_template.str().length() + 1]);
473     std::strcpy(buf.get(), full_template.c_str());
474     if (::mkdtemp(buf.get()) == NULL) {
475         const int original_errno = errno;
476         throw fs::system_error(F("Cannot create temporary directory using "
477                                  "template %s") % full_template,
478                                original_errno);
479     }
480     const fs::path path(buf.get());
481 
482     if (::chmod(path.c_str(), 0755) == -1) {
483         const int original_errno = errno;
484 
485         try {
486             rmdir(path);
487         } catch (const fs::system_error& e) {
488             // This really should not fail.  We just created the directory and
489             // have not written anything to it so there is no reason for this to
490             // fail.  But better handle the failure just in case.
491             LW(F("Failed to delete just-created temporary directory %s")
492                % path);
493         }
494 
495         throw fs::system_error(F("Failed to grant search permissions on "
496                                  "temporary directory %s") % path,
497                                original_errno);
498     }
499 
500     return path;
501 }
502 
503 
504 /// Creates a temporary file.
505 ///
506 /// The temporary file is created using mkstemp(3) using the provided template.
507 /// This should be most likely used in conjunction with fs::auto_file.
508 ///
509 /// \param path_template The template for the temporary path, which is a
510 ///     basename that is created within the TMPDIR.  Must contain the XXXXXX
511 ///     pattern, which is atomically replaced by a random unique string.
512 ///
513 /// \return The generated path for the temporary directory.
514 ///
515 /// \throw fs::system_error If the call to mkstemp(3) fails.
516 fs::path
517 fs::mkstemp(const std::string& path_template)
518 {
519     PRE(path_template.find("XXXXXX") != std::string::npos);
520 
521     const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp"));
522     const fs::path full_template = tmpdir / path_template;
523 
524     utils::auto_array< char > buf(new char[full_template.str().length() + 1]);
525     std::strcpy(buf.get(), full_template.c_str());
526     if (::mkstemp(buf.get()) == -1) {
527         const int original_errno = errno;
528         throw fs::system_error(F("Cannot create temporary file using template "
529                                  "%s") % full_template, original_errno);
530     }
531     return fs::path(buf.get());
532 }
533 
534 
535 /// Mounts a temporary file system with unlimited size.
536 ///
537 /// \param in_mount_point The path on which the file system will be mounted.
538 ///
539 /// \throw fs::system_error If the attempt to mount process fails.
540 /// \throw fs::unsupported_operation_error If the code does not know how to
541 ///     mount a temporary file system in the current operating system.
542 void
543 fs::mount_tmpfs(const fs::path& in_mount_point)
544 {
545     mount_tmpfs(in_mount_point, units::bytes());
546 }
547 
548 
549 /// Mounts a temporary file system.
550 ///
551 /// \param in_mount_point The path on which the file system will be mounted.
552 /// \param size The size of the tmpfs to mount.  If 0, use unlimited.
553 ///
554 /// \throw fs::system_error If the attempt to mount process fails.
555 /// \throw fs::unsupported_operation_error If the code does not know how to
556 ///     mount a temporary file system in the current operating system.
557 void
558 fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size)
559 {
560     // SunOS's mount(8) requires paths to be absolute.  To err on the side of
561     // caution, let's make the mount point absolute in all cases.
562     const fs::path mount_point = in_mount_point.is_absolute() ?
563         in_mount_point : in_mount_point.to_absolute();
564 
565     const pid_t pid = ::fork();
566     if (pid == -1) {
567         const int original_errno = errno;
568         throw fs::system_error("Cannot fork to execute mount tool",
569                                original_errno);
570     }
571     if (pid == 0)
572         run_mount_tmpfs(mount_point, size);
573 
574     int status;
575 retry:
576     if (::waitpid(pid, &status, 0) == -1) {
577         const int original_errno = errno;
578         if (errno == EINTR)
579             goto retry;
580         throw fs::system_error("Failed to wait for mount subprocess",
581                                original_errno);
582     }
583 
584     if (WIFEXITED(status)) {
585         if (WEXITSTATUS(status) == exit_known_error)
586             throw fs::unsupported_operation_error(
587                 "Don't know how to mount a tmpfs on this operating system");
588         else if (WEXITSTATUS(status) == EXIT_SUCCESS)
589             return;
590         else
591             throw fs::error(F("Failed to mount tmpfs on %s; returned exit "
592                               "code %s") % mount_point % WEXITSTATUS(status));
593     } else {
594         throw fs::error(F("Failed to mount tmpfs on %s; mount tool "
595                           "received signal") % mount_point);
596     }
597 }
598 
599 
600 /// Recursively removes a directory.
601 ///
602 /// This operation simulates a "rm -r".  No effort is made to forcibly delete
603 /// files and no attention is paid to mount points.
604 ///
605 /// \param directory The directory to remove.
606 ///
607 /// \throw fs::error If there is a problem removing any directory or file.
608 void
609 fs::rm_r(const fs::path& directory)
610 {
611     const fs::directory dir(directory);
612 
613     ::chmod(directory.c_str(), 0700);
614     for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end();
615          ++iter) {
616         if (iter->name == "." || iter->name == "..")
617             continue;
618 
619         const fs::path entry = directory / iter->name;
620 
621         if (fs::is_directory(entry)) {
622             LD(F("Descending into %s") % entry);
623             ::chmod(entry.c_str(), 0700);
624             fs::rm_r(entry);
625         } else {
626             LD(F("Removing file %s") % entry);
627             fs::unlink(entry);
628         }
629     }
630 
631     LD(F("Removing empty directory %s") % directory);
632     fs::rmdir(directory);
633 }
634 
635 
636 /// Removes an empty directory.
637 ///
638 /// \param file The directory to remove.
639 ///
640 /// \throw fs::system_error If the call to rmdir(2) fails.
641 void
642 fs::rmdir(const path& file)
643 {
644     if (::rmdir(file.c_str()) == -1) {
645         const int original_errno = errno;
646         throw fs::system_error(F("Removal of %s failed") % file,
647                                original_errno);
648     }
649 }
650 
651 
652 /// Obtains all the entries in a directory.
653 ///
654 /// \param path The directory to scan.
655 ///
656 /// \return The set of all directory entries in the given directory.
657 ///
658 /// \throw fs::system_error If reading the directory fails for any reason.
659 std::set< fs::directory_entry >
660 fs::scan_directory(const fs::path& path)
661 {
662     std::set< fs::directory_entry > contents;
663 
664     fs::directory dir(path);
665     for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end();
666          ++iter) {
667         contents.insert(*iter);
668     }
669 
670     return contents;
671 }
672 
673 
674 /// Removes a file.
675 ///
676 /// \param file The file to remove.
677 ///
678 /// \throw fs::system_error If the call to unlink(2) fails.
679 void
680 fs::unlink(const path& file)
681 {
682     if (::unlink(file.c_str()) == -1) {
683         const int original_errno = errno;
684         throw fs::system_error(F("Removal of %s failed") % file,
685                                original_errno);
686     }
687 }
688 
689 
690 /// Unmounts a file system.
691 ///
692 /// \param in_mount_point The file system to unmount.
693 ///
694 /// \throw fs::error If the unmount fails.
695 void
696 fs::unmount(const fs::path& in_mount_point)
697 {
698     // FreeBSD's unmount(2) requires paths to be absolute.  To err on the side
699     // of caution, let's make it absolute in all cases.
700     const fs::path mount_point = in_mount_point.is_absolute() ?
701         in_mount_point : in_mount_point.to_absolute();
702 
703     static const int unmount_retries = 3;
704     static const int unmount_retry_delay_seconds = 1;
705 
706     int retries = unmount_retries;
707 retry:
708     try {
709         unmount_impl(mount_point);
710     } catch (const fs::system_error& error) {
711         if (error.original_errno() == EBUSY && retries > 0) {
712             LW(F("%s busy; unmount retries left %s") % mount_point % retries);
713             retries--;
714             ::sleep(unmount_retry_delay_seconds);
715             goto retry;
716         }
717         throw;
718     }
719 }
720