| /* |
| * 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 "apexd.h" |
| |
| #include <android-base/file.h> |
| #include <android-base/properties.h> |
| #include <android-base/result-gmock.h> |
| #include <android-base/scopeguard.h> |
| #include <android-base/stringprintf.h> |
| #include <android-base/unique_fd.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <libdm/dm.h> |
| #include <microdroid/metadata.h> |
| #include <selinux/selinux.h> |
| #include <sys/stat.h> |
| |
| #include <functional> |
| #include <optional> |
| #include <string> |
| #include <tuple> |
| #include <unordered_set> |
| #include <vector> |
| |
| #include "apex_database.h" |
| #include "apex_file.h" |
| #include "apex_file_repository.h" |
| #include "apex_manifest.pb.h" |
| #include "apexd_checkpoint.h" |
| #include "apexd_loop.h" |
| #include "apexd_session.h" |
| #include "apexd_test_utils.h" |
| #include "apexd_utils.h" |
| #include "com_android_apex.h" |
| #include "gmock/gmock-matchers.h" |
| |
| namespace android { |
| namespace apex { |
| |
| namespace fs = std::filesystem; |
| |
| using MountedApexData = MountedApexDatabase::MountedApexData; |
| using android::apex::testing::ApexFileEq; |
| using android::base::GetExecutableDirectory; |
| using android::base::GetProperty; |
| using android::base::Join; |
| using android::base::make_scope_guard; |
| using android::base::ReadFileToString; |
| using android::base::ReadFully; |
| using android::base::RemoveFileIfExists; |
| using android::base::Result; |
| using android::base::Split; |
| using android::base::StringPrintf; |
| using android::base::unique_fd; |
| using android::base::WriteStringToFile; |
| using android::base::testing::HasError; |
| using android::base::testing::HasValue; |
| using android::base::testing::Ok; |
| using android::base::testing::WithMessage; |
| using android::dm::DeviceMapper; |
| using ::apex::proto::SessionState; |
| using com::android::apex::testing::ApexInfoXmlEq; |
| using ::testing::ByRef; |
| using ::testing::Contains; |
| using ::testing::ElementsAre; |
| using ::testing::EndsWith; |
| using ::testing::HasSubstr; |
| using ::testing::IsEmpty; |
| using ::testing::Not; |
| using ::testing::StartsWith; |
| using ::testing::UnorderedElementsAre; |
| using ::testing::UnorderedElementsAreArray; |
| using ::testing::internal::CaptureStderr; |
| using ::testing::internal::GetCapturedStderr; |
| |
| static std::string GetTestDataDir() { return GetExecutableDirectory(); } |
| static std::string GetTestFile(const std::string& name) { |
| return GetTestDataDir() + "/" + name; |
| } |
| |
| static int64_t GetMTime(const std::string& path) { |
| struct stat st_buf; |
| if (stat(path.c_str(), &st_buf) != 0) { |
| PLOG(ERROR) << "Failed to stat " << path; |
| return 0; |
| } |
| return st_buf.st_mtime; |
| } |
| |
| // A very basic mock of CheckpointInterface. |
| class MockCheckpointInterface : public CheckpointInterface { |
| public: |
| Result<bool> SupportsFsCheckpoints() override { |
| return supports_fs_checkpoint_; |
| } |
| |
| Result<bool> NeedsCheckpoint() override { return needs_checkpoint_; } |
| |
| Result<bool> NeedsRollback() override { return needs_rollback_; } |
| |
| Result<void> StartCheckpoint(int32_t num_retries) override { return {}; } |
| |
| Result<void> AbortChanges(const std::string& msg, bool retry) override { |
| return {}; |
| } |
| |
| void SetSupportsCheckpoint(bool value) { supports_fs_checkpoint_ = value; } |
| |
| void SetNeedsCheckpoint(bool value) { needs_checkpoint_ = value; } |
| |
| void SetNeedsRollback(bool value) { needs_rollback_ = value; } |
| |
| private: |
| bool supports_fs_checkpoint_, needs_checkpoint_, needs_rollback_; |
| }; |
| |
| static constexpr const char* kTestApexdStatusSysprop = "apexd.status.test"; |
| static constexpr const char* kTestVmPayloadMetadataPartitionProp = |
| "apexd.vm.payload_metadata_partition.test"; |
| |
| static constexpr const char* kTestActiveApexSelinuxCtx = |
| "u:object_r:shell_data_file:s0"; |
| |
| // A test fixture that provides frequently required temp directories for tests |
| class ApexdUnitTest : public ::testing::Test { |
| public: |
| ApexdUnitTest() { |
| built_in_dir_ = StringPrintf("%s/pre-installed-apex", td_.path); |
| data_dir_ = StringPrintf("%s/data-apex", td_.path); |
| decompression_dir_ = StringPrintf("%s/decompressed-apex", td_.path); |
| ota_reserved_dir_ = StringPrintf("%s/ota-reserved", td_.path); |
| hash_tree_dir_ = StringPrintf("%s/apex-hash-tree", td_.path); |
| staged_session_dir_ = StringPrintf("%s/staged-session-dir", td_.path); |
| |
| sessions_metadata_dir_ = |
| StringPrintf("%s/metadata-staged-session-dir", td_.path); |
| session_manager_ = ApexSessionManager::Create(sessions_metadata_dir_); |
| |
| config_ = {kTestApexdStatusSysprop, |
| {built_in_dir_}, |
| data_dir_.c_str(), |
| decompression_dir_.c_str(), |
| ota_reserved_dir_.c_str(), |
| hash_tree_dir_.c_str(), |
| staged_session_dir_.c_str(), |
| kTestVmPayloadMetadataPartitionProp, |
| kTestActiveApexSelinuxCtx}; |
| } |
| |
| const std::string& GetBuiltInDir() { return built_in_dir_; } |
| const std::string& GetDataDir() { return data_dir_; } |
| const std::string& GetDecompressionDir() { return decompression_dir_; } |
| const std::string& GetOtaReservedDir() { return ota_reserved_dir_; } |
| const std::string& GetHashTreeDir() { return hash_tree_dir_; } |
| const std::string GetStagedDir(int session_id) { |
| return StringPrintf("%s/session_%d", staged_session_dir_.c_str(), |
| session_id); |
| } |
| ApexSessionManager* GetSessionManager() { return session_manager_.get(); } |
| |
| std::string GetRootDigest(const ApexFile& apex) { |
| if (apex.IsCompressed()) { |
| return ""; |
| } |
| auto digest = apex.VerifyApexVerity(apex.GetBundledPublicKey()); |
| if (!digest.ok()) { |
| return ""; |
| } |
| return digest->root_digest; |
| } |
| |
| std::string AddPreInstalledApex(const std::string& apex_name) { |
| fs::copy(GetTestFile(apex_name), built_in_dir_); |
| return StringPrintf("%s/%s", built_in_dir_.c_str(), apex_name.c_str()); |
| } |
| |
| std::string AddDataApex(const std::string& apex_name) { |
| fs::copy(GetTestFile(apex_name), data_dir_); |
| return StringPrintf("%s/%s", data_dir_.c_str(), apex_name.c_str()); |
| } |
| |
| std::string AddDataApex(const std::string& apex_name, |
| const std::string& target_name) { |
| fs::copy(GetTestFile(apex_name), data_dir_ + "/" + target_name); |
| return StringPrintf("%s/%s", data_dir_.c_str(), target_name.c_str()); |
| } |
| |
| std::string AddDecompressedApex(const std::string& apex_name) { |
| auto apex_file = ApexFile::Open(GetTestFile(apex_name)); |
| CHECK(apex_file.ok()); |
| std::string target_name = |
| apex_file->GetManifest().name() + "@" + |
| std::to_string(apex_file->GetManifest().version()) + |
| std::string(kDecompressedApexPackageSuffix); |
| fs::copy(GetTestFile(apex_name), decompression_dir_ + "/" + target_name); |
| return StringPrintf("%s/%s", decompression_dir_.c_str(), |
| target_name.c_str()); |
| } |
| |
| // Copies the compressed apex to |built_in_dir| and decompresses it to |
| // |decompressed_dir| and then hard links to |target_dir| |
| std::string PrepareCompressedApex(const std::string& name, |
| const std::string& built_in_dir) { |
| fs::copy(GetTestFile(name), built_in_dir); |
| auto compressed_apex = ApexFile::Open( |
| StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str())); |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex)); |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /*is_ota_chroot*/ false); |
| return StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str()); |
| } |
| |
| std::string PrepareCompressedApex(const std::string& name) { |
| return PrepareCompressedApex(name, built_in_dir_); |
| } |
| |
| void PrepareStagedSession(const std::string& apex_name, int session_id) { |
| CreateDirIfNeeded(GetStagedDir(session_id), 0755); |
| fs::copy(GetTestFile(apex_name), GetStagedDir(session_id)); |
| } |
| |
| Result<ApexSession> CreateStagedSession(const std::string& apex_name, |
| int session_id) { |
| PrepareStagedSession(apex_name, session_id); |
| auto result = session_manager_->CreateSession(session_id); |
| if (!result.ok()) { |
| return result.error(); |
| } |
| result->SetBuildFingerprint(GetProperty("ro.build.fingerprint", "")); |
| return result; |
| } |
| |
| protected: |
| void SetUp() override { |
| SetConfig(config_); |
| ApexFileRepository::GetInstance().Reset(decompression_dir_); |
| ASSERT_EQ(mkdir(built_in_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(data_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(decompression_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(ota_reserved_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(hash_tree_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(staged_session_dir_.c_str(), 0755), 0); |
| ASSERT_EQ(mkdir(sessions_metadata_dir_.c_str(), 0755), 0); |
| |
| // We don't really need for all the test cases, but until we refactor apexd |
| // to use dependency injection instead of this SetConfig approach, it is not |
| // trivial to figure out which test cases need the session manager, so we |
| // initialize it for all of them. |
| InitializeSessionManager(GetSessionManager()); |
| DeleteDirContent(GetSessionsDir()); |
| } |
| |
| void TearDown() override { DeleteDirContent(GetSessionsDir()); } |
| |
| protected: |
| TemporaryDir td_; |
| std::string built_in_dir_; |
| std::string data_dir_; |
| std::string decompression_dir_; |
| std::string ota_reserved_dir_; |
| std::string hash_tree_dir_; |
| |
| std::string staged_session_dir_; |
| std::string sessions_metadata_dir_; |
| std::unique_ptr<ApexSessionManager> session_manager_; |
| |
| ApexdConfig config_; |
| }; |
| |
| // Apex that does not have pre-installed version, does not get selected |
| TEST_F(ApexdUnitTest, ApexMustHavePreInstalledVersionForSelection) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| AddPreInstalledApex("com.android.apex.cts.shim.apex"); |
| auto shared_lib_1 = ApexFile::Open(AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); |
| auto& instance = ApexFileRepository::GetInstance(); |
| // Pre-installed data needs to be present so that we can add data apex |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| auto apexd_test_file = ApexFile::Open(AddDataApex("apex.apexd_test.apex")); |
| auto shim_v1 = ApexFile::Open(AddDataApex("com.android.apex.cts.shim.apex")); |
| // Normally both pre-installed and data apex would be activated for a shared |
| // libs apex, but if they are the same version only the data apex will be. |
| auto shared_lib_2 = ApexFile::Open( |
| AddDataApex("com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| const auto all_apex = instance.AllApexFilesByName(); |
| // Pass a blank instance so that the data apex files are not considered |
| // pre-installed |
| const ApexFileRepository instance_blank; |
| auto result = SelectApexForActivation(all_apex, instance_blank); |
| ASSERT_EQ(result.size(), 0u); |
| // When passed proper instance they should get selected |
| result = SelectApexForActivation(all_apex, instance); |
| ASSERT_EQ(result.size(), 3u); |
| ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file)), |
| ApexFileEq(ByRef(*shim_v1)), |
| ApexFileEq(ByRef(*shared_lib_2)))); |
| } |
| |
| // Higher version gets priority when selecting for activation |
| TEST_F(ApexdUnitTest, HigherVersionOfApexIsSelected) { |
| auto apexd_test_file_v2 = |
| ApexFile::Open(AddPreInstalledApex("apex.apexd_test_v2.apex")); |
| AddPreInstalledApex("com.android.apex.cts.shim.apex"); |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| TemporaryDir data_dir; |
| AddDataApex("apex.apexd_test.apex"); |
| auto shim_v2 = |
| ApexFile::Open(AddDataApex("com.android.apex.cts.shim.v2.apex")); |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| auto all_apex = instance.AllApexFilesByName(); |
| auto result = SelectApexForActivation(all_apex, instance); |
| ASSERT_EQ(result.size(), 2u); |
| |
| ASSERT_THAT(result, |
| UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file_v2)), |
| ApexFileEq(ByRef(*shim_v2)))); |
| } |
| |
| // When versions are equal, non-pre-installed version gets priority |
| TEST_F(ApexdUnitTest, DataApexGetsPriorityForSameVersions) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| AddPreInstalledApex("com.android.apex.cts.shim.apex"); |
| // Initialize pre-installed APEX information |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| auto apexd_test_file = ApexFile::Open(AddDataApex("apex.apexd_test.apex")); |
| auto shim_v1 = ApexFile::Open(AddDataApex("com.android.apex.cts.shim.apex")); |
| // Initialize ApexFile repo |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| auto all_apex = instance.AllApexFilesByName(); |
| auto result = SelectApexForActivation(all_apex, instance); |
| ASSERT_EQ(result.size(), 2u); |
| |
| ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file)), |
| ApexFileEq(ByRef(*shim_v1)))); |
| } |
| |
| // Both versions of shared libs can be selected when preinstalled version is |
| // lower than data version |
| TEST_F(ApexdUnitTest, SharedLibsCanHaveBothVersionSelected) { |
| auto shared_lib_v1 = ApexFile::Open(AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); |
| // Initialize pre-installed APEX information |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| auto shared_lib_v2 = ApexFile::Open( |
| AddDataApex("com.android.apex.test.sharedlibs_generated.v2.libvY.apex")); |
| // Initialize data APEX information |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| auto all_apex = instance.AllApexFilesByName(); |
| auto result = SelectApexForActivation(all_apex, instance); |
| ASSERT_EQ(result.size(), 2u); |
| |
| ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*shared_lib_v1)), |
| ApexFileEq(ByRef(*shared_lib_v2)))); |
| } |
| |
| // Data version of shared libs should not be selected if lower than |
| // preinstalled version |
| TEST_F(ApexdUnitTest, SharedLibsDataVersionDeletedIfLower) { |
| auto shared_lib_v2 = ApexFile::Open(AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v2.libvY.apex")); |
| // Initialize pre-installed APEX information |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| auto shared_lib_v1 = ApexFile::Open( |
| AddDataApex("com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); |
| // Initialize data APEX information |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| auto all_apex = instance.AllApexFilesByName(); |
| auto result = SelectApexForActivation(all_apex, instance); |
| ASSERT_EQ(result.size(), 1u); |
| |
| ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*shared_lib_v2)))); |
| } |
| |
| TEST_F(ApexdUnitTest, ProcessCompressedApex) { |
| auto compressed_apex = ApexFile::Open( |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex")); |
| |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex)); |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| |
| std::string decompressed_file_path = StringPrintf( |
| "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| // Assert output path is not empty |
| auto exists = PathExists(decompressed_file_path); |
| ASSERT_THAT(exists, HasValue(true)); |
| |
| // Assert that return value contains decompressed APEX |
| auto decompressed_apex = ApexFile::Open(decompressed_file_path); |
| ASSERT_THAT(return_value, |
| UnorderedElementsAre(ApexFileEq(ByRef(*decompressed_apex)))); |
| } |
| |
| TEST_F(ApexdUnitTest, ProcessCompressedApexRunsVerification) { |
| auto compressed_apex_mismatch_key = ApexFile::Open(AddPreInstalledApex( |
| "com.android.apex.compressed_key_mismatch_with_original.capex")); |
| auto compressed_apex_version_mismatch = ApexFile::Open( |
| AddPreInstalledApex("com.android.apex.compressed.v1_with_v2_apex.capex")); |
| |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex_mismatch_key)); |
| compressed_apex_list.emplace_back( |
| std::cref(*compressed_apex_version_mismatch)); |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| ASSERT_EQ(return_value.size(), 0u); |
| } |
| |
| TEST_F(ApexdUnitTest, ValidateDecompressedApex) { |
| auto capex = ApexFile::Open( |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex")); |
| auto decompressed_v1 = |
| ApexFile::Open(AddDataApex("com.android.apex.compressed.v1.apex")); |
| |
| auto result = |
| ValidateDecompressedApex(std::cref(*capex), std::cref(*decompressed_v1)); |
| ASSERT_THAT(result, Ok()); |
| |
| // Validation checks version |
| auto decompressed_v2 = ApexFile::Open( |
| AddDataApex("com.android.apex.compressed.v2_original.apex")); |
| result = |
| ValidateDecompressedApex(std::cref(*capex), std::cref(*decompressed_v2)); |
| ASSERT_THAT( |
| result, |
| HasError(WithMessage(HasSubstr( |
| "Compressed APEX has different version than decompressed APEX")))); |
| |
| // Validation check root digest |
| auto decompressed_v1_different_digest = ApexFile::Open(AddDataApex( |
| "com.android.apex.compressed.v1_different_digest_original.apex")); |
| result = ValidateDecompressedApex( |
| std::cref(*capex), std::cref(*decompressed_v1_different_digest)); |
| ASSERT_THAT(result, HasError(WithMessage(HasSubstr( |
| "does not match with expected root digest")))); |
| |
| // Validation checks key |
| auto capex_different_key = ApexFile::Open( |
| AddDataApex("com.android.apex.compressed_different_key.capex")); |
| result = ValidateDecompressedApex(std::cref(*capex_different_key), |
| std::cref(*decompressed_v1)); |
| ASSERT_THAT( |
| result, |
| HasError(WithMessage(HasSubstr( |
| "Public key of compressed APEX is different than original")))); |
| } |
| |
| TEST_F(ApexdUnitTest, ProcessCompressedApexCanBeCalledMultipleTimes) { |
| auto compressed_apex = ApexFile::Open( |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex")); |
| |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex)); |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| ASSERT_EQ(return_value.size(), 1u); |
| |
| // Capture the creation time of the decompressed APEX |
| std::error_code ec; |
| auto decompressed_apex_path = StringPrintf( |
| "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| auto last_write_time_1 = fs::last_write_time(decompressed_apex_path, ec); |
| ASSERT_FALSE(ec) << "Failed to capture last write time of " |
| << decompressed_apex_path; |
| |
| // Now try to decompress the same capex again. It should not fail. |
| return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| ASSERT_EQ(return_value.size(), 1u); |
| |
| // Ensure the decompressed APEX file did not change |
| auto last_write_time_2 = fs::last_write_time(decompressed_apex_path, ec); |
| ASSERT_FALSE(ec) << "Failed to capture last write time of " |
| << decompressed_apex_path; |
| ASSERT_EQ(last_write_time_1, last_write_time_2); |
| } |
| |
| // Test behavior of ProcessCompressedApex when is_ota_chroot is true |
| TEST_F(ApexdUnitTest, ProcessCompressedApexOnOtaChroot) { |
| auto compressed_apex = ApexFile::Open( |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex")); |
| |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex)); |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ true); |
| ASSERT_EQ(return_value.size(), 1u); |
| |
| // Decompressed APEX should be located in decompression_dir |
| std::string decompressed_file_path = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| // Assert output path is not empty |
| auto exists = PathExists(decompressed_file_path); |
| ASSERT_THAT(exists, HasValue(true)) |
| << decompressed_file_path << " does not exist"; |
| |
| // Assert that return value contains the decompressed APEX |
| auto apex_file = ApexFile::Open(decompressed_file_path); |
| ASSERT_THAT(return_value, |
| UnorderedElementsAre(ApexFileEq(ByRef(*apex_file)))); |
| } |
| |
| // When decompressing APEX, reuse existing OTA APEX |
| TEST_F(ApexdUnitTest, ProcessCompressedApexReuseOtaApex) { |
| // Push a compressed APEX that will fail to decompress |
| auto compressed_apex = ApexFile::Open(AddPreInstalledApex( |
| "com.android.apex.compressed.v1_not_decompressible.capex")); |
| |
| std::vector<ApexFileRef> compressed_apex_list; |
| compressed_apex_list.emplace_back(std::cref(*compressed_apex)); |
| |
| // If we try to decompress capex directly, it should fail since the capex |
| // pushed is faulty and cannot be decompressed |
| auto return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| ASSERT_EQ(return_value.size(), 0u); |
| |
| // But, if there is an ota_apex present for reuse, it should reuse that |
| // and avoid decompressing the faulty capex |
| |
| // Push an OTA apex that should be reused to skip decompression |
| auto ota_apex_path = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| fs::copy(GetTestFile("com.android.apex.compressed.v1.apex"), ota_apex_path); |
| return_value = |
| ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); |
| ASSERT_EQ(return_value.size(), 1u); |
| |
| // Ota Apex should be cleaned up |
| ASSERT_THAT(PathExists(ota_apex_path), HasValue(false)); |
| ASSERT_EQ(return_value[0].GetPath(), |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix)); |
| } |
| |
| TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompressionNewApex) { |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| // A brand new compressed APEX is being introduced: selected |
| bool result = |
| ShouldAllocateSpaceForDecompression("com.android.brand.new", 1, instance); |
| ASSERT_TRUE(result); |
| } |
| |
| TEST_F(ApexdUnitTest, |
| ShouldAllocateSpaceForDecompressionWasNotCompressedBefore) { |
| // Prepare fake pre-installed apex |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| // An existing pre-installed APEX is now compressed in the OTA: selected |
| { |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.test_package", 1, instance); |
| ASSERT_TRUE(result); |
| } |
| |
| // Even if there is a data apex (lower version) |
| // Include data apex within calculation now |
| AddDataApex("apex.apexd_test_v2.apex"); |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| { |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.test_package", 3, instance); |
| ASSERT_TRUE(result); |
| } |
| |
| // But not if data apex has equal or higher version |
| { |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.test_package", 2, instance); |
| ASSERT_FALSE(result); |
| } |
| } |
| |
| TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompressionVersionCompare) { |
| // Prepare fake pre-installed apex |
| PrepareCompressedApex("com.android.apex.compressed.v1.capex"); |
| auto& instance = ApexFileRepository::GetInstance(); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok()); |
| |
| { |
| // New Compressed apex has higher version than decompressed data apex: |
| // selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 2, instance); |
| ASSERT_TRUE(result) |
| << "Higher version test with decompressed data returned false"; |
| } |
| |
| // Compare against decompressed data apex |
| { |
| // New Compressed apex has same version as decompressed data apex: not |
| // selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 1, instance); |
| ASSERT_FALSE(result) |
| << "Same version test with decompressed data returned true"; |
| } |
| |
| { |
| // New Compressed apex has lower version than decompressed data apex: |
| // selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 0, instance); |
| ASSERT_TRUE(result) |
| << "lower version test with decompressed data returned false"; |
| } |
| |
| // Replace decompressed data apex with a higher version |
| ApexFileRepository instance_new(GetDecompressionDir()); |
| ASSERT_THAT(instance_new.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| TemporaryDir data_dir_new; |
| fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), |
| data_dir_new.path); |
| ASSERT_THAT(instance_new.AddDataApex(data_dir_new.path), Ok()); |
| |
| { |
| // New Compressed apex has higher version as data apex: selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 3, instance_new); |
| ASSERT_TRUE(result) << "Higher version test with new data returned false"; |
| } |
| |
| { |
| // New Compressed apex has same version as data apex: not selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 2, instance_new); |
| ASSERT_FALSE(result) << "Same version test with new data returned true"; |
| } |
| |
| { |
| // New Compressed apex has lower version than data apex: not selected |
| bool result = ShouldAllocateSpaceForDecompression( |
| "com.android.apex.compressed", 1, instance_new); |
| ASSERT_FALSE(result) << "lower version test with new data returned true"; |
| } |
| } |
| |
| TEST_F(ApexdUnitTest, CalculateSizeForCompressedApexEmptyList) { |
| ApexFileRepository instance; |
| int64_t result = CalculateSizeForCompressedApex({}, instance); |
| ASSERT_EQ(0LL, result); |
| } |
| |
| TEST_F(ApexdUnitTest, CalculateSizeForCompressedApex) { |
| ApexFileRepository instance; |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| ASSERT_THAT(instance.AddPreInstalledApex({GetBuiltInDir()}), Ok()); |
| |
| std::vector<std::tuple<std::string, int64_t, int64_t>> input = { |
| std::make_tuple("new_apex", 1, 1), |
| std::make_tuple("new_apex_2", 1, 2), |
| std::make_tuple("com.android.apex.compressed", 1, 4), // will be ignored |
| std::make_tuple("com.android.apex.compressed", 2, 8), |
| }; |
| int64_t result = CalculateSizeForCompressedApex(input, instance); |
| ASSERT_EQ(1 + 2 + 8LL, result); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexCreatesSingleFile) { |
| TemporaryDir dest_dir; |
| // Reserving space should create a single file in dest_dir with exact size |
| |
| ASSERT_THAT(ReserveSpaceForCompressedApex(100, dest_dir.path), Ok()); |
| auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 1u); |
| EXPECT_EQ(fs::file_size((*files)[0]), 100u); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexSafeToCallMultipleTimes) { |
| TemporaryDir dest_dir; |
| // Calling ReserveSpaceForCompressedApex multiple times should still create |
| // a single file |
| ASSERT_THAT(ReserveSpaceForCompressedApex(100, dest_dir.path), Ok()); |
| ASSERT_THAT(ReserveSpaceForCompressedApex(100, dest_dir.path), Ok()); |
| auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 1u); |
| EXPECT_EQ(fs::file_size((*files)[0]), 100u); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexShrinkAndGrow) { |
| TemporaryDir dest_dir; |
| |
| // Create a 100 byte file |
| ASSERT_THAT(ReserveSpaceForCompressedApex(100, dest_dir.path), Ok()); |
| |
| // Should be able to shrink and grow the reserved space |
| ASSERT_THAT(ReserveSpaceForCompressedApex(1000, dest_dir.path), Ok()); |
| |
| auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 1u); |
| EXPECT_EQ(fs::file_size((*files)[0]), 1000u); |
| |
| ASSERT_THAT(ReserveSpaceForCompressedApex(10, dest_dir.path), Ok()); |
| files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 1u); |
| EXPECT_EQ(fs::file_size((*files)[0]), 10u); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexDeallocateIfPassedZero) { |
| TemporaryDir dest_dir; |
| |
| // Create a file first |
| ASSERT_THAT(ReserveSpaceForCompressedApex(100, dest_dir.path), Ok()); |
| auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 1u); |
| |
| // Should delete the reserved file if size passed is 0 |
| ASSERT_THAT(ReserveSpaceForCompressedApex(0, dest_dir.path), Ok()); |
| files = ReadDir(dest_dir.path, [](auto _) { return true; }); |
| ASSERT_THAT(files, Ok()); |
| ASSERT_EQ(files->size(), 0u); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCapexCleansOtaApex) { |
| TemporaryDir dest_dir; |
| |
| auto ota_apex_path = StringPrintf( |
| "%s/ota_apex%s", GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| auto create_ota_apex = [&]() { |
| // Create an ota_apex first |
| fs::copy(GetTestFile("com.android.apex.compressed.v1.apex"), ota_apex_path); |
| ASSERT_THAT(PathExists(ota_apex_path), HasValue(true)); |
| }; |
| create_ota_apex(); |
| |
| // Should not delete the reserved file if size passed is negative |
| ASSERT_THAT(ReserveSpaceForCompressedApex(-1, dest_dir.path), Not(Ok())); |
| ASSERT_THAT(PathExists(ota_apex_path), HasValue(true)); |
| |
| // Should delete the reserved file if size passed is 0 |
| ASSERT_THAT(ReserveSpaceForCompressedApex(0, dest_dir.path), Ok()); |
| ASSERT_THAT(PathExists(ota_apex_path), HasValue(false)); |
| |
| create_ota_apex(); |
| // Should delete the reserved file if size passed is positive |
| ASSERT_THAT(ReserveSpaceForCompressedApex(10, dest_dir.path), Ok()); |
| ASSERT_THAT(PathExists(ota_apex_path), HasValue(false)); |
| } |
| |
| TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexErrorForNegativeValue) { |
| TemporaryDir dest_dir; |
| // Should return error if negative value is passed |
| ASSERT_THAT(ReserveSpaceForCompressedApex(-1, dest_dir.path), Not(Ok())); |
| } |
| |
| TEST_F(ApexdUnitTest, GetStagedApexFilesNoChild) { |
| // Create staged session |
| auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); |
| apex_session->UpdateStateAndCommit(SessionState::STAGED); |
| |
| // Query for its file |
| auto result = GetStagedApexFiles(123, {}); |
| |
| auto apex_file = ApexFile::Open( |
| StringPrintf("%s/apex.apexd_test.apex", GetStagedDir(123).c_str())); |
| ASSERT_THAT(result, |
| HasValue(UnorderedElementsAre(ApexFileEq(ByRef(*apex_file))))); |
| } |
| |
| TEST_F(ApexdUnitTest, GetStagedApexFilesOnlyStaged) { |
| // Create staged session |
| auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); |
| apex_session->UpdateStateAndCommit(SessionState::VERIFIED); |
| |
| // Query for its file |
| auto result = GetStagedApexFiles(123, {}); |
| |
| ASSERT_THAT( |
| result, |
| HasError(WithMessage(HasSubstr("Session 123 is not in state STAGED")))); |
| } |
| |
| TEST_F(ApexdUnitTest, GetStagedApexFilesChecksNumberOfApexFiles) { |
| // Create staged session |
| auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); |
| apex_session->UpdateStateAndCommit(SessionState::STAGED); |
| auto staged_dir = GetStagedDir(123); |
| |
| { |
| // Delete the staged apex file |
| DeleteDirContent(staged_dir); |
| |
| // Query for its file |
| auto result = GetStagedApexFiles(123, {}); |
| ASSERT_THAT(result, HasError(WithMessage(HasSubstr( |
| "Expected exactly one APEX file in directory")))); |
| ASSERT_THAT(result, HasError(WithMessage(HasSubstr("Found: 0")))); |
| } |
| { |
| // Copy multiple files to staged dir |
| fs::copy(GetTestFile("apex.apexd_test.apex"), staged_dir); |
| fs::copy(GetTestFile("apex.apexd_test_v2.apex"), staged_dir); |
| |
| // Query for its file |
| auto result = GetStagedApexFiles(123, {}); |
| ASSERT_THAT(result, HasError(WithMessage(HasSubstr( |
| "Expected exactly one APEX file in directory")))); |
| ASSERT_THAT(result, HasError(WithMessage(HasSubstr("Found: 2")))); |
| } |
| } |
| |
| TEST_F(ApexdUnitTest, GetStagedApexFilesWithChildren) { |
| // Create staged session |
| auto parent_apex_session = CreateStagedSession("apex.apexd_test.apex", 123); |
| parent_apex_session->UpdateStateAndCommit(SessionState::STAGED); |
| auto child_session_1 = CreateStagedSession("apex.apexd_test.apex", 124); |
| auto child_session_2 = CreateStagedSession("apex.apexd_test.apex", 125); |
| |
| // Query for its file |
| auto result = GetStagedApexFiles(123, {124, 125}); |
| |
| ASSERT_THAT(result, Ok()); |
| auto child_apex_file_1 = ApexFile::Open( |
| StringPrintf("%s/apex.apexd_test.apex", GetStagedDir(124).c_str())); |
| auto child_apex_file_2 = ApexFile::Open( |
| StringPrintf("%s/apex.apexd_test.apex", GetStagedDir(125).c_str())); |
| ASSERT_THAT(*result, |
| UnorderedElementsAre(ApexFileEq(ByRef(*child_apex_file_1)), |
| ApexFileEq(ByRef(*child_apex_file_2)))); |
| } |
| |
| // A test fixture to use for tests that mount/unmount apexes. |
| // This also supports test-purpose BlockApex via mount. |
| class ApexdMountTest : public ApexdUnitTest { |
| public: |
| ApexdMountTest() { |
| vm_payload_disk_ = StringPrintf("%s/vm-payload", td_.path); |
| } |
| |
| void UnmountOnTearDown(const std::string& apex_file) { |
| to_unmount_.push_back(apex_file); |
| } |
| |
| protected: |
| void SetUp() final { |
| ApexdUnitTest::SetUp(); |
| GetApexDatabaseForTesting().Reset(); |
| GetChangedActiveApexesForTesting().clear(); |
| ASSERT_THAT(SetUpApexTestEnvironment(), Ok()); |
| } |
| |
| void TearDown() final { |
| ApexdUnitTest::TearDown(); |
| SetBlockApexEnabled(false); |
| for (const auto& apex : to_unmount_) { |
| if (auto status = DeactivatePackage(apex); !status.ok()) { |
| LOG(ERROR) << "Failed to unmount " << apex << " : " << status.error(); |
| } |
| } |
| } |
| |
| void SetBlockApexEnabled(bool enabled) { |
| // The first partition(1) is "metadata" partition |
| base::SetProperty(kTestVmPayloadMetadataPartitionProp, |
| enabled ? (vm_payload_disk_ + "1") : ""); |
| } |
| |
| std::string AddBlockApex(const std::string& apex_name, |
| const std::string& public_key = "", |
| const std::string& root_digest = "", |
| bool is_factory = true) { |
| auto apex_path = vm_payload_disk_ + std::to_string(block_device_index_++); |
| auto apex_file = GetTestFile(apex_name); |
| AddToMetadata(apex_name, public_key, root_digest, is_factory); |
| // block_apexes_ will be disposed after each test |
| auto block_apex = WriteBlockApex(apex_file, apex_path); |
| if (!block_apex.ok()) { |
| PLOG(ERROR) << block_apex.error(); |
| } |
| block_apexes_.push_back(std::move(*block_apex)); |
| return apex_path; |
| } |
| |
| void AddToMetadata(const std::string& apex_name, |
| const std::string& public_key, |
| const std::string& root_digest, bool is_factory) { |
| android::microdroid::Metadata metadata; |
| // The first partition is metadata partition |
| auto metadata_partition = vm_payload_disk_ + "1"; |
| if (access(metadata_partition.c_str(), F_OK) == 0) { |
| auto result = android::microdroid::ReadMetadata(metadata_partition); |
| ASSERT_THAT(result, Ok()); |
| metadata = *result; |
| } |
| |
| auto apex = metadata.add_apexes(); |
| apex->set_name(apex_name); |
| apex->set_public_key(public_key); |
| apex->set_root_digest(root_digest); |
| apex->set_is_factory(is_factory); |
| |
| std::ofstream out(metadata_partition); |
| ASSERT_THAT(android::microdroid::WriteMetadata(metadata, out), Ok()); |
| } |
| |
| private: |
| MountNamespaceRestorer restorer_; |
| std::vector<std::string> to_unmount_; |
| |
| // Block APEX specific stuff. |
| std::string vm_payload_disk_; |
| int block_device_index_ = 2; // "1" is reserved for metadata; |
| // This should be freed before ~MountNamespaceRestorer() because it |
| // switches to the original mount namespace while block apexes are mounted |
| // in test-purpose mount namespace. |
| std::vector<BlockApex> block_apexes_; |
| }; |
| |
| // TODO(b/187864524): cover other negative scenarios. |
| TEST_F(ApexdMountTest, InstallPackageRejectsApexWithoutRebootlessSupport) { |
| std::string file_path = AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("apex.apexd_test.apex"), /* force= */ false); |
| ASSERT_THAT( |
| ret, |
| HasError(WithMessage(HasSubstr("does not support non-staged update")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsNoPreInstalledApex) { |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex"), |
| /* force= */ false); |
| ASSERT_THAT( |
| ret, HasError(WithMessage(HasSubstr( |
| "No active version found for package test.apex.rebootless")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsNoHashtree) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("test.rebootless_apex_v2_no_hashtree.apex"), |
| /* force= */ false); |
| ASSERT_THAT( |
| ret, |
| HasError(WithMessage(HasSubstr(" does not have an embedded hash tree")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsNoActiveApex) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT( |
| ret, HasError(WithMessage(HasSubstr( |
| "No active version found for package test.apex.rebootless")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsManifestMismatch) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("test.rebootless_apex_manifest_mismatch.apex"), |
| /* force= */ false); |
| ASSERT_THAT( |
| ret, |
| HasError(WithMessage(HasSubstr( |
| "Manifest inside filesystem does not match manifest outside it")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsCorrupted) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_corrupted.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, |
| HasError(WithMessage(HasSubstr("Can't verify /dev/block/dm-")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsProvidesSharedLibs) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage( |
| GetTestFile("test.rebootless_apex_provides_sharedlibs.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, HasError(WithMessage(HasSubstr(" is a shared libs APEX")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsProvidesNativeLibs) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage( |
| GetTestFile("test.rebootless_apex_provides_native_libs.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, HasError(WithMessage(HasSubstr(" provides native libs")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsRequiresSharedApexLibs) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage( |
| GetTestFile("test.rebootless_apex_requires_shared_apex_libs.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, |
| HasError(WithMessage(HasSubstr(" requires shared apex libs")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsJniLibs) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_jni_libs.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, HasError(WithMessage(HasSubstr(" requires JNI libs")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageAcceptsAddRequiredNativeLib) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("test.rebootless_apex_add_native_lib.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageAcceptsRemoveRequiredNativeLib) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("test.rebootless_apex_remove_native_lib.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsAppInApex) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage( |
| GetTestFile("test.rebootless_apex_app_in_apex.apex"), /* force= */ false); |
| ASSERT_THAT(ret, HasError(WithMessage(HasSubstr("contains app inside")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageRejectsPrivAppInApex) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = |
| InstallPackage(GetTestFile("test.rebootless_apex_priv_app_in_apex.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, |
| HasError(WithMessage(HasSubstr("contains priv-app inside")))); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackagePreInstallVersionActive) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@2")); |
| |
| // Check that /apex/test.apex.rebootless is a bind mount of |
| // /apex/test.apex.rebootless@2. |
| auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); |
| ASSERT_THAT(manifest, Ok()); |
| ASSERT_EQ(2u, manifest->version()); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); |
| |
| // Check that pre-installed APEX is still around |
| ASSERT_EQ(0, access(file_path.c_str(), F_OK)) |
| << "Can't access " << file_path << " : " << strerror(errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, ret->GetPath()); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackagePreInstallVersionActiveSamegrade) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@1")); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); |
| |
| // Check that pre-installed APEX is still around |
| ASSERT_EQ(0, access(file_path.c_str(), F_OK)) |
| << "Can't access " << file_path << " : " << strerror(errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, ret->GetPath()); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@1_1"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageUnloadOldApex) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| bool unloaded = false; |
| bool loaded = false; |
| const std::string prop = "apex.test.apex.rebootless.ready"; |
| std::thread monitor_apex_ready_prop([&]() { |
| unloaded = base::WaitForProperty(prop, "false", 10s); |
| loaded = base::WaitForProperty(prop, "true", 10s); |
| }); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| monitor_apex_ready_prop.join(); |
| ASSERT_TRUE(unloaded); |
| ASSERT_TRUE(loaded); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageWithService) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_service_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_service_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); |
| ASSERT_THAT(manifest, Ok()); |
| ASSERT_EQ(2u, manifest->version()); |
| UnmountOnTearDown(ret->GetPath()); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageDataVersionActive) { |
| AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("test.rebootless_apex_v1.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@2")); |
| |
| // Check that /apex/test.apex.rebootless is a bind mount of |
| // /apex/test.apex.rebootless@2. |
| auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); |
| ASSERT_THAT(manifest, Ok()); |
| ASSERT_EQ(2u, manifest->version()); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); |
| |
| // Check that previously active APEX was deleted. |
| ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); |
| ASSERT_EQ(ENOENT, errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, ret->GetPath()); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageResolvesPathCollision) { |
| AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("test.rebootless_apex_v1.apex", |
| "test.apex.rebootless@1_1.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@1")); |
| |
| // Check that /apex/test.apex.rebootless is a bind mount of |
| // /apex/test.apex.rebootless@2. |
| auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); |
| ASSERT_THAT(manifest, Ok()); |
| ASSERT_EQ(1u, manifest->version()); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); |
| |
| // Check that we correctly resolved active apex path collision. |
| ASSERT_EQ(active_apex->GetPath(), |
| GetDataDir() + "/test.apex.rebootless@1_2.apex"); |
| |
| // Check that previously active APEX was deleted. |
| ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); |
| ASSERT_EQ(ENOENT, errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, ret->GetPath()); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@1_2"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageDataVersionActiveSamegrade) { |
| AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("test.rebootless_apex_v2.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@2")); |
| |
| // Check that /apex/test.apex.rebootless is a bind mount of |
| // /apex/test.apex.rebootless@2. |
| auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); |
| ASSERT_THAT(manifest, Ok()); |
| ASSERT_EQ(2u, manifest->version()); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); |
| |
| // Check that previously active APEX was deleted. |
| ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); |
| ASSERT_EQ(ENOENT, errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, ret->GetPath()); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageUnmountFailsPreInstalledApexActive) { |
| std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| unique_fd fd(open("/apex/test.apex.rebootless/apex_manifest.pb", |
| O_RDONLY | O_CLOEXEC)); |
| ASSERT_NE(-1, fd.get()); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Not(Ok())); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@1")); |
| |
| // Check that GetActivePackage correctly reports upgraded version. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| |
| // Check that old APEX is still around |
| ASSERT_EQ(0, access(file_path.c_str(), F_OK)) |
| << "Can't access " << file_path << " : " << strerror(errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that upgraded APEX is mounted on top of dm-verity device. |
| db.ForallMountedApexes("test.apex.rebootless", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, file_path); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageUnmountFailedUpdatedApexActive) { |
| AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("test.rebootless_apex_v1.apex"); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| { |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| } |
| |
| unique_fd fd(open("/apex/test.apex.rebootless/apex_manifest.pb", |
| O_RDONLY | O_CLOEXEC)); |
| ASSERT_NE(-1, fd.get()); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Not(Ok())); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/test.apex.rebootless", |
| "/apex/test.apex.rebootless@1")); |
| |
| // Check that GetActivePackage correctly reports old apex. |
| auto active_apex = GetActivePackage("test.apex.rebootless"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| |
| // Check that old APEX is still around |
| ASSERT_EQ(0, access(file_path.c_str(), F_OK)) |
| << "Can't access " << file_path << " : " << strerror(errno); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| db.ForallMountedApexes( |
| "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, file_path); |
| ASSERT_EQ(data.device_name, "test.apex.rebootless@1"); |
| }); |
| } |
| |
| TEST_F(ApexdMountTest, InstallPackageUpdatesApexInfoList) { |
| auto apex_1 = AddPreInstalledApex("test.rebootless_apex_v1.apex"); |
| auto apex_2 = AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| UnmountOnTearDown(apex_1); |
| UnmountOnTearDown(apex_2); |
| ASSERT_THAT(ActivatePackage(apex_1), Ok()); |
| ASSERT_THAT(ActivatePackage(apex_2), Ok()); |
| |
| // Call OnAllPackagesActivated to create /apex/apex-info-list.xml. |
| OnAllPackagesActivated(/* is_bootstrap= */ false); |
| // Check /apex/apex-info-list.xml was created. |
| ASSERT_EQ(0, access("/apex/apex-info-list.xml", F_OK)); |
| |
| auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"), |
| /* force= */ false); |
| ASSERT_THAT(ret, Ok()); |
| UnmountOnTearDown(ret->GetPath()); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "test.apex.rebootless", |
| /* modulePath= */ apex_1, |
| /* preinstalledModulePath= */ apex_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_2, /* preinstalledModulePath= */ apex_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_2), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_3 = com::android::apex::ApexInfo( |
| /* moduleName= */ "test.apex.rebootless", |
| /* modulePath= */ ret->GetPath(), |
| /* preinstalledModulePath= */ apex_1, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(ret->GetPath()), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2), |
| ApexInfoXmlEq(apex_info_xml_3))); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageBannedName) { |
| auto status = ActivatePackage(GetTestFile("sharedlibs.apex")); |
| ASSERT_THAT(status, |
| HasError(WithMessage("Package name sharedlibs is not allowed."))); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageNoCode) { |
| std::string file_path = AddPreInstalledApex("apex.apexd_test_nocode.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| std::string mountinfo; |
| ASSERT_TRUE(ReadFileToString("/proc/self/mountinfo", &mountinfo)); |
| bool found_apex_mountpoint = false; |
| for (const auto& line : Split(mountinfo, "\n")) { |
| std::vector<std::string> tokens = Split(line, " "); |
| // line format: |
| // mnt_id parent_mnt_id major:minor source target option propagation_type |
| // ex) 33 260:19 / /apex rw,nosuid,nodev - |
| if (tokens.size() >= 7 && |
| tokens[4] == "/apex/com.android.apex.test_package@1") { |
| found_apex_mountpoint = true; |
| // Make sure that option contains noexec |
| std::vector<std::string> options = Split(tokens[5], ","); |
| EXPECT_THAT(options, Contains("noexec")); |
| break; |
| } |
| } |
| EXPECT_TRUE(found_apex_mountpoint); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageManifestMissmatch) { |
| std::string file_path = |
| AddPreInstalledApex("apex.apexd_test_manifest_mismatch.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| auto status = ActivatePackage(file_path); |
| ASSERT_THAT( |
| status, |
| HasError(WithMessage(HasSubstr( |
| "Manifest inside filesystem does not match manifest outside it")))); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackage) { |
| std::string file_path = AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto active_apex = GetActivePackage("com.android.apex.test_package"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1")); |
| |
| ASSERT_THAT(DeactivatePackage(file_path), Ok()); |
| ASSERT_THAT(GetActivePackage("com.android.apex.test_package"), Not(Ok())); |
| |
| auto new_apex_mounts = GetApexMounts(); |
| ASSERT_EQ(new_apex_mounts.size(), 0u); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageShowsUpInMountedApexDatabase) { |
| std::string file_path = AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| auto active_apex = GetActivePackage("com.android.apex.test_package"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1")); |
| |
| // Check that mounted apex database contains information about our APEX. |
| auto& db = GetApexDatabaseForTesting(); |
| std::optional<MountedApexData> mounted_apex; |
| db.ForallMountedApexes("com.android.apex.test_package", |
| [&](const MountedApexData& d, bool active) { |
| if (active) { |
| mounted_apex.emplace(d); |
| } |
| }); |
| ASSERT_TRUE(mounted_apex) |
| << "Haven't found com.android.apex.test_package in the database of " |
| << "mounted apexes"; |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageNoHashtree) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("apex.apexd_test_no_hashtree.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| // Check that hashtree was generated |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1"; |
| ASSERT_EQ(0, access(hashtree_path.c_str(), F_OK)); |
| |
| // Check that block device can be read. |
| auto block_device = GetBlockDeviceForApex("com.android.apex.test_package@1"); |
| ASSERT_THAT(block_device, Ok()); |
| ASSERT_THAT(ReadDevice(*block_device), Ok()); |
| } |
| |
| TEST_F(ApexdMountTest, ActivatePackageNoHashtreeShowsUpInMountedDatabase) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("apex.apexd_test_no_hashtree.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| // Get loop devices that were used to mount APEX. |
| auto children = ListChildLoopDevices("com.android.apex.test_package@1"); |
| ASSERT_THAT(children, Ok()); |
| ASSERT_EQ(2u, children->size()) |
| << "Unexpected number of children: " << Join(*children, ","); |
| |
| auto& db = GetApexDatabaseForTesting(); |
| std::optional<MountedApexData> mounted_apex; |
| db.ForallMountedApexes("com.android.apex.test_package", |
| [&](const MountedApexData& d, bool active) { |
| if (active) { |
| mounted_apex.emplace(d); |
| } |
| }); |
| ASSERT_TRUE(mounted_apex) |
| << "Haven't found com.android.apex.test_package@1 in the database of " |
| << "mounted apexes"; |
| |
| ASSERT_EQ(file_path, mounted_apex->full_path); |
| ASSERT_EQ("/apex/com.android.apex.test_package@1", mounted_apex->mount_point); |
| ASSERT_EQ("com.android.apex.test_package@1", mounted_apex->device_name); |
| // For loops we only check that both loop_name and hashtree_loop_name are |
| // children of the top device mapper device. |
| ASSERT_THAT(*children, Contains(mounted_apex->loop_name)); |
| ASSERT_THAT(*children, Contains(mounted_apex->hashtree_loop_name)); |
| ASSERT_NE(mounted_apex->loop_name, mounted_apex->hashtree_loop_name); |
| } |
| |
| TEST_F(ApexdMountTest, DeactivePackageFreesLoopDevices) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("apex.apexd_test_no_hashtree.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| // Get loop devices that were used to mount APEX. |
| auto children = ListChildLoopDevices("com.android.apex.test_package@1"); |
| ASSERT_THAT(children, Ok()); |
| ASSERT_EQ(2u, children->size()) |
| << "Unexpected number of children: " << Join(*children, ","); |
| |
| ASSERT_THAT(DeactivatePackage(file_path), Ok()); |
| for (const auto& loop : *children) { |
| struct loop_info li; |
| unique_fd fd(TEMP_FAILURE_RETRY(open(loop.c_str(), O_RDWR | O_CLOEXEC))); |
| EXPECT_NE(-1, fd.get()) |
| << "Failed to open " << loop << " : " << strerror(errno); |
| EXPECT_EQ(-1, ioctl(fd.get(), LOOP_GET_STATUS, &li)) |
| << loop << " is still alive"; |
| EXPECT_EQ(ENXIO, errno) << "Unexpected errno : " << strerror(errno); |
| } |
| } |
| |
| TEST_F(ApexdMountTest, NoHashtreeApexNewSessionDoesNotImpactActivePackage) { |
| MockCheckpointInterface checkpoint_interface; |
| checkpoint_interface.SetSupportsCheckpoint(true); |
| InitializeVold(&checkpoint_interface); |
| |
| AddPreInstalledApex("apex.apexd_test_no_hashtree.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("apex.apexd_test_no_hashtree.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| ASSERT_THAT(CreateStagedSession("apex.apexd_test_no_hashtree_2.apex", 239), |
| Ok()); |
| auto status = |
| SubmitStagedSession(239, {}, /* has_rollback_enabled= */ false, |
| /* is_rollback= */ false, /* rollback_id= */ -1); |
| ASSERT_THAT(status, Ok()); |
| |
| // Check that new hashtree file was created. |
| { |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1.new"; |
| ASSERT_THAT(PathExists(hashtree_path), HasValue(true)) |
| << hashtree_path << " does not exist"; |
| } |
| // Check that active hashtree is still there. |
| { |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1"; |
| ASSERT_THAT(PathExists(hashtree_path), HasValue(true)) |
| << hashtree_path << " does not exist"; |
| } |
| |
| // Check that block device of active APEX can still be read. |
| auto block_device = GetBlockDeviceForApex("com.android.apex.test_package@1"); |
| ASSERT_THAT(block_device, Ok()); |
| ASSERT_THAT(ReadDevice(*block_device), Ok()); |
| } |
| |
| TEST_F(ApexdMountTest, NoHashtreeApexStagePackagesMovesHashtree) { |
| MockCheckpointInterface checkpoint_interface; |
| checkpoint_interface.SetSupportsCheckpoint(true); |
| InitializeVold(&checkpoint_interface); |
| |
| AddPreInstalledApex("apex.apexd_test_no_hashtree.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| auto read_fn = [](const std::string& path) -> std::vector<uint8_t> { |
| static constexpr size_t kBufSize = 4096; |
| std::vector<uint8_t> buffer(kBufSize); |
| unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY | O_CLOEXEC))); |
| if (fd.get() == -1) { |
| PLOG(ERROR) << "Failed to open " << path; |
| ADD_FAILURE(); |
| return buffer; |
| } |
| if (!ReadFully(fd.get(), buffer.data(), kBufSize)) { |
| PLOG(ERROR) << "Failed to read " << path; |
| ADD_FAILURE(); |
| } |
| return buffer; |
| }; |
| |
| ASSERT_THAT(CreateStagedSession("apex.apexd_test_no_hashtree_2.apex", 37), |
| Ok()); |
| auto status = |
| SubmitStagedSession(37, {}, /* has_rollback_enabled= */ false, |
| /* is_rollback= */ false, /* rollback_id= */ -1); |
| ASSERT_THAT(status, Ok()); |
| auto staged_apex = std::move((*status)[0]); |
| |
| // Check that new hashtree file was created. |
| std::vector<uint8_t> original_hashtree_data; |
| { |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1.new"; |
| ASSERT_THAT(PathExists(hashtree_path), HasValue(true)); |
| original_hashtree_data = read_fn(hashtree_path); |
| } |
| |
| ASSERT_THAT(StagePackages({staged_apex.GetPath()}), Ok()); |
| // Check that hashtree file was moved. |
| { |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1.new"; |
| ASSERT_THAT(PathExists(hashtree_path), HasValue(false)); |
| } |
| { |
| std::string hashtree_path = |
| GetHashTreeDir() + "/com.android.apex.test_package@1"; |
| ASSERT_THAT(PathExists(hashtree_path), HasValue(true)); |
| std::vector<uint8_t> moved_hashtree_data = read_fn(hashtree_path); |
| ASSERT_EQ(moved_hashtree_data, original_hashtree_data); |
| } |
| } |
| |
| TEST_F(ApexdMountTest, DeactivePackageTearsDownVerityDevice) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| std::string file_path = AddDataApex("apex.apexd_test_v2.apex"); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| UnmountOnTearDown(file_path); |
| |
| ASSERT_THAT(DeactivatePackage(file_path), Ok()); |
| auto& dm = DeviceMapper::Instance(); |
| ASSERT_EQ(dm::DmDeviceState::INVALID, |
| dm.GetState("com.android.apex.test_package@2")); |
| } |
| |
| TEST_F(ApexdMountTest, ActivateDeactivateSharedLibsApex) { |
| ASSERT_EQ(mkdir("/apex/sharedlibs", 0755), 0); |
| ASSERT_EQ(mkdir("/apex/sharedlibs/lib", 0755), 0); |
| ASSERT_EQ(mkdir("/apex/sharedlibs/lib64", 0755), 0); |
| auto deleter = make_scope_guard([]() { |
| std::error_code ec; |
| fs::remove_all("/apex/sharedlibs", ec); |
| if (ec) { |
| LOG(ERROR) << "Failed to delete /apex/sharedlibs : " << ec; |
| } |
| }); |
| |
| std::string file_path = AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| |
| UnmountOnTearDown(file_path); |
| ASSERT_THAT(ActivatePackage(file_path), Ok()); |
| |
| auto active_apex = GetActivePackage("com.android.apex.test.sharedlibs"); |
| ASSERT_THAT(active_apex, Ok()); |
| ASSERT_EQ(active_apex->GetPath(), file_path); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test.sharedlibs@1")); |
| |
| ASSERT_THAT(DeactivatePackage(file_path), Ok()); |
| ASSERT_THAT(GetActivePackage("com.android.apex.test.sharedlibs"), Not(Ok())); |
| |
| auto new_apex_mounts = GetApexMounts(); |
| ASSERT_EQ(new_apex_mounts.size(), 0u); |
| } |
| |
| TEST_F(ApexdMountTest, RemoveInactiveDataApex) { |
| AddPreInstalledApex("com.android.apex.compressed.v2.capex"); |
| // Add a decompressed apex that will not be mounted, so should be removed |
| auto decompressed_apex = StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| fs::copy(GetTestFile("com.android.apex.compressed.v1.apex"), |
| decompressed_apex); |
| // Add a decompressed apex that will be mounted, so should be not be removed |
| auto active_decompressed_apex = StringPrintf( |
| "%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), |
| active_decompressed_apex); |
| // Apex that do not have kDecompressedApexPackageSuffix, should not be removed |
| // from decompression_dir |
| auto decompressed_different_suffix = |
| StringPrintf("%s/com.android.apex.compressed@2%s", |
| GetDecompressionDir().c_str(), kApexPackageSuffix); |
| fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), |
| decompressed_different_suffix); |
| |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| auto data_apex = AddDataApex("apex.apexd_test.apex"); |
| auto active_data_apex = AddDataApex("apex.apexd_test_v2.apex"); |
| |
| // Activate some of the apex |
| ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); |
| UnmountOnTearDown(active_decompressed_apex); |
| UnmountOnTearDown(active_data_apex); |
| ASSERT_THAT(ActivatePackage(active_decompressed_apex), Ok()); |
| ASSERT_THAT(ActivatePackage(active_data_apex), Ok()); |
| // Clean up inactive apex packages |
| RemoveInactiveDataApex(); |
| |
| // Verify inactive apex packages have been deleted |
| ASSERT_TRUE(*PathExists(active_decompressed_apex)); |
| ASSERT_TRUE(*PathExists(active_data_apex)); |
| ASSERT_TRUE(*PathExists(decompressed_different_suffix)); |
| ASSERT_FALSE(*PathExists(decompressed_apex)); |
| ASSERT_FALSE(*PathExists(data_apex)); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapOnlyPreInstalledApexes) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| UnmountOnTearDown(apex_path_1); |
| UnmountOnTearDown(apex_path_2); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapFailsToScanPreInstalledApexes) { |
| AddPreInstalledApex("apex.apexd_test.apex"); |
| AddPreInstalledApex("apex.apexd_test_corrupt_superblock_apex.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 1); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataHasHigherVersion) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_2); |
| UnmountOnTearDown(apex_path_3); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@2", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_3 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_3, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_3), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2), |
| ApexInfoXmlEq(apex_info_xml_3))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataHasSameVersion) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| std::string apex_path_3 = AddDataApex("apex.apexd_test.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_2); |
| UnmountOnTearDown(apex_path_3); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_3 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_3, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_3), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2), |
| ApexInfoXmlEq(apex_info_xml_3))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapSystemHasHigherVersion) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test_v2.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| AddDataApex("apex.apexd_test.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_1); |
| UnmountOnTearDown(apex_path_2); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@2", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataHasSameVersionButDifferentKey) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| AddDataApex("apex.apexd_test_different_key.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_1); |
| UnmountOnTearDown(apex_path_2); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2))); |
| } |
| |
| TEST_F(ApexdMountTest, |
| OnOtaChrootBootstrapDataHasHigherVersionButDifferentKey) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = |
| AddPreInstalledApex("apex.apexd_test_different_app.apex"); |
| std::string apex_path_3 = |
| AddDataApex("apex.apexd_test_different_key_v2.apex"); |
| |
| { |
| auto apex = ApexFile::Open(apex_path_3); |
| ASSERT_THAT(apex, Ok()); |
| ASSERT_EQ(static_cast<uint64_t>(apex->GetManifest().version()), 2ULL); |
| } |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_1); |
| UnmountOnTearDown(apex_path_2); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1", |
| "/apex/com.android.apex.test_package_2", |
| "/apex/com.android.apex.test_package_2@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package_2", |
| /* modulePath= */ apex_path_2, /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", /* isFactory= */ true, |
| /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataApexWithoutPreInstalledApex) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| AddDataApex("apex.apexd_test_different_app.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_1); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1))); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapPreInstalledSharedLibsApex) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); |
| std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_2); |
| UnmountOnTearDown(apex_path_3); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@2", |
| "/apex/com.android.apex.test.sharedlibs@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test.sharedlibs", |
| /* modulePath= */ apex_path_2, |
| /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_3 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_3, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_3), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2), |
| ApexInfoXmlEq(apex_info_xml_3))); |
| |
| ASSERT_EQ(access("/apex/sharedlibs", F_OK), 0); |
| |
| // Check /apex/sharedlibs is populated properly. |
| std::vector<std::string> sharedlibs; |
| for (const auto& p : fs::recursive_directory_iterator("/apex/sharedlibs")) { |
| if (fs::is_symlink(p)) { |
| auto src = fs::read_symlink(p.path()); |
| ASSERT_EQ(p.path().filename(), src.filename()); |
| sharedlibs.push_back(p.path().parent_path().string() + "->" + |
| src.parent_path().string()); |
| } |
| } |
| |
| std::vector<std::string> expected = { |
| "/apex/sharedlibs/lib/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib/libsharedlibtest.so", |
| "/apex/sharedlibs/lib/libc++.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib/libc++.so", |
| }; |
| |
| // On 64bit devices we also have lib64. |
| if (!GetProperty("ro.product.cpu.abilist64", "").empty()) { |
| expected.push_back( |
| "/apex/sharedlibs/lib64/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib64/libsharedlibtest.so"); |
| expected.push_back( |
| "/apex/sharedlibs/lib64/libc++.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib64/libc++.so"); |
| } |
| ASSERT_THAT(sharedlibs, UnorderedElementsAreArray(expected)); |
| } |
| |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapSharedLibsApexBothVersions) { |
| std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); |
| std::string apex_path_2 = AddPreInstalledApex( |
| "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); |
| std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); |
| std::string apex_path_4 = |
| AddDataApex("com.android.apex.test.sharedlibs_generated.v2.libvY.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| UnmountOnTearDown(apex_path_2); |
| UnmountOnTearDown(apex_path_3); |
| UnmountOnTearDown(apex_path_4); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.test_package", |
| "/apex/com.android.apex.test_package@2", |
| "/apex/com.android.apex.test.sharedlibs@1", |
| "/apex/com.android.apex.test.sharedlibs@2")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_1 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_1, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_2 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test.sharedlibs", |
| /* modulePath= */ apex_path_2, |
| /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_2), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_3 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test_package", |
| /* modulePath= */ apex_path_3, |
| /* preinstalledModulePath= */ apex_path_1, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_3), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_4 = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.test.sharedlibs", |
| /* modulePath= */ apex_path_4, |
| /* preinstalledModulePath= */ apex_path_2, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_4), |
| /* provideSharedApexLibs= */ false); |
| |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), |
| ApexInfoXmlEq(apex_info_xml_2), |
| ApexInfoXmlEq(apex_info_xml_3), |
| ApexInfoXmlEq(apex_info_xml_4))); |
| |
| ASSERT_EQ(access("/apex/sharedlibs", F_OK), 0); |
| |
| // Check /apex/sharedlibs is populated properly. |
| // Because we don't want to hardcode full paths (they are pretty long and have |
| // a hash in them which might change if new prebuilts are dropped in), the |
| // assertion logic is a little bit clunky. |
| std::vector<std::string> sharedlibs; |
| for (const auto& p : fs::recursive_directory_iterator("/apex/sharedlibs")) { |
| if (fs::is_symlink(p)) { |
| auto src = fs::read_symlink(p.path()); |
| ASSERT_EQ(p.path().filename(), src.filename()); |
| sharedlibs.push_back(p.path().parent_path().string() + "->" + |
| src.parent_path().string()); |
| } |
| } |
| |
| std::vector<std::string> expected = { |
| "/apex/sharedlibs/lib/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@2/lib/libsharedlibtest.so", |
| "/apex/sharedlibs/lib/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib/libsharedlibtest.so", |
| "/apex/sharedlibs/lib/libc++.so->" |
| "/apex/com.android.apex.test.sharedlibs@2/lib/libc++.so", |
| }; |
| // On 64bit devices we also have lib64. |
| if (!GetProperty("ro.product.cpu.abilist64", "").empty()) { |
| expected.push_back( |
| "/apex/sharedlibs/lib64/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@2/lib64/libsharedlibtest.so"); |
| expected.push_back( |
| "/apex/sharedlibs/lib64/libsharedlibtest.so->" |
| "/apex/com.android.apex.test.sharedlibs@1/lib64/libsharedlibtest.so"); |
| expected.push_back( |
| "/apex/sharedlibs/lib64/libc++.so->" |
| "/apex/com.android.apex.test.sharedlibs@2/lib64/libc++.so"); |
| } |
| |
| ASSERT_THAT(sharedlibs, UnorderedElementsAreArray(expected)); |
| } |
| |
| // Test when we move from uncompressed APEX to CAPEX via ota |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapOnlyCompressedApexes) { |
| std::string apex_path = |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Decompressed APEX should be mounted from decompression_dir |
| std::string decompressed_apex = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(decompressed_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, decompressed_apex); |
| ASSERT_EQ(data.device_name, |
| "com.android.apex.compressed@1.chroot"); |
| }); |
| } |
| |
| // Test we decompress only once even if OnOtaChrootBootstrap is called multiple |
| // times |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDecompressOnlyOnceMultipleCalls) { |
| std::string apex_path = |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Decompressed OTA APEX should be mounted |
| std::string decompressed_ota_apex = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_ota_apex); |
| |
| // Capture the creation time of the OTA APEX |
| std::error_code ec; |
| auto last_write_time_1 = fs::last_write_time(decompressed_ota_apex, ec); |
| ASSERT_FALSE(ec) << "Failed to capture last write time of " |
| << decompressed_ota_apex; |
| |
| // Call OnOtaChrootBootstrap again. Since we do not hardlink decompressed APEX |
| // to /data/apex/active directory when in chroot, when selecting apex for |
| // activation, we will end up selecting compressed APEX again. |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Compare write time to ensure we did not decompress again |
| auto last_write_time_2 = fs::last_write_time(decompressed_ota_apex, ec); |
| ASSERT_FALSE(ec) << "Failed to capture last write time of " |
| << decompressed_ota_apex << ec.message(); |
| ASSERT_EQ(last_write_time_1, last_write_time_2); |
| } |
| |
| // Test when we upgrade existing CAPEX to higher version via OTA |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapUpgradeCapex) { |
| TemporaryDir previous_built_in_dir; |
| PrepareCompressedApex("com.android.apex.compressed.v1.capex", |
| previous_built_in_dir.path); |
| // Place a higher version capex in current built_in_dir |
| std::string apex_path = |
| AddPreInstalledApex("com.android.apex.compressed.v2.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Upgraded decompressed APEX should be mounted from decompression dir |
| std::string decompressed_active_apex = |
| StringPrintf("%s/com.android.apex.compressed@2%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_active_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@2")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_active_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ true, /* isActive= */ true, |
| GetMTime(decompressed_active_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, decompressed_active_apex); |
| ASSERT_EQ(data.device_name, |
| "com.android.apex.compressed@2.chroot"); |
| }); |
| } |
| |
| // Test when we update existing CAPEX to same version via OTA |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapex) { |
| TemporaryDir previous_built_in_dir; |
| PrepareCompressedApex("com.android.apex.compressed.v1.capex", |
| previous_built_in_dir.path); |
| // Place a same version capex in current built_in_dir, under a different name |
| auto apex_path = |
| StringPrintf("%s/different-name.capex", GetBuiltInDir().c_str()); |
| fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), apex_path); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Previously decompressed APEX should be mounted from decompression_dir |
| std::string decompressed_active_apex = StringPrintf( |
| "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| UnmountOnTearDown(decompressed_active_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_active_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, |
| GetMTime(decompressed_active_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, decompressed_active_apex); |
| ASSERT_EQ(data.device_name, |
| "com.android.apex.compressed@1.chroot"); |
| }); |
| } |
| |
| // Test when we update existing CAPEX to same version, but different digest |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapexDifferentDigest) { |
| TemporaryDir previous_built_in_dir; |
| auto different_digest_apex_path = PrepareCompressedApex( |
| "com.android.apex.compressed.v1_different_digest.capex", |
| previous_built_in_dir.path); |
| // Place a same version capex in current built_in_dir, which has different |
| // digest |
| auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // New decompressed ota APEX should be mounted with kOtaApexPackageSuffix |
| std::string decompressed_ota_apex = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_ota_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_ota_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, |
| GetMTime(decompressed_ota_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, decompressed_ota_apex); |
| ASSERT_EQ(data.device_name, |
| "com.android.apex.compressed@1.chroot"); |
| }); |
| |
| // Ensure decompressed apex has same digest as pre-installed |
| auto pre_installed_apex = ApexFile::Open(apex_path); |
| auto decompressed_apex = ApexFile::Open(decompressed_ota_apex); |
| auto different_digest_apex = ApexFile::Open(different_digest_apex_path); |
| ASSERT_EQ( |
| pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(), |
| GetRootDigest(*decompressed_apex)); |
| ASSERT_NE( |
| pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(), |
| GetRootDigest(*different_digest_apex)); |
| |
| // Ensure we didn't remove previous decompressed APEX |
| std::string previous_decompressed_apex = StringPrintf( |
| "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), |
| kDecompressedApexPackageSuffix); |
| auto path_exists = PathExists(previous_decompressed_apex); |
| ASSERT_TRUE(*path_exists); |
| } |
| |
| // Test when we update existing CAPEX to same version, but different key via OTA |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapexDifferentKey) { |
| TemporaryDir previous_built_in_dir; |
| PrepareCompressedApex("com.android.apex.compressed_different_key.capex", |
| previous_built_in_dir.path); |
| // Place a same version capex in current built_in_dir, which has different key |
| auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // New decompressed APEX should be mounted from ota_reserved directory |
| std::string decompressed_active_apex = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_active_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_active_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, |
| GetMTime(decompressed_active_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, decompressed_active_apex); |
| ASSERT_EQ(data.device_name, |
| "com.android.apex.compressed@1.chroot"); |
| }); |
| } |
| |
| // Test when we remove CAPEX via OTA |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapCapexToApex) { |
| TemporaryDir previous_built_in_dir; |
| PrepareCompressedApex("com.android.apex.compressed.v1.capex", |
| previous_built_in_dir.path); |
| // Place a uncompressed version apex in current built_in_dir |
| std::string apex_path = |
| AddPreInstalledApex("com.android.apex.compressed.v1.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // New uncompressed APEX should be mounted |
| UnmountOnTearDown(apex_path); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_uncompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ apex_path, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_uncompressed))); |
| } |
| |
| TEST_F(ApexdMountTest, |
| OnOtaChrootBootstrapDecompressedApexVersionDifferentThanCapex) { |
| TemporaryDir previous_built_in_dir; |
| PrepareCompressedApex("com.android.apex.compressed.v2.capex", |
| previous_built_in_dir.path); |
| // Place a lower version capex in current built_in_dir, so that previously |
| // decompressed APEX has higher version but still doesn't get picked during |
| // selection. |
| std::string apex_path = |
| AddPreInstalledApex("com.android.apex.compressed.v1.capex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Pre-installed CAPEX should be decompressed again and mounted from |
| // decompression_dir |
| std::string decompressed_active_apex = |
| StringPrintf("%s/com.android.apex.compressed@1%s", |
| GetDecompressionDir().c_str(), kOtaApexPackageSuffix); |
| UnmountOnTearDown(decompressed_active_apex); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@1")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_decompressed = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ decompressed_active_apex, |
| /* preinstalledModulePath= */ apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ true, |
| GetMTime(decompressed_active_apex), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); |
| } |
| |
| // Test when we update CAPEX and there is a higher version present in data |
| TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataHigherThanCapex) { |
| auto system_apex_path = |
| PrepareCompressedApex("com.android.apex.compressed.v1.capex"); |
| auto data_apex_path = |
| AddDataApex("com.android.apex.compressed.v2_original.apex"); |
| |
| ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0); |
| |
| // Data APEX should be mounted |
| UnmountOnTearDown(data_apex_path); |
| |
| auto apex_mounts = GetApexMounts(); |
| ASSERT_THAT(apex_mounts, |
| UnorderedElementsAre("/apex/com.android.apex.compressed", |
| "/apex/com.android.apex.compressed@2")); |
| |
| ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); |
| auto info_list = |
| com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); |
| ASSERT_TRUE(info_list.has_value()); |
| auto apex_info_xml_data = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ data_apex_path, |
| /* preinstalledModulePath= */ system_apex_path, |
| /* versionCode= */ 2, /* versionName= */ "2", |
| /* isFactory= */ false, /* isActive= */ true, GetMTime(data_apex_path), |
| /* provideSharedApexLibs= */ false); |
| auto apex_info_xml_system = com::android::apex::ApexInfo( |
| /* moduleName= */ "com.android.apex.compressed", |
| /* modulePath= */ system_apex_path, |
| /* preinstalledModulePath= */ system_apex_path, |
| /* versionCode= */ 1, /* versionName= */ "1", |
| /* isFactory= */ true, /* isActive= */ false, GetMTime(system_apex_path), |
| /* provideSharedApexLibs= */ false); |
| ASSERT_THAT(info_list->getApexInfo(), |
| UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_data), |
| ApexInfoXmlEq(apex_info_xml_system))); |
| auto& db = GetApexDatabaseForTesting(); |
| // Check that it was mounted from decompressed apex. It should also be mounted |
| // on dm-verity device. |
| db.ForallMountedApexes("com.android.apex.compressed", |
| [&](const MountedApexData& data, bool latest) { |
| ASSERT_TRUE(latest); |
| ASSERT_EQ(data.full_path, data_apex_path); |
| ASSERT_EQ(data.device_name, |
|