| // Copyright 2018 The ChromiumOS Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "init/mount_encrypted/encrypted_fs.h" | |
| #include <fcntl.h> | |
| #include <grp.h> | |
| #include <pwd.h> | |
| #include <string.h> | |
| #include <sys/mount.h> | |
| #include <sys/statvfs.h> | |
| #include <sys/types.h> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <base/files/file_util.h> | |
| #include <base/logging.h> | |
| #include <base/strings/string_number_conversions.h> | |
| #include <brillo/blkdev_utils/lvm.h> | |
| #include <brillo/process/process.h> | |
| #include <brillo/secure_blob.h> | |
| #include <libhwsec-foundation/crypto/secure_blob_util.h> | |
| #include <libhwsec-foundation/crypto/sha.h> | |
| #include <libstorage/platform/platform.h> | |
| #include <libstorage/storage_container/backing_device.h> | |
| #include <libstorage/storage_container/filesystem_key.h> | |
| #include <libstorage/storage_container/storage_container.h> | |
| #include <libstorage/storage_container/storage_container_factory.h> | |
| namespace mount_encrypted { | |
| namespace { | |
| constexpr char kEncryptedFSType[] = "ext4"; | |
| constexpr char kCryptDevName[] = "encstateful"; | |
| constexpr char kDevMapperPath[] = "/dev/mapper"; | |
| constexpr char kDumpe2fsLogPath[] = "/run/mount_encrypted/dumpe2fs.log"; | |
| constexpr char kProcDirtyExpirePath[] = "/proc/sys/vm/dirty_expire_centisecs"; | |
| constexpr float kSizePercent = 0.3; | |
| constexpr uint64_t kExt4BlockSize = 4096; | |
| // Block size is 4k => Minimum free space available to try resizing is 400MB. | |
| constexpr int64_t kMinBlocksAvailForResize = 102400; | |
| constexpr char kExt4ExtendedOptions[] = "discard"; | |
| constexpr char kDmCryptDefaultCipher[] = "aes-cbc-essiv:sha256"; | |
| bool CheckBind(libstorage::Platform* platform, const BindMount& bind) { | |
| if (platform->Access(bind.src, R_OK) && | |
| !platform->CreateDirectory(bind.src)) { | |
| PLOG(ERROR) << "mkdir " << bind.src; | |
| return false; | |
| } | |
| if (platform->Access(bind.dst, R_OK) && | |
| !(platform->CreateDirectory(bind.dst) && | |
| platform->SetPermissions(bind.dst, bind.mode))) { | |
| PLOG(ERROR) << "mkdir " << bind.dst; | |
| return false; | |
| } | |
| // Destination may be on read-only filesystem, so skip tweaks. | |
| // Must do explicit chmod since mkdir()'s mode respects umask. | |
| if (!platform->SetPermissions(bind.src, bind.mode)) { | |
| PLOG(ERROR) << "chmod " << bind.src; | |
| return false; | |
| } | |
| if (!platform->SetOwnership(bind.src, bind.owner, bind.group, true)) { | |
| PLOG(ERROR) << "chown " << bind.src; | |
| return false; | |
| } | |
| return true; | |
| } | |
| std::string GetMountOpts() { | |
| // Use vm.dirty_expire_centisecs / 100 as the commit interval. | |
| std::string dirty_expire; | |
| uint64_t dirty_expire_centisecs; | |
| uint64_t commit_interval = 600; | |
| if (base::ReadFileToString(base::FilePath(kProcDirtyExpirePath), | |
| &dirty_expire)) { | |
| base::TrimWhitespaceASCII(dirty_expire, base::TRIM_ALL, &dirty_expire); | |
| if (!base::StringToUint64(dirty_expire, &dirty_expire_centisecs)) { | |
| LOG(INFO) << "Failed to parse contents of " << dirty_expire; | |
| } | |
| LOG(INFO) << "Using vm.dirty_expire_centisecs/100 as the commit interval"; | |
| // Keep commit interval as 5 seconds (default for ext4) for smaller | |
| // values of dirty_expire_centisecs. | |
| if (dirty_expire_centisecs < 600) | |
| commit_interval = 5; | |
| else | |
| commit_interval = dirty_expire_centisecs / 100; | |
| } | |
| return "discard,commit=" + std::to_string(commit_interval); | |
| } | |
| } // namespace | |
| EncryptedFs::EncryptedFs( | |
| const base::FilePath& rootdir, | |
| const base::FilePath& statefulmnt, | |
| uint64_t fs_size, | |
| const std::string& dmcrypt_name, | |
| std::unique_ptr<libstorage::StorageContainer> container, | |
| libstorage::Platform* platform) | |
| : rootdir_(rootdir), | |
| fs_size_(fs_size), | |
| dmcrypt_name_(dmcrypt_name), | |
| stateful_mount_(statefulmnt), | |
| dmcrypt_dev_(base::FilePath(kDevMapperPath).Append(dmcrypt_name_)), | |
| encrypted_mount_(stateful_mount_.Append("encrypted")), | |
| platform_(platform), | |
| container_(std::move(container)), | |
| bind_mounts_( | |
| {{encrypted_mount_.Append("var"), rootdir_.Append("var"), | |
| libstorage::kRootUid, libstorage::kRootGid, | |
| S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, false}, | |
| {encrypted_mount_.Append("chronos"), rootdir_.Append("home/chronos"), | |
| libstorage::kChronosUid, libstorage::kChronosGid, | |
| S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, true}}) {} | |
| // static | |
| std::unique_ptr<EncryptedFs> EncryptedFs::Generate( | |
| const base::FilePath& rootdir, | |
| const base::FilePath& statefulmnt, | |
| libstorage::Platform* platform, | |
| libstorage::StorageContainerFactory* storage_container_factory) { | |
| // Calculate the maximum size of the encrypted stateful partition. | |
| // truncate()/ftruncate() use int64_t for file size. | |
| struct statvfs stateful_statbuf; | |
| if (!platform->StatVFS(statefulmnt, &stateful_statbuf)) { | |
| PLOG(ERROR) << "stat() failed on: " << statefulmnt; | |
| return nullptr; | |
| } | |
| int64_t fs_bytes_max = static_cast<int64_t>(stateful_statbuf.f_blocks); | |
| fs_bytes_max *= kSizePercent; | |
| fs_bytes_max *= stateful_statbuf.f_frsize; | |
| std::string dmcrypt_name = std::string(kCryptDevName); | |
| if (rootdir != base::FilePath("/")) { | |
| brillo::SecureBlob digest = | |
| hwsec_foundation::Sha256(brillo::SecureBlob(rootdir.value())); | |
| std::string hex = hwsec_foundation::SecureBlobToHex(digest); | |
| dmcrypt_name += "_" + hex.substr(0, 16); | |
| } | |
| // Initialize the encrypted container. | |
| libstorage::BackingDeviceConfig backing_device_config; | |
| base::FilePath sparse_backing_file = statefulmnt.Append("encrypted.block"); | |
| base::FilePath stateful_device = platform->GetStatefulDevice(); | |
| // Use the loopback sparse file in 2 cases: | |
| // 1. If the device is set up using an ext4 stateful partition. | |
| // 2. If the device already has an existing sparse loopback file: this | |
| // situation can occur during migration of a device to an LVM stateful | |
| // stateful partition. | |
| // TODO(sarthakkukreti@): Loopback backing devices use size in bytes whereas | |
| // logical volume backing devices use size in megabytes. Fix this | |
| // inconsistency. | |
| if (!platform->IsStatefulLogicalVolumeSupported() || | |
| platform->FileExists(sparse_backing_file)) { | |
| backing_device_config = { | |
| .type = libstorage::BackingDeviceType::kLoopbackDevice, | |
| .name = dmcrypt_name, | |
| .size = fs_bytes_max, | |
| .loopback = {.backing_file_path = sparse_backing_file}}; | |
| } else { | |
| brillo::LogicalVolumeManager* lvm = platform->GetLogicalVolumeManager(); | |
| brillo::PhysicalVolume pv(stateful_device, | |
| std::make_shared<brillo::LvmCommandRunner>()); | |
| std::optional<brillo::VolumeGroup> vg = lvm->GetVolumeGroup(pv); | |
| if (!vg || !vg->IsValid()) { | |
| LOG(WARNING) << "Failed to get volume group."; | |
| return nullptr; | |
| } | |
| std::optional<brillo::Thinpool> thinpool = | |
| lvm->GetThinpool(*vg, "thinpool"); | |
| if (!thinpool || !thinpool->IsValid()) { | |
| LOG(WARNING) << "Failed to get thinpool."; | |
| return nullptr; | |
| } | |
| backing_device_config = { | |
| .type = libstorage::BackingDeviceType::kLogicalVolumeBackingDevice, | |
| .name = dmcrypt_name, | |
| .size = fs_bytes_max / (1024 * 1024), | |
| .logical_volume = { | |
| .vg = std::make_shared<brillo::VolumeGroup>(*vg), | |
| .thinpool = std::make_shared<brillo::Thinpool>(*thinpool)}}; | |
| } | |
| libstorage::StorageContainerConfig container_config( | |
| {.filesystem_config = | |
| {.mkfs_opts = {"-T", "default", "-b", std::to_string(kExt4BlockSize), | |
| "-m", "0", "-O", "^huge_file,^flex_bg", "-E", | |
| kExt4ExtendedOptions}, | |
| .tune2fs_opts = {}, | |
| .backend_type = libstorage::StorageContainerType::kDmcrypt, | |
| .recovery = libstorage::RecoveryType::kEnforceCleaning}, | |
| .dmcrypt_config = { | |
| .backing_device_config = backing_device_config, | |
| .dmcrypt_device_name = dmcrypt_name, | |
| .dmcrypt_cipher = std::string(kDmCryptDefaultCipher)}}); | |
| libstorage::FileSystemKeyReference key_reference; | |
| key_reference.fek_sig = brillo::SecureBlob("encstateful"); | |
| std::unique_ptr<libstorage::StorageContainer> container = | |
| storage_container_factory->Generate( | |
| container_config, libstorage::StorageContainerType::kExt4, | |
| key_reference); | |
| return std::make_unique<EncryptedFs>(rootdir, statefulmnt, fs_bytes_max, | |
| dmcrypt_name, std::move(container), | |
| platform); | |
| } | |
| bool EncryptedFs::Purge() { | |
| LOG(INFO) << "Purging block device"; | |
| return container_->Purge(); | |
| } | |
| // Do all the work needed to actually set up the encrypted partition. | |
| bool EncryptedFs::Setup(const libstorage::FileSystemKey& encryption_key, | |
| bool rebuild) { | |
| // Get stateful partition statistics. This acts as an indicator of how large | |
| // we want the encrypted stateful partition to be. | |
| struct statvfs stateful_statbuf; | |
| if (!platform_->StatVFS(stateful_mount_, &stateful_statbuf)) { | |
| PLOG(ERROR) << "stat() failed on: " << stateful_mount_; | |
| return false; | |
| } | |
| if (rebuild) { | |
| // Wipe out the old files, and ignore errors. | |
| Purge(); | |
| // Create new sparse file. | |
| LOG(INFO) << "Creating sparse backing file with size " << fs_size_; | |
| } else if (!container_->Exists()) { | |
| // If not rebuilding, we expect the container to be present. | |
| LOG(ERROR) << "Encrypted container doesn't exist"; | |
| return false; | |
| } | |
| if (!container_->Setup(encryption_key)) { | |
| LOG(ERROR) << "Failed to set up encrypted container"; | |
| TeardownByStage(TeardownStage::kTeardownContainer, true); | |
| return false; | |
| } | |
| // Trigger filesystem resizer, in case growth was interrupted. | |
| // TODO(keescook): if already full size, don't resize. | |
| // If there aren't enough blocks | |
| // available, we might succeed here but eventually fail to resize and corrupt | |
| // the encrypted stateful file system. Check if there are at least a few | |
| // blocks available on the stateful partition. | |
| if (stateful_statbuf.f_bfree > kMinBlocksAvailForResize && | |
| !container_->Resize(0)) { | |
| LOG(ERROR) << "Failed to resize stateful"; | |
| return false; | |
| } | |
| // Mount the dm-crypt partition finally. | |
| LOG(INFO) << "Mounting " << dmcrypt_dev_ << " onto " << encrypted_mount_; | |
| if (platform_->Access(encrypted_mount_, R_OK) && | |
| !(platform_->CreateDirectory(encrypted_mount_) && | |
| platform_->SetPermissions(encrypted_mount_, | |
| S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH))) { | |
| PLOG(ERROR) << dmcrypt_dev_; | |
| TeardownByStage(TeardownStage::kTeardownContainer, true); | |
| return false; | |
| } | |
| if (!platform_->Mount(dmcrypt_dev_, encrypted_mount_, kEncryptedFSType, | |
| MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_NOATIME, | |
| GetMountOpts().c_str())) { | |
| PLOG(ERROR) << "mount: " << dmcrypt_dev_ << ", " << encrypted_mount_; | |
| // On failure to mount, use dumpe2fs to collect debugging data about | |
| // the unencrypted block device that failed to mount. Since mount-encrypted | |
| // cleans up afterwards, this is the only point where this data can be | |
| // collected. | |
| platform_->ReportFilesystemDetails(dmcrypt_dev_, | |
| base::FilePath(kDumpe2fsLogPath)); | |
| TeardownByStage(TeardownStage::kTeardownContainer, true); | |
| return false; | |
| } | |
| // Perform bind mounts. | |
| for (auto& bind : bind_mounts_) { | |
| LOG(INFO) << "Bind mounting " << bind.src << " onto " << bind.dst; | |
| if (!CheckBind(platform_, bind)) { | |
| TeardownByStage(TeardownStage::kTeardownUnbind, true); | |
| return false; | |
| } | |
| if (!platform_->Bind(bind.src, bind.dst)) { | |
| PLOG(ERROR) << "mount: " << bind.src << ", " << bind.dst; | |
| TeardownByStage(TeardownStage::kTeardownUnbind, true); | |
| return false; | |
| } | |
| } | |
| // Everything completed without error. | |
| return true; | |
| } | |
| // Clean up all bind mounts, mounts, attaches, etc. Only the final | |
| // action informs the return value. This makes it so that failures | |
| // can be cleaned up from, and continue the shutdown process on a | |
| // second call. If the loopback cannot be found, claim success. | |
| bool EncryptedFs::Teardown() { | |
| return TeardownByStage(TeardownStage::kTeardownUnbind, false); | |
| } | |
| bool EncryptedFs::TeardownByStage(TeardownStage stage, bool ignore_errors) { | |
| switch (stage) { | |
| case TeardownStage::kTeardownUnbind: | |
| for (auto& bind : bind_mounts_) { | |
| LOG(INFO) << "Unmounting " << bind.dst; | |
| errno = 0; | |
| // Allow either success or a "not mounted" failure. | |
| if (!platform_->Unmount(bind.dst, false, nullptr) && !ignore_errors) { | |
| if (errno != EINVAL) { | |
| PLOG(ERROR) << "umount " << bind.dst; | |
| return false; | |
| } | |
| } | |
| } | |
| LOG(INFO) << "Unmounting " << encrypted_mount_; | |
| errno = 0; | |
| // Allow either success or a "not mounted" failure. | |
| if (!platform_->Unmount(encrypted_mount_, false, nullptr) && | |
| !ignore_errors) { | |
| if (errno != EINVAL) { | |
| PLOG(ERROR) << "umount " << encrypted_mount_; | |
| return false; | |
| } | |
| } | |
| // Force syncs to make sure we don't tickle racey/buggy kernel | |
| // routines that might be causing crosbug.com/p/17610. | |
| platform_->Sync(); | |
| // Intentionally fall through here to teardown the lower dmcrypt device. | |
| [[fallthrough]]; | |
| case TeardownStage::kTeardownContainer: | |
| LOG(INFO) << "Removing " << dmcrypt_dev_; | |
| if (!container_->Teardown() && !ignore_errors) { | |
| LOG(ERROR) << "Failed to teardown encrypted container"; | |
| return false; | |
| } | |
| platform_->Sync(); | |
| return true; | |
| } | |
| LOG(ERROR) << "Teardown failed."; | |
| return false; | |
| } | |
| bool EncryptedFs::CheckStates() { | |
| // Verify stateful partition exists. | |
| if (platform_->Access(stateful_mount_, R_OK)) { | |
| LOG(INFO) << stateful_mount_ << "does not exist."; | |
| return false; | |
| } | |
| // Verify stateful is either a separate mount, or that the | |
| // root directory is writable (i.e. a factory install, dev mode | |
| // where root remounted rw, etc). | |
| if (platform_->SameVFS(stateful_mount_, rootdir_) && | |
| platform_->Access(rootdir_, W_OK)) { | |
| LOG(INFO) << stateful_mount_ << " is not mounted."; | |
| return false; | |
| } | |
| // Verify encrypted partition is missing or not already mounted. | |
| if (platform_->Access(encrypted_mount_, R_OK) == 0 && | |
| !platform_->SameVFS(encrypted_mount_, stateful_mount_)) { | |
| LOG(INFO) << encrypted_mount_ << " already appears to be mounted."; | |
| return true; | |
| } | |
| // Verify that bind mount targets exist. | |
| for (auto& bind : bind_mounts_) { | |
| if (platform_->Access(bind.dst, R_OK)) { | |
| PLOG(ERROR) << bind.dst << " mount point is missing."; | |
| return false; | |
| } | |
| } | |
| // Verify that old bind mounts on stateful haven't happened yet. | |
| for (auto& bind : bind_mounts_) { | |
| if (bind.submount) | |
| continue; | |
| if (platform_->SameVFS(bind.dst, stateful_mount_)) { | |
| LOG(INFO) << bind.dst << " already bind mounted."; | |
| return false; | |
| } | |
| } | |
| LOG(INFO) << "VFS mount state validity check ok."; | |
| return true; | |
| } | |
| bool EncryptedFs::ReportInfo() const { | |
| printf("rootdir: %s\n", rootdir_.value().c_str()); | |
| printf("stateful_mount: %s\n", stateful_mount_.value().c_str()); | |
| printf("encrypted_mount: %s\n", encrypted_mount_.value().c_str()); | |
| printf("dmcrypt_name: %s\n", dmcrypt_name_.c_str()); | |
| printf("dmcrypt_dev: %s\n", dmcrypt_dev_.value().c_str()); | |
| printf("bind mounts:\n"); | |
| for (auto& mnt : bind_mounts_) { | |
| printf("\tsrc:%s\n", mnt.src.value().c_str()); | |
| printf("\tdst:%s\n", mnt.dst.value().c_str()); | |
| printf("\towner:%d\n", mnt.owner); | |
| printf("\tmode:%o\n", mnt.mode); | |
| printf("\tsubmount:%d\n", mnt.submount); | |
| printf("\n"); | |
| } | |
| return true; | |
| } | |
| } // namespace mount_encrypted |