blob: e722a60f9219a5212bea4abf7ed8a7e918540b44 [file] [log] [blame]
/*
* 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-matchers.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 <unistd.h>
#include <chrono>
#include <functional>
#include <optional>
#include <string>
#include <thread>
#include <tuple>
#include <unordered_set>
#include <vector>
#include "apex_constants.h"
#include "apex_database.h"
#include "apex_file.h"
#include "apex_file_repository.h"
#include "apex_manifest.pb.h"
#include "apexd_checkpoint.h"
#include "apexd_dm.h"
#include "apexd_image_manager.h"
#include "apexd_loop.h"
#include "apexd_metrics.h"
#include "apexd_session.h"
#include "apexd_test_utils.h"
#include "apexd_utils.h"
#include "apexd_verity.h"
#include "com_android_apex.h"
#include "com_android_apex_flags.h"
namespace android {
namespace apex {
using namespace std::literals;
namespace fs = std::filesystem;
namespace flags = com::android::apex::flags;
using MountedApexData = MountedApexDatabase::MountedApexData;
using android::apex::testing::ApexFileEq;
using android::base::Error;
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::SetProperty;
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::WithCode;
using android::base::testing::WithMessage;
using android::dm::DeviceMapper;
using ::apex::proto::SessionState;
using com::android::apex::testing::ApexInfoXmlEq;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::EndsWith;
using ::testing::Eq;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Optional;
using ::testing::Pointwise;
using ::testing::Property;
using ::testing::SizeIs;
using ::testing::StartsWith;
using ::testing::UnorderedElementsAre;
using ::testing::UnorderedElementsAreArray;
using ::testing::internal::CaptureStderr;
using ::testing::internal::GetCapturedStderr;
#define OR_FAIL(expr) \
UNWRAP_OR_DO(__or_fail, expr, { ASSERT_THAT(__or_fail, Ok()); })
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;
}
static int64_t GetSizeByBlocks(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_blocks * st_buf.st_blksize;
}
static Result<ApexFile> GetActivePackage(const std::string& packageName) {
std::vector<ApexFile> packages = GetActivePackages();
for (ApexFile& apex : packages) {
if (apex.GetManifest().name() == packageName) {
return std::move(apex);
}
}
return base::ErrnoError()
<< "Cannot find matching package for: " << packageName;
}
// 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> 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* kTestApexdChangedActiveApexesSysprop =
"apexd.test.changed_active_apexes";
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);
partition_ = ApexPartition::System;
partition_string_ = "SYSTEM";
block_partition_string_ = "SYSTEM";
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);
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_);
metadata_images_dir_ = StringPrintf("%s/metadata-images", td_.path);
data_images_dir_ = StringPrintf("%s/data-images", td_.path);
image_manager_ =
ApexImageManager::Create(metadata_images_dir_, data_images_dir_);
metadata_config_dir_ = StringPrintf("%s/metadata-config", td_.path);
brand_new_config_dir_ = StringPrintf("%s/brand-new-config", td_.path);
config_ = ApexdConfig{
kTestApexdStatusSysprop,
kTestApexdChangedActiveApexesSysprop,
{{partition_, built_in_dir_}},
data_dir_.c_str(),
decompression_dir_.c_str(),
ota_reserved_dir_.c_str(),
staged_session_dir_.c_str(),
kTestVmPayloadMetadataPartitionProp,
kTestActiveApexSelinuxCtx,
{{partition_, brand_new_config_dir_}}, /* brand_new_apex_config_dirs */
flags::mount_before_data(),
metadata_config_dir_.c_str(),
};
}
const std::string& GetBuiltInDir() { return built_in_dir_; }
ApexPartition GetPartition() { return partition_; }
const std::string& GetPartitionString() { return partition_string_; }
const std::string& GetBlockPartitionString() {
return block_partition_string_;
}
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 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());
}
// Returns image name
std::string AddPinnedDataApex(const std::string& apex_name) {
auto apex_file = ApexFile::Open(GetTestFile(apex_name));
CHECK(apex_file.ok());
auto images = image_manager_->PinApexFiles(Single(*apex_file));
CHECK(images.ok());
// Add it to the ACTIVE
auto list = image_manager_->GetApexList(ApexListType::ACTIVE);
CHECK(list.ok());
ApexListEntry new_entry{images->at(0), apex_file->GetManifest().name()};
auto update = image_manager_->UpdateApexList(
ApexListType::ACTIVE,
UpdateApexListWithNewEntries(std::move(*list), {new_entry}));
CHECK(update.ok());
return images->at(0);
}
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 returns both paths as tuple.
std::tuple<std::string, std::string> PrepareCompressedApex(
const std::string& name, const std::string& built_in_dir) {
fs::copy(GetTestFile(name), built_in_dir);
auto compressed_file_path =
StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str());
auto compressed_apex = ApexFile::Open(compressed_file_path);
auto decompressed =
ProcessCompressedApex(*compressed_apex, /*is_ota_chroot*/ false);
CHECK(decompressed.ok());
return std::make_tuple(compressed_file_path, decompressed->GetPath());
}
std::tuple<std::string, std::string> PrepareCompressedApex(
const std::string& name) {
return PrepareCompressedApex(name, built_in_dir_);
}
std::string PrepareStagedSession(const std::string& apex_name,
int session_id) {
auto session_dir = GetStagedDir(session_id);
CreateDirIfNeeded(session_dir, 0755);
fs::copy(GetTestFile(apex_name), session_dir);
return session_dir + "/" + apex_name;
}
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;
}
bool IsMountBeforeDataEnabled() const { return config_.mount_before_data; }
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(staged_session_dir_.c_str(), 0755), 0);
ASSERT_EQ(mkdir(sessions_metadata_dir_.c_str(), 0755), 0);
ASSERT_EQ(mkdir(metadata_images_dir_.c_str(), 0755), 0);
ASSERT_EQ(mkdir(metadata_config_dir_.c_str(), 0755), 0);
ASSERT_EQ(mkdir(data_images_dir_.c_str(), 0755), 0);
ASSERT_EQ(mkdir(brand_new_config_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());
InitializeImageManager(image_manager_.get());
SetProperty(kTestApexdChangedActiveApexesSysprop, "");
}
void TearDown() override {
DeleteDirContent(GetSessionsDir());
// Reset vold; some tests changing this might affect other tests.
InitializeVold(nullptr);
}
protected:
TemporaryDir td_;
std::string built_in_dir_;
ApexPartition partition_;
std::string partition_string_;
std::string block_partition_string_;
std::string data_dir_;
std::string decompression_dir_;
std::string ota_reserved_dir_;
std::string staged_session_dir_;
std::string sessions_metadata_dir_;
std::unique_ptr<ApexSessionManager> session_manager_;
std::string metadata_images_dir_;
std::string data_images_dir_;
std::unique_ptr<ApexImageManager> image_manager_;
std::string metadata_config_dir_;
std::string brand_new_config_dir_;
ApexdConfig config_;
};
TEST_F(ApexdUnitTest, VerifyVerityRootDigest) {
auto apex_ok = ApexFile::Open(GetTestFile("apex.apexd_test.apex"));
ASSERT_THAT(apex_ok, Ok());
ASSERT_THAT(VerifyVerityRootDigest(*apex_ok), Ok());
auto apex_bad =
ApexFile::Open(GetTestFile("apex.apexd_test_corrupt_apex.apex"));
ASSERT_THAT(apex_bad, Ok());
ASSERT_THAT(VerifyVerityRootDigest(*apex_bad),
HasError(WithMessage(HasSubstr("root digest mismatch"))));
}
TEST_F(ApexdUnitTest, ProcessCompressedApex) {
auto compressed_apex = ApexFile::Open(
AddPreInstalledApex("com.android.apex.compressed.v1.capex"));
auto return_value = ProcessCompressedApex(*compressed_apex, 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, HasValue(ApexFileEq(*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"));
ASSERT_THAT(ProcessCompressedApex(*compressed_apex_mismatch_key, false),
Not(Ok()));
ASSERT_THAT(ProcessCompressedApex(*compressed_apex_version_mismatch, false),
Not(Ok()));
}
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"));
auto return_value =
ProcessCompressedApex(*compressed_apex, /* is_ota_chroot= */ false);
ASSERT_THAT(return_value, Ok());
// 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, /* is_ota_chroot= */ false);
ASSERT_THAT(return_value, Ok());
// 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"));
auto return_value =
ProcessCompressedApex(*compressed_apex, /* is_ota_chroot= */ true);
ASSERT_THAT(return_value, Ok());
// 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, HasValue(ApexFileEq(*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"));
// 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, /* is_ota_chroot= */ false);
ASSERT_THAT(return_value, Not(Ok()));
// 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, /* is_ota_chroot= */ false);
ASSERT_THAT(return_value, Ok());
// Ota Apex should be cleaned up
ASSERT_THAT(PathExists(ota_apex_path), HasValue(false));
ASSERT_EQ(return_value->GetPath(),
StringPrintf("%s/com.android.apex.compressed@1%s",
GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix));
}
TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompression_NewApex) {
ApexFileRepository instance;
MountedApexDatabase db;
// A brand new compressed APEX is being introduced: selected
bool result = ShouldAllocateSpaceForDecompression("com.android.brand.new", 1,
instance, db);
ASSERT_TRUE(result);
}
TEST_F(ApexdUnitTest,
ShouldAllocateSpaceForDecompression_WasNotCompressedBefore) {
ApexFileRepository instance;
auto preinstalled_path = AddPreInstalledApex("apex.apexd_test.apex");
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
// An existing pre-installed APEX is now compressed in the OTA: selected
{
MountedApexDatabase db;
db.AddMountedApex("com.android.apex.test_package", 1, "", preinstalled_path,
"mount_point", "device_name", "");
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.test_package", 1, instance, db);
ASSERT_TRUE(result);
}
// Even if there is a data apex (lower version)
// Include data apex within calculation now
auto data_path = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok());
{
MountedApexDatabase db;
db.AddMountedApex("com.android.apex.test_package", 2, "", data_path,
"mount_point", "device_name", "");
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.test_package", 3, instance, db);
ASSERT_TRUE(result);
}
// But not if data apex has equal or higher version
{
MountedApexDatabase db;
db.AddMountedApex("com.android.apex.test_package", 2, "", data_path,
"mount_point", "device_name", "");
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.test_package", 2, instance, db);
ASSERT_FALSE(result);
}
}
TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompression_VersionCompare) {
// Prepare fake pre-installed apex
ApexFileRepository instance(decompression_dir_);
auto [_, decompressed_path] =
PrepareCompressedApex("com.android.apex.compressed.v1.capex");
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
// Fake mount
MountedApexDatabase db;
db.AddMountedApex("com.android.apex.compressed", 1, "", decompressed_path,
"mount_point", "device_name", "");
{
// New Compressed apex has higher version than decompressed data apex:
// selected
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.compressed", 2, instance, db);
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: selected
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.compressed", 1, instance, db);
ASSERT_TRUE(result) << "Even with same version, the incoming apex may have "
"a different size. Need to decompress";
}
{
// New Compressed apex has lower version than decompressed data apex:
// selected
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.compressed", 0, instance, db);
ASSERT_TRUE(result)
<< "lower version test with decompressed data returned false";
}
// Replace decompressed data apex with a higher version
auto data_path = AddDataApex("com.android.apex.compressed.v2_original.apex");
ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok());
db.Reset();
db.AddMountedApex("com.android.apex.compressed", 2, "", data_path,
"mount_point", "device_name", "");
{
// New Compressed apex has higher version as data apex: selected
bool result = ShouldAllocateSpaceForDecompression(
"com.android.apex.compressed", 3, instance, db);
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, db);
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, db);
ASSERT_FALSE(result) << "lower version test with new data returned true";
}
}
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);
EXPECT_GE(GetSizeByBlocks((*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);
EXPECT_GE(GetSizeByBlocks((*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);
EXPECT_GE(GetSizeByBlocks((*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);
EXPECT_GE(GetSizeByBlocks((*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(*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(*child_apex_file_1),
ApexFileEq(*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);
}
protected:
void SetUp() override {
ApexdUnitTest::SetUp();
GetApexDatabaseForTesting().Reset();
ASSERT_THAT(SetUpApexTestEnvironment(), Ok());
}
void TearDown() override {
SetBlockApexEnabled(false);
DeactivateAllPackages();
InitMetrics({}); // reset
ApexdUnitTest::TearDown();
}
void DeactivateAllPackages() {
auto activated = std::vector<std::string>{};
GetApexDatabaseForTesting().ForallMountedApexes(
[&](auto pkg, auto data, auto latest) {
activated.push_back(data.full_path);
});
for (const auto& apex : activated) {
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_;
// 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_;
};
TEST_F(ApexdMountTest, CalculateSizeForCompressedApexEmptyList) {
int64_t result = CalculateSizeForCompressedApex({});
ASSERT_EQ(0LL, result);
}
TEST_F(ApexdMountTest, CalculateSizeForCompressedApex) {
auto& instance = ApexFileRepository::GetInstance();
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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, 8),
};
int64_t result = CalculateSizeForCompressedApex(input);
ASSERT_EQ(1 + 2 + 8LL, result);
}
TEST_F(
ApexdMountTest,
CalculateSizeForCompressedApex_SkipIfDataApexIsNewerThanOrEqualToPreInstalledApex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto& instance = ApexFileRepository::GetInstance();
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
AddDataApex("com.android.apex.compressed.v2_original.apex");
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
ASSERT_THAT(instance.AddDataApex(GetDataDir()), Ok());
OnStart();
std::vector<std::tuple<std::string, int64_t, int64_t>> input = {
std::make_tuple("new_apex", 1, 1),
std::make_tuple("com.android.apex.compressed", 2, 8), // ignored
};
int64_t result = CalculateSizeForCompressedApex(input);
ASSERT_EQ(1LL, result);
}
// TODO(b/187864524): cover other negative scenarios.
TEST_F(ApexdMountTest, InstallPackageRejectsApexWithoutRebootlessSupport) {
std::string file_path = AddPreInstalledApex("apex.apexd_test.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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, InstallPackageRejectsNoActiveApex) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), 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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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, InstallPackageRejectsProvidesNativeLibs) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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, InstallPackageRejectsJniLibs) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
auto ret =
InstallPackage(GetTestFile("test.rebootless_apex_add_native_lib.apex"),
/* force= */ false);
ASSERT_THAT(ret, Ok());
}
TEST_F(ApexdMountTest, InstallPackageAcceptsRemoveRequiredNativeLib) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
auto ret =
InstallPackage(GetTestFile("test.rebootless_apex_remove_native_lib.apex"),
/* force= */ false);
ASSERT_THAT(ret, Ok());
}
TEST_F(ApexdMountTest, InstallPackageRejectsAppInApex) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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());
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.verity_name, "test.apex.rebootless@2_1");
});
}
TEST_F(ApexdMountTest, InstallPackagePreInstallVersionActiveSamegrade) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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());
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.verity_name, "test.apex.rebootless@1_1");
});
}
TEST_F(ApexdMountTest, InstallPackageUnloadOldApex) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), 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());
auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex"),
/* force= */ false);
ASSERT_THAT(ret, Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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());
}
TEST_F(ApexdMountTest, InstallPackageDataVersionActive) {
AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("test.rebootless_apex_v1.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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());
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.verity_name, "test.apex.rebootless@2_1");
});
}
TEST_F(ApexdMountTest, InstallPackageResolvesPathCollision) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("test.rebootless_apex_v1.apex",
"test.apex.rebootless@1_1.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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());
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.verity_name, "test.apex.rebootless@1_2");
});
}
TEST_F(ApexdMountTest, InstallPackageDataVersionActiveSamegrade) {
AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("test.rebootless_apex_v2.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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());
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.verity_name, "test.apex.rebootless@2_1");
});
}
TEST_F(ApexdMountTest, InstallPackageUnmountFailsPreInstalledApexActive) {
std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("test.rebootless_apex_v1.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
{
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.verity_name, "test.apex.rebootless@1");
});
}
TEST_F(ApexdMountTest, InstallPackageUpdatesApexInfoList) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_1 = AddPreInstalledApex("test.rebootless_apex_v1.apex");
auto apex_2 = AddPreInstalledApex("apex.apexd_test.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
OnStart();
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());
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1),
ApexInfoXmlEq(apex_info_xml_2),
ApexInfoXmlEq(apex_info_xml_3)));
}
TEST_F(ApexdMountTest, ActivatePackageNoCode) {
std::string file_path = AddPreInstalledApex("apex.apexd_test_nocode.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), 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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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(
{{GetPartition(), GetBuiltInDir()}});
ASSERT_THAT(ActivatePackage(file_path), Ok());
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, DeactivePackageFreesLoopDevices) {
AddPreInstalledApex("apex.apexd_test.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
// Get loop devices that were used to mount APEX.
auto children = ListChildLoopDevices("com.android.apex.test_package@2");
ASSERT_THAT(children, Ok());
ASSERT_EQ(1u, 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, DeactivePackageTearsDownVerityDevice) {
AddPreInstalledApex("apex.apexd_test.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
std::string file_path = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
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, 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(
{{GetPartition(), GetBuiltInDir()}});
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1),
ApexInfoXmlEq(apex_info_xml_2)));
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrap_ActivatesPinnedApex_UnmountAll) {
if constexpr (!flags::mount_before_data()) {
GTEST_SKIP() << "mount_before_data flag not set";
}
AddPreInstalledApex("apex.apexd_test.apex");
auto image = AddPinnedDataApex("apex.apexd_test_v2.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
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"));
// UnmountAll unmounts pinned APEX as well.
ASSERT_EQ(UnmountAll(), 0);
ASSERT_THAT(GetApexMounts(), IsEmpty());
// The dm-linear device created for the APEX is not destroyed.
ASSERT_THAT(DeleteDmDevice(image, /*deferred=*/false), Ok());
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrap_WithStaged_ActivatesPinnedApex) {
if constexpr (!flags::mount_before_data()) {
GTEST_SKIP() << "mount_before_data flag not set";
}
// preinstalled: V1
AddPreInstalledApex("apex.apexd_test.apex");
// pinned data: V1
auto image = AddPinnedDataApex("apex.apexd_test.apex");
// staged: V2
auto apex_session = CreateStagedSession("apex.apexd_test_v2.apex", 123);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/true), 0);
// Verify that the staged one (V2) is chosen.
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.test_package",
"/apex/com.android.apex.test_package@2"));
// UnmountAll unmounts pinned APEX as well.
ASSERT_EQ(UnmountAll(), 0);
ASSERT_THAT(GetApexMounts(), IsEmpty());
// The dm-linear device created for the APEX is not destroyed.
ASSERT_THAT(DeleteDmDevice(image, /*deferred=*/false), Ok());
}
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1)));
}
// 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);
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,
/* partition= */ GetPartitionString());
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.verity_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);
// 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);
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,
/* partition= */ GetPartitionString());
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.verity_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);
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,
/* partition= */ GetPartitionString());
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.verity_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);
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,
/* partition= */ GetPartitionString());
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.verity_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);
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,
/* partition= */ GetPartitionString());
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.verity_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
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,
/* partition= */ GetPartitionString());
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);
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,
/* partition= */ GetPartitionString());
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
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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.verity_name,
"com.android.apex.compressed@2.chroot");
});
}
// Test when we update CAPEX and there is a lower version present in data
TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataLowerThanCapex) {
auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v2.capex");
AddDataApex("com.android.apex.compressed.v1.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
// Decompressed APEX should be mounted from reserved dir
std::string decompressed_active_apex =
StringPrintf("%s/com.android.apex.compressed@2%s",
GetDecompressionDir().c_str(), kOtaApexPackageSuffix);
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 = 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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml)));
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.verity_name,
"com.android.apex.compressed@2.chroot");
});
}
// Test when we update CAPEX and there is a same version present in data
TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataSameAsCapex) {
auto [system_apex_path, _] =
PrepareCompressedApex("com.android.apex.compressed.v1.capex");
auto data_apex_path = AddDataApex("com.android.apex.compressed.v1.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
// Data APEX should be mounted
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_data = com::android::apex::ApexInfo(
/* moduleName= */ "com.android.apex.compressed",
/* modulePath= */ data_apex_path,
/* preinstalledModulePath= */ system_apex_path,
/* versionCode= */ 1, /* versionName= */ "1",
/* isFactory= */ false, /* isActive= */ true, GetMTime(data_apex_path),
/* provideSharedApexLibs= */ false,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
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.verity_name,
"com.android.apex.compressed@1.chroot");
});
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrapDataHasDifferentKeyThanCapex) {
AddDataApex("com.android.apex.compressed_different_key.capex");
// 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);
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,
/* partition= */ GetPartitionString());
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.verity_name,
"com.android.apex.compressed@1.chroot");
});
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrapSystemDataStagedInSameVersion) {
// The APEXes on system, data, and staged are all in the same version. The
// staged one should be picked.
std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex");
AddDataApex("apex.apexd_test.apex");
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
std::string apex_path_3 =
GetStagedDir(apex_session->GetId()) + "/" + "apex.apexd_test.apex";
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/true), 0);
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= */ false, GetMTime(apex_path_1),
/* provideSharedApexLibs= */ false,
/* partition= */ GetPartitionString());
auto apex_info_xml_2 = 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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1),
ApexInfoXmlEq(apex_info_xml_2)));
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrapSystemNewerThanDataStaged) {
// The system one is newer than the data one and the staged one. The system
// one should be picked.
std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test_v2.apex");
AddDataApex("apex.apexd_test.apex");
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/true), 0);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.test_package",
"/apex/com.android.apex.test_package@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 = 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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml)));
}
static std::string GetSelinuxContext(const std::string& file) {
char* ctx;
if (getfilecon(file.c_str(), &ctx) < 0) {
PLOG(ERROR) << "Failed to getfilecon " << file;
return "";
}
std::string result(ctx);
freecon(ctx);
return result;
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrapSelinuxLabelsAreCorrect) {
std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex");
std::string apex_path_2 = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
EXPECT_EQ(GetSelinuxContext("/apex/apex-info-list.xml"),
"u:object_r:apex_info_file:s0");
EXPECT_EQ(GetSelinuxContext("/apex/com.android.apex.test_package"),
"u:object_r:system_file:s0");
EXPECT_EQ(GetSelinuxContext("/apex/com.android.apex.test_package@2"),
"u:object_r:system_file:s0");
}
TEST_F(ApexdMountTest, OnOtaChrootBootstrapDmDevicesHaveCorrectName) {
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);
MountedApexDatabase& db = GetApexDatabaseForTesting();
// com.android.apex.test_package_2 should be mounted directly on top of loop
// device.
db.ForallMountedApexes("com.android.apex.test_package_2",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_THAT(data.verity_name, IsEmpty());
ASSERT_THAT(data.loop_name, StartsWith("/dev"));
});
// com.android.apex.test_package should be mounted on top of dm-verity device.
db.ForallMountedApexes("com.android.apex.test_package",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.verity_name,
"com.android.apex.test_package@2.chroot");
ASSERT_THAT(data.loop_name, StartsWith("/dev"));
});
}
TEST_F(ApexdMountTest,
OnOtaChrootBootstrapFailsToActivatePreInstalledApexKeepsGoing) {
std::string apex_path_1 =
AddPreInstalledApex("apex.apexd_test_manifest_mismatch.apex");
std::string apex_path_2 =
AddPreInstalledApex("apex.apexd_test_different_app.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/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= */ 137, /* versionName= */ "1",
/* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1),
/* provideSharedApexLibs= */ false,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1),
ApexInfoXmlEq(apex_info_xml_2)));
}
TEST_F(ApexdMountTest,
OnOtaChrootBootstrapFailsToActivateDataApexFallsBackToPreInstalled) {
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_manifest_mismatch.apex");
ASSERT_EQ(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/false), 0);
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,
/* partition= */ GetPartitionString());
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,
/* partition= */ GetPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1),
ApexInfoXmlEq(apex_info_xml_2)));
}
TEST_F(ApexdMountTest, OnStartSetsStatusAsStarting) {
std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting");
}
TEST_F(ApexdMountTest, OnStartOnlyPreInstalledApexes) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex");
std::string apex_path_2 =
AddPreInstalledApex("apex.apexd_test_different_app.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
}
TEST_F(ApexdMountTest, OnStartDataHasHigherVersion) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
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_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
}
TEST_F(ApexdMountTest, OnStartDataHasWrongSHA) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
std::string apex_path = AddPreInstalledApex("com.android.apex.cts.shim.apex");
AddDataApex("com.android.apex.cts.shim.v2_wrong_sha.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Check system shim apex is activated instead of the data one.
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.cts.shim",
"/apex/com.android.apex.cts.shim@1"));
}
TEST_F(ApexdMountTest, OnStartDataHasSameVersion) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
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_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from data apex, not pre-installed one.
db.ForallMountedApexes("com.android.apex.test_package",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_3);
});
}
TEST_F(ApexdMountTest, OnStartSystemHasHigherVersion) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
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_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from pre-installed one.
db.ForallMountedApexes("com.android.apex.test_package",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_1);
});
}
TEST_F(ApexdMountTest, OnStartFailsToActivateApexOnDataFallsBackToBuiltIn) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
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_manifest_mismatch.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from pre-installed apex.
db.ForallMountedApexes("com.android.apex.test_package",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_1);
});
}
TEST_F(ApexdMountTest, OnStartApexOnDataHasWrongKeyFallsBackToBuiltIn) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
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_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
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"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from pre-installed apex.
db.ForallMountedApexes("com.android.apex.test_package",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_1);
});
}
TEST_F(ApexdMountTest, OnStartOnlyPreInstalledCapexes) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
std::string apex_path_1 =
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Decompressed APEX should be mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from decompressed apex.
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.verity_name,
"com.android.apex.compressed");
});
}
TEST_F(ApexdMountTest, OnStartDataHasHigherVersionThanCapex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
std::string apex_path_2 =
AddDataApex("com.android.apex.compressed.v2_original.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@2"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from data apex.
db.ForallMountedApexes("com.android.apex.compressed",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_2);
ASSERT_EQ(data.verity_name,
"com.android.apex.compressed");
});
}
TEST_F(ApexdMountTest, OnStartDataHasSameVersionAsCapex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
std::string apex_path_2 = AddDataApex("com.android.apex.compressed.v1.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Data APEX should be mounted
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from data apex, not pre-installed one.
db.ForallMountedApexes("com.android.apex.compressed",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path_2);
ASSERT_EQ(data.verity_name,
"com.android.apex.compressed");
});
}
TEST_F(ApexdMountTest, OnStartSystemHasHigherVersionCapexThanData) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
std::string apex_path_1 =
AddPreInstalledApex("com.android.apex.compressed.v2.capex");
AddDataApex("com.android.apex.compressed.v1.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Decompressed APEX should be mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@2"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from compressed apex
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.verity_name,
"com.android.apex.compressed");
});
}
TEST_F(ApexdMountTest, OnStartFailsToActivateApexOnDataFallsBackToCapex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
AddDataApex("com.android.apex.compressed.v2_manifest_mismatch.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Decompressed APEX should be mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
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.verity_name,
"com.android.apex.compressed");
});
}
// Test scenario when we fallback to capex but it already has a decompressed
// version on data
TEST_F(ApexdMountTest, OnStartFallbackToAlreadyDecompressedCapex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
PrepareCompressedApex("com.android.apex.compressed.v1.capex");
AddDataApex("com.android.apex.compressed.v2_manifest_mismatch.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Decompressed APEX should be mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from decompressed apex.
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.verity_name,
"com.android.apex.compressed");
});
}
// Test scenario when we fallback to capex but it has same version as corrupt
// data apex
TEST_F(ApexdMountTest, OnStartFallbackToCapexSameVersion) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("com.android.apex.compressed.v2.capex");
// Add data apex using the common naming convention for /data/apex/active
// directory
fs::copy(GetTestFile("com.android.apex.compressed.v2_manifest_mismatch.apex"),
GetDataDir() + "/com.android.apex.compressed@2.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Decompressed APEX should be mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@2"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from decompressed apex.
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.verity_name,
"com.android.apex.compressed");
});
}
TEST_F(ApexdMountTest, OnStartCapexToApex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
TemporaryDir previous_built_in_dir;
PrepareCompressedApex("com.android.apex.compressed.v1.capex",
previous_built_in_dir.path);
auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Uncompressed APEX should be mounted
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from decompressed apex.
db.ForallMountedApexes("com.android.apex.compressed",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path);
ASSERT_THAT(data.verity_name, IsEmpty());
});
}
// Test to ensure we do not mount decompressed APEX from /data/apex/active
TEST_F(ApexdMountTest, OnStartOrphanedDecompressedApexInActiveDirectory) {
// Place a decompressed APEX in /data/apex/active. This apex should not
// be mounted since it's not in correct location. Instead, the
// pre-installed APEX should be mounted.
auto decompressed_apex_in_active_dir =
StringPrintf("%s/com.android.apex.compressed@1%s", GetDataDir().c_str(),
kDecompressedApexPackageSuffix);
fs::copy(GetTestFile("com.android.apex.compressed.v1.apex"),
decompressed_apex_in_active_dir);
auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Pre-installed APEX should be mounted
auto& db = GetApexDatabaseForTesting();
// Check that pre-installed APEX has been activated
db.ForallMountedApexes("com.android.apex.compressed",
[&](const MountedApexData& data, bool latest) {
ASSERT_TRUE(latest);
ASSERT_EQ(data.full_path, apex_path);
ASSERT_THAT(data.verity_name, IsEmpty());
});
}
// Test scenario when decompressed version has different version than
// pre-installed CAPEX
TEST_F(ApexdMountTest, OnStartDecompressedApexVersionDifferentThanCapex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
TemporaryDir previous_built_in_dir;
PrepareCompressedApex("com.android.apex.compressed.v2.capex",
previous_built_in_dir.path);
auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Existing higher version decompressed APEX should be ignored and new
// pre-installed CAPEX should be decompressed and mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto apex_mounts = GetApexMounts();
ASSERT_THAT(apex_mounts,
UnorderedElementsAre("/apex/com.android.apex.compressed",
"/apex/com.android.apex.compressed@1"));
auto& db = GetApexDatabaseForTesting();
// Check that it was mounted from newly decompressed apex.
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.verity_name,
"com.android.apex.compressed");
});
}
// Test that ota_apex is persisted until slot switch
TEST_F(ApexdMountTest, OnStartOtaApexKeptUntilSlotSwitch) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
// Imagine current system has v1 capex and we have v2 incoming via ota
auto old_capex = AddPreInstalledApex("com.android.apex.compressed.v1.capex");
auto ota_apex_path =
StringPrintf("%s/com.android.apex.compressed@2%s",
GetDecompressionDir().c_str(), kOtaApexPackageSuffix);
fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"),
ota_apex_path.c_str());
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
// When we call OnStart for the first time, it will decompress v1 capex and
// activate it, while after second call it will decompress v2 capex and
// activate it. We need to make sure that activated APEXes are cleaned up
// after test finishes.
auto old_decompressed_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
auto new_decompressed_apex = StringPrintf(
"%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
// First try starting without slot switch. Since we are booting with
// old pre-installed capex, ota_apex should not be deleted
OnStart();
auto path_exists = PathExists(ota_apex_path);
ASSERT_TRUE(*path_exists);
// When we switch slot, the pre-installed APEX will match ota_apex
// and the ota_apex will end up getting renamed.
RemoveFileIfExists(old_capex);
AddPreInstalledApex("com.android.apex.compressed.v2.capex");
ApexFileRepository::GetInstance().Reset(GetDecompressionDir());
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
path_exists = PathExists(ota_apex_path);
ASSERT_FALSE(*path_exists);
}
// Test scenario when decompressed version has same version but different
// digest
TEST_F(ApexdMountTest,
OnStartDecompressedApexVersionSameAsCapexDifferentDigest) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
// Push a CAPEX to system without decompressing it
auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex");
auto pre_installed_apex = ApexFile::Open(apex_path);
// Now push an APEX with different root digest as decompressed APEX
auto decompressed_apex_path = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
fs::copy(GetTestFile(
"com.android.apex.compressed.v1_different_digest_original.apex"),
decompressed_apex_path);
auto different_digest_apex = ApexFile::Open(decompressed_apex_path);
auto different_digest = GetRootDigest(*different_digest_apex);
ASSERT_NE(
pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(),
different_digest);
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Existing same version decompressed APEX with different root digest should
// be ignored and the pre-installed CAPEX should be decompressed again.
// Ensure decompressed apex has same digest as pre-installed
auto decompressed_apex = ApexFile::Open(decompressed_apex_path);
ASSERT_EQ(
pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(),
GetRootDigest(*decompressed_apex));
ASSERT_NE(GetRootDigest(*decompressed_apex), different_digest);
}
// Test when decompressed APEX has different key than CAPEX
TEST_F(ApexdMountTest, OnStartDecompressedApexVersionSameAsCapexDifferentKey) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
TemporaryDir previous_built_in_dir;
auto [different_key_apex_path, _] =
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_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Existing same version decompressed APEX should be ignored and new
// pre-installed CAPEX should be decompressed and mounted
std::string decompressed_active_apex = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
// Ensure decompressed apex has same digest as pre-installed
auto pre_installed_apex = ApexFile::Open(apex_path);
auto decompressed_apex = ApexFile::Open(decompressed_active_apex);
auto different_key_apex = ApexFile::Open(different_key_apex_path);
ASSERT_EQ(
pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(),
GetRootDigest(*decompressed_apex));
ASSERT_NE(
pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(),
GetRootDigest(*different_key_apex));
}
TEST_F(ApexdMountTest, UnmountAll) {
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");
// Mount an apex from decomrpession_dir
PrepareCompressedApex("com.android.apex.compressed.v1.capex");
std::string decompressed_apex =
StringPrintf("%s/com.android.apex.compressed@1.decompressed.apex",
GetDecompressionDir().c_str());
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
ASSERT_THAT(ActivatePackage(apex_path_2), Ok());
ASSERT_THAT(ActivatePackage(apex_path_3), Ok());
ASSERT_THAT(ActivatePackage(decompressed_apex), Ok());
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.compressed",
"/apex/com.android.apex.compressed@1",
"/apex/com.android.apex.test_package_2",
"/apex/com.android.apex.test_package_2@1"));
ASSERT_EQ(0, UnmountAll());
ASSERT_THAT(GetApexMounts(), IsEmpty());
}
TEST_F(ApexdMountTest, UnmountAllDeferred) {
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");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
ASSERT_THAT(ActivatePackage(apex_path_2), Ok());
ASSERT_THAT(ActivatePackage(apex_path_3), Ok());
ASSERT_THAT(GetApexMounts(),
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"));
const std::string kDeviceName = "com.android.apex.test_package@2";
Result<std::vector<std::string>> loop_devices =
ListChildLoopDevices(kDeviceName);
ASSERT_THAT(loop_devices, HasValue(Not(IsEmpty())));
// Open a file. This should make unmounting in `UnmountAll` deferred.
unique_fd fd(
open("/apex/com.android.apex.test_package/etc/sample_prebuilt_file",
O_RDONLY));
ASSERT_GE(fd, 0) << strerror(errno);
// UnmountAll should succeed despite the open file.
ASSERT_EQ(UnmountAll(), 0);
// The mount should still be there, but it should be detached from the
// filesystem, so the mount point should be gone.
EXPECT_THAT(GetApexMounts(), IsEmpty());
// The DM device and the loop device should still be there.
auto& dm = DeviceMapper::Instance();
EXPECT_EQ(dm.GetState(kDeviceName), dm::DmDeviceState::ACTIVE);
for (const std::string& loop_device : *loop_devices) {
EXPECT_THAT(GetLoopDeviceStatus(loop_device), Ok());
}
// Close the file. Unmounting should be automatically performed after then.
fd.reset();
// Wait for the kernel to clean things up.
std::this_thread::sleep_for(std::chrono::milliseconds(300));
// The DM device and the loop device should be gone.
EXPECT_EQ(dm.GetState(kDeviceName), dm::DmDeviceState::INVALID);
for (const std::string& loop_device : *loop_devices) {
EXPECT_THAT(GetLoopDeviceStatus(loop_device), HasError(WithCode(ENXIO)));
}
}
TEST_F(ApexdMountTest, UnmountAllStaged) {
// Both a pre-installed apex and a staged apex are mounted. UnmountAll should
// unmount both.
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
auto session = OR_FAIL(CreateStagedSession("apex.apexd_test_v2.apex", 123));
ASSERT_THAT(session.UpdateStateAndCommit(SessionState::STAGED), Ok());
ASSERT_THAT(OnOtaChrootBootstrap(/*also_include_staged_apexes=*/true), 0);
ASSERT_THAT(GetApexMounts(),
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(0, UnmountAll());
ASSERT_THAT(GetApexMounts(), IsEmpty());
}
TEST_F(ApexdMountTest, OnStartInVmModeActivatesPreInstalled) {
auto path1 = AddPreInstalledApex("apex.apexd_test.apex");
auto path2 = AddPreInstalledApex("apex.apexd_test_different_app.apex");
// In VM mode, we don't scan /data/apex
AddDataApex("apex.apexd_test_v2.apex");
ASSERT_EQ(0, OnStartInVmMode());
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"));
}
TEST_F(ApexdMountTest, OnStartInVmModeSetsStatusAsReady) {
AddPreInstalledApex("apex.apexd_test.apex");
ASSERT_EQ(0, OnStartInVmMode());
ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "ready");
}
TEST_F(ApexdMountTest, OnStartInVmModeFailsWithCapex) {
AddPreInstalledApex("com.android.apex.compressed.v2.capex");
ASSERT_EQ(1, OnStartInVmMode());
}
TEST_F(ApexdMountTest, OnStartInVmModeActivatesBlockDevicesAsWell) {
// Set system property to enable block apexes
SetBlockApexEnabled(true);
auto path1 = AddBlockApex("apex.apexd_test.apex");
ASSERT_EQ(0, OnStartInVmMode());
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= */ path1,
/* preinstalledModulePath= */ path1,
/* versionCode= */ 1, /* versionName= */ "1",
/* isFactory= */ true, /* isActive= */ true, GetMTime(path1),
/* provideSharedApexLibs= */ false,
/* partition= */ GetBlockPartitionString());
ASSERT_THAT(info_list->getApexInfo(),
UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1)));
}
TEST_F(ApexdMountTest, OnStartInVmModeFailsWithDuplicateNames) {
// Set system property to enable block apexes
SetBlockApexEnabled(true);
AddPreInstalledApex("apex.apexd_test.apex");
AddBlockApex("apex.apexd_test_v2.apex");
ASSERT_EQ(1, OnStartInVmMode());
}
TEST_F(ApexdMountTest, OnStartInVmModeFailsWithWrongPubkey) {
// Set system property to enable block apexes
SetBlockApexEnabled(true);
AddBlockApex("apex.apexd_test.apex", /*public_key=*/"wrong pubkey");
ASSERT_EQ(1, OnStartInVmMode());
}
TEST_F(ApexdMountTest, GetActivePackagesReturningBlockApexesAsWell) {
// Set system property to enable block apexes
SetBlockApexEnabled(true);
auto path1 = AddBlockApex("apex.apexd_test.apex");
ASSERT_EQ(0, OnStartInVmMode());
auto active_apexes = GetActivePackages();
ASSERT_EQ(1u, active_apexes.size());
ASSERT_EQ(path1, active_apexes[0].GetPath());
}
TEST_F(ApexdMountTest, OnStartInVmModeFailsWithWrongRootDigest) {
// Set system property to enable block apexes
SetBlockApexEnabled(true);
AddBlockApex("apex.apexd_test.apex", /*public_key=*/"",
/*root_digest=*/"wrong root digest");
ASSERT_EQ(1, OnStartInVmMode());
}
class ApexActivationFailureTests : public ApexdMountTest {};
TEST_F(ApexActivationFailureTests, BuildFingerprintDifferent) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->SetBuildFingerprint("wrong fingerprint");
ASSERT_RESULT_OK(apex_session->UpdateStateAndCommit(SessionState::STAGED));
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("APEX build fingerprint has changed"));
}
TEST_F(ApexActivationFailureTests, BuildFingerprintDifferent_Verified) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->SetBuildFingerprint("wrong fingerprint");
ASSERT_RESULT_OK(apex_session->UpdateStateAndCommit(SessionState::VERIFIED));
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("APEX build fingerprint has changed"));
}
TEST_F(ApexActivationFailureTests, ApexFileMissingInStagingDirectory) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
// Delete the apex file in staging directory
DeleteDirContent(GetStagedDir(123));
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(), HasSubstr("Found: 0"));
}
TEST_F(ApexActivationFailureTests, MultipleApexFileInStagingDirectory) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
CreateStagedSession("com.android.apex.compressed.v1.apex", 123);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(), HasSubstr("Found: 2"));
}
TEST_F(ApexActivationFailureTests, CorruptedSuperblockApexCannotBeStaged) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session =
CreateStagedSession("apex.apexd_test_corrupt_superblock_apex.apex", 123);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
ASSERT_RESULT_OK(apex_session);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("Couldn't find filesystem magic"));
}
TEST_F(ApexActivationFailureTests, CorruptedApexCannotBeStaged) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto apex_session = CreateStagedSession("corrupted_b146895998.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("Activation failed for packages"));
}
TEST_F(ApexActivationFailureTests, ActivatePackageImplFails) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto shim_path = AddPreInstalledApex("com.android.apex.cts.shim.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_RESULT_OK(
instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}));
auto apex_session =
CreateStagedSession("com.android.apex.cts.shim.v2_wrong_sha.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("Failed to activate packages"));
ASSERT_THAT(apex_session->GetErrorMessage(),
HasSubstr("has unexpected SHA512 hash"));
}
TEST_F(ApexActivationFailureTests,
StagedSessionFailsWhenNotInFsCheckpointMode) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
// Need to call InitializeVold before calling OnStart
InitializeVold(&checkpoint_interface);
auto pre_installed_apex = AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_RESULT_OK(
instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}));
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_EQ(apex_session->GetState(), SessionState::ACTIVATION_FAILED);
ASSERT_THAT(
apex_session->GetErrorMessage(),
HasSubstr("Cannot install apex session if not in fs-checkpoint mode"));
}
TEST_F(ApexActivationFailureTests, StagedSessionRevertsWhenInFsRollbackMode) {
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
checkpoint_interface.SetNeedsRollback(true);
// Need to call InitializeVold before calling OnStart
InitializeVold(&checkpoint_interface);
auto pre_installed_apex = AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_RESULT_OK(
instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}));
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123);
ASSERT_RESULT_OK(apex_session);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
OnStart();
apex_session = GetSessionManager()->GetSession(123);
ASSERT_RESULT_OK(apex_session);
ASSERT_EQ(apex_session->GetState(), SessionState::REVERTED);
}
TEST_F(ApexdMountTest, OnBootstrapCreatesEmptyDmDevices) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
DeviceMapper& dm = DeviceMapper::Instance();
auto cleaner = make_scope_guard([&]() {
dm.DeleteDeviceIfExists("com.android.apex.test_package", 1s);
dm.DeleteDeviceIfExists("com.android.apex.compressed", 1s);
});
ASSERT_EQ(0, OnBootstrap());
ASSERT_EQ(dm::DmDeviceState::SUSPENDED,
dm.GetState("com.android.apex.test_package"));
ASSERT_EQ(dm::DmDeviceState::SUSPENDED,
dm.GetState("com.android.apex.compressed"));
}
TEST_F(ApexdMountTest, OnBootstrapLoadBootstrapApexOnly) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_bootstrap_test.apex");
DeviceMapper& dm = DeviceMapper::Instance();
auto cleaner = make_scope_guard([&]() {
dm.DeleteDeviceIfExists("com.android.apex.test_package", 1s);
dm.DeleteDeviceIfExists("com.android.apex.bootstrap_test_package", 1s);
});
ASSERT_EQ(0, OnBootstrap());
// Check bootstrap apex was loaded
auto active_bootstrap_apex =
GetActivePackage("com.android.apex.bootstrap_test_package");
ASSERT_THAT(active_bootstrap_apex, Ok());
// Check that non-bootstrap apex was not loaded
ASSERT_THAT(GetActivePackage("com.android.apex.test_package"), Not(Ok()));
}
TEST_F(ApexdUnitTest, StagePackagesFailKey) {
auto status =
StagePackages({GetTestFile("apex.apexd_test_no_inst_key.apex")});
ASSERT_THAT(
status,
HasError(WithMessage(("No preinstalled apex found for unverified package "
"com.android.apex.test_package.no_inst_key"))));
}
TEST_F(ApexdUnitTest, StagePackagesSuccess) {
AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
auto status = StagePackages({GetTestFile("apex.apexd_test.apex")});
ASSERT_THAT(status, Ok());
auto staged_path = StringPrintf("%s/com.android.apex.test_package@1.apex",
GetDataDir().c_str());
ASSERT_EQ(0, access(staged_path.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, StagePackagesClearsPreviouslyActivePackage) {
AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
auto current_apex = AddDataApex("apex.apexd_test.apex");
ASSERT_EQ(0, access(current_apex.c_str(), F_OK));
auto status = StagePackages({GetTestFile("apex.apexd_test_v2.apex")});
ASSERT_THAT(status, Ok());
auto staged_path = StringPrintf("%s/com.android.apex.test_package@2.apex",
GetDataDir().c_str());
ASSERT_EQ(0, access(staged_path.c_str(), F_OK));
ASSERT_EQ(-1, access(current_apex.c_str(), F_OK));
ASSERT_EQ(ENOENT, errno);
}
TEST_F(ApexdUnitTest, StagePackagesClearsPreviouslyActivePackageDowngrade) {
AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
auto current_apex = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_EQ(0, access(current_apex.c_str(), F_OK));
auto status = StagePackages({GetTestFile("apex.apexd_test.apex")});
ASSERT_THAT(status, Ok());
auto staged_path = StringPrintf("%s/com.android.apex.test_package@1.apex",
GetDataDir().c_str());
ASSERT_EQ(0, access(staged_path.c_str(), F_OK));
ASSERT_EQ(-1, access(current_apex.c_str(), F_OK));
ASSERT_EQ(ENOENT, errno);
}
TEST_F(ApexdUnitTest, StagePackagesAlreadyStagedPackage) {
AddPreInstalledApex("apex.apexd_test.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
auto status = StagePackages({GetTestFile("apex.apexd_test.apex")});
ASSERT_THAT(status, Ok());
auto staged_path = StringPrintf("%s/com.android.apex.test_package@1.apex",
GetDataDir().c_str());
struct stat stat1;
ASSERT_EQ(0, stat(staged_path.c_str(), &stat1));
ASSERT_TRUE(S_ISREG(stat1.st_mode));
{
auto apex = ApexFile::Open(staged_path);
ASSERT_THAT(apex, Ok());
ASSERT_FALSE(apex->GetManifest().nocode());
}
auto status2 = StagePackages({GetTestFile("apex.apexd_test_nocode.apex")});
ASSERT_THAT(status2, Ok());
struct stat stat2;
ASSERT_EQ(0, stat(staged_path.c_str(), &stat2));
ASSERT_TRUE(S_ISREG(stat2.st_mode));
ASSERT_NE(stat1.st_ino, stat2.st_ino);
{
auto apex = ApexFile::Open(staged_path);
ASSERT_THAT(apex, Ok());
ASSERT_TRUE(apex->GetManifest().nocode());
}
}
TEST_F(ApexdUnitTest, StagePackagesMultiplePackages) {
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
auto status =
StagePackages({GetTestFile("apex.apexd_test_v2.apex"),
GetTestFile("apex.apexd_test_different_app.apex")});
ASSERT_THAT(status, Ok());
auto staged_path1 = StringPrintf("%s/com.android.apex.test_package@2.apex",
GetDataDir().c_str());
auto staged_path2 = StringPrintf("%s/com.android.apex.test_package_2@1.apex",
GetDataDir().c_str());
ASSERT_EQ(0, access(staged_path1.c_str(), F_OK));
ASSERT_EQ(0, access(staged_path2.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, UnstagePackages) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto file_path1 = AddDataApex("apex.apexd_test.apex");
auto file_path2 = AddDataApex("apex.apexd_test_different_app.apex");
ASSERT_THAT(UnstagePackages({file_path1}), Ok());
ASSERT_EQ(-1, access(file_path1.c_str(), F_OK));
ASSERT_EQ(errno, ENOENT);
ASSERT_EQ(0, access(file_path2.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, UnstagePackagesEmptyInput) {
auto file_path1 = AddDataApex("apex.apexd_test.apex");
auto file_path2 = AddDataApex("apex.apexd_test_different_app.apex");
ASSERT_THAT(UnstagePackages({}),
HasError(WithMessage("Empty set of inputs")));
ASSERT_EQ(0, access(file_path1.c_str(), F_OK));
ASSERT_EQ(0, access(file_path2.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, UnstagePackagesFail) {
auto file_path1 = AddDataApex("apex.apexd_test.apex");
auto bad_path = GetDataDir() + "/missing.apex";
ASSERT_THAT(UnstagePackages({file_path1, bad_path}), Not(Ok()));
ASSERT_EQ(0, access(file_path1.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, UnstagePackagesFailPreInstalledApex) {
auto file_path1 = AddPreInstalledApex("apex.apexd_test.apex");
auto file_path2 = AddDataApex("apex.apexd_test_different_app.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
ASSERT_THAT(UnstagePackages({file_path1, file_path2}),
HasError(WithMessage("Can't uninstall pre-installed apex " +
file_path1)));
ASSERT_EQ(0, access(file_path1.c_str(), F_OK));
ASSERT_EQ(0, access(file_path2.c_str(), F_OK));
}
TEST_F(ApexdUnitTest, RevertStoresCrashingNativeProcess) {
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
InitializeVold(&checkpoint_interface);
auto apex_session = CreateStagedSession("apex.apexd_test.apex", 1543);
ASSERT_THAT(apex_session, Ok());
ASSERT_THAT(apex_session->UpdateStateAndCommit(SessionState::ACTIVATED),
Ok());
ASSERT_THAT(RevertActiveSessions("test_process", ""), Ok());
apex_session = GetSessionManager()->GetSession(1543);
ASSERT_THAT(apex_session, Ok());
ASSERT_EQ(apex_session->GetCrashingNativeProcess(), "test_process");
}
TEST_F(ApexdUnitTest, MountAndDeriveClasspathNoJar) {
AddPreInstalledApex("apex.apexd_test_classpath.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
// Call MountAndDeriveClassPath
auto apex_file = ApexFile::Open(GetTestFile("apex.apexd_test.apex"));
auto package_name = apex_file->GetManifest().name();
std::vector<ApexFile> apex_files;
apex_files.emplace_back(std::move(*apex_file));
auto class_path = MountAndDeriveClassPath(apex_files);
ASSERT_THAT(class_path, Ok());
ASSERT_THAT(class_path->HasClassPathJars(package_name), false);
}
TEST_F(ApexdUnitTest, MountAndDeriveClassPathJarsPresent) {
AddPreInstalledApex("apex.apexd_test_classpath.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
// Call MountAndDeriveClassPath
auto apex_file =
ApexFile::Open(GetTestFile("apex.apexd_test_classpath.apex"));
auto package_name = apex_file->GetManifest().name();
std::vector<ApexFile> apex_files;
apex_files.emplace_back(std::move(*apex_file));
auto class_path = MountAndDeriveClassPath(apex_files);
ASSERT_THAT(class_path, Ok());
ASSERT_THAT(class_path->HasClassPathJars(package_name), true);
}
TEST_F(ApexdUnitTest, ProcessCompressedApexWrongSELinuxContext) {
auto compressed_apex = ApexFile::Open(
AddPreInstalledApex("com.android.apex.compressed.v1.capex"));
auto return_value =
ProcessCompressedApex(*compressed_apex, /* is_ota_chroot= */ false);
ASSERT_THAT(return_value, Ok());
auto decompressed_apex_path = StringPrintf(
"%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(),
kDecompressedApexPackageSuffix);
// Verify that so far it has correct context.
ASSERT_EQ(kTestActiveApexSelinuxCtx,
GetSelinuxContext(decompressed_apex_path));
// Manually mess up the context
ASSERT_EQ(0, setfilecon(decompressed_apex_path.c_str(),
"u:object_r:apex_data_file:s0"));
ASSERT_EQ("u:object_r:apex_data_file:s0",
GetSelinuxContext(decompressed_apex_path));
auto attempt_2 =
ProcessCompressedApex(*compressed_apex, /* is_ota_chroot= */ false);
ASSERT_THAT(attempt_2, Ok());
// Verify that it again has correct context.
ASSERT_EQ(kTestActiveApexSelinuxCtx,
GetSelinuxContext(decompressed_apex_path));
}
TEST_F(ApexdMountTest, OnStartNoApexUpdated) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("com.android.apex.compressed.v1.capex");
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
AddDataApex("apex.apexd_test_v2.apex");
AddDecompressedApex("com.android.apex.compressed.v1.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
ASSERT_THAT(GetChangedActiveApexes(), IsEmpty());
ASSERT_THAT(GetApexMounts(), SizeIs(6));
}
TEST_F(ApexdMountTest, ActivatesStagedSession) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
std::string preinstalled_apex = AddPreInstalledApex("apex.apexd_test.apex");
auto apex_session = CreateStagedSession("apex.apexd_test_v2.apex", 37);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Quick check that session was activated
{
auto session = GetSessionManager()->GetSession(37);
ASSERT_THAT(session, Ok());
ASSERT_EQ(session->GetState(), SessionState::ACTIVATED);
}
ASSERT_THAT(GetChangedActiveApexes(),
UnorderedElementsAre("com.android.apex.test_package"));
}
TEST_F(ApexdMountTest, FailsToActivateStagedSession) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("apex.apexd_test.apex");
auto apex_session =
CreateStagedSession("apex.apexd_test_manifest_mismatch.apex", 73);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
// Quick check that session failed.
{
auto session = GetSessionManager()->GetSession(73);
ASSERT_THAT(session, Ok());
ASSERT_NE(session->GetState(), SessionState::ACTIVATED);
}
// Failed session should trigger "revert & reboot". However, "revert" fails
// while running tests (We'd better not reboot the device under test).
// Hence, failed session causes activating fallback(preinstalled) apexes.
// Note that activating pre-installed apexes doesn't necessarily mean
// "changed" active apexes because they could be the same previous active
// apexes, but we don't know if there was data APEXes that's removed by the
// failed session.
ASSERT_THAT(GetChangedActiveApexes(),
UnorderedElementsAre("com.android.apex.test_package"));
}
TEST_F(ApexdMountTest, FailsToActivateApexFallbacksToSystemOne) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("apex.apexd_test.apex");
AddDataApex("apex.apexd_test_manifest_mismatch.apex");
ASSERT_THAT(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
ASSERT_THAT(GetChangedActiveApexes(),
UnorderedElementsAre("com.android.apex.test_package"));
}
TEST_F(ApexdMountTest, SubmitSingleStagedSessionKeepsPreviousSessions) {
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
InitializeVold(&checkpoint_interface);
std::string preinstalled_apex = AddPreInstalledApex("apex.apexd_test.apex");
ASSERT_RESULT_OK(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}}));
// First simulate existence of a bunch of sessions.
auto session1 = GetSessionManager()->CreateSession(37);
ASSERT_RESULT_OK(session1);
ASSERT_RESULT_OK(session1->UpdateStateAndCommit(SessionState::STAGED));
auto session2 = GetSessionManager()->CreateSession(57);
ASSERT_RESULT_OK(session2);
ASSERT_RESULT_OK(session2->UpdateStateAndCommit(SessionState::STAGED));
auto session3 = GetSessionManager()->CreateSession(73);
ASSERT_RESULT_OK(session3);
ASSERT_RESULT_OK(session3->UpdateStateAndCommit(SessionState::SUCCESS));
PrepareStagedSession("apex.apexd_test.apex", 239);
ASSERT_RESULT_OK(SubmitStagedSession(239, {}, false, false, -1));
auto sessions = GetSessionManager()->GetSessions();
std::sort(
sessions.begin(), sessions.end(),
[](const auto& s1, const auto& s2) { return s1.GetId() < s2.GetId(); });
ASSERT_EQ(4u, sessions.size());
ASSERT_EQ(37, sessions[0].GetId());
ASSERT_EQ(SessionState::STAGED, sessions[0].GetState());
ASSERT_EQ(57, sessions[1].GetId());
ASSERT_EQ(SessionState::STAGED, sessions[1].GetState());
ASSERT_EQ(73, sessions[2].GetId());
ASSERT_EQ(SessionState::SUCCESS, sessions[2].GetState());
ASSERT_EQ(239, sessions[3].GetId());
ASSERT_EQ(SessionState::VERIFIED, sessions[3].GetState());
}
struct SpyMetrics : Metrics {
std::vector<std::tuple<InstallType, bool, ApexFileInfo>> requested;
std::vector<std::tuple<std::string, InstallResult>> ended;
void SendInstallationRequested(InstallType install_type, bool is_rollback,
const ApexFileInfo& info) override {
requested.emplace_back(install_type, is_rollback, info);
}
void SendInstallationEnded(const std::string& file_hash,
InstallResult result) override {
ended.emplace_back(file_hash, result);
}
};
TEST_F(ApexdMountTest, SendEventOnSubmitStagedSession) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
InitializeVold(&checkpoint_interface);
InitMetrics(std::make_unique<SpyMetrics>());
std::string preinstalled_apex =
AddPreInstalledApex("com.android.apex.vendor.foo.apex");
// Test APEX is a "vendor" APEX. Preinstalled partition should be vendor.
ASSERT_RESULT_OK(ApexFileRepository::GetInstance().AddPreInstalledApex(
{{ApexPartition::Vendor, GetBuiltInDir()}}));
OnStart();
PrepareStagedSession("com.android.apex.vendor.foo.with_vintf.apex", 239);
ASSERT_RESULT_OK(SubmitStagedSession(239, {}, false, false, -1));
auto spy = std::unique_ptr<SpyMetrics>(
static_cast<SpyMetrics*>(InitMetrics(nullptr).release()));
ASSERT_NE(nullptr, spy.get());
ASSERT_EQ(1u, spy->requested.size());
const auto& requested = spy->requested[0];
ASSERT_EQ(InstallType::Staged, std::get<0>(requested));
ASSERT_EQ("com.android.apex.vendor.foo"s, std::get<2>(requested).name);
ASSERT_THAT(std::get<2>(requested).hals, ElementsAre("android.apex.foo@1"s));
ASSERT_EQ(0u, spy->ended.size());
}
TEST_F(ApexdMountTest, SubmitStagedSessionSucceedVerifiedBrandNewApex) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
PrepareStagedSession("com.android.apex.brand.new.apex", 239);
ASSERT_RESULT_OK(SubmitStagedSession(239, {}, false, false, -1));
auto sessions = GetSessionManager()->GetSessions();
ASSERT_EQ(1u, sessions.size());
ASSERT_EQ(239, sessions[0].GetId());
ASSERT_EQ(SessionState::VERIFIED, sessions[0].GetState());
file_repository.Reset();
}
TEST_F(ApexdMountTest,
SubmitStagedSessionSucceedVerifiedBrandNewApexWithActiveVersion) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto data_apex = AddDataApex("com.android.apex.brand.new.apex");
ASSERT_THAT(ActivatePackage(data_apex), Ok());
PrepareStagedSession("com.android.apex.brand.new.v2.apex", 239);
ASSERT_RESULT_OK(SubmitStagedSession(239, {}, false, false, -1));
auto sessions = GetSessionManager()->GetSessions();
ASSERT_EQ(1u, sessions.size());
ASSERT_EQ(239, sessions[0].GetId());
ASSERT_EQ(SessionState::VERIFIED, sessions[0].GetState());
file_repository.Reset();
}
TEST_F(ApexdMountTest,
SubmitStagedSessionFailBrandNewApexMismatchActiveVersion) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
fs::copy(GetTestFile(
"apexd_testdata/com.android.apex.brand.new.another.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto data_apex = AddDataApex("com.android.apex.brand.new.apex");
ASSERT_THAT(ActivatePackage(data_apex), Ok());
PrepareStagedSession("com.android.apex.brand.new.v2.diffkey.apex", 239);
auto ret = SubmitStagedSession(239, {}, false, false, -1);
ASSERT_THAT(
ret,
HasError(WithMessage(("Brand-new APEX public key doesn't match existing "
"active APEX: com.android.apex.brand.new"))));
file_repository.Reset();
}
TEST_F(ApexdMountTest, SubmitStagedSessionFailBrandNewApexDisabled) {
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
PrepareStagedSession("com.android.apex.brand.new.apex", 239);
auto ret = SubmitStagedSession(239, {}, false, false, -1);
ASSERT_THAT(ret,
HasError(WithMessage(("No preinstalled apex found for unverified "
"package com.android.apex.brand.new"))));
file_repository.Reset();
}
TEST_F(ApexdUnitTest, StagePackagesSucceedVerifiedBrandNewApex) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto status = StagePackages({GetTestFile("com.android.apex.brand.new.apex")});
ASSERT_RESULT_OK(status);
auto staged_path = StringPrintf("%s/com.android.apex.brand.new@1.apex",
GetDataDir().c_str());
ASSERT_EQ(0, access(staged_path.c_str(), F_OK));
file_repository.Reset();
}
TEST_F(ApexdUnitTest, StagePackagesFailUnverifiedBrandNewApex) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile(
"apexd_testdata/com.android.apex.brand.new.another.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto status = StagePackages({GetTestFile("com.android.apex.brand.new.apex")});
ASSERT_THAT(status,
HasError(WithMessage(("No preinstalled apex found for unverified "
"package com.android.apex.brand.new"))));
file_repository.Reset();
}
TEST_F(ApexdMountTest, ActivatesStagedSessionSucceedVerifiedBrandNewApex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto apex_session =
CreateStagedSession("com.android.apex.brand.new.apex", 37);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
std::string active_apex =
GetDataDir() + "/" + "com.android.apex.brand.new@1.apex";
OnStart();
// Quick check that session was activated
{
auto session = GetSessionManager()->GetSession(37);
ASSERT_THAT(session, Ok());
ASSERT_EQ(session->GetState(), SessionState::ACTIVATED);
}
ASSERT_THAT(GetChangedActiveApexes(),
UnorderedElementsAre("com.android.apex.brand.new"));
file_repository.Reset();
}
TEST_F(ApexdMountTest, ActivatesStagedSessionFailUnverifiedBrandNewApex) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir;
fs::copy(GetTestFile(
"apexd_testdata/com.android.apex.brand.new.another.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto apex_session =
CreateStagedSession("com.android.apex.brand.new.apex", 37);
apex_session->UpdateStateAndCommit(SessionState::STAGED);
std::string active_apex =
GetDataDir() + "/" + "com.android.apex.brand.new@1.apex";
OnStart();
// Quick check that session was activated
{
auto session = GetSessionManager()->GetSession(37);
ASSERT_THAT(session, Ok());
ASSERT_EQ(session->GetState(), SessionState::ACTIVATION_FAILED);
}
// Failed session would trigger revert or fallback to preinstalled. In tests,
// fallback happens, but since brand-new apexes don't have pre-installed
// counterparts, fallback doesn't cause any "change".
ASSERT_THAT(GetChangedActiveApexes(), IsEmpty());
file_repository.Reset();
}
TEST_F(ApexdMountTest, NonStagedUpdateFailVerifiedBrandNewApex) {
ApexFileRepository::EnableBrandNewApex();
auto& file_repository = ApexFileRepository::GetInstance();
const auto partition = ApexPartition::System;
TemporaryDir trusted_key_dir, data_dir;
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
trusted_key_dir.path);
file_repository.AddBrandNewApexCredentialAndBlocklist(
{{partition, trusted_key_dir.path}});
auto file_path = AddDataApex("com.android.apex.brand.new.apex");
ASSERT_THAT(ActivatePackage(file_path), Ok());
auto ret = InstallPackage(GetTestFile("com.android.apex.brand.new.apex"),
/* force= */ false);
ASSERT_THAT(
ret,
HasError(WithMessage(HasSubstr("No preinstalled apex found for package "
"com.android.apex.brand.new"))));
file_repository.Reset();
}
TEST_F(ApexdMountTest, BootCompletedCleanup_CleanupInactiveApexes) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
AddPreInstalledApex("apex.apexd_test.apex");
auto selected = AddDataApex("apex.apexd_test_v2.apex");
auto ignored1 = AddDataApex("apex.apexd_test.apex");
auto ignored2 = AddDataApex("apex.apexd_test_different_app.apex");
auto& instance = ApexFileRepository::GetInstance();
ASSERT_THAT(instance.AddPreInstalledApex({{GetPartition(), GetBuiltInDir()}}),
Ok());
OnStart();
ASSERT_THAT(FindFilesBySuffix(data_dir_, {kApexPackageSuffix}),
HasValue(UnorderedElementsAre(selected, ignored1, ignored2)));
// Inactive data apexes are removed on boot completion.
BootCompletedCleanup();
ASSERT_THAT(FindFilesBySuffix(data_dir_, {kApexPackageSuffix}),
HasValue(UnorderedElementsAre(selected)));
}
class SubmitStagedSessionTest : public ApexdMountTest {
protected:
void SetUp() override {
ApexdMountTest::SetUp();
MockCheckpointInterface checkpoint_interface;
checkpoint_interface.SetSupportsCheckpoint(true);
InitializeVold(&checkpoint_interface);
// Has two preinstalled APEXes (for testing multi-APEX session)
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
ApexFileRepository::GetInstance().AddPreInstalledApex(
{{GetPartition(), GetBuiltInDir()}});
OnStart();
}
void TearDown() override {
// Should not leak temporary verity devices regardless of success.
// Why EXPECT? Needs to call TearDown() for unmounting even when something
// goes wrong with the test.
std::vector<DeviceMapper::DmBlockDevice> devices;
EXPECT_TRUE(DeviceMapper::Instance().GetAvailableDevices(&devices));
for (const auto& device : devices) {
EXPECT_THAT(device.name(), Not(EndsWith(".tmp")));
}
ApexdMountTest::TearDown();
}
};
TEST_F(SubmitStagedSessionTest, SimpleSuccess) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
}
TEST_F(SubmitStagedSessionTest, SuccessStoresBuildFingerprint) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
auto session = GetSessionManager()->GetSession(session_id);
ASSERT_NE(session->GetBuildFingerprint(), ""s);
}
TEST_F(SubmitStagedSessionTest,
RejectIfSamePackageIsAlreadyStaged_SameVersion) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(MarkStagedSessionReady(session_id), Ok());
auto session_id2 = 43;
PrepareStagedSession("apex.apexd_test.apex", session_id2);
ASSERT_THAT(SubmitStagedSession(session_id2, {}, false, false, -1),
HasError(WithMessage(HasSubstr("already staged"))));
}
TEST_F(SubmitStagedSessionTest,
RejectIfSamePackageIsAlreadyStaged_DifferentVersion) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(MarkStagedSessionReady(session_id), Ok());
auto session_id2 = 43;
PrepareStagedSession("apex.apexd_test_v2.apex", session_id2);
ASSERT_THAT(SubmitStagedSession(session_id2, {}, false, false, -1),
HasError(WithMessage(HasSubstr("already staged"))));
}
TEST_F(SubmitStagedSessionTest, RejectInstallPackageForStagedPackage) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(MarkStagedSessionReady(session_id), Ok());
ASSERT_THAT(
InstallPackage(GetTestFile("apex.apexd_test.apex"), /* force= */ true),
HasError(WithMessage(HasSubstr("already staged"))));
}
TEST_F(SubmitStagedSessionTest, AbortedSessionDoesNotBlockNewStagingOrInstall) {
if (IsMountBeforeDataEnabled()) GTEST_SKIP() << "mount_before_data enabled";
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(AbortStagedSession(session_id), Ok());
auto session_id2 = 43;
PrepareStagedSession("apex.apexd_test.apex", session_id2);
ASSERT_THAT(SubmitStagedSession(session_id2, {}, false, false, -1), Ok());
ASSERT_THAT(AbortStagedSession(session_id2), Ok());
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test.apex"),
/* force= */ true),
Ok());
}
TEST_F(SubmitStagedSessionTest, FailWithManifestMismatch) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test_manifest_mismatch.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1),
HasError(WithMessage(HasSubstr("does not match manifest"))));
}
TEST_F(SubmitStagedSessionTest, FailedSessionNotPersisted) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test_manifest_mismatch.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Not(Ok()));
auto session = GetSessionManager()->GetSession(session_id);
ASSERT_THAT(session, Not(Ok()));
}
TEST_F(SubmitStagedSessionTest, CannotBeRollbackAndHaveRollbackEnabled) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, /*has_rollback=*/true,
/*is_rollback*/ true, -1),
HasError(WithMessage(
HasSubstr("both a rollback and enabled for rollback"))));
}
TEST_F(SubmitStagedSessionTest, FailWithCorruptApex) {
auto session_id = 42;
PrepareStagedSession("apex.apexd_test_corrupt_apex.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1),
HasError(WithMessage(HasSubstr("corrupted?"))));
}
TEST_F(SubmitStagedSessionTest, SuccessWithMultiSession) {
auto parent_session_id = 42;
auto child_session1_id = 43;
auto child_session2_id = 44;
auto file1 = PrepareStagedSession("apex.apexd_test.apex", child_session1_id);
auto file2 = PrepareStagedSession("apex.apexd_test_different_app.apex",
child_session2_id);
auto ret = SubmitStagedSession(parent_session_id,
{child_session1_id, child_session2_id}, false,
false, -1);
ASSERT_THAT(ret, HasValue(ElementsAre(Property(&ApexFile::GetPath, file1),
Property(&ApexFile::GetPath, file2))));
auto session = GetSessionManager()->GetSession(parent_session_id);
ASSERT_THAT(session->GetChildSessionIds(),
ElementsAre(child_session1_id, child_session2_id));
}
// Test cases specific to mount_before_data
class MountBeforeDataTest : public ApexdMountTest {
protected:
void SetUp() override {
config_.mount_before_data = true;
ApexdMountTest::SetUp();
// preinstalled APEXes
AddPreInstalledApex("apex.apexd_test.apex");
AddPreInstalledApex("apex.apexd_test_different_app.apex");
}
void TearDown() override {
ApexdMountTest::TearDown();
// Unmap dm-linear devices mapped by ApexImageManager
for (const auto& image : image_manager_->GetAllImages()) {
image_manager_->UnmapImageIfExists(image);
}
}
void SimulateReboot() {
DeactivateAllPackages();
ApexFileRepository::GetInstance().Reset();
// Staged apexes in /data/app-staging are not accessible
DeleteDirContent(staged_session_dir_);
InitializeVold(nullptr);
}
};
TEST_F(MountBeforeDataTest, ActivatePinnedApex) {
ASSERT_EQ(0, OnBootstrap());
auto orig = ApexFile::Open(GetTestFile("apex.apexd_test_v2.apex"));
ASSERT_THAT(orig, Ok());
auto name = orig->GetManifest().name();
auto pinned = image_manager_->PinApexFiles(Single(*orig));
ASSERT_THAT(pinned, Ok());
auto image = pinned->at(0);
auto block_dev_path = image_manager_->MapImage(image);
ASSERT_THAT(block_dev_path, Ok());
auto unmap =
base::make_scope_guard([&]() { image_manager_->UnmapImage(image); });
ASSERT_THAT(ActivatePackage(*block_dev_path), Ok());
auto deactivate = base::make_scope_guard(
[&]() { ASSERT_THAT(DeactivatePackage(*block_dev_path), Ok()); });
// Checks if PopulateFromMounts() works okay with dm-linear device
MountedApexDatabase db;
db.PopulateFromMounts();
auto linear_name = image + kDmLinearPayloadSuffix;
ASSERT_THAT(db.GetLatestMountedApex(name),
Optional(Field(&MountedApexData::linear_name, linear_name)));
}
TEST_F(MountBeforeDataTest, NonStagedInstall_SucceedAgainstPreinstalled) {
ASSERT_EQ(0, OnBootstrap());
// Install succeeds.
const auto apex_name = "com.android.apex.test_package"s;
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test_v2.apex"),
/* force= */ true),
Ok());
// Active list is updated with new install.
auto active_list = image_manager_->GetApexList(ApexListType::ACTIVE);
ASSERT_THAT(active_list, HasValue(SizeIs(1)));
auto entry = active_list->at(0);
ASSERT_EQ(entry.apex_name, apex_name);
// Active mount is backed by the mapped device.
auto mount_data = GetApexDatabaseForTesting().GetLatestMountedApex(apex_name);
ASSERT_TRUE(mount_data.has_value());
ASSERT_THAT(image_manager_->MapImage(entry.image_name),
HasValue(mount_data->full_path));
}
TEST_F(MountBeforeDataTest, NonStagedInstall_SucceedAgainstData) {
ASSERT_EQ(0, OnBootstrap());
const auto apex_name = "com.android.apex.test_package"s;
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test.apex"),
/* force= */ true),
Ok());
// Keep the path of the newly installed apex
auto mount_data = GetApexDatabaseForTesting().GetLatestMountedApex(apex_name);
ASSERT_TRUE(mount_data.has_value());
auto data_apex = ApexFile::Open(mount_data->full_path);
// Second installation replaces the previous one.
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test_v2.apex"),
/* force= */ true),
Ok());
// and the previous apex is removed.
ASSERT_THAT(PathExists(data_apex->GetPath()), HasValue(false));
ASSERT_THAT(GetImageManager()->FindPinnedApex(*data_apex), Eq(std::nullopt));
}
TEST_F(MountBeforeDataTest, NonStagedInstall_FailToCreateBackingImage) {
// Setup failing image manager
struct MockApexImageManager : public ApexImageManager {
MockApexImageManager(std::string metadata_dir, std::string data_dir)
: ApexImageManager(metadata_dir, data_dir) {}
Result<std::vector<std::string>> PinApexFiles(
std::span<const ApexFile>) override {
return Error() << "Can't pin apex";
}
} test_im{metadata_images_dir_, data_images_dir_};
InitializeImageManager(&test_im);
auto guard =
make_scope_guard([&] { InitializeImageManager(image_manager_.get()); });
ASSERT_EQ(0, OnBootstrap());
auto mounts = GetApexMounts();
auto active_list = GetImageManager()->GetApexList(ApexListType::ACTIVE);
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test_v2.apex"),
/* force= */ true),
HasError(WithMessage("Can't pin apex")));
// Others remain the same
ASSERT_THAT(GetApexMounts(), UnorderedElementsAreArray(mounts));
ASSERT_EQ(GetImageManager()->GetApexList(ApexListType::ACTIVE), active_list);
}
TEST_F(MountBeforeDataTest, NonStagedInstall_FailToMapImage) {
// Setup failing image manager
struct MockApexImageManager : public ApexImageManager {
MockApexImageManager(std::string metadata_dir, std::string data_dir)
: ApexImageManager(metadata_dir, data_dir) {}
Result<std::string> MapImage(const std::string&) override {
return Error() << "Can't map image";
}
} test_im{metadata_images_dir_, data_images_dir_};
InitializeImageManager(&test_im);
auto guard =
make_scope_guard([&] { InitializeImageManager(image_manager_.get()); });
ASSERT_EQ(0, OnBootstrap());
auto mounts = GetApexMounts();
auto active_list = GetImageManager()->GetApexList(ApexListType::ACTIVE);
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test_v2.apex"),
/* force= */ true),
HasError(WithMessage("Can't map image")));
// Others remain the same
ASSERT_THAT(GetApexMounts(), UnorderedElementsAreArray(mounts));
ASSERT_EQ(GetImageManager()->GetApexList(ApexListType::ACTIVE), active_list);
// Pinned apex is deleted on error.
ASSERT_THAT(GetImageManager()->GetAllImages(), IsEmpty());
}
TEST_F(MountBeforeDataTest, StagingCreatesBackingImages) {
ASSERT_EQ(0, OnBootstrap());
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
auto session = GetSessionManager()->GetSession(session_id);
ASSERT_THAT(session->GetApexImages(),
Pointwise(Eq(), image_manager_->GetAllImages()));
}
TEST_F(MountBeforeDataTest, AbortSessionRemovesBackingImages) {
ASSERT_EQ(0, OnBootstrap());
auto session_id = 42;
PrepareStagedSession("apex.apexd_test.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(AbortStagedSession(session_id), Ok());
ASSERT_THAT(image_manager_->GetAllImages(), IsEmpty());
}
TEST_F(MountBeforeDataTest, OnBootstrapActivatesAllApexes) {
ASSERT_EQ(0, OnBootstrap());
ASSERT_THAT(GetApexMounts(),
UnorderedElementsAre("/apex/com.android.apex.test_package_2"s,
"/apex/com.android.apex.test_package_2@1"s,
"/apex/com.android.apex.test_package"s,
"/apex/com.android.apex.test_package@1"s));
}
TEST_F(MountBeforeDataTest, OnBootstrapActivatesAllApexes_ActivateData) {
// Prepare pinned data apex before onBootstrap()
auto data_apex = ApexFile::Open(GetTestFile("apex.apexd_test_v2.apex"));
auto pinned = image_manager_->PinApexFiles(Single(*data_apex));
ASSERT_THAT(pinned, Ok());
// Prepare the active list
std::vector<ApexListEntry> list;
list.emplace_back(pinned->at(0), data_apex->GetManifest().name());
ASSERT_THAT(image_manager_->UpdateApexList(ApexListType::ACTIVE, list), Ok());
ASSERT_EQ(0, OnBootstrap());
// Pinned apex (com.android.apex.test_package@2) is activated.
ASSERT_THAT(GetApexMounts(),
Contains("/apex/com.android.apex.test_package@2"));
}
TEST_F(MountBeforeDataTest, OnBootstrapActivatesAllApexes_IgnoreInvalidImage) {
// Prepare pinned data apex before onBootstrap()
auto data_apex = ApexFile::Open(GetTestFile("apex.apexd_test_v2.apex"));
auto pinned = image_manager_->PinApexFiles(Single(*data_apex));
ASSERT_THAT(pinned, Ok());
// Prepare the active list
std::vector<ApexListEntry> list;
list.emplace_back("invalid", "invalid"); // invalid
list.emplace_back(pinned->at(0), data_apex->GetManifest().name()); // valid
ASSERT_THAT(image_manager_->UpdateApexList(ApexListType::ACTIVE, list), Ok());
// OnBootstrap() should succeed with valid ones.
ASSERT_EQ(0, OnBootstrap());
ASSERT_THAT(GetApexMounts(),
Contains("/apex/com.android.apex.test_package@2"));
}
TEST_F(MountBeforeDataTest,
OnBootstrapActivatesAllApexes_FallbackToPreinstalled) {
// Prepare pinned data apex before onBootstrap()
auto data_apex =
ApexFile::Open(GetTestFile("apex.apexd_test_manifest_mismatch.apex"));
ASSERT_THAT(data_apex, Ok());
auto pinned = image_manager_->PinApexFiles(Single(*data_apex));
ASSERT_THAT(pinned, Ok());
// Prepare the active list
std::vector<ApexListEntry> list;
list.emplace_back(pinned->at(0), data_apex->GetManifest().name());
ASSERT_THAT(image_manager_->UpdateApexList(ApexListType::ACTIVE, list), Ok());
// OnBootstrap() should succeed with preinstalled apexes (@1).
ASSERT_EQ(0, OnBootstrap());
ASSERT_THAT(GetApexMounts(),
Contains("/apex/com.android.apex.test_package@1"));
// On boot-completed, the problematic apex should be removed.
BootCompletedCleanup();
ASSERT_THAT(image_manager_->GetAllImages(), IsEmpty());
ASSERT_THAT(image_manager_->GetApexList(ApexListType::ACTIVE),
HasValue(IsEmpty()));
}
TEST_F(MountBeforeDataTest, OnBootstrapActivatesStagedSessions) {
// Given that com.android.apex.test_package@1 is preinstalled
ASSERT_EQ(0, OnBootstrap());
auto mounts = GetApexMounts();
// Stage com.android.apex.test_package@2
auto session_id = 42;
PrepareStagedSession("apex.apexd_test_v2.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(MarkStagedSessionReady(session_id), Ok());
SimulateReboot();
ASSERT_THAT(OnBootstrap(), Eq(0));
// Staged session should be activated.
auto session = GetSessionManager()->GetSession(session_id);
ASSERT_THAT(session, Ok());
ASSERT_EQ(session->GetState(), SessionState::ACTIVATED);
// The apex in the session should be activated.
std::ranges::replace(mounts, "/apex/com.android.apex.test_package@1"s,
"/apex/com.android.apex.test_package@2"s);
ASSERT_THAT(GetApexMounts(), UnorderedElementsAreArray(mounts));
}
TEST_F(MountBeforeDataTest, OnStartSkipsActivation) {
ASSERT_EQ(0, OnBootstrap());
auto mounts = GetApexMounts();
// Apexes in /data/apex/active should be ignored.
AddDataApex("apex.apexd_test_v2.apex");
OnStart();
// Mounts remain unchanged.
ASSERT_THAT(GetApexMounts(), Eq(mounts));
}
TEST_F(MountBeforeDataTest, BootCompletedCleanup_RemovesInactiveDataApexes) {
// apex0 is valid and apex1 is unknown.
auto apex0 = ApexFile::Open(GetTestFile("apex.apexd_test_v2.apex"));
auto apex1 = ApexFile::Open(GetTestFile("test.rebootless_apex_v1.apex"));
auto pinned = image_manager_->PinApexFiles(std::vector{*apex0, *apex1});
ASSERT_THAT(pinned, HasValue(SizeIs(2)));
std::vector<ApexListEntry> active_list{
{pinned->at(0), apex0->GetManifest().name()},
{pinned->at(1), apex1->GetManifest().name()},
};
ASSERT_THAT(image_manager_->UpdateApexList(ApexListType::ACTIVE, active_list),
Ok());
// APEX files in /data/apex/active should be skipped and removed.
auto data_apex = AddDataApex("apex.apexd_test_v2.apex");
ASSERT_EQ(0, OnBootstrap());
BootCompletedCleanup();
ASSERT_THAT(PathExists(data_apex), HasValue(false));
ASSERT_THAT(image_manager_->GetAllImages(),
UnorderedElementsAre(pinned->at(0)));
}
TEST_F(MountBeforeDataTest, BootCompletedCleanup_CreatesConfigFile) {
if (!flags::mount_before_data()) {
GTEST_SKIP() << "mount_before_data is off";
}
ASSERT_EQ(0, OnBootstrap());
BootCompletedCleanup();
auto config_file = metadata_config_dir_ + "/mount_before_data";
ASSERT_EQ(0, access(config_file.c_str(), F_OK));
}
TEST_F(MountBeforeDataTest, BrandNewApex) {
fs::copy(GetTestFile("apexd_testdata/com.android.apex.brand.new.avbpubkey"),
brand_new_config_dir_);
ApexFileRepository::EnableBrandNewApex();
ASSERT_EQ(0, OnBootstrap());
// Prepare brand-new apex installation
auto session_id = 42;
PrepareStagedSession("com.android.apex.brand.new.apex", session_id);
ASSERT_THAT(SubmitStagedSession(session_id, {}, false, false, -1), Ok());
ASSERT_THAT(MarkStagedSessionReady(session_id), Ok());
SimulateReboot();
ApexFileRepository::EnableBrandNewApex();
ASSERT_EQ(0, OnBootstrap());
// Staged session should be activated.
auto session = GetSessionManager()->GetSession(session_id);
ASSERT_THAT(session, Ok());
ASSERT_EQ(session->GetState(), SessionState::ACTIVATED);
ApexFileRepository::GetInstance().Reset();
}
TEST_F(MountBeforeDataTest, UnstagePackages) {
ASSERT_EQ(0, OnBootstrap());
// Install v2.
const auto apex_name = "com.android.apex.test_package"s;
ASSERT_THAT(InstallPackage(GetTestFile("apex.apexd_test_v2.apex"),
/* force= */ true),
Ok());
ASSERT_THAT(GetApexMounts(),
Contains("/apex/com.android.apex.test_package@2"));
auto mount_data = GetApexDatabaseForTesting().GetLatestMountedApex(apex_name);
ASSERT_TRUE(mount_data.has_value());
// Remove v2.
ASSERT_THAT(UnstagePackages({mount_data->full_path}), Ok());
SimulateReboot();
ASSERT_THAT(OnBootstrap(), Eq(0));
// Now v1 is activated.
ASSERT_THAT(GetApexMounts(),
Contains("/apex/com.android.apex.test_package@1"));
}
class LogTestToLogcat : public ::testing::EmptyTestEventListener {
void OnTestStart(const ::testing::TestInfo& test_info) override {
#ifdef __ANDROID__
using base::LogId;
using base::LogSeverity;
using base::StringPrintf;
base::LogdLogger l;
std::string msg =
StringPrintf("=== %s::%s (%s:%d)", test_info.test_suite_name(),
test_info.name(), test_info.file(), test_info.line());
l(LogId::MAIN, LogSeverity::INFO, "ApexTestCases", __FILE__, __LINE__,
msg.c_str());
#else
UNUSED(test_info);
#endif
}
};
} // namespace apex
} // namespace android
int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
android::base::InitLogging(argv, &android::base::StderrLogger);
android::base::SetMinimumLogSeverity(android::base::VERBOSE);
::testing::UnitTest::GetInstance()->listeners().Append(
new android::apex::LogTestToLogcat());
return RUN_ALL_TESTS();
}