// 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 #include #include #include #include #include #include #include #include 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 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, SafeFD::Error> SafeFD::ReadContents( size_t max_size) { std::vector 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 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