| // Copyright 2018 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 "cros-disks/drivefs_helper.h" |
| |
| #include <utility> |
| |
| #include <base/files/file_enumerator.h> |
| #include <base/files/file_path.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/strings/string_util.h> |
| |
| #include "cros-disks/fuse_mounter.h" |
| #include "cros-disks/mount_options.h" |
| #include "cros-disks/platform.h" |
| #include "cros-disks/quote.h" |
| #include "cros-disks/system_mounter.h" |
| #include "cros-disks/uri.h" |
| |
| namespace cros_disks { |
| namespace { |
| |
| const char kDataDirOptionPrefix[] = "datadir="; |
| const char kIdentityOptionPrefix[] = "identity="; |
| const char kMyFilesOptionPrefix[] = "myfiles="; |
| const char kPathPrefixOptionPrefix[] = "prefix="; |
| |
| const char kOldUser[] = "fuse-drivefs"; |
| const char kHelperTool[] = "/opt/google/drive-file-stream/drivefs"; |
| const char kSeccompPolicyFile[] = |
| "/opt/google/drive-file-stream/drivefs-seccomp.policy"; |
| const char kType[] = "drivefs"; |
| const char kDbusSocketPath[] = "/run/dbus"; |
| |
| // The deepest expected path within a DriveFS datadir is |
| // {content,thumbnails}_cache/d<number>/d<number>/<number>. Allow one extra |
| // level just in case. |
| constexpr int kMaxTraversalDepth = 5; |
| |
| // Ensures that the datadir has the correct owner. If not, recursively chown the |
| // contents, skipping directories with the expected owner. During directory |
| // descent, set group to the eventual group to allow directory traversal to |
| // allow access to the contents. On ascent, set user to the expected user, |
| // marking the directory as having the correct ownership. |
| bool EnsureOwnership(const Platform& platform, |
| const base::FilePath& path, |
| uid_t mounter_uid, |
| gid_t files_gid, |
| uid_t old_mounter_uid, |
| int depth = 0) { |
| if (depth > kMaxTraversalDepth) { |
| LOG(ERROR) << "Reached maximum traversal depth ensuring drivefs datadir " |
| "ownership: " |
| << path.value(); |
| return false; |
| } |
| uid_t current_uid; |
| gid_t current_gid; |
| if (!platform.GetOwnership(path.value(), ¤t_uid, ¤t_gid)) { |
| LOG(ERROR) << "Cannot access datadir " << quote(path); |
| return false; |
| } |
| if (current_uid == mounter_uid && current_gid == files_gid) { |
| return true; |
| } |
| if (current_uid != old_mounter_uid) { |
| LOG(ERROR) << "Unexpected old uid for " << quote(path) << ": Expected " |
| << old_mounter_uid << " but found " << current_uid; |
| return false; |
| } |
| // Set group to |files_gid| to ensure the directory is traversable. Keep the |
| // |current_uid| so this directory isn't treated as having the correct |
| // ownership in case this operation is interrupted. |
| if (!platform.SetOwnership(path.value(), current_uid, files_gid)) { |
| LOG(ERROR) << "Cannot chown " << quote(path) << " to " << current_uid << ":" |
| << files_gid; |
| return false; |
| } |
| base::FileEnumerator dirs(path, false, base::FileEnumerator::DIRECTORIES); |
| for (auto dir_path = dirs.Next(); !dir_path.empty(); dir_path = dirs.Next()) { |
| if (!EnsureOwnership(platform, dir_path, mounter_uid, files_gid, |
| old_mounter_uid, depth + 1)) { |
| return false; |
| } |
| } |
| base::FileEnumerator files(path, false, base::FileEnumerator::FILES); |
| for (auto file_path = files.Next(); !file_path.empty(); |
| file_path = files.Next()) { |
| if (!platform.SetOwnership(file_path.value(), mounter_uid, files_gid)) { |
| LOG(ERROR) << "Cannot chown " << quote(file_path) << " to " << mounter_uid |
| << ":" << files_gid; |
| return false; |
| } |
| } |
| if (!platform.SetOwnership(path.value(), mounter_uid, files_gid)) { |
| LOG(ERROR) << "Cannot chown " << quote(path) << " to " << mounter_uid << ":" |
| << files_gid; |
| return false; |
| } |
| return true; |
| } |
| |
| class DrivefsMounter : public FUSEMounter { |
| public: |
| DrivefsMounter(std::string filesystem_type, |
| MountOptions mount_options, |
| const Platform* platform, |
| brillo::ProcessReaper* process_reaper, |
| std::string mount_program, |
| std::string mount_user, |
| std::string seccomp_policy, |
| BindPaths bind_paths) |
| : FUSEMounter({.bind_paths = std::move(bind_paths), |
| .filesystem_type = std::move(filesystem_type), |
| .mount_options = std::move(mount_options), |
| .mount_program = std::move(mount_program), |
| .mount_user = std::move(mount_user), |
| .network_access = true, |
| .platform = platform, |
| .process_reaper = process_reaper, |
| .seccomp_policy = seccomp_policy}) {} |
| |
| // FUSEMounter overrides: |
| std::unique_ptr<MountPoint> Mount(const std::string& source, |
| const base::FilePath& target_path, |
| std::vector<std::string> options, |
| MountErrorType* error) const override { |
| return FUSEMounter::Mount("", target_path, options, error); |
| } |
| }; |
| |
| } // namespace |
| |
| DrivefsHelper::DrivefsHelper(const Platform* platform, |
| brillo::ProcessReaper* process_reaper) |
| : FUSEHelper(kType, |
| platform, |
| process_reaper, |
| base::FilePath(kHelperTool), |
| kFilesUser) {} |
| |
| DrivefsHelper::~DrivefsHelper() = default; |
| |
| std::unique_ptr<FUSEMounter> DrivefsHelper::CreateMounter( |
| const base::FilePath& working_dir, |
| const Uri& source, |
| const base::FilePath& target_path, |
| const std::vector<std::string>& options) const { |
| const std::string& identity = source.path(); |
| |
| // Enforced by FUSEHelper::CanMount(). |
| DCHECK(!identity.empty()); |
| |
| auto data_dir = GetValidatedDirectory(options, kDataDirOptionPrefix); |
| if (data_dir.empty()) { |
| return nullptr; |
| } |
| |
| uid_t files_uid; |
| gid_t files_gid; |
| if (!platform()->GetUserAndGroupId(kFilesUser, &files_uid, nullptr) || |
| !platform()->GetGroupId(kFilesGroup, &files_gid)) { |
| LOG(ERROR) << "Invalid user configuration."; |
| return nullptr; |
| } |
| |
| auto my_files_path = GetValidatedDirectory(options, kMyFilesOptionPrefix); |
| if (!my_files_path.empty() && !CheckMyFilesPermissions(my_files_path)) { |
| return nullptr; |
| } |
| |
| if (!SetupDirectoryForFUSEAccess(data_dir)) { |
| return nullptr; |
| } |
| MountOptions mount_options; |
| mount_options.EnforceOption(kDataDirOptionPrefix + data_dir.value()); |
| mount_options.EnforceOption(kIdentityOptionPrefix + identity); |
| mount_options.EnforceOption(kPathPrefixOptionPrefix + target_path.value()); |
| if (!my_files_path.empty()) { |
| mount_options.EnforceOption(kMyFilesOptionPrefix + my_files_path.value()); |
| } |
| mount_options.Initialize(options, true, base::NumberToString(files_uid), |
| base::NumberToString(files_gid)); |
| |
| // TODO(crbug.com/859802): Make seccomp mandatory when testing done. |
| std::string seccomp = |
| platform()->PathExists(kSeccompPolicyFile) ? kSeccompPolicyFile : ""; |
| |
| // Bind datadir and DBus communication socket into the sandbox. |
| FUSEMounter::BindPaths paths = {{.path = data_dir.value(), .writable = true}, |
| {.path = kDbusSocketPath, .writable = true}}; |
| if (!my_files_path.empty()) { |
| paths.push_back( |
| {.path = my_files_path.value(), .writable = true, .recursive = true}); |
| } |
| return std::make_unique<DrivefsMounter>( |
| type(), mount_options, platform(), process_reaper(), |
| program_path().value(), user(), seccomp, paths); |
| } |
| |
| base::FilePath DrivefsHelper::GetValidatedDirectory( |
| const std::vector<std::string>& options, |
| const base::StringPiece prefix) const { |
| for (const auto& option : options) { |
| if (base::StartsWith(option, prefix, base::CompareCase::SENSITIVE)) { |
| std::string path_string = option.substr(prefix.size()); |
| base::FilePath data_dir(path_string); |
| if (data_dir.empty() || !data_dir.IsAbsolute() || |
| data_dir.ReferencesParent()) { |
| LOG(ERROR) << "Invalid DriveFS option " << prefix << path_string; |
| return {}; |
| } |
| base::FilePath suffix_component; |
| // If the datadir doesn't exist, canonicalize the parent directory |
| // instead, and append the last path component to that path. |
| if (!platform()->DirectoryExists(data_dir.value())) { |
| suffix_component = data_dir.BaseName(); |
| data_dir = data_dir.DirName(); |
| } |
| if (!platform()->GetRealPath(data_dir.value(), &path_string)) { |
| return {}; |
| } |
| return base::FilePath(path_string).Append(suffix_component); |
| } |
| } |
| return {}; |
| } |
| |
| bool DrivefsHelper::SetupDirectoryForFUSEAccess( |
| const base::FilePath& dir) const { |
| CHECK(dir.IsAbsolute() && !dir.ReferencesParent()) |
| << "Unsafe path " << quote(dir); |
| |
| uid_t mounter_uid, old_mounter_uid; |
| gid_t files_gid; |
| if (!platform()->GetUserAndGroupId(user(), &mounter_uid, nullptr) || |
| !platform()->GetUserAndGroupId(kOldUser, &old_mounter_uid, nullptr) || |
| !platform()->GetGroupId(kFilesGroup, &files_gid)) { |
| LOG(ERROR) << "Invalid user configuration."; |
| return false; |
| } |
| |
| std::string path = dir.value(); |
| if (platform()->DirectoryExists(path)) { |
| return EnsureOwnership(*platform(), dir, mounter_uid, files_gid, |
| old_mounter_uid); |
| } |
| if (!platform()->CreateDirectory(path)) { |
| LOG(ERROR) << "Cannot create datadir " << quote(path); |
| return false; |
| } |
| if (!platform()->SetPermissions(path, 0770)) { |
| LOG(ERROR) << "Cannot chmod datadir " << quote(path); |
| return false; |
| } |
| if (!platform()->SetOwnership(path, mounter_uid, files_gid)) { |
| LOG(ERROR) << "Cannot chown datadir " << quote(path); |
| return false; |
| } |
| return true; |
| } |
| |
| bool DrivefsHelper::CheckMyFilesPermissions(const base::FilePath& dir) const { |
| CHECK(dir.IsAbsolute() && !dir.ReferencesParent()) |
| << "Unsafe 'My Files' path " << quote(dir); |
| |
| uid_t mounter_uid; |
| if (!platform()->GetUserAndGroupId(user(), &mounter_uid, nullptr)) { |
| LOG(ERROR) << "Invalid user configuration."; |
| return false; |
| } |
| |
| std::string path = dir.value(); |
| if (!platform()->DirectoryExists(path)) { |
| LOG(ERROR) << "My files directory " << quote(path) << " does not exist"; |
| return false; |
| } |
| uid_t current_uid; |
| if (!platform()->GetOwnership(path, ¤t_uid, nullptr)) { |
| LOG(WARNING) << "Cannot access my files directory " << quote(path); |
| return false; |
| } |
| if (current_uid != mounter_uid) { |
| LOG(ERROR) << "Incorrect owner for my files directory " << quote(path); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace cros_disks |