// // Copyright (C) 2012 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // #include "update_engine/payload_consumer/filesystem_verifier_action.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "update_engine/common/dynamic_partition_control_stub.h" #include "update_engine/common/hash_calculator.h" #include "update_engine/common/mock_dynamic_partition_control.h" #include "update_engine/common/test_utils.h" #include "update_engine/common/utils.h" #include "update_engine/payload_consumer/fake_file_descriptor.h" #include "update_engine/payload_consumer/install_plan.h" #include "update_engine/payload_consumer/verity_writer_android.h" using brillo::MessageLoop; using std::string; using testing::_; using testing::AtLeast; using testing::DoAll; using testing::NiceMock; using testing::Return; using testing::SetArgPointee; namespace chromeos_update_engine { class FilesystemVerifierActionTest : public ::testing::Test { public: static constexpr size_t BLOCK_SIZE = 4096; // We use SHA256 for testing, so hash size is 256bits / 8 static constexpr size_t HASH_SIZE = 256 / 8; static constexpr size_t PARTITION_SIZE = BLOCK_SIZE * 1024; static constexpr size_t HASH_TREE_START_OFFSET = 800 * BLOCK_SIZE; size_t hash_tree_size = 0; size_t fec_start_offset = 0; size_t fec_data_size = 0; static constexpr size_t FEC_ROOTS = 2; size_t fec_rounds = 0; size_t fec_size = 0; protected: void SetUp() override { hash_tree_size = HashTreeBuilder::CalculateSize( HASH_TREE_START_OFFSET, BLOCK_SIZE, HASH_SIZE); fec_start_offset = HASH_TREE_START_OFFSET + hash_tree_size; fec_data_size = fec_start_offset; static constexpr size_t FEC_ROOTS = 2; fec_rounds = utils::DivRoundUp(fec_data_size / BLOCK_SIZE, FEC_RSM - FEC_ROOTS); fec_size = fec_rounds * FEC_ROOTS * BLOCK_SIZE; fec_data_.resize(fec_size); hash_tree_data_.resize(hash_tree_size); // Globally readable writable, as we want to write data ASSERT_EQ(0, fchmod(source_part_.fd(), 0666)) << " Failed to set " << source_part_.path() << " as writable " << strerror(errno); ASSERT_EQ(0, fchmod(target_part_.fd(), 0666)) << " Failed to set " << target_part_.path() << " as writable " << strerror(errno); brillo::Blob part_data(PARTITION_SIZE); test_utils::FillWithData(&part_data); ASSERT_TRUE(utils::WriteFile( source_part_.path().c_str(), part_data.data(), part_data.size())); // FillWithData() will fill with different data next call. We want // source/target partitions to contain different data for testing. test_utils::FillWithData(&part_data); ASSERT_TRUE(utils::WriteFile( target_part_.path().c_str(), part_data.data(), part_data.size())); loop_.SetAsCurrent(); } void TearDown() override { EXPECT_EQ(0, brillo::MessageLoopRunMaxIterations(&loop_, 1)); } void DoTestVABC(bool clear_target_hash, bool enable_verity); // Returns true iff test has completed successfully. bool DoTest(bool terminate_early, bool hash_fail); void BuildActions(const InstallPlan& install_plan); void BuildActions(const InstallPlan& install_plan, DynamicPartitionControlInterface* dynamic_control); InstallPlan::Partition* AddFakePartition(InstallPlan* install_plan, std::string name = "fake_part") { InstallPlan::Partition& part = install_plan->partitions.emplace_back(); part.name = name; part.target_path = target_part_.path(); part.readonly_target_path = part.target_path; part.target_size = PARTITION_SIZE; part.block_size = BLOCK_SIZE; part.source_path = source_part_.path(); part.source_size = PARTITION_SIZE; EXPECT_TRUE( HashCalculator::RawHashOfFile(source_part_.path(), &part.source_hash)); EXPECT_TRUE( HashCalculator::RawHashOfFile(target_part_.path(), &part.target_hash)); return ∂ } static void ZeroRange(FileDescriptorPtr fd, size_t start_block, size_t num_blocks) { std::vector buffer(BLOCK_SIZE); ASSERT_EQ((ssize_t)(start_block * BLOCK_SIZE), fd->Seek(start_block * BLOCK_SIZE, SEEK_SET)); for (size_t i = 0; i < num_blocks; i++) { ASSERT_TRUE(utils::WriteAll(fd, buffer.data(), buffer.size())); } } void SetHashWithVerity(InstallPlan::Partition* partition) { partition->hash_tree_algorithm = "sha256"; partition->hash_tree_size = hash_tree_size; partition->hash_tree_offset = HASH_TREE_START_OFFSET; partition->hash_tree_data_offset = 0; partition->hash_tree_data_size = HASH_TREE_START_OFFSET; partition->fec_size = fec_size; partition->fec_offset = fec_start_offset; partition->fec_data_offset = 0; partition->fec_data_size = fec_data_size; partition->fec_roots = FEC_ROOTS; VerityWriterAndroid verity_writer; ASSERT_TRUE(verity_writer.Init(*partition)); LOG(INFO) << "Opening " << partition->readonly_target_path; auto fd = std::make_shared(); ASSERT_TRUE(fd->Open(partition->readonly_target_path.c_str(), O_RDWR)) << "Failed to open " << partition->target_path.c_str() << " " << strerror(errno); std::vector buffer(BLOCK_SIZE); // Only need to read up to hash tree auto bytes_to_read = HASH_TREE_START_OFFSET; auto offset = 0; while (bytes_to_read > 0) { const auto bytes_read = fd->Read( buffer.data(), std::min(buffer.size(), bytes_to_read)); ASSERT_GT(bytes_read, 0) << "offset: " << offset << " bytes to read: " << bytes_to_read << " error: " << strerror(errno); ASSERT_TRUE(verity_writer.Update(offset, buffer.data(), bytes_read)); bytes_to_read -= bytes_read; offset += bytes_read; } ASSERT_TRUE(verity_writer.Finalize(fd, fd)); ASSERT_TRUE(fd->IsOpen()); ASSERT_TRUE(HashCalculator::RawHashOfFile(target_part_.path(), &partition->target_hash)); ASSERT_TRUE(fd->Seek(HASH_TREE_START_OFFSET, SEEK_SET)); ASSERT_EQ(fd->Read(hash_tree_data_.data(), hash_tree_data_.size()), static_cast(hash_tree_data_.size())) << "Failed to read hashtree " << strerror(errno); ASSERT_TRUE(fd->Seek(fec_start_offset, SEEK_SET)); ASSERT_EQ(fd->Read(fec_data_.data(), fec_data_.size()), static_cast(fec_data_.size())) << "Failed to read FEC " << strerror(errno); // Fs verification action is expected to write them, so clear verity data to // ensure that they are re-created correctly. ZeroRange( fd, HASH_TREE_START_OFFSET / BLOCK_SIZE, hash_tree_size / BLOCK_SIZE); ZeroRange(fd, fec_start_offset / BLOCK_SIZE, fec_size / BLOCK_SIZE); } brillo::FakeMessageLoop loop_{nullptr}; ActionProcessor processor_; DynamicPartitionControlStub dynamic_control_stub_; std::vector fec_data_; std::vector hash_tree_data_; static ScopedTempFile source_part_; static ScopedTempFile target_part_; InstallPlan install_plan_; }; ScopedTempFile FilesystemVerifierActionTest::source_part_{ "source_part.XXXXXX", true, PARTITION_SIZE}; ScopedTempFile FilesystemVerifierActionTest::target_part_{ "target_part.XXXXXX", true, PARTITION_SIZE}; static void EnableVABC(MockDynamicPartitionControl* dynamic_control, const std::string& part_name) { ON_CALL(*dynamic_control, GetDynamicPartitionsFeatureFlag()) .WillByDefault(Return(FeatureFlag(FeatureFlag::Value::LAUNCH))); ON_CALL(*dynamic_control, UpdateUsesSnapshotCompression()) .WillByDefault(Return(true)); ON_CALL(*dynamic_control, IsDynamicPartition(part_name, _)) .WillByDefault(Return(true)); } class FilesystemVerifierActionTestDelegate : public ActionProcessorDelegate { public: FilesystemVerifierActionTestDelegate() : ran_(false), code_(ErrorCode::kError) {} void ProcessingDone(const ActionProcessor* processor, ErrorCode code) { MessageLoop::current()->BreakLoop(); } void ProcessingStopped(const ActionProcessor* processor) { MessageLoop::current()->BreakLoop(); } void ActionCompleted(ActionProcessor* processor, AbstractAction* action, ErrorCode code) { if (action->Type() == FilesystemVerifierAction::StaticType()) { ran_ = true; code_ = code; EXPECT_FALSE( static_cast(action)->partition_fd_); } else if (action->Type() == ObjectCollectorAction::StaticType()) { auto collector_action = static_cast*>(action); install_plan_.reset(new InstallPlan(collector_action->object())); } } bool ran() const { return ran_; } ErrorCode code() const { return code_; } std::unique_ptr install_plan_; private: bool ran_; ErrorCode code_; }; bool FilesystemVerifierActionTest::DoTest(bool terminate_early, bool hash_fail) { ScopedTempFile a_loop_file("a_loop_file.XXXXXX"); // Make random data for a. const size_t kLoopFileSize = 10 * 1024 * 1024 + 512; brillo::Blob a_loop_data(kLoopFileSize); test_utils::FillWithData(&a_loop_data); // Write data to disk if (!(test_utils::WriteFileVector(a_loop_file.path(), a_loop_data))) { ADD_FAILURE(); return false; } // Attach loop devices to the files string a_dev; test_utils::ScopedLoopbackDeviceBinder a_dev_releaser( a_loop_file.path(), false, &a_dev); if (!(a_dev_releaser.is_bound())) { ADD_FAILURE(); return false; } LOG(INFO) << "verifying: " << a_loop_file.path() << " (" << a_dev << ")"; bool success = true; // Set up the action objects install_plan_.source_slot = 0; install_plan_.target_slot = 1; InstallPlan::Partition part; part.name = "part"; part.target_size = kLoopFileSize - (hash_fail ? 1 : 0); part.target_path = a_dev; if (!HashCalculator::RawHashOfData(a_loop_data, &part.target_hash)) { ADD_FAILURE(); success = false; } part.source_size = kLoopFileSize; part.source_path = a_dev; if (!HashCalculator::RawHashOfData(a_loop_data, &part.source_hash)) { ADD_FAILURE(); success = false; } install_plan_.partitions = {part}; BuildActions(install_plan_); FilesystemVerifierActionTestDelegate delegate; processor_.set_delegate(&delegate); loop_.PostTask(base::Bind(&ActionProcessor::StartProcessing, base::Unretained(&processor_))); if (terminate_early) { loop_.PostTask(base::Bind(&ActionProcessor::StopProcessing, base::Unretained(&processor_))); } loop_.Run(); if (!terminate_early) { bool is_delegate_ran = delegate.ran(); EXPECT_TRUE(is_delegate_ran); success = success && is_delegate_ran; } else { EXPECT_EQ(ErrorCode::kError, delegate.code()); return (ErrorCode::kError == delegate.code()); } if (hash_fail) { ErrorCode expected_exit_code = ErrorCode::kNewRootfsVerificationError; EXPECT_EQ(expected_exit_code, delegate.code()); return (expected_exit_code == delegate.code()); } EXPECT_EQ(ErrorCode::kSuccess, delegate.code()); // Make sure everything in the out_image is there brillo::Blob a_out; if (!utils::ReadFile(a_dev, &a_out)) { ADD_FAILURE(); return false; } const bool is_a_file_reading_eq = test_utils::ExpectVectorsEq(a_loop_data, a_out); EXPECT_TRUE(is_a_file_reading_eq); success = success && is_a_file_reading_eq; bool is_install_plan_eq = (*delegate.install_plan_ == install_plan_); EXPECT_TRUE(is_install_plan_eq); success = success && is_install_plan_eq; return success; } void FilesystemVerifierActionTest::BuildActions( const InstallPlan& install_plan, DynamicPartitionControlInterface* dynamic_control) { auto feeder_action = std::make_unique>(); auto verifier_action = std::make_unique(dynamic_control); auto collector_action = std::make_unique>(); feeder_action->set_obj(install_plan); BondActions(feeder_action.get(), verifier_action.get()); BondActions(verifier_action.get(), collector_action.get()); processor_.EnqueueAction(std::move(feeder_action)); processor_.EnqueueAction(std::move(verifier_action)); processor_.EnqueueAction(std::move(collector_action)); } void FilesystemVerifierActionTest::BuildActions( const InstallPlan& install_plan) { BuildActions(install_plan, &dynamic_control_stub_); } class FilesystemVerifierActionTest2Delegate : public ActionProcessorDelegate { public: void ActionCompleted(ActionProcessor* processor, AbstractAction* action, ErrorCode code) { if (action->Type() == FilesystemVerifierAction::StaticType()) { ran_ = true; code_ = code; } } bool ran_; ErrorCode code_; }; TEST_F(FilesystemVerifierActionTest, MissingInputObjectTest) { auto copier_action = std::make_unique(&dynamic_control_stub_); auto collector_action = std::make_unique>(); BondActions(copier_action.get(), collector_action.get()); processor_.EnqueueAction(std::move(copier_action)); processor_.EnqueueAction(std::move(collector_action)); FilesystemVerifierActionTest2Delegate delegate; processor_.set_delegate(&delegate); processor_.StartProcessing(); ASSERT_FALSE(processor_.IsRunning()); ASSERT_TRUE(delegate.ran_); EXPECT_EQ(ErrorCode::kError, delegate.code_); } TEST_F(FilesystemVerifierActionTest, NonExistentDriveTest) { InstallPlan::Partition part; part.name = "nope"; part.source_path = "/no/such/file"; part.target_path = "/no/such/file"; install_plan_.partitions = {part}; BuildActions(install_plan_); FilesystemVerifierActionTest2Delegate delegate; processor_.set_delegate(&delegate); processor_.StartProcessing(); EXPECT_FALSE(processor_.IsRunning()); EXPECT_TRUE(delegate.ran_); EXPECT_EQ(ErrorCode::kFilesystemVerifierError, delegate.code_); } TEST_F(FilesystemVerifierActionTest, RunAsRootVerifyHashTest) { ASSERT_EQ(0U, getuid()); EXPECT_TRUE(DoTest(false, false)); } TEST_F(FilesystemVerifierActionTest, RunAsRootVerifyHashFailTest) { ASSERT_EQ(0U, getuid()); EXPECT_TRUE(DoTest(false, true)); } TEST_F(FilesystemVerifierActionTest, RunAsRootTerminateEarlyTest) { ASSERT_EQ(0U, getuid()); EXPECT_TRUE(DoTest(true, false)); // TerminateEarlyTest may leak some null callbacks from the Stream class. while (loop_.RunOnce(false)) { } } #ifdef __ANDROID__ TEST_F(FilesystemVerifierActionTest, RunAsRootWriteVerityTest) { ScopedTempFile part_file("part_file.XXXXXX"); constexpr size_t filesystem_size = 200 * 4096; constexpr size_t part_size = 256 * 4096; brillo::Blob part_data(filesystem_size, 0x1); part_data.resize(part_size); ASSERT_TRUE(test_utils::WriteFileVector(part_file.path(), part_data)); string target_path; test_utils::ScopedLoopbackDeviceBinder target_device( part_file.path(), true, &target_path); InstallPlan::Partition part; part.name = "part"; part.target_path = target_path; part.target_size = part_size; part.block_size = 4096; part.hash_tree_algorithm = "sha1"; part.hash_tree_data_offset = 0; part.hash_tree_data_size = filesystem_size; part.hash_tree_offset = filesystem_size; part.hash_tree_size = 3 * 4096; part.fec_data_offset = 0; part.fec_data_size = filesystem_size + part.hash_tree_size; part.fec_offset = part.fec_data_size; part.fec_size = 2 * 4096; part.fec_roots = 2; // for i in {1..$((200 * 4096))}; do echo -n -e '\x1' >> part; done // avbtool add_hashtree_footer --image part --partition_size $((256 * 4096)) // --partition_name part --do_not_append_vbmeta_image // --output_vbmeta_image vbmeta // truncate -s $((256 * 4096)) part // sha256sum part | xxd -r -p | hexdump -v -e '/1 "0x%02x, "' part.target_hash = {0x28, 0xd4, 0x96, 0x75, 0x4c, 0xf5, 0x8a, 0x3e, 0x31, 0x85, 0x08, 0x92, 0x85, 0x62, 0xf0, 0x37, 0xbc, 0x8d, 0x7e, 0xa4, 0xcb, 0x24, 0x18, 0x7b, 0xf3, 0xeb, 0xb5, 0x8d, 0x6f, 0xc8, 0xd8, 0x1a}; // avbtool info_image --image vbmeta | grep Salt | cut -d':' -f 2 | // xxd -r -p | hexdump -v -e '/1 "0x%02x, "' part.hash_tree_salt = {0x9e, 0xcb, 0xf8, 0xd5, 0x0b, 0xb4, 0x43, 0x0a, 0x7a, 0x10, 0xad, 0x96, 0xd7, 0x15, 0x70, 0xba, 0xed, 0x27, 0xe2, 0xae}; install_plan_.partitions = {part}; BuildActions(install_plan_); FilesystemVerifierActionTestDelegate delegate; processor_.set_delegate(&delegate); loop_.PostTask( FROM_HERE, base::Bind( [](ActionProcessor* processor) { processor->StartProcessing(); }, base::Unretained(&processor_))); loop_.Run(); EXPECT_FALSE(processor_.IsRunning()); EXPECT_TRUE(delegate.ran()); EXPECT_EQ(ErrorCode::kSuccess, delegate.code()); } #endif // __ANDROID__ TEST_F(FilesystemVerifierActionTest, RunAsRootSkipWriteVerityTest) { ScopedTempFile part_file("part_file.XXXXXX"); constexpr size_t filesystem_size = 200 * 4096; constexpr size_t part_size = 256 * 4096; brillo::Blob part_data(part_size); test_utils::FillWithData(&part_data); ASSERT_TRUE(test_utils::WriteFileVector(part_file.path(), part_data)); string target_path; test_utils::ScopedLoopbackDeviceBinder target_device( part_file.path(), true, &target_path); install_plan_.write_verity = false; InstallPlan::Partition part; part.name = "part"; part.target_path = target_path; part.target_size = part_size; part.block_size = 4096; part.hash_tree_data_offset = 0; part.hash_tree_data_size = filesystem_size; part.hash_tree_offset = filesystem_size; part.hash_tree_size = 3 * 4096; part.fec_data_offset = 0; part.fec_data_size = filesystem_size + part.hash_tree_size; part.fec_offset = part.fec_data_size; part.fec_size = 2 * 4096; EXPECT_TRUE(HashCalculator::RawHashOfData(part_data, &part.target_hash)); install_plan_.partitions = {part}; BuildActions(install_plan_); FilesystemVerifierActionTestDelegate delegate; processor_.set_delegate(&delegate); loop_.PostTask( FROM_HERE, base::Bind( [](ActionProcessor* processor) { processor->StartProcessing(); }, base::Unretained(&processor_))); loop_.Run(); ASSERT_FALSE(processor_.IsRunning()); ASSERT_TRUE(delegate.ran()); ASSERT_EQ(ErrorCode::kSuccess, delegate.code()); } void FilesystemVerifierActionTest::DoTestVABC(bool clear_target_hash, bool enable_verity) { auto part_ptr = AddFakePartition(&install_plan_); if (::testing::Test::HasFailure()) { return; } ASSERT_NE(part_ptr, nullptr); InstallPlan::Partition& part = *part_ptr; part.target_path = "Shouldn't attempt to open this path"; if (enable_verity) { install_plan_.write_verity = true; ASSERT_NO_FATAL_FAILURE(SetHashWithVerity(&part)); } if (clear_target_hash) { part.target_hash.clear(); } NiceMock dynamic_control; EnableVABC(&dynamic_control, part.name); auto open_cow = [part]() { auto cow_fd = std::make_shared(); EXPECT_TRUE(cow_fd->Open(part.readonly_target_path.c_str(), O_RDWR)) << "Failed to open part " << part.readonly_target_path << strerror(errno); return cow_fd; }; EXPECT_CALL(dynamic_control, UpdateUsesSnapshotCompression()) .Times(AtLeast(1)); auto cow_fd = open_cow(); if (HasFailure()) { return; } if (enable_verity) { ON_CALL(dynamic_control, OpenCowFd(part.name, {part.source_path}, _)) .WillByDefault(open_cow); EXPECT_CALL(dynamic_control, OpenCowFd(part.name, {part.source_path}, _)) .Times(AtLeast(1)); // fs verification isn't supposed to write to |readonly_target_path|. All // writes should go through fd returned by |OpenCowFd|. Therefore we set // target part as read-only to make sure. ASSERT_EQ(0, chmod(part.readonly_target_path.c_str(), 0444)) << " Failed to set " << part.readonly_target_path << " as read-only " << strerror(errno); } else { // Since we are not writing verity, we should not attempt to OpenCowFd() // reads should go through regular file descriptors on mapped partitions. EXPECT_CALL(dynamic_control, OpenCowFd(part.name, {part.source_path}, _)) .Times(0); EXPECT_CALL(dynamic_control, MapAllPartitions()).Times(AtLeast(1)); } EXPECT_CALL(dynamic_control, ListDynamicPartitionsForSlot(_, _, _)) .WillRepeatedly( DoAll(SetArgPointee<2, std::vector>({part.name}), Return(true))); BuildActions(install_plan_, &dynamic_control); FilesystemVerifierActionTestDelegate delegate; processor_.set_delegate(&delegate); loop_.PostTask(FROM_HERE, base::Bind(&ActionProcessor::StartProcessing, base::Unretained(&processor_))); loop_.Run(); ASSERT_FALSE(processor_.IsRunning()); ASSERT_TRUE(delegate.ran()); if (enable_verity) { std::vector actual_fec(fec_size); ssize_t bytes_read = 0; ASSERT_TRUE(utils::PReadAll(cow_fd, actual_fec.data(), actual_fec.size(), fec_start_offset, &bytes_read)); ASSERT_EQ(actual_fec, fec_data_); std::vector actual_hash_tree(hash_tree_size); ASSERT_TRUE(utils::PReadAll(cow_fd, actual_hash_tree.data(), actual_hash_tree.size(), HASH_TREE_START_OFFSET, &bytes_read)); ASSERT_EQ(actual_hash_tree, hash_tree_data_); } if (clear_target_hash) { ASSERT_EQ(ErrorCode::kNewRootfsVerificationError, delegate.code()); } else { ASSERT_EQ(ErrorCode::kSuccess, delegate.code()); } } TEST_F(FilesystemVerifierActionTest, VABC_NoVerity_Success) { DoTestVABC(false, false); } TEST_F(FilesystemVerifierActionTest, VABC_NoVerity_Target_Mismatch) { DoTestVABC(true, false); } TEST_F(FilesystemVerifierActionTest, VABC_Verity_Success) { DoTestVABC(false, true); } TEST_F(FilesystemVerifierActionTest, VABC_Verity_ReadAfterWrite) { ASSERT_NO_FATAL_FAILURE(DoTestVABC(false, true)); // Run FS verification again, w/o writing verity. We have seen a bug where // attempting to run fs again will cause previously written verity data to be // dropped, so cover this scenario. ASSERT_GE(install_plan_.partitions.size(), 1UL); auto& part = install_plan_.partitions[0]; install_plan_.write_verity = false; part.readonly_target_path = target_part_.path(); NiceMock dynamic_control; EnableVABC(&dynamic_control, part.name); // b/186196758 is only visible if we repeatedely run FS verification w/o // writing verity for (int i = 0; i < 3; i++) { BuildActions(install_plan_, &dynamic_control); FilesystemVerifierActionTestDelegate delegate; processor_.set_delegate(&delegate); loop_.PostTask( FROM_HERE, base::Bind( [](ActionProcessor* processor) { processor->StartProcessing(); }, base::Unretained(&processor_))); loop_.Run(); ASSERT_FALSE(processor_.IsRunning()); ASSERT_TRUE(delegate.ran()); ASSERT_EQ(ErrorCode::kSuccess, delegate.code()); } } TEST_F(FilesystemVerifierActionTest, VABC_Verity_Target_Mismatch) { DoTestVABC(true, true); } } // namespace chromeos_update_engine