// Copyright 2012 Google Inc. // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // * Neither the name of Google Inc. nor the names of its contributors // may be used to endorse or promote products derived from this software // without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "fs.h" #if defined(HAVE_CONFIG_H) # include "config.h" #endif #if defined(HAVE_UNMOUNT) # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "defs.h" #include "error.h" /// Specifies if a real unmount(2) is available. /// /// We use this as a constant instead of a macro so that we can compile both /// versions of the unmount code unconditionally. This is a way to prevent /// compilation bugs going unnoticed for long. static const bool have_unmount2 = #if defined(HAVE_UNMOUNT) true; #else false; #endif #if !defined(UMOUNT) /// Fake replacement value to the path to umount(8). # define UMOUNT "do-not-use-this-value" #else # if defined(HAVE_UNMOUNT) # error "umount(8) detected when unmount(2) is also available" # endif #endif #if !defined(HAVE_UNMOUNT) /// Fake unmount(2) function for systems without it. /// /// This is only provided to allow our code to compile in all platforms /// regardless of whether they actually have an unmount(2) or not. /// /// \param unused_path The mount point to be unmounted. /// \param unused_flags The flags to the unmount(2) call. /// /// \return -1 to indicate error, although this should never happen. static int unmount(const char* KYUA_DEFS_UNUSED_PARAM(path), const int KYUA_DEFS_UNUSED_PARAM(flags)) { assert(false); return -1; } #endif /// Scans a directory and executes a callback on each entry. /// /// \param directory The directory to scan. /// \param callback The function to execute on each entry. /// \param argument A cookie to pass to the callback function. /// /// \return True if the directory scan and the calls to the callback function /// are all successful; false otherwise. /// /// \note Errors are logged to stderr and do not stop the algorithm. static bool try_iterate_directory(const char* directory, bool (*callback)(const char*, const void*), const void* argument) { bool ok = true; DIR* dirp = opendir(directory); if (dirp == NULL) { warn("opendir(%s) failed", directory); ok &= false; } else { struct dirent* dp; while ((dp = readdir(dirp)) != NULL) { const char* name = dp->d_name; if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) continue; char* subdir; const kyua_error_t error = kyua_fs_concat(&subdir, directory, name, NULL); if (kyua_error_is_set(error)) { kyua_error_free(error); warn("path concatenation failed"); ok &= false; } else { ok &= callback(subdir, argument); free(subdir); } } closedir(dirp); } return ok; } /// Stats a file, without following links. /// /// \param path The file to stat. /// \param [out] sb Pointer to the stat structure in which to place the result. /// /// \return The stat structure on success; none on failure. /// /// \note Errors are logged to stderr. static bool try_stat(const char* path, struct stat* sb) { if (lstat(path, sb) == -1) { warn("lstat(%s) failed", path); return false; } else return true; } /// Removes a directory. /// /// \param path The directory to remove. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr. static bool try_rmdir(const char* path) { if (rmdir(path) == -1) { warn("rmdir(%s) failed", path); return false; } else return true; } /// Removes a file. /// /// \param path The file to remove. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr. static bool try_unlink(const char* path) { if (unlink(path) == -1) { warn("unlink(%s) failed", path); return false; } else return true; } /// Unmounts a mount point. /// /// \param path The location to unmount. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr. static bool try_unmount(const char* path) { const kyua_error_t error = kyua_fs_unmount(path); if (kyua_error_is_set(error)) { kyua_error_warn(error, "Cannot unmount %s", path); kyua_error_free(error); return false; } else return true; } /// Attempts to weaken the permissions of a file. /// /// \param path The file to unprotect. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr. static bool try_unprotect(const char* path) { static const mode_t new_mode = 0700; if (chmod(path, new_mode) == -1) { warnx("chmod(%s, %04o) failed", path, new_mode); return false; } else return true; } /// Attempts to weaken the permissions of a symbolic link. /// /// \param path The symbolic link to unprotect. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr. static bool try_unprotect_symlink(const char* path) { static const mode_t new_mode = 0700; #if HAVE_WORKING_LCHMOD if (lchmod(path, new_mode) == -1) { warnx("lchmod(%s, %04o) failed", path, new_mode); return false; } else return true; #else warnx("lchmod(%s, %04o) failed; system call not implemented", path, new_mode); return false; #endif } /// Traverses a hierarchy unmounting any mount points in it. /// /// \param current_path The file or directory to traverse. /// \param raw_parent_sb The stat structure of the enclosing directory. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr and do not stop the algorithm. static bool recursive_unmount(const char* current_path, const void* raw_parent_sb) { const struct stat* parent_sb = raw_parent_sb; struct stat current_sb; bool ok = try_stat(current_path, ¤t_sb); if (ok) { if (S_ISDIR(current_sb.st_mode)) { assert(!S_ISLNK(current_sb.st_mode)); ok &= try_iterate_directory(current_path, recursive_unmount, ¤t_sb); } if (current_sb.st_dev != parent_sb->st_dev) ok &= try_unmount(current_path); } return ok; } /// Traverses a hierarchy and removes all of its contents. /// /// This honors mount points: when a mount point is encountered, it is traversed /// in search for other mount points, but no files within any of these are /// removed. /// /// \param current_path The file or directory to traverse. /// \param raw_parent_sb The stat structure of the enclosing directory. /// /// \return True on success; false otherwise. /// /// \note Errors are logged to stderr and do not stop the algorithm. static bool recursive_cleanup(const char* current_path, const void* raw_parent_sb) { const struct stat* parent_sb = raw_parent_sb; struct stat current_sb; bool ok = try_stat(current_path, ¤t_sb); if (ok) { // Weakening the protections of a file is just a best-effort operation. // If this fails, we may still be able to do the file/directory removal // later on, so ignore any failures from try_unprotect(). // // One particular case in which this fails is if try_unprotect() is run // on a symbolic link that points to a file for which the unprotect is // not possible, and lchmod(3) is not available. if (S_ISLNK(current_sb.st_mode)) try_unprotect_symlink(current_path); else try_unprotect(current_path); if (current_sb.st_dev != parent_sb->st_dev) { ok &= recursive_unmount(current_path, parent_sb); if (ok) ok &= recursive_cleanup(current_path, parent_sb); } else { if (S_ISDIR(current_sb.st_mode)) { assert(!S_ISLNK(current_sb.st_mode)); ok &= try_iterate_directory(current_path, recursive_cleanup, ¤t_sb); ok &= try_rmdir(current_path); } else { ok &= try_unlink(current_path); } } } return ok; } /// Unmounts a file system using unmount(2). /// /// \pre unmount(2) must be available; i.e. have_unmount2 must be true. /// /// \param mount_point The file system to unmount. /// /// \return An error object. static kyua_error_t unmount_with_unmount2(const char* mount_point) { assert(have_unmount2); if (unmount(mount_point, 0) == -1) { return kyua_libc_error_new(errno, "unmount(%s) failed", mount_point); } return kyua_error_ok(); } /// Unmounts a file system using umount(8). /// /// \pre umount(2) must not be available; i.e. have_unmount2 must be false. /// /// \param mount_point The file system to unmount. /// /// \return An error object. static kyua_error_t unmount_with_umount8(const char* mount_point) { assert(!have_unmount2); const pid_t pid = fork(); if (pid == -1) { return kyua_libc_error_new(errno, "fork() failed"); } else if (pid == 0) { const int ret = execlp(UMOUNT, "umount", mount_point, NULL); assert(ret == -1); err(EXIT_FAILURE, "Failed to execute " UMOUNT); } kyua_error_t error = kyua_error_ok(); int status; if (waitpid(pid, &status, 0) == -1) { error = kyua_libc_error_new(errno, "waitpid(%d) failed", pid); } else { if (WIFEXITED(status)) { if (WEXITSTATUS(status) == EXIT_SUCCESS) assert(!kyua_error_is_set(error)); else { error = kyua_libc_error_new(EBUSY, "unmount(%s) failed", mount_point); } } else error = kyua_libc_error_new(EFAULT, "umount(8) crashed"); } return error; } /// Recursively removes a directory. /// /// \param root The directory or file to remove. Cannot be a mount point. /// /// \return An error object. kyua_error_t kyua_fs_cleanup(const char* root) { struct stat current_sb; bool ok = try_stat(root, ¤t_sb); if (ok) ok &= recursive_cleanup(root, ¤t_sb); if (!ok) { warnx("Cleanup of '%s' failed", root); return kyua_libc_error_new(EPERM, "Cleanup of %s failed", root); } else return kyua_error_ok(); } /// Concatenates a set of strings to form a path. /// /// \param [out] output Pointer to a dynamically-allocated string that will hold /// the resulting path, if all goes well. /// \param first First component of the path to concatenate. /// \param ... All other components to concatenate. /// /// \return An error if there is not enough memory to fulfill the request; OK /// otherwise. kyua_error_t kyua_fs_concat(char** const output, const char* first, ...) { va_list ap; const char* component; va_start(ap, first); size_t length = strlen(first) + 1; while ((component = va_arg(ap, const char*)) != NULL) { length += 1 + strlen(component); } va_end(ap); *output = (char*)malloc(length); if (output == NULL) return kyua_oom_error_new(); char* iterator = *output; int added_size; added_size = snprintf(iterator, length, "%s", first); iterator += added_size; length -= added_size; va_start(ap, first); while ((component = va_arg(ap, const char*)) != NULL) { added_size = snprintf(iterator, length, "/%s", component); iterator += added_size; length -= added_size; } va_end(ap); return kyua_error_ok(); } /// Queries the path to the current directory. /// /// \param [out] out_cwd Dynamically-allocated pointer to a string holding the /// current path. The caller must use free() to release it. /// /// \return An error object. kyua_error_t kyua_fs_current_path(char** out_cwd) { char* cwd; #if defined(HAVE_GETCWD_DYN) cwd = getcwd(NULL, 0); #else { const char* static_cwd = ::getcwd(NULL, MAXPATHLEN); const kyua_error_t error = kyua_fs_concat(&cwd, static_cwd, NULL); if (kyua_error_is_set(error)) return error; } #endif if (cwd == NULL) { return kyua_libc_error_new(errno, "getcwd() failed"); } else { *out_cwd = cwd; return kyua_error_ok(); } } /// Converts a path to absolute. /// /// \param original The path to convert; may already be absolute. /// \param [out] output Pointer to a dynamically-allocated string that will hold /// the absolute path, if all goes well. /// /// \return An error if there is not enough memory to fulfill the request; OK /// otherwise. kyua_error_t kyua_fs_make_absolute(const char* original, char** const output) { if (original[0] == '/') { *output = (char*)malloc(strlen(original) + 1); if (output == NULL) return kyua_oom_error_new(); strcpy(*output, original); return kyua_error_ok(); } else { char* current_path; kyua_error_t error; error = kyua_fs_current_path(¤t_path); if (kyua_error_is_set(error)) return error; error = kyua_fs_concat(output, current_path, original, NULL); free(current_path); return error; } } /// Unmounts a file system. /// /// \param mount_point The file system to unmount. /// /// \return An error object. kyua_error_t kyua_fs_unmount(const char* mount_point) { kyua_error_t error; // FreeBSD's unmount(2) requires paths to be absolute. To err on the side // of caution, let's make it absolute in all cases. char* abs_mount_point; error = kyua_fs_make_absolute(mount_point, &abs_mount_point); if (kyua_error_is_set(error)) goto out; static const int unmount_retries = 3; static const int unmount_retry_delay_seconds = 1; int retries = unmount_retries; retry: if (have_unmount2) { error = unmount_with_unmount2(abs_mount_point); } else { error = unmount_with_umount8(abs_mount_point); } if (kyua_error_is_set(error)) { assert(kyua_error_is_type(error, "libc")); if (kyua_libc_error_errno(error) == EBUSY && retries > 0) { kyua_error_warn(error, "%s busy; unmount retries left %d", abs_mount_point, retries); kyua_error_free(error); retries--; sleep(unmount_retry_delay_seconds); goto retry; } } out: free(abs_mount_point); return error; }