Skip to content

Commit 9105f4b

Browse files
committed
Fix verity discarded bug
If update_engine opens CowWriterFileDescriptor w/o writing anything, data past the resume label is readable while fd is open, but will be discarded once the fd is closed. Such "phantom read" causes inconsistency. This CL contains two changes to address the above bug: 1. When device reboots after update, all I/O are served by snapuserd. update_engine should use snapuserd for verification to emulate bahvior of device after reboot. 2. When a CowWriterFd is opened, don't call Finalize() if no verity is written. Since past-the-end data is discarded when we call Finalize() Test: th Bug: 186196758 Change-Id: Ia1d31b671c16fded7319677fe0397f1288457201
1 parent a9b5d8c commit 9105f4b

6 files changed

+99
-27
lines changed

Diff for: common/hash_calculator.cc

+5
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ bool HashCalculator::RawHashOfData(const brillo::Blob& data,
9595
return RawHashOfBytes(data.data(), data.size(), out_hash);
9696
}
9797

98+
bool HashCalculator::RawHashOfFile(const string& name, brillo::Blob* out_hash) {
99+
const auto file_size = utils::FileSize(name);
100+
return RawHashOfFile(name, file_size, out_hash) == file_size;
101+
}
102+
98103
off_t HashCalculator::RawHashOfFile(const string& name,
99104
off_t length,
100105
brillo::Blob* out_hash) {

Diff for: common/hash_calculator.h

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class HashCalculator {
7575
static off_t RawHashOfFile(const std::string& name,
7676
off_t length,
7777
brillo::Blob* out_hash);
78+
static bool RawHashOfFile(const std::string& name, brillo::Blob* out_hash);
7879

7980
private:
8081
// If non-empty, the final raw hash. Will only be set to non-empty when

Diff for: common/utils.h

+7-1
Original file line numberDiff line numberDiff line change
@@ -399,13 +399,19 @@ class ScopedTempFile {
399399

400400
// If |open_fd| is true, a writable file descriptor will be opened for this
401401
// file.
402-
explicit ScopedTempFile(const std::string& pattern, bool open_fd = false) {
402+
// If |truncate_size| is non-zero, truncate file to that size on creation.
403+
explicit ScopedTempFile(const std::string& pattern,
404+
bool open_fd = false,
405+
size_t truncate_size = 0) {
403406
CHECK(utils::MakeTempFile(pattern, &path_, open_fd ? &fd_ : nullptr));
404407
unlinker_.reset(new ScopedPathUnlinker(path_));
405408
if (open_fd) {
406409
CHECK_GE(fd_, 0);
407410
fd_closer_.reset(new ScopedFdCloser(&fd_));
408411
}
412+
if (truncate_size > 0) {
413+
CHECK_EQ(0, truncate(path_.c_str(), truncate_size));
414+
}
409415
}
410416
virtual ~ScopedTempFile() = default;
411417

Diff for: payload_consumer/cow_writer_file_descriptor.cc

+15-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ namespace chromeos_update_engine {
2828
CowWriterFileDescriptor::CowWriterFileDescriptor(
2929
std::unique_ptr<android::snapshot::ISnapshotWriter> cow_writer)
3030
: cow_writer_(std::move(cow_writer)),
31-
cow_reader_(cow_writer_->OpenReader()) {}
31+
cow_reader_(cow_writer_->OpenReader()) {
32+
CHECK_NE(cow_writer_, nullptr);
33+
CHECK_NE(cow_reader_, nullptr);
34+
}
3235

3336
bool CowWriterFileDescriptor::Open(const char* path, int flags, mode_t mode) {
3437
LOG(ERROR) << "CowWriterFileDescriptor doesn't support Open()";
@@ -113,7 +116,17 @@ bool CowWriterFileDescriptor::Flush() {
113116

114117
bool CowWriterFileDescriptor::Close() {
115118
if (cow_writer_) {
116-
TEST_AND_RETURN_FALSE(cow_writer_->Finalize());
119+
// b/186196758
120+
// When calling
121+
// InitializeAppend(kEndOfInstall), the SnapshotWriter only reads up to the
122+
// given label. But OpenReader() completely disregards the resume label and
123+
// reads all ops. Therefore, update_engine sees the verity data. However,
124+
// when calling SnapshotWriter::Finalize(), data after resume label are
125+
// discarded, therefore verity data is gone. To prevent phantom reads, don't
126+
// call Finalize() unless we actually write something.
127+
if (dirty_) {
128+
TEST_AND_RETURN_FALSE(cow_writer_->Finalize());
129+
}
117130
cow_writer_ = nullptr;
118131
}
119132
if (cow_reader_) {

Diff for: payload_consumer/filesystem_verifier_action.cc

+23
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ void FilesystemVerifierAction::Cleanup(ErrorCode code) {
112112
// This memory is not used anymore.
113113
buffer_.clear();
114114

115+
// If we didn't write verity, partitions were maped. Releaase resource now.
116+
if (!install_plan_.write_verity &&
117+
dynamic_control_->UpdateUsesSnapshotCompression()) {
118+
LOG(INFO) << "Not writing verity and VABC is enabled, unmapping all "
119+
"partitions";
120+
dynamic_control_->UnmapAllPartitions();
121+
}
122+
115123
if (cancelled_)
116124
return;
117125
if (code == ErrorCode::kSuccess && HasOutputPipe())
@@ -130,6 +138,21 @@ bool FilesystemVerifierAction::InitializeFdVABC() {
130138
const InstallPlan::Partition& partition =
131139
install_plan_.partitions[partition_index_];
132140

141+
if (!ShouldWriteVerity()) {
142+
// In VABC, if we are not writing verity, just map all partitions,
143+
// and read using regular fd on |postinstall_mount_device| .
144+
// All read will go through snapuserd, which provides a consistent
145+
// view: device will use snapuserd to read partition during boot.
146+
// b/186196758
147+
// Call UnmapAllPartitions() first, because if we wrote verity before, these
148+
// writes won't be visible to previously opened snapuserd daemon. To ensure
149+
// that we will see the most up to date data from partitions, call Unmap()
150+
// then Map() to re-spin daemon.
151+
dynamic_control_->UnmapAllPartitions();
152+
dynamic_control_->MapAllPartitions();
153+
return InitializeFd(partition.readonly_target_path);
154+
}
155+
133156
// FilesystemVerifierAction need the read_fd_.
134157
partition_fd_ =
135158
dynamic_control_->OpenCowFd(partition.name, partition.source_path, true);

Diff for: payload_consumer/filesystem_verifier_action_unittest.cc

+48-24
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,23 @@ using testing::SetArgPointee;
4848
namespace chromeos_update_engine {
4949

5050
class FilesystemVerifierActionTest : public ::testing::Test {
51+
public:
52+
static constexpr size_t BLOCK_SIZE = 4096;
53+
static constexpr size_t PARTITION_SIZE = BLOCK_SIZE * 1024;
54+
5155
protected:
52-
void SetUp() override { loop_.SetAsCurrent(); }
56+
void SetUp() override {
57+
brillo::Blob part_data(PARTITION_SIZE);
58+
test_utils::FillWithData(&part_data);
59+
ASSERT_TRUE(utils::WriteFile(
60+
source_part.path().c_str(), part_data.data(), part_data.size()));
61+
// FillWithData() will will with different data next call. We want
62+
// source/target partitions to contain different data for testing.
63+
test_utils::FillWithData(&part_data);
64+
ASSERT_TRUE(utils::WriteFile(
65+
target_part.path().c_str(), part_data.data(), part_data.size()));
66+
loop_.SetAsCurrent();
67+
}
5368

5469
void TearDown() override {
5570
EXPECT_EQ(0, brillo::MessageLoopRunMaxIterations(&loop_, 1));
@@ -62,11 +77,34 @@ class FilesystemVerifierActionTest : public ::testing::Test {
6277
void BuildActions(const InstallPlan& install_plan,
6378
DynamicPartitionControlInterface* dynamic_control);
6479

80+
InstallPlan::Partition* AddFakePartition(InstallPlan* install_plan,
81+
std::string name = "fake_part") {
82+
InstallPlan::Partition& part = install_plan->partitions.emplace_back();
83+
part.name = name;
84+
part.target_path = target_part.path();
85+
part.readonly_target_path = part.target_path;
86+
part.target_size = PARTITION_SIZE;
87+
part.block_size = BLOCK_SIZE;
88+
part.source_path = source_part.path();
89+
EXPECT_TRUE(
90+
HashCalculator::RawHashOfFile(source_part.path(), &part.source_hash));
91+
EXPECT_TRUE(
92+
HashCalculator::RawHashOfFile(target_part.path(), &part.target_hash));
93+
return &part;
94+
}
95+
6596
brillo::FakeMessageLoop loop_{nullptr};
6697
ActionProcessor processor_;
6798
DynamicPartitionControlStub dynamic_control_stub_;
99+
static ScopedTempFile source_part;
100+
static ScopedTempFile target_part;
68101
};
69102

103+
ScopedTempFile FilesystemVerifierActionTest::source_part{
104+
"source_part.XXXXXX", false, PARTITION_SIZE};
105+
ScopedTempFile FilesystemVerifierActionTest::target_part{
106+
"target_part.XXXXXX", false, PARTITION_SIZE};
107+
70108
class FilesystemVerifierActionTestDelegate : public ActionProcessorDelegate {
71109
public:
72110
FilesystemVerifierActionTestDelegate()
@@ -406,32 +444,27 @@ TEST_F(FilesystemVerifierActionTest, RunAsRootSkipWriteVerityTest) {
406444

407445
TEST_F(FilesystemVerifierActionTest, RunWithVABCNoVerity) {
408446
InstallPlan install_plan;
409-
InstallPlan::Partition& part = install_plan.partitions.emplace_back();
410-
part.name = "fake_part";
411-
part.target_path = "/dev/fake_target_path";
412-
part.target_size = 4096 * 4096;
413-
part.block_size = 4096;
414-
part.source_path = "/dev/fake_source_path";
415-
part.fec_size = 0;
416-
part.hash_tree_size = 0;
417-
part.target_hash.clear();
418-
part.source_hash.clear();
447+
auto part_ptr = AddFakePartition(&install_plan);
448+
ASSERT_NE(part_ptr, nullptr);
449+
InstallPlan::Partition& part = *part_ptr;
450+
part.target_path = "Shouldn't attempt to open this path";
419451

420452
NiceMock<MockDynamicPartitionControl> dynamic_control;
421-
auto fake_fd = std::make_shared<FakeFileDescriptor>();
422453

423454
ON_CALL(dynamic_control, GetDynamicPartitionsFeatureFlag())
424455
.WillByDefault(Return(FeatureFlag(FeatureFlag::Value::LAUNCH)));
425456
ON_CALL(dynamic_control, UpdateUsesSnapshotCompression())
426457
.WillByDefault(Return(true));
427-
ON_CALL(dynamic_control, OpenCowFd(_, _, _)).WillByDefault(Return(fake_fd));
428458
ON_CALL(dynamic_control, IsDynamicPartition(part.name, _))
429459
.WillByDefault(Return(true));
430460

431461
EXPECT_CALL(dynamic_control, UpdateUsesSnapshotCompression())
432462
.Times(AtLeast(1));
463+
// Since we are not writing verity, we should not attempt to OpenCowFd()
464+
// reads should go through regular file descriptors on mapped partitions.
433465
EXPECT_CALL(dynamic_control, OpenCowFd(part.name, {part.source_path}, _))
434-
.Times(1);
466+
.Times(0);
467+
EXPECT_CALL(dynamic_control, MapAllPartitions()).Times(AtLeast(1));
435468
EXPECT_CALL(dynamic_control, ListDynamicPartitionsForSlot(_, _, _))
436469
.WillRepeatedly(
437470
DoAll(SetArgPointee<2, std::vector<std::string>>({part.name}),
@@ -451,16 +484,7 @@ TEST_F(FilesystemVerifierActionTest, RunWithVABCNoVerity) {
451484

452485
ASSERT_FALSE(processor_.IsRunning());
453486
ASSERT_TRUE(delegate.ran());
454-
// Filesystem verifier will fail, because we set an empty hash
455-
ASSERT_EQ(ErrorCode::kNewRootfsVerificationError, delegate.code());
456-
const auto& read_pos = fake_fd->GetReadOps();
457-
size_t expected_offset = 0;
458-
for (const auto& [off, size] : read_pos) {
459-
ASSERT_EQ(off, expected_offset);
460-
expected_offset += size;
461-
}
462-
const auto actual_read_size = expected_offset;
463-
ASSERT_EQ(actual_read_size, part.target_size);
487+
ASSERT_EQ(ErrorCode::kSuccess, delegate.code());
464488
}
465489

466490
TEST_F(FilesystemVerifierActionTest, ReadAfterWrite) {

0 commit comments

Comments
 (0)