| // | |
| // Copyright (C) 2021 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 <algorithm> | |
| #include <memory> | |
| #include <string> | |
| #include <utility> | |
| #include <fcntl.h> | |
| #include <sys/sendfile.h> | |
| #include <unistd.h> | |
| #include <android-base/strings.h> | |
| #include <brillo/data_encoding.h> | |
| #include <brillo/message_loops/fake_message_loop.h> | |
| #include <bsdiff/bsdiff.h> | |
| #include <gtest/gtest.h> | |
| #include <liblp/builder.h> | |
| #include <fs_mgr.h> | |
| #include <liblp/liblp.h> | |
| #include "update_engine/aosp/boot_control_android.h" | |
| #include "update_engine/aosp/daemon_state_android.h" | |
| #include "update_engine/aosp/update_attempter_android.h" | |
| #include "update_engine/common/constants.h" | |
| #include "update_engine/common/fake_boot_control.h" | |
| #include "update_engine/common/fake_hardware.h" | |
| #include "update_engine/common/hash_calculator.h" | |
| #include "update_engine/common/prefs.h" | |
| #include "update_engine/common/test_utils.h" | |
| #include "update_engine/common/testing_constants.h" | |
| #include "update_engine/common/utils.h" | |
| #include "update_engine/payload_consumer/file_descriptor.h" | |
| #include "update_engine/payload_consumer/install_plan.h" | |
| #include "update_engine/payload_consumer/payload_constants.h" | |
| #include "update_engine/payload_generator/delta_diff_generator.h" | |
| #include "update_engine/payload_generator/extent_ranges.h" | |
| #include "update_engine/payload_generator/payload_file.h" | |
| #include "update_engine/payload_generator/payload_signer.h" | |
| #include "update_engine/update_metadata.pb.h" | |
| #include "update_engine/update_status_utils.h" | |
| namespace chromeos_update_engine { | |
| class UpdateAttempterAndroidIntegrationTest : public ::testing::Test, | |
| public ServiceObserverInterface { | |
| void AddFakePartitionGroup() { | |
| dynamic_control_ = boot_control_.dynamic_control_.get(); | |
| auto super_device = dynamic_control_->GetSuperDevice(); | |
| ASSERT_TRUE(super_device.has_value()); | |
| super_device_ = super_device->value(); | |
| builder_ = android::fs_mgr::MetadataBuilder::New( | |
| super_device->value(), boot_control_.GetCurrentSlot()); | |
| ASSERT_NE(builder_, nullptr); | |
| // Remove dangling fake partitions, if test crashed before they might not | |
| // get cleaned up properly. | |
| RemoveFakePartitionGroup(); | |
| ASSERT_TRUE(builder_->AddGroup("fake_group", kFakePartitionSize * 2)); | |
| ExportPartitionTable(); | |
| } | |
| void RemoveFakePartitionGroup() { | |
| builder_->RemovePartition("fake_a"); | |
| builder_->RemovePartition("fake_b"); | |
| builder_->RemoveGroupAndPartitions("fake_group"); | |
| } | |
| // Fill partition |path| with arbitrary data. | |
| void FillPartition(const std::string& path, const bool is_source) { | |
| std::array<uint8_t, kBlockSize> data; | |
| EintrSafeFileDescriptor fd; | |
| fd.Open(path.c_str(), O_RDWR); | |
| for (size_t i = 0; i < kFakePartitionSize / kBlockSize; i++) { | |
| if (is_source) { | |
| std::fill(data.begin(), data.end(), i); | |
| } else { | |
| std::fill( | |
| data.begin(), data.end(), kFakePartitionSize / kBlockSize - i - 1); | |
| } | |
| fd.Write(data.data(), kBlockSize); | |
| } | |
| } | |
| void SetUp() override { | |
| FillPartition(old_part_.path(), true); | |
| FillPartition(new_part_.path(), false); | |
| ASSERT_TRUE(boot_control_.Init()); | |
| if (!DynamicPartitionEnabled()) { | |
| return; | |
| } | |
| ASSERT_NO_FATAL_FAILURE(AddFakePartitionGroup()); | |
| message_loop_.SetAsCurrent(); | |
| // Set official build to false so that hash checks are non-mandatory | |
| hardware_.SetIsOfficialBuild(false); | |
| truncate64(blob_file_.path().c_str(), 0); | |
| update_attempter_android_.set_update_certificates_path( | |
| test_utils::GetBuildArtifactsPath(kUnittestOTACertsPath)); | |
| // Basic setup to create VABC OTA | |
| manifest_.set_partial_update(true); | |
| manifest_.set_minor_version(kPartialUpdateMinorPayloadVersion); | |
| auto dap_group = | |
| manifest_.mutable_dynamic_partition_metadata()->add_groups(); | |
| dap_group->set_name("fake_group"); | |
| dap_group->add_partition_names("fake"); | |
| manifest_.mutable_dynamic_partition_metadata()->set_snapshot_enabled(true); | |
| manifest_.mutable_dynamic_partition_metadata()->set_vabc_enabled(true); | |
| manifest_.mutable_dynamic_partition_metadata()->set_cow_version( | |
| android::snapshot::kCowVersionMajor); | |
| } | |
| void TearDown() override { | |
| if (!builder_ || !DynamicPartitionEnabled()) { | |
| return; | |
| } | |
| auto dynamic_control = boot_control_.GetDynamicPartitionControl(); | |
| if (dynamic_control) { | |
| dynamic_control->UnmapAllPartitions(); | |
| dynamic_control->ResetUpdate(&prefs_); | |
| } | |
| RemoveFakePartitionGroup(); | |
| ExportPartitionTable(); | |
| } | |
| void CreateFakePartition() { | |
| // Create a fake partition for testing purposes | |
| auto partition_a = builder_->AddPartition( | |
| boot_control_.GetCurrentSlot() == 0 ? "fake_a" : "fake_b", | |
| "fake_group", | |
| 0); | |
| ASSERT_NE(partition_a, nullptr); | |
| ASSERT_TRUE(builder_->ResizePartition(partition_a, kFakePartitionSize)); | |
| ExportPartitionTable(); | |
| std::string source_part; | |
| ASSERT_TRUE( | |
| dynamic_control_->GetPartitionDevice("fake", | |
| boot_control_.GetCurrentSlot(), | |
| boot_control_.GetCurrentSlot(), | |
| &source_part)); | |
| int out_fd = open(source_part.c_str(), O_RDWR); | |
| ScopedFdCloser closer{&out_fd}; | |
| ASSERT_GE(out_fd, 0) << android::base::ErrnoNumberAsString(errno); | |
| ASSERT_TRUE(utils::SendFile(out_fd, old_part_.fd(), kFakePartitionSize)); | |
| } | |
| void SendStatusUpdate( | |
| const update_engine::UpdateEngineStatus& update_engine_status) override { | |
| LOG(INFO) << UpdateStatusToString(update_engine_status.status) << ", " | |
| << update_engine_status.progress; | |
| } | |
| // Called whenever an update attempt is completed. | |
| void SendPayloadApplicationComplete(ErrorCode error_code) override { | |
| completion_code_ = error_code; | |
| } | |
| void ExportPartitionTable() { | |
| auto metadata = builder_->Export(); | |
| ASSERT_NE(metadata, nullptr); | |
| android::fs_mgr::UpdatePartitionTable( | |
| super_device_, *metadata, boot_control_.GetCurrentSlot()); | |
| } | |
| public: | |
| bool DynamicPartitionEnabled() { | |
| auto dynamic_control = boot_control_.GetDynamicPartitionControl(); | |
| return dynamic_control && | |
| dynamic_control->GetDynamicPartitionsFeatureFlag().IsEnabled(); | |
| } | |
| void AddSignatureInfoToPayload(DeltaArchiveManifest* manifest, | |
| const std::string& private_key_path) { | |
| size_t total_blob_size = 0; | |
| for (const auto& part : manifest->partitions()) { | |
| for (const auto& op : part.operations()) { | |
| if (!op.has_data_offset()) | |
| continue; | |
| ASSERT_EQ(total_blob_size, op.data_offset()) | |
| << "Ops not ordered by blob isze"; | |
| total_blob_size += op.data_length(); | |
| } | |
| } | |
| // Signatures appear at the end of the blobs. Note the offset in the | |
| // |manifest_|. | |
| uint64_t signature_blob_length = 0; | |
| if (!private_key_path.empty()) { | |
| ASSERT_TRUE(PayloadSigner::SignatureBlobLength({private_key_path}, | |
| &signature_blob_length)); | |
| PayloadSigner::AddSignatureToManifest( | |
| total_blob_size, signature_blob_length, manifest); | |
| } | |
| } | |
| // Generate blob data according to ops specified in the manifest. | |
| // Also update |new_part_|'s content to match expectation of ops. | |
| void HydratePayload(DeltaArchiveManifest* manifest) { | |
| for (auto& partition : *manifest->mutable_partitions()) { | |
| for (auto& op : *partition.mutable_operations()) { | |
| if (op.type() == InstallOperation::REPLACE) { | |
| ASSERT_GE(lseek64(blob_file_.fd(), op.data_offset(), SEEK_SET), 0); | |
| ASSERT_TRUE(utils::SendFile( | |
| new_part_.fd(), blob_file_.fd(), op.data_length())); | |
| } else if (op.type() == InstallOperation::BROTLI_BSDIFF) { | |
| brillo::Blob old_data; | |
| ASSERT_TRUE(utils::ReadExtents( | |
| old_part_.path(), op.src_extents(), &old_data, kBlockSize)) | |
| << "Failed to read source data: " | |
| << android::base::ErrnoNumberAsString(errno); | |
| brillo::Blob new_data; | |
| ASSERT_TRUE(utils::ReadExtents( | |
| new_part_.path(), op.dst_extents(), &new_data, kBlockSize)) | |
| << "Failed to read target data: " | |
| << android::base::ErrnoNumberAsString(errno); | |
| ScopedTempFile patch_file{"bspatch.XXXXXX", true}; | |
| ASSERT_EQ(bsdiff::bsdiff(old_data.data(), | |
| old_data.size(), | |
| new_data.data(), | |
| new_data.size(), | |
| patch_file.path().c_str(), | |
| nullptr), | |
| 0); | |
| op.set_data_length(utils::FileSize(patch_file.fd())); | |
| const auto offset = lseek64(blob_file_.fd(), 0, SEEK_CUR); | |
| ASSERT_GE(offset, 0); | |
| op.set_data_offset(offset); | |
| brillo::Blob src_data_hash; | |
| HashCalculator::RawHashOfData(old_data, &src_data_hash); | |
| op.set_src_sha256_hash(src_data_hash.data(), src_data_hash.size()); | |
| utils::SendFile(blob_file_.fd(), patch_file.fd(), op.data_length()); | |
| } else if (op.type() == InstallOperation::ZERO) { | |
| auto zero = utils::GetReadonlyZeroString( | |
| utils::BlocksInExtents(op.dst_extents()) * kBlockSize); | |
| for (const auto& ext : op.dst_extents()) { | |
| utils::PWriteAll(new_part_.fd(), | |
| zero.data(), | |
| ext.num_blocks() * kBlockSize, | |
| ext.start_block() * kBlockSize); | |
| } | |
| } else if (op.type() == InstallOperation::SOURCE_COPY) { | |
| brillo::Blob data; | |
| ASSERT_TRUE(utils::ReadExtents( | |
| old_part_.path(), op.src_extents(), &data, kBlockSize)); | |
| ASSERT_TRUE(utils::WriteExtents( | |
| new_part_.path(), op.dst_extents(), data, kBlockSize)); | |
| } else { | |
| FAIL() << "Unsupported install op type: " << op.type(); | |
| } | |
| } | |
| } | |
| } | |
| void ApplyPayload(DeltaArchiveManifest* manifest) { | |
| ASSERT_FALSE(manifest->partitions().empty()); | |
| ASSERT_NO_FATAL_FAILURE(HydratePayload(manifest)); | |
| const auto private_key_path = | |
| test_utils::GetBuildArtifactsPath(kUnittestPrivateKeyPath); | |
| ASSERT_NO_FATAL_FAILURE( | |
| AddSignatureInfoToPayload(manifest, private_key_path)); | |
| brillo::Blob hash; | |
| HashCalculator::RawHashOfFile(new_part_.path(), &hash); | |
| auto partition = &manifest->mutable_partitions()->at(0); | |
| partition->mutable_new_partition_info()->set_size(kFakePartitionSize); | |
| partition->mutable_new_partition_info()->set_hash(hash.data(), hash.size()); | |
| const bool source_exist = | |
| std::any_of(partition->operations().begin(), | |
| partition->operations().end(), | |
| [](const auto& op) { return op.src_extents_size() > 0; }); | |
| if (source_exist) { | |
| HashCalculator::RawHashOfFile(old_part_.path(), &hash); | |
| partition->mutable_old_partition_info()->set_size(kFakePartitionSize); | |
| partition->mutable_old_partition_info()->set_hash(hash.data(), | |
| hash.size()); | |
| // Only create fake partition if the update is incremental | |
| LOG(INFO) << "Creating fake partition"; | |
| ASSERT_NO_FATAL_FAILURE(CreateFakePartition()); | |
| } | |
| uint64_t metadata_size = 0; | |
| ASSERT_TRUE(PayloadFile::WritePayload(payload_file_.path(), | |
| blob_file_.path(), | |
| private_key_path, | |
| kBrilloMajorPayloadVersion, | |
| *manifest, | |
| &metadata_size)); | |
| LOG(INFO) << "Signature offset: " << manifest->signatures_offset() | |
| << ", Signature size: " << manifest->signatures_size(); | |
| brillo::ErrorPtr error; | |
| HashCalculator::RawHashOfFile(payload_file_.path(), &hash); | |
| daemon_state_.AddObserver(this); | |
| ASSERT_TRUE(update_attempter_android_.ApplyPayload( | |
| "file://" + payload_file_.path(), | |
| 0, | |
| utils::FileSize(payload_file_.path()), | |
| {kPayloadPropertyMetadataSize + ("=" + std::to_string(metadata_size)), | |
| kPayloadPropertyFileHash + | |
| ("=" + brillo::data_encoding::Base64Encode(hash))}, | |
| &error)); | |
| brillo::MessageLoop::current()->Run(); | |
| if (error) { | |
| LOG(ERROR) << error->GetMessage(); | |
| } | |
| ASSERT_EQ(error, nullptr); | |
| ASSERT_EQ(completion_code_, ErrorCode::kSuccess); | |
| } | |
| // Compare contents of fake_b partition to |new_part_| and print difference | |
| void DumpTargetPartitionDiff() { | |
| dynamic_control_->MapAllPartitions(); | |
| auto partition_device = | |
| dynamic_control_->GetPartitionDevice("fake", | |
| 1 - boot_control_.GetCurrentSlot(), | |
| boot_control_.GetCurrentSlot(), | |
| false); | |
| if (!partition_device.has_value()) { | |
| LOG(INFO) << "Failed to get target fake partition, skip diff report"; | |
| return; | |
| } | |
| EintrSafeFileDescriptor actual_part; | |
| CHECK(actual_part.Open(partition_device->readonly_device_path.c_str(), | |
| O_RDONLY)); | |
| EintrSafeFileDescriptor expected_part; | |
| CHECK(expected_part.Open(new_part_.path().c_str(), O_RDONLY)); | |
| std::array<uint8_t, kBlockSize> actual_block; | |
| std::array<uint8_t, kBlockSize> expected_block; | |
| for (size_t i = 0; i < kFakePartitionSize / kBlockSize; i++) { | |
| actual_part.Read(actual_block.data(), actual_block.size()); | |
| expected_part.Read(expected_block.data(), expected_block.size()); | |
| if (actual_block != expected_block) { | |
| LOG(ERROR) << "Block " << i << " differs."; | |
| } | |
| } | |
| } | |
| // use 25MB max to avoid super not having enough space | |
| static constexpr size_t kFakePartitionSize = 1024 * 1024 * 25; | |
| static_assert(kFakePartitionSize % kBlockSize == 0); | |
| BootControlAndroid boot_control_; | |
| std::unique_ptr<android::fs_mgr::MetadataBuilder> builder_; | |
| std::string super_device_; | |
| FakeHardware hardware_; | |
| ScopedTempFile payload_file_; | |
| ScopedTempFile blob_file_{"blob_file.XXXXXX", true}; | |
| // Contains expected data for old partition. Will be copied to fake_a on test | |
| // start. | |
| ScopedTempFile old_part_{"old_part.XXXXXX", true, kFakePartitionSize}; | |
| // Expected data for new partition, will be compared against actual data in | |
| // fake_b once test finishes. | |
| ScopedTempFile new_part_{"new_part.XXXXXX", true, kFakePartitionSize}; | |
| DaemonStateAndroid daemon_state_; | |
| MemoryPrefs prefs_; | |
| ErrorCode completion_code_; | |
| DynamicPartitionControlAndroid* dynamic_control_{nullptr}; | |
| brillo::FakeMessageLoop message_loop_{nullptr}; | |
| DeltaArchiveManifest manifest_; | |
| UpdateAttempterAndroid update_attempter_android_{ | |
| &daemon_state_, &prefs_, &boot_control_, &hardware_, nullptr}; | |
| }; | |
| namespace { | |
| TEST_F(UpdateAttempterAndroidIntegrationTest, NewPartitionTest) { | |
| if (!DynamicPartitionEnabled()) { | |
| return; | |
| } | |
| auto partition = manifest_.add_partitions(); | |
| partition->set_partition_name("fake"); | |
| partition->set_estimate_cow_size(kFakePartitionSize); | |
| { | |
| auto op = partition->add_operations(); | |
| op->set_type(InstallOperation::REPLACE); | |
| *op->add_dst_extents() = ExtentForRange(0, 1); | |
| op->set_data_offset(0); | |
| op->set_data_length(kBlockSize); | |
| truncate(blob_file_.path().c_str(), kBlockSize); | |
| } | |
| { | |
| auto op = partition->add_operations(); | |
| op->set_type(InstallOperation::ZERO); | |
| *op->add_dst_extents() = | |
| ExtentForRange(1, kFakePartitionSize / kBlockSize - 1); | |
| } | |
| ApplyPayload(&manifest_); | |
| if (completion_code_ == ErrorCode::kNewRootfsVerificationError) { | |
| DumpTargetPartitionDiff(); | |
| } | |
| } | |
| TEST_F(UpdateAttempterAndroidIntegrationTest, XorOpsTest) { | |
| if (!DynamicPartitionEnabled()) { | |
| return; | |
| } | |
| auto partition = manifest_.add_partitions(); | |
| partition->set_partition_name("fake"); | |
| partition->set_estimate_cow_size(kFakePartitionSize); | |
| { | |
| auto op = partition->add_operations(); | |
| op->set_type(InstallOperation::BROTLI_BSDIFF); | |
| *op->add_src_extents() = ExtentForRange(0, 10); | |
| *op->add_dst_extents() = ExtentForRange(0, 10); | |
| } | |
| { | |
| auto op = partition->add_operations(); | |
| op->set_type(InstallOperation::BROTLI_BSDIFF); | |
| *op->add_src_extents() = ExtentForRange(10, 10); | |
| *op->add_dst_extents() = ExtentForRange(10, 10); | |
| } | |
| { | |
| auto op = partition->add_operations(); | |
| op->set_type(InstallOperation::SOURCE_COPY); | |
| *op->add_src_extents() = | |
| ExtentForRange(20, kFakePartitionSize / kBlockSize - 20); | |
| *op->add_dst_extents() = | |
| ExtentForRange(20, kFakePartitionSize / kBlockSize - 20); | |
| } | |
| { | |
| auto op = partition->add_merge_operations(); | |
| op->set_type(CowMergeOperation::COW_XOR); | |
| op->set_src_offset(123); | |
| *op->mutable_src_extent() = ExtentForRange(2, 8); | |
| *op->mutable_dst_extent() = ExtentForRange(0, 8); | |
| } | |
| { | |
| auto op = partition->add_merge_operations(); | |
| op->set_type(CowMergeOperation::COW_XOR); | |
| op->set_src_offset(456); | |
| *op->mutable_src_extent() = ExtentForRange(10, 8); | |
| *op->mutable_dst_extent() = ExtentForRange(12, 8); | |
| } | |
| ApplyPayload(&manifest_); | |
| if (completion_code_ == ErrorCode::kNewRootfsVerificationError) { | |
| DumpTargetPartitionDiff(); | |
| } | |
| } | |
| } // namespace | |
| } // namespace chromeos_update_engine |