blob: a473481101de6f4fd247990532c1b2127eae542c [file] [log] [blame]
// 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 <fcntl.h>
#include <linux/loop.h>
#include <stdio.h>
#include <string>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <unistd.h>
#include <base/files/file.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/posix/eintr_wrapper.h>
#include <base/strings/stringprintf.h>
#include <brillo/process.h>
#include <gtest/gtest.h>
namespace {
// Size of a disk sector in bytes.
constexpr int kSectorSize = 512;
constexpr int kSectorCount = 100 * 1024;
// How many sectors the start of a partition needs to be aligned to.
constexpr int kSectorAlign = 2048;
constexpr int kNumPartitions = 5;
// Split disk space evenly between partitions (minus |kSectorAlign| chunk at
// beginning needed for alignment).
constexpr int kMaxPartitionSize =
(kSectorCount - kSectorAlign) / kNumPartitions;
// Largest partition size possible that is a multiple of |kSectorAlign|.
constexpr int kAlignedPartitionSize =
kMaxPartitionSize - (kMaxPartitionSize % kSectorAlign);
constexpr char kTestImageFileName[] = "test.img";
constexpr char kLoopbackDeviceFormat[] = "/dev/loop%d";
constexpr char kLoopbackDevicePartitionFormat[] = "/dev/loop%dp%d";
constexpr char kStatefulPath[] = "/mnt/stateful_partition";
constexpr char kMkfsCommandFormat[] = "/sbin/mkfs.%s";
constexpr char kWriteGptPath[] = "/usr/sbin/write_gpt.sh";
// File format for fake write_gpt.sh which will be imported by clobber-state.
constexpr char kFakeWriteGptFormat[] =
"#!/bin/sh\n"
"ROOT_DEV=%sp2\n"
"ROOT_DISK=%s\n"
"load_base_vars() {\n"
" PARTITION_NUM_KERN_A=1\n"
" PARTITION_NUM_ROOT_A=2\n"
" PARTITION_NUM_KERN_B=3\n"
" PARTITION_NUM_ROOT_B=4\n"
" PARTITION_NUM_STATE=5\n"
"}\n";
constexpr char kSfdiskInputName[] = "sfdisk_input";
// Commands for disk formatting utility sfdisk.
// Specify that partition table should use gpt format.
constexpr char kSfdiskPartitionTableTypeCommand[] = "label: gpt\n";
// Template for partition command (size specified in number of sectors).
constexpr char kSfdiskPartitionCommandFormat[] = "size=%d, type=%s\n";
// ChromeOS Kernel Partition Type.
constexpr char kPartitionTypeKernel[] = "FE3A2A5D-4F32-41A7-B725-ACCC3285A309";
// ChromeOS Root Partition Type.
constexpr char kPartitionTypeRoot[] = "3CB8E202-3B7E-47DD-8A3C-7FF2A13CFCEC";
// Linux Data Partition Type.
constexpr char kPartitionTypeData[] = "0FC63DAF-8483-4772-8E79-3D69D8477DE4";
class FileHandler {
public:
explicit FileHandler(base::FilePath root_path) : root_path_(root_path) {}
bool WriteFile(const std::string& path, const char* contents) {
int size = strlen(contents);
base::FilePath fp = root_path_.Append(path);
if (!base::CreateDirectory(fp.DirName())) {
return false;
}
return base::WriteFile(fp, contents, size) == size;
}
bool FileExists(const std::string& path) {
return base::PathExists(root_path_.Append(path));
}
bool ContentsEqual(const std::string& path,
const std::string& expected_contents) {
base::FilePath fp = root_path_.Append(path);
std::string actual_contents;
if (!base::ReadFileToString(fp, &actual_contents)) {
return false;
}
return actual_contents == expected_contents;
}
private:
base::FilePath root_path_;
};
} // namespace
class ClobberStateTest : public testing::Test {
protected:
ClobberStateTest() : loop_device_number_(-1) {}
void SetUp() override {
ASSERT_EQ(getuid(), 0) << "ClobberStateTest requires root permission.";
base::FilePath temp_directory;
ASSERT_TRUE(base::CreateNewTempDirectory("", &temp_directory));
test_image_path_ = temp_directory.Append(kTestImageFileName);
stateful_mount_path_ = base::FilePath(kStatefulPath);
write_gpt_path_ = base::FilePath(kWriteGptPath);
// Create backing file for loopback device.
base::File test_image(test_image_path_,
base::File::FLAG_CREATE | base::File::FLAG_WRITE);
ASSERT_TRUE(test_image.IsValid());
ASSERT_GE(test_image.SetLength(kSectorSize * kSectorCount), 0);
ASSERT_TRUE(test_image.Flush());
test_image.Close();
// Setup sfdisk partitioning commands.
// Write to an intermediate file first because writing to sfdisk's
// stdin caused the loopback device to unmount prematurely.
base::FilePath sfdisk_input_path = temp_directory.Append(kSfdiskInputName);
base::File sfdisk_input(sfdisk_input_path,
base::File::FLAG_CREATE | base::File::FLAG_WRITE);
ASSERT_TRUE(sfdisk_input.IsValid());
std::vector<std::string> sfdisk_commands{
kSfdiskPartitionTableTypeCommand,
base::StringPrintf(kSfdiskPartitionCommandFormat, kAlignedPartitionSize,
kPartitionTypeKernel),
base::StringPrintf(kSfdiskPartitionCommandFormat, kAlignedPartitionSize,
kPartitionTypeRoot),
base::StringPrintf(kSfdiskPartitionCommandFormat, kAlignedPartitionSize,
kPartitionTypeKernel),
base::StringPrintf(kSfdiskPartitionCommandFormat, kAlignedPartitionSize,
kPartitionTypeRoot),
base::StringPrintf(kSfdiskPartitionCommandFormat, kAlignedPartitionSize,
kPartitionTypeData)};
for (const std::string& command_string : sfdisk_commands) {
const char* command = command_string.c_str();
EXPECT_EQ(sfdisk_input.WriteAtCurrentPos(command, strlen(command)),
strlen(command));
}
sfdisk_input.Close();
// Build partition table on backing file.
brillo::ProcessImpl proc;
proc.AddArg("/sbin/sfdisk");
proc.AddArg(test_image_path_.value());
proc.RedirectInput(sfdisk_input_path.value());
ASSERT_EQ(proc.Run(), 0);
ASSERT_TRUE(base::DeleteFile(sfdisk_input_path, false));
// Mount test image to loopback device.
loop_control_ = base::File(base::FilePath("/dev/loop-control"),
base::File::FLAG_OPEN | base::File::FLAG_READ);
ASSERT_TRUE(loop_control_.IsValid());
loop_device_number_ =
HANDLE_EINTR(ioctl(loop_control_.GetPlatformFile(), LOOP_CTL_GET_FREE));
ASSERT_GE(loop_device_number_, 0);
std::string loop_device_path =
base::StringPrintf(kLoopbackDeviceFormat, loop_device_number_);
loop_device_ = base::File(
base::FilePath(loop_device_path),
base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE);
ASSERT_TRUE(loop_device_.IsValid());
test_image_ = base::File(test_image_path_, base::File::FLAG_OPEN |
base::File::FLAG_READ |
base::File::FLAG_WRITE);
ASSERT_TRUE(test_image_.IsValid());
ASSERT_GE(ioctl(loop_device_.GetPlatformFile(), LOOP_SET_FD,
test_image_.GetPlatformFile()),
0);
// Update loop device properties.
struct loop_info64 loop_info;
ASSERT_GE(
ioctl(loop_device_.GetPlatformFile(), LOOP_GET_STATUS64, &loop_info),
0);
// Enable scanning for partitions on loopback device.
loop_info.lo_flags |= LO_FLAGS_PARTSCAN;
// Automatically detach loop device when last file is closed (i.e. when
// kStatefulPath is unmounted).
loop_info.lo_flags |= LO_FLAGS_AUTOCLEAR;
ASSERT_GE(
ioctl(loop_device_.GetPlatformFile(), LOOP_SET_STATUS64, &loop_info),
0);
// Make filesystems on root and stateful partitions.
ASSERT_TRUE(MakeFilesystem("ext2", 2));
ASSERT_TRUE(MakeFilesystem("ext2", 4));
ASSERT_TRUE(MakeFilesystem("ext4", 5));
// Create mount point and mount stateful partition off of loopback device.
ASSERT_TRUE(base::CreateDirectory(stateful_mount_path_));
std::string stateful_loopback_device = base::StringPrintf(
kLoopbackDevicePartitionFormat, loop_device_number_, 5);
ASSERT_GE(mount(stateful_loopback_device.c_str(),
stateful_mount_path_.value().c_str(), "ext4",
MS_SYNCHRONOUS | MS_DIRSYNC, NULL),
0);
// Create fake gpt file listing partition structure.
std::string fake_write_gpt =
base::StringPrintf(kFakeWriteGptFormat, loop_device_path.c_str(),
loop_device_path.c_str());
size_t length = fake_write_gpt.length();
EXPECT_EQ(base::WriteFile(write_gpt_path_, fake_write_gpt.c_str(), length),
length);
}
~ClobberStateTest() {
// umount call will detach loop device as well.
EXPECT_GE(umount(kStatefulPath), 0);
test_image_.Close();
EXPECT_TRUE(base::DeleteFile(write_gpt_path_, false));
EXPECT_TRUE(base::DeleteFile(test_image_path_, false));
}
bool MakeFilesystem(const char* type, int partition_number) {
brillo::ProcessImpl proc;
proc.AddArg(base::StringPrintf(kMkfsCommandFormat, type));
proc.AddArg(base::StringPrintf(kLoopbackDevicePartitionFormat,
loop_device_number_, partition_number));
return proc.Run() == 0;
}
// Path at which the stateful partition should be mounted.
base::FilePath stateful_mount_path_;
private:
// Image file used as backing file for loopback filesystem.
base::File test_image_;
base::FilePath test_image_path_;
// Path to write_gpt.sh file which will be imported by clobber-state.
base::FilePath write_gpt_path_;
// Kernel loopback device control file.
base::File loop_control_;
// Loop device behind which the test image is mounted.
base::File loop_device_;
int loop_device_number_;
};
TEST_F(ClobberStateTest, BasicTest) {
FileHandler fh(stateful_mount_path_);
EXPECT_TRUE(fh.WriteFile("unencrypted/preserve/powerwash_count", "5\n"));
EXPECT_TRUE(
fh.WriteFile("unencrypted/preserve/tpm_firmware_update_request", ""));
EXPECT_TRUE(fh.WriteFile(
"unencrypted/preserve/update_engine/prefs/rollback-happened", ""));
EXPECT_TRUE(fh.WriteFile(
"unencrypted/preserve/update_engine/prefs/rollback-version", "3"));
EXPECT_TRUE(fh.WriteFile("unencrypted/preserve/attestation.epb", "TEST"));
EXPECT_TRUE(fh.WriteFile("unencrypted/preserve/not_saved", "not_saved"));
brillo::ProcessImpl proc;
proc.AddArg("./clobber-state");
proc.AddArg("rollback");
proc.AddArg("fast");
proc.AddArg("keepimg");
proc.AddArg("safe");
proc.AddArg("factory");
EXPECT_EQ(proc.Run(), 0);
EXPECT_TRUE(fh.ContentsEqual("unencrypted/preserve/powerwash_count", "6\n"));
EXPECT_TRUE(
fh.ContentsEqual("unencrypted/preserve/tpm_firmware_update_request", ""));
EXPECT_TRUE(fh.ContentsEqual(
"unencrypted/preserve/update_engine/prefs/rollback-happened", ""));
EXPECT_TRUE(fh.ContentsEqual(
"unencrypted/preserve/update_engine/prefs/rollback-version", "3"));
EXPECT_TRUE(fh.ContentsEqual("unencrypted/preserve/attestation.epb", "TEST"));
EXPECT_FALSE(fh.FileExists("unencrypted/preserve/not_saved"));
}