| // Copyright 2017 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 "secure_erase_file/secure_erase_file.h" |
| |
| #include <fcntl.h> |
| #include <linux/fiemap.h> |
| #include <linux/fs.h> |
| #include <linux/major.h> |
| #include <linux/mmc/ioctl.h> |
| #include <mntent.h> |
| #include <stdio.h> |
| #include <sys/ioctl.h> |
| #include <sys/stat.h> |
| #include <sys/sysmacros.h> |
| #include <sys/types.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include <base/files/file_path.h> |
| #include <base/files/file_util.h> |
| #include <base/files/scoped_file.h> |
| #include <base/logging.h> |
| #include <base/memory/free_deleter.h> |
| #include <base/process/launch.h> |
| #include <base/strings/string_util.h> |
| #include <base/strings/stringprintf.h> |
| |
| namespace secure_erase_file { |
| namespace { |
| |
| // A container for fiemap that handles cleaning up allocated memory. |
| // base::FreeDeleter must be used here because the underlying struct fiemap is |
| // allocated with malloc(). |
| typedef std::unique_ptr<struct fiemap, base::FreeDeleter> ScopedFiemap; |
| |
| // For simplicity, we only support files with up to 32 extents, and fail |
| // otherwise. |
| // |
| // This is somewhat arbitrary and could be increased in the future if there is a |
| // need to securely erase larger files. |
| constexpr int kMaxExtents = 32; |
| |
| // When verifying that the original data can not be read back, we read in 1M |
| // chunks instead of reading the whole file at once. |
| constexpr size_t kVerifyReadSizeBytes = 1024 * 1024; |
| |
| const char kMountsPath[] = "/proc/mounts"; |
| |
| // Given a file path, return the corresponding mount entry info in /proc/mount. |
| // TODO(teravest): This will not work for files on eCryptfs partitions. |
| // See Platform::FindFilesystemDevice() in cryptohome for logic that would work |
| // for more paths. |
| std::string PartitionForPath(const base::FilePath& file_path) { |
| std::string partition; |
| |
| const std::string path = file_path.value(); |
| std::unique_ptr<FILE, int (*)(FILE*)> mnts(setmntent(kMountsPath, "re"), |
| endmntent); |
| if (!mnts) { |
| PLOG(ERROR) << "Unable to open " << kMountsPath; |
| return std::string(); |
| } |
| |
| size_t best_length = 0; |
| auto best_mntent = std::make_unique<struct mntent>(); |
| |
| struct mntent* mnt; |
| // getmntent() returns a thread local, so it's safe. |
| while ((mnt = getmntent(mnts.get())) != nullptr) { |
| auto l = strlen(mnt->mnt_dir); |
| if (l > best_length && path.size() > l && path[l] == '/' && |
| path.compare(0, l, mnt->mnt_dir) == 0) { |
| partition = std::string(mnt->mnt_fsname); |
| best_length = l; |
| } |
| } |
| |
| if (best_length == 0) { |
| LOG(ERROR) << "Didn't find a partition to match path " << path; |
| } |
| return partition; |
| } |
| |
| // Verifies that the data for an extent has been erased by seeking within the |
| // partition and confirming that the original file data is erased. |
| // |
| // Returns true only if the read succeeds and all bytes are 0x00 or 0xFF. |
| bool VerifyExtentErased(int partition_fd, uint64_t start, uint64_t len) { |
| // NOTE: This verification scheme assumes that blocks can be read after being |
| // trimmed. According to gwendal@, this is not true for NVMe 1.3 devices with |
| // NSFEAT bit 2 set to 1. If that is something we need to support, we could |
| // confirm that the blocks cannot be read on those devices. |
| if (lseek(partition_fd, start, SEEK_SET) < 0) { |
| PLOG(ERROR) << "Failed to seek in partition fd"; |
| return false; |
| } |
| |
| std::unique_ptr<char[]> buf(new char[kVerifyReadSizeBytes]); |
| uint64_t to_read = len; |
| do { |
| // Limit file reads to 1M regions to keep our memory footprint reasonable. |
| size_t bytes_to_read = std::min<size_t>(to_read, kVerifyReadSizeBytes); |
| int rc = HANDLE_EINTR(read(partition_fd, buf.get(), bytes_to_read)); |
| if (rc < 0) { |
| PLOG(ERROR) << "Failed to read LBAs to verify erase"; |
| return false; |
| } |
| for (int i = 0; i < rc; i++) { |
| unsigned char ch = buf.get()[i]; |
| if (ch != 0x00 && ch != 0xFF) { |
| LOG(ERROR) << "Found uncleared data at partition byte: " |
| << start + (len - to_read) + i; |
| return false; |
| } |
| } |
| to_read -= rc; |
| } while (to_read > 0); |
| return true; |
| } |
| |
| // Fetches extent data for the requested file path. |
| ScopedFiemap GetExtentsForFile(const base::FilePath& path) { |
| size_t alloc_size = offsetof(struct fiemap, fm_extents[kMaxExtents]); |
| ScopedFiemap fm(static_cast<struct fiemap*>(malloc(alloc_size))); |
| memset(fm.get(), 0, alloc_size); |
| fm->fm_length = std::numeric_limits<uint64_t>::max(); |
| fm->fm_flags |= FIEMAP_FLAG_SYNC; |
| fm->fm_extent_count = kMaxExtents; |
| |
| base::ScopedFD fd(open(path.value().c_str(), O_RDONLY | O_CLOEXEC)); |
| if (fd.get() < 0) { |
| PLOG(ERROR) << "Unable to open file: " << path.value(); |
| return nullptr; |
| } |
| |
| // There's no need to sync() before getting the extents with the ioctl() here; |
| // the kernel takes care of that with FIEMAP_FLAG_SYNC set above. See |
| // fs/ioctl.c in the kernel for details. |
| if (HANDLE_EINTR(ioctl(fd.get(), FS_IOC_FIEMAP, fm.get())) < -1) { |
| PLOG(ERROR) << "Unable to get FIEMAP for file: " << path.value(); |
| return nullptr; |
| } |
| |
| // We require that the target file has at least 1 extent. |
| // This means that we don't support inlined files in ext4, but don't have to |
| // handle the case where sensitive data is stored inside the inode. |
| if (fm->fm_mapped_extents < 1 || fm->fm_mapped_extents > kMaxExtents) { |
| LOG(ERROR) << "Bad number of mapped extents (" << fm->fm_mapped_extents |
| << ") for path: " << path.value(); |
| return nullptr; |
| } |
| |
| // We don't want to erase data for any files that have shared extents. Doing |
| // so may destroy data for other files. |
| for (uint32_t i = 0; i < fm->fm_mapped_extents; i++) { |
| if (fm->fm_extents[i].fe_flags & FIEMAP_EXTENT_SHARED) { |
| LOG(ERROR) << "Shared extent found for path: " << path.value(); |
| return nullptr; |
| } |
| if (fm->fm_extents[i].fe_flags & FIEMAP_EXTENT_DATA_INLINE) { |
| LOG(ERROR) << "Data mixed with metadata in extent found for path: " |
| << path.value(); |
| return nullptr; |
| } |
| if (fm->fm_extents[i].fe_flags & FIEMAP_EXTENT_DATA_TAIL) { |
| LOG(ERROR) << "Multiple files in block for extent found for path: " |
| << path.value(); |
| return nullptr; |
| } |
| } |
| |
| return fm; |
| } |
| |
| // Trims extents specified in |fm| on the partition specified in |partition_fd|. |
| bool TrimExtents(int partition_fd, const struct fiemap* fm) { |
| for (uint32_t i = 0; i < fm->fm_mapped_extents; i++) { |
| uint64_t range[2]; |
| range[0] = fm->fm_extents[i].fe_physical; |
| range[1] = fm->fm_extents[i].fe_length; |
| |
| // TODO(crbug.com/724169): Explicitly send TRIM+SANITIZE from userspace. |
| // |
| // BLKDISCARD can't be used as it may send DISCARD instead of a TRIM, which |
| // isn't guaranteed to completely remove data with SANITIZE. |
| // |
| // Similarly, we cannot use FITRIM here because it will skip requests that |
| // are smaller than the discard granularity. |
| // |
| // Sending a TRIM will require either explicitly crafting eMMC commands from |
| // userspace, or modifying the kernel to force a TRIM from some other |
| // interface. |
| // |
| // BLKSECDISCARD (Secure Erase) is eMMC-only and is deprecated in favor of |
| // TRIM+SANITIZE as of eMMC 4.51. |
| if (HANDLE_EINTR(ioctl(partition_fd, BLKSECDISCARD, &range)) < 0) { |
| PLOG(ERROR) << "Unable to BLKSECDISCARD target range"; |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Verifies that the data in given extents cannot be recovered. |
| bool VerifyExtentsErased(int partition_fd, const struct fiemap* fm) { |
| for (uint32_t i = 0; i < fm->fm_mapped_extents; i++) { |
| if (!VerifyExtentErased(partition_fd, fm->fm_extents[i].fe_physical, |
| fm->fm_extents[i].fe_length)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| bool IsSupported(const base::FilePath& path) { |
| struct stat stat_buf; |
| if (stat(path.value().c_str(), &stat_buf) < 0) { |
| PLOG(WARNING) << "Could not stat: " << path.value(); |
| return false; |
| } |
| if (!S_ISREG(stat_buf.st_mode)) { |
| LOG(WARNING) << "File is not a regular file: " << path.value(); |
| return false; |
| } |
| |
| // Note that this prevents us from supporting files on mount points like /var. |
| // TODO(teravest): Support files that aren't directly mounted. |
| if (major(stat_buf.st_dev) != MMC_BLOCK_MAJOR) { |
| LOG(WARNING) << "secure_erase_file only supports eMMC devices. " |
| << "Ineligible file: " << path.value(); |
| return false; |
| } |
| |
| // TODO(teravest): Send EXT_CSD commands to probe for eMMC command support. |
| return true; |
| } |
| |
| bool SecureErase(const base::FilePath& path) { |
| if (!IsSupported(path)) { |
| LOG(ERROR) << "Could not erase file, device unsupported for path: " |
| << path.value(); |
| return false; |
| } |
| |
| ScopedFiemap fm = GetExtentsForFile(path); |
| if (!fm) { |
| LOG(ERROR) << "Failed to get extents for file: " << path.value(); |
| return false; |
| } |
| |
| std::string partition = PartitionForPath(path); |
| if (partition.empty()) { |
| LOG(ERROR) << "Partition could not be found for file: " << path.value(); |
| return false; |
| } |
| |
| base::ScopedFD partition_fd( |
| open(partition.c_str(), O_RDWR | O_LARGEFILE | O_CLOEXEC)); |
| if (partition_fd.get() < 0) { |
| PLOG(ERROR) << "Unable to open partition: " << partition; |
| } |
| |
| if (!TrimExtents(partition_fd.get(), fm.get())) { |
| return false; |
| } |
| |
| if (!VerifyExtentsErased(partition_fd.get(), fm.get())) { |
| return false; |
| } |
| |
| if (unlink(path.value().c_str()) < 0) { |
| PLOG(ERROR) << "Failed to unlink() file: " << path.value(); |
| return false; |
| } |
| sync(); |
| |
| return true; |
| } |
| |
| bool DropCaches() { |
| // Drop all clean cache data to ensure that erased data does not stay visible. |
| // This clears the page cache and slab objects (maybe unnecessary). |
| // https://www.kernel.org/doc/Documentation/sysctl/vm.txt |
| constexpr char kData = '3'; |
| if (!base::WriteFile(base::FilePath("/proc/sys/vm/drop_caches"), &kData, |
| sizeof(kData))) { |
| PLOG(ERROR) << "Failed to drop cache."; |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace secure_erase_file |