diff options
author | Yifan Hong <elsk@google.com> | 2020-04-08 19:42:18 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-04-08 19:42:18 +0000 |
commit | d5300af3758c23bf4966daddb5db8309e7fcb573 (patch) | |
tree | 7a2738e3aa27f36ea63cd59a03fa523a56ff8b72 | |
parent | b447317e4e71f22c11c132a4d81398fcdb8344ce (diff) | |
parent | f7339988c6b09dcbb2d0e3847ced760f813adf41 (diff) | |
download | update_engine-d5300af3758c23bf4966daddb5db8309e7fcb573.tar.gz |
Add DynamicPartitionControl::EraseSystemOtherAvbFooter am: cba9c46108 am: f7339988c6
Change-Id: I94c71ebc6dfc5958aea871fc5f91e45baa45b6ab
-rw-r--r-- | Android.bp | 3 | ||||
-rw-r--r-- | dynamic_partition_control_android.cc | 214 | ||||
-rw-r--r-- | dynamic_partition_control_android.h | 37 | ||||
-rw-r--r-- | dynamic_partition_control_android_unittest.cc | 141 | ||||
-rw-r--r-- | dynamic_partition_test_utils.h | 4 | ||||
-rw-r--r-- | mock_dynamic_partition_control.h | 24 |
6 files changed, 413 insertions, 10 deletions
@@ -211,6 +211,9 @@ cc_defaults { "android.hardware.boot@1.0", "android.hardware.boot@1.1", ], + header_libs: [ + "avb_headers", + ], target: { recovery: { static_libs: [ diff --git a/dynamic_partition_control_android.cc b/dynamic_partition_control_android.cc index 09f61adb..1e92f45b 100644 --- a/dynamic_partition_control_android.cc +++ b/dynamic_partition_control_android.cc @@ -32,6 +32,7 @@ #include <fs_mgr.h> #include <fs_mgr_dm_linear.h> #include <fs_mgr_overlayfs.h> +#include <libavb/libavb.h> #include <libdm/dm.h> #include <libsnapshot/snapshot.h> @@ -42,12 +43,14 @@ #include "update_engine/payload_consumer/delta_performer.h" using android::base::GetBoolProperty; +using android::base::GetProperty; using android::base::Join; using android::dm::DeviceMapper; using android::dm::DmDeviceState; using android::fs_mgr::CreateLogicalPartition; using android::fs_mgr::CreateLogicalPartitionParams; using android::fs_mgr::DestroyLogicalPartition; +using android::fs_mgr::Fstab; using android::fs_mgr::MetadataBuilder; using android::fs_mgr::Partition; using android::fs_mgr::PartitionOpener; @@ -64,6 +67,7 @@ constexpr char kRetrfoitDynamicPartitions[] = "ro.boot.dynamic_partitions_retrofit"; constexpr char kVirtualAbEnabled[] = "ro.virtual_ab.enabled"; constexpr char kVirtualAbRetrofit[] = "ro.virtual_ab.retrofit"; +constexpr char kPostinstallFstabPrefix[] = "ro.postinstall.fstab.prefix"; // Map timeout for dynamic partitions. constexpr std::chrono::milliseconds kMapTimeout{1000}; // Map timeout for dynamic partitions with snapshots. Since several devices @@ -401,6 +405,15 @@ bool DynamicPartitionControlAndroid::PreparePartitionsForUpdate( << "run adb enable-verity to deactivate if required and try again."; } + if (GetVirtualAbFeatureFlag().IsEnabled() && metadata_device_ == nullptr) { + metadata_device_ = snapshot_->EnsureMetadataMounted(); + TEST_AND_RETURN_FALSE(metadata_device_ != nullptr); + } + + if (update) { + TEST_AND_RETURN_FALSE(EraseSystemOtherAvbFooter(source_slot, target_slot)); + } + if (!GetDynamicPartitionsFeatureFlag().IsEnabled()) { return true; } @@ -421,11 +434,6 @@ bool DynamicPartitionControlAndroid::PreparePartitionsForUpdate( target_supports_snapshot_ = manifest.dynamic_partition_metadata().snapshot_enabled(); - if (GetVirtualAbFeatureFlag().IsEnabled()) { - metadata_device_ = snapshot_->EnsureMetadataMounted(); - TEST_AND_RETURN_FALSE(metadata_device_ != nullptr); - } - if (!update) return true; @@ -471,6 +479,202 @@ bool DynamicPartitionControlAndroid::PreparePartitionsForUpdate( source_slot, target_slot, manifest, delete_source); } +namespace { +// Try our best to erase AVB footer. +class AvbFooterEraser { + public: + explicit AvbFooterEraser(const std::string& path) : path_(path) {} + bool Erase() { + // Try to mark the block device read-only. Ignore any + // failure since this won't work when passing regular files. + ignore_result(utils::SetBlockDeviceReadOnly(path_, false /* readonly */)); + + fd_.reset(new EintrSafeFileDescriptor()); + int flags = O_WRONLY | O_TRUNC | O_CLOEXEC | O_SYNC; + TEST_AND_RETURN_FALSE(fd_->Open(path_.c_str(), flags)); + + // Need to write end-AVB_FOOTER_SIZE to end. + static_assert(AVB_FOOTER_SIZE > 0); + off64_t offset = fd_->Seek(-AVB_FOOTER_SIZE, SEEK_END); + TEST_AND_RETURN_FALSE_ERRNO(offset >= 0); + uint64_t write_size = AVB_FOOTER_SIZE; + LOG(INFO) << "Zeroing " << path_ << " @ [" << offset << ", " + << (offset + write_size) << "] (" << write_size << " bytes)"; + brillo::Blob zeros(write_size); + TEST_AND_RETURN_FALSE(utils::WriteAll(fd_, zeros.data(), zeros.size())); + return true; + } + ~AvbFooterEraser() { + TEST_AND_RETURN(fd_ != nullptr && fd_->IsOpen()); + if (!fd_->Close()) { + LOG(WARNING) << "Failed to close fd for " << path_; + } + } + + private: + std::string path_; + FileDescriptorPtr fd_; +}; + +} // namespace + +std::optional<bool> +DynamicPartitionControlAndroid::IsAvbEnabledOnSystemOther() { + auto prefix = GetProperty(kPostinstallFstabPrefix, ""); + if (prefix.empty()) { + LOG(WARNING) << "Cannot get " << kPostinstallFstabPrefix; + return std::nullopt; + } + auto path = base::FilePath(prefix).Append("etc/fstab.postinstall").value(); + return IsAvbEnabledInFstab(path); +} + +std::optional<bool> DynamicPartitionControlAndroid::IsAvbEnabledInFstab( + const std::string& path) { + Fstab fstab; + if (!ReadFstabFromFile(path, &fstab)) { + LOG(WARNING) << "Cannot read fstab from " << path; + return std::nullopt; + } + for (const auto& entry : fstab) { + if (!entry.avb_keys.empty()) { + return true; + } + } + return false; +} + +bool DynamicPartitionControlAndroid::GetSystemOtherPath( + uint32_t source_slot, + uint32_t target_slot, + const std::string& partition_name_suffix, + std::string* path, + bool* should_unmap) { + path->clear(); + *should_unmap = false; + + // In recovery, just erase no matter what. + // - On devices with retrofit dynamic partitions, no logical partitions + // should be mounted at this point. Hence it should be safe to erase. + // Otherwise, do check that AVB is enabled on system_other before erasing. + if (!IsRecovery()) { + auto has_avb = IsAvbEnabledOnSystemOther(); + TEST_AND_RETURN_FALSE(has_avb.has_value()); + if (!has_avb.value()) { + LOG(INFO) << "AVB is not enabled on system_other. Skip erasing."; + return true; + } + + // Found unexpected avb_keys for system_other on devices retrofitting + // dynamic partitions. Previous crash in update_engine may leave logical + // partitions mapped on physical system_other partition. It is difficult to + // handle these cases. Just fail. + if (GetDynamicPartitionsFeatureFlag().IsRetrofit()) { + LOG(ERROR) << "Cannot erase AVB footer on system_other on devices with " + << "retrofit dynamic partitions. They should not have AVB " + << "enabled on system_other."; + return false; + } + } + + std::string device_dir_str; + TEST_AND_RETURN_FALSE(GetDeviceDir(&device_dir_str)); + base::FilePath device_dir(device_dir_str); + + // On devices without dynamic partition, search for static partitions. + if (!GetDynamicPartitionsFeatureFlag().IsEnabled()) { + *path = device_dir.Append(partition_name_suffix).value(); + TEST_AND_RETURN_FALSE(DeviceExists(*path)); + return true; + } + + auto source_super_device = + device_dir.Append(GetSuperPartitionName(source_slot)).value(); + + auto builder = LoadMetadataBuilder(source_super_device, source_slot); + if (builder == nullptr) { + if (IsRecovery()) { + // It might be corrupted for some reason. It should still be able to + // sideload. + LOG(WARNING) << "Super partition metadata cannot be read from the source " + << "slot, skip erasing."; + return true; + } else { + // Device has booted into Android mode, indicating that the super + // partition metadata should be there. + LOG(ERROR) << "Super partition metadata cannot be read from the source " + << "slot. This is unexpected on devices with dynamic " + << "partitions enabled."; + return false; + } + } + auto p = builder->FindPartition(partition_name_suffix); + if (p == nullptr) { + // If the source slot is flashed without system_other, it does not exist + // in super partition metadata at source slot. It is safe to skip it. + LOG(INFO) << "Can't find " << partition_name_suffix + << " in metadata source slot, skip erasing."; + return true; + } + // System_other created by flashing tools should be erased. + // If partition is created by update_engine (via NewForUpdate), it is a + // left-over partition from the previous update and does not contain + // system_other, hence there is no need to erase. + // Note the reverse is not necessary true. If the flag is not set, we don't + // know if the partition is created by update_engine or by flashing tools + // because older versions of super partition metadata does not contain this + // flag. It is okay to erase the AVB footer anyways. + if (p->attributes() & LP_PARTITION_ATTR_UPDATED) { + LOG(INFO) << partition_name_suffix + << " does not contain system_other, skip erasing."; + return true; + } + + // Delete any pre-existing device with name |partition_name_suffix| and + // also remove it from |mapped_devices_|. + TEST_AND_RETURN_FALSE(UnmapPartitionOnDeviceMapper(partition_name_suffix)); + // Use CreateLogicalPartition directly to avoid mapping with existing + // snapshots. + CreateLogicalPartitionParams params = { + .block_device = source_super_device, + .metadata_slot = source_slot, + .partition_name = partition_name_suffix, + .force_writable = true, + .timeout_ms = kMapTimeout, + }; + TEST_AND_RETURN_FALSE(CreateLogicalPartition(params, path)); + *should_unmap = true; + return true; +} + +bool DynamicPartitionControlAndroid::EraseSystemOtherAvbFooter( + uint32_t source_slot, uint32_t target_slot) { + LOG(INFO) << "Erasing AVB footer of system_other partition before update."; + + const std::string target_suffix = SlotSuffixForSlotNumber(target_slot); + const std::string partition_name_suffix = "system" + target_suffix; + + std::string path; + bool should_unmap = false; + + TEST_AND_RETURN_FALSE(GetSystemOtherPath( + source_slot, target_slot, partition_name_suffix, &path, &should_unmap)); + + if (path.empty()) { + return true; + } + + bool ret = AvbFooterEraser(path).Erase(); + + // Delete |partition_name_suffix| from device mapper and from + // |mapped_devices_| again so that it does not interfere with update process. + if (should_unmap) { + TEST_AND_RETURN_FALSE(UnmapPartitionOnDeviceMapper(partition_name_suffix)); + } + + return ret; +} + bool DynamicPartitionControlAndroid::PrepareDynamicPartitionsForUpdate( uint32_t source_slot, uint32_t target_slot, diff --git a/dynamic_partition_control_android.h b/dynamic_partition_control_android.h index 6dbe3704..9dcdcf1a 100644 --- a/dynamic_partition_control_android.h +++ b/dynamic_partition_control_android.h @@ -136,6 +136,43 @@ class DynamicPartitionControlAndroid : public DynamicPartitionControlInterface { // Allow mock objects to override this to test recovery mode. virtual bool IsRecovery(); + // Determine path for system_other partition. + // |source_slot| should be current slot. + // |target_slot| should be "other" slot. + // |partition_name_suffix| should be "system" + suffix(|target_slot|). + // Return true and set |path| if successful. + // Set |path| to empty if no need to erase system_other. + // Set |should_unmap| to true if path needs to be unmapped later. + // + // Note: system_other cannot use GetPartitionDevice or + // GetDynamicPartitionDevice because: + // - super partition metadata may be loaded from the source slot + // - UPDATED flag needs to be check to skip erasing if partition is not + // created by flashing tools + // - Snapshots from previous update attempts should not be used. + virtual bool GetSystemOtherPath(uint32_t source_slot, + uint32_t target_slot, + const std::string& partition_name_suffix, + std::string* path, + bool* should_unmap); + + // Returns true if any entry in the fstab file in |path| has AVB enabled, + // false if not enabled, and nullopt for any error. + virtual std::optional<bool> IsAvbEnabledInFstab(const std::string& path); + + // Returns true if system_other has AVB enabled, false if not enabled, and + // nullopt for any error. + virtual std::optional<bool> IsAvbEnabledOnSystemOther(); + + // Erase system_other partition that may contain system_other.img. + // After the update, the content of system_other may be corrupted but with + // valid AVB footer. If the update is rolled back and factory data reset is + // triggered, system_b fails to be mapped with verity errors (see + // b/152444348). Erase the system_other so that mapping system_other is + // skipped. + virtual bool EraseSystemOtherAvbFooter(uint32_t source_slot, + uint32_t target_slot); + private: friend class DynamicPartitionControlAndroidTest; diff --git a/dynamic_partition_control_android_unittest.cc b/dynamic_partition_control_android_unittest.cc index 457ea108..20819182 100644 --- a/dynamic_partition_control_android_unittest.cc +++ b/dynamic_partition_control_android_unittest.cc @@ -23,12 +23,16 @@ #include <base/strings/string_util.h> #include <gmock/gmock.h> #include <gtest/gtest.h> +#include <libavb/libavb.h> #include "update_engine/common/mock_prefs.h" +#include "update_engine/common/test_utils.h" #include "update_engine/dynamic_partition_test_utils.h" #include "update_engine/mock_dynamic_partition_control.h" using android::dm::DmDeviceState; +using chromeos_update_engine::test_utils::ScopedLoopbackDeviceBinder; +using chromeos_update_engine::test_utils::ScopedTempFile; using std::string; using testing::_; using testing::AnyNumber; @@ -36,6 +40,7 @@ using testing::AnyOf; using testing::Invoke; using testing::NiceMock; using testing::Not; +using testing::Optional; using testing::Return; namespace chromeos_update_engine { @@ -64,6 +69,9 @@ class DynamicPartitionControlAndroidTest : public ::testing::Test { *device = GetDmDevice(partition_name_suffix); return true; })); + + ON_CALL(dynamicControl(), EraseSystemOtherAvbFooter(_, _)) + .WillByDefault(Return(true)); } // Return the mocked DynamicPartitionControlInterface. @@ -90,12 +98,15 @@ class DynamicPartitionControlAndroidTest : public ::testing::Test { // Set the fake metadata to return when LoadMetadataBuilder is called on // |slot|. - void SetMetadata(uint32_t slot, const PartitionSuffixSizes& sizes) { + void SetMetadata(uint32_t slot, + const PartitionSuffixSizes& sizes, + uint32_t partition_attr = 0) { EXPECT_CALL(dynamicControl(), LoadMetadataBuilder(GetSuperDevice(slot), slot, _)) .Times(AnyNumber()) - .WillRepeatedly(Invoke([sizes](auto, auto, auto) { - return NewFakeMetadata(PartitionSuffixSizesToManifest(sizes)); + .WillRepeatedly(Invoke([sizes, partition_attr](auto, auto, auto) { + return NewFakeMetadata(PartitionSuffixSizesToManifest(sizes), + partition_attr); })); } @@ -757,4 +768,128 @@ TEST_F(DynamicPartitionControlAndroidTest, ResetUpdate) { ASSERT_TRUE(dynamicControl().ResetUpdate(&prefs)); } +TEST_F(DynamicPartitionControlAndroidTest, IsAvbNotEnabledInFstab) { + // clang-format off + std::string fstab_content = + "system /postinstall ext4 ro,nosuid,nodev,noexec slotselect_other,logical\n" // NOLINT(whitespace/line_length) + "/dev/block/by-name/system /postinstall ext4 ro,nosuid,nodev,noexec slotselect_other\n"; // NOLINT(whitespace/line_length) + // clang-format on + ScopedTempFile fstab; + ASSERT_TRUE(test_utils::WriteFileString(fstab.path(), fstab_content)); + ASSERT_THAT(dynamicControl().RealIsAvbEnabledInFstab(fstab.path()), + Optional(false)); +} + +TEST_F(DynamicPartitionControlAndroidTest, IsAvbEnabledInFstab) { + // clang-format off + std::string fstab_content = + "system /postinstall ext4 ro,nosuid,nodev,noexec slotselect_other,logical,avb_keys=/foo\n"; // NOLINT(whitespace/line_length) + // clang-format on + ScopedTempFile fstab; + ASSERT_TRUE(test_utils::WriteFileString(fstab.path(), fstab_content)); + ASSERT_THAT(dynamicControl().RealIsAvbEnabledInFstab(fstab.path()), + Optional(true)); +} + +TEST_P(DynamicPartitionControlAndroidTestP, AvbNotEnabledOnSystemOther) { + ON_CALL(dynamicControl(), GetSystemOtherPath(_, _, _, _, _)) + .WillByDefault(Invoke([&](auto source_slot, + auto target_slot, + const auto& name, + auto path, + auto should_unmap) { + return dynamicControl().RealGetSystemOtherPath( + source_slot, target_slot, name, path, should_unmap); + })); + ON_CALL(dynamicControl(), IsAvbEnabledOnSystemOther()) + .WillByDefault(Return(false)); + EXPECT_TRUE( + dynamicControl().RealEraseSystemOtherAvbFooter(source(), target())); +} + +TEST_P(DynamicPartitionControlAndroidTestP, NoSystemOtherToErase) { + SetMetadata(source(), {{S("system"), 100_MiB}}); + ON_CALL(dynamicControl(), IsAvbEnabledOnSystemOther()) + .WillByDefault(Return(true)); + std::string path; + bool should_unmap; + ASSERT_TRUE(dynamicControl().RealGetSystemOtherPath( + source(), target(), T("system"), &path, &should_unmap)); + ASSERT_TRUE(path.empty()) << path; + ASSERT_FALSE(should_unmap); + ON_CALL(dynamicControl(), GetSystemOtherPath(_, _, _, _, _)) + .WillByDefault(Invoke([&](auto source_slot, + auto target_slot, + const auto& name, + auto path, + auto should_unmap) { + return dynamicControl().RealGetSystemOtherPath( + source_slot, target_slot, name, path, should_unmap); + })); + EXPECT_TRUE( + dynamicControl().RealEraseSystemOtherAvbFooter(source(), target())); +} + +TEST_P(DynamicPartitionControlAndroidTestP, SkipEraseUpdatedSystemOther) { + PartitionSuffixSizes sizes{{S("system"), 100_MiB}, {T("system"), 100_MiB}}; + SetMetadata(source(), sizes, LP_PARTITION_ATTR_UPDATED); + ON_CALL(dynamicControl(), IsAvbEnabledOnSystemOther()) + .WillByDefault(Return(true)); + std::string path; + bool should_unmap; + ASSERT_TRUE(dynamicControl().RealGetSystemOtherPath( + source(), target(), T("system"), &path, &should_unmap)); + ASSERT_TRUE(path.empty()) << path; + ASSERT_FALSE(should_unmap); + ON_CALL(dynamicControl(), GetSystemOtherPath(_, _, _, _, _)) + .WillByDefault(Invoke([&](auto source_slot, + auto target_slot, + const auto& name, + auto path, + auto should_unmap) { + return dynamicControl().RealGetSystemOtherPath( + source_slot, target_slot, name, path, should_unmap); + })); + EXPECT_TRUE( + dynamicControl().RealEraseSystemOtherAvbFooter(source(), target())); +} + +TEST_P(DynamicPartitionControlAndroidTestP, EraseSystemOtherAvbFooter) { + constexpr uint64_t file_size = 1_MiB; + static_assert(file_size > AVB_FOOTER_SIZE); + ScopedTempFile system_other; + brillo::Blob original(file_size, 'X'); + ASSERT_TRUE(test_utils::WriteFileVector(system_other.path(), original)); + std::string mnt_path; + ScopedLoopbackDeviceBinder dev(system_other.path(), true, &mnt_path); + ASSERT_TRUE(dev.is_bound()); + + brillo::Blob device_content; + ASSERT_TRUE(utils::ReadFile(mnt_path, &device_content)); + ASSERT_EQ(original, device_content); + + PartitionSuffixSizes sizes{{S("system"), 100_MiB}, {T("system"), file_size}}; + SetMetadata(source(), sizes); + ON_CALL(dynamicControl(), IsAvbEnabledOnSystemOther()) + .WillByDefault(Return(true)); + EXPECT_CALL(dynamicControl(), + GetSystemOtherPath(source(), target(), T("system"), _, _)) + .WillRepeatedly( + Invoke([&](auto, auto, const auto&, auto path, auto should_unmap) { + *path = mnt_path; + *should_unmap = false; + return true; + })); + ASSERT_TRUE( + dynamicControl().RealEraseSystemOtherAvbFooter(source(), target())); + + device_content.clear(); + ASSERT_TRUE(utils::ReadFile(mnt_path, &device_content)); + brillo::Blob new_expected(original); + // Clear the last AVB_FOOTER_SIZE bytes. + new_expected.resize(file_size - AVB_FOOTER_SIZE); + new_expected.resize(file_size, '\0'); + ASSERT_EQ(new_expected, device_content); +} + } // namespace chromeos_update_engine diff --git a/dynamic_partition_test_utils.h b/dynamic_partition_test_utils.h index 346998fc..70a176b5 100644 --- a/dynamic_partition_test_utils.h +++ b/dynamic_partition_test_utils.h @@ -175,7 +175,7 @@ inline DeltaArchiveManifest PartitionSizesToManifest( } inline std::unique_ptr<MetadataBuilder> NewFakeMetadata( - const DeltaArchiveManifest& manifest) { + const DeltaArchiveManifest& manifest, uint32_t partition_attr = 0) { auto builder = MetadataBuilder::New(kDefaultSuperSize, kFakeMetadataSize, kMaxNumSlots); for (const auto& group : manifest.dynamic_partition_metadata().groups()) { @@ -183,7 +183,7 @@ inline std::unique_ptr<MetadataBuilder> NewFakeMetadata( for (const auto& partition_name : group.partition_names()) { EXPECT_NE( nullptr, - builder->AddPartition(partition_name, group.name(), 0 /* attr */)); + builder->AddPartition(partition_name, group.name(), partition_attr)); } } for (const auto& partition : manifest.partitions()) { diff --git a/mock_dynamic_partition_control.h b/mock_dynamic_partition_control.h index 169c2657..1e4e5fd8 100644 --- a/mock_dynamic_partition_control.h +++ b/mock_dynamic_partition_control.h @@ -77,10 +77,34 @@ class MockDynamicPartitionControlAndroid MOCK_METHOD1(GetSuperPartitionName, std::string(uint32_t)); MOCK_METHOD0(GetVirtualAbFeatureFlag, FeatureFlag()); MOCK_METHOD1(FinishUpdate, bool(bool)); + MOCK_METHOD5( + GetSystemOtherPath, + bool(uint32_t, uint32_t, const std::string&, std::string*, bool*)); + MOCK_METHOD2(EraseSystemOtherAvbFooter, bool(uint32_t, uint32_t)); + MOCK_METHOD0(IsAvbEnabledOnSystemOther, std::optional<bool>()); void set_fake_mapped_devices(const std::set<std::string>& fake) override { DynamicPartitionControlAndroid::set_fake_mapped_devices(fake); } + + bool RealGetSystemOtherPath(uint32_t source_slot, + uint32_t target_slot, + const std::string& partition_name_suffix, + std::string* path, + bool* should_unmap) { + return DynamicPartitionControlAndroid::GetSystemOtherPath( + source_slot, target_slot, partition_name_suffix, path, should_unmap); + } + + bool RealEraseSystemOtherAvbFooter(uint32_t source_slot, + uint32_t target_slot) { + return DynamicPartitionControlAndroid::EraseSystemOtherAvbFooter( + source_slot, target_slot); + } + + std::optional<bool> RealIsAvbEnabledInFstab(const std::string& path) { + return DynamicPartitionControlAndroid::IsAvbEnabledInFstab(path); + } }; } // namespace chromeos_update_engine |