| // 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 "cryptohome/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 "cryptohome/mount_encrypted/mount_encrypted.h" |
| #include "cryptohome/storage/encrypted_container/backing_device.h" |
| #include "cryptohome/storage/encrypted_container/encrypted_container.h" |
| #include "cryptohome/storage/encrypted_container/encrypted_container_factory.h" |
| #include "cryptohome/storage/encrypted_container/filesystem_key.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; |
| constexpr uint64_t kExt4MinBytes = 16 * 1024 * 1024; |
| constexpr unsigned int kResizeStepSeconds = 2; |
| constexpr uint64_t kExt4ResizeBlocks = 32768 * 10; |
| // 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"; |
| constexpr uid_t kRootUid = 0; |
| constexpr gid_t kRootGid = 0; |
| constexpr uid_t kChronosUid = 1000; |
| constexpr gid_t kChronosGid = 1000; |
| |
| bool CheckBind(cryptohome::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; |
| } |
| |
| // TODO(sarthakkukreti): Evaulate resizing: it is a no-op on new encrypted |
| // stateful setups and would slow down boot once for legacy devices on update, |
| // as long as we do not iteratively resize. |
| // Spawns a filesystem resizing process and waits for it to finish. |
| void SpawnResizer(cryptohome::Platform* platform, |
| const base::FilePath& device, |
| uint64_t blocks, |
| uint64_t blocks_max) { |
| // Ignore resizing if we know the filesystem was built to max size. |
| if (blocks >= blocks_max) { |
| PLOG(ERROR) << " Resizing aborted"; |
| return; |
| } |
| |
| // TODO(keescook): Read superblock to find out the current size of |
| // the filesystem (since statvfs does not report the correct value). |
| // For now, instead of doing multi-step resizing, just resize to the |
| // full size of the block device in one step. |
| blocks = blocks_max; |
| |
| LOG(INFO) << "Resizing started in " << kResizeStepSeconds << " second steps."; |
| |
| do { |
| blocks += kExt4ResizeBlocks; |
| |
| if (blocks > blocks_max) |
| blocks = blocks_max; |
| |
| // Run the resizing function. For a fresh setup, the resize should be |
| // a no-op, the only case where this might be slow is legacy devices which |
| // have a smaller encrypted stateful partition. |
| platform->ResizeFilesystem(device, blocks); |
| } while (blocks < blocks_max); |
| |
| LOG(INFO) << "Resizing done."; |
| return; |
| } |
| |
| 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::StringToUint64(dirty_expire, &dirty_expire_centisecs)) { |
| 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); |
| } |
| |
| std::vector<std::string> BuildExt4FormatOpts(uint64_t block_bytes, |
| uint64_t blocks_min, |
| uint64_t blocks_max) { |
| return {"-T", "default", |
| "-b", std::to_string(block_bytes), |
| "-m", "0", |
| "-O", "^huge_file,^flex_bg", |
| "-E", kExt4ExtendedOptions}; |
| } |
| |
| void CheckSparseFileSize(const base::FilePath& sparse_file, int64_t file_size) { |
| base::File file(sparse_file, base::File::FLAG_OPEN | base::File::FLAG_WRITE); |
| |
| if (file.IsValid() && file.GetLength() < file_size) { |
| LOG(INFO) << "Expanding underlying sparse file to " << file_size; |
| file.SetLength(file_size); |
| } |
| } |
| |
| void Dumpe2fs(const base::FilePath& device_path) { |
| brillo::ProcessImpl dumpe2fs; |
| dumpe2fs.AddArg("/sbin/dumpe2fs"); |
| dumpe2fs.AddArg("-fh"); |
| dumpe2fs.AddArg(device_path.value()); |
| dumpe2fs.RedirectOutput(kDumpe2fsLogPath); |
| |
| dumpe2fs.Run(); |
| } |
| |
| } // namespace |
| |
| EncryptedFs::EncryptedFs( |
| const base::FilePath& rootdir, |
| uint64_t fs_size, |
| const std::string& dmcrypt_name, |
| std::unique_ptr<cryptohome::EncryptedContainer> container, |
| cryptohome::Platform* platform, |
| brillo::DeviceMapper* device_mapper) |
| : rootdir_(rootdir), |
| fs_size_(fs_size), |
| dmcrypt_name_(dmcrypt_name), |
| stateful_mount_(rootdir_.Append(STATEFUL_MNT)), |
| block_path_(stateful_mount_.Append("encrypted.block")), |
| dmcrypt_dev_(base::FilePath(kDevMapperPath).Append(dmcrypt_name_)), |
| encrypted_mount_(rootdir_.Append(ENCRYPTED_MNT)), |
| platform_(platform), |
| device_mapper_(device_mapper), |
| container_(std::move(container)), |
| bind_mounts_({{rootdir_.Append(ENCRYPTED_MNT "/var"), |
| rootdir_.Append("var"), kRootUid, kRootGid, |
| S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, false}, |
| {rootdir_.Append(ENCRYPTED_MNT "/chronos"), |
| rootdir_.Append("home/chronos"), kChronosUid, kChronosGid, |
| S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH, true}}) {} |
| |
| // static |
| std::unique_ptr<EncryptedFs> EncryptedFs::Generate( |
| const base::FilePath& rootdir, |
| cryptohome::Platform* platform, |
| brillo::DeviceMapper* device_mapper, |
| brillo::LogicalVolumeManager* lvm, |
| cryptohome::EncryptedContainerFactory* encrypted_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(rootdir.Append(STATEFUL_MNT), &stateful_statbuf)) { |
| PLOG(ERROR) << "stat() failed on: " << rootdir.Append(STATEFUL_MNT); |
| 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. |
| cryptohome::BackingDeviceConfig backing_device_config; |
| |
| base::FilePath sparse_backing_file = |
| rootdir.Append(STATEFUL_MNT "/encrypted.block"); |
| |
| base::FilePath stateful_device = platform->GetStatefulDevice(); |
| std::optional<brillo::PhysicalVolume> pv = |
| lvm->GetPhysicalVolume(stateful_device); |
| |
| // 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 (!pv || !pv->IsValid() || base::PathExists(sparse_backing_file)) { |
| backing_device_config = { |
| .type = cryptohome::BackingDeviceType::kLoopbackDevice, |
| .name = dmcrypt_name, |
| .size = fs_bytes_max, |
| .loopback = {.backing_file_path = |
| rootdir.Append(STATEFUL_MNT "/encrypted.block")}}; |
| } else { |
| backing_device_config = { |
| .type = cryptohome::BackingDeviceType::kLogicalVolumeBackingDevice, |
| .name = dmcrypt_name, |
| .size = fs_bytes_max / (1024 * 1024), |
| .logical_volume = {.thinpool_name = "thinpool", |
| .physical_volume = stateful_device}}; |
| } |
| |
| cryptohome::EncryptedContainerConfig container_config( |
| {.type = cryptohome::EncryptedContainerType::kDmcrypt, |
| .dmcrypt_config = {.backing_device_config = backing_device_config, |
| .dmcrypt_device_name = dmcrypt_name, |
| .dmcrypt_cipher = std::string(kDmCryptDefaultCipher), |
| .mkfs_opts = BuildExt4FormatOpts( |
| kExt4BlockSize, kExt4MinBytes / kExt4BlockSize, |
| fs_bytes_max / kExt4BlockSize), |
| .tune2fs_opts = {}}}); |
| |
| cryptohome::FileSystemKeyReference key_reference; |
| key_reference.fek_sig = brillo::SecureBlob("encstateful"); |
| |
| std::unique_ptr<cryptohome::EncryptedContainer> container = |
| encrypted_container_factory->Generate(container_config, key_reference); |
| |
| return std::make_unique<EncryptedFs>(rootdir, fs_bytes_max, dmcrypt_name, |
| std::move(container), platform, |
| device_mapper); |
| } |
| |
| bool EncryptedFs::Purge() { |
| LOG(INFO) << "Purging block device"; |
| return container_->Purge(); |
| } |
| |
| // Do all the work needed to actually set up the encrypted partition. |
| result_code EncryptedFs::Setup(const cryptohome::FileSystemKey& encryption_key, |
| bool rebuild) { |
| result_code rc = RESULT_FAIL_FATAL; |
| struct statvfs stateful_statbuf; |
| |
| // Get stateful partition statistics. This acts as an indicator of how large |
| // we want the encrypted stateful partition to be. |
| if (!platform_->StatVFS(stateful_mount_, &stateful_statbuf)) { |
| PLOG(ERROR) << "stat() failed on: " << stateful_mount_; |
| return rc; |
| } |
| |
| // b/131123943: Check the size of the sparse file and resize if necessary. |
| // Resizing the sparse file via truncate() should be a no-op but resizing |
| // the filesystem residing on the file is a bit more involved and may need |
| // to write metadata to several blocks. 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) |
| CheckSparseFileSize(block_path_, fs_size_); |
| else |
| LOG(WARNING) << "Low space on stateful partition; not attempting to resize " |
| << "the underlying block file."; |
| |
| 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 rc; |
| } |
| |
| if (!container_->Setup(encryption_key)) { |
| LOG(ERROR) << "Failed to set up encrypted container"; |
| TeardownByStage(TeardownStage::kTeardownContainer, true); |
| return rc; |
| } |
| |
| // 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 rc; |
| } |
| 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. |
| Dumpe2fs(dmcrypt_dev_); |
| TeardownByStage(TeardownStage::kTeardownContainer, true); |
| return rc; |
| } |
| |
| // Always spawn filesystem resizer, in case growth was interrupted. |
| // TODO(keescook): if already full size, don't resize. |
| SpawnResizer(platform_, dmcrypt_dev_, kExt4MinBytes / kExt4BlockSize, |
| fs_size_ / kExt4BlockSize); |
| |
| // 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 rc; |
| } |
| if (!platform_->Bind(bind.src, bind.dst)) { |
| PLOG(ERROR) << "mount: " << bind.src << ", " << bind.dst; |
| TeardownByStage(TeardownStage::kTeardownUnbind, true); |
| return rc; |
| } |
| } |
| |
| // Everything completed without error. |
| return RESULT_SUCCESS; |
| } |
| |
| // 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. |
| result_code EncryptedFs::Teardown() { |
| return TeardownByStage(TeardownStage::kTeardownUnbind, false); |
| } |
| |
| result_code 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 RESULT_FAIL_FATAL; |
| } |
| } |
| } |
| |
| 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 RESULT_FAIL_FATAL; |
| } |
| } |
| |
| // 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 RESULT_FAIL_FATAL; |
| } |
| platform_->Sync(); |
| return RESULT_SUCCESS; |
| } |
| |
| LOG(ERROR) << "Teardown failed."; |
| return RESULT_FAIL_FATAL; |
| } |
| |
| result_code EncryptedFs::CheckStates(void) { |
| // Verify stateful partition exists. |
| if (platform_->Access(stateful_mount_, R_OK)) { |
| LOG(INFO) << stateful_mount_ << "does not exist."; |
| return RESULT_FAIL_FATAL; |
| } |
| // 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 RESULT_FAIL_FATAL; |
| } |
| |
| // 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 RESULT_SUCCESS; |
| } |
| |
| // 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 RESULT_FAIL_FATAL; |
| } |
| } |
| |
| // 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 RESULT_FAIL_FATAL; |
| } |
| } |
| |
| LOG(INFO) << "VFS mount state validity check ok."; |
| return RESULT_SUCCESS; |
| } |
| |
| result_code EncryptedFs::ReportInfo(void) const { |
| printf("rootdir: %s\n", rootdir_.value().c_str()); |
| printf("stateful_mount: %s\n", stateful_mount_.value().c_str()); |
| printf("block_path: %s\n", block_path_.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 RESULT_SUCCESS; |
| } |
| |
| } // namespace mount_encrypted |