blob: 85dd2826593b62c3e82cc6203a3a89f4238b1c0e [file] [log] [blame]
// 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 <linux/capability.h>
#include <base/file_util.h>
#include <base/files/file_path.h>
#include <base/logging.h>
#include <base/strings/string_util.h>
#include <base/strings/stringprintf.h>
#include <chromeos/cryptohome.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/sandboxed_process.h"
#include "cros-disks/system_mounter.h"
using base::FilePath;
using std::map;
using std::string;
using std::vector;
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;
};
// Process capabilities required by the avfsd process:
// CAP_SYS_ADMIN for mounting/unmounting filesystem
const uint64_t kAVFSMountProgramCapabilities = 1 << CAP_SYS_ADMIN;
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[] =
"/opt/google/cros-disks/avfsd-seccomp.policy";
const char kAVFSMountProgram[] = "/usr/bin/avfsd";
const char kAVFSRootDirectory[] = "/var/run/avfsroot";
const char kAVFSLogFile[] = "/var/run/avfsroot/avfs.log";
const char kAVFSMediaDirectory[] = "/var/run/avfsroot/media";
const char kAVFSUsersDirectory[] = "/var/run/avfsroot/users";
const char kMediaDirectory[] = "/media";
const char kUserRootDirectory[] = "/home/chronos";
const AVFSPathMapping kAVFSPathMapping[] = {
{ kMediaDirectory, kAVFSMediaDirectory },
{ kUserRootDirectory, kAVFSUsersDirectory },
};
} // namespace
namespace cros_disks {
ArchiveManager::ArchiveManager(const string& mount_root,
Platform* platform,
Metrics* metrics)
: MountManager(mount_root, platform, metrics),
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 string& source_path) const {
// The following paths can be mounted:
// /home/chronos/user/Downloads/...<file>
// /home/chronos/user/GCache/...<file>
// /home/chronos/u-<user-id>/Downloads/...<file>
// /home/chronos/u-<user-id>/GCache/...<file>
// /media/<dir>/<dir>/...<file>
//
// TODO(benchan): Deprecate the support of /home/chronos/user once
// multi-profile is fully enabled (crbug.com/244909)
FilePath file_path(source_path);
if (FilePath(kUserRootDirectory).IsParent(file_path)) {
vector<FilePath::StringType> 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 &&
(components[3] == "user" ||
(StartsWithASCII(components[3], "u-", false) &&
chromeos::cryptohome::home::IsSanitizedUserName(
components[3].substr(2)))) &&
(components[4] == "Downloads" || components[4] == "GCache")) {
return true;
}
}
if (FilePath(kMediaDirectory).IsParent(file_path)) {
vector<FilePath::StringType> 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 string& source_path,
const string& source_format,
const vector<string>& options,
const string& mount_path) {
CHECK(!source_path.empty()) << "Invalid source path argument";
CHECK(!mount_path.empty()) << "Invalid mount path argument";
string extension = GetFileExtension(source_format);
if (extension.empty())
extension = GetFileExtension(source_path);
metrics()->RecordArchiveType(extension);
string avfs_path = GetAVFSPath(source_path, extension);
if (avfs_path.empty()) {
LOG(ERROR) << "Path '" << source_path << "' is not a supported archive";
return MOUNT_ERROR_UNSUPPORTED_ARCHIVE;
}
if (!StartAVFS()) {
LOG(ERROR) << "Failed to start AVFS mounts.";
return MOUNT_ERROR_INTERNAL;
}
// Perform a bind mount from the archive path under the AVFS mount
// to /media/archive/<archive name>.
vector<string> extended_options = options;
extended_options.push_back(MountOptions::kOptionBind);
MountOptions mount_options;
mount_options.Initialize(extended_options, false, "", "");
SystemMounter mounter(avfs_path, mount_path, "", mount_options);
MountErrorType error_type = mounter.Mount();
if (error_type == MOUNT_ERROR_NONE) {
AddMountVirtualPath(mount_path, avfs_path);
}
return error_type;
}
MountErrorType ArchiveManager::DoUnmount(const string& path,
const vector<string>& options) {
CHECK(!path.empty()) << "Invalid path argument";
// TODO(benchan): Extract error from low-level unmount operation.
if (platform()->Unmount(path)) {
// DoUnmount() is always called with |path| being the mount path.
RemoveMountVirtualPath(path);
return MOUNT_ERROR_NONE;
}
return MOUNT_ERROR_UNKNOWN;
}
string ArchiveManager::SuggestMountPath(const string& source_path) const {
// Use the archive name to name the mount directory.
FilePath base_name = FilePath(source_path).BaseName();
return FilePath(mount_root()).Append(base_name).value();
}
void ArchiveManager::RegisterDefaultFileExtensions() {
// TODO(benchan): Perhaps these settings can be read from a config file.
// zip
RegisterFileExtension("zip", "#uzip");
// tar
RegisterFileExtension("tar", "#utar");
// All variants of bzip2-compessed tar
RegisterFileExtension("tar.bz2", "#ubz2#utar");
RegisterFileExtension("tbz", "#ubz2#utar");
RegisterFileExtension("tbz2", "#ubz2#utar");
// All variants of gzip-compessed tar
RegisterFileExtension("tar.gz", "#ugz#utar");
RegisterFileExtension("tgz", "#ugz#utar");
// rar
RegisterFileExtension("rar", "#urar");
}
void ArchiveManager::RegisterFileExtension(const string& extension,
const string& avfs_handler) {
extension_handlers_[extension] = avfs_handler;
}
string ArchiveManager::GetFileExtension(const string& path) const {
FilePath file_path(path);
string extension = file_path.Extension();
if (!extension.empty()) {
// Strip the leading dot and convert the extension to lower case.
extension.erase(0, 1);
StringToLowerASCII(&extension);
}
return extension;
}
string ArchiveManager::GetAVFSPath(const string& path,
const 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
// "/var/run/avfsroot/media/layer2.zip#/test/doc/layer1.zip#"
// instead of the "nested" form, such as
// "/var/run/avfsroot/media/archive/layer2.zip/test/doc/layer1.zip#"
// where "/media/archive/layer2.zip" is a mount point to the virtual
// path "/var/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 "/var/run/avfsroot/media/layer2.zip#" within the AVFS mount.
// The following code should return the virtual path of |path| as
// "/var/run/avfsroot/media/layer2.zip#/test/doc/layer1.zip#".
map<string, string>::const_iterator handler_iterator =
extension_handlers_.find(extension);
if (handler_iterator == extension_handlers_.end())
return string();
FilePath file_path(path);
FilePath current_path = file_path.DirName();
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()) {
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 (size_t i = 0; i < arraysize(kAVFSPathMapping); ++i) {
FilePath base_path(kAVFSPathMapping[i].base_path);
FilePath avfs_path(kAVFSPathMapping[i].avfs_path);
if (base_path.AppendRelativePath(file_path, &avfs_path)) {
return avfs_path.value() + handler_iterator->second;
}
}
return string();
}
bool ArchiveManager::StartAVFS() {
if (avfs_started_)
return true;
uid_t user_id;
gid_t group_id;
if (!platform()->GetUserAndGroupId(kAVFSMountUser, &user_id, &group_id) ||
!platform()->CreateDirectory(kAVFSRootDirectory) ||
!platform()->SetOwnership(kAVFSRootDirectory, user_id, group_id) ||
!platform()->SetPermissions(kAVFSRootDirectory, S_IRWXU)) {
platform()->RemoveEmptyDirectory(kAVFSRootDirectory);
return false;
}
// 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 (size_t i = 0; i < arraysize(kAVFSPathMapping); ++i) {
bool base_path_exists =
base::PathExists(FilePath(kAVFSPathMapping[i].base_path));
const string& avfs_path = kAVFSPathMapping[i].avfs_path;
if (!base_path_exists ||
!platform()->CreateDirectory(avfs_path) ||
!platform()->SetOwnership(avfs_path, user_id, group_id) ||
!platform()->SetPermissions(avfs_path, S_IRWXU) ||
!MountAVFSPath(kAVFSPathMapping[i].base_path, avfs_path)) {
StopAVFS();
return false;
}
}
return true;
}
bool ArchiveManager::StopAVFS() {
if (!avfs_started_)
return true;
avfs_started_ = false;
// Unmounts all mounted archives before unmounting AVFS mounts.
bool all_unmounted = UnmountAll();
for (size_t i = 0; i < arraysize(kAVFSPathMapping); ++i) {
const string& path = kAVFSPathMapping[i].avfs_path;
if (!base::PathExists(FilePath(path)))
continue;
if (!platform()->Unmount(path))
all_unmounted = false;
platform()->RemoveEmptyDirectory(path);
}
platform()->RemoveEmptyDirectory(kAVFSRootDirectory);
return all_unmounted;
}
bool ArchiveManager::MountAVFSPath(const string& base_path,
const string& avfs_path) const {
MountInfo mount_info;
if (!mount_info.RetrieveFromCurrentProcess())
return false;
if (mount_info.HasMountPath(avfs_path)) {
LOG(WARNING) << "Path '" << avfs_path << "' is already mounted.";
return false;
}
uid_t user_id;
gid_t group_id;
if (!platform()->GetUserAndGroupId(kAVFSMountUser, &user_id, NULL) ||
!platform()->GetGroupId(kAVFSMountGroup, &group_id)) {
return false;
}
SandboxedProcess mount_process;
mount_process.AddArgument(kAVFSMountProgram);
mount_process.AddArgument("-o");
mount_process.AddArgument(base::StringPrintf(
"ro,nodev,noexec,nosuid,allow_other,user=%s,modules=subdir,subdir=%s",
kAVFSMountUser, base_path.c_str()));
mount_process.AddArgument(avfs_path);
if (base::PathExists(FilePath(kAVFSSeccompFilterPolicyFile))) {
mount_process.LoadSeccompFilterPolicy(kAVFSSeccompFilterPolicyFile);
} else {
// TODO(benchan): Remove this fallback mechanism once we have policy files
// for all supported platforms.
LOG(WARNING) << "Seccomp filter policy '" << kAVFSSeccompFilterPolicyFile
<< "' not found. Use POSIX capabilities mechanism instead";
mount_process.SetCapabilities(kAVFSMountProgramCapabilities);
}
// TODO(benchan): Enable PID and VFS namespace.
// TODO(wad,ellyjones,benchan): Enable network namespace once libminijail
// supports it.
mount_process.SetUserId(user_id);
mount_process.SetGroupId(group_id);
if (mount_process.Run() != 0 ||
!mount_info.RetrieveFromCurrentProcess() ||
!mount_info.HasMountPath(avfs_path)) {
LOG(WARNING) << "Failed to mount '" << base_path << "' to '"
<< avfs_path << "' via AVFS";
return false;
}
LOG(INFO) << "Mounted '" << base_path << "' to '" << avfs_path
<< "' via AVFS";
return true;
}
void ArchiveManager::AddMountVirtualPath(const string& mount_path,
const string& virtual_path) {
virtual_paths_[mount_path] = virtual_path;
}
void ArchiveManager::RemoveMountVirtualPath(const string& mount_path) {
virtual_paths_.erase(mount_path);
}
} // namespace cros_disks