Path: blob/main/contrib/kyua/utils/fs/operations.cpp
48081 views
// Copyright 2010 The Kyua Authors.1// All rights reserved.2//3// Redistribution and use in source and binary forms, with or without4// modification, are permitted provided that the following conditions are5// met:6//7// * Redistributions of source code must retain the above copyright8// notice, this list of conditions and the following disclaimer.9// * Redistributions in binary form must reproduce the above copyright10// notice, this list of conditions and the following disclaimer in the11// documentation and/or other materials provided with the distribution.12// * Neither the name of Google Inc. nor the names of its contributors13// may be used to endorse or promote products derived from this software14// without specific prior written permission.15//16// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS17// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT18// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR19// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT20// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,21// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT22// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,23// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY24// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT25// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE26// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.2728#include "utils/fs/operations.hpp"2930#if defined(HAVE_CONFIG_H)31# include "config.h"32#endif3334extern "C" {35#include <sys/param.h>36#if defined(HAVE_SYS_MOUNT_H)37# include <sys/mount.h>38#endif39#include <sys/stat.h>40#if defined(HAVE_SYS_STATVFS_H) && defined(HAVE_STATVFS)41# include <sys/statvfs.h>42#endif43#if defined(HAVE_SYS_VFS_H)44# include <sys/vfs.h>45#endif46#include <sys/wait.h>4748#include <unistd.h>49}5051#include <cerrno>52#include <cstdlib>53#include <cstring>54#include <fstream>55#include <iostream>56#include <sstream>57#include <string>5859#include "utils/auto_array.ipp"60#include "utils/defs.hpp"61#include "utils/env.hpp"62#include "utils/format/macros.hpp"63#include "utils/fs/directory.hpp"64#include "utils/fs/exceptions.hpp"65#include "utils/fs/path.hpp"66#include "utils/logging/macros.hpp"67#include "utils/optional.ipp"68#include "utils/sanity.hpp"69#include "utils/units.hpp"7071namespace fs = utils::fs;72namespace units = utils::units;7374using utils::optional;757677namespace {787980/// Operating systems recognized by the code below.81enum os_type {82os_unsupported = 0,83os_freebsd,84os_linux,85os_netbsd,86os_sunos,87};888990/// The current operating system.91static enum os_type current_os =92#if defined(__FreeBSD__)93os_freebsd94#elif defined(__linux__)95os_linux96#elif defined(__NetBSD__)97os_netbsd98#elif defined(__SunOS__)99os_sunos100#else101os_unsupported102#endif103;104105106/// Specifies if a real unmount(2) is available.107///108/// We use this as a constant instead of a macro so that we can compile both109/// versions of the unmount code unconditionally. This is a way to prevent110/// compilation bugs going unnoticed for long.111static const bool have_unmount2 =112#if defined(HAVE_UNMOUNT)113true;114#else115false;116#endif117118119#if !defined(UMOUNT)120/// Fake replacement value to the path to umount(8).121# define UMOUNT "do-not-use-this-value"122#else123# if defined(HAVE_UNMOUNT)124# error "umount(8) detected when unmount(2) is also available"125# endif126#endif127128129#if !defined(HAVE_UNMOUNT)130/// Fake unmount(2) function for systems without it.131///132/// This is only provided to allow our code to compile in all platforms133/// regardless of whether they actually have an unmount(2) or not.134///135/// \return -1 to indicate error, although this should never happen.136static int137unmount(const char* /* path */,138const int /* flags */)139{140PRE(false);141return -1;142}143#endif144145146/// Error code returned by subprocess to indicate a controlled failure.147const int exit_known_error = 123;148149150static void run_mount_tmpfs(const fs::path&, const uint64_t) UTILS_NORETURN;151152153/// Executes 'mount -t tmpfs' (or a similar variant).154///155/// This function must be called from a subprocess as it never returns.156///157/// \param mount_point Location on which to mount a tmpfs.158/// \param size The size of the tmpfs to mount. If 0, use unlimited.159static void160run_mount_tmpfs(const fs::path& mount_point, const uint64_t size)161{162const char* mount_args[16];163std::string size_arg;164165std::size_t last = 0;166switch (current_os) {167case os_freebsd:168mount_args[last++] = "mount";169mount_args[last++] = "-ttmpfs";170if (size > 0) {171size_arg = F("-osize=%s") % size;172mount_args[last++] = size_arg.c_str();173}174mount_args[last++] = "tmpfs";175mount_args[last++] = mount_point.c_str();176break;177178case os_linux:179mount_args[last++] = "mount";180mount_args[last++] = "-ttmpfs";181if (size > 0) {182size_arg = F("-osize=%s") % size;183mount_args[last++] = size_arg.c_str();184}185mount_args[last++] = "tmpfs";186mount_args[last++] = mount_point.c_str();187break;188189case os_netbsd:190mount_args[last++] = "mount";191mount_args[last++] = "-ttmpfs";192if (size > 0) {193size_arg = F("-o-s%s") % size;194mount_args[last++] = size_arg.c_str();195}196mount_args[last++] = "tmpfs";197mount_args[last++] = mount_point.c_str();198break;199200case os_sunos:201mount_args[last++] = "mount";202mount_args[last++] = "-Ftmpfs";203if (size > 0) {204size_arg = F("-o-s%s") % size;205mount_args[last++] = size_arg.c_str();206}207mount_args[last++] = "tmpfs";208mount_args[last++] = mount_point.c_str();209break;210211default:212std::cerr << "Don't know how to mount a temporary file system in this "213"host operating system\n";214std::exit(exit_known_error);215}216mount_args[last] = NULL;217218const char** arg;219std::cout << "Mounting tmpfs onto " << mount_point << " with:";220for (arg = &mount_args[0]; *arg != NULL; arg++)221std::cout << " " << *arg;222std::cout << "\n";223224const int ret = ::execvp(mount_args[0],225UTILS_UNCONST(char* const, mount_args));226INV(ret == -1);227std::cerr << "Failed to exec " << mount_args[0] << "\n";228std::exit(EXIT_FAILURE);229}230231232/// Unmounts a file system using unmount(2).233///234/// \pre unmount(2) must be available; i.e. have_unmount2 must be true.235///236/// \param mount_point The file system to unmount.237///238/// \throw fs::system_error If the call to unmount(2) fails.239static void240unmount_with_unmount2(const fs::path& mount_point)241{242PRE(have_unmount2);243244if (::unmount(mount_point.c_str(), 0) == -1) {245const int original_errno = errno;246throw fs::system_error(F("unmount(%s) failed") % mount_point,247original_errno);248}249}250251252/// Unmounts a file system using umount(8).253///254/// \pre umount(2) must not be available; i.e. have_unmount2 must be false.255///256/// \param mount_point The file system to unmount.257///258/// \throw fs::error If the execution of umount(8) fails.259static void260unmount_with_umount8(const fs::path& mount_point)261{262PRE(!have_unmount2);263264const pid_t pid = ::fork();265if (pid == -1) {266const int original_errno = errno;267throw fs::system_error("Cannot fork to execute unmount tool",268original_errno);269} else if (pid == 0) {270const int ret = ::execlp(UMOUNT, "umount", mount_point.c_str(), NULL);271INV(ret == -1);272std::cerr << "Failed to exec " UMOUNT "\n";273std::exit(EXIT_FAILURE);274}275276int status;277retry:278if (::waitpid(pid, &status, 0) == -1) {279const int original_errno = errno;280if (errno == EINTR)281goto retry;282throw fs::system_error("Failed to wait for unmount subprocess",283original_errno);284}285286if (WIFEXITED(status)) {287if (WEXITSTATUS(status) == EXIT_SUCCESS)288return;289else290throw fs::error(F("Failed to unmount %s; returned exit code %s")291% mount_point % WEXITSTATUS(status));292} else293throw fs::error(F("Failed to unmount %s; unmount tool received signal")294% mount_point);295}296297298/// Stats a file, without following links.299///300/// \param path The file to stat.301///302/// \return The stat structure on success.303///304/// \throw system_error An error on failure.305static struct ::stat306safe_stat(const fs::path& path)307{308struct ::stat sb;309if (::lstat(path.c_str(), &sb) == -1) {310const int original_errno = errno;311throw fs::system_error(F("Cannot get information about %s") % path,312original_errno);313}314return sb;315}316317318} // anonymous namespace319320321/// Copies a file.322///323/// \param source The file to copy.324/// \param target The destination of the new copy; must be a file name, not a325/// directory.326///327/// \throw error If there is a problem copying the file.328void329fs::copy(const fs::path& source, const fs::path& target)330{331std::ifstream input(source.c_str());332if (!input)333throw error(F("Cannot open copy source %s") % source);334335std::ofstream output(target.c_str());336if (!output)337throw error(F("Cannot create copy target %s") % target);338339char buffer[1024];340while (input.good()) {341input.read(buffer, sizeof(buffer));342if (input.good() || input.eof())343output.write(buffer, input.gcount());344}345if (!input.good() && !input.eof())346throw error(F("Error while reading input file %s") % source);347}348349350/// Queries the path to the current directory.351///352/// \return The path to the current directory.353///354/// \throw fs::error If there is a problem querying the current directory.355fs::path356fs::current_path(void)357{358char* cwd;359#if defined(HAVE_GETCWD_DYN)360cwd = ::getcwd(NULL, 0);361#else362cwd = ::getcwd(NULL, MAXPATHLEN);363#endif364if (cwd == NULL) {365const int original_errno = errno;366throw fs::system_error(F("Failed to get current working directory"),367original_errno);368}369370try {371const fs::path result(cwd);372std::free(cwd);373return result;374} catch (...) {375std::free(cwd);376throw;377}378}379380381/// Checks if a file exists.382///383/// Be aware that this is racy in the same way as access(2) is.384///385/// \param path The file to check the existance of.386///387/// \return True if the file exists; false otherwise.388bool389fs::exists(const fs::path& path)390{391return ::access(path.c_str(), F_OK) == 0;392}393394395/// Locates a file in the PATH.396///397/// \param name The file to locate.398///399/// \return The path to the located file or none if it was not found. The400/// returned path is always absolute.401optional< fs::path >402fs::find_in_path(const char* name)403{404const optional< std::string > current_path = utils::getenv("PATH");405if (!current_path || current_path.get().empty())406return none;407408std::istringstream path_input(current_path.get() + ":");409std::string path_component;410while (std::getline(path_input, path_component, ':').good()) {411const fs::path candidate = path_component.empty() ?412fs::path(name) : (fs::path(path_component) / name);413if (exists(candidate)) {414if (candidate.is_absolute())415return utils::make_optional(candidate);416else417return utils::make_optional(candidate.to_absolute());418}419}420return none;421}422423424/// Calculates the free space in a given file system.425///426/// \param path Path to a file in the file system for which to check the free427/// disk space.428///429/// \return The amount of free space usable by a non-root user.430///431/// \throw system_error If the call to statfs(2) fails.432utils::units::bytes433fs::free_disk_space(const fs::path& path)434{435#if defined(HAVE_STATVFS)436struct ::statvfs buf;437if (::statvfs(path.c_str(), &buf) == -1) {438const int original_errno = errno;439throw fs::system_error(F("Failed to stat file system for %s") % path,440original_errno);441}442return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail);443#elif defined(HAVE_STATFS)444struct ::statfs buf;445if (::statfs(path.c_str(), &buf) == -1) {446const int original_errno = errno;447throw fs::system_error(F("Failed to stat file system for %s") % path,448original_errno);449}450return units::bytes(uint64_t(buf.f_bsize) * buf.f_bavail);451#else452# error "Don't know how to query free disk space"453#endif454}455456457/// Checks if the given path is a directory or not.458///459/// \return True if the path is a directory; false otherwise.460bool461fs::is_directory(const fs::path& path)462{463const struct ::stat sb = safe_stat(path);464return S_ISDIR(sb.st_mode);465}466467468/// Creates a directory.469///470/// \param dir The path to the directory to create.471/// \param mode The permissions for the new directory.472///473/// \throw system_error If the call to mkdir(2) fails.474void475fs::mkdir(const fs::path& dir, const int mode)476{477if (::mkdir(dir.c_str(), static_cast< mode_t >(mode)) == -1) {478const int original_errno = errno;479throw fs::system_error(F("Failed to create directory %s") % dir,480original_errno);481}482}483484485/// Creates a directory and any missing parents.486///487/// This is separate from the fs::mkdir function to clearly differentiate the488/// libc wrapper from the more complex algorithm implemented here.489///490/// \param dir The path to the directory to create.491/// \param mode The permissions for the new directories.492///493/// \throw system_error If any call to mkdir(2) fails.494void495fs::mkdir_p(const fs::path& dir, const int mode)496{497try {498fs::mkdir(dir, mode);499} catch (const fs::system_error& e) {500if (e.original_errno() == ENOENT) {501fs::mkdir_p(dir.branch_path(), mode);502fs::mkdir(dir, mode);503} else if (e.original_errno() != EEXIST)504throw e;505}506}507508509/// Creates a temporary directory that is world readable/accessible.510///511/// The temporary directory is created using mkdtemp(3) using the provided512/// template. This should be most likely used in conjunction with513/// fs::auto_directory.514///515/// The temporary directory is given read and execute permissions to everyone516/// and thus should not be used to protect data that may be subject to snooping.517/// This goes together with the assumption that this function is used to create518/// temporary directories for test cases, and that those test cases may519/// sometimes be executed as an unprivileged user. In those cases, we need to520/// support two different things:521///522/// - Allow the unprivileged code to write to files in the work directory by523/// name (e.g. to write the results file, whose name is provided by the524/// monitor code running as root). This requires us to grant search525/// permissions.526///527/// - Allow the test cases themselves to call getcwd(3) at any point. At least528/// on NetBSD 7.x, getcwd(3) requires both read and search permissions on all529/// path components leading to the current directory. This requires us to530/// grant both read and search permissions.531///532/// TODO(jmmv): A cleaner way to support this would be for the test executor to533/// create two work directory hierarchies directly rooted under TMPDIR: one for534/// root and one for the unprivileged user. However, that requires more535/// bookkeeping for no real gain, because we are not really trying to protect536/// the data within our temporary directories against attacks.537///538/// \param path_template The template for the temporary path, which is a539/// basename that is created within the TMPDIR. Must contain the XXXXXX540/// pattern, which is atomically replaced by a random unique string.541///542/// \return The generated path for the temporary directory.543///544/// \throw fs::system_error If the call to mkdtemp(3) fails.545fs::path546fs::mkdtemp_public(const std::string& path_template)547{548PRE(path_template.find("XXXXXX") != std::string::npos);549550const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp"));551const fs::path full_template = tmpdir / path_template;552553utils::auto_array< char > buf(new char[full_template.str().length() + 1]);554std::strcpy(buf.get(), full_template.c_str());555if (::mkdtemp(buf.get()) == NULL) {556const int original_errno = errno;557throw fs::system_error(F("Cannot create temporary directory using "558"template %s") % full_template,559original_errno);560}561const fs::path path(buf.get());562563if (::chmod(path.c_str(), 0755) == -1) {564const int original_errno = errno;565566try {567rmdir(path);568} catch (const fs::system_error& e) {569// This really should not fail. We just created the directory and570// have not written anything to it so there is no reason for this to571// fail. But better handle the failure just in case.572LW(F("Failed to delete just-created temporary directory %s")573% path);574}575576throw fs::system_error(F("Failed to grant search permissions on "577"temporary directory %s") % path,578original_errno);579}580581return path;582}583584585/// Creates a temporary file.586///587/// The temporary file is created using mkstemp(3) using the provided template.588/// This should be most likely used in conjunction with fs::auto_file.589///590/// \param path_template The template for the temporary path, which is a591/// basename that is created within the TMPDIR. Must contain the XXXXXX592/// pattern, which is atomically replaced by a random unique string.593///594/// \return The generated path for the temporary directory.595///596/// \throw fs::system_error If the call to mkstemp(3) fails.597fs::path598fs::mkstemp(const std::string& path_template)599{600PRE(path_template.find("XXXXXX") != std::string::npos);601602const fs::path tmpdir(utils::getenv_with_default("TMPDIR", "/tmp"));603const fs::path full_template = tmpdir / path_template;604605utils::auto_array< char > buf(new char[full_template.str().length() + 1]);606std::strcpy(buf.get(), full_template.c_str());607if (::mkstemp(buf.get()) == -1) {608const int original_errno = errno;609throw fs::system_error(F("Cannot create temporary file using template "610"%s") % full_template, original_errno);611}612return fs::path(buf.get());613}614615616/// Mounts a temporary file system with unlimited size.617///618/// \param in_mount_point The path on which the file system will be mounted.619///620/// \throw fs::system_error If the attempt to mount process fails.621/// \throw fs::unsupported_operation_error If the code does not know how to622/// mount a temporary file system in the current operating system.623void624fs::mount_tmpfs(const fs::path& in_mount_point)625{626mount_tmpfs(in_mount_point, units::bytes());627}628629630/// Mounts a temporary file system.631///632/// \param in_mount_point The path on which the file system will be mounted.633/// \param size The size of the tmpfs to mount. If 0, use unlimited.634///635/// \throw fs::system_error If the attempt to mount process fails.636/// \throw fs::unsupported_operation_error If the code does not know how to637/// mount a temporary file system in the current operating system.638void639fs::mount_tmpfs(const fs::path& in_mount_point, const units::bytes& size)640{641// SunOS's mount(8) requires paths to be absolute. To err on the side of642// caution, let's make the mount point absolute in all cases.643const fs::path mount_point = in_mount_point.is_absolute() ?644in_mount_point : in_mount_point.to_absolute();645646const pid_t pid = ::fork();647if (pid == -1) {648const int original_errno = errno;649throw fs::system_error("Cannot fork to execute mount tool",650original_errno);651}652if (pid == 0)653run_mount_tmpfs(mount_point, size);654655int status;656retry:657if (::waitpid(pid, &status, 0) == -1) {658const int original_errno = errno;659if (errno == EINTR)660goto retry;661throw fs::system_error("Failed to wait for mount subprocess",662original_errno);663}664665if (WIFEXITED(status)) {666if (WEXITSTATUS(status) == exit_known_error)667throw fs::unsupported_operation_error(668"Don't know how to mount a tmpfs on this operating system");669else if (WEXITSTATUS(status) == EXIT_SUCCESS)670return;671else672throw fs::error(F("Failed to mount tmpfs on %s; returned exit "673"code %s") % mount_point % WEXITSTATUS(status));674} else {675throw fs::error(F("Failed to mount tmpfs on %s; mount tool "676"received signal") % mount_point);677}678}679680681/// Recursively removes a directory.682///683/// This operation simulates a "rm -r". No effort is made to forcibly delete684/// files and no attention is paid to mount points.685///686/// \param directory The directory to remove.687///688/// \throw fs::error If there is a problem removing any directory or file.689void690fs::rm_r(const fs::path& directory)691{692const fs::directory dir(directory);693694::chmod(directory.c_str(), 0700);695for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end();696++iter) {697if (iter->name == "." || iter->name == "..")698continue;699700const fs::path entry = directory / iter->name;701702if (fs::is_directory(entry)) {703LD(F("Descending into %s") % entry);704::chmod(entry.c_str(), 0700);705fs::rm_r(entry);706} else {707LD(F("Removing file %s") % entry);708fs::unlink(entry);709}710}711712LD(F("Removing empty directory %s") % directory);713fs::rmdir(directory);714}715716717/// Removes an empty directory.718///719/// \param file The directory to remove.720///721/// \throw fs::system_error If the call to rmdir(2) fails.722void723fs::rmdir(const path& file)724{725if (::rmdir(file.c_str()) == -1) {726const int original_errno = errno;727throw fs::system_error(F("Removal of %s failed") % file,728original_errno);729}730}731732733/// Obtains all the entries in a directory.734///735/// \param path The directory to scan.736///737/// \return The set of all directory entries in the given directory.738///739/// \throw fs::system_error If reading the directory fails for any reason.740std::set< fs::directory_entry >741fs::scan_directory(const fs::path& path)742{743std::set< fs::directory_entry > contents;744745fs::directory dir(path);746for (fs::directory::const_iterator iter = dir.begin(); iter != dir.end();747++iter) {748contents.insert(*iter);749}750751return contents;752}753754755/// Removes a file.756///757/// \param file The file to remove.758///759/// \throw fs::system_error If the call to unlink(2) fails.760void761fs::unlink(const path& file)762{763if (::unlink(file.c_str()) == -1) {764const int original_errno = errno;765throw fs::system_error(F("Removal of %s failed") % file,766original_errno);767}768}769770771/// Unmounts a file system.772///773/// \param in_mount_point The file system to unmount.774///775/// \throw fs::error If the unmount fails.776void777fs::unmount(const fs::path& in_mount_point)778{779// FreeBSD's unmount(2) requires paths to be absolute. To err on the side780// of caution, let's make it absolute in all cases.781const fs::path mount_point = in_mount_point.is_absolute() ?782in_mount_point : in_mount_point.to_absolute();783784static const int unmount_retries = 3;785static const int unmount_retry_delay_seconds = 1;786787int retries = unmount_retries;788retry:789try {790if (have_unmount2) {791unmount_with_unmount2(mount_point);792} else {793unmount_with_umount8(mount_point);794}795} catch (const fs::system_error& error) {796if (error.original_errno() == EBUSY && retries > 0) {797LW(F("%s busy; unmount retries left %s") % mount_point % retries);798retries--;799::sleep(unmount_retry_delay_seconds);800goto retry;801}802throw;803}804}805806807