You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
553 lines
16 KiB
553 lines
16 KiB
// Copyright 2019 The Chromium OS Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
#include "brillo/files/safe_fd.h"
|
|
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#include <base/files/file_util.h>
|
|
#include <base/logging.h>
|
|
#include <base/posix/eintr_wrapper.h>
|
|
#include <brillo/files/file_util.h>
|
|
#include <brillo/files/scoped_dir.h>
|
|
#include <brillo/syslog_logging.h>
|
|
|
|
namespace brillo {
|
|
|
|
namespace {
|
|
|
|
SafeFD::SafeFDResult MakeErrorResult(SafeFD::Error error) {
|
|
return std::make_pair(SafeFD(), error);
|
|
}
|
|
|
|
SafeFD::SafeFDResult MakeSuccessResult(SafeFD&& fd) {
|
|
return std::make_pair(std::move(fd), SafeFD::Error::kNoError);
|
|
}
|
|
|
|
SafeFD::SafeFDResult OpenPathComponentInternal(int parent_fd,
|
|
const std::string& file,
|
|
int flags,
|
|
mode_t mode) {
|
|
if (file != "/" && file.find("/") != std::string::npos) {
|
|
return MakeErrorResult(SafeFD::Error::kBadArgument);
|
|
}
|
|
SafeFD fd;
|
|
|
|
// O_NONBLOCK is used to avoid hanging on edge cases (e.g. a serial port with
|
|
// flow control, or a FIFO without a writer).
|
|
if (parent_fd >= 0 || parent_fd == AT_FDCWD) {
|
|
fd.UnsafeReset(HANDLE_EINTR(openat(parent_fd, file.c_str(),
|
|
flags | O_NONBLOCK | O_NOFOLLOW, mode)));
|
|
} else if (file == "/") {
|
|
fd.UnsafeReset(HANDLE_EINTR(open(
|
|
file.c_str(), flags | O_DIRECTORY | O_NONBLOCK | O_NOFOLLOW, mode)));
|
|
}
|
|
|
|
if (!fd.is_valid()) {
|
|
// open(2) fails with ELOOP when the last component of the |path| is a
|
|
// symlink. It fails with ENXIO when |path| is a FIFO and |flags| is for
|
|
// writing because of the O_NONBLOCK flag added above.
|
|
switch (errno) {
|
|
case ENOENT:
|
|
// Do not write to the log because opening a non-existent file is a
|
|
// frequent occurrence.
|
|
return MakeErrorResult(SafeFD::Error::kDoesNotExist);
|
|
case ELOOP:
|
|
// PLOG prints something along the lines of the symlink depth being too
|
|
// great which is is misleading so LOG is used instead.
|
|
LOG(ERROR) << "Symlink detected! failed to open \"" << file
|
|
<< "\" safely.";
|
|
return MakeErrorResult(SafeFD::Error::kSymlinkDetected);
|
|
case EISDIR:
|
|
PLOG(ERROR) << "Directory detected! failed to open \"" << file
|
|
<< "\" safely";
|
|
return MakeErrorResult(SafeFD::Error::kWrongType);
|
|
case ENOTDIR:
|
|
PLOG(ERROR) << "Not a directory! failed to open \"" << file
|
|
<< "\" safely";
|
|
return MakeErrorResult(SafeFD::Error::kWrongType);
|
|
case ENXIO:
|
|
PLOG(ERROR) << "FIFO detected! failed to open \"" << file
|
|
<< "\" safely";
|
|
return MakeErrorResult(SafeFD::Error::kWrongType);
|
|
default:
|
|
PLOG(ERROR) << "Failed to open \"" << file << '"';
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
}
|
|
|
|
// Remove the O_NONBLOCK flag unless the original |flags| have it.
|
|
if ((flags & O_NONBLOCK) == 0) {
|
|
flags = fcntl(fd.get(), F_GETFL);
|
|
if (flags == -1) {
|
|
PLOG(ERROR) << "Failed to get fd flags for " << file;
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
if (fcntl(fd.get(), F_SETFL, flags & ~O_NONBLOCK)) {
|
|
PLOG(ERROR) << "Failed to set fd flags for " << file;
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
}
|
|
|
|
return MakeSuccessResult(std::move(fd));
|
|
}
|
|
|
|
SafeFD::SafeFDResult OpenSafelyInternal(int parent_fd,
|
|
const base::FilePath& path,
|
|
int flags,
|
|
mode_t mode) {
|
|
std::vector<std::string> components;
|
|
path.GetComponents(&components);
|
|
|
|
auto itr = components.begin();
|
|
if (itr == components.end()) {
|
|
LOG(ERROR) << "A path is required.";
|
|
return MakeErrorResult(SafeFD::Error::kBadArgument);
|
|
}
|
|
|
|
SafeFD::SafeFDResult child_fd;
|
|
int parent_flags = flags | O_NONBLOCK | O_RDONLY | O_DIRECTORY | O_PATH;
|
|
for (; itr + 1 != components.end(); ++itr) {
|
|
child_fd = OpenPathComponentInternal(parent_fd, *itr, parent_flags, 0);
|
|
// Operation failed, so directly return the error result.
|
|
if (!child_fd.first.is_valid()) {
|
|
return child_fd;
|
|
}
|
|
parent_fd = child_fd.first.get();
|
|
}
|
|
|
|
return OpenPathComponentInternal(parent_fd, *itr, flags, mode);
|
|
}
|
|
|
|
SafeFD::Error CheckAttributes(int fd,
|
|
mode_t permissions,
|
|
uid_t uid,
|
|
gid_t gid) {
|
|
struct stat fd_attributes;
|
|
if (fstat(fd, &fd_attributes) != 0) {
|
|
PLOG(ERROR) << "fstat failed";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
if (fd_attributes.st_uid != uid) {
|
|
LOG(ERROR) << "Owner uid is " << fd_attributes.st_uid << " instead of "
|
|
<< uid;
|
|
return SafeFD::Error::kWrongUID;
|
|
}
|
|
|
|
if (fd_attributes.st_gid != gid) {
|
|
LOG(ERROR) << "Owner gid is " << fd_attributes.st_gid << " instead of "
|
|
<< gid;
|
|
return SafeFD::Error::kWrongGID;
|
|
}
|
|
|
|
if ((0777 & (fd_attributes.st_mode ^ permissions)) != 0) {
|
|
mode_t mask = umask(0);
|
|
umask(mask);
|
|
LOG(ERROR) << "Permissions are " << std::oct
|
|
<< (0777 & fd_attributes.st_mode) << " instead of "
|
|
<< (0777 & permissions) << ". Umask is " << std::oct << mask
|
|
<< std::dec;
|
|
return SafeFD::Error::kWrongPermissions;
|
|
}
|
|
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
SafeFD::Error GetFileSize(int fd, size_t* file_size) {
|
|
struct stat fd_attributes;
|
|
if (fstat(fd, &fd_attributes) != 0) {
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
*file_size = fd_attributes.st_size;
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool SafeFD::IsError(SafeFD::Error err) {
|
|
return err != Error::kNoError;
|
|
}
|
|
|
|
const char* SafeFD::RootPath = "/";
|
|
|
|
SafeFD::SafeFDResult SafeFD::Root() {
|
|
SafeFD::SafeFDResult root =
|
|
OpenPathComponentInternal(-1, "/", O_DIRECTORY, 0);
|
|
if (strcmp(SafeFD::RootPath, "/") == 0) {
|
|
return root;
|
|
}
|
|
|
|
if (!root.first.is_valid()) {
|
|
LOG(ERROR) << "Failed to open root directory!";
|
|
return root;
|
|
}
|
|
return root.first.OpenExistingDir(base::FilePath(SafeFD::RootPath));
|
|
}
|
|
|
|
void SafeFD::SetRootPathForTesting(const char* new_root_path) {
|
|
SafeFD::RootPath = new_root_path;
|
|
}
|
|
|
|
int SafeFD::get() const {
|
|
return fd_.get();
|
|
}
|
|
|
|
bool SafeFD::is_valid() const {
|
|
return fd_.is_valid();
|
|
}
|
|
|
|
void SafeFD::reset() {
|
|
return fd_.reset();
|
|
}
|
|
|
|
void SafeFD::UnsafeReset(int fd) {
|
|
return fd_.reset(fd);
|
|
}
|
|
|
|
SafeFD::Error SafeFD::Write(const char* data, size_t size) {
|
|
if (!fd_.is_valid()) {
|
|
return SafeFD::Error::kNotInitialized;
|
|
}
|
|
errno = 0;
|
|
if (!base::WriteFileDescriptor(fd_.get(), data, size)) {
|
|
PLOG(ERROR) << "Failed to write to file";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
if (HANDLE_EINTR(ftruncate(fd_.get(), size)) != 0) {
|
|
PLOG(ERROR) << "Failed to truncate file";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
std::pair<std::vector<char>, SafeFD::Error> SafeFD::ReadContents(
|
|
size_t max_size) {
|
|
std::vector<char> buffer;
|
|
if (!fd_.is_valid()) {
|
|
return std::make_pair(std::move(buffer), SafeFD::Error::kNotInitialized);
|
|
}
|
|
|
|
size_t file_size = 0;
|
|
SafeFD::Error err = GetFileSize(fd_.get(), &file_size);
|
|
if (IsError(err)) {
|
|
return std::make_pair(std::move(buffer), err);
|
|
}
|
|
|
|
if (file_size > max_size) {
|
|
return std::make_pair(std::move(buffer), SafeFD::Error::kExceededMaximum);
|
|
}
|
|
|
|
buffer.resize(file_size);
|
|
|
|
err = Read(buffer.data(), buffer.size());
|
|
if (IsError(err)) {
|
|
buffer.clear();
|
|
}
|
|
return std::make_pair(std::move(buffer), err);
|
|
}
|
|
|
|
SafeFD::Error SafeFD::Read(char* data, size_t size) {
|
|
if (!fd_.is_valid()) {
|
|
return SafeFD::Error::kNotInitialized;
|
|
}
|
|
|
|
if (!base::ReadFromFD(fd_.get(), data, size)) {
|
|
PLOG(ERROR) << "Failed to read file";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
SafeFD::SafeFDResult SafeFD::OpenExistingFile(const base::FilePath& path,
|
|
int flags) {
|
|
if (!fd_.is_valid()) {
|
|
return MakeErrorResult(SafeFD::Error::kNotInitialized);
|
|
}
|
|
|
|
return OpenSafelyInternal(get(), path, flags, 0 /*mode*/);
|
|
}
|
|
|
|
SafeFD::SafeFDResult SafeFD::OpenExistingDir(const base::FilePath& path,
|
|
int flags) {
|
|
if (!fd_.is_valid()) {
|
|
return MakeErrorResult(SafeFD::Error::kNotInitialized);
|
|
}
|
|
|
|
return OpenSafelyInternal(get(), path, O_DIRECTORY | flags /*flags*/,
|
|
0 /*mode*/);
|
|
}
|
|
|
|
SafeFD::SafeFDResult SafeFD::MakeFile(const base::FilePath& path,
|
|
mode_t permissions,
|
|
uid_t uid,
|
|
gid_t gid,
|
|
int flags) {
|
|
if (!fd_.is_valid()) {
|
|
return MakeErrorResult(SafeFD::Error::kNotInitialized);
|
|
}
|
|
|
|
// Open (and create if necessary) the parent directory.
|
|
base::FilePath dir_name = path.DirName();
|
|
SafeFD::SafeFDResult parent_dir;
|
|
int parent_dir_fd = get();
|
|
if (!dir_name.empty() &&
|
|
dir_name.value() != base::FilePath::kCurrentDirectory) {
|
|
// Apply execute permission where read permission are present for parent
|
|
// directories.
|
|
int dir_permissions = permissions | ((permissions & 0444) >> 2);
|
|
parent_dir =
|
|
MakeDir(dir_name, dir_permissions, uid, gid, O_RDONLY | O_CLOEXEC);
|
|
if (!parent_dir.first.is_valid()) {
|
|
return parent_dir;
|
|
}
|
|
parent_dir_fd = parent_dir.first.get();
|
|
}
|
|
|
|
// If file already exists, validate permissions.
|
|
SafeFDResult file = OpenPathComponentInternal(
|
|
parent_dir_fd, path.BaseName().value(), flags, permissions /*mode*/);
|
|
if (file.first.is_valid()) {
|
|
SafeFD::Error err =
|
|
CheckAttributes(file.first.get(), permissions, uid, gid);
|
|
if (IsError(err)) {
|
|
return MakeErrorResult(err);
|
|
}
|
|
return file;
|
|
} else if (errno != ENOENT) {
|
|
return file;
|
|
}
|
|
|
|
// The file does exist, create it and set the ownership.
|
|
file =
|
|
OpenPathComponentInternal(parent_dir_fd, path.BaseName().value(),
|
|
O_CREAT | O_EXCL | flags, permissions /*mode*/);
|
|
if (!file.first.is_valid()) {
|
|
return file;
|
|
}
|
|
if (HANDLE_EINTR(fchown(file.first.get(), uid, gid)) != 0) {
|
|
PLOG(ERROR) << "Failed to set ownership in MakeFile() for \""
|
|
<< path.value() << '"';
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
SafeFD::SafeFDResult SafeFD::MakeDir(const base::FilePath& path,
|
|
mode_t permissions,
|
|
uid_t uid,
|
|
gid_t gid,
|
|
int flags) {
|
|
if (!fd_.is_valid()) {
|
|
return MakeErrorResult(SafeFD::Error::kNotInitialized);
|
|
}
|
|
|
|
std::vector<std::string> components;
|
|
path.GetComponents(&components);
|
|
if (components.empty()) {
|
|
LOG(ERROR) << "Called MakeDir() with an empty path";
|
|
return MakeErrorResult(SafeFD::Error::kBadArgument);
|
|
}
|
|
|
|
// Walk the path creating directories as necessary.
|
|
SafeFD dir;
|
|
SafeFDResult child_dir;
|
|
int parent_dir_fd = get();
|
|
int dir_flags = O_NONBLOCK | O_DIRECTORY | O_PATH;
|
|
bool made_dir = false;
|
|
for (const auto& component : components) {
|
|
if (mkdirat(parent_dir_fd, component.c_str(), permissions) != 0) {
|
|
if (errno != EEXIST) {
|
|
PLOG(ERROR) << "Failed to mkdirat() " << component << ": full_path=\""
|
|
<< path.value() << '"';
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
} else {
|
|
made_dir = true;
|
|
}
|
|
|
|
// For the last component in the path, use the flags provided by the caller.
|
|
if (&component == &components.back()) {
|
|
dir_flags = flags | O_DIRECTORY;
|
|
}
|
|
child_dir = OpenPathComponentInternal(parent_dir_fd, component, dir_flags,
|
|
0 /*mode*/);
|
|
if (!child_dir.first.is_valid()) {
|
|
return child_dir;
|
|
}
|
|
|
|
dir = std::move(child_dir.first);
|
|
parent_dir_fd = dir.get();
|
|
}
|
|
|
|
if (made_dir) {
|
|
// If the directory was created, set the ownership.
|
|
if (HANDLE_EINTR(fchown(dir.get(), uid, gid)) != 0) {
|
|
PLOG(ERROR) << "Failed to set ownership in MakeDir() for \""
|
|
<< path.value() << '"';
|
|
return MakeErrorResult(SafeFD::Error::kIOError);
|
|
}
|
|
}
|
|
// If the directory already existed, validate the permissions.
|
|
SafeFD::Error err = CheckAttributes(dir.get(), permissions, uid, gid);
|
|
if (IsError(err)) {
|
|
return MakeErrorResult(err);
|
|
}
|
|
|
|
return MakeSuccessResult(std::move(dir));
|
|
}
|
|
|
|
SafeFD::Error SafeFD::Link(const SafeFD& source_dir,
|
|
const std::string& source_name,
|
|
const std::string& destination_name) {
|
|
if (!fd_.is_valid() || !source_dir.is_valid()) {
|
|
return SafeFD::Error::kNotInitialized;
|
|
}
|
|
|
|
SafeFD::Error err = IsValidFilename(source_name);
|
|
if (IsError(err)) {
|
|
return err;
|
|
}
|
|
|
|
err = IsValidFilename(destination_name);
|
|
if (IsError(err)) {
|
|
return err;
|
|
}
|
|
|
|
if (HANDLE_EINTR(linkat(source_dir.get(), source_name.c_str(), fd_.get(),
|
|
destination_name.c_str(), 0)) != 0) {
|
|
PLOG(ERROR) << "Failed to link \"" << destination_name << "\"";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
SafeFD::Error SafeFD::Unlink(const std::string& name) {
|
|
if (!fd_.is_valid()) {
|
|
return SafeFD::Error::kNotInitialized;
|
|
}
|
|
|
|
SafeFD::Error err = IsValidFilename(name);
|
|
if (IsError(err)) {
|
|
return err;
|
|
}
|
|
|
|
if (HANDLE_EINTR(unlinkat(fd_.get(), name.c_str(), 0 /*flags*/)) != 0) {
|
|
PLOG(ERROR) << "Failed to unlink \"" << name << "\"";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
SafeFD::Error SafeFD::Rmdir(const std::string& name,
|
|
bool recursive,
|
|
size_t max_depth,
|
|
bool keep_going) {
|
|
if (!fd_.is_valid()) {
|
|
return SafeFD::Error::kNotInitialized;
|
|
}
|
|
|
|
if (max_depth == 0) {
|
|
return SafeFD::Error::kExceededMaximum;
|
|
}
|
|
|
|
SafeFD::Error err = IsValidFilename(name);
|
|
if (IsError(err)) {
|
|
return err;
|
|
}
|
|
|
|
SafeFD::Error last_err = SafeFD::Error::kNoError;
|
|
|
|
if (recursive) {
|
|
SafeFD dir_fd;
|
|
std::tie(dir_fd, err) =
|
|
OpenPathComponentInternal(fd_.get(), name, O_DIRECTORY, 0);
|
|
if (!dir_fd.is_valid()) {
|
|
return err;
|
|
}
|
|
|
|
// The ScopedDIR takes ownership of this so dup_fd is not scoped on its own.
|
|
int dup_fd = dup(dir_fd.get());
|
|
if (dup_fd < 0) {
|
|
PLOG(ERROR) << "dup failed";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
ScopedDIR dir(fdopendir(dup_fd));
|
|
if (!dir.is_valid()) {
|
|
PLOG(ERROR) << "fdopendir failed";
|
|
close(dup_fd);
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
struct stat dir_info;
|
|
if (fstat(dir_fd.get(), &dir_info) != 0) {
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
errno = 0;
|
|
const dirent* entry = HANDLE_EINTR_IF_EQ(readdir(dir.get()), nullptr);
|
|
while (entry != nullptr) {
|
|
SafeFD::Error err = [&]() {
|
|
if (strcmp(entry->d_name, ".") == 0 ||
|
|
strcmp(entry->d_name, "..") == 0) {
|
|
return SafeFD::Error::kNoError;
|
|
}
|
|
|
|
struct stat child_info;
|
|
if (fstatat(dir_fd.get(), entry->d_name, &child_info,
|
|
AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW) != 0) {
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
if (child_info.st_dev != dir_info.st_dev) {
|
|
return SafeFD::Error::kBoundaryDetected;
|
|
}
|
|
|
|
if (entry->d_type != DT_DIR) {
|
|
return dir_fd.Unlink(entry->d_name);
|
|
}
|
|
|
|
return dir_fd.Rmdir(entry->d_name, true, max_depth - 1, keep_going);
|
|
}();
|
|
|
|
if (IsError(err)) {
|
|
if (!keep_going) {
|
|
return err;
|
|
}
|
|
last_err = err;
|
|
}
|
|
|
|
errno = 0;
|
|
entry = HANDLE_EINTR_IF_EQ(readdir(dir.get()), nullptr);
|
|
}
|
|
if (errno != 0) {
|
|
PLOG(ERROR) << "readdir failed";
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
}
|
|
|
|
if (HANDLE_EINTR(unlinkat(fd_.get(), name.c_str(), AT_REMOVEDIR)) != 0) {
|
|
PLOG(ERROR) << "unlinkat failed";
|
|
if (errno == ENOTDIR) {
|
|
return SafeFD::Error::kWrongType;
|
|
}
|
|
// If there was an error during the recursive delete, we expect unlink
|
|
// to fail with ENOTEMPTY and we bubble the error from recursion
|
|
// instead.
|
|
if (IsError(last_err) && errno == ENOTEMPTY) {
|
|
return last_err;
|
|
}
|
|
return SafeFD::Error::kIOError;
|
|
}
|
|
|
|
return last_err;
|
|
}
|
|
|
|
} // namespace brillo
|