| // 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 |