fb69480b9a
Change-Id: I7c41b355a503f97501e9ecb768c77a80d1d7ef0c
577 lines
16 KiB
C
577 lines
16 KiB
C
// 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 <sys/param.h>
|
|
# include <sys/mount.h>
|
|
#endif
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include <assert.h>
|
|
#include <dirent.h>
|
|
#include <err.h>
|
|
#include <errno.h>
|
|
#include <stdarg.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#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= NULL;
|
|
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;
|
|
}
|