| // Copyright (c) 2012 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/archive_manager.h" |
| |
| #include <sys/mount.h> |
| |
| #include <memory> |
| |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/logging.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| #include <brillo/cryptohome.h> |
| |
| #include "cros-disks/error_logger.h" |
| #include "cros-disks/fuse_helper.h" |
| #include "cros-disks/fuse_mounter.h" |
| #include "cros-disks/metrics.h" |
| #include "cros-disks/mount_info.h" |
| #include "cros-disks/mount_options.h" |
| #include "cros-disks/platform.h" |
| #include "cros-disks/quote.h" |
| #include "cros-disks/system_mounter.h" |
| |
| // TODO(benchan): Remove entire archive manager after deprecating the rar |
| // support (see chromium:707327). |
| |
| namespace cros_disks { |
| namespace { |
| |
| // Mapping from a base path to its corresponding path inside the AVFS mount. |
| struct AVFSPathMapping { |
| const char* const base_path; |
| const char* const avfs_path; |
| }; |
| |
| const char kAVFSMountGroup[] = "chronos-access"; |
| const char kAVFSMountUser[] = "avfs"; |
| // TODO(wad,benchan): Revisit the location of policy files once more system |
| // daemons are sandboxed with seccomp filters. |
| const char kAVFSSeccompFilterPolicyFile[] = |
| "/usr/share/policy/avfsd-seccomp.policy"; |
| const char kAVFSMountProgram[] = "/usr/bin/avfsd"; |
| const char kAVFSRootDirectory[] = "/run/avfsroot"; |
| const mode_t kAVFSDirectoryPermissions = 0770; // rwx by avfs user and group |
| const char kAVFSLogFile[] = "/run/avfsroot/avfs.log"; |
| const char kAVFSMediaDirectory[] = "/run/avfsroot/media"; |
| const char kAVFSUsersDirectory[] = "/run/avfsroot/users"; |
| const char kMediaDirectory[] = "/media"; |
| const char kUserRootDirectory[] = "/home/chronos"; |
| const AVFSPathMapping kAVFSPathMapping[] = { |
| {kMediaDirectory, kAVFSMediaDirectory}, |
| {kUserRootDirectory, kAVFSUsersDirectory}, |
| }; |
| const char kAVFSModulesOption[] = "modules=subdir"; |
| const char kAVFSSubdirOptionPrefix[] = "subdir="; |
| |
| } // namespace |
| |
| ArchiveManager::ArchiveManager(const std::string& mount_root, |
| Platform* platform, |
| Metrics* metrics, |
| brillo::ProcessReaper* process_reaper) |
| : MountManager(mount_root, platform, metrics, process_reaper), |
| avfs_started_(false) {} |
| |
| ArchiveManager::~ArchiveManager() { |
| // StopAVFS() unmounts all mounted archives as well as AVFS mount points. |
| StopAVFS(); |
| } |
| |
| bool ArchiveManager::Initialize() { |
| RegisterDefaultFileExtensions(); |
| return MountManager::Initialize(); |
| } |
| |
| bool ArchiveManager::StopSession() { |
| return StopAVFS(); |
| } |
| |
| bool ArchiveManager::CanMount(const std::string& source_path) const { |
| // The following paths can be mounted: |
| // /home/chronos/u-<user-id>/Downloads/...<file> |
| // /home/chronos/u-<user-id>/MyFiles/...<file> |
| // /home/chronos/u-<user-id>/GCache/...<file> |
| // /media/<dir>/<dir>/...<file> |
| // |
| base::FilePath file_path(source_path); |
| if (base::FilePath(kUserRootDirectory).IsParent(file_path)) { |
| std::vector<std::string> components; |
| file_path.StripTrailingSeparators().GetComponents(&components); |
| // The file path of an archive file under a user's Downloads or GCache |
| // directory path is split into the following components: |
| // '/', 'home', 'chronos', 'u-<userid>', 'Downloads', ..., 'doc.zip' |
| // '/', 'home', 'chronos', 'u-<userid>', 'GCache', ..., 'doc.zip' |
| if (components.size() > 5 && |
| (base::StartsWith(components[3], "u-", |
| base::CompareCase::INSENSITIVE_ASCII) && |
| brillo::cryptohome::home::IsSanitizedUserName( |
| components[3].substr(2))) && |
| (components[4] == "Downloads" || components[4] == "GCache" || |
| components[4] == "MyFiles")) { |
| return true; |
| } |
| } |
| |
| if (base::FilePath(kMediaDirectory).IsParent(file_path)) { |
| std::vector<std::string> components; |
| file_path.StripTrailingSeparators().GetComponents(&components); |
| // A mount directory is always created under /media/<sub type>/<mount dir>, |
| // so the file path of an archive file under a mount directory is split |
| // into more than 4 components: |
| // '/', 'media', 'removable', 'usb', ..., 'doc.zip' |
| if (components.size() > 4) |
| return true; |
| } |
| return false; |
| } |
| |
| MountErrorType ArchiveManager::DoMount(const std::string& source_path, |
| const std::string& source_format, |
| const std::vector<std::string>& options, |
| const std::string& mount_path, |
| MountOptions* applied_options) { |
| CHECK(!source_path.empty()) << "Invalid source path argument"; |
| CHECK(!mount_path.empty()) << "Invalid mount path argument"; |
| |
| std::string extension = GetFileExtension(source_format); |
| if (extension.empty()) |
| extension = GetFileExtension(source_path); |
| |
| metrics()->RecordArchiveType(extension); |
| |
| std::string avfs_path = GetAVFSPath(source_path, extension); |
| if (avfs_path.empty()) { |
| LOG(ERROR) << "Path " << quote(source_path) |
| << " is not a supported archive"; |
| return MOUNT_ERROR_UNSUPPORTED_ARCHIVE; |
| } |
| |
| MountErrorType avfs_start_error = StartAVFS(); |
| if (avfs_start_error != MOUNT_ERROR_NONE) { |
| LOG(ERROR) << "Failed to start AVFS mounts: " << avfs_start_error; |
| return avfs_start_error; |
| } |
| |
| // Perform a bind mount from the archive path under the AVFS mount |
| // to /media/archive/<archive name>. |
| std::vector<std::string> extended_options = options; |
| extended_options.push_back(MountOptions::kOptionBind); |
| MountOptions mount_options; |
| mount_options.WhitelistOption(MountOptions::kOptionNoSymFollow); |
| mount_options.Initialize(extended_options, false, "", ""); |
| MounterCompat mounter(std::make_unique<SystemMounter>("", platform()), |
| avfs_path, base::FilePath(mount_path), mount_options); |
| |
| MountErrorType error_type = mounter.Mount(); |
| if (error_type != MOUNT_ERROR_NONE) { |
| return error_type; |
| } |
| |
| AddMountVirtualPath(mount_path, avfs_path); |
| return MOUNT_ERROR_NONE; |
| } |
| |
| MountErrorType ArchiveManager::DoUnmount(const std::string& path) { |
| CHECK(!path.empty()) << "Invalid path argument"; |
| |
| // Since all archives are read-only, always use lazy unmount. |
| const MountErrorType error = platform()->Unmount(path, MNT_DETACH); |
| if (error != MOUNT_ERROR_NONE) { |
| return error; |
| } |
| |
| // DoUnmount() is always called with |path| being the mount path. |
| RemoveMountVirtualPath(path); |
| return MOUNT_ERROR_NONE; |
| } |
| |
| std::string ArchiveManager::SuggestMountPath( |
| const std::string& source_path) const { |
| // Use the archive name to name the mount directory. |
| base::FilePath base_name = base::FilePath(source_path).BaseName(); |
| return base::FilePath(mount_root()).Append(base_name).value(); |
| } |
| |
| void ArchiveManager::RegisterDefaultFileExtensions() { |
| // Different archive formats can now be supported via an extension (built-in |
| // or installed by user) using the chrome.fileSystemProvider API. Thus, zip, |
| // tar, and gzip/bzip2 compressed tar formats are no longer supported here. |
| |
| // rar is still supported until there is a replacement using a built-in |
| // extension. |
| RegisterFileExtension("rar", "#urar"); |
| } |
| |
| void ArchiveManager::RegisterFileExtension(const std::string& extension, |
| const std::string& avfs_handler) { |
| extension_handlers_[extension] = avfs_handler; |
| } |
| |
| std::string ArchiveManager::GetFileExtension(const std::string& path) const { |
| base::FilePath file_path(path); |
| std::string extension = file_path.Extension(); |
| if (!extension.empty()) { |
| // Strip the leading dot and convert the extension to lower case. |
| extension.erase(0, 1); |
| extension = base::ToLowerASCII(extension); |
| } |
| return extension; |
| } |
| |
| std::string ArchiveManager::GetAVFSPath(const std::string& path, |
| const std::string& extension) const { |
| // When mounting an archive within another mounted archive, we need to |
| // resolve the virtual path of the inner archive to the "unfolded" |
| // form within the AVFS mount, such as |
| // "/run/avfsroot/media/layer2.zip#/test/doc/layer1.zip#" |
| // instead of the "nested" form, such as |
| // "/run/avfsroot/media/archive/layer2.zip/test/doc/layer1.zip#" |
| // where "/media/archive/layer2.zip" is a mount point to the virtual |
| // path "/run/avfsroot/media/layer2.zip#". |
| // |
| // Mounting the inner archive using the nested form may cause problems |
| // reading files from the inner archive. To avoid that, we first try to |
| // find the longest parent path of |path| that is an existing mount |
| // point to a virtual path within the AVFS mount. If such a parent path |
| // is found, we construct the virtual path of |path| within the AVFS |
| // mount as a subpath of its parent's virtual path. |
| // |
| // e.g. Given |path| is "/media/archive/layer2.zip/test/doc/layer1.zip", |
| // and "/media/archive/layer2.zip" is a mount point to the virtual |
| // path "/run/avfsroot/media/layer2.zip#" within the AVFS mount. |
| // The following code should return the virtual path of |path| as |
| // "/run/avfsroot/media/layer2.zip#/test/doc/layer1.zip#". |
| std::map<std::string, std::string>::const_iterator handler_iterator = |
| extension_handlers_.find(extension); |
| if (handler_iterator == extension_handlers_.end()) |
| return std::string(); |
| |
| base::FilePath file_path(path); |
| base::FilePath current_path = file_path.DirName(); |
| base::FilePath parent_path = current_path.DirName(); |
| while (current_path != parent_path) { // Search till the root |
| VirtualPathMap::const_iterator path_iterator = |
| virtual_paths_.find(current_path.value()); |
| if (path_iterator != virtual_paths_.end()) { |
| base::FilePath avfs_path(path_iterator->second); |
| // As current_path is a parent of file_path, AppendRelativePath() |
| // should return true here. |
| CHECK(current_path.AppendRelativePath(file_path, &avfs_path)); |
| return avfs_path.value() + handler_iterator->second; |
| } |
| current_path = parent_path; |
| parent_path = parent_path.DirName(); |
| } |
| |
| // If no parent path is a mounted via AVFS, we are not mounting a nested |
| // archive and thus construct the virtual path of the archive based on a |
| // corresponding AVFS mount path. |
| for (const auto& mapping : kAVFSPathMapping) { |
| base::FilePath base_path(mapping.base_path); |
| base::FilePath avfs_path(mapping.avfs_path); |
| if (base_path.AppendRelativePath(file_path, &avfs_path)) { |
| return avfs_path.value() + handler_iterator->second; |
| } |
| } |
| return std::string(); |
| } |
| |
| MountErrorType ArchiveManager::StartAVFS() { |
| if (avfs_started_) |
| return MOUNT_ERROR_NONE; |
| |
| // As cros-disks is now a non-privileged process, the directory tree under |
| // |kAVFSRootDirectory| is created by the pre-start script of the cros-disks |
| // upstart job. We simply check to make sure the directory tree is created |
| // with the expected file ownership and permissions. |
| uid_t avfs_user_id, dir_user_id; |
| gid_t avfs_group_id, dir_group_id; |
| mode_t dir_mode; |
| if (!platform()->PathExists(kAVFSRootDirectory) || |
| !platform()->GetUserAndGroupId(kAVFSMountUser, &avfs_user_id, |
| &avfs_group_id) || |
| !platform()->GetOwnership(kAVFSRootDirectory, &dir_user_id, |
| &dir_group_id) || |
| !platform()->GetPermissions(kAVFSRootDirectory, &dir_mode) || |
| (dir_user_id != avfs_user_id) || (dir_group_id != avfs_group_id) || |
| ((dir_mode & 07777) != kAVFSDirectoryPermissions)) { |
| LOG(ERROR) << kAVFSRootDirectory << " isn't created properly"; |
| return MOUNT_ERROR_INTERNAL; |
| } |
| |
| // Set the AVFS_LOGFILE environment variable so that the AVFS daemon |
| // writes log messages to a file instead of syslog. Otherwise, writing |
| // to syslog may trigger the socket/connect/send system calls, which are |
| // disabled by the seccomp filters policy file. This only affects the |
| // child processes spawned by cros-disks and does not persist after |
| // cros-disks restarts. |
| setenv("AVFS_LOGFILE", kAVFSLogFile, 1); |
| |
| avfs_started_ = true; |
| for (const auto& mapping : kAVFSPathMapping) { |
| MountErrorType mount_error = |
| MountAVFSPath(mapping.base_path, mapping.avfs_path); |
| if (mount_error != MOUNT_ERROR_NONE) { |
| LOG(ERROR) << "Cannot mount AVFS path " << quote(mapping.avfs_path) |
| << ": " << mount_error; |
| StopAVFS(); |
| return mount_error; |
| } |
| } |
| return MOUNT_ERROR_NONE; |
| } |
| |
| bool ArchiveManager::StopAVFS() { |
| if (!avfs_started_) |
| return true; |
| |
| avfs_started_ = false; |
| // Unmounts all mounted archives before unmounting AVFS mounts. |
| bool all_unmounted = UnmountAll(); |
| for (const auto& mapping : kAVFSPathMapping) { |
| const std::string& path = mapping.avfs_path; |
| if (!platform()->PathExists(path)) |
| continue; |
| |
| const MountErrorType error = platform()->Unmount(path, 0); |
| if (error != MOUNT_ERROR_NONE) |
| all_unmounted = false; |
| } |
| |
| return all_unmounted; |
| } |
| |
| bool ArchiveManager::CreateMountDirectory(const std::string& path) const { |
| // If an empty directory was left behind for any reason, remove it first. |
| if (platform()->DirectoryExists(path) && |
| !platform()->RemoveEmptyDirectory(path)) { |
| return false; |
| } |
| |
| // Create directory. This works because /run/avfsroot is owned by avfs:avfs, |
| // and cros-disks is in the avfs group. |
| if (!platform()->CreateDirectory(path)) { |
| return false; |
| } |
| |
| uid_t uid; |
| gid_t gid; |
| |
| // Set directory's permissions and owner. |
| if (!platform()->SetPermissions(path, kAVFSDirectoryPermissions) || |
| !platform()->GetUserAndGroupId(kAVFSMountUser, &uid, &gid) || |
| !platform()->SetOwnership(path, uid, gid)) { |
| // Remove directory in case of error. |
| platform()->RemoveEmptyDirectory(path); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| MountErrorType ArchiveManager::MountAVFSPath( |
| const std::string& base_path, const std::string& avfs_path) const { |
| MountInfo mount_info; |
| if (!mount_info.RetrieveFromCurrentProcess()) |
| return MOUNT_ERROR_INTERNAL; |
| |
| if (mount_info.HasMountPath(avfs_path)) { |
| LOG(WARNING) << "Path " << quote(avfs_path) << " is already mounted"; |
| // Not using MOUNT_ERROR_PATH_ALREADY_MOUNTED here because that implies an |
| // error on the user-requested mount. The error here is for the avfsd |
| // daemon. |
| return MOUNT_ERROR_INTERNAL; |
| } |
| |
| // Create avfs_path with the right uid, gid and permissions. |
| if (!CreateMountDirectory(avfs_path)) { |
| LOG(ERROR) << "Cannot create mount directory " << quote(avfs_path); |
| return MOUNT_ERROR_INTERNAL; |
| } |
| |
| MountOptions mount_options; |
| mount_options.WhitelistOption(FUSEHelper::kOptionAllowOther); |
| mount_options.WhitelistOption(kAVFSModulesOption); |
| mount_options.WhitelistOptionPrefix(kAVFSSubdirOptionPrefix); |
| std::vector<std::string> options = { |
| MountOptions::kOptionReadOnly, |
| kAVFSModulesOption, |
| kAVFSSubdirOptionPrefix + base_path, |
| }; |
| mount_options.Initialize(options, false, "", ""); |
| |
| std::unique_ptr<FUSEMounter> fuse_mounter = std::make_unique<FUSEMounter>( |
| "", avfs_path, "avfs", mount_options, platform(), process_reaper(), |
| kAVFSMountProgram, kAVFSMountUser, kAVFSSeccompFilterPolicyFile, |
| std::vector<FUSEMounter::BindPath>({ |
| // This needs to be recursively bind mounted so that any external |
| // media (mounted under /media) or user (under /home/chronos) mounts |
| // are visible to AVFS. |
| {base_path, false /* writable*/, true /* recursive */}, |
| }), |
| false /* permit_network_access */, kAVFSMountGroup); |
| |
| MountErrorType mount_error = fuse_mounter->Mount(); |
| if (mount_error != MOUNT_ERROR_NONE) { |
| return mount_error; |
| } |
| |
| if (!mount_info.RetrieveFromCurrentProcess() || |
| !mount_info.HasMountPath(avfs_path)) { |
| LOG(WARNING) << "Cannot mount " << quote(base_path) << " to " |
| << quote(avfs_path) << " via AVFS"; |
| return MOUNT_ERROR_INTERNAL; |
| } |
| |
| LOG(INFO) << "Mounted " << quote(base_path) << " to " << quote(avfs_path) |
| << " via AVFS"; |
| return MOUNT_ERROR_NONE; |
| } |
| |
| void ArchiveManager::AddMountVirtualPath(const std::string& mount_path, |
| const std::string& virtual_path) { |
| virtual_paths_[mount_path] = virtual_path; |
| } |
| |
| void ArchiveManager::RemoveMountVirtualPath(const std::string& mount_path) { |
| virtual_paths_.erase(mount_path); |
| } |
| |
| } // namespace cros_disks |