diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-07-15 01:37:38 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-07-15 01:37:38 +0000 |
commit | 70d7a402c973601eb2ef02d8fe106c179c3dc092 (patch) | |
tree | af0ac8b6efbbfc1f757d21c6ca571de5c9ab1f6d | |
parent | 0bf2df681092be4d4fd192c45ab730bcd7d1a919 (diff) | |
parent | 36eaa49ffd1383c74e08a04241f9a2d647d1d08d (diff) | |
download | apex-70d7a402c973601eb2ef02d8fe106c179c3dc092.tar.gz |
Snap for 7550844 from 36eaa49ffd1383c74e08a04241f9a2d647d1d08d to mainline-os-statsd-release
Change-Id: Ia0c365feeee34e58d169fc17d33091fd5356dbd9
336 files changed, 16736 insertions, 3117 deletions
diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_not_pre_installed_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_not_pre_installed_apex.asciipb new file mode 100644 index 00000000..c822b0bc --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_not_pre_installed_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_arm64/com.android.apex.cts.shim_not_pre_installed.apex" + } + dest_file: "shim/prebuilts//arm/com.android.apex.cts.shim_not_pre_installed.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v1_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v1_apex.asciipb index 5bef6b27..543d8c5f 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v1_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v1_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v1.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_file_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_file_apex.asciipb index f2032412..d2dfb053 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_file_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_file_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_additional_file.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb index d5b1515a..c889a0d3 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_additional_folder.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apex.asciipb index 5c1ecd8d..1e50d3e2 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb index a3e05b58..4b98849a 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb index d4f42de6..80044ca9 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_different_certificate.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb index 30c1658b..f5aa4a92 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_different_package_name.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb index 7a4ba73a..c504e63f 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_no_hashtree.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_rebootless_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_rebootless_apex.asciipb new file mode 100644 index 00000000..1d000697 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_rebootless_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_arm64/com.android.apex.cts.shim.v2_rebootless.apex" + } + dest_file: "shim/prebuilts//arm/com.android.apex.cts.shim.v2_rebootless.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb index e046e3ab..cc9b5ace 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_sdk_target_p.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb new file mode 100644 index 00000000..94aa04c2 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_arm64/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex" + } + dest_file: "shim/prebuilts//arm/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb index 6ea9a6b5..011e61c6 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_signed_bob.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb index fe76dce4..e9de8cdf 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_signed_bob_rot.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb index c90ffbbe..361a6509 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb index 779f4993..be2c4d01 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_unsigned_payload.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb index 0bf9288a..d141aba8 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_with_post_install_hook.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb index 5a1baf9b..b1be9fa1 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_with_pre_install_hook.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb index 316c8d12..99d90dd7 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_without_apk_in_apex.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb index 3d5c0e90..069a9a26 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v2_wrong_sha.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_apex.asciipb index e03ee6e7..7aca1421 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v3.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_rebootless_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_rebootless_apex.asciipb new file mode 100644 index 00000000..d959e8ca --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_rebootless_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_arm64/com.android.apex.cts.shim.v3_rebootless.apex" + } + dest_file: "shim/prebuilts//arm/com.android.apex.cts.shim.v3_rebootless.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb index daecf3c3..1afd6a1e 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v3_signed_bob.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb index 420522bb..db11b22e 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__arm_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_arm64/com.android.apex.cts.shim.v3_signed_bob_rot.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_not_pre_installed_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_not_pre_installed_apex.asciipb new file mode 100644 index 00000000..0fc57dc4 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_not_pre_installed_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_x86_64/com.android.apex.cts.shim_not_pre_installed.apex" + } + dest_file: "shim/prebuilts//x86/com.android.apex.cts.shim_not_pre_installed.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v1_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v1_apex.asciipb index 239132ac..48d0ec27 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v1_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v1_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v1.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_file_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_file_apex.asciipb index f84e0c89..bedd9d44 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_file_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_file_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_additional_file.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb index 635e2bbd..6d573350 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_additional_folder_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_additional_folder.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apex.asciipb index cea53405..80483302 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb index 2622af8f..ab706c27 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_apk_in_apex_sdk_target_p_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb index c25509a2..bfcdbfb6 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_certificate_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_different_certificate.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb index ab8601fc..ec85bbb4 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_different_package_name_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_different_package_name.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb index f1ccd780..c917485e 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_no_hashtree_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_no_hashtree.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_rebootless_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_rebootless_apex.asciipb new file mode 100644 index 00000000..ecaff669 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_rebootless_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_rebootless.apex" + } + dest_file: "shim/prebuilts//x86/com.android.apex.cts.shim.v2_rebootless.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb index 1f24dedd..508be286 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sdk_target_p_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_sdk_target_p.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb new file mode 100644 index 00000000..8cce7ac6 --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_sign_payload_with_different_key_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex" + } + dest_file: "shim/prebuilts//x86/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb index e8c48e1e..5b1ebcea 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_signed_bob.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb index faf0e28b..de02bc9a 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_signed_bob_rot.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb index 5f1e6642..42a8c798 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_signed_bob_rot_rollback_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb index aef234a7..89fc7b6f 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_unsigned_payload_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_unsigned_payload.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb index 87b7c50f..0b378c1b 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_post_install_hook_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_with_post_install_hook.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb index 727bce0d..3a5a6a87 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_with_pre_install_hook_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_with_pre_install_hook.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb index 23155ba5..5c7f1186 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_without_apk_in_apex_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_without_apk_in_apex.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb index 2190954d..beedae5d 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v2_wrong_sha_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v2_wrong_sha.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_apex.asciipb index e7006ae6..53ddf732 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v3.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_rebootless_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_rebootless_apex.asciipb new file mode 100644 index 00000000..2bc42d2d --- /dev/null +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_rebootless_apex.asciipb @@ -0,0 +1,13 @@ +drops { + android_build_drop { + build_id: "7533747" + target: "CtsShim" + source_file: "aosp_x86_64/com.android.apex.cts.shim.v3_rebootless.apex" + } + dest_file: "shim/prebuilts//x86/com.android.apex.cts.shim.v3_rebootless.apex" + version: "" + version_group: "" + git_project: "platform/system/apex" + git_branch: "sc-dev" + transform: TRANSFORM_NONE +} diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb index bdcb62fb..0e4cbdf0 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v3_signed_bob.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb index 4f258589..9faf7dfd 100644 --- a/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb +++ b/.prebuilt_info/prebuilt_info_shim_prebuilts__x86_com_android_apex_cts_shim_v3_signed_bob_rot_apex.asciipb @@ -1,6 +1,6 @@ drops { android_build_drop { - build_id: "6508977" + build_id: "7533747" target: "CtsShim" source_file: "aosp_x86_64/com.android.apex.cts.shim.v3_signed_bob_rot.apex" } @@ -8,5 +8,6 @@ drops { version: "" version_group: "" git_project: "platform/system/apex" - git_branch: "rvc-dev" + git_branch: "sc-dev" + transform: TRANSFORM_NONE } diff --git a/Android.bp b/Android.bp new file mode 100644 index 00000000..b198f250 --- /dev/null +++ b/Android.bp @@ -0,0 +1,68 @@ +// This introduces the module type library_linking_strategy_cc_defaults +// To use in other Android.bp files, add the following lines: +// soong_config_module_type_import { +// from: "system/apex/Android.bp", +// module_types: ["library_linking_strategy_cc_defaults"], +// } + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +soong_config_string_variable { + name: "library_linking_strategy", + values: [ + "prefer_static", + ], +} + +soong_config_module_type { + name: "library_linking_strategy_cc_defaults", + module_type: "cc_defaults", + config_namespace: "ANDROID", + variables: ["library_linking_strategy"], + properties: [ + "shared_libs", + "static_libs", + "stl", + ], +} + +// TODO(b/178585590): delete this after testing linking strategy +soong_config_module_type { + name: "library_linking_strategy_apex_defaults", + module_type: "apex_defaults", + config_namespace: "ANDROID", + variables: ["library_linking_strategy"], + properties: [ + "manifest", + "min_sdk_version", + ], +} + +library_linking_strategy_cc_defaults { + name: "library_linking_strategy_sample_defaults", + soong_config_variables: { + library_linking_strategy: { + prefer_static: { + static_libs: [ + "libbase", + "liblog", + ], + stl: "c++_static", + }, + conditions_default: { + shared_libs: [ + "libbase", + "liblog", + ], + }, + }, + }, +} + +cc_binary { + name: "library_linking_strategy_sample_binary", + srcs: ["library_linking_strategy.cc"], + defaults: ["library_linking_strategy_sample_defaults"], +} diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 4c6fbd61..ccaf5feb 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -6,6 +6,7 @@ clang_format = true commit_msg_changeid_field = true commit_msg_test_field = true gofmt = true +pylint3 = true [Builtin Hooks Options] -clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
\ No newline at end of file +clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp diff --git a/apexd/Android.bp b/apexd/Android.bp index e8d221d5..ba2de033 100644 --- a/apexd/Android.bp +++ b/apexd/Android.bp @@ -1,25 +1,46 @@ // List of clang-tidy checks that are reported as errors. // Please keep this list ordered lexicographically. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + tidy_errors = [ + "android-*", + "bugprone-infinite-loop", + "bugprone-macro-parentheses", + "bugprone-misplaced-widening-cast", + "bugprone-move-forwarding-reference", + "bugprone-sizeof-container", + "bugprone-sizeof-expression", + "bugprone-string-constructor", + "bugprone-terminating-continue", + "bugprone-undefined-memory-manipulation", + "bugprone-undelegated-constructor", + // "bugprone-unhandled-self-assignment", // found in apex_manifest.proto + "bugprone-unused-raii", "cert-err34-c", "google-default-arguments", - "google-explicit-constructor", + // "google-explicit-constructor", // found in com_android_apex.h + "google-readability-avoid-underscore-in-googletest-name", + "google-readability-todo", "google-runtime-int", "google-runtime-member-string-references", "misc-move-const-arg", "misc-move-forwarding-reference", - "misc-unused-parameters", + // "misc-unused-parameters", // found in apexd_utils.h "misc-unused-using-decls", "misc-use-after-move", - "modernize-pass-by-value", + // "modernize-pass-by-value", // found in apex_database.h "performance-faster-string-find", "performance-for-range-copy", "performance-implicit-conversion-in-loop", "performance-inefficient-vector-operation", "performance-move-const-arg", - "performance-move-constructor-init", + // "performance-move-constructor-init", // found in apexd_loop.h "performance-noexcept-move-constructor", + "performance-unnecessary-copy-initialization", "performance-unnecessary-value-param", + // "readability-avoid-const-params-in-decls", // found in apexd.h ] cc_defaults { @@ -47,8 +68,8 @@ cc_defaults { tidy_checks: tidy_errors, tidy_checks_as_errors: tidy_errors, tidy_flags: [ - "-format-style='file'", - "--header-filter='system/apex/'", + "-format-style=file", + "-header-filter=system/apex/", ], } @@ -58,7 +79,6 @@ cc_defaults { defaults: ["libapex-deps"], shared_libs: [ "libbinder", - "libselinux", "liblog", "liblogwrap", ], @@ -69,6 +89,7 @@ cc_defaults { "libext2_uuid", "libverity_tree", "libvold_binder", + "libxml2", ], whole_static_libs: ["com.android.sysprop.apex"], } @@ -81,6 +102,8 @@ aidl_interface { "aidl/android/apex/ApexInfoList.aidl", "aidl/android/apex/ApexSessionInfo.aidl", "aidl/android/apex/ApexSessionParams.aidl", + "aidl/android/apex/CompressedApexInfo.aidl", + "aidl/android/apex/CompressedApexInfoList.aidl", "aidl/android/apex/IApexService.aidl", ], local_include_dir: "aidl", @@ -127,14 +150,15 @@ cc_library_static { srcs: [ "apex_database.cpp", "apexd.cpp", + "apexd_lifecycle.cpp", "apexd_loop.cpp", "apexd_prepostinstall.cpp", "apexd_private.cpp", - "apexd_prop.cpp", "apexd_session.cpp", "apexd_verity.cpp", ], export_include_dirs: ["."], + generated_sources: ["apex-info-list"], // Don't add shared/static libs here; add to libapexd_defaults instead. } @@ -170,6 +194,9 @@ cc_library_static { static_libs: [ "libapexd", ], + cflags: [ + "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION", + ], } cc_defaults { @@ -180,6 +207,7 @@ cc_defaults { "libcutils", "libprotobuf-cpp-full", "libziparchive", + "libselinux", ], static_libs: [ "lib_apex_session_state_proto", @@ -189,6 +217,7 @@ cc_defaults { static: { whole_static_libs: ["libc++fs"], }, + cpp_std: "experimental", shared: { static_libs: ["libc++fs"], }, @@ -202,8 +231,8 @@ cc_library_static { ], srcs: [ "apex_file.cpp", + "apex_file_repository.cpp", "apex_manifest.cpp", - "apex_preinstalled_data.cpp", "apex_shim.cpp", ], host_supported: true, @@ -223,7 +252,7 @@ cc_library_static { genrule { // Generates an apex which has a different manifest outside the filesystem // image. - name: "gen_bad_apexes", + name: "gen_manifest_mismatch_apex", out: ["apex.apexd_test_manifest_mismatch.apex"], srcs: [":apex.apexd_test"], tools: ["soong_zip", "zipalign", "conv_apex_manifest"], @@ -237,6 +266,38 @@ genrule { } genrule { + // Generates an apex which has a different manifest outside the filesystem + // image. + name: "gen_manifest_mismatch_apex_no_hashtree", + out: ["apex.apexd_test_no_hashtree_manifest_mismatch.apex"], + srcs: [":apex.apexd_test_no_hashtree"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop version 137 $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/apex.apexd_test_no_hashtree_manifest_mismatch.apex" +} + +genrule { + // Generates an apex with a corrupted filesystem superblock, which should cause + // Apex::Open to fail + name: "gen_corrupt_superblock_apex", + out: ["apex.apexd_test_corrupt_superblock_apex.apex"], + srcs: [":apex.apexd_test"], + tools: ["soong_zip", "zipalign"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "dd if=/dev/zero of=$(genDir)/apex_payload.img conv=notrunc bs=1024 seek=1 count=1 && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/apex.apexd_test_corrupt_superblock_apex.apex" +} + +genrule { // Generates an apex with a corrupted filesystem image, which should cause // dm-verity verification to fail name: "gen_corrupt_apex", @@ -252,6 +313,93 @@ genrule { "$(genDir)/apex.apexd_test_corrupt_apex.apex" } +genrule { + // Extract the root digest with avbtool + name: "apex.apexd_test_digest", + out: ["apex.apexd_test_digest.txt"], + srcs: [":apex.apexd_test"], + tools: ["avbtool"], + cmd: "unzip -q $(in) -d $(genDir) apex_payload.img && " + + "$(location avbtool) print_partition_digests --image $(genDir)/apex_payload.img " + + "| cut -c 3-| tee $(out)" +} + +genrule { + // Extract the root digest with avbtool + name: "apex.apexd_test_f2fs_digest", + out: ["apex.apexd_test_f2fs_digest.txt"], + srcs: [":apex.apexd_test_f2fs"], + tools: ["avbtool"], + cmd: "unzip -q $(in) -d $(genDir) apex_payload.img && " + + "$(location avbtool) print_partition_digests --image $(genDir)/apex_payload.img " + + "| cut -c 3-| tee $(out)" +} + +genrule { + // Generates an apex which has same module name as apex.apexd_test.apex, but + // is actually signed with a different key. + name: "gen_key_mismatch_apex", + out: ["apex.apexd_test_different_key.apex"], + srcs: [":apex.apexd_test_no_inst_key"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop name com.android.apex.test_package $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/apex.apexd_test_different_key.apex" +} + +genrule { + // Generates an apex which has same module name as apex.apexd_test.apex, but + // is actually signed with a different key. + name: "gen_key_mismatch_apex_v2", + out: ["apex.apexd_test_different_key_v2.apex"], + srcs: [":apex.apexd_test_no_inst_key"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop name com.android.apex.test_package $(genDir)/apex_manifest.pb && " + + "$(location conv_apex_manifest) setprop version 2 $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/apex.apexd_test_different_key_v2.apex" +} + +genrule { + // Generates an apex which has a different manifest outside the filesystem + // image. + name: "gen_manifest_mismatch_rebootless_apex", + out: ["test.rebootless_apex_manifest_mismatch.apex"], + srcs: [":test.rebootless_apex_v1"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop version 137 $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/test.rebootless_apex_manifest_mismatch.apex" +} + +genrule { + // Generates an apex with a corrupted filesystem image, which should cause + // dm-verity verification to fail + name: "gen_corrupt_rebootless_apex", + out: ["test.rebootless_apex_corrupted.apex"], + srcs: [":test.rebootless_apex_v1"], + tools: ["soong_zip", "zipalign"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "dd if=/dev/zero of=$(genDir)/apex_payload.img conv=notrunc bs=1024 seek=16 count=1 && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/test.rebootless_apex_corrupted.apex" +} + cc_test { name: "ApexTestCases", defaults: [ @@ -266,18 +414,32 @@ cc_test { ], data: [ ":apex.apexd_test", + ":apex.apexd_test_f2fs", + ":apex.apexd_test_digest", + ":apex.apexd_test_f2fs_digest", ":apex.apexd_test_different_app", ":apex.apexd_test_no_hashtree", ":apex.apexd_test_no_hashtree_2", ":apex.apexd_test_no_inst_key", + ":apex.apexd_test_f2fs_no_inst_key", ":apex.apexd_test_nocode", ":apex.apexd_test_postinstall", ":apex.apexd_test_preinstall", ":apex.apexd_test_prepostinstall.fail", ":apex.apexd_test_v2", ":apex.corrupted_b146895998", - ":gen_bad_apexes", + ":apex.banned_name", + ":gen_key_mismatch_apex", + ":gen_key_mismatch_apex_v2", + ":gen_key_mismatch_capex", + ":gen_manifest_mismatch_apex", + ":gen_manifest_mismatch_apex_no_hashtree", + ":gen_corrupt_superblock_apex", ":gen_corrupt_apex", + ":gen_capex_not_decompressible", + ":gen_capex_without_apex", + ":gen_capex_with_v2_apex", + ":gen_key_mismatch_with_original_capex", ":com.android.apex.cts.shim.v1_prebuilt", ":com.android.apex.cts.shim.v2_prebuilt", ":com.android.apex.cts.shim.v2_wrong_sha_prebuilt", @@ -285,13 +447,41 @@ cc_test { ":com.android.apex.cts.shim.v2_additional_folder_prebuilt", ":com.android.apex.cts.shim.v2_with_pre_install_hook_prebuilt", ":com.android.apex.cts.shim.v2_with_post_install_hook_prebuilt", + ":com.android.apex.compressed_sharedlibs", + ":com.android.apex.compressed.v1", + ":com.android.apex.compressed.v1_different_digest", + ":com.android.apex.compressed.v1_different_digest_original", + ":com.android.apex.compressed.v1_original", + ":com.android.apex.compressed.v2", + ":com.android.apex.compressed.v2_original", + ":gen_manifest_mismatch_compressed_apex_v2", "apexd_testdata/com.android.apex.test_package.avbpubkey", + "apexd_testdata/com.android.apex.compressed.avbpubkey", + ":com.android.apex.test.sharedlibs_generated.v1.libvX_prebuilt", + ":com.android.apex.test.sharedlibs_generated.v2.libvY_prebuilt", + ":test.rebootless_apex_v1", + ":test.rebootless_apex_v2", + ":test.rebootless_apex_v2_no_hashtree", + ":gen_manifest_mismatch_rebootless_apex", + ":gen_corrupt_rebootless_apex", + ":test.rebootless_apex_provides_sharedlibs", + ":test.rebootless_apex_provides_native_libs", + ":test.rebootless_apex_requires_shared_apex_libs", + ":test.rebootless_apex_jni_libs", + ":test.rebootless_apex_add_native_lib", + ":test.rebootless_apex_remove_native_lib", + ":test.rebootless_apex_app_in_apex", + ":test.rebootless_apex_priv_app_in_apex", ], srcs: [ "apex_database_test.cpp", "apex_file_test.cpp", + "apex_file_repository_test.cpp", "apex_manifest_test.cpp", + "apexd_test.cpp", + "apexd_session_test.cpp", "apexd_verity_test.cpp", + "apexd_utils_test.cpp", "apexservice_test.cpp", ], host_supported: false, @@ -308,6 +498,7 @@ cc_test { "libfs_mgr", "libutils", ], + generated_sources: ["apex-info-list"], test_suites: ["device-tests"], test_config: "AndroidTest.xml", } @@ -330,3 +521,10 @@ cc_test { test_config: "flattened_apex_test_config.xml", } +xsd_config { + name: "apex-info-list", + srcs: ["ApexInfoList.xsd"], + package_name: "com.android.apex", + api_dir: "apex-info-list-api", + gen_writer: true, +} diff --git a/apexd/ApexInfoList.xsd b/apexd/ApexInfoList.xsd new file mode 100644 index 00000000..440b975f --- /dev/null +++ b/apexd/ApexInfoList.xsd @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> +<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> + <xs:element name="apex-info-list"> + <xs:complexType> + <xs:sequence> + <xs:element ref="apex-info" minOccurs="1" maxOccurs="unbounded"/> + </xs:sequence> + </xs:complexType> + </xs:element> + + <xs:element name="apex-info"> + <xs:complexType> + <xs:attribute name="moduleName" type="xs:string" use="required"/> + <xs:attribute name="modulePath" type="xs:string" use="required"/> + <xs:attribute name="preinstalledModulePath" type="xs:string"/> + <xs:attribute name="versionCode" type="xs:long" use="required"/> + <xs:attribute name="versionName" type="xs:string" use="required"/> + <xs:attribute name="isFactory" type="xs:boolean" use="required"/> + <xs:attribute name="isActive" type="xs:boolean" use="required"/> + <xs:attribute name="lastUpdateMillis" type="xs:long"/> + </xs:complexType> + </xs:element> +</xs:schema> diff --git a/apexd/OWNERS b/apexd/OWNERS deleted file mode 100644 index ed9813ee..00000000 --- a/apexd/OWNERS +++ /dev/null @@ -1 +0,0 @@ -agampe@google.com diff --git a/apexd/aidl/android/apex/ApexSessionInfo.aidl b/apexd/aidl/android/apex/ApexSessionInfo.aidl index a7a6daf2..65da1135 100644 --- a/apexd/aidl/android/apex/ApexSessionInfo.aidl +++ b/apexd/aidl/android/apex/ApexSessionInfo.aidl @@ -29,4 +29,5 @@ parcelable ApexSessionInfo { boolean isReverted; boolean isRevertFailed; @utf8InCpp String crashingNativeProcess; + @utf8InCpp String errorMessage; } diff --git a/tests/src/com/android/tests/apex/ConscryptHostTest.java b/apexd/aidl/android/apex/CompressedApexInfo.aidl index 01cb840e..48015bd0 100644 --- a/tests/src/com/android/tests/apex/ConscryptHostTest.java +++ b/apexd/aidl/android/apex/CompressedApexInfo.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,15 +14,10 @@ * limitations under the License. */ -package com.android.tests.apex; +package android.apex; -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class ConscryptHostTest extends ApexE2EBaseHostTest { +parcelable CompressedApexInfo { + @utf8InCpp String moduleName; + long versionCode; + long decompressedSize; } diff --git a/tests/src/com/android/tests/apex/IpSecHostTest.java b/apexd/aidl/android/apex/CompressedApexInfoList.aidl index 765f4355..e1620bd1 100644 --- a/tests/src/com/android/tests/apex/IpSecHostTest.java +++ b/apexd/aidl/android/apex/CompressedApexInfoList.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * 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. @@ -14,13 +14,10 @@ * limitations under the License. */ -package com.android.tests.apex; +package android.apex; -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import android.apex.CompressedApexInfo; -import org.junit.runner.RunWith; - -/** Test to check if Apex can be staged, activated and uninstalled successfully. */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class IpSecHostTest extends ApexE2EBaseHostTest { +parcelable CompressedApexInfoList { + CompressedApexInfo[] apexInfos; } diff --git a/apexd/aidl/android/apex/IApexService.aidl b/apexd/aidl/android/apex/IApexService.aidl index 3626d9ac..2d77a822 100644 --- a/apexd/aidl/android/apex/IApexService.aidl +++ b/apexd/aidl/android/apex/IApexService.aidl @@ -20,6 +20,7 @@ import android.apex.ApexInfo; import android.apex.ApexInfoList; import android.apex.ApexSessionInfo; import android.apex.ApexSessionParams; +import android.apex.CompressedApexInfoList; interface IApexService { void submitStagedSession(in ApexSessionParams params, out ApexInfoList packages); @@ -36,9 +37,9 @@ interface IApexService { /** * Copies the CE apex data directory for the given user to the backup - * location, and returns the inode of the snapshot directory. + * location. */ - long snapshotCeData(int user_id, int rollback_id, in @utf8InCpp String apex_name); + void snapshotCeData(int user_id, int rollback_id, in @utf8InCpp String apex_name); /** * Restores the snapshot of the CE apex data directory for the given user and @@ -52,6 +53,11 @@ interface IApexService { void destroyDeSnapshots(int rollback_id); /** + * Deletes credential-encrypted snapshots for the given user, for the given rollback id. + */ + void destroyCeSnapshots(int user_id, int rollback_id); + + /** * Deletes all credential-encrypted snapshots for the given user, except for * those listed in retain_rollback_ids. */ @@ -115,4 +121,42 @@ interface IApexService { * on user builds. Only root is allowed to call this method. */ void remountPackages(); + /** + * Forces apexd to recollect pre-installed data from the given |paths|. + * + * Not meant for use outside of testing. This call will not be functional + * on user builds. Only root is allowed to call this method. + */ + void recollectPreinstalledData(in @utf8InCpp List<String> paths); + /** + * Forces apexd to recollect data apex from the given |path|. + * + * Not meant for use outside of testing. This call will not be functional + * on user builds. Only root is allowed to call this method. + */ + void recollectDataApex(in @utf8InCpp String path, in@utf8InCpp String decompression_dir); + + /** + * Informs apexd that the boot has completed. + */ + void markBootCompleted(); + + /** + * Assuming the provided compressed APEX will be installed on next boot, + * calculate how much space will be required for decompression + */ + long calculateSizeForCompressedApex(in CompressedApexInfoList compressed_apex_info_list); + + /** + * Reserve space on /data partition for compressed APEX decompression. Returns error if + * reservation fails. If empty list is passed, then reserved space is deallocated. + */ + void reserveSpaceForCompressedApex(in CompressedApexInfoList compressed_apex_info_list); + + /** + * Performs a non-staged install of the given APEX. + * Note: don't confuse this to preInstall and postInstall binder calls which are only used to + * test corresponding features of APEX packages. + */ + ApexInfo installAndActivatePackage(in @utf8InCpp String packagePath); } diff --git a/apexd/apex-info-list-api/current.txt b/apexd/apex-info-list-api/current.txt new file mode 100644 index 00000000..8db5a9a0 --- /dev/null +++ b/apexd/apex-info-list-api/current.txt @@ -0,0 +1,45 @@ +// Signature format: 2.0 +package com.android.apex { + + public class ApexInfo { + ctor public ApexInfo(); + method public boolean getIsActive(); + method public boolean getIsFactory(); + method public long getLastUpdateMillis(); + method public String getModuleName(); + method public String getModulePath(); + method public String getPreinstalledModulePath(); + method public long getVersionCode(); + method public String getVersionName(); + method public void setIsActive(boolean); + method public void setIsFactory(boolean); + method public void setLastUpdateMillis(long); + method public void setModuleName(String); + method public void setModulePath(String); + method public void setPreinstalledModulePath(String); + method public void setVersionCode(long); + method public void setVersionName(String); + } + + public class ApexInfoList { + ctor public ApexInfoList(); + method public java.util.List<com.android.apex.ApexInfo> getApexInfo(); + } + + public class XmlParser { + ctor public XmlParser(); + method public static com.android.apex.ApexInfo readApexInfo(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static com.android.apex.ApexInfoList readApexInfoList(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + } + + public class XmlWriter implements java.io.Closeable { + ctor public XmlWriter(java.io.PrintWriter); + method public void close(); + method public static void write(com.android.apex.XmlWriter, com.android.apex.ApexInfoList) throws java.io.IOException; + method public static void write(com.android.apex.XmlWriter, com.android.apex.ApexInfo) throws java.io.IOException; + } + +} + diff --git a/apexd/apex-info-list-api/last_current.txt b/apexd/apex-info-list-api/last_current.txt new file mode 100644 index 00000000..8db5a9a0 --- /dev/null +++ b/apexd/apex-info-list-api/last_current.txt @@ -0,0 +1,45 @@ +// Signature format: 2.0 +package com.android.apex { + + public class ApexInfo { + ctor public ApexInfo(); + method public boolean getIsActive(); + method public boolean getIsFactory(); + method public long getLastUpdateMillis(); + method public String getModuleName(); + method public String getModulePath(); + method public String getPreinstalledModulePath(); + method public long getVersionCode(); + method public String getVersionName(); + method public void setIsActive(boolean); + method public void setIsFactory(boolean); + method public void setLastUpdateMillis(long); + method public void setModuleName(String); + method public void setModulePath(String); + method public void setPreinstalledModulePath(String); + method public void setVersionCode(long); + method public void setVersionName(String); + } + + public class ApexInfoList { + ctor public ApexInfoList(); + method public java.util.List<com.android.apex.ApexInfo> getApexInfo(); + } + + public class XmlParser { + ctor public XmlParser(); + method public static com.android.apex.ApexInfo readApexInfo(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static com.android.apex.ApexInfoList readApexInfoList(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException; + } + + public class XmlWriter implements java.io.Closeable { + ctor public XmlWriter(java.io.PrintWriter); + method public void close(); + method public static void write(com.android.apex.XmlWriter, com.android.apex.ApexInfoList) throws java.io.IOException; + method public static void write(com.android.apex.XmlWriter, com.android.apex.ApexInfo) throws java.io.IOException; + } + +} + diff --git a/apexd/apex-info-list-api/last_removed.txt b/apexd/apex-info-list-api/last_removed.txt new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/apexd/apex-info-list-api/last_removed.txt diff --git a/apexd/apex-info-list-api/removed.txt b/apexd/apex-info-list-api/removed.txt new file mode 100644 index 00000000..d802177e --- /dev/null +++ b/apexd/apex-info-list-api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/apexd/apex_constants.h b/apexd/apex_constants.h index 4e989431..798afd5a 100644 --- a/apexd/apex_constants.h +++ b/apexd/apex_constants.h @@ -17,6 +17,7 @@ #pragma once #include <string> +#include <unordered_set> #include <vector> namespace android { @@ -26,6 +27,8 @@ static constexpr const char* kApexDataDir = "/data/apex"; static constexpr const char* kActiveApexPackagesDataDir = "/data/apex/active"; static constexpr const char* kApexBackupDir = "/data/apex/backup"; static constexpr const char* kApexHashTreeDir = "/data/apex/hashtree"; +static constexpr const char* kApexDecompressedDir = "/data/apex/decompressed"; +static constexpr const char* kOtaReservedDir = "/data/apex/ota_reserved"; static constexpr const char* kApexPackageSystemDir = "/system/apex"; static constexpr const char* kApexPackageSystemExtDir = "/system_ext/apex"; static constexpr const char* kApexPackageVendorDir = "/vendor/apex"; @@ -39,6 +42,7 @@ static constexpr const char* kApexRoot = "/apex"; static constexpr const char* kStagedSessionsDir = "/data/app-staging"; static constexpr const char* kApexDataSubDir = "apexdata"; +static constexpr const char* kApexSharedLibsSubDir = "sharedlibs"; static constexpr const char* kApexSnapshotSubDir = "apexrollback"; static constexpr const char* kPreRestoreSuffix = "-prerestore"; @@ -47,8 +51,26 @@ static constexpr const char* kDeNDataDir = "/data/misc_de"; static constexpr const char* kCeDataDir = "/data/misc_ce"; static constexpr const char* kApexPackageSuffix = ".apex"; +static constexpr const char* kCompressedApexPackageSuffix = ".capex"; +static constexpr const char* kDecompressedApexPackageSuffix = + ".decompressed.apex"; +static constexpr const char* kOtaApexPackageSuffix = ".ota.apex"; static constexpr const char* kManifestFilenameJson = "apex_manifest.json"; static constexpr const char* kManifestFilenamePb = "apex_manifest.pb"; + +static constexpr const char* kApexInfoList = "apex-info-list.xml"; + +// These should be in-sync with system/sepolicy/private/property_contexts +static constexpr const char* kApexStatusSysprop = "apexd.status"; +static constexpr const char* kApexStatusStarting = "starting"; +static constexpr const char* kApexStatusActivated = "activated"; +static constexpr const char* kApexStatusReady = "ready"; + +// Banned APEX names +static const std::unordered_set<std::string> kBannedApexName = { + kApexSharedLibsSubDir, // To avoid conflicts with predefined + // /apex/sharedlibs directory +}; } // namespace apex } // namespace android diff --git a/apexd/apex_database.cpp b/apexd/apex_database.cpp index d027f95c..7a1ee360 100644 --- a/apexd/apex_database.cpp +++ b/apexd/apex_database.cpp @@ -35,6 +35,9 @@ #include <utility> using android::base::ConsumeSuffix; +using android::base::EndsWith; +using android::base::ErrnoError; +using android::base::Error; using android::base::ParseInt; using android::base::ReadFileToString; using android::base::Result; @@ -76,12 +79,12 @@ class BlockDevice { fs::path DevPath() const { return kDevBlock / name; } Result<std::string> GetProperty(const std::string& property) const { - auto propertyFile = SysPath() / property; - std::string propertyValue; - if (!ReadFileToString(propertyFile, &propertyValue)) { + auto property_file = SysPath() / property; + std::string property_value; + if (!ReadFileToString(property_file, &property_value)) { return ErrnoError() << "Fail to read"; } - return Trim(propertyValue); + return Trim(property_value); } std::vector<BlockDevice> GetSlaves() const { @@ -100,17 +103,17 @@ class BlockDevice { } }; -std::pair<fs::path, fs::path> parseMountInfo(const std::string& mountInfo) { - const auto& tokens = Split(mountInfo, " "); +std::pair<fs::path, fs::path> ParseMountInfo(const std::string& mount_info) { + const auto& tokens = Split(mount_info, " "); if (tokens.size() < 2) { return std::make_pair("", ""); } return std::make_pair(tokens[0], tokens[1]); } -std::pair<std::string, int> parseMountPoint(const std::string& mountPoint) { - auto packageId = fs::path(mountPoint).filename(); - auto split = Split(packageId, "@"); +std::pair<std::string, int> ParseMountPoint(const std::string& mount_point) { + auto package_id = fs::path(mount_point).filename(); + auto split = Split(package_id, "@"); if (split.size() == 2) { int version; if (!ParseInt(split[1], &version)) { @@ -118,14 +121,17 @@ std::pair<std::string, int> parseMountPoint(const std::string& mountPoint) { } return std::make_pair(split[0], version); } - return std::make_pair(packageId, -1); + return std::make_pair(package_id, -1); } -bool isActiveMountPoint(const std::string& mountPoint) { - return (mountPoint.find('@') == std::string::npos); +bool IsActiveMountPoint(const std::string& mount_point) { + return (mount_point.find('@') == std::string::npos); } Result<void> PopulateLoopInfo(const BlockDevice& top_device, + const std::string& active_apex_dir, + const std::string& decompression_dir, + const std::string& apex_hash_tree_dir, MountedApexData* apex_data) { std::vector<BlockDevice> slaves = top_device.GetSlaves(); if (slaves.size() != 1 && slaves.size() != 2) { @@ -147,18 +153,22 @@ Result<void> PopulateLoopInfo(const BlockDevice& top_device, // Enforce following invariant: // * slaves[0] always represents a data loop device // * if size = 2 then slaves[1] represents an external hashtree loop device + auto is_data_loop_device = [&](const std::string& backing_file) { + return StartsWith(backing_file, active_apex_dir) || + StartsWith(backing_file, decompression_dir); + }; if (slaves.size() == 2) { - if (!StartsWith(backing_files[0], kActiveApexPackagesDataDir)) { + if (!is_data_loop_device(backing_files[0])) { std::swap(slaves[0], slaves[1]); std::swap(backing_files[0], backing_files[1]); } } - if (!StartsWith(backing_files[0], kActiveApexPackagesDataDir)) { + if (!is_data_loop_device(backing_files[0])) { return Error() << "Data loop device " << slaves[0].DevPath() << " has unexpected backing file " << backing_files[0]; } if (slaves.size() == 2) { - if (!StartsWith(backing_files[1], kApexHashTreeDir)) { + if (!StartsWith(backing_files[1], apex_hash_tree_dir)) { return Error() << "Hashtree loop device " << slaves[1].DevPath() << " has unexpected backing file " << backing_files[1]; } @@ -171,7 +181,7 @@ Result<void> PopulateLoopInfo(const BlockDevice& top_device, // This is not the right place to do this normalization, but proper solution // will require some refactoring first. :( -// TODO(ioffe): introduce MountedApexDataBuilder and delegate all +// TODO(b/158469911): introduce MountedApexDataBuilder and delegate all // building/normalization logic to it. void NormalizeIfDeleted(MountedApexData* apex_data) { std::string_view full_path = apex_data->full_path; @@ -188,18 +198,22 @@ void NormalizeIfDeleted(MountedApexData* apex_data) { apex_data->full_path = full_path; } -Result<MountedApexData> resolveMountInfo(const BlockDevice& block, - const std::string& mountPoint) { +Result<MountedApexData> ResolveMountInfo( + const BlockDevice& block, const std::string& mount_point, + const std::string& active_apex_dir, const std::string& decompression_dir, + const std::string& apex_hash_tree_dir) { + bool temp_mount = EndsWith(mount_point, ".tmp"); // Now, see if it is dm-verity or loop mounted switch (block.GetType()) { case LoopDevice: { - auto backingFile = block.GetProperty("loop/backing_file"); - if (!backingFile.ok()) { - return backingFile.error(); + auto backing_file = block.GetProperty("loop/backing_file"); + if (!backing_file.ok()) { + return backing_file.error(); } - auto result = MountedApexData(block.DevPath(), *backingFile, mountPoint, + auto result = MountedApexData(block.DevPath(), *backing_file, mount_point, /* device_name= */ "", - /* hashtree_loop_name= */ ""); + /* hashtree_loop_name= */ "", + /* is_temp_mount */ temp_mount); NormalizeIfDeleted(&result); return result; } @@ -209,9 +223,12 @@ Result<MountedApexData> resolveMountInfo(const BlockDevice& block, return name.error(); } MountedApexData result; - result.mount_point = mountPoint; + result.mount_point = mount_point; result.device_name = *name; - if (auto status = PopulateLoopInfo(block, &result); !status.ok()) { + result.is_temp_mount = temp_mount; + auto status = PopulateLoopInfo(block, active_apex_dir, decompression_dir, + apex_hash_tree_dir, &result); + if (!status.ok()) { return status.error(); } NormalizeIfDeleted(&result); @@ -246,40 +263,45 @@ Result<MountedApexData> resolveMountInfo(const BlockDevice& block, // By synchronizing the mounts info with Database on startup, // Apexd serves the correct package list even on the devices // which are not ro.apex.updatable. -void MountedApexDatabase::PopulateFromMounts() { +void MountedApexDatabase::PopulateFromMounts( + const std::string& active_apex_dir, const std::string& decompression_dir, + const std::string& apex_hash_tree_dir) REQUIRES(!mounted_apexes_mutex_) { LOG(INFO) << "Populating APEX database from mounts..."; - std::unordered_map<std::string, int> activeVersions; + std::unordered_map<std::string, int> active_versions; std::ifstream mounts("/proc/mounts"); std::string line; + std::lock_guard lock(mounted_apexes_mutex_); while (std::getline(mounts, line)) { - auto [block, mountPoint] = parseMountInfo(line); - // TODO(jooyung): ignore tmp mount? - if (fs::path(mountPoint).parent_path() != kApexRoot) { + auto [block, mount_point] = ParseMountInfo(line); + // TODO(b/158469914): distinguish between temp and non-temp mounts + if (fs::path(mount_point).parent_path() != kApexRoot) { continue; } - if (isActiveMountPoint(mountPoint)) { + if (IsActiveMountPoint(mount_point)) { continue; } - auto mountData = resolveMountInfo(BlockDevice(block), mountPoint); - if (!mountData.ok()) { - LOG(WARNING) << "Can't resolve mount info " << mountData.error(); + auto mount_data = + ResolveMountInfo(BlockDevice(block), mount_point, active_apex_dir, + decompression_dir, apex_hash_tree_dir); + if (!mount_data.ok()) { + LOG(WARNING) << "Can't resolve mount info " << mount_data.error(); continue; } - auto [package, version] = parseMountPoint(mountPoint); - AddMountedApex(package, false, *mountData); + auto [package, version] = ParseMountPoint(mount_point); + AddMountedApexLocked(package, false, *mount_data); - auto active = activeVersions[package] < version; + auto active = active_versions[package] < version; if (active) { - activeVersions[package] = version; - SetLatest(package, mountData->full_path); + active_versions[package] = version; + SetLatestLocked(package, mount_data->full_path); } - LOG(INFO) << "Found " << mountPoint << " backed by" - << (mountData->deleted ? " deleted " : " ") << "file " - << mountData->full_path; + LOG(INFO) << "Found " << mount_point << " backed by" + << (mount_data->deleted ? " deleted " : " ") << "file " + << mount_data->full_path; } LOG(INFO) << mounted_apexes_.size() << " packages restored."; diff --git a/apexd/apex_database.h b/apexd/apex_database.h index 1fb5bd3a..113b88ec 100644 --- a/apexd/apex_database.h +++ b/apexd/apex_database.h @@ -18,10 +18,14 @@ #define ANDROID_APEXD_APEX_DATABASE_H_ #include <map> +#include <mutex> +#include <optional> #include <string> #include <unordered_set> #include <android-base/logging.h> +#include <android-base/result.h> +#include <android-base/thread_annotations.h> namespace android { namespace apex { @@ -40,17 +44,21 @@ class MountedApexDatabase { std::string hashtree_loop_name; // Whenever apex file specified in full_path was deleted. bool deleted; + // Whether the mount is a temp mount or not. + bool is_temp_mount; MountedApexData() {} MountedApexData(const std::string& loop_name, const std::string& full_path, const std::string& mount_point, const std::string& device_name, - const std::string& hashtree_loop_name) + const std::string& hashtree_loop_name, + bool is_temp_mount = false) : loop_name(loop_name), full_path(full_path), mount_point(mount_point), device_name(device_name), - hashtree_loop_name(hashtree_loop_name) {} + hashtree_loop_name(hashtree_loop_name), + is_temp_mount(is_temp_mount) {} inline bool operator<(const MountedApexData& rhs) const { int compare_val = loop_name.compare(rhs.loop_name); @@ -81,42 +89,10 @@ class MountedApexDatabase { } }; - inline void CheckAtMostOneLatest() { - for (const auto& apex_set : mounted_apexes_) { - size_t count = 0; - for (const auto& pair : apex_set.second) { - if (pair.second) { - count++; - } - } - CHECK_LE(count, 1u) << apex_set.first; - } - } - - inline void CheckUniqueLoopDm() { - std::unordered_set<std::string> loop_devices; - std::unordered_set<std::string> dm_devices; - for (const auto& apex_set : mounted_apexes_) { - for (const auto& pair : apex_set.second) { - if (pair.first.loop_name != "") { - CHECK(loop_devices.insert(pair.first.loop_name).second) - << "Duplicate loop device: " << pair.first.loop_name; - } - if (pair.first.device_name != "") { - CHECK(dm_devices.insert(pair.first.device_name).second) - << "Duplicate dm device: " << pair.first.device_name; - } - if (pair.first.hashtree_loop_name != "") { - CHECK(loop_devices.insert(pair.first.hashtree_loop_name).second) - << "Duplicate loop device: " << pair.first.hashtree_loop_name; - } - } - } - } - template <typename... Args> - inline void AddMountedApex(const std::string& package, bool latest, - Args&&... args) { + inline void AddMountedApexLocked(const std::string& package, bool latest, + Args&&... args) + REQUIRES(mounted_apexes_mutex_) { auto it = mounted_apexes_.find(package); if (it == mounted_apexes_.end()) { auto insert_it = @@ -133,8 +109,18 @@ class MountedApexDatabase { CheckUniqueLoopDm(); } + template <typename... Args> + inline void AddMountedApex(const std::string& package, bool latest, + Args&&... args) REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); + AddMountedApexLocked(package, latest, args...); + } + inline void RemoveMountedApex(const std::string& package, - const std::string& full_path) { + const std::string& full_path, + bool match_temp_mounts = false) + REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); auto it = mounted_apexes_.find(package); if (it == mounted_apexes_.end()) { return; @@ -143,7 +129,8 @@ class MountedApexDatabase { auto& pkg_map = it->second; for (auto pkg_it = pkg_map.begin(); pkg_it != pkg_map.end(); ++pkg_it) { - if (pkg_it->first.full_path == full_path) { + if (pkg_it->first.full_path == full_path && + pkg_it->first.is_temp_mount == match_temp_mounts) { pkg_map.erase(pkg_it); return; } @@ -151,7 +138,15 @@ class MountedApexDatabase { } inline void SetLatest(const std::string& package, - const std::string& full_path) { + const std::string& full_path) + REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); + SetLatestLocked(package, full_path); + } + + inline void SetLatestLocked(const std::string& package, + const std::string& full_path) + REQUIRES(mounted_apexes_mutex_) { auto it = mounted_apexes_.find(package); CHECK(it != mounted_apexes_.end()); @@ -173,47 +168,108 @@ class MountedApexDatabase { LOG(FATAL) << "Did not find " << package << " " << full_path; } - inline void UnsetLatestForall(const std::string& package) { - auto it = mounted_apexes_.find(package); - if (it == mounted_apexes_.end()) { - return; - } - for (auto& data : it->second) { - data.second = false; - } - } - template <typename T> - inline void ForallMountedApexes(const std::string& package, - const T& handler) const { + inline void ForallMountedApexes(const std::string& package, const T& handler, + bool match_temp_mounts = false) const + REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); auto it = mounted_apexes_.find(package); if (it == mounted_apexes_.end()) { return; } for (auto& pair : it->second) { - handler(pair.first, pair.second); + if (pair.first.is_temp_mount == match_temp_mounts) { + handler(pair.first, pair.second); + } } } template <typename T> - inline void ForallMountedApexes(const T& handler) const { + inline void ForallMountedApexes(const T& handler, + bool match_temp_mounts = false) const + REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); for (const auto& pkg : mounted_apexes_) { for (const auto& pair : pkg.second) { - handler(pkg.first, pair.first, pair.second); + if (pair.first.is_temp_mount == match_temp_mounts) { + handler(pkg.first, pair.first, pair.second); + } } } } - void PopulateFromMounts(); + inline std::optional<MountedApexData> GetLatestMountedApex( + const std::string& package) REQUIRES(!mounted_apexes_mutex_) { + std::optional<MountedApexData> ret; + ForallMountedApexes(package, + [&ret](const MountedApexData& data, bool latest) { + if (latest) { + ret.emplace(data); + } + }); + return ret; + } + + void PopulateFromMounts(const std::string& active_apex_dir, + const std::string& decompression_dir, + const std::string& apex_hash_tree_dir); + + // Resets state of the database. Should only be used in testing. + inline void Reset() REQUIRES(!mounted_apexes_mutex_) { + std::lock_guard lock(mounted_apexes_mutex_); + mounted_apexes_.clear(); + } private: // A map from package name to mounted apexes. // Note: using std::maps to // a) so we do not have to worry about iterator invalidation. // b) do not have to const_cast (over std::set) - // TODO: Eventually this structure (and functions) need to be guarded by - // locks. - std::map<std::string, std::map<MountedApexData, bool>> mounted_apexes_; + // TODO(b/158467745): This structure (and functions) need to be guarded by + // locks. + std::map<std::string, std::map<MountedApexData, bool>> mounted_apexes_ + GUARDED_BY(mounted_apexes_mutex_); + + // To fix thread safety negative capability warning + class Mutex : public std::mutex { + public: + // for negative capabilities + const Mutex& operator!() const { return *this; } + }; + mutable Mutex mounted_apexes_mutex_; + + inline void CheckAtMostOneLatest() REQUIRES(mounted_apexes_mutex_) { + for (const auto& apex_set : mounted_apexes_) { + size_t count = 0; + for (const auto& pair : apex_set.second) { + if (pair.second) { + count++; + } + } + CHECK_LE(count, 1u) << apex_set.first; + } + } + + inline void CheckUniqueLoopDm() REQUIRES(mounted_apexes_mutex_) { + std::unordered_set<std::string> loop_devices; + std::unordered_set<std::string> dm_devices; + for (const auto& apex_set : mounted_apexes_) { + for (const auto& pair : apex_set.second) { + if (pair.first.loop_name != "") { + CHECK(loop_devices.insert(pair.first.loop_name).second) + << "Duplicate loop device: " << pair.first.loop_name; + } + if (pair.first.device_name != "") { + CHECK(dm_devices.insert(pair.first.device_name).second) + << "Duplicate dm device: " << pair.first.device_name; + } + if (pair.first.hashtree_loop_name != "") { + CHECK(loop_devices.insert(pair.first.hashtree_loop_name).second) + << "Duplicate loop device: " << pair.first.hashtree_loop_name; + } + } + } + } }; } // namespace apex diff --git a/apexd/apex_database_test.cpp b/apexd/apex_database_test.cpp index 522619fc..8c3a98d0 100644 --- a/apexd/apex_database_test.cpp +++ b/apexd/apex_database_test.cpp @@ -35,6 +35,7 @@ TEST(MountedApexDataTest, LinearOrder) { constexpr const char* kDm[] = {"dm1", "dm2", "dm3"}; constexpr const char* kHashtreeLoopName[] = {"hash-loop1", "hash-loop2", "hash-loop3"}; + // NOLINTNEXTLINE(bugprone-sizeof-expression) constexpr size_t kCount = arraysize(kLoopName) * arraysize(kPath) * arraysize(kMount) * arraysize(kDm); @@ -200,6 +201,37 @@ TEST(ApexDatabaseTest, MountMultiple) { kDeviceName[3], kHashtreeLoopName[3])); } +TEST(ApexDatabaseTest, GetLatestMountedApex) { + constexpr const char* kPackage = "package"; + constexpr const char* kLoopName = "loop"; + constexpr const char* kPath = "path"; + constexpr const char* kMountPoint = "mount"; + constexpr const char* kDeviceName = "dev"; + constexpr const char* kHashtreeLoopName = "hash-loop"; + + MountedApexDatabase db; + ASSERT_EQ(CountPackages(db), 0u); + + db.AddMountedApex(kPackage, true, kLoopName, kPath, kMountPoint, kDeviceName, + kHashtreeLoopName); + + auto ret = db.GetLatestMountedApex(kPackage); + MountedApexData expected(kLoopName, kPath, kMountPoint, kDeviceName, + kHashtreeLoopName); + ASSERT_TRUE(ret.has_value()); + ASSERT_EQ(ret->loop_name, std::string(kLoopName)); + ASSERT_EQ(ret->full_path, std::string(kPath)); + ASSERT_EQ(ret->mount_point, std::string(kMountPoint)); + ASSERT_EQ(ret->device_name, std::string(kDeviceName)); + ASSERT_EQ(ret->hashtree_loop_name, std::string(kHashtreeLoopName)); +} + +TEST(ApexDatabaseTest, GetLatestMountedApexReturnsNullopt) { + MountedApexDatabase db; + auto ret = db.GetLatestMountedApex("no-such-name"); + ASSERT_FALSE(ret.has_value()); +} + #pragma clang diagnostic push // error: 'ReturnSentinel' was marked unused but was used // [-Werror,-Wused-but-marked-unused] diff --git a/apexd/apex_file.cpp b/apexd/apex_file.cpp index f91b5d18..c043defa 100644 --- a/apexd/apex_file.cpp +++ b/apexd/apex_file.cpp @@ -23,61 +23,108 @@ #include <filesystem> #include <fstream> +#include <span> #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/scopeguard.h> #include <android-base/strings.h> #include <android-base/unique_fd.h> -#include <google/protobuf/util/message_differencer.h> #include <libavb/libavb.h> +#include <ziparchive/zip_archive.h> #include "apex_constants.h" -#include "apex_preinstalled_data.h" #include "apexd_utils.h" -#include "string_log.h" -using android::base::EndsWith; +using android::base::borrowed_fd; +using android::base::ErrnoError; using android::base::Error; using android::base::ReadFullyAtOffset; +using android::base::RemoveFileIfExists; using android::base::Result; -using android::base::StartsWith; using android::base::unique_fd; -using google::protobuf::util::MessageDifferencer; +using ::apex::proto::ApexManifest; namespace android { namespace apex { namespace { constexpr const char* kImageFilename = "apex_payload.img"; +constexpr const char* kCompressedApexFilename = "original_apex"; constexpr const char* kBundledPublicKeyFilename = "apex_pubkey"; +struct FsMagic { + const char* type; + int32_t offset; + int16_t len; + const char* magic; +}; +constexpr const FsMagic kFsType[] = {{"f2fs", 1024, 4, "\x10\x20\xf5\xf2"}, + {"ext4", 1024 + 0x38, 2, "\123\357"}}; + +Result<std::string> RetrieveFsType(borrowed_fd fd, int32_t image_offset) { + for (const auto& fs : kFsType) { + char buf[fs.len]; + if (!ReadFullyAtOffset(fd, buf, fs.len, image_offset + fs.offset)) { + return ErrnoError() << "Couldn't read filesystem magic"; + } + if (memcmp(buf, fs.magic, fs.len) == 0) { + return std::string(fs.type); + } + } + return Error() << "Couldn't find filesystem magic"; +} + } // namespace Result<ApexFile> ApexFile::Open(const std::string& path) { - int32_t image_offset; - size_t image_size; + std::optional<int32_t> image_offset; + std::optional<size_t> image_size; std::string manifest_content; std::string pubkey; + std::optional<std::string> fs_type; + ZipEntry entry; + + unique_fd fd(open(path.c_str(), O_RDONLY | O_BINARY | O_CLOEXEC)); + if (fd < 0) { + return Error() << "Failed to open package " << path << ": " + << "I/O error"; + } ZipArchiveHandle handle; auto handle_guard = android::base::make_scope_guard([&handle] { CloseArchive(handle); }); - int ret = OpenArchive(path.c_str(), &handle); + int ret = OpenArchiveFd(fd.get(), path.c_str(), &handle, false); if (ret < 0) { return Error() << "Failed to open package " << path << ": " << ErrorCodeString(ret); } - // Locate the mountable image within the zipfile and store offset and size. - ZipEntry entry; - ret = FindEntry(handle, kImageFilename, &entry); + bool is_compressed = true; + ret = FindEntry(handle, kCompressedApexFilename, &entry); if (ret < 0) { - return Error() << "Could not find entry \"" << kImageFilename - << "\" in package " << path << ": " << ErrorCodeString(ret); + is_compressed = false; + } + + if (!is_compressed) { + // Locate the mountable image within the zipfile and store offset and size. + ret = FindEntry(handle, kImageFilename, &entry); + if (ret < 0) { + return Error() << "Could not find entry \"" << kImageFilename + << "\" or \"" << kCompressedApexFilename + << "\" in package " << path << ": " + << ErrorCodeString(ret); + } + image_offset = entry.offset; + image_size = entry.uncompressed_length; + + auto fs_type_result = RetrieveFsType(fd, image_offset.value()); + if (!fs_type_result.ok()) { + return Error() << "Failed to retrieve filesystem type for " << path + << ": " << fs_type_result.error(); + } + fs_type = std::move(*fs_type_result); } - image_offset = entry.offset; - image_size = entry.uncompressed_length; ret = FindEntry(handle, kManifestFilenamePb, &entry); if (ret < 0) { @@ -112,8 +159,20 @@ Result<ApexFile> ApexFile::Open(const std::string& path) { return manifest.error(); } - return ApexFile(path, image_offset, image_size, std::move(*manifest), pubkey, - isPathForBuiltinApexes(path)); + if (is_compressed && manifest->providesharedapexlibs()) { + return Error() << "Apex providing sharedlibs shouldn't be compressed"; + } + + // b/179211712 the stored path should be the realpath, otherwise the path we + // get by scanning the directory would be different from the path we get + // by reading /proc/mounts, if the apex file is on a symlink dir. + std::string realpath; + if (!android::base::Realpath(path, &realpath)) { + return ErrnoError() << "can't get realpath of " << path; + } + + return ApexFile(realpath, image_offset, image_size, std::move(*manifest), + pubkey, fs_type, is_compressed); } // AVB-related code. @@ -122,7 +181,7 @@ namespace { static constexpr int kVbMetaMaxSize = 64 * 1024; -std::string bytes_to_hex(const uint8_t* bytes, size_t bytes_len) { +std::string BytesToHex(const uint8_t* bytes, size_t bytes_len) { std::ostringstream s; s << std::hex << std::setfill('0'); @@ -132,28 +191,32 @@ std::string bytes_to_hex(const uint8_t* bytes, size_t bytes_len) { return s.str(); } -std::string getSalt(const AvbHashtreeDescriptor& desc, - const uint8_t* trailingData) { - const uint8_t* desc_salt = trailingData + desc.partition_name_len; +std::string GetSalt(const AvbHashtreeDescriptor& desc, + const uint8_t* trailing_data) { + const uint8_t* desc_salt = trailing_data + desc.partition_name_len; - return bytes_to_hex(desc_salt, desc.salt_len); + return BytesToHex(desc_salt, desc.salt_len); } -std::string getDigest(const AvbHashtreeDescriptor& desc, - const uint8_t* trailingData) { +std::string GetDigest(const AvbHashtreeDescriptor& desc, + const uint8_t* trailing_data) { const uint8_t* desc_digest = - trailingData + desc.partition_name_len + desc.salt_len; + trailing_data + desc.partition_name_len + desc.salt_len; - return bytes_to_hex(desc_digest, desc.root_digest_len); + return BytesToHex(desc_digest, desc.root_digest_len); } -Result<std::unique_ptr<AvbFooter>> getAvbFooter(const ApexFile& apex, +Result<std::unique_ptr<AvbFooter>> GetAvbFooter(const ApexFile& apex, const unique_fd& fd) { std::array<uint8_t, AVB_FOOTER_SIZE> footer_data; auto footer = std::make_unique<AvbFooter>(); // The AVB footer is located in the last part of the image - off_t offset = apex.GetImageSize() + apex.GetImageOffset() - AVB_FOOTER_SIZE; + if (!apex.GetImageOffset() || !apex.GetImageSize()) { + return Error() << "Cannot check avb footer without image offset and size"; + } + off_t offset = apex.GetImageSize().value() + apex.GetImageOffset().value() - + AVB_FOOTER_SIZE; int ret = lseek(fd, offset, SEEK_SET); if (ret == -1) { return ErrnoError() << "Couldn't seek to AVB footer"; @@ -179,8 +242,10 @@ bool CompareKeys(const uint8_t* key, size_t length, memcmp(&public_key_content[0], key, length) == 0; } -Result<void> verifyVbMetaSignature(const ApexFile& apex, const uint8_t* data, - size_t length) { +// Verifies correctness of vbmeta and returns public key it was signed with. +Result<std::span<const uint8_t>> VerifyVbMetaSignature(const ApexFile& apex, + const uint8_t* data, + size_t length) { const uint8_t* pk; size_t pk_len; AvbVBMetaVerifyResult res; @@ -201,49 +266,46 @@ Result<void> verifyVbMetaSignature(const ApexFile& apex, const uint8_t* data, return Error() << "Error verifying " << apex.GetPath() << ": " << "unsupported version"; default: - return Errorf("Unknown vmbeta_image_verify return value"); + return Error() << "Unknown vmbeta_image_verify return value : " << res; } - Result<const std::string> public_key = getApexKey(apex.GetManifest().name()); - if (public_key.ok()) { - // TODO(b/115718846) - // We need to decide whether we need rollback protection, and whether - // we can use the rollback protection provided by libavb. - if (!CompareKeys(pk, pk_len, *public_key)) { - return Error() << "Error verifying " << apex.GetPath() << ": " - << "public key doesn't match the pre-installed one"; - } - } else { - return public_key.error(); - } - LOG(VERBOSE) << apex.GetPath() << ": public key matches."; - return {}; + return std::span<const uint8_t>(pk, pk_len); } -Result<std::unique_ptr<uint8_t[]>> verifyVbMeta(const ApexFile& apex, +Result<std::unique_ptr<uint8_t[]>> VerifyVbMeta(const ApexFile& apex, const unique_fd& fd, - const AvbFooter& footer) { + const AvbFooter& footer, + const std::string& public_key) { if (footer.vbmeta_size > kVbMetaMaxSize) { return Errorf("VbMeta size in footer exceeds kVbMetaMaxSize."); } - off_t offset = apex.GetImageOffset() + footer.vbmeta_offset; + if (!apex.GetImageOffset()) { + return Error() << "Cannot check VbMeta size without image offset"; + } + + off_t offset = apex.GetImageOffset().value() + footer.vbmeta_offset; std::unique_ptr<uint8_t[]> vbmeta_buf(new uint8_t[footer.vbmeta_size]); if (!ReadFullyAtOffset(fd, vbmeta_buf.get(), footer.vbmeta_size, offset)) { return ErrnoError() << "Couldn't read AVB meta-data"; } - Result<void> st = - verifyVbMetaSignature(apex, vbmeta_buf.get(), footer.vbmeta_size); + Result<std::span<const uint8_t>> st = + VerifyVbMetaSignature(apex, vbmeta_buf.get(), footer.vbmeta_size); if (!st.ok()) { return st.error(); } + if (!CompareKeys(st->data(), st->size(), public_key)) { + return Error() << "Error verifying " << apex.GetPath() << " : " + << "public key doesn't match the pre-installed one"; + } + return vbmeta_buf; } -Result<const AvbHashtreeDescriptor*> findDescriptor(uint8_t* vbmeta_data, +Result<const AvbHashtreeDescriptor*> FindDescriptor(uint8_t* vbmeta_data, size_t vbmeta_size) { const AvbDescriptor** descriptors; size_t num_descriptors; @@ -277,118 +339,127 @@ Result<const AvbHashtreeDescriptor*> findDescriptor(uint8_t* vbmeta_data, return Errorf("Couldn't find any AVB hashtree descriptors."); } -Result<std::unique_ptr<AvbHashtreeDescriptor>> verifyDescriptor( +Result<std::unique_ptr<AvbHashtreeDescriptor>> VerifyDescriptor( const AvbHashtreeDescriptor* desc) { - auto verifiedDesc = std::make_unique<AvbHashtreeDescriptor>(); + auto verified_desc = std::make_unique<AvbHashtreeDescriptor>(); if (!avb_hashtree_descriptor_validate_and_byteswap(desc, - verifiedDesc.get())) { + verified_desc.get())) { return Errorf("Couldn't validate AvbDescriptor."); } - return verifiedDesc; + return verified_desc; } } // namespace -Result<ApexVerityData> ApexFile::VerifyApexVerity() const { - ApexVerityData verityData; +Result<ApexVerityData> ApexFile::VerifyApexVerity( + const std::string& public_key) const { + if (IsCompressed()) { + return Error() << "Cannot verify ApexVerity of compressed APEX"; + } + + ApexVerityData verity_data; unique_fd fd(open(GetPath().c_str(), O_RDONLY | O_CLOEXEC)); if (fd.get() == -1) { return ErrnoError() << "Failed to open " << GetPath(); } - Result<std::unique_ptr<AvbFooter>> footer = getAvbFooter(*this, fd); + Result<std::unique_ptr<AvbFooter>> footer = GetAvbFooter(*this, fd); if (!footer.ok()) { return footer.error(); } Result<std::unique_ptr<uint8_t[]>> vbmeta_data = - verifyVbMeta(*this, fd, **footer); + VerifyVbMeta(*this, fd, **footer, public_key); if (!vbmeta_data.ok()) { return vbmeta_data.error(); } Result<const AvbHashtreeDescriptor*> descriptor = - findDescriptor(vbmeta_data->get(), (*footer)->vbmeta_size); + FindDescriptor(vbmeta_data->get(), (*footer)->vbmeta_size); if (!descriptor.ok()) { return descriptor.error(); } - Result<std::unique_ptr<AvbHashtreeDescriptor>> verifiedDescriptor = - verifyDescriptor(*descriptor); - if (!verifiedDescriptor.ok()) { - return verifiedDescriptor.error(); + Result<std::unique_ptr<AvbHashtreeDescriptor>> verified_descriptor = + VerifyDescriptor(*descriptor); + if (!verified_descriptor.ok()) { + return verified_descriptor.error(); } - verityData.desc = std::move(*verifiedDescriptor); + verity_data.desc = std::move(*verified_descriptor); // This area is now safe to access, because we just verified it - const uint8_t* trailingData = + const uint8_t* trailing_data = (const uint8_t*)*descriptor + sizeof(AvbHashtreeDescriptor); - verityData.hash_algorithm = + verity_data.hash_algorithm = reinterpret_cast<const char*>((*descriptor)->hash_algorithm); - verityData.salt = getSalt(*verityData.desc, trailingData); - verityData.root_digest = getDigest(*verityData.desc, trailingData); + verity_data.salt = GetSalt(*verity_data.desc, trailing_data); + verity_data.root_digest = GetDigest(*verity_data.desc, trailing_data); - return verityData; + return verity_data; } -Result<void> ApexFile::VerifyManifestMatches( - const std::string& mount_path) const { - Result<ApexManifest> verifiedManifest = - ReadManifest(mount_path + "/" + kManifestFilenamePb); - if (!verifiedManifest.ok()) { - return verifiedManifest.error(); - } +Result<void> ApexFile::Decompress(const std::string& dest_path) const { + const std::string& src_path = GetPath(); + + LOG(INFO) << "Decompressing" << src_path << " to " << dest_path; - if (!MessageDifferencer::Equals(manifest_, *verifiedManifest)) { - return Errorf( - "Manifest inside filesystem does not match manifest outside it"); + // We should decompress compressed APEX files only + if (!IsCompressed()) { + return ErrnoError() << "Cannot decompress an uncompressed APEX"; } - return {}; -} + // Get file descriptor of the compressed apex file + unique_fd src_fd(open(src_path.c_str(), O_RDONLY | O_CLOEXEC)); + if (src_fd.get() == -1) { + return ErrnoError() << "Failed to open compressed APEX " << GetPath(); + } -Result<std::vector<std::string>> FindApexes( - const std::vector<std::string>& paths) { - std::vector<std::string> result; - for (const auto& path : paths) { - auto exist = PathExists(path); - if (!exist.ok()) { - return exist.error(); - } - if (!*exist) continue; + // Open it as a zip file + ZipArchiveHandle handle; + int ret = OpenArchiveFd(src_fd.get(), src_path.c_str(), &handle, false); + if (ret < 0) { + return Error() << "Failed to open package " << src_path << ": " + << ErrorCodeString(ret); + } + auto handle_guard = + android::base::make_scope_guard([&handle] { CloseArchive(handle); }); - const auto& apexes = FindApexFilesByName(path); - if (!apexes.ok()) { - return apexes; - } + // Find the original apex file inside the zip and extract to dest + ZipEntry entry; + ret = FindEntry(handle, kCompressedApexFilename, &entry); + if (ret < 0) { + return Error() << "Could not find entry \"" << kCompressedApexFilename + << "\" in package " << src_path << ": " + << ErrorCodeString(ret); + } - result.insert(result.end(), apexes->begin(), apexes->end()); + // Open destination file descriptor + unique_fd dest_fd( + open(dest_path.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT | O_EXCL, 0644)); + if (dest_fd.get() == -1) { + return ErrnoError() << "Failed to open decompression destination " + << dest_path.c_str(); } - return result; -} -Result<std::vector<std::string>> FindApexFilesByName(const std::string& path) { - auto filter_fn = [](const std::filesystem::directory_entry& entry) { - std::error_code ec; - if (entry.is_regular_file(ec) && - EndsWith(entry.path().filename().string(), kApexPackageSuffix)) { - return true; // APEX file, take. - } - return false; - }; - return ReadDir(path, filter_fn); -} + // Prepare a guard that deletes the extracted file if anything goes wrong + auto decompressed_guard = android::base::make_scope_guard( + [&dest_path] { RemoveFileIfExists(dest_path); }); -bool isPathForBuiltinApexes(const std::string& path) { - for (const auto& dir : kApexPackageBuiltinDirs) { - if (StartsWith(path, dir)) { - return true; - } + // Extract the original_apex to dest_path + ret = ExtractEntryToFile(handle, &entry, dest_fd.get()); + if (ret < 0) { + return Error() << "Could not decompress to file " << dest_path << " " + << ErrorCodeString(ret); } - return false; + + // Verification complete. Accept the decompressed file + decompressed_guard.Disable(); + LOG(VERBOSE) << "Decompressed " << src_path << " to " << dest_path; + + return {}; } } // namespace apex diff --git a/apexd/apex_file.h b/apexd/apex_file.h index 0476911c..60d5feb2 100644 --- a/apexd/apex_file.h +++ b/apexd/apex_file.h @@ -23,9 +23,7 @@ #include <android-base/result.h> #include <libavb/libavb.h> -#include <ziparchive/zip_archive.h> -#include "apex_constants.h" #include "apex_manifest.h" namespace android { @@ -46,43 +44,42 @@ class ApexFile { static android::base::Result<ApexFile> Open(const std::string& path); ApexFile() = delete; ApexFile(ApexFile&&) = default; + ApexFile& operator=(ApexFile&&) = default; const std::string& GetPath() const { return apex_path_; } - int32_t GetImageOffset() const { return image_offset_; } - size_t GetImageSize() const { return image_size_; } - const ApexManifest& GetManifest() const { return manifest_; } + const std::optional<int32_t>& GetImageOffset() const { return image_offset_; } + const std::optional<size_t>& GetImageSize() const { return image_size_; } + const ::apex::proto::ApexManifest& GetManifest() const { return manifest_; } const std::string& GetBundledPublicKey() const { return apex_pubkey_; } - bool IsBuiltin() const { return is_builtin_; } - android::base::Result<ApexVerityData> VerifyApexVerity() const; - android::base::Result<void> VerifyManifestMatches( - const std::string& mount_path) const; + const std::optional<std::string>& GetFsType() const { return fs_type_; } + android::base::Result<ApexVerityData> VerifyApexVerity( + const std::string& public_key) const; + bool IsCompressed() const { return is_compressed_; } + android::base::Result<void> Decompress(const std::string& output_path) const; private: - ApexFile(const std::string& apex_path, int32_t image_offset, - size_t image_size, ApexManifest manifest, - const std::string& apex_pubkey, bool is_builtin) + ApexFile(const std::string& apex_path, + const std::optional<int32_t>& image_offset, + const std::optional<size_t>& image_size, + ::apex::proto::ApexManifest manifest, const std::string& apex_pubkey, + const std::optional<std::string>& fs_type, bool is_compressed) : apex_path_(apex_path), image_offset_(image_offset), image_size_(image_size), manifest_(std::move(manifest)), apex_pubkey_(apex_pubkey), - is_builtin_(is_builtin) {} + fs_type_(fs_type), + is_compressed_(is_compressed) {} std::string apex_path_; - int32_t image_offset_; - size_t image_size_; - ApexManifest manifest_; + std::optional<int32_t> image_offset_; + std::optional<size_t> image_size_; + ::apex::proto::ApexManifest manifest_; std::string apex_pubkey_; - bool is_builtin_; + std::optional<std::string> fs_type_; + bool is_compressed_; }; -android::base::Result<std::vector<std::string>> FindApexes( - const std::vector<std::string>& paths); -android::base::Result<std::vector<std::string>> FindApexFilesByName( - const std::string& path); - -bool isPathForBuiltinApexes(const std::string& path); - } // namespace apex } // namespace android diff --git a/apexd/apex_file_repository.cpp b/apexd/apex_file_repository.cpp new file mode 100644 index 00000000..c50d136f --- /dev/null +++ b/apexd/apex_file_repository.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2019 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. + */ + +#define LOG_TAG "apexd" + +#include "apex_file_repository.h" + +#include <unordered_map> + +#include <android-base/file.h> +#include <android-base/properties.h> +#include <android-base/result.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> + +#include "apex_constants.h" +#include "apex_file.h" +#include "apexd_utils.h" + +using android::base::EndsWith; +using android::base::Error; +using android::base::GetProperty; +using android::base::Result; + +namespace android { +namespace apex { + +Result<void> ApexFileRepository::ScanBuiltInDir(const std::string& dir) { + LOG(INFO) << "Scanning " << dir << " for pre-installed ApexFiles"; + if (access(dir.c_str(), F_OK) != 0 && errno == ENOENT) { + LOG(WARNING) << dir << " does not exist. Skipping"; + return {}; + } + + Result<std::vector<std::string>> all_apex_files = FindFilesBySuffix( + dir, {kApexPackageSuffix, kCompressedApexPackageSuffix}); + if (!all_apex_files.ok()) { + return all_apex_files.error(); + } + + // TODO(b/179248390): scan parallelly if possible + for (const auto& file : *all_apex_files) { + LOG(INFO) << "Found pre-installed APEX " << file; + Result<ApexFile> apex_file = ApexFile::Open(file); + if (!apex_file.ok()) { + return Error() << "Failed to open " << file << " : " << apex_file.error(); + } + + const std::string& name = apex_file->GetManifest().name(); + auto it = pre_installed_store_.find(name); + if (it == pre_installed_store_.end()) { + pre_installed_store_.emplace(name, std::move(*apex_file)); + } else if (it->second.GetPath() != apex_file->GetPath()) { + auto level = base::FATAL; + // On some development (non-REL) builds the VNDK apex could be in /vendor. + // When testing CTS-on-GSI on these builds, there would be two VNDK apexes + // in the system, one in /system and one in /vendor. + static constexpr char kVndkApexModuleNamePrefix[] = "com.android.vndk."; + static constexpr char kPlatformVersionCodenameProperty[] = + "ro.build.version.codename"; + if (android::base::StartsWith(name, kVndkApexModuleNamePrefix) && + GetProperty(kPlatformVersionCodenameProperty, "REL") != "REL") { + level = android::base::INFO; + } + LOG(level) << "Found two apex packages " << it->second.GetPath() + << " and " << apex_file->GetPath() + << " with the same module name " << name; + } else if (it->second.GetBundledPublicKey() != + apex_file->GetBundledPublicKey()) { + LOG(FATAL) << "Public key of apex package " << it->second.GetPath() + << " (" << name << ") has unexpectedly changed"; + } + } + return {}; +} + +ApexFileRepository& ApexFileRepository::GetInstance() { + static ApexFileRepository instance; + return instance; +} + +android::base::Result<void> ApexFileRepository::AddPreInstalledApex( + const std::vector<std::string>& prebuilt_dirs) { + for (const auto& dir : prebuilt_dirs) { + if (auto result = ScanBuiltInDir(dir); !result.ok()) { + return result.error(); + } + } + return {}; +} + +// TODO(b/179497746): AddDataApex should not concern with filtering out invalid +// apex. +Result<void> ApexFileRepository::AddDataApex(const std::string& data_dir) { + LOG(INFO) << "Scanning " << data_dir << " for data ApexFiles"; + if (access(data_dir.c_str(), F_OK) != 0 && errno == ENOENT) { + LOG(WARNING) << data_dir << " does not exist. Skipping"; + return {}; + } + + Result<std::vector<std::string>> active_apex = + FindFilesBySuffix(data_dir, {kApexPackageSuffix}); + if (!active_apex.ok()) { + return active_apex.error(); + } + + // TODO(b/179248390): scan parallelly if possible + for (const auto& file : *active_apex) { + LOG(INFO) << "Found updated apex " << file; + Result<ApexFile> apex_file = ApexFile::Open(file); + if (!apex_file.ok()) { + LOG(ERROR) << "Failed to open " << file << " : " << apex_file.error(); + continue; + } + + const std::string& name = apex_file->GetManifest().name(); + if (!HasPreInstalledVersion(name)) { + LOG(ERROR) << "Skipping " << file << " : no preisntalled apex"; + // Ignore data apex without corresponding pre-installed apex + continue; + } + auto pre_installed_public_key = GetPublicKey(name); + if (!pre_installed_public_key.ok() || + apex_file->GetBundledPublicKey() != *pre_installed_public_key) { + // Ignore data apex if public key doesn't match with pre-installed apex + LOG(ERROR) << "Skipping " << file + << " : public key doesn't match pre-installed one"; + continue; + } + + if (EndsWith(apex_file->GetPath(), kDecompressedApexPackageSuffix)) { + LOG(WARNING) << "Skipping " << file + << " : Non-decompressed APEX should not have " + << kDecompressedApexPackageSuffix << " suffix"; + continue; + } + + auto it = data_store_.find(name); + if (it == data_store_.end()) { + data_store_.emplace(name, std::move(*apex_file)); + continue; + } + + const auto& existing_version = it->second.GetManifest().version(); + const auto new_version = apex_file->GetManifest().version(); + // If multiple data apexs are preset, select the one with highest version + bool prioritize_higher_version = new_version > existing_version; + // For same version, non-decompressed apex gets priority + if (prioritize_higher_version) { + it->second = std::move(*apex_file); + } + } + return {}; +} + +// TODO(b/179497746): remove this method when we add api for fetching ApexFile +// by name +Result<const std::string> ApexFileRepository::GetPublicKey( + const std::string& name) const { + auto it = pre_installed_store_.find(name); + if (it == pre_installed_store_.end()) { + return Error() << "No preinstalled apex found for package " << name; + } + return it->second.GetBundledPublicKey(); +} + +// TODO(b/179497746): remove this method when we add api for fetching ApexFile +// by name +Result<const std::string> ApexFileRepository::GetPreinstalledPath( + const std::string& name) const { + auto it = pre_installed_store_.find(name); + if (it == pre_installed_store_.end()) { + return Error() << "No preinstalled data found for package " << name; + } + return it->second.GetPath(); +} + +// TODO(b/179497746): remove this method when we add api for fetching ApexFile +// by name +Result<const std::string> ApexFileRepository::GetDataPath( + const std::string& name) const { + auto it = data_store_.find(name); + if (it == data_store_.end()) { + return Error() << "No data apex found for package " << name; + } + return it->second.GetPath(); +} + +bool ApexFileRepository::HasPreInstalledVersion(const std::string& name) const { + return pre_installed_store_.find(name) != pre_installed_store_.end(); +} + +bool ApexFileRepository::HasDataVersion(const std::string& name) const { + return data_store_.find(name) != data_store_.end(); +} + +// ApexFile is considered a decompressed APEX if it is located in decompression +// dir +bool ApexFileRepository::IsDecompressedApex(const ApexFile& apex) const { + return apex.GetPath().starts_with(decompression_dir_); +} + +bool ApexFileRepository::IsPreInstalledApex(const ApexFile& apex) const { + auto it = pre_installed_store_.find(apex.GetManifest().name()); + if (it == pre_installed_store_.end()) { + return false; + } + return it->second.GetPath() == apex.GetPath() || IsDecompressedApex(apex); +} + +std::vector<ApexFileRef> ApexFileRepository::GetPreInstalledApexFiles() const { + std::vector<ApexFileRef> result; + for (const auto& it : pre_installed_store_) { + result.emplace_back(std::cref(it.second)); + } + return std::move(result); +} + +std::vector<ApexFileRef> ApexFileRepository::GetDataApexFiles() const { + std::vector<ApexFileRef> result; + for (const auto& it : data_store_) { + result.emplace_back(std::cref(it.second)); + } + return std::move(result); +} + +// Group pre-installed APEX and data APEX by name +std::unordered_map<std::string, std::vector<ApexFileRef>> +ApexFileRepository::AllApexFilesByName() const { + // Collect all apex files + std::vector<ApexFileRef> all_apex_files; + auto pre_installed_apexs = GetPreInstalledApexFiles(); + auto data_apexs = GetDataApexFiles(); + std::move(pre_installed_apexs.begin(), pre_installed_apexs.end(), + std::back_inserter(all_apex_files)); + std::move(data_apexs.begin(), data_apexs.end(), + std::back_inserter(all_apex_files)); + + // Group them by name + std::unordered_map<std::string, std::vector<ApexFileRef>> result; + for (const auto& apex_file_ref : all_apex_files) { + const ApexFile& apex_file = apex_file_ref.get(); + const std::string& package_name = apex_file.GetManifest().name(); + if (result.find(package_name) == result.end()) { + result[package_name] = std::vector<ApexFileRef>{}; + } + result[package_name].emplace_back(apex_file_ref); + } + + return std::move(result); +} + +ApexFileRef ApexFileRepository::GetDataApex(const std::string& name) const { + auto it = data_store_.find(name); + CHECK(it != data_store_.end()); + return std::cref(it->second); +} + +ApexFileRef ApexFileRepository::GetPreInstalledApex( + const std::string& name) const { + auto it = pre_installed_store_.find(name); + CHECK(it != pre_installed_store_.end()); + return std::cref(it->second); +} + +} // namespace apex +} // namespace android diff --git a/apexd/apex_file_repository.h b/apexd/apex_file_repository.h new file mode 100644 index 00000000..e9ccf7ee --- /dev/null +++ b/apexd/apex_file_repository.h @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2019 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. + */ + +#pragma once + +#include <functional> +#include <string> +#include <unordered_map> +#include <vector> +#include "apex_constants.h" +#include "apex_file.h" + +#include <android-base/result.h> + +namespace android { +namespace apex { + +using ApexFileRef = std::reference_wrapper<const android::apex::ApexFile>; + +// This class serves as a ApexFile repository for all apexes on device. It also +// provides information about the ApexFiles it hosts, such as which are +// pre-installed and which are data. Such information can be used, for example, +// to verify validity of an apex before trying to mount it. +// +// It's expected to have a single instance of this class in a process that +// mounts apexes (e.g. apexd, otapreopt_chroot). +class ApexFileRepository final { + public: + // c-tor and d-tor are exposed for testing. + explicit ApexFileRepository( + const std::string& decompression_dir = kApexDecompressedDir) + : decompression_dir_(decompression_dir){}; + + ~ApexFileRepository() { + pre_installed_store_.clear(); + data_store_.clear(); + }; + + // Returns a singletone instance of this class. + static ApexFileRepository& GetInstance(); + + // Populate instance by collecting pre-installed apex files from the given + // |prebuilt_dirs|. + // Note: this call is **not thread safe** and is expected to be performed in a + // single thread during initialization of apexd. After initialization is + // finished, all queries to the instance are thread safe. + android::base::Result<void> AddPreInstalledApex( + const std::vector<std::string>& prebuilt_dirs); + + // Populate instance by collecting data apex files from the given |data_dir|. + // Note: this call is **not thread safe** and is expected to be performed in a + // single thread during initialization of apexd. After initialization is + // finished, all queries to the instance are thread safe. + android::base::Result<void> AddDataApex(const std::string& data_dir); + + // Returns trusted public key for an apex with the given |name|. + android::base::Result<const std::string> GetPublicKey( + const std::string& name) const; + + // Returns path to the pre-installed version of an apex with the given |name|. + android::base::Result<const std::string> GetPreinstalledPath( + const std::string& name) const; + + // Returns path to the data version of an apex with the given |name|. + android::base::Result<const std::string> GetDataPath( + const std::string& name) const; + + // Checks whether there is a pre-installed version of an apex with the given + // |name|. + bool HasPreInstalledVersion(const std::string& name) const; + + // Checks whether there is a data version of an apex with the given |name|. + bool HasDataVersion(const std::string& name) const; + + // Checks if given |apex| is pre-installed. + bool IsPreInstalledApex(const ApexFile& apex) const; + + // Checks if given |apex| is decompressed from a pre-installed APEX + bool IsDecompressedApex(const ApexFile& apex) const; + + // Returns reference to all pre-installed APEX on device + std::vector<ApexFileRef> GetPreInstalledApexFiles() const; + + // Returns reference to all data APEX on device + std::vector<ApexFileRef> GetDataApexFiles() const; + + // Group all ApexFiles on device by their package name + std::unordered_map<std::string, std::vector<ApexFileRef>> AllApexFilesByName() + const; + + // Returns a pre-installed version of apex with the given name. Caller is + // expected to check if there is a pre-installed apex with the given name + // using |HasPreinstalledVersion| function. + ApexFileRef GetPreInstalledApex(const std::string& name) const; + // Returns a data version of apex with the given name. Caller is + // expected to check if there is a data apex with the given name + // using |HasDataVersion| function. + ApexFileRef GetDataApex(const std::string& name) const; + + // Clears ApexFileRepostiry. + // Only use in tests. + void Reset(const std::string& decompression_dir = kApexDecompressedDir) { + pre_installed_store_.clear(); + data_store_.clear(); + decompression_dir_ = decompression_dir; + } + + private: + // Non-copyable && non-moveable. + ApexFileRepository(const ApexFileRepository&) = delete; + ApexFileRepository& operator=(const ApexFileRepository&) = delete; + ApexFileRepository& operator=(ApexFileRepository&&) = delete; + ApexFileRepository(ApexFileRepository&&) = delete; + + // Scans apexes in the given directory and adds collected data into + // |pre_installed_store_|. + android::base::Result<void> ScanBuiltInDir(const std::string& dir); + + std::unordered_map<std::string, ApexFile> pre_installed_store_, data_store_; + // Decompression directory which will be used to determine if apex is + // decompressed or not + std::string decompression_dir_; +}; + +} // namespace apex +} // namespace android diff --git a/apexd/apex_file_repository_test.cpp b/apexd/apex_file_repository_test.cpp new file mode 100644 index 00000000..c1f5d533 --- /dev/null +++ b/apexd/apex_file_repository_test.cpp @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2020 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 <filesystem> +#include <string> + +#include <errno.h> +#include <sys/stat.h> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "apex_file.h" +#include "apex_file_repository.h" +#include "apexd_test_utils.h" +#include "apexd_verity.h" + +namespace android { +namespace apex { + +using namespace std::literals; + +namespace fs = std::filesystem; + +using android::apex::testing::ApexFileEq; +using android::apex::testing::IsOk; +using android::base::GetExecutableDirectory; +using android::base::StringPrintf; +using ::testing::ByRef; +using ::testing::UnorderedElementsAre; + +static std::string GetTestDataDir() { return GetExecutableDirectory(); } +static std::string GetTestFile(const std::string& name) { + return GetTestDataDir() + "/" + name; +} + +namespace { +// Copies the compressed apex to |built_in_dir| and decompresses it to +// |decompression_dir +void PrepareCompressedApex(const std::string& name, + const std::string& built_in_dir, + const std::string& decompression_dir) { + fs::copy(GetTestFile(name), built_in_dir); + auto compressed_apex = + ApexFile::Open(StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str())); + + const auto& pkg_name = compressed_apex->GetManifest().name(); + const int version = compressed_apex->GetManifest().version(); + + auto decompression_path = + StringPrintf("%s/%s@%d%s", decompression_dir.c_str(), pkg_name.c_str(), + version, kDecompressedApexPackageSuffix); + compressed_apex->Decompress(decompression_path); +} +} // namespace + +TEST(ApexFileRepositoryTest, InitializeSuccess) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("apex.apexd_test_different_app.apex"), + built_in_dir.path); + + fs::copy(GetTestFile("apex.apexd_test.apex"), data_dir.path); + fs::copy(GetTestFile("apex.apexd_test_different_app.apex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + // Now test that apexes were scanned correctly; + auto test_fn = [&](const std::string& apex_name) { + auto apex = ApexFile::Open(GetTestFile(apex_name)); + ASSERT_TRUE(IsOk(apex)); + + { + auto ret = instance.GetPublicKey(apex->GetManifest().name()); + ASSERT_TRUE(IsOk(ret)); + ASSERT_EQ(apex->GetBundledPublicKey(), *ret); + } + + { + auto ret = instance.GetPreinstalledPath(apex->GetManifest().name()); + ASSERT_TRUE(IsOk(ret)); + ASSERT_EQ(StringPrintf("%s/%s", built_in_dir.path, apex_name.c_str()), + *ret); + } + + { + auto ret = instance.GetDataPath(apex->GetManifest().name()); + ASSERT_TRUE(IsOk(ret)); + ASSERT_EQ(StringPrintf("%s/%s", data_dir.path, apex_name.c_str()), *ret); + } + + ASSERT_TRUE(instance.HasPreInstalledVersion(apex->GetManifest().name())); + ASSERT_TRUE(instance.HasDataVersion(apex->GetManifest().name())); + }; + + test_fn("apex.apexd_test.apex"); + test_fn("apex.apexd_test_different_app.apex"); + + // Check that second call will succeed as well. + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + test_fn("apex.apexd_test.apex"); + test_fn("apex.apexd_test_different_app.apex"); +} + +TEST(ApexFileRepositoryTest, InitializeFailureCorruptApex) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("apex.apexd_test.apex"), td.path); + fs::copy(GetTestFile("apex.apexd_test_corrupt_superblock_apex.apex"), + td.path); + + ApexFileRepository instance; + ASSERT_FALSE(IsOk(instance.AddPreInstalledApex({td.path}))); +} + +TEST(ApexFileRepositoryTest, InitializeCompressedApexWithoutApex) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("com.android.apex.compressed.v1_without_apex.capex"), + td.path); + + ApexFileRepository instance; + // Compressed APEX without APEX cannot be opened + ASSERT_FALSE(IsOk(instance.AddPreInstalledApex({td.path}))); +} + +TEST(ApexFileRepositoryTest, InitializeSameNameDifferentPathAborts) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("apex.apexd_test.apex"), td.path); + fs::copy(GetTestFile("apex.apexd_test.apex"), + StringPrintf("%s/other.apex", td.path)); + + ASSERT_DEATH( + { + ApexFileRepository instance; + instance.AddPreInstalledApex({td.path}); + }, + ""); +} + +TEST(ApexFileRepositoryTest, + InitializeSameNameDifferentPathAbortsCompressedApex) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), td.path); + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), + StringPrintf("%s/other.capex", td.path)); + + ASSERT_DEATH( + { + ApexFileRepository instance; + instance.AddPreInstalledApex({td.path}); + }, + ""); +} + +TEST(ApexFileRepositoryTest, InitializePublicKeyUnexpectdlyChangedAborts) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("apex.apexd_test.apex"), td.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({td.path}))); + + // Check that apex was loaded. + auto path = instance.GetPreinstalledPath("com.android.apex.test_package"); + ASSERT_TRUE(IsOk(path)); + ASSERT_EQ(StringPrintf("%s/apex.apexd_test.apex", td.path), *path); + + auto public_key = instance.GetPublicKey("com.android.apex.test_package"); + ASSERT_TRUE(IsOk(public_key)); + + // Substitute it with another apex with the same name, but different public + // key. + fs::copy(GetTestFile("apex.apexd_test_different_key.apex"), *path, + fs::copy_options::overwrite_existing); + + { + auto apex = ApexFile::Open(*path); + ASSERT_TRUE(IsOk(apex)); + // Check module name hasn't changed. + ASSERT_EQ("com.android.apex.test_package", apex->GetManifest().name()); + // Check public key has changed. + ASSERT_NE(*public_key, apex->GetBundledPublicKey()); + } + + ASSERT_DEATH({ instance.AddPreInstalledApex({td.path}); }, ""); +} + +TEST(ApexFileRepositoryTest, + InitializePublicKeyUnexpectdlyChangedAbortsCompressedApex) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), td.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({td.path}))); + + // Check that apex was loaded. + auto path = instance.GetPreinstalledPath("com.android.apex.compressed"); + ASSERT_TRUE(IsOk(path)); + ASSERT_EQ(StringPrintf("%s/com.android.apex.compressed.v1.capex", td.path), + *path); + + auto public_key = instance.GetPublicKey("com.android.apex.compressed"); + ASSERT_TRUE(IsOk(public_key)); + + // Substitute it with another apex with the same name, but different public + // key. + fs::copy(GetTestFile("com.android.apex.compressed_different_key.capex"), + *path, fs::copy_options::overwrite_existing); + + { + auto apex = ApexFile::Open(*path); + ASSERT_TRUE(IsOk(apex)); + // Check module name hasn't changed. + ASSERT_EQ("com.android.apex.compressed", apex->GetManifest().name()); + // Check public key has changed. + ASSERT_NE(*public_key, apex->GetBundledPublicKey()); + } + + ASSERT_DEATH({ instance.AddPreInstalledApex({td.path}); }, ""); +} + +TEST(ApexFileRepositoryTest, IsPreInstalledApex) { + // Prepare test data. + TemporaryDir td; + fs::copy(GetTestFile("apex.apexd_test.apex"), td.path); + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), td.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({td.path}))); + + auto compressed_apex = ApexFile::Open( + StringPrintf("%s/com.android.apex.compressed.v1.capex", td.path)); + ASSERT_TRUE(IsOk(compressed_apex)); + ASSERT_TRUE(instance.IsPreInstalledApex(*compressed_apex)); + + auto apex1 = ApexFile::Open(StringPrintf("%s/apex.apexd_test.apex", td.path)); + ASSERT_TRUE(IsOk(apex1)); + ASSERT_TRUE(instance.IsPreInstalledApex(*apex1)); + + // It's same apex, but path is different. Shouldn't be treated as + // pre-installed. + auto apex2 = ApexFile::Open(GetTestFile("apex.apexd_test.apex")); + ASSERT_TRUE(IsOk(apex2)); + ASSERT_FALSE(instance.IsPreInstalledApex(*apex2)); + + auto apex3 = + ApexFile::Open(GetTestFile("apex.apexd_test_different_app.apex")); + ASSERT_TRUE(IsOk(apex3)); + ASSERT_FALSE(instance.IsPreInstalledApex(*apex3)); +} + +TEST(ApexFileRepositoryTest, IsDecompressedApex) { + // Prepare instance + TemporaryDir decompression_dir; + ApexFileRepository instance(decompression_dir.path); + + // Prepare decompressed apex + std::string filename = "com.android.apex.compressed.v1_original.apex"; + fs::copy(GetTestFile(filename), decompression_dir.path); + auto decompressed_path = + StringPrintf("%s/%s", decompression_dir.path, filename.c_str()); + auto decompressed_apex = ApexFile::Open(decompressed_path); + + // Any file which is already located in |decompression_dir| should be + // considered decompressed + ASSERT_TRUE(instance.IsDecompressedApex(*decompressed_apex)); + + // Hard links with same file name is not considered decompressed + TemporaryDir active_dir; + auto active_path = StringPrintf("%s/%s", active_dir.path, filename.c_str()); + std::error_code ec; + fs::create_hard_link(decompressed_path, active_path, ec); + ASSERT_FALSE(ec) << "Failed to create hardlink"; + auto active_apex = ApexFile::Open(active_path); + ASSERT_FALSE(instance.IsDecompressedApex(*active_apex)); +} + +TEST(ApexFileRepositoryTest, AddAndGetDataApex) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("apex.apexd_test_v2.apex"), data_dir.path); + PrepareCompressedApex("com.android.apex.compressed.v1.capex", + built_in_dir.path, decompression_dir.path); + // Add a data apex that has kDecompressedApexPackageSuffix + fs::copy(GetTestFile("com.android.apex.compressed.v1_original.apex"), + StringPrintf("%s/com.android.apex.compressed@1%s", data_dir.path, + kDecompressedApexPackageSuffix)); + + ApexFileRepository instance(decompression_dir.path); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + // ApexFileRepository should only deal with APEX in /data/apex/active. + // Decompressed APEX should not be included + auto data_apexs = instance.GetDataApexFiles(); + auto normal_apex = + ApexFile::Open(StringPrintf("%s/apex.apexd_test_v2.apex", data_dir.path)); + ASSERT_THAT(data_apexs, + UnorderedElementsAre(ApexFileEq(ByRef(*normal_apex)))); +} + +TEST(ApexFileRepositoryTest, AddDataApexIgnoreCompressedApex) { + // Prepare test data. + TemporaryDir data_dir, decompression_dir; + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto data_apexs = instance.GetDataApexFiles(); + ASSERT_EQ(data_apexs.size(), 0u); +} + +TEST(ApexFileRepositoryTest, AddDataApexIgnoreIfNotPreInstalled) { + // Prepare test data. + TemporaryDir data_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto data_apexs = instance.GetDataApexFiles(); + ASSERT_EQ(data_apexs.size(), 0u); +} + +TEST(ApexFileRepositoryTest, AddDataApexPrioritizeHigherVersionApex) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("apex.apexd_test.apex"), data_dir.path); + fs::copy(GetTestFile("apex.apexd_test_v2.apex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto data_apexs = instance.GetDataApexFiles(); + auto normal_apex = + ApexFile::Open(StringPrintf("%s/apex.apexd_test_v2.apex", data_dir.path)); + ASSERT_THAT(data_apexs, + UnorderedElementsAre(ApexFileEq(ByRef(*normal_apex)))); +} + +TEST(ApexFileRepositoryTest, AddDataApexDoesNotScanDecompressedApex) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir, decompression_dir; + PrepareCompressedApex("com.android.apex.compressed.v1.capex", + built_in_dir.path, decompression_dir.path); + + ApexFileRepository instance(decompression_dir.path); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto data_apexs = instance.GetDataApexFiles(); + ASSERT_EQ(data_apexs.size(), 0u); +} + +TEST(ApexFileRepositoryTest, AddDataApexIgnoreWrongPublicKey) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("apex.apexd_test_different_key.apex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto data_apexs = instance.GetDataApexFiles(); + ASSERT_EQ(data_apexs.size(), 0u); +} + +TEST(ApexFileRepositoryTest, GetPreInstalledApexFiles) { + // Prepare test data. + TemporaryDir built_in_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), + built_in_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + + auto pre_installed_apexs = instance.GetPreInstalledApexFiles(); + auto pre_apex_1 = ApexFile::Open( + StringPrintf("%s/apex.apexd_test.apex", built_in_dir.path)); + auto pre_apex_2 = ApexFile::Open(StringPrintf( + "%s/com.android.apex.compressed.v1.capex", built_in_dir.path)); + ASSERT_THAT(pre_installed_apexs, + UnorderedElementsAre(ApexFileEq(ByRef(*pre_apex_1)), + ApexFileEq(ByRef(*pre_apex_2)))); +} + +TEST(ApexFileRepositoryTest, AllApexFilesByName) { + TemporaryDir built_in_dir, decompression_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("com.android.apex.cts.shim.apex"), built_in_dir.path); + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), + built_in_dir.path); + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + + TemporaryDir data_dir; + fs::copy(GetTestFile("com.android.apex.cts.shim.v2.apex"), data_dir.path); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto result = instance.AllApexFilesByName(); + + // Verify the contents of result + auto apexd_test_file = ApexFile::Open( + StringPrintf("%s/apex.apexd_test.apex", built_in_dir.path)); + auto shim_v1 = ApexFile::Open( + StringPrintf("%s/com.android.apex.cts.shim.apex", built_in_dir.path)); + auto compressed_apex = ApexFile::Open(StringPrintf( + "%s/com.android.apex.compressed.v1.capex", built_in_dir.path)); + auto shim_v2 = ApexFile::Open( + StringPrintf("%s/com.android.apex.cts.shim.v2.apex", data_dir.path)); + + ASSERT_EQ(result.size(), 3u); + ASSERT_THAT(result[apexd_test_file->GetManifest().name()], + UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file)))); + ASSERT_THAT(result[shim_v1->GetManifest().name()], + UnorderedElementsAre(ApexFileEq(ByRef(*shim_v1)), + ApexFileEq(ByRef(*shim_v2)))); + ASSERT_THAT(result[compressed_apex->GetManifest().name()], + UnorderedElementsAre(ApexFileEq(ByRef(*compressed_apex)))); +} + +TEST(ApexFileRepositoryTest, GetDataApex) { + // Prepare test data. + TemporaryDir built_in_dir, data_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + fs::copy(GetTestFile("apex.apexd_test_v2.apex"), data_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(data_dir.path))); + + auto apex = + ApexFile::Open(StringPrintf("%s/apex.apexd_test_v2.apex", data_dir.path)); + ASSERT_RESULT_OK(apex); + + auto ret = instance.GetDataApex("com.android.apex.test_package"); + ASSERT_THAT(ret, ApexFileEq(ByRef(*apex))); +} + +TEST(ApexFileRepositoryTest, GetDataApexNoSuchApexAborts) { + ASSERT_DEATH( + { + ApexFileRepository instance; + instance.GetDataApex("whatever"); + }, + ""); +} + +TEST(ApexFileRepositoryTest, GetPreInstalledApex) { + // Prepare test data. + TemporaryDir built_in_dir; + fs::copy(GetTestFile("apex.apexd_test.apex"), built_in_dir.path); + + ApexFileRepository instance; + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({built_in_dir.path}))); + + auto apex = ApexFile::Open( + StringPrintf("%s/apex.apexd_test.apex", built_in_dir.path)); + ASSERT_RESULT_OK(apex); + + auto ret = instance.GetPreInstalledApex("com.android.apex.test_package"); + ASSERT_THAT(ret, ApexFileEq(ByRef(*apex))); +} + +TEST(ApexFileRepositoryTest, GetPreInstalledApexNoSuchApexAborts) { + ASSERT_DEATH( + { + ApexFileRepository instance; + instance.GetPreInstalledApex("whatever"); + }, + ""); +} + +} // namespace apex +} // namespace android diff --git a/apexd/apex_file_test.cpp b/apexd/apex_file_test.cpp index 711d0246..3da74484 100644 --- a/apexd/apex_file_test.cpp +++ b/apexd/apex_file_test.cpp @@ -19,31 +19,47 @@ #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/scopeguard.h> +#include <android-base/strings.h> +#include <gmock/gmock.h> #include <gtest/gtest.h> #include <libavb/libavb.h> #include <ziparchive/zip_archive.h> #include "apex_file.h" -#include "apex_preinstalled_data.h" +#include "apexd_test_utils.h" +#include "apexd_utils.h" +using android::base::GetExecutableDirectory; using android::base::Result; -static std::string testDataDir = android::base::GetExecutableDirectory() + "/"; +static const std::string kTestDataDir = GetExecutableDirectory() + "/"; namespace android { namespace apex { namespace { -TEST(ApexFileTest, GetOffsetOfSimplePackage) { - const std::string filePath = testDataDir + "apex.apexd_test.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_TRUE(apexFile.ok()); +struct ApexFileTestParam { + const char* type; + const char* prefix; +}; + +constexpr const ApexFileTestParam kParameters[] = { + {"ext4", "apex.apexd_test"}, {"f2fs", "apex.apexd_test_f2fs"}}; + +class ApexFileTest : public ::testing::TestWithParam<ApexFileTestParam> {}; + +INSTANTIATE_TEST_SUITE_P(Apex, ApexFileTest, ::testing::ValuesIn(kParameters)); + +TEST_P(ApexFileTest, GetOffsetOfSimplePackage) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_TRUE(apex_file.ok()); int32_t zip_image_offset; size_t zip_image_size; { ZipArchiveHandle handle; - int32_t rc = OpenArchive(filePath.c_str(), &handle); + int32_t rc = OpenArchive(file_path.c_str(), &handle); ASSERT_EQ(0, rc); auto close_guard = android::base::make_scope_guard([&handle]() { CloseArchive(handle); }); @@ -58,34 +74,33 @@ TEST(ApexFileTest, GetOffsetOfSimplePackage) { EXPECT_EQ(zip_image_size, entry.compressed_length); } - EXPECT_EQ(zip_image_offset, apexFile->GetImageOffset()); - EXPECT_EQ(zip_image_size, apexFile->GetImageSize()); + EXPECT_EQ(zip_image_offset, apex_file->GetImageOffset().value()); + EXPECT_EQ(zip_image_size, apex_file->GetImageSize().value()); } TEST(ApexFileTest, GetOffsetMissingFile) { - const std::string filePath = testDataDir + "missing.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_FALSE(apexFile.ok()); - EXPECT_NE(std::string::npos, - apexFile.error().message().find("Failed to open package")) - << apexFile.error(); + const std::string file_path = kTestDataDir + "missing.apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_FALSE(apex_file.ok()); + ASSERT_THAT(apex_file.error().message(), + ::testing::HasSubstr("Failed to open package")); } -TEST(ApexFileTest, GetApexManifest) { - const std::string filePath = testDataDir + "apex.apexd_test.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_RESULT_OK(apexFile); - EXPECT_EQ("com.android.apex.test_package", apexFile->GetManifest().name()); - EXPECT_EQ(1u, apexFile->GetManifest().version()); +TEST_P(ApexFileTest, GetApexManifest) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); + EXPECT_EQ("com.android.apex.test_package", apex_file->GetManifest().name()); + EXPECT_EQ(1u, apex_file->GetManifest().version()); } -TEST(ApexFileTest, VerifyApexVerity) { - ASSERT_RESULT_OK(collectPreinstalledData({"/system_ext/apex"})); - const std::string filePath = testDataDir + "apex.apexd_test.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_RESULT_OK(apexFile); +TEST_P(ApexFileTest, VerifyApexVerity) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); - auto verity_or = apexFile->VerifyApexVerity(); + auto verity_or = + apex_file->VerifyApexVerity(apex_file->GetBundledPublicKey()); ASSERT_RESULT_OK(verity_or); const ApexVerityData& data = *verity_or; @@ -93,52 +108,229 @@ TEST(ApexFileTest, VerifyApexVerity) { EXPECT_EQ(std::string("368a22e64858647bc45498e92f749f85482ac468" "50ca7ec8071f49dfa47a243c"), data.salt); - EXPECT_EQ( - std::string( - "8e841019e41e8c40bca6dd6304cbf163ea257ba0a268304832c4105eba1c2747"), - data.root_digest); -} -// TODO: May consider packaging a debug key in debug builds (again). -TEST(ApexFileTest, DISABLED_VerifyApexVerityNoKeyDir) { - const std::string filePath = testDataDir + "apex.apexd_test.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_RESULT_OK(apexFile); + const std::string digest_path = + kTestDataDir + GetParam().prefix + "_digest.txt"; + std::string root_digest; + ASSERT_TRUE(android::base::ReadFileToString(digest_path, &root_digest)) + << "Failed to read " << digest_path; + root_digest = android::base::Trim(root_digest); - auto verity_or = apexFile->VerifyApexVerity(); - ASSERT_FALSE(verity_or.ok()); + EXPECT_EQ(std::string(root_digest), data.root_digest); } -TEST(ApexFileTest, VerifyApexVerityNoKeyInst) { - const std::string filePath = testDataDir + "apex.apexd_test_no_inst_key.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_RESULT_OK(apexFile); +TEST_P(ApexFileTest, VerifyApexVerityWrongKey) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); - auto verity_or = apexFile->VerifyApexVerity(); + auto verity_or = apex_file->VerifyApexVerity("wrong-key"); ASSERT_FALSE(verity_or.ok()); } -TEST(ApexFileTest, GetBundledPublicKey) { - const std::string filePath = testDataDir + "apex.apexd_test.apex"; - Result<ApexFile> apexFile = ApexFile::Open(filePath); - ASSERT_RESULT_OK(apexFile); +TEST_P(ApexFileTest, GetBundledPublicKey) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); - const std::string keyPath = - testDataDir + "apexd_testdata/com.android.apex.test_package.avbpubkey"; - std::string keyContent; - ASSERT_TRUE(android::base::ReadFileToString(keyPath, &keyContent)) - << "Failed to read " << keyPath; + const std::string key_path = + kTestDataDir + "apexd_testdata/com.android.apex.test_package.avbpubkey"; + std::string key_content; + ASSERT_TRUE(android::base::ReadFileToString(key_path, &key_content)) + << "Failed to read " << key_path; - EXPECT_EQ(keyContent, apexFile->GetBundledPublicKey()); + EXPECT_EQ(key_content, apex_file->GetBundledPublicKey()); } -TEST(ApexFileTest, CorrutedApex_b146895998) { - const std::string apex_path = testDataDir + "corrupted_b146895998.apex"; +TEST(ApexFileTest, CorrutedApexB146895998) { + const std::string apex_path = kTestDataDir + "corrupted_b146895998.apex"; Result<ApexFile> apex = ApexFile::Open(apex_path); ASSERT_RESULT_OK(apex); - ASSERT_FALSE(apex->VerifyApexVerity()); + ASSERT_FALSE(apex->VerifyApexVerity("ignored").ok()); +} + +TEST_P(ApexFileTest, RetrieveFsType) { + const std::string file_path = kTestDataDir + GetParam().prefix + ".apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_TRUE(apex_file.ok()); + + EXPECT_EQ(std::string(GetParam().type), apex_file->GetFsType().value()); +} + +TEST(ApexFileTest, OpenInvalidFilesystem) { + const std::string file_path = + kTestDataDir + "apex.apexd_test_corrupt_superblock_apex.apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_FALSE(apex_file.ok()); + ASSERT_THAT(apex_file.error().message(), + ::testing::HasSubstr("Failed to retrieve filesystem type")); +} + +TEST(ApexFileTest, OpenCompressedApexFile) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_TRUE(apex_file.ok()); + + ASSERT_TRUE(apex_file->IsCompressed()); + ASSERT_FALSE(apex_file->GetImageOffset().has_value()); + ASSERT_FALSE(apex_file->GetImageSize().has_value()); + ASSERT_FALSE(apex_file->GetFsType().has_value()); +} + +TEST(ApexFileTest, OpenFailureForCompressedApexWithoutApex) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1_without_apex.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_FALSE(apex_file.ok()); + ASSERT_THAT(apex_file.error().message(), + ::testing::HasSubstr("Could not find entry")); +} + +TEST(ApexFileTest, GetCompressedApexManifest) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); + EXPECT_EQ("com.android.apex.compressed", apex_file->GetManifest().name()); + EXPECT_EQ(1u, apex_file->GetManifest().version()); +} + +TEST(ApexFileTest, GetBundledPublicKeyOfCompressedApex) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); + + const std::string key_path = + kTestDataDir + "apexd_testdata/com.android.apex.compressed.avbpubkey"; + std::string key_content; + ASSERT_TRUE(android::base::ReadFileToString(key_path, &key_content)) + << "Failed to read " << key_path; + + EXPECT_EQ(key_content, apex_file->GetBundledPublicKey()); +} + +TEST(ApexFileTest, CannotVerifyApexVerityForCompressedApex) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + auto apex = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex); + auto result = apex->VerifyApexVerity(apex->GetBundledPublicKey()); + ASSERT_FALSE(result.ok()); + ASSERT_THAT( + result.error().message(), + ::testing::HasSubstr("Cannot verify ApexVerity of compressed APEX")); } +TEST(ApexFileTest, DecompressCompressedApex) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); + + // Create a temp dir for decompression + TemporaryDir tmp_dir; + + const std::string package_name = apex_file->GetManifest().name(); + const std::string decompression_file_path = + tmp_dir.path + package_name + ".capex"; + + auto result = apex_file->Decompress(decompression_file_path); + ASSERT_RESULT_OK(result); + + // Assert output path is not empty + auto exists = PathExists(decompression_file_path); + ASSERT_RESULT_OK(exists); + ASSERT_TRUE(*exists) << decompression_file_path << " does not exist"; + + // Assert that decompressed apex is same as original apex + const std::string original_apex_file_path = + kTestDataDir + "com.android.apex.compressed.v1_original.apex"; + auto comparison_result = + CompareFiles(original_apex_file_path, decompression_file_path); + ASSERT_RESULT_OK(comparison_result); + ASSERT_TRUE(*comparison_result); +} + +TEST(ApexFileTest, DecompressFailForNormalApex) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1_original.apex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + ASSERT_RESULT_OK(apex_file); + + TemporaryFile decompression_file; + + auto result = apex_file->Decompress(decompression_file.path); + ASSERT_FALSE(result.ok()); + ASSERT_THAT(result.error().message(), + ::testing::HasSubstr("Cannot decompress an uncompressed APEX")); +} + +TEST(ApexFileTest, DecompressFailIfDecompressionPathExists) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + + // Attempt to decompress in a path that already exists + TemporaryFile decompression_file; + auto exists = PathExists(decompression_file.path); + ASSERT_RESULT_OK(exists); + ASSERT_TRUE(*exists) << decompression_file.path << " does not exist"; + + auto result = apex_file->Decompress(decompression_file.path); + ASSERT_FALSE(result.ok()); + ASSERT_THAT(result.error().message(), + ::testing::HasSubstr("Failed to open decompression destination")); +} + +TEST(ApexFileTest, GetPathReturnsRealpath) { + const std::string real_path = kTestDataDir + "apex.apexd_test.apex"; + const std::string symlink_path = + kTestDataDir + "apex.apexd_test.symlink.apex"; + + // In case the link already exists + int ret = unlink(symlink_path.c_str()); + ASSERT_TRUE(ret == 0 || errno == ENOENT) + << "failed to unlink " << symlink_path; + + ret = symlink(real_path.c_str(), symlink_path.c_str()); + ASSERT_EQ(0, ret) << "failed to create symlink at " << symlink_path; + + // Open with the symlink. Realpath is expected. + Result<ApexFile> apex_file = ApexFile::Open(symlink_path); + ASSERT_RESULT_OK(apex_file); + ASSERT_EQ(real_path, apex_file->GetPath()); +} + +TEST(ApexFileTest, CompressedSharedLibsApexIsRejected) { + const std::string file_path = + kTestDataDir + "com.android.apex.compressed_sharedlibs.capex"; + Result<ApexFile> apex_file = ApexFile::Open(file_path); + + ASSERT_FALSE(apex_file.ok()); + ASSERT_THAT(apex_file.error().message(), + ::testing::HasSubstr("Apex providing sharedlibs shouldn't " + "be compressed")); +} + +// Check if CAPEX contains originalApexDigest in its manifest +TEST(ApexFileTest, OriginalApexDigest) { + const std::string capex_path = + kTestDataDir + "com.android.apex.compressed.v1.capex"; + auto capex = ApexFile::Open(capex_path); + ASSERT_TRUE(capex.ok()); + const std::string decompressed_apex_path = + kTestDataDir + "com.android.apex.compressed.v1_original.apex"; + auto decompressed_apex = ApexFile::Open(decompressed_apex_path); + ASSERT_TRUE(decompressed_apex.ok()); + // Validate root digest + auto digest = decompressed_apex->VerifyApexVerity( + decompressed_apex->GetBundledPublicKey()); + ASSERT_TRUE(digest.ok()); + ASSERT_EQ(digest->root_digest, + capex->GetManifest().capexmetadata().originalapexdigest()); +} } // namespace } // namespace apex } // namespace android diff --git a/apexd/apex_manifest.cpp b/apexd/apex_manifest.cpp index 1994633c..bfba216e 100644 --- a/apexd/apex_manifest.cpp +++ b/apexd/apex_manifest.cpp @@ -22,6 +22,7 @@ using android::base::Error; using android::base::Result; +using ::apex::proto::ApexManifest; namespace android { namespace apex { @@ -46,8 +47,8 @@ Result<ApexManifest> ParseManifest(const std::string& content) { return apex_manifest; } -std::string GetPackageId(const ApexManifest& apexManifest) { - return apexManifest.name() + "@" + std::to_string(apexManifest.version()); +std::string GetPackageId(const ApexManifest& apex_manifest) { + return apex_manifest.name() + "@" + std::to_string(apex_manifest.version()); } Result<ApexManifest> ReadManifest(const std::string& path) { diff --git a/apexd/apex_manifest.h b/apexd/apex_manifest.h index f2996483..5bdd43bd 100644 --- a/apexd/apex_manifest.h +++ b/apexd/apex_manifest.h @@ -23,16 +23,16 @@ #include <string> -using ::apex::proto::ApexManifest; - namespace android { namespace apex { // Parses and validates APEX manifest. -android::base::Result<ApexManifest> ParseManifest(const std::string& content); +android::base::Result<::apex::proto::ApexManifest> ParseManifest( + const std::string& content); // Returns package id of an ApexManifest -std::string GetPackageId(const ApexManifest& apex_manifest); +std::string GetPackageId(const ::apex::proto::ApexManifest& apex_manifest); // Reads and parses APEX manifest from the file on disk. -android::base::Result<ApexManifest> ReadManifest(const std::string& path); +android::base::Result<::apex::proto::ApexManifest> ReadManifest( + const std::string& path); } // namespace apex } // namespace android diff --git a/apexd/apex_manifest_test.cpp b/apexd/apex_manifest_test.cpp index 5a4e3da5..f5bc93ca 100644 --- a/apexd/apex_manifest_test.cpp +++ b/apexd/apex_manifest_test.cpp @@ -22,6 +22,8 @@ #include "apex_manifest.h" +using ::apex::proto::ApexManifest; + namespace android { namespace apex { diff --git a/apexd/apex_preinstalled_data.cpp b/apexd/apex_preinstalled_data.cpp deleted file mode 100644 index d62c28c5..00000000 --- a/apexd/apex_preinstalled_data.cpp +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -#define LOG_TAG "apexd" - -#include "apex_preinstalled_data.h" - -#include <unordered_map> - -#include <android-base/file.h> -#include <android-base/result.h> -#include <android-base/strings.h> - -#include "apex_constants.h" -#include "apex_file.h" -#include "apexd_utils.h" -#include "string_log.h" - -using android::base::Error; -using android::base::Result; - -namespace android { -namespace apex { - -namespace { - -struct ApexPreinstalledData { - std::string name; - std::string key; - std::string path; -}; - -std::unordered_map<std::string, ApexPreinstalledData> gScannedPreinstalledData; - -Result<std::vector<ApexPreinstalledData>> collectPreinstalleDataFromDir( - const std::string& dir) { - LOG(INFO) << "Scanning " << dir << " for preinstalled data"; - std::vector<ApexPreinstalledData> ret; - if (access(dir.c_str(), F_OK) != 0 && errno == ENOENT) { - LOG(INFO) << "... does not exist. Skipping"; - return ret; - } - const bool scanBuiltinApexes = isPathForBuiltinApexes(dir); - if (!scanBuiltinApexes) { - return Error() << "Can't scan preinstalled APEX data from " << dir; - } - Result<std::vector<std::string>> apex_files = FindApexFilesByName(dir); - if (!apex_files.ok()) { - return apex_files.error(); - } - - for (const auto& file : *apex_files) { - Result<ApexFile> apex_file = ApexFile::Open(file); - if (!apex_file.ok()) { - return Error() << "Failed to open " << file << " : " << apex_file.error(); - } - ApexPreinstalledData apexPreInstalledData; - apexPreInstalledData.name = apex_file->GetManifest().name(); - apexPreInstalledData.key = apex_file->GetBundledPublicKey(); - apexPreInstalledData.path = apex_file->GetPath(); - ret.push_back(apexPreInstalledData); - } - return ret; -} - -Result<void> updatePreinstalledData( - const std::vector<ApexPreinstalledData>& apexes) { - for (const ApexPreinstalledData& apex : apexes) { - if (gScannedPreinstalledData.find(apex.name) == - gScannedPreinstalledData.end()) { - gScannedPreinstalledData.insert({apex.name, apex}); - } else { - const std::string& existing_key = - gScannedPreinstalledData.at(apex.name).key; - if (existing_key != apex.key) { - return Error() << "Key for package " << apex.name - << " does not match with previously scanned key"; - } - } - } - return {}; -} - -} // namespace - -Result<void> collectPreinstalledData(const std::vector<std::string>& dirs) { - for (const auto& dir : dirs) { - Result<std::vector<ApexPreinstalledData>> preinstalledData = - collectPreinstalleDataFromDir(dir); - if (!preinstalledData.ok()) { - return Error() << "Failed to collect keys from " << dir << " : " - << preinstalledData.error(); - } - Result<void> st = updatePreinstalledData(*preinstalledData); - if (!st.ok()) { - return st; - } - } - return {}; -} - -Result<const std::string> getApexKey(const std::string& name) { - if (gScannedPreinstalledData.find(name) == gScannedPreinstalledData.end()) { - return Error() << "No preinstalled data found for package " << name; - } - return gScannedPreinstalledData[name].key; -} - -Result<const std::string> getApexPreinstalledPath(const std::string& name) { - if (gScannedPreinstalledData.find(name) == gScannedPreinstalledData.end()) { - return Error() << "No preinstalled data found for package " << name; - } - return gScannedPreinstalledData[name].path; -} - -bool HasPreInstalledVersion(const std::string& name) { - return gScannedPreinstalledData.find(name) != gScannedPreinstalledData.end(); -} - -} // namespace apex -} // namespace android diff --git a/apexd/apex_preinstalled_data.h b/apexd/apex_preinstalled_data.h deleted file mode 100644 index 1b80aad5..00000000 --- a/apexd/apex_preinstalled_data.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -#pragma once - -#include <string> -#include <vector> - -#include <android-base/result.h> - -namespace android { -namespace apex { - -android::base::Result<void> collectPreinstalledData( - const std::vector<std::string>& apex_dirs); -android::base::Result<const std::string> getApexKey(const std::string& name); -android::base::Result<const std::string> getApexPreinstalledPath( - const std::string& name); -bool HasPreInstalledVersion(const std::string& name); -} // namespace apex -} // namespace android diff --git a/apexd/apex_shim.cpp b/apexd/apex_shim.cpp index d1ff90d9..0031c3f7 100644 --- a/apexd/apex_shim.cpp +++ b/apexd/apex_shim.cpp @@ -33,6 +33,7 @@ using android::base::ErrnoError; using android::base::Error; using android::base::Result; +using ::apex::proto::ApexManifest; namespace android { namespace apex { @@ -126,7 +127,6 @@ Result<void> ValidateShimApex(const std::string& mount_point, // Unfortunately fs::recursive_directory_iterator::operator++ can throw an // exception, which means that it's impossible to use range-based for loop // here. - // TODO: wrap into a non-throwing iterator to support range-based for loop. while (iter != fs::end(iter)) { auto path = iter->path(); // Resolve the mount point to ensure any trailing slash is removed. diff --git a/apexd/apexd.cpp b/apexd/apexd.cpp index 0010c42c..dc9bfb4b 100644 --- a/apexd/apexd.cpp +++ b/apexd/apexd.cpp @@ -15,25 +15,26 @@ */ #include "apexd.h" +#include "apex_file_repository.h" #include "apexd_private.h" #include "apex_constants.h" #include "apex_database.h" #include "apex_file.h" #include "apex_manifest.h" -#include "apex_preinstalled_data.h" #include "apex_shim.h" #include "apexd_checkpoint.h" +#include "apexd_lifecycle.h" #include "apexd_loop.h" #include "apexd_prepostinstall.h" -#include "apexd_prop.h" #include "apexd_rollback_utils.h" #include "apexd_session.h" #include "apexd_utils.h" #include "apexd_verity.h" -#include "string_log.h" +#include "com_android_apex.h" #include <ApexProperties.sysprop.h> +#include <android-base/chrono_utils.h> #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/macros.h> @@ -43,6 +44,7 @@ #include <android-base/stringprintf.h> #include <android-base/strings.h> #include <android-base/unique_fd.h> +#include <google/protobuf/util/message_differencer.h> #include <libavb/libavb.h> #include <libdm/dm.h> #include <libdm/dm_table.h> @@ -51,14 +53,17 @@ #include <dirent.h> #include <fcntl.h> +#include <linux/f2fs.h> #include <linux/loop.h> #include <stdlib.h> #include <sys/inotify.h> #include <sys/ioctl.h> #include <sys/mount.h> #include <sys/stat.h> +#include <sys/sysinfo.h> #include <sys/types.h> #include <unistd.h> +#include <algorithm> #include <algorithm> #include <array> @@ -66,31 +71,40 @@ #include <cstdlib> #include <filesystem> #include <fstream> +#include <future> #include <iomanip> #include <iterator> #include <memory> +#include <mutex> #include <optional> +#include <queue> +#include <sstream> #include <string> +#include <string_view> #include <thread> #include <unordered_map> #include <unordered_set> +using android::base::boot_clock; +using android::base::ConsumePrefix; using android::base::ErrnoError; using android::base::Error; using android::base::GetProperty; using android::base::Join; using android::base::ParseUint; using android::base::ReadFully; +using android::base::RemoveFileIfExists; using android::base::Result; -using android::base::StartsWith; +using android::base::SetProperty; using android::base::StringPrintf; using android::base::unique_fd; using android::dm::DeviceMapper; using android::dm::DmDeviceState; using android::dm::DmTable; using android::dm::DmTargetVerity; - +using ::apex::proto::ApexManifest; using apex::proto::SessionState; +using google::protobuf::util::MessageDifferencer; namespace android { namespace apex { @@ -99,12 +113,6 @@ using MountedApexData = MountedApexDatabase::MountedApexData; namespace { -// These should be in-sync with system/sepolicy/public/property_contexts -static constexpr const char* kApexStatusSysprop = "apexd.status"; -static constexpr const char* kApexStatusStarting = "starting"; -static constexpr const char* kApexStatusActivated = "activated"; -static constexpr const char* kApexStatusReady = "ready"; - static constexpr const char* kBuildFingerprintSysprop = "ro.build.fingerprint"; // This should be in UAPI, but it's not :-( @@ -113,20 +121,20 @@ static constexpr const char* kDmVerityRestartOnCorruption = MountedApexDatabase gMountedApexes; +std::optional<ApexdConfig> gConfig; + CheckpointInterface* gVoldService; bool gSupportsFsCheckpoints = false; bool gInFsCheckpointMode = false; static constexpr size_t kLoopDeviceSetupAttempts = 3u; -bool gBootstrap = false; +// Please DO NOT add new modules to this list without contacting mainline-modularization@ first. static const std::vector<std::string> kBootstrapApexes = ([]() { std::vector<std::string> ret = { - "com.android.art", "com.android.i18n", "com.android.runtime", "com.android.tzdata", - "com.android.os.statsd", }; auto vendor_vndk_ver = GetProperty("ro.vndk.version", ""); @@ -142,14 +150,39 @@ static const std::vector<std::string> kBootstrapApexes = ([]() { static constexpr const int kNumRetriesWhenCheckpointingEnabled = 1; -bool isBootstrapApex(const ApexFile& apex) { +bool IsBootstrapApex(const ApexFile& apex) { return std::find(kBootstrapApexes.begin(), kBootstrapApexes.end(), apex.GetManifest().name()) != kBootstrapApexes.end(); } +void ReleaseF2fsCompressedBlocks(const std::string& file_path) { + unique_fd fd( + TEMP_FAILURE_RETRY(open(file_path.c_str(), O_RDONLY | O_CLOEXEC, 0))); + if (fd.get() == -1) { + PLOG(ERROR) << "Failed to open " << file_path; + return; + } + unsigned int flags; + if (ioctl(fd, FS_IOC_GETFLAGS, &flags) == -1) { + PLOG(ERROR) << "Failed to call FS_IOC_GETFLAGS on " << file_path; + return; + } + if ((flags & FS_COMPR_FL) == 0) { + // Doesn't support f2fs-compression. + return; + } + uint64_t blk_cnt; + if (ioctl(fd, F2FS_IOC_RELEASE_COMPRESS_BLOCKS, &blk_cnt) == -1) { + PLOG(ERROR) << "Failed to call F2FS_IOC_RELEASE_COMPRESS_BLOCKS on " + << file_path; + } + LOG(INFO) << "Released " << blk_cnt << " compressed blocks from " + << file_path; +} + // Pre-allocate loop devices so that we don't have to wait for them // later when actually activating APEXes. -Result<void> preAllocateLoopDevices() { +Result<void> PreAllocateLoopDevices() { auto scan = FindApexes(kApexPackageBuiltinDirs); if (!scan.ok()) { return scan.error(); @@ -157,28 +190,28 @@ Result<void> preAllocateLoopDevices() { auto size = 0; for (const auto& path : *scan) { - auto apexFile = ApexFile::Open(path); - if (!apexFile.ok()) { + auto apex_file = ApexFile::Open(path); + if (!apex_file.ok()) { continue; } size++; // bootstrap Apexes may be activated on separate namespaces. - if (isBootstrapApex(*apexFile)) { + if (IsBootstrapApex(*apex_file)) { size++; } } - // note: do not call preAllocateLoopDevices() if size == 0. + // note: do not call PreAllocateLoopDevices() if size == 0. // For devices (e.g. ARC) which doesn't support loop-control - // preAllocateLoopDevices() can cause problem when it tries + // PreAllocateLoopDevices() can cause problem when it tries // to access /dev/loop-control. if (size == 0) { return {}; } - return loop::preAllocateLoopDevices(size); + return loop::PreAllocateLoopDevices(size); } -std::unique_ptr<DmTable> createVerityTable(const ApexVerityData& verity_data, +std::unique_ptr<DmTable> CreateVerityTable(const ApexVerityData& verity_data, const std::string& block_device, const std::string& hash_device, bool restart_on_corruption) { @@ -209,9 +242,18 @@ std::unique_ptr<DmTable> createVerityTable(const ApexVerityData& verity_data, // Deletes a dm-verity device with a given name and path // Synchronizes on the device actually being deleted from userspace. -Result<void> DeleteVerityDevice(const std::string& name) { +Result<void> DeleteVerityDevice(const std::string& name, bool deferred) { DeviceMapper& dm = DeviceMapper::Instance(); - if (!dm.DeleteDevice(name, 750ms)) { + if (deferred) { + if (!dm.DeleteDeviceDeferred(name)) { + return ErrnoError() << "Failed to issue deferred delete of verity device " + << name; + } + return {}; + } + auto timeout = std::chrono::milliseconds( + android::sysprop::ApexProperties::dm_delete_timeout().value_or(750)); + if (!dm.DeleteDevice(name, timeout)) { return Error() << "Failed to delete dm-device " << name; } return {}; @@ -244,7 +286,7 @@ class DmVerityDevice { ~DmVerityDevice() { if (!cleared_) { - Result<void> ret = DeleteVerityDevice(name_); + Result<void> ret = DeleteVerityDevice(name_, /* deferred= */ false); if (!ret.ok()) { LOG(ERROR) << ret.error(); } @@ -262,32 +304,46 @@ class DmVerityDevice { bool cleared_; }; -Result<DmVerityDevice> createVerityDevice(const std::string& name, +Result<DmVerityDevice> CreateVerityDevice(const std::string& name, const DmTable& table) { DeviceMapper& dm = DeviceMapper::Instance(); if (dm.GetState(name) != DmDeviceState::INVALID) { - // TODO: since apexd tears down devices during unmount, can this happen? + // Delete dangling dm-device. This can happen if apexd fails to delete it + // while unmounting an apex. LOG(WARNING) << "Deleting existing dm device " << name; - const Result<void>& result = DeleteVerityDevice(name); + auto result = DeleteVerityDevice(name, /* deferred= */ false); if (!result.ok()) { - // TODO: should we fail instead? - LOG(ERROR) << "Failed to delete device " << name << " : " - << result.error(); + return result.error(); } } + auto timeout = std::chrono::milliseconds( + android::sysprop::ApexProperties::dm_create_timeout().value_or(1000)); std::string dev_path; - if (!dm.CreateDevice(name, table, &dev_path, 500ms)) { + if (!dm.CreateDevice(name, table, &dev_path, timeout)) { return Errorf("Couldn't create verity device."); } return DmVerityDevice(name, dev_path); } +/** + * When we create hardlink for a new apex package in kActiveApexPackagesDataDir, + * there might be an older version of the same package already present in there. + * Since a new version of the same package is being installed on this boot, the + * old one needs to deleted so that we don't end up activating same package + * twice. + * + * @param affected_packages package names of the news apex that are being + * installed in this boot + * @param files_to_keep path to the new apex packages in + * kActiveApexPackagesDataDir + */ Result<void> RemovePreviouslyActiveApexFiles( const std::unordered_set<std::string>& affected_packages, const std::unordered_set<std::string>& files_to_keep) { - auto all_active_apex_files = FindApexFilesByName(kActiveApexPackagesDataDir); + auto all_active_apex_files = + FindFilesBySuffix(gConfig->active_apex_data_dir, {kApexPackageSuffix}); if (!all_active_apex_files.ok()) { return all_active_apex_files.error(); @@ -321,13 +377,14 @@ Result<void> RemovePreviouslyActiveApexFiles( } // Reads the entire device to verify the image is authenticatic -Result<void> readVerityDevice(const std::string& verity_device, +Result<void> ReadVerityDevice(const std::string& verity_device, uint64_t device_size) { static constexpr int kBlockSize = 4096; static constexpr size_t kBufSize = 1024 * kBlockSize; std::vector<uint8_t> buffer(kBufSize); - unique_fd fd(TEMP_FAILURE_RETRY(open(verity_device.c_str(), O_RDONLY))); + unique_fd fd( + TEMP_FAILURE_RETRY(open(verity_device.c_str(), O_RDONLY | O_CLOEXEC))); if (fd.get() == -1) { return ErrnoError() << "Can't open " << verity_device; } @@ -346,9 +403,16 @@ Result<void> readVerityDevice(const std::string& verity_device, Result<void> VerifyMountedImage(const ApexFile& apex, const std::string& mount_point) { - auto result = apex.VerifyManifestMatches(mount_point); - if (!result.ok()) { - return result; + // Verify that apex_manifest.pb inside mounted image matches the one in the + // outer .apex container. + Result<ApexManifest> verified_manifest = + ReadManifest(mount_point + "/" + kManifestFilenamePb); + if (!verified_manifest.ok()) { + return verified_manifest.error(); + } + if (!MessageDifferencer::Equals(*verified_manifest, apex.GetManifest())) { + return Errorf( + "Manifest inside filesystem does not match manifest outside it"); } if (shim::IsShimApex(apex)) { return shim::ValidateShimApex(mount_point, apex); @@ -357,11 +421,18 @@ Result<void> VerifyMountedImage(const ApexFile& apex, } Result<MountedApexData> MountPackageImpl(const ApexFile& apex, - const std::string& mountPoint, + const std::string& mount_point, const std::string& device_name, const std::string& hashtree_file, - bool verifyImage) { - LOG(VERBOSE) << "Creating mount point: " << mountPoint; + bool verify_image, + bool temp_mount = false) { + if (apex.IsCompressed()) { + return Error() << "Cannot directly mount compressed APEX " + << apex.GetPath(); + } + + LOG(VERBOSE) << "Creating mount point: " << mount_point; + auto time_started = boot_clock::now(); // Note: the mount point could exist in case when the APEX was activated // during the bootstrap phase (e.g., the runtime or tzdata APEX). // Although we have separate mount namespaces to separate the early activated @@ -370,31 +441,34 @@ Result<MountedApexData> MountPackageImpl(const ApexFile& apex, // mounted at / which is (and has to be) a shared mount. Therefore, if apexd // finds an empty directory under /apex, it's not a problem and apexd can use // it. - auto exists = PathExists(mountPoint); + auto exists = PathExists(mount_point); if (!exists.ok()) { return exists.error(); } - if (!*exists && mkdir(mountPoint.c_str(), kMkdirMode) != 0) { - return ErrnoError() << "Could not create mount point " << mountPoint; + if (!*exists && mkdir(mount_point.c_str(), kMkdirMode) != 0) { + return ErrnoError() << "Could not create mount point " << mount_point; } - auto deleter = [&mountPoint]() { - if (rmdir(mountPoint.c_str()) != 0) { - PLOG(WARNING) << "Could not rmdir " << mountPoint; + auto deleter = [&mount_point]() { + if (rmdir(mount_point.c_str()) != 0) { + PLOG(WARNING) << "Could not rmdir " << mount_point; } }; auto scope_guard = android::base::make_scope_guard(deleter); - if (!IsEmptyDirectory(mountPoint)) { - return ErrnoError() << mountPoint << " is not empty"; + if (!IsEmptyDirectory(mount_point)) { + return ErrnoError() << mount_point << " is not empty"; } const std::string& full_path = apex.GetPath(); - loop::LoopbackDeviceUniqueFd loopbackDevice; + if (!apex.GetImageOffset() || !apex.GetImageSize()) { + return Error() << "Cannot create mount point without image offset and size"; + } + loop::LoopbackDeviceUniqueFd loopback_device; for (size_t attempts = 1;; ++attempts) { - Result<loop::LoopbackDeviceUniqueFd> ret = loop::createLoopDevice( - full_path, apex.GetImageOffset(), apex.GetImageSize()); + Result<loop::LoopbackDeviceUniqueFd> ret = loop::CreateLoopDevice( + full_path, apex.GetImageOffset().value(), apex.GetImageSize().value()); if (ret.ok()) { - loopbackDevice = std::move(*ret); + loopback_device = std::move(*ret); break; } if (attempts >= kLoopDeviceSetupAttempts) { @@ -402,33 +476,43 @@ Result<MountedApexData> MountPackageImpl(const ApexFile& apex, << ret.error(); } } - LOG(VERBOSE) << "Loopback device created: " << loopbackDevice.name; + LOG(VERBOSE) << "Loopback device created: " << loopback_device.name; - auto verityData = apex.VerifyApexVerity(); - if (!verityData.ok()) { + auto& instance = ApexFileRepository::GetInstance(); + + auto public_key = instance.GetPublicKey(apex.GetManifest().name()); + if (!public_key.ok()) { + return public_key.error(); + } + + auto verity_data = apex.VerifyApexVerity(*public_key); + if (!verity_data.ok()) { return Error() << "Failed to verify Apex Verity data for " << full_path - << ": " << verityData.error(); + << ": " << verity_data.error(); } - std::string blockDevice = loopbackDevice.name; - MountedApexData apex_data(loopbackDevice.name, apex.GetPath(), mountPoint, + std::string block_device = loopback_device.name; + MountedApexData apex_data(loopback_device.name, apex.GetPath(), mount_point, /* device_name = */ "", - /* hashtree_loop_name = */ ""); + /* hashtree_loop_name = */ "", + /* is_temp_mount */ temp_mount); // for APEXes in immutable partitions, we don't need to mount them on // dm-verity because they are already in the dm-verity protected partition; // system. However, note that we don't skip verification to ensure that APEXes // are correctly signed. - const bool mountOnVerity = !isPathForBuiltinApexes(full_path); - DmVerityDevice verityDev; + const bool mount_on_verity = + !instance.IsPreInstalledApex(apex) || instance.IsDecompressedApex(apex); + + DmVerityDevice verity_dev; loop::LoopbackDeviceUniqueFd loop_for_hash; - if (mountOnVerity) { - std::string hash_device = loopbackDevice.name; - if (verityData->desc->tree_size == 0) { - if (auto st = PrepareHashTree(apex, *verityData, hashtree_file); + if (mount_on_verity) { + std::string hash_device = loopback_device.name; + if (verity_data->desc->tree_size == 0) { + if (auto st = PrepareHashTree(apex, *verity_data, hashtree_file); !st.ok()) { return st.error(); } - auto create_loop_status = loop::createLoopDevice(hashtree_file, 0, 0); + auto create_loop_status = loop::CreateLoopDevice(hashtree_file, 0, 0); if (!create_loop_status.ok()) { return create_loop_status.error(); } @@ -436,54 +520,59 @@ Result<MountedApexData> MountPackageImpl(const ApexFile& apex, hash_device = loop_for_hash.name; apex_data.hashtree_loop_name = hash_device; } - auto verityTable = - createVerityTable(*verityData, loopbackDevice.name, hash_device, - /* restart_on_corruption = */ !verifyImage); - Result<DmVerityDevice> verityDevRes = - createVerityDevice(device_name, *verityTable); - if (!verityDevRes.ok()) { + auto verity_table = + CreateVerityTable(*verity_data, loopback_device.name, hash_device, + /* restart_on_corruption = */ !verify_image); + Result<DmVerityDevice> verity_dev_res = + CreateVerityDevice(device_name, *verity_table); + if (!verity_dev_res.ok()) { return Error() << "Failed to create Apex Verity device " << full_path - << ": " << verityDevRes.error(); + << ": " << verity_dev_res.error(); } - verityDev = std::move(*verityDevRes); + verity_dev = std::move(*verity_dev_res); apex_data.device_name = device_name; - blockDevice = verityDev.GetDevPath(); + block_device = verity_dev.GetDevPath(); - Result<void> readAheadStatus = - loop::configureReadAhead(verityDev.GetDevPath()); - if (!readAheadStatus.ok()) { - return readAheadStatus.error(); + Result<void> read_ahead_status = + loop::ConfigureReadAhead(verity_dev.GetDevPath()); + if (!read_ahead_status.ok()) { + return read_ahead_status.error(); } } - // TODO: consider moving this inside RunVerifyFnInsideTempMount. - if (mountOnVerity && verifyImage) { - Result<void> verityStatus = - readVerityDevice(blockDevice, (*verityData).desc->image_size); - if (!verityStatus.ok()) { - return verityStatus.error(); + // TODO(b/158467418): consider moving this inside RunVerifyFnInsideTempMount. + if (mount_on_verity && verify_image) { + Result<void> verity_status = + ReadVerityDevice(block_device, (*verity_data).desc->image_size); + if (!verity_status.ok()) { + return verity_status.error(); } } - uint32_t mountFlags = MS_NOATIME | MS_NODEV | MS_DIRSYNC | MS_RDONLY; + uint32_t mount_flags = MS_NOATIME | MS_NODEV | MS_DIRSYNC | MS_RDONLY; if (apex.GetManifest().nocode()) { - mountFlags |= MS_NOEXEC; + mount_flags |= MS_NOEXEC; } - if (mount(blockDevice.c_str(), mountPoint.c_str(), "ext4", mountFlags, - nullptr) == 0) { + if (!apex.GetFsType()) { + return Error() << "Cannot mount package without FsType"; + } + if (mount(block_device.c_str(), mount_point.c_str(), + apex.GetFsType().value().c_str(), mount_flags, nullptr) == 0) { + auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( + boot_clock::now() - time_started).count(); LOG(INFO) << "Successfully mounted package " << full_path << " on " - << mountPoint; - auto status = VerifyMountedImage(apex, mountPoint); + << mount_point << " duration=" << time_elapsed; + auto status = VerifyMountedImage(apex, mount_point); if (!status.ok()) { - if (umount2(mountPoint.c_str(), UMOUNT_NOFOLLOW) != 0) { - PLOG(ERROR) << "Failed to umount " << mountPoint; + if (umount2(mount_point.c_str(), UMOUNT_NOFOLLOW) != 0) { + PLOG(ERROR) << "Failed to umount " << mount_point; } return Error() << "Failed to verify " << full_path << ": " << status.error(); } // Time to accept the temporaries as good. - verityDev.Release(); - loopbackDevice.CloseGood(); + verity_dev.Release(); + loopback_device.CloseGood(); loop_for_hash.CloseGood(); scope_guard.Disable(); // Accept the mount. @@ -494,8 +583,9 @@ Result<MountedApexData> MountPackageImpl(const ApexFile& apex, } std::string GetHashTreeFileName(const ApexFile& apex, bool is_new) { + const std::string& id = GetPackageId(apex.GetManifest()); std::string ret = - std::string(kApexHashTreeDir) + "/" + GetPackageId(apex.GetManifest()); + StringPrintf("%s/%s", gConfig->apex_hash_tree_dir, id.c_str()); return is_new ? ret + ".new" : ret; } @@ -511,28 +601,40 @@ Result<MountedApexData> VerifyAndTempMountPackage( return ErrnoError() << "Failed to unlink " << hashtree_file; } } - return MountPackageImpl(apex, mount_point, temp_device_name, - GetHashTreeFileName(apex, /* is_new = */ true), - /* verifyImage = */ true); + auto ret = + MountPackageImpl(apex, mount_point, temp_device_name, hashtree_file, + /* verify_image = */ true, /* temp_mount = */ true); + if (!ret.ok()) { + LOG(DEBUG) << "Cleaning up " << hashtree_file; + if (TEMP_FAILURE_RETRY(unlink(hashtree_file.c_str())) != 0) { + PLOG(ERROR) << "Failed to unlink " << hashtree_file; + } + } else { + gMountedApexes.AddMountedApex(apex.GetManifest().name(), false, *ret); + } + return ret; } -Result<void> Unmount(const MountedApexData& data) { +} // namespace + +Result<void> Unmount(const MountedApexData& data, bool deferred) { LOG(DEBUG) << "Unmounting " << data.full_path << " from mount point " - << data.mount_point; + << data.mount_point << " deferred = " << deferred; // Lazily try to umount whatever is mounted. if (umount2(data.mount_point.c_str(), UMOUNT_NOFOLLOW) != 0 && errno != EINVAL && errno != ENOENT) { return ErrnoError() << "Failed to unmount directory " << data.mount_point; } - // Attempt to delete the folder. If the folder is retained, other - // data may be incorrect. - if (rmdir(data.mount_point.c_str()) != 0) { - PLOG(ERROR) << "Failed to rmdir directory " << data.mount_point; + + if (!deferred) { + if (rmdir(data.mount_point.c_str()) != 0) { + PLOG(ERROR) << "Failed to rmdir " << data.mount_point; + } } // Try to free up the device-mapper device. if (!data.device_name.empty()) { - const auto& result = DeleteVerityDevice(data.device_name); + const auto& result = DeleteVerityDevice(data.device_name, deferred); if (!result.ok()) { return result; } @@ -542,19 +644,28 @@ Result<void> Unmount(const MountedApexData& data) { auto log_fn = [](const std::string& path, const std::string& /*id*/) { LOG(VERBOSE) << "Freeing loop device " << path << " for unmount."; }; - if (!data.loop_name.empty()) { + + // Since we now use LO_FLAGS_AUTOCLEAR when configuring loop devices, in + // theory we don't need to manually call DestroyLoopDevice here even if + // |deferred| is false. However we prefer to call it to ensure the invariant + // of SubmitStagedSession (after it's done, loop devices created for temp + // mount are freed). + if (!data.loop_name.empty() && !deferred) { loop::DestroyLoopDevice(data.loop_name, log_fn); } - if (!data.hashtree_loop_name.empty()) { + if (!data.hashtree_loop_name.empty() && !deferred) { loop::DestroyLoopDevice(data.hashtree_loop_name, log_fn); } return {}; } +namespace { + template <typename VerifyFn> Result<void> RunVerifyFnInsideTempMount(const ApexFile& apex, - const VerifyFn& verify_fn) { + const VerifyFn& verify_fn, + bool unmount_during_cleanup) { // Temp mount image of this apex to validate it was properly signed; // this will also read the entire block device through dm-verity, so // we can be sure there is no corruption. @@ -569,11 +680,15 @@ Result<void> RunVerifyFnInsideTempMount(const ApexFile& apex, return mount_status.error(); } auto cleaner = [&]() { - LOG(DEBUG) << "Unmounting " << temp_mount_point; - Result<void> result = Unmount(*mount_status); - if (!result.ok()) { - LOG(WARNING) << "Failed to unmount " << temp_mount_point << " : " - << result.error(); + if (unmount_during_cleanup) { + LOG(DEBUG) << "Unmounting " << temp_mount_point; + Result<void> result = Unmount(*mount_status, /* deferred= */ false); + if (!result.ok()) { + LOG(WARNING) << "Failed to unmount " << temp_mount_point << " : " + << result.error(); + } + gMountedApexes.RemoveMountedApex(apex.GetManifest().name(), + apex.GetPath(), true); } }; auto scope_guard = android::base::make_scope_guard(cleaner); @@ -583,6 +698,11 @@ Result<void> RunVerifyFnInsideTempMount(const ApexFile& apex, template <typename HookFn, typename HookCall> Result<void> PrePostinstallPackages(const std::vector<ApexFile>& apexes, HookFn fn, HookCall call) { + auto scope_guard = android::base::make_scope_guard([&]() { + for (const ApexFile& apex_file : apexes) { + apexd_private::UnmountTempMount(apex_file); + } + }); if (apexes.empty()) { return Errorf("Empty set of inputs"); } @@ -596,9 +716,26 @@ Result<void> PrePostinstallPackages(const std::vector<ApexFile>& apexes, } } - // 2) If we found hooks, run the pre/post-install. + // 2) If we found hooks, temp mount if required, and run the pre/post-install. if (has_hooks) { - Result<void> install_status = (*call)(apexes); + std::vector<std::string> mount_points; + for (const ApexFile& apex : apexes) { + // Retrieve the mount data if the apex is already temp mounted, temp + // mount it otherwise. + std::string mount_point = + apexd_private::GetPackageTempMountPoint(apex.GetManifest()); + Result<MountedApexData> mount_data = + apexd_private::GetTempMountedApexData(apex.GetManifest().name()); + if (!mount_data.ok()) { + mount_data = VerifyAndTempMountPackage(apex, mount_point); + if (!mount_data.ok()) { + return mount_data.error(); + } + } + mount_points.push_back(mount_point); + } + + Result<void> install_status = (*call)(apexes, mount_points); if (!install_status.ok()) { return install_status; } @@ -617,20 +754,23 @@ Result<void> PostinstallPackages(const std::vector<ApexFile>& apexes) { &StagePostInstall); } -template <typename RetType, typename Fn> -RetType HandlePackages(const std::vector<std::string>& paths, Fn fn) { - // 1) Open all APEXes. - std::vector<ApexFile> apex_files; +// Converts a list of apex file paths into a list of ApexFile objects +// +// Returns error when trying to open empty set of inputs. +Result<std::vector<ApexFile>> OpenApexFiles( + const std::vector<std::string>& paths) { + if (paths.empty()) { + return Errorf("Empty set of inputs"); + } + std::vector<ApexFile> ret; for (const std::string& path : paths) { Result<ApexFile> apex_file = ApexFile::Open(path); if (!apex_file.ok()) { return apex_file.error(); } - apex_files.emplace_back(std::move(*apex_file)); + ret.emplace_back(std::move(*apex_file)); } - - // 2) Dispatch. - return fn(apex_files); + return ret; } Result<void> ValidateStagingShimApex(const ApexFile& to) { @@ -643,14 +783,20 @@ Result<void> ValidateStagingShimApex(const ApexFile& to) { auto verify_fn = [&](const std::string& system_apex_path) { return shim::ValidateUpdate(system_apex_path, to.GetPath()); }; - return RunVerifyFnInsideTempMount(*system_shim, verify_fn); + return RunVerifyFnInsideTempMount(*system_shim, verify_fn, true); } // A version of apex verification that happens during boot. // This function should only verification checks that are necessary to run on // each boot. Try to avoid putting expensive checks inside this function. Result<void> VerifyPackageBoot(const ApexFile& apex_file) { - Result<ApexVerityData> verity_or = apex_file.VerifyApexVerity(); + // TODO(ioffe): why do we need this here? + auto& instance = ApexFileRepository::GetInstance(); + auto public_key = instance.GetPublicKey(apex_file.GetManifest().name()); + if (!public_key.ok()) { + return public_key.error(); + } + Result<ApexVerityData> verity_or = apex_file.VerifyApexVerity(*public_key); if (!verity_or.ok()) { return verity_or.error(); } @@ -667,49 +813,48 @@ Result<void> VerifyPackageBoot(const ApexFile& apex_file) { return {}; } -// A version of apex verification that happens on submitStagedSession. +// A version of apex verification that happens on SubmitStagedSession. // This function contains checks that might be expensive to perform, e.g. temp // mounting a package and reading entire dm-verity device, and shouldn't be run // during boot. -Result<void> VerifyPackageInstall(const ApexFile& apex_file) { +Result<void> VerifyPackageStagedInstall(const ApexFile& apex_file) { const auto& verify_package_boot_status = VerifyPackageBoot(apex_file); if (!verify_package_boot_status.ok()) { return verify_package_boot_status; } - Result<ApexVerityData> verity_or = apex_file.VerifyApexVerity(); constexpr const auto kSuccessFn = [](const std::string& /*mount_point*/) { return Result<void>{}; }; - return RunVerifyFnInsideTempMount(apex_file, kSuccessFn); + return RunVerifyFnInsideTempMount(apex_file, kSuccessFn, false); } template <typename VerifyApexFn> -Result<std::vector<ApexFile>> verifyPackages( +Result<std::vector<ApexFile>> VerifyPackages( const std::vector<std::string>& paths, const VerifyApexFn& verify_apex_fn) { - if (paths.empty()) { - return Errorf("Empty set of inputs"); + Result<std::vector<ApexFile>> apex_files = OpenApexFiles(paths); + if (!apex_files.ok()) { + return apex_files.error(); } - LOG(DEBUG) << "verifyPackages() for " << Join(paths, ','); - auto verify_fn = [&](std::vector<ApexFile>& apexes) { - for (const ApexFile& apex_file : apexes) { - Result<void> result = verify_apex_fn(apex_file); - if (!result.ok()) { - return Result<std::vector<ApexFile>>(result.error()); - } + LOG(DEBUG) << "VerifyPackages() for " << Join(paths, ','); + + for (const ApexFile& apex_file : *apex_files) { + Result<void> result = verify_apex_fn(apex_file); + if (!result.ok()) { + return result.error(); } - return Result<std::vector<ApexFile>>(std::move(apexes)); - }; - return HandlePackages<Result<std::vector<ApexFile>>>(paths, verify_fn); + } + return std::move(*apex_files); } -Result<ApexFile> verifySessionDir(const int session_id) { - std::string sessionDirPath = std::string(kStagedSessionsDir) + "/session_" + - std::to_string(session_id); - LOG(INFO) << "Scanning " << sessionDirPath +Result<ApexFile> VerifySessionDir(const int session_id) { + std::string session_dir_path = std::string(kStagedSessionsDir) + "/session_" + + std::to_string(session_id); + LOG(INFO) << "Scanning " << session_dir_path << " looking for packages to be validated"; - Result<std::vector<std::string>> scan = FindApexFilesByName(sessionDirPath); + Result<std::vector<std::string>> scan = + FindFilesBySuffix(session_dir_path, {kApexPackageSuffix}); if (!scan.ok()) { LOG(WARNING) << scan.error(); return scan.error(); @@ -720,7 +865,7 @@ Result<ApexFile> verifySessionDir(const int session_id) { "More than one APEX package found in the same session directory."); } - auto verified = verifyPackages(*scan, VerifyPackageInstall); + auto verified = VerifyPackages(*scan, VerifyPackageStagedInstall); if (!verified.ok()) { return verified.error(); } @@ -741,25 +886,27 @@ Result<void> DeleteBackup() { } Result<void> BackupActivePackages() { - LOG(DEBUG) << "Initializing backup of " << kActiveApexPackagesDataDir; + LOG(DEBUG) << "Initializing backup of " << gConfig->active_apex_data_dir; // Previous restore might've delete backups folder. - auto create_status = createDirIfNeeded(kApexBackupDir, 0700); + auto create_status = CreateDirIfNeeded(kApexBackupDir, 0700); if (!create_status.ok()) { return Error() << "Backup failed : " << create_status.error(); } - auto apex_active_exists = PathExists(std::string(kActiveApexPackagesDataDir)); + auto apex_active_exists = + PathExists(std::string(gConfig->active_apex_data_dir)); if (!apex_active_exists.ok()) { return Error() << "Backup failed : " << apex_active_exists.error(); } if (!*apex_active_exists) { - LOG(DEBUG) << kActiveApexPackagesDataDir + LOG(DEBUG) << gConfig->active_apex_data_dir << " does not exist. Nothing to backup"; return {}; } - auto active_packages = FindApexFilesByName(kActiveApexPackagesDataDir); + auto active_packages = + FindFilesBySuffix(gConfig->active_apex_data_dir, {kApexPackageSuffix}); if (!active_packages.ok()) { return Error() << "Backup failed : " << active_packages.error(); } @@ -800,7 +947,7 @@ Result<void> BackupActivePackages() { } Result<void> RestoreActivePackages() { - LOG(DEBUG) << "Initializing restore of " << kActiveApexPackagesDataDir; + LOG(DEBUG) << "Initializing restore of " << gConfig->active_apex_data_dir; auto backup_exists = PathExists(std::string(kApexBackupDir)); if (!backup_exists.ok()) { @@ -811,37 +958,38 @@ Result<void> RestoreActivePackages() { } struct stat stat_data; - if (stat(kActiveApexPackagesDataDir, &stat_data) != 0) { - return ErrnoError() << "Failed to access " << kActiveApexPackagesDataDir; + if (stat(gConfig->active_apex_data_dir, &stat_data) != 0) { + return ErrnoError() << "Failed to access " << gConfig->active_apex_data_dir; } - LOG(DEBUG) << "Deleting existing packages in " << kActiveApexPackagesDataDir; + LOG(DEBUG) << "Deleting existing packages in " + << gConfig->active_apex_data_dir; auto delete_status = - DeleteDirContent(std::string(kActiveApexPackagesDataDir)); + DeleteDirContent(std::string(gConfig->active_apex_data_dir)); if (!delete_status.ok()) { return delete_status; } LOG(DEBUG) << "Renaming " << kApexBackupDir << " to " - << kActiveApexPackagesDataDir; - if (rename(kApexBackupDir, kActiveApexPackagesDataDir) != 0) { + << gConfig->active_apex_data_dir; + if (rename(kApexBackupDir, gConfig->active_apex_data_dir) != 0) { return ErrnoError() << "Failed to rename " << kApexBackupDir << " to " - << kActiveApexPackagesDataDir; + << gConfig->active_apex_data_dir; } LOG(DEBUG) << "Restoring original permissions for " - << kActiveApexPackagesDataDir; - if (chmod(kActiveApexPackagesDataDir, stat_data.st_mode & ALLPERMS) != 0) { - // TODO: should we wipe out /data/apex/active if chmod fails? + << gConfig->active_apex_data_dir; + if (chmod(gConfig->active_apex_data_dir, stat_data.st_mode & ALLPERMS) != 0) { return ErrnoError() << "Failed to restore original permissions for " - << kActiveApexPackagesDataDir; + << gConfig->active_apex_data_dir; } return {}; } -Result<void> UnmountPackage(const ApexFile& apex, bool allow_latest) { - LOG(VERBOSE) << "Unmounting " << GetPackageId(apex.GetManifest()); +Result<void> UnmountPackage(const ApexFile& apex, bool allow_latest, + bool deferred) { + LOG(INFO) << "Unmounting " << GetPackageId(apex.GetManifest()); const ApexManifest& manifest = apex.GetManifest(); @@ -860,33 +1008,39 @@ Result<void> UnmountPackage(const ApexFile& apex, bool allow_latest) { return Error() << "Did not find " << apex.GetPath(); } - if (latest) { + // Concept of latest sharedlibs apex is somewhat blurred. Since this is only + // used in testing, it is ok to always allow unmounting sharedlibs apex. + if (latest && !manifest.providesharedapexlibs()) { if (!allow_latest) { return Error() << "Package " << apex.GetPath() << " is active"; } std::string mount_point = apexd_private::GetActiveMountPoint(manifest); - LOG(VERBOSE) << "Unmounting and deleting " << mount_point; + LOG(INFO) << "Unmounting " << mount_point; if (umount2(mount_point.c_str(), UMOUNT_NOFOLLOW) != 0) { return ErrnoError() << "Failed to unmount " << mount_point; } - if (rmdir(mount_point.c_str()) != 0) { - PLOG(ERROR) << "Could not rmdir " << mount_point; - // Continue here. + + if (!deferred) { + if (rmdir(mount_point.c_str()) != 0) { + PLOG(ERROR) << "Failed to rmdir " << mount_point; + } } } // Clean up gMountedApexes now, even though we're not fully done. gMountedApexes.RemoveMountedApex(manifest.name(), apex.GetPath()); - return Unmount(*data); + return Unmount(*data, deferred); } } // namespace -Result<void> MountPackage(const ApexFile& apex, const std::string& mountPoint) { - auto ret = - MountPackageImpl(apex, mountPoint, GetPackageId(apex.GetManifest()), - GetHashTreeFileName(apex, /* is_new = */ false), - /* verifyImage = */ false); +void SetConfig(const ApexdConfig& config) { gConfig = config; } + +Result<void> MountPackage(const ApexFile& apex, const std::string& mount_point, + const std::string& device_name) { + auto ret = MountPackageImpl(apex, mount_point, device_name, + GetHashTreeFileName(apex, /* is_new= */ false), + /* verify_image = */ false); if (!ret.ok()) { return ret.error(); } @@ -897,15 +1051,41 @@ Result<void> MountPackage(const ApexFile& apex, const std::string& mountPoint) { namespace apexd_private { -Result<MountedApexData> TempMountPackage(const ApexFile& apex, - const std::string& mount_point) { - // TODO(ioffe): consolidate these two methods. - return android::apex::VerifyAndTempMountPackage(apex, mount_point); +Result<void> UnmountTempMount(const ApexFile& apex) { + const ApexManifest& manifest = apex.GetManifest(); + LOG(VERBOSE) << "Unmounting all temp mounts for package " << manifest.name(); + + bool finished_unmounting = false; + // If multiple temp mounts exist, ensure that all are unmounted. + while (!finished_unmounting) { + Result<MountedApexData> data = + apexd_private::GetTempMountedApexData(manifest.name()); + if (!data.ok()) { + finished_unmounting = true; + } else { + gMountedApexes.RemoveMountedApex(manifest.name(), data->full_path, true); + Unmount(*data, /* deferred= */ false); + } + } + return {}; } -Result<void> Unmount(const MountedApexData& data) { - // TODO(ioffe): consolidate these two methods. - return android::apex::Unmount(data); +Result<MountedApexData> GetTempMountedApexData(const std::string& package) { + bool found = false; + Result<MountedApexData> mount_data; + gMountedApexes.ForallMountedApexes( + package, + [&](const MountedApexData& data, [[maybe_unused]] bool latest) { + if (!found) { + mount_data = data; + found = true; + } + }, + true); + if (found) { + return mount_data; + } + return Error() << "No temp mount data found for " << package; } bool IsMounted(const std::string& full_path) { @@ -934,77 +1114,219 @@ std::string GetActiveMountPoint(const ApexManifest& manifest) { } // namespace apexd_private -Result<void> resumeRevertIfNeeded() { +Result<void> ResumeRevertIfNeeded() { auto sessions = ApexSession::GetSessionsInState(SessionState::REVERT_IN_PROGRESS); if (sessions.empty()) { return {}; } - return revertActiveSessions(""); + return RevertActiveSessions("", ""); +} + +Result<void> ActivateSharedLibsPackage(const std::string& mount_point) { + for (const auto& lib_path : {"lib", "lib64"}) { + std::string apex_lib_path = mount_point + "/" + lib_path; + auto lib_dir = PathExists(apex_lib_path); + if (!lib_dir.ok() || !*lib_dir) { + continue; + } + + auto iter = std::filesystem::directory_iterator(apex_lib_path); + std::error_code ec; + + while (iter != std::filesystem::end(iter)) { + const auto& lib_entry = *iter; + if (!lib_entry.is_directory()) { + iter = iter.increment(ec); + if (ec) { + return Error() << "Failed to scan " << apex_lib_path << " : " + << ec.message(); + } + continue; + } + + const auto library_name = lib_entry.path().filename(); + const std::string library_symlink_dir = + StringPrintf("%s/%s/%s/%s", kApexRoot, kApexSharedLibsSubDir, + lib_path, library_name.c_str()); + + auto symlink_dir = PathExists(library_symlink_dir); + if (!symlink_dir.ok() || !*symlink_dir) { + std::filesystem::create_directory(library_symlink_dir, ec); + if (ec) { + return Error() << "Failed to create directory " << library_symlink_dir + << ": " << ec.message(); + } + } + + auto inner_iter = + std::filesystem::directory_iterator(lib_entry.path().string()); + + while (inner_iter != std::filesystem::end(inner_iter)) { + const auto& lib_items = *inner_iter; + const auto hash_value = lib_items.path().filename(); + const std::string library_symlink_hash = StringPrintf( + "%s/%s", library_symlink_dir.c_str(), hash_value.c_str()); + + auto hash_dir = PathExists(library_symlink_hash); + if (hash_dir.ok() && *hash_dir) { + // Compare file size for two library files with same name and hash + // value + auto existing_file_path = + library_symlink_hash + "/" + library_name.string(); + auto existing_file_size = GetFileSize(existing_file_path); + if (!existing_file_size.ok()) { + return existing_file_size.error(); + } + + auto new_file_path = + lib_items.path().string() + "/" + library_name.string(); + auto new_file_size = GetFileSize(new_file_path); + if (!new_file_size.ok()) { + return new_file_size.error(); + } + + if (*existing_file_size != *new_file_size) { + return Error() << "There are two libraries with same hash and " + "different file size : " + << existing_file_path << " and " << new_file_path; + } + + inner_iter = inner_iter.increment(ec); + if (ec) { + return Error() << "Failed to scan " << lib_entry.path().string() + << " : " << ec.message(); + } + continue; + } + std::filesystem::create_directory_symlink(lib_items.path(), + library_symlink_hash, ec); + if (ec) { + return Error() << "Failed to create symlink from " << lib_items.path() + << " to " << library_symlink_hash << ec.message(); + } + + inner_iter = inner_iter.increment(ec); + if (ec) { + return Error() << "Failed to scan " << lib_entry.path().string() + << " : " << ec.message(); + } + } + + iter = iter.increment(ec); + if (ec) { + return Error() << "Failed to scan " << apex_lib_path << " : " + << ec.message(); + } + } + } + + return {}; } -Result<void> activatePackageImpl(const ApexFile& apex_file) { +bool IsValidPackageName(const std::string& package_name) { + return kBannedApexName.count(package_name) == 0; +} + +Result<void> ActivatePackageImpl(const ApexFile& apex_file, + const std::string& device_name) { const ApexManifest& manifest = apex_file.GetManifest(); - if (gBootstrap && !isBootstrapApex(apex_file)) { - return {}; + if (!IsValidPackageName(manifest.name())) { + return Errorf("Package name {} is not allowed.", manifest.name()); + } + + // Validate upgraded shim apex + if (shim::IsShimApex(apex_file) && + !ApexFileRepository::GetInstance().IsPreInstalledApex(apex_file)) { + // This is not cheap for shim apex, but it is fine here since we have + // upgraded shim apex only during CTS tests. + Result<void> result = VerifyPackageBoot(apex_file); + if (!result.ok()) { + LOG(ERROR) << "Failed to validate shim apex: " << apex_file.GetPath(); + return result; + } } // See whether we think it's active, and do not allow to activate the same // version. Also detect whether this is the highest version. // We roll this into a single check. bool is_newest_version = true; - bool found_other_version = false; bool version_found_mounted = false; { uint64_t new_version = manifest.version(); bool version_found_active = false; gMountedApexes.ForallMountedApexes( manifest.name(), [&](const MountedApexData& data, bool latest) { - Result<ApexFile> otherApex = ApexFile::Open(data.full_path); - if (!otherApex.ok()) { + Result<ApexFile> other_apex = ApexFile::Open(data.full_path); + if (!other_apex.ok()) { return; } - found_other_version = true; - if (static_cast<uint64_t>(otherApex->GetManifest().version()) == + if (static_cast<uint64_t>(other_apex->GetManifest().version()) == new_version) { version_found_mounted = true; version_found_active = latest; } - if (static_cast<uint64_t>(otherApex->GetManifest().version()) > + if (static_cast<uint64_t>(other_apex->GetManifest().version()) > new_version) { is_newest_version = false; } }); - if (version_found_active) { + // If the package provides shared libraries to other APEXs, we need to + // activate all versions available (i.e. preloaded on /system/apex and + // available on /data/apex/active). The reason is that there might be some + // APEXs loaded from /system/apex that reference the libraries contained on + // the preloaded version of the apex providing shared libraries. + if (version_found_active && !manifest.providesharedapexlibs()) { LOG(DEBUG) << "Package " << manifest.name() << " with version " << manifest.version() << " already active"; return {}; } } - const std::string& mountPoint = apexd_private::GetPackageMountPoint(manifest); + const std::string& mount_point = + apexd_private::GetPackageMountPoint(manifest); if (!version_found_mounted) { - auto mountStatus = MountPackage(apex_file, mountPoint); - if (!mountStatus.ok()) { - return mountStatus; + auto mount_status = MountPackage(apex_file, mount_point, device_name); + if (!mount_status.ok()) { + return mount_status; } } - bool mounted_latest = false; - if (is_newest_version) { - const Result<void>& update_st = apexd_private::BindMount( - apexd_private::GetActiveMountPoint(manifest), mountPoint); - mounted_latest = update_st.has_value(); - if (!update_st.ok()) { - return Error() << "Failed to update package " << manifest.name() - << " to version " << manifest.version() << " : " - << update_st.error(); + // For packages providing shared libraries, avoid creating a bindmount since + // there is no use for the /apex/<package_name> directory. However, mark the + // highest version as latest so that the latest version of the package can be + // properly reported to PackageManager. + if (manifest.providesharedapexlibs()) { + if (is_newest_version) { + gMountedApexes.SetLatest(manifest.name(), apex_file.GetPath()); + } + } else { + bool mounted_latest = false; + // Bind mount the latest version to /apex/<package_name>, unless the + // package provides shared libraries to other APEXs. + if (is_newest_version) { + const Result<void>& update_st = apexd_private::BindMount( + apexd_private::GetActiveMountPoint(manifest), mount_point); + mounted_latest = update_st.has_value(); + if (!update_st.ok()) { + return Error() << "Failed to update package " << manifest.name() + << " to version " << manifest.version() << " : " + << update_st.error(); + } + } + if (mounted_latest) { + gMountedApexes.SetLatest(manifest.name(), apex_file.GetPath()); } } - if (mounted_latest) { - gMountedApexes.SetLatest(manifest.name(), apex_file.GetPath()); + + if (manifest.providesharedapexlibs()) { + const auto& handle_shared_libs_apex = + ActivateSharedLibsPackage(mount_point); + if (!handle_shared_libs_apex.ok()) { + return handle_shared_libs_apex; + } } LOG(DEBUG) << "Successfully activated " << apex_file.GetPath() @@ -1013,28 +1335,30 @@ Result<void> activatePackageImpl(const ApexFile& apex_file) { return {}; } -Result<void> activatePackage(const std::string& full_path) { +Result<void> ActivatePackage(const std::string& full_path) { LOG(INFO) << "Trying to activate " << full_path; Result<ApexFile> apex_file = ApexFile::Open(full_path); if (!apex_file.ok()) { return apex_file.error(); } - return activatePackageImpl(*apex_file); + return ActivatePackageImpl(*apex_file, + GetPackageId(apex_file->GetManifest())); } -Result<void> deactivatePackage(const std::string& full_path) { +Result<void> DeactivatePackage(const std::string& full_path) { LOG(INFO) << "Trying to deactivate " << full_path; - Result<ApexFile> apexFile = ApexFile::Open(full_path); - if (!apexFile.ok()) { - return apexFile.error(); + Result<ApexFile> apex_file = ApexFile::Open(full_path); + if (!apex_file.ok()) { + return apex_file.error(); } - return UnmountPackage(*apexFile, /* allow_latest= */ true); + return UnmountPackage(*apex_file, /* allow_latest= */ true, + /* deferred= */ false); } -std::vector<ApexFile> getActivePackages() { +std::vector<ApexFile> GetActivePackages() { std::vector<ApexFile> ret; gMountedApexes.ForallMountedApexes( [&](const std::string&, const MountedApexData& data, bool latest) { @@ -1042,20 +1366,84 @@ std::vector<ApexFile> getActivePackages() { return; } - Result<ApexFile> apexFile = ApexFile::Open(data.full_path); - if (!apexFile.ok()) { - // TODO: Fail? + Result<ApexFile> apex_file = ApexFile::Open(data.full_path); + if (!apex_file.ok()) { return; } - ret.emplace_back(std::move(*apexFile)); + ret.emplace_back(std::move(*apex_file)); }); return ret; } +std::vector<ApexFile> CalculateInactivePackages( + const std::vector<ApexFile>& active) { + std::vector<ApexFile> inactive = GetFactoryPackages(); + auto new_end = std::remove_if( + inactive.begin(), inactive.end(), [&active](const ApexFile& apex) { + return std::any_of(active.begin(), active.end(), + [&apex](const ApexFile& active_apex) { + return apex.GetPath() == active_apex.GetPath(); + }); + }); + inactive.erase(new_end, inactive.end()); + return std::move(inactive); +} + +Result<void> EmitApexInfoList(bool is_bootstrap) { + // on a non-updatable device, we don't have APEX database to emit + if (!android::sysprop::ApexProperties::updatable().value_or(false)) { + return {}; + } + + // Apexd runs both in "bootstrap" and "default" mount namespace. + // To expose /apex/apex-info-list.xml separately in each mount namespaces, + // we write /apex/.<namespace>-apex-info-list .xml file first and then + // bind mount it to the canonical file (/apex/apex-info-list.xml). + const std::string file_name = + fmt::format("{}/.{}-{}", kApexRoot, + is_bootstrap ? "bootstrap" : "default", kApexInfoList); + + unique_fd fd(TEMP_FAILURE_RETRY( + open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644))); + if (fd.get() == -1) { + return ErrnoErrorf("Can't open {}", file_name); + } + + const std::vector<ApexFile> active(GetActivePackages()); + + std::vector<ApexFile> inactive; + // we skip for non-activated built-in apexes in bootstrap mode + // in order to avoid boottime increase + if (!is_bootstrap) { + inactive = CalculateInactivePackages(active); + } + + std::stringstream xml; + CollectApexInfoList(xml, active, inactive); + + if (!android::base::WriteStringToFd(xml.str(), fd)) { + return ErrnoErrorf("Can't write to {}", file_name); + } + + fd.reset(); + + const std::string mount_point = + fmt::format("{}/{}", kApexRoot, kApexInfoList); + if (access(mount_point.c_str(), F_OK) != 0) { + close(open(mount_point.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, + 0644)); + } + if (mount(file_name.c_str(), mount_point.c_str(), nullptr, MS_BIND, + nullptr) == -1) { + return ErrnoErrorf("Can't bind mount {} to {}", file_name, mount_point); + } + return RestoreconPath(file_name); +} + namespace { std::unordered_map<std::string, uint64_t> GetActivePackagesMap() { - std::vector<ApexFile> active_packages = getActivePackages(); + std::vector<ApexFile> active_packages = GetActivePackages(); std::unordered_map<std::string, uint64_t> ret; for (const auto& package : active_packages) { const ApexManifest& manifest = package.GetManifest(); @@ -1066,28 +1454,50 @@ std::unordered_map<std::string, uint64_t> GetActivePackagesMap() { } // namespace -std::vector<ApexFile> getFactoryPackages() { +std::vector<ApexFile> GetFactoryPackages() { std::vector<ApexFile> ret; - for (const auto& dir : kApexPackageBuiltinDirs) { - auto apex_files = FindApexFilesByName(dir); - if (!apex_files.ok()) { - LOG(ERROR) << apex_files.error(); + + // Decompressed APEX is considered factory package + std::vector<std::string> decompressed_pkg_names; + auto active_pkgs = GetActivePackages(); + for (ApexFile& apex : active_pkgs) { + if (ApexFileRepository::GetInstance().IsDecompressedApex(apex)) { + decompressed_pkg_names.push_back(apex.GetManifest().name()); + ret.emplace_back(std::move(apex)); + } + } + + for (const auto& dir : gConfig->apex_built_in_dirs) { + auto all_apex_files = FindFilesBySuffix( + dir, {kApexPackageSuffix, kCompressedApexPackageSuffix}); + if (!all_apex_files.ok()) { + LOG(ERROR) << all_apex_files.error(); continue; } - for (const std::string& path : *apex_files) { + + for (const std::string& path : *all_apex_files) { Result<ApexFile> apex_file = ApexFile::Open(path); if (!apex_file.ok()) { LOG(ERROR) << apex_file.error(); - } else { - ret.emplace_back(std::move(*apex_file)); + continue; + } + // Ignore compressed APEX if it has been decompressed already + if (apex_file->IsCompressed() && + std::find(decompressed_pkg_names.begin(), + decompressed_pkg_names.end(), + apex_file->GetManifest().name()) != + decompressed_pkg_names.end()) { + continue; } + + ret.emplace_back(std::move(*apex_file)); } } return ret; } -Result<ApexFile> getActivePackage(const std::string& packageName) { - std::vector<ApexFile> packages = getActivePackages(); +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); @@ -1102,7 +1512,7 @@ Result<ApexFile> getActivePackage(const std::string& packageName) { * * Returns without error only if session was successfully aborted. **/ -Result<void> abortStagedSession(int session_id) { +Result<void> AbortStagedSession(int session_id) { auto session = ApexSession::GetSession(session_id); if (!session.ok()) { return Error() << "No session found with id " << session_id; @@ -1117,23 +1527,29 @@ Result<void> abortStagedSession(int session_id) { } } -// TODO(ioffe): cleanup activation logic to avoid unnecessary scanning. namespace { -Result<std::vector<ApexFile>> ScanApexFiles(const char* apex_package_dir) { +// TODO(b/179497746): Avoid scanning apex directly here +// Only used in OnBootstrap. Should we remove this function? +Result<std::vector<ApexFile>> ScanApexFiles(const char* apex_package_dir, + bool include_compressed = false) { LOG(INFO) << "Scanning " << apex_package_dir << " looking for APEX packages."; if (access(apex_package_dir, F_OK) != 0 && errno == ENOENT) { LOG(INFO) << "... does not exist. Skipping"; return {}; } - Result<std::vector<std::string>> scan = FindApexFilesByName(apex_package_dir); + std::vector<std::string> suffix_list = {kApexPackageSuffix}; + if (include_compressed) { + suffix_list.push_back(kCompressedApexPackageSuffix); + } + Result<std::vector<std::string>> scan = + FindFilesBySuffix(apex_package_dir, suffix_list); if (!scan.ok()) { return Error() << "Failed to scan " << apex_package_dir << " : " << scan.error(); } std::vector<ApexFile> ret; for (const auto& name : *scan) { - LOG(INFO) << "Found " << name; Result<ApexFile> apex_file = ApexFile::Open(name); if (!apex_file.ok()) { LOG(ERROR) << "Failed to scan " << name << " : " << apex_file.error(); @@ -1144,64 +1560,152 @@ Result<std::vector<ApexFile>> ScanApexFiles(const char* apex_package_dir) { return ret; } -Result<void> ActivateApexPackages(const std::vector<ApexFile>& apexes) { - const auto& packages_with_code = GetActivePackagesMap(); - size_t failed_cnt = 0; - size_t skipped_cnt = 0; - size_t activated_cnt = 0; - for (const auto& apex : apexes) { - uint64_t new_version = static_cast<uint64_t>(apex.GetManifest().version()); - const auto& it = packages_with_code.find(apex.GetManifest().name()); - if (it != packages_with_code.end() && it->second >= new_version) { - LOG(INFO) << "Skipping activation of " << apex.GetPath() - << " same package with higher version " << it->second - << " is already active"; - skipped_cnt++; - continue; +std::vector<Result<void>> ActivateApexWorker( + bool is_ota_chroot, std::queue<const ApexFile*>& apex_queue, + std::mutex& mutex) { + std::vector<Result<void>> ret; + + while (true) { + const ApexFile* apex; + { + std::lock_guard lock(mutex); + if (apex_queue.empty()) break; + apex = apex_queue.front(); + apex_queue.pop(); } - if (auto res = activatePackageImpl(apex); !res.ok()) { - LOG(ERROR) << "Failed to activate " << apex.GetPath() << " : " - << res.error(); - failed_cnt++; + std::string device_name = GetPackageId(apex->GetManifest()); + if (is_ota_chroot) { + device_name += ".chroot"; + } + if (auto res = ActivatePackageImpl(*apex, device_name); !res.ok()) { + ret.push_back(Error() << "Failed to activate " << apex->GetPath() << " : " + << res.error()); } else { - activated_cnt++; + ret.push_back({}); } } + + return ret; +} + +Result<void> ActivateApexPackages(const std::vector<ApexFileRef>& apexes, + bool is_ota_chroot) { + std::queue<const ApexFile*> apex_queue; + std::mutex apex_queue_mutex; + + for (const ApexFile& apex : apexes) { + apex_queue.emplace(&apex); + } + + // Creates threads as many as half number of cores for the performance. + size_t worker_num = std::max(get_nprocs_conf() >> 1, 1); + worker_num = std::min(apex_queue.size(), worker_num); + + // On -eng builds there might be two different pre-installed art apexes. + // Attempting to activate them in parallel will result in UB (e.g. + // apexd-bootstrap might crash). In order to avoid this, for the time being on + // -eng builds activate apexes sequentially. + // TODO(b/176497601): remove this. + if (GetProperty("ro.build.type", "") == "eng") { + worker_num = 1; + } + + std::vector<std::future<std::vector<Result<void>>>> futures; + futures.reserve(worker_num); + for (size_t i = 0; i < worker_num; i++) { + futures.push_back(std::async(std::launch::async, ActivateApexWorker, + std::ref(is_ota_chroot), std::ref(apex_queue), + std::ref(apex_queue_mutex))); + } + + size_t activated_cnt = 0; + size_t failed_cnt = 0; + std::string error_message; + for (size_t i = 0; i < futures.size(); i++) { + for (const auto& res : futures[i].get()) { + if (res.ok()) { + ++activated_cnt; + } else { + ++failed_cnt; + LOG(ERROR) << res.error(); + if (failed_cnt == 1) { + error_message = res.error().message(); + } + } + } + } + if (failed_cnt > 0) { - return Error() << "Failed to activate " << failed_cnt << " APEX packages"; + return Error() << "Failed to activate " << failed_cnt + << " APEX packages. One of the errors: " << error_message; } - LOG(INFO) << "Activated " << activated_cnt - << " packages. Skipped: " << skipped_cnt; + LOG(INFO) << "Activated " << activated_cnt << " packages."; return {}; } -bool ShouldActivateApexOnData(const ApexFile& apex) { - return HasPreInstalledVersion(apex.GetManifest().name()); -} - -} // namespace +// A fallback function in case some of the apexes failed to activate. For all +// such apexes that were coming from /data partition we will attempt to activate +// their corresponding pre-installed copies. +Result<void> ActivateMissingApexes(const std::vector<ApexFileRef>& apexes, + bool is_ota_chroot) { + LOG(INFO) << "Trying to activate pre-installed versions of missing apexes"; + const auto& file_repository = ApexFileRepository::GetInstance(); + const auto& activated_apexes = GetActivePackagesMap(); + std::vector<ApexFileRef> fallback_apexes; + for (const auto& apex_ref : apexes) { + const auto& apex = apex_ref.get(); + if (apex.GetManifest().providesharedapexlibs()) { + // We must mount both versions of sharedlibs apex anyway. Not much we can + // do here. + continue; + } + if (file_repository.IsPreInstalledApex(apex)) { + // We tried to activate pre-installed apex in the first place. No need to + // try again. + continue; + } + const std::string& name = apex.GetManifest().name(); + if (activated_apexes.find(name) == activated_apexes.end()) { + fallback_apexes.push_back(file_repository.GetPreInstalledApex(name)); + } + } -Result<void> scanPackagesDirAndActivate(const char* apex_package_dir) { - auto apexes = ScanApexFiles(apex_package_dir); - if (!apexes) { - return apexes.error(); + // Process compressed APEX, if any + std::vector<ApexFileRef> compressed_apex; + for (auto it = fallback_apexes.begin(); it != fallback_apexes.end();) { + if (it->get().IsCompressed()) { + compressed_apex.emplace_back(*it); + it = fallback_apexes.erase(it); + } else { + it++; + } + } + std::vector<ApexFile> decompressed_apex; + if (!compressed_apex.empty()) { + decompressed_apex = + ProcessCompressedApex(compressed_apex, /* is_ota_chroot= */ false); + for (const ApexFile& apex_file : decompressed_apex) { + fallback_apexes.emplace_back(std::cref(apex_file)); + } } - return ActivateApexPackages(*apexes); + return ActivateApexPackages(fallback_apexes, is_ota_chroot); } +} // namespace + /** * Snapshots data from base_dir/apexdata/<apex name> to * base_dir/apexrollback/<rollback id>/<apex name>. */ -Result<void> snapshotDataDirectory(const std::string& base_dir, +Result<void> SnapshotDataDirectory(const std::string& base_dir, const int rollback_id, const std::string& apex_name, bool pre_restore = false) { auto rollback_path = StringPrintf("%s/%s/%d%s", base_dir.c_str(), kApexSnapshotSubDir, rollback_id, pre_restore ? kPreRestoreSuffix : ""); - const Result<void> result = createDirIfNeeded(rollback_path, 0700); + const Result<void> result = CreateDirIfNeeded(rollback_path, 0700); if (!result.ok()) { return Error() << "Failed to create snapshot directory for rollback " << rollback_id << " : " << result.error(); @@ -1219,7 +1723,7 @@ Result<void> snapshotDataDirectory(const std::string& base_dir, * to base_dir/apexdata/<apex name>. * Note the snapshot will be deleted after restoration succeeded. */ -Result<void> restoreDataDirectory(const std::string& base_dir, +Result<void> RestoreDataDirectory(const std::string& base_dir, const int rollback_id, const std::string& apex_name, bool pre_restore = false) { @@ -1243,13 +1747,13 @@ Result<void> restoreDataDirectory(const std::string& base_dir, return {}; } -void snapshotOrRestoreDeIfNeeded(const std::string& base_dir, +void SnapshotOrRestoreDeIfNeeded(const std::string& base_dir, const ApexSession& session) { if (session.HasRollbackEnabled()) { for (const auto& apex_name : session.GetApexNames()) { Result<void> result = - snapshotDataDirectory(base_dir, session.GetRollbackId(), apex_name); - if (!result) { + SnapshotDataDirectory(base_dir, session.GetRollbackId(), apex_name); + if (!result.ok()) { LOG(ERROR) << "Snapshot failed for " << apex_name << ": " << result.error(); } @@ -1258,11 +1762,11 @@ void snapshotOrRestoreDeIfNeeded(const std::string& base_dir, for (const auto& apex_name : session.GetApexNames()) { if (!gInFsCheckpointMode) { // Snapshot before restore so this rollback can be reverted. - snapshotDataDirectory(base_dir, session.GetRollbackId(), apex_name, + SnapshotDataDirectory(base_dir, session.GetRollbackId(), apex_name, true /* pre_restore */); } Result<void> result = - restoreDataDirectory(base_dir, session.GetRollbackId(), apex_name); + RestoreDataDirectory(base_dir, session.GetRollbackId(), apex_name); if (!result.ok()) { LOG(ERROR) << "Restore of data failed for " << apex_name << ": " << result.error(); @@ -1271,18 +1775,18 @@ void snapshotOrRestoreDeIfNeeded(const std::string& base_dir, } } -void snapshotOrRestoreDeSysData() { +void SnapshotOrRestoreDeSysData() { auto sessions = ApexSession::GetSessionsInState(SessionState::ACTIVATED); for (const ApexSession& session : sessions) { - snapshotOrRestoreDeIfNeeded(kDeSysDataDir, session); + SnapshotOrRestoreDeIfNeeded(kDeSysDataDir, session); } } -int snapshotOrRestoreDeUserData() { +int SnapshotOrRestoreDeUserData() { auto user_dirs = GetDeUserDirs(); - if (!user_dirs) { + if (!user_dirs.ok()) { LOG(ERROR) << "Error reading dirs " << user_dirs.error(); return 1; } @@ -1291,93 +1795,69 @@ int snapshotOrRestoreDeUserData() { for (const ApexSession& session : sessions) { for (const auto& user_dir : *user_dirs) { - snapshotOrRestoreDeIfNeeded(user_dir, session); + SnapshotOrRestoreDeIfNeeded(user_dir, session); } } return 0; } -Result<ino_t> snapshotCeData(const int user_id, const int rollback_id, - const std::string& apex_name) { +Result<void> SnapshotCeData(const int user_id, const int rollback_id, + const std::string& apex_name) { auto base_dir = StringPrintf("%s/%d", kCeDataDir, user_id); - Result<void> result = snapshotDataDirectory(base_dir, rollback_id, apex_name); - if (!result) { - return result.error(); - } - auto ce_snapshot_path = - StringPrintf("%s/%s/%d/%s", base_dir.c_str(), kApexSnapshotSubDir, - rollback_id, apex_name.c_str()); - return get_path_inode(ce_snapshot_path); + return SnapshotDataDirectory(base_dir, rollback_id, apex_name); } -Result<void> restoreCeData(const int user_id, const int rollback_id, +Result<void> RestoreCeData(const int user_id, const int rollback_id, const std::string& apex_name) { auto base_dir = StringPrintf("%s/%d", kCeDataDir, user_id); - return restoreDataDirectory(base_dir, rollback_id, apex_name); + return RestoreDataDirectory(base_dir, rollback_id, apex_name); } // Migrates sessions directory from /data/apex/sessions to // /metadata/apex/sessions, if necessary. -Result<void> migrateSessionsDirIfNeeded() { - namespace fs = std::filesystem; - auto from_path = std::string(kApexDataDir) + "/sessions"; - auto exists = PathExists(from_path); - if (!exists) { - return Error() << "Failed to access " << from_path << ": " - << exists.error(); - } - if (!*exists) { - LOG(DEBUG) << from_path << " does not exist. Nothing to migrate."; - return {}; - } - auto to_path = kApexSessionsDir; - std::error_code error_code; - fs::copy(from_path, to_path, fs::copy_options::recursive, error_code); - if (error_code) { - return Error() << "Failed to copy old sessions directory" - << error_code.message(); - } - fs::remove_all(from_path, error_code); - if (error_code) { - return Error() << "Failed to delete old sessions directory " - << error_code.message(); - } - return {}; +Result<void> MigrateSessionsDirIfNeeded() { + return ApexSession::MigrateToMetadataSessionsDir(); } -Result<void> destroySnapshots(const std::string& base_dir, +Result<void> DestroySnapshots(const std::string& base_dir, const int rollback_id) { auto path = StringPrintf("%s/%s/%d", base_dir.c_str(), kApexSnapshotSubDir, rollback_id); return DeleteDir(path); } -Result<void> destroyDeSnapshots(const int rollback_id) { - destroySnapshots(kDeSysDataDir, rollback_id); +Result<void> DestroyDeSnapshots(const int rollback_id) { + DestroySnapshots(kDeSysDataDir, rollback_id); auto user_dirs = GetDeUserDirs(); - if (!user_dirs) { + if (!user_dirs.ok()) { return Error() << "Error reading user dirs " << user_dirs.error(); } for (const auto& user_dir : *user_dirs) { - destroySnapshots(user_dir, rollback_id); + DestroySnapshots(user_dir, rollback_id); } return {}; } +Result<void> DestroyCeSnapshots(const int user_id, const int rollback_id) { + auto path = StringPrintf("%s/%d/%s/%d", kCeDataDir, user_id, + kApexSnapshotSubDir, rollback_id); + return DeleteDir(path); +} + /** * Deletes all credential-encrypted snapshots for the given user, except for * those listed in retain_rollback_ids. */ -Result<void> destroyCeSnapshotsNotSpecified( +Result<void> DestroyCeSnapshotsNotSpecified( int user_id, const std::vector<int>& retain_rollback_ids) { auto snapshot_root = StringPrintf("%s/%d/%s", kCeDataDir, user_id, kApexSnapshotSubDir); auto snapshot_dirs = GetSubdirs(snapshot_root); - if (!snapshot_dirs) { + if (!snapshot_dirs.ok()) { return Error() << "Error reading snapshot dirs " << snapshot_dirs.error(); } @@ -1389,7 +1869,7 @@ Result<void> destroyCeSnapshotsNotSpecified( std::find(retain_rollback_ids.begin(), retain_rollback_ids.end(), snapshot_id) == retain_rollback_ids.end()) { Result<void> result = DeleteDir(snapshot_dir); - if (!result) { + if (!result.ok()) { return Error() << "Destroy CE snapshot failed for " << snapshot_dir << " : " << result.error(); } @@ -1398,16 +1878,16 @@ Result<void> destroyCeSnapshotsNotSpecified( return {}; } -void restorePreRestoreSnapshotsIfPresent(const std::string& base_dir, +void RestorePreRestoreSnapshotsIfPresent(const std::string& base_dir, const ApexSession& session) { auto pre_restore_snapshot_path = StringPrintf("%s/%s/%d%s", base_dir.c_str(), kApexSnapshotSubDir, session.GetRollbackId(), kPreRestoreSuffix); - if (PathExists(pre_restore_snapshot_path)) { + if (PathExists(pre_restore_snapshot_path).ok()) { for (const auto& apex_name : session.GetApexNames()) { - Result<void> result = restoreDataDirectory( + Result<void> result = RestoreDataDirectory( base_dir, session.GetRollbackId(), apex_name, true /* pre_restore */); - if (!result) { + if (!result.ok()) { LOG(ERROR) << "Restore of pre-restore snapshot failed for " << apex_name << ": " << result.error(); } @@ -1415,71 +1895,77 @@ void restorePreRestoreSnapshotsIfPresent(const std::string& base_dir, } } -void restoreDePreRestoreSnapshotsIfPresent(const ApexSession& session) { - restorePreRestoreSnapshotsIfPresent(kDeSysDataDir, session); +void RestoreDePreRestoreSnapshotsIfPresent(const ApexSession& session) { + RestorePreRestoreSnapshotsIfPresent(kDeSysDataDir, session); auto user_dirs = GetDeUserDirs(); - if (!user_dirs) { + if (!user_dirs.ok()) { LOG(ERROR) << "Error reading user dirs to restore pre-restore snapshots" << user_dirs.error(); } for (const auto& user_dir : *user_dirs) { - restorePreRestoreSnapshotsIfPresent(user_dir, session); + RestorePreRestoreSnapshotsIfPresent(user_dir, session); } } -void deleteDePreRestoreSnapshots(const std::string& base_dir, +void DeleteDePreRestoreSnapshots(const std::string& base_dir, const ApexSession& session) { auto pre_restore_snapshot_path = StringPrintf("%s/%s/%d%s", base_dir.c_str(), kApexSnapshotSubDir, session.GetRollbackId(), kPreRestoreSuffix); Result<void> result = DeleteDir(pre_restore_snapshot_path); - if (!result) { + if (!result.ok()) { LOG(ERROR) << "Deletion of pre-restore snapshot failed: " << result.error(); } } -void deleteDePreRestoreSnapshots(const ApexSession& session) { - deleteDePreRestoreSnapshots(kDeSysDataDir, session); +void DeleteDePreRestoreSnapshots(const ApexSession& session) { + DeleteDePreRestoreSnapshots(kDeSysDataDir, session); auto user_dirs = GetDeUserDirs(); - if (!user_dirs) { + if (!user_dirs.ok()) { LOG(ERROR) << "Error reading user dirs to delete pre-restore snapshots" << user_dirs.error(); } for (const auto& user_dir : *user_dirs) { - deleteDePreRestoreSnapshots(user_dir, session); + DeleteDePreRestoreSnapshots(user_dir, session); } } -void scanStagedSessionsDirAndStage() { - LOG(INFO) << "Scanning " << kApexSessionsDir +void OnBootCompleted() { + ApexdLifecycle::GetInstance().MarkBootCompleted(); + BootCompletedCleanup(); +} + +// Returns true if any session gets staged +void ScanStagedSessionsDirAndStage() { + LOG(INFO) << "Scanning " << ApexSession::GetSessionsDir() << " looking for sessions to be activated."; - auto sessionsToActivate = + auto sessions_to_activate = ApexSession::GetSessionsInState(SessionState::STAGED); if (gSupportsFsCheckpoints) { // A session that is in the ACTIVATED state should still be re-activated if // fs checkpointing is supported. In this case, a session may be in the // ACTIVATED state yet the data/apex/active directory may have been // reverted. The session should be reverted in this scenario. - auto activatedSessions = + auto activated_sessions = ApexSession::GetSessionsInState(SessionState::ACTIVATED); - sessionsToActivate.insert(sessionsToActivate.end(), - activatedSessions.begin(), - activatedSessions.end()); + sessions_to_activate.insert(sessions_to_activate.end(), + activated_sessions.begin(), + activated_sessions.end()); } - for (auto& session : sessionsToActivate) { - auto sessionId = session.GetId(); + for (auto& session : sessions_to_activate) { + auto session_id = session.GetId(); auto session_failed_fn = [&]() { - LOG(WARNING) << "Marking session " << sessionId << " as failed."; + LOG(WARNING) << "Marking session " << session_id << " as failed."; auto st = session.UpdateStateAndCommit(SessionState::ACTIVATION_FAILED); if (!st.ok()) { - LOG(WARNING) << "Failed to mark session " << sessionId + LOG(WARNING) << "Failed to mark session " << session_id << " as failed : " << st.error(); } }; @@ -1487,74 +1973,90 @@ void scanStagedSessionsDirAndStage() { std::string build_fingerprint = GetProperty(kBuildFingerprintSysprop, ""); if (session.GetBuildFingerprint().compare(build_fingerprint) != 0) { - LOG(ERROR) << "APEX build fingerprint has changed"; + auto error_message = "APEX build fingerprint has changed"; + LOG(ERROR) << error_message; + session.SetErrorMessage(error_message); continue; } - std::vector<std::string> dirsToScan; + std::vector<std::string> dirs_to_scan; if (session.GetChildSessionIds().empty()) { - dirsToScan.push_back(std::string(kStagedSessionsDir) + "/session_" + - std::to_string(sessionId)); + dirs_to_scan.push_back(std::string(gConfig->staged_session_dir) + + "/session_" + std::to_string(session_id)); } else { - for (auto childSessionId : session.GetChildSessionIds()) { - dirsToScan.push_back(std::string(kStagedSessionsDir) + "/session_" + - std::to_string(childSessionId)); + for (auto child_session_id : session.GetChildSessionIds()) { + dirs_to_scan.push_back(std::string(gConfig->staged_session_dir) + + "/session_" + std::to_string(child_session_id)); } } std::vector<std::string> apexes; - bool scanSuccessful = true; - for (const auto& dirToScan : dirsToScan) { - Result<std::vector<std::string>> scan = FindApexFilesByName(dirToScan); + bool scan_successful = true; + for (const auto& dir_to_scan : dirs_to_scan) { + Result<std::vector<std::string>> scan = + FindFilesBySuffix(dir_to_scan, {kApexPackageSuffix}); if (!scan.ok()) { LOG(WARNING) << scan.error(); - scanSuccessful = false; + session.SetErrorMessage(scan.error().message()); + scan_successful = false; break; } if (scan->size() > 1) { - LOG(WARNING) << "More than one APEX package found in the same session " - << "directory " << dirToScan << ", skipping activation."; - scanSuccessful = false; + std::string error_message = StringPrintf( + "More than one APEX package found in the same session directory %s " + ", skipping activation", + dir_to_scan.c_str()); + LOG(WARNING) << error_message; + session.SetErrorMessage(error_message); + scan_successful = false; break; } if (scan->empty()) { - LOG(WARNING) << "No APEX packages found while scanning " << dirToScan - << " session id: " << sessionId << "."; - scanSuccessful = false; + std::string error_message = StringPrintf( + "No APEX packages found while scanning %s session id: %d.", + dir_to_scan.c_str(), session_id); + LOG(WARNING) << error_message; + session.SetErrorMessage(error_message); + scan_successful = false; break; } apexes.push_back(std::move((*scan)[0])); } - if (!scanSuccessful) { + if (!scan_successful) { continue; } // Run postinstall, if necessary. - Result<void> postinstall_status = postinstallPackages(apexes); + Result<void> postinstall_status = PostinstallPackages(apexes); if (!postinstall_status.ok()) { - LOG(ERROR) << "Postinstall failed for session " - << std::to_string(sessionId) << ": " - << postinstall_status.error(); + std::string error_message = + StringPrintf("Postinstall failed for session %d %s", session_id, + postinstall_status.error().message().c_str()); + LOG(ERROR) << error_message; + session.SetErrorMessage(error_message); continue; } for (const auto& apex : apexes) { - // TODO: Avoid opening ApexFile repeatedly. + // TODO(b/158470836): Avoid opening ApexFile repeatedly. Result<ApexFile> apex_file = ApexFile::Open(apex); - if (!apex_file) { + if (!apex_file.ok()) { LOG(ERROR) << "Cannot open apex file during staging: " << apex; continue; } session.AddApexName(apex_file->GetManifest().name()); } - const Result<void> result = stagePackages(apexes); + const Result<void> result = StagePackages(apexes); if (!result.ok()) { - LOG(ERROR) << "Activation failed for packages " << Join(apexes, ',') - << ": " << result.error(); + std::string error_message = StringPrintf( + "Activation failed for packages %s : %s", Join(apexes, ',').c_str(), + result.error().message().c_str()); + LOG(ERROR) << error_message; + session.SetErrorMessage(error_message); continue; } @@ -1569,49 +2071,61 @@ void scanStagedSessionsDirAndStage() { } } -Result<void> preinstallPackages(const std::vector<std::string>& paths) { - if (paths.empty()) { - return Errorf("Empty set of inputs"); +Result<void> PreinstallPackages(const std::vector<std::string>& paths) { + Result<std::vector<ApexFile>> apex_files = OpenApexFiles(paths); + if (!apex_files.ok()) { + return apex_files.error(); } - LOG(DEBUG) << "preinstallPackages() for " << Join(paths, ','); - return HandlePackages<Result<void>>(paths, PreinstallPackages); + LOG(DEBUG) << "PreinstallPackages() for " << Join(paths, ','); + return PreinstallPackages(*apex_files); } -Result<void> postinstallPackages(const std::vector<std::string>& paths) { - if (paths.empty()) { - return Errorf("Empty set of inputs"); +Result<void> PostinstallPackages(const std::vector<std::string>& paths) { + Result<std::vector<ApexFile>> apex_files = OpenApexFiles(paths); + if (!apex_files.ok()) { + return apex_files.error(); } - LOG(DEBUG) << "postinstallPackages() for " << Join(paths, ','); - return HandlePackages<Result<void>>(paths, PostinstallPackages); + LOG(DEBUG) << "PostinstallPackages() for " << Join(paths, ','); + return PostinstallPackages(*apex_files); } namespace { std::string StageDestPath(const ApexFile& apex_file) { - return StringPrintf("%s/%s%s", kActiveApexPackagesDataDir, + return StringPrintf("%s/%s%s", gConfig->active_apex_data_dir, GetPackageId(apex_file.GetManifest()).c_str(), kApexPackageSuffix); } } // namespace -Result<void> stagePackages(const std::vector<std::string>& tmpPaths) { - if (tmpPaths.empty()) { +Result<void> StagePackages(const std::vector<std::string>& tmp_paths) { + if (tmp_paths.empty()) { return Errorf("Empty set of inputs"); } - LOG(DEBUG) << "stagePackages() for " << Join(tmpPaths, ','); + LOG(DEBUG) << "StagePackages() for " << Join(tmp_paths, ','); // Note: this function is temporary. As such the code is not optimized, e.g., // it will open ApexFiles multiple times. // 1) Verify all packages. - auto verify_status = verifyPackages(tmpPaths, VerifyPackageBoot); - if (!verify_status.ok()) { - return verify_status.error(); + Result<std::vector<ApexFile>> apex_files = OpenApexFiles(tmp_paths); + if (!apex_files.ok()) { + return apex_files.error(); + } + for (const ApexFile& apex_file : *apex_files) { + if (shim::IsShimApex(apex_file)) { + // Shim apex will be validated on every boot. No need to do it here. + continue; + } + Result<void> result = VerifyPackageBoot(apex_file); + if (!result.ok()) { + return result.error(); + } } // Make sure that kActiveApexPackagesDataDir exists. auto create_dir_status = - createDirIfNeeded(std::string(kActiveApexPackagesDataDir), 0755); + CreateDirIfNeeded(std::string(gConfig->active_apex_data_dir), 0755); if (!create_dir_status.ok()) { return create_dir_status.error(); } @@ -1636,16 +2150,12 @@ Result<void> stagePackages(const std::vector<std::string>& tmpPaths) { auto scope_guard = android::base::make_scope_guard(deleter); std::unordered_set<std::string> staged_packages; - for (const std::string& path : tmpPaths) { - Result<ApexFile> apex_file = ApexFile::Open(path); - if (!apex_file.ok()) { - return apex_file.error(); - } + for (const ApexFile& apex_file : *apex_files) { // First promote new hashtree file to the one that will be used when // mounting apex. - std::string new_hashtree_file = GetHashTreeFileName(*apex_file, + std::string new_hashtree_file = GetHashTreeFileName(apex_file, /* is_new = */ true); - std::string old_hashtree_file = GetHashTreeFileName(*apex_file, + std::string old_hashtree_file = GetHashTreeFileName(apex_file, /* is_new = */ false); if (access(new_hashtree_file.c_str(), F_OK) == 0) { if (TEMP_FAILURE_RETRY(rename(new_hashtree_file.c_str(), @@ -1656,7 +2166,7 @@ Result<void> stagePackages(const std::vector<std::string>& tmpPaths) { changed_hashtree_files.emplace_back(std::move(old_hashtree_file)); } // And only then move apex to /data/apex/active. - std::string dest_path = StageDestPath(*apex_file); + std::string dest_path = StageDestPath(apex_file); if (access(dest_path.c_str(), F_OK) == 0) { LOG(DEBUG) << dest_path << " already exists. Deleting"; if (TEMP_FAILURE_RETRY(unlink(dest_path.c_str())) != 0) { @@ -1664,15 +2174,14 @@ Result<void> stagePackages(const std::vector<std::string>& tmpPaths) { } } - if (link(apex_file->GetPath().c_str(), dest_path.c_str()) != 0) { - // TODO: Get correct binder error status. - return ErrnoError() << "Unable to link " << apex_file->GetPath() << " to " + if (link(apex_file.GetPath().c_str(), dest_path.c_str()) != 0) { + return ErrnoError() << "Unable to link " << apex_file.GetPath() << " to " << dest_path; } staged_files.insert(dest_path); - staged_packages.insert(apex_file->GetManifest().name()); + staged_packages.insert(apex_file.GetManifest().name()); - LOG(DEBUG) << "Success linking " << apex_file->GetPath() << " to " + LOG(DEBUG) << "Success linking " << apex_file.GetPath() << " to " << dest_path; } @@ -1681,18 +2190,19 @@ Result<void> stagePackages(const std::vector<std::string>& tmpPaths) { return RemovePreviouslyActiveApexFiles(staged_packages, staged_files); } -Result<void> unstagePackages(const std::vector<std::string>& paths) { +Result<void> UnstagePackages(const std::vector<std::string>& paths) { if (paths.empty()) { return Errorf("Empty set of inputs"); } - LOG(DEBUG) << "unstagePackages() for " << Join(paths, ','); - - // TODO: to make unstage safer, we can copy to be unstaged packages to a - // temporary folder and restore state from it in case unstagePackages fails. + LOG(DEBUG) << "UnstagePackages() for " << Join(paths, ','); for (const std::string& path : paths) { - if (access(path.c_str(), F_OK) != 0) { - return ErrnoError() << "Can't access " << path; + auto apex = ApexFile::Open(path); + if (!apex.ok()) { + return apex.error(); + } + if (ApexFileRepository::GetInstance().IsPreInstalledApex(*apex)) { + return Error() << "Can't uninstall pre-installed apex " << path; } } @@ -1714,54 +2224,57 @@ Result<void> unstagePackages(const std::vector<std::string>& paths) { * Also, we need to put staged sessions in /data/apex/sessions in REVERTED state * so that they do not get activated on next reboot. */ -Result<void> revertActiveSessions(const std::string& crashing_native_process) { +Result<void> RevertActiveSessions(const std::string& crashing_native_process, + const std::string& error_message) { // First check whenever there is anything to revert. If there is none, then // fail. This prevents apexd from boot looping a device in case a native // process is crashing and there are no apex updates. - auto activeSessions = ApexSession::GetActiveSessions(); - if (activeSessions.empty()) { + auto active_sessions = ApexSession::GetActiveSessions(); + if (active_sessions.empty()) { return Error() << "Revert requested, when there are no active sessions."; } - for (auto& session : activeSessions) { + for (auto& session : active_sessions) { if (!crashing_native_process.empty()) { session.SetCrashingNativeProcess(crashing_native_process); } + if (!error_message.empty()) { + session.SetErrorMessage(error_message); + } auto status = session.UpdateStateAndCommit(SessionState::REVERT_IN_PROGRESS); - if (!status) { - // TODO: should we continue with a revert? + if (!status.ok()) { return Error() << "Revert of session " << session << " failed : " << status.error(); } } if (!gInFsCheckpointMode) { - auto restoreStatus = RestoreActivePackages(); - if (!restoreStatus.ok()) { - for (auto& session : activeSessions) { + auto restore_status = RestoreActivePackages(); + if (!restore_status.ok()) { + for (auto& session : active_sessions) { auto st = session.UpdateStateAndCommit(SessionState::REVERT_FAILED); LOG(DEBUG) << "Marking " << session << " as failed to revert"; - if (!st) { + if (!st.ok()) { LOG(WARNING) << "Failed to mark session " << session << " as failed to revert : " << st.error(); } } - return restoreStatus; + return restore_status; } } else { LOG(INFO) << "Not restoring active packages in checkpoint mode."; } - for (auto& session : activeSessions) { + for (auto& session : active_sessions) { if (!gInFsCheckpointMode && session.IsRollback()) { // If snapshots have already been restored, undo that by restoring the // pre-restore snapshot. - restoreDePreRestoreSnapshotsIfPresent(session); + RestoreDePreRestoreSnapshotsIfPresent(session); } auto status = session.UpdateStateAndCommit(SessionState::REVERTED); - if (!status) { + if (!status.ok()) { LOG(WARNING) << "Failed to mark session " << session << " as reverted : " << status.error(); } @@ -1770,9 +2283,10 @@ Result<void> revertActiveSessions(const std::string& crashing_native_process) { return {}; } -Result<void> revertActiveSessionsAndReboot( - const std::string& crashing_native_process) { - auto status = revertActiveSessions(crashing_native_process); +Result<void> RevertActiveSessionsAndReboot( + const std::string& crashing_native_process, + const std::string& error_message) { + auto status = RevertActiveSessions(crashing_native_process, error_message); if (!status.ok()) { return status; } @@ -1788,49 +2302,101 @@ Result<void> revertActiveSessionsAndReboot( return {}; } -int onBootstrap() { - gBootstrap = true; +Result<void> CreateSharedLibsApexDir() { + // Creates /apex/sharedlibs/lib{,64} for SharedLibs APEXes. + std::string shared_libs_sub_dir = + StringPrintf("%s/%s", kApexRoot, kApexSharedLibsSubDir); + auto dir_exists = PathExists(shared_libs_sub_dir); + if (!dir_exists.ok() || !*dir_exists) { + std::error_code error_code; + std::filesystem::create_directory(shared_libs_sub_dir, error_code); + if (error_code) { + return Error() << "Failed to create directory " << shared_libs_sub_dir + << ": " << error_code.message(); + } + } + for (const auto& lib_path : {"lib", "lib64"}) { + std::string apex_lib_path = + StringPrintf("%s/%s", shared_libs_sub_dir.c_str(), lib_path); + auto lib_dir_exists = PathExists(apex_lib_path); + if (!lib_dir_exists.ok() || !*lib_dir_exists) { + std::error_code error_code; + std::filesystem::create_directory(apex_lib_path, error_code); + if (error_code) { + return Error() << "Failed to create directory " << apex_lib_path << ": " + << error_code.message(); + } + } + } - Result<void> preAllocate = preAllocateLoopDevices(); - if (!preAllocate.ok()) { + return {}; +} + +int OnBootstrap() { + auto time_started = boot_clock::now(); + Result<void> pre_allocate = PreAllocateLoopDevices(); + if (!pre_allocate.ok()) { LOG(ERROR) << "Failed to pre-allocate loop devices : " - << preAllocate.error(); + << pre_allocate.error(); } - std::vector<std::string> bootstrap_apex_dirs{ + ApexFileRepository& instance = ApexFileRepository::GetInstance(); + static const std::vector<std::string> kBootstrapApexDirs{ kApexPackageSystemDir, kApexPackageSystemExtDir, kApexPackageVendorDir}; - Result<void> status = collectPreinstalledData(bootstrap_apex_dirs); + Result<void> status = instance.AddPreInstalledApex(kBootstrapApexDirs); if (!status.ok()) { LOG(ERROR) << "Failed to collect APEX keys : " << status.error(); return 1; } - // Activate built-in APEXes for processes launched before /data is mounted. - for (const auto& dir : bootstrap_apex_dirs) { - auto scan_status = ScanApexFiles(dir.c_str()); - if (!scan_status.ok()) { + // Create directories for APEX shared libraries. + auto sharedlibs_apex_dir = CreateSharedLibsApexDir(); + if (!sharedlibs_apex_dir.ok()) { + LOG(ERROR) << sharedlibs_apex_dir.error(); + return 1; + } + + // Find all bootstrap apexes + std::vector<ApexFile> bootstrap_apexes; + for (const auto& dir : kBootstrapApexDirs) { + auto scan = ScanApexFiles(dir.c_str()); + if (!scan.ok()) { LOG(ERROR) << "Failed to scan APEX files in " << dir << " : " - << scan_status.error(); - return 1; - } - if (auto ret = ActivateApexPackages(*scan_status); !ret.ok()) { - LOG(ERROR) << "Failed to activate APEX files in " << dir << " : " - << ret.error(); + << scan.error(); return 1; } + std::copy_if(std::make_move_iterator(scan->begin()), + std::make_move_iterator(scan->end()), + std::back_inserter(bootstrap_apexes), IsBootstrapApex); + } + + // Now activate bootstrap apexes. + std::vector<ApexFileRef> bootstrap_apexes_ref; + std::transform(bootstrap_apexes.begin(), bootstrap_apexes.end(), + std::back_inserter(bootstrap_apexes_ref), + [](const auto& x) { return std::cref(x); }); + auto ret = ActivateApexPackages(bootstrap_apexes_ref, + /* is_ota_chroot= */ false); + if (!ret.ok()) { + LOG(ERROR) << "Failed to activate bootstrap apex files : " << ret.error(); + return 1; } - LOG(INFO) << "Bootstrapping done"; + + OnAllPackagesActivated(/*is_bootstrap=*/true); + auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( + boot_clock::now() - time_started).count(); + LOG(INFO) << "OnBootstrap done, duration=" << time_elapsed; return 0; } -Result<void> remountApexFile(const std::string& path) { - if (auto ret = deactivatePackage(path); !ret.ok()) { +Result<void> RemountApexFile(const std::string& path) { + if (auto ret = DeactivatePackage(path); !ret.ok()) { return ret; } - return activatePackage(path); + return ActivatePackage(path); } -void initializeVold(CheckpointInterface* checkpoint_service) { +void InitializeVold(CheckpointInterface* checkpoint_service) { if (checkpoint_service != nullptr) { gVoldService = checkpoint_service; Result<bool> supports_fs_checkpoints = @@ -1853,21 +2419,300 @@ void initializeVold(CheckpointInterface* checkpoint_service) { } } -void initialize(CheckpointInterface* checkpoint_service) { - initializeVold(checkpoint_service); - Result<void> status = collectPreinstalledData(kApexPackageBuiltinDirs); +void Initialize(CheckpointInterface* checkpoint_service) { + InitializeVold(checkpoint_service); + ApexFileRepository& instance = ApexFileRepository::GetInstance(); + Result<void> status = instance.AddPreInstalledApex(kApexPackageBuiltinDirs); if (!status.ok()) { - LOG(ERROR) << "Failed to collect APEX keys : " << status.error(); + LOG(ERROR) << "Failed to collect pre-installed APEX files : " + << status.error(); return; } + gMountedApexes.PopulateFromMounts(gConfig->active_apex_data_dir, + gConfig->decompression_dir, + gConfig->apex_hash_tree_dir); +} - gMountedApexes.PopulateFromMounts(); +// Note: Pre-installed apex are initialized in Initialize(CheckpointInterface*) +// TODO(b/172911822): Consolidate this with Initialize() when +// ApexFileRepository can act as cache and re-scanning is not expensive +void InitializeDataApex() { + ApexFileRepository& instance = ApexFileRepository::GetInstance(); + Result<void> status = instance.AddDataApex(kActiveApexPackagesDataDir); + if (!status.ok()) { + LOG(ERROR) << "Failed to collect data APEX files : " << status.error(); + return; + } } -void onStart() { +/** + * For every package X, there can be at most two APEX, pre-installed vs + * installed on data. We usually select only one of these APEX for each package + * based on the following conditions: + * - Package X must be pre-installed on one of the built-in directories. + * - If there are multiple APEX, we select the one with highest version. + * - If there are multiple with same version, we give priority to APEX on + * /data partition. + * + * Typically, only one APEX is activated for each package, but APEX that provide + * shared libs are exceptions. We have to activate both APEX for them. + * + * @param all_apex all the APEX grouped by their package name + * @return list of ApexFile that needs to be activated + */ +std::vector<ApexFileRef> SelectApexForActivation( + const std::unordered_map<std::string, std::vector<ApexFileRef>>& all_apex, + const ApexFileRepository& instance) { + LOG(INFO) << "Selecting APEX for activation"; + std::vector<ApexFileRef> activation_list; + // For every package X, select which APEX to activate + for (auto& apex_it : all_apex) { + const std::string& package_name = apex_it.first; + const std::vector<ApexFileRef>& apex_files = apex_it.second; + + if (apex_files.size() > 2 || apex_files.size() == 0) { + LOG(FATAL) << "Unexpectedly found more than two versions or none for " + "APEX package " + << package_name; + continue; + } + + // The package must have a pre-installed version before we consider it for + // activation + if (!instance.HasPreInstalledVersion(package_name)) { + LOG(INFO) << "Package " << package_name << " is not pre-installed"; + continue; + } + + if (apex_files.size() == 1) { + LOG(DEBUG) << "Selecting the only APEX: " << package_name << " " + << apex_files[0].get().GetPath(); + activation_list.emplace_back(apex_files[0]); + continue; + } + + // TODO(b/179497746): Now that we are dealing with list of reference, this + // selection process can be simplified by sorting the vector. + + // Given an APEX A and the version of the other APEX B, should we activate + // it? + auto select_apex = [&instance, &activation_list]( + const ApexFileRef& a_ref, + const int version_b) mutable { + const ApexFile& a = a_ref.get(); + // APEX that provides shared library always gets activated + const bool provides_shared_apex_libs = + a.GetManifest().providesharedapexlibs(); + // If A has higher version than B, then it should be activated + const bool higher_version = a.GetManifest().version() > version_b; + // If A has same version as B, then data version should get activated + const bool same_version_priority_to_data = + a.GetManifest().version() == version_b && + !instance.IsPreInstalledApex(a); + if (provides_shared_apex_libs || higher_version || + same_version_priority_to_data) { + LOG(DEBUG) << "Selecting between two APEX: " << a.GetManifest().name() + << " " << a.GetPath(); + activation_list.emplace_back(a_ref); + } + }; + const int version_0 = apex_files[0].get().GetManifest().version(); + const int version_1 = apex_files[1].get().GetManifest().version(); + select_apex(apex_files[0].get(), version_1); + select_apex(apex_files[1].get(), version_0); + } + return activation_list; +} + +namespace { + +Result<ApexFile> OpenAndValidateDecompressedApex(const ApexFile& capex, + const std::string& apex_path) { + auto apex = ApexFile::Open(apex_path); + if (!apex.ok()) { + return Error() << "Failed to open decompressed APEX: " << apex.error(); + } + auto result = ValidateDecompressedApex(capex, *apex); + if (!result.ok()) { + return result.error(); + } + return std::move(*apex); +} + +// Process a single compressed APEX. Returns the decompressed APEX if +// successful. +Result<ApexFile> ProcessCompressedApex(const ApexFile& capex, + bool is_ota_chroot) { + LOG(INFO) << "Processing compressed APEX " << capex.GetPath(); + const auto decompressed_apex_path = + StringPrintf("%s/%s%s", gConfig->decompression_dir, + GetPackageId(capex.GetManifest()).c_str(), + kDecompressedApexPackageSuffix); + // Check if decompressed APEX already exist + auto decompressed_path_exists = PathExists(decompressed_apex_path); + if (decompressed_path_exists.ok() && *decompressed_path_exists) { + // Check if existing decompressed APEX is valid + auto result = + OpenAndValidateDecompressedApex(capex, decompressed_apex_path); + if (result.ok()) { + LOG(INFO) << "Skipping decompression for " << capex.GetPath(); + return result; + } + // Do not delete existing decompressed APEX when is_ota_chroot is true + if (!is_ota_chroot) { + // Existing decompressed APEX is not valid. We will have to redecompress + LOG(WARNING) << "Existing decompressed APEX is invalid: " + << result.error(); + RemoveFileIfExists(decompressed_apex_path); + } + } + + // We can also reuse existing OTA APEX, depending on situation + auto ota_apex_path = StringPrintf("%s/%s%s", gConfig->decompression_dir, + GetPackageId(capex.GetManifest()).c_str(), + kOtaApexPackageSuffix); + auto ota_path_exists = PathExists(ota_apex_path); + if (ota_path_exists.ok() && *ota_path_exists) { + if (is_ota_chroot) { + // During ota_chroot, we try to reuse ota APEX as is + auto result = OpenAndValidateDecompressedApex(capex, ota_apex_path); + if (result.ok()) { + LOG(INFO) << "Skipping decompression for " << ota_apex_path; + return result; + } + // Existing ota_apex is not valid. We will have to decompress + LOG(WARNING) << "Existing decompressed OTA APEX is invalid: " + << result.error(); + RemoveFileIfExists(ota_apex_path); + } else { + // During boot, we can avoid decompression by renaming OTA apex + // to expected decompressed_apex path + + // Check if ota_apex APEX is valid + auto result = OpenAndValidateDecompressedApex(capex, ota_apex_path); + if (result.ok()) { + // ota_apex matches with capex. Slot has been switched. + + // Rename ota_apex to expected decompressed_apex path + if (rename(ota_apex_path.c_str(), decompressed_apex_path.c_str()) == + 0) { + // Check if renamed decompressed APEX is valid + result = + OpenAndValidateDecompressedApex(capex, decompressed_apex_path); + if (result.ok()) { + LOG(INFO) << "Renamed " << ota_apex_path << " to " + << decompressed_apex_path; + return result; + } + // Renamed ota_apex is not valid. We will have to decompress + LOG(WARNING) << "Renamed decompressed APEX from " << ota_apex_path + << " to " << decompressed_apex_path + << " is invalid: " << result.error(); + RemoveFileIfExists(decompressed_apex_path); + } else { + PLOG(ERROR) << "Failed to rename file " << ota_apex_path; + } + } + } + } + + // There was no way to avoid decompression + + // Clean up reserved space before decompressing capex + if (auto ret = DeleteDirContent(gConfig->ota_reserved_dir); !ret.ok()) { + LOG(ERROR) << "Failed to clean up reserved space: " << ret.error(); + } + + auto decompression_dest = + is_ota_chroot ? ota_apex_path : decompressed_apex_path; + auto scope_guard = android::base::make_scope_guard( + [&]() { RemoveFileIfExists(decompression_dest); }); + + auto decompression_result = capex.Decompress(decompression_dest); + if (!decompression_result.ok()) { + return Error() << "Failed to decompress : " << capex.GetPath().c_str() + << " " << decompression_result.error(); + } + + // Fix label of decompressed file + auto restore = RestoreconPath(decompression_dest); + if (!restore.ok()) { + return restore.error(); + } + + // Validate the newly decompressed APEX + auto return_apex = OpenAndValidateDecompressedApex(capex, decompression_dest); + if (!return_apex.ok()) { + return Error() << "Failed to decompress CAPEX: " << return_apex.error(); + } + + /// Release compressed blocks in case decompression_dest is on f2fs-compressed + // filesystem. + ReleaseF2fsCompressedBlocks(decompression_dest); + + scope_guard.Disable(); + return return_apex; +} +} // namespace + +/** + * For each compressed APEX, decompress it to kApexDecompressedDir + * and return the decompressed APEX. + * + * Returns list of decompressed APEX. + */ +std::vector<ApexFile> ProcessCompressedApex( + const std::vector<ApexFileRef>& compressed_apex, bool is_ota_chroot) { + LOG(INFO) << "Processing compressed APEX"; + + std::vector<ApexFile> decompressed_apex_list; + for (const ApexFile& capex : compressed_apex) { + if (!capex.IsCompressed()) { + continue; + } + + auto decompressed_apex = ProcessCompressedApex(capex, is_ota_chroot); + if (decompressed_apex.ok()) { + decompressed_apex_list.emplace_back(std::move(*decompressed_apex)); + continue; + } + LOG(ERROR) << "Failed to process compressed APEX: " + << decompressed_apex.error(); + } + return std::move(decompressed_apex_list); +} + +Result<void> ValidateDecompressedApex(const ApexFile& capex, + const ApexFile& apex) { + // Decompressed APEX must have same public key as CAPEX + if (capex.GetBundledPublicKey() != apex.GetBundledPublicKey()) { + return Error() + << "Public key of compressed APEX is different than original " + << "APEX for " << apex.GetPath(); + } + // Decompressed APEX must have same version as CAPEX + if (capex.GetManifest().version() != apex.GetManifest().version()) { + return Error() + << "Compressed APEX has different version than decompressed APEX " + << apex.GetPath(); + } + // Decompressed APEX must have same root digest as what is stored in CAPEX + auto apex_verity = apex.VerifyApexVerity(apex.GetBundledPublicKey()); + if (!apex_verity.ok() || + capex.GetManifest().capexmetadata().originalapexdigest() != + apex_verity->root_digest) { + return Error() << "Root digest of " << apex.GetPath() + << " does not match with" + << " expected root digest in " << capex.GetPath(); + } + return {}; +} + +void OnStart() { LOG(INFO) << "Marking APEXd as starting"; - if (!android::base::SetProperty(kApexStatusSysprop, kApexStatusStarting)) { - PLOG(ERROR) << "Failed to set " << kApexStatusSysprop << " to " + auto time_started = boot_clock::now(); + if (!SetProperty(gConfig->apex_status_sysprop, kApexStatusStarting)) { + PLOG(ERROR) << "Failed to set " << gConfig->apex_status_sysprop << " to " << kApexStatusStarting; } @@ -1883,98 +2728,134 @@ void onStart() { LOG(INFO) << "Exceeded number of session retries (" << kNumRetriesWhenCheckpointingEnabled << "). Starting a revert"; - revertActiveSessions(""); + RevertActiveSessions("", ""); } } - // Activate APEXes from /data/apex. If one in the directory is newer than the - // system one, the new one will eclipse the old one. - scanStagedSessionsDirAndStage(); - auto status = resumeRevertIfNeeded(); + // Create directories for APEX shared libraries. + auto sharedlibs_apex_dir = CreateSharedLibsApexDir(); + if (!sharedlibs_apex_dir.ok()) { + LOG(ERROR) << sharedlibs_apex_dir.error(); + } + + // If there is any new apex to be installed on /data/app-staging, hardlink + // them to /data/apex/active first. + ScanStagedSessionsDirAndStage(); + if (auto status = ApexFileRepository::GetInstance().AddDataApex( + gConfig->active_apex_data_dir); + !status.ok()) { + LOG(ERROR) << "Failed to collect data APEX files : " << status.error(); + } + + auto status = ResumeRevertIfNeeded(); if (!status.ok()) { LOG(ERROR) << "Failed to resume revert : " << status.error(); } - std::vector<ApexFile> data_apex; - if (auto scan = ScanApexFiles(kActiveApexPackagesDataDir); !scan.ok()) { - LOG(ERROR) << "Failed to scan packages from " << kActiveApexPackagesDataDir - << " : " << scan.error(); - if (auto revert = revertActiveSessionsAndReboot(""); !revert.ok()) { - LOG(ERROR) << "Failed to revert : " << revert.error(); + // Group every ApexFile on device by name + const auto& instance = ApexFileRepository::GetInstance(); + const auto& all_apex = instance.AllApexFilesByName(); + // There can be multiple APEX packages with package name X. Determine which + // one to activate. + auto activation_list = SelectApexForActivation(all_apex, instance); + + // Process compressed APEX, if any + std::vector<ApexFileRef> compressed_apex; + for (auto it = activation_list.begin(); it != activation_list.end();) { + if (it->get().IsCompressed()) { + compressed_apex.emplace_back(*it); + it = activation_list.erase(it); + } else { + it++; } - } else { - auto filter_fn = [](const ApexFile& apex) { - if (!ShouldActivateApexOnData(apex)) { - LOG(WARNING) << "Skipping " << apex.GetPath(); - return false; - } - return true; - }; - std::copy_if(std::make_move_iterator(scan->begin()), - std::make_move_iterator(scan->end()), - std::back_inserter(data_apex), filter_fn); } - - if (auto ret = ActivateApexPackages(data_apex); !ret.ok()) { - LOG(ERROR) << "Failed to activate packages from " - << kActiveApexPackagesDataDir << " : " << status.error(); - Result<void> revert_status = revertActiveSessionsAndReboot(""); - if (!revert_status.ok()) { - // TODO: should we kill apexd in this case? - LOG(ERROR) << "Failed to revert : " << revert_status.error() - << kActiveApexPackagesDataDir << " : " << ret.error(); + std::vector<ApexFile> decompressed_apex; + if (!compressed_apex.empty()) { + decompressed_apex = + ProcessCompressedApex(compressed_apex, /* is_ota_chroot= */ false); + for (const ApexFile& apex_file : decompressed_apex) { + activation_list.emplace_back(std::cref(apex_file)); } } - // Now also scan and activate APEXes from pre-installed directories. - for (const auto& dir : kApexPackageBuiltinDirs) { - auto scan_status = ScanApexFiles(dir.c_str()); - if (!scan_status.ok()) { - LOG(ERROR) << "Failed to scan APEX packages from " << dir << " : " - << scan_status.error(); - if (auto revert = revertActiveSessionsAndReboot(""); !revert.ok()) { - LOG(ERROR) << "Failed to revert : " << revert.error(); - } + int data_apex_cnt = std::count_if( + activation_list.begin(), activation_list.end(), [](const auto& a) { + return !ApexFileRepository::GetInstance().IsPreInstalledApex(a.get()); + }); + if (data_apex_cnt > 0) { + Result<void> pre_allocate = loop::PreAllocateLoopDevices(data_apex_cnt); + if (!pre_allocate.ok()) { + LOG(ERROR) << "Failed to pre-allocate loop devices : " + << pre_allocate.error(); + } + } + + // TODO(b/179248390): activate parallelly if possible + auto activate_status = + ActivateApexPackages(activation_list, /* is_ota_chroot= */ false); + if (!activate_status.ok()) { + std::string error_message = + StringPrintf("Failed to activate packages: %s", + activate_status.error().message().c_str()); + LOG(ERROR) << error_message; + Result<void> revert_status = + RevertActiveSessionsAndReboot("", error_message); + if (!revert_status.ok()) { + LOG(ERROR) << "Failed to revert : " << revert_status.error(); } - if (auto activate = ActivateApexPackages(*scan_status); !activate.ok()) { - // This should never happen. Like **really** never. - // TODO: should we kill apexd in this case? - LOG(ERROR) << "Failed to activate packages from " << dir << " : " - << activate.error(); + auto retry_status = ActivateMissingApexes(activation_list, + /* is_ota_chroot= */ false); + if (!retry_status.ok()) { + LOG(ERROR) << retry_status.error(); } } // Now that APEXes are mounted, snapshot or restore DE_sys data. - snapshotOrRestoreDeSysData(); + SnapshotOrRestoreDeSysData(); + + auto time_elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( + boot_clock::now() - time_started).count(); + LOG(INFO) << "OnStart done, duration=" << time_elapsed; } -void onAllPackagesActivated() { +void OnAllPackagesActivated(bool is_bootstrap) { + auto result = EmitApexInfoList(is_bootstrap); + if (!result.ok()) { + LOG(ERROR) << "cannot emit apex info list: " << result.error(); + } + + // Because apexd in bootstrap mode runs in blocking mode + // we don't have to set as activated. + if (is_bootstrap) { + return; + } + // Set a system property to let other components know that APEXs are // activated, but are not yet ready to be used. init is expected to wait // for this status before performing configuration based on activated // apexes. Other components that need to use APEXs should wait for the // ready state instead. LOG(INFO) << "Marking APEXd as activated"; - if (!android::base::SetProperty(kApexStatusSysprop, kApexStatusActivated)) { - PLOG(ERROR) << "Failed to set " << kApexStatusSysprop << " to " + if (!SetProperty(gConfig->apex_status_sysprop, kApexStatusActivated)) { + PLOG(ERROR) << "Failed to set " << gConfig->apex_status_sysprop << " to " << kApexStatusActivated; } } -void onAllPackagesReady() { +void OnAllPackagesReady() { // Set a system property to let other components know that APEXs are // correctly mounted and ready to be used. Before using any file from APEXs, // they can query this system property to ensure that they are okay to // access. Or they may have a on-property trigger to delay a task until // APEXs become ready. LOG(INFO) << "Marking APEXd as ready"; - if (!android::base::SetProperty(kApexStatusSysprop, kApexStatusReady)) { - PLOG(ERROR) << "Failed to set " << kApexStatusSysprop << " to " + if (!SetProperty(gConfig->apex_status_sysprop, kApexStatusReady)) { + PLOG(ERROR) << "Failed to set " << gConfig->apex_status_sysprop << " to " << kApexStatusReady; } } -Result<std::vector<ApexFile>> submitStagedSession( +Result<std::vector<ApexFile>> SubmitStagedSession( const int session_id, const std::vector<int>& child_session_ids, const bool has_rollback_enabled, const bool is_rollback, const int rollback_id) { @@ -1999,7 +2880,7 @@ Result<std::vector<ApexFile>> submitStagedSession( std::vector<ApexFile> ret; for (int id_to_scan : ids_to_scan) { - auto verified = verifySessionDir(id_to_scan); + auto verified = VerifySessionDir(id_to_scan); if (!verified.ok()) { return verified.error(); } @@ -2033,10 +2914,15 @@ Result<std::vector<ApexFile>> submitStagedSession( return commit_status.error(); } + for (const auto& apex : ret) { + // Release compressed blocks in case /data is f2fs-compressed filesystem. + ReleaseF2fsCompressedBlocks(apex.GetPath()); + } + return ret; } -Result<void> markStagedSessionReady(const int session_id) { +Result<void> MarkStagedSessionReady(const int session_id) { auto session = ApexSession::GetSession(session_id); if (!session.ok()) { return session.error(); @@ -2055,7 +2941,7 @@ Result<void> markStagedSessionReady(const int session_id) { << ". Cannot mark it as ready."; } -Result<void> markStagedSessionSuccessful(const int session_id) { +Result<void> MarkStagedSessionSuccessful(const int session_id) { auto session = ApexSession::GetSession(session_id); if (!session.ok()) { return session.error(); @@ -2071,7 +2957,7 @@ Result<void> markStagedSessionSuccessful(const int session_id) { << " as successful : " << cleanup_status.error(); } if (session->IsRollback() && !gInFsCheckpointMode) { - deleteDePreRestoreSnapshots(*session); + DeleteDePreRestoreSnapshots(*session); } return session->UpdateStateAndCommit(SessionState::SUCCESS); } else { @@ -2079,86 +2965,63 @@ Result<void> markStagedSessionSuccessful(const int session_id) { } } -namespace { - -// Find dangling mounts and unmount them. -// If one is on /data/apex/active, remove it. -void UnmountDanglingMounts() { - std::multimap<std::string, MountedApexData> danglings; - gMountedApexes.ForallMountedApexes([&](const std::string& package, - const MountedApexData& data, - bool latest) { - if (!latest) { - danglings.insert({package, data}); - } - }); - - for (const auto& [package, data] : danglings) { - const std::string& path = data.full_path; - LOG(VERBOSE) << "Unmounting " << data.mount_point; - gMountedApexes.RemoveMountedApex(package, path); - if (auto st = Unmount(data); !st.ok()) { - LOG(ERROR) << st.error(); - } - if (StartsWith(path, kActiveApexPackagesDataDir)) { - LOG(VERBOSE) << "Deleting old APEX " << path; - if (unlink(path.c_str()) != 0) { - PLOG(ERROR) << "Failed to delete " << path; - } - } +// Removes APEXes on /data that have not been activated +void RemoveInactiveDataApex() { + std::vector<std::string> all_apex_files; + Result<std::vector<std::string>> active_apex = + FindFilesBySuffix(gConfig->active_apex_data_dir, {kApexPackageSuffix}); + if (!active_apex.ok()) { + LOG(ERROR) << "Failed to scan " << gConfig->active_apex_data_dir << " : " + << active_apex.error(); + } else { + all_apex_files.insert(all_apex_files.end(), + std::make_move_iterator(active_apex->begin()), + std::make_move_iterator(active_apex->end())); + } + Result<std::vector<std::string>> decompressed_apex = FindFilesBySuffix( + gConfig->decompression_dir, {kDecompressedApexPackageSuffix}); + if (!decompressed_apex.ok()) { + LOG(ERROR) << "Failed to scan " << gConfig->decompression_dir << " : " + << decompressed_apex.error(); + } else { + all_apex_files.insert(all_apex_files.end(), + std::make_move_iterator(decompressed_apex->begin()), + std::make_move_iterator(decompressed_apex->end())); } - RemoveObsoleteHashTrees(); -} - -// Removes APEXes on /data that don't have corresponding pre-installed version -// or that are corrupt -void RemoveOrphanedApexes() { - auto data_apexes = FindApexFilesByName(kActiveApexPackagesDataDir); - if (!data_apexes.ok()) { - LOG(ERROR) << "Failed to scan " << kActiveApexPackagesDataDir << " : " - << data_apexes.error(); - return; - } - for (const auto& path : *data_apexes) { - auto apex = ApexFile::Open(path); - if (!apex.ok()) { - LOG(DEBUG) << "Failed to open APEX " << path << " : " << apex.error(); - // before removing, double-check if the path is active or not - // just in case ApexFile::Open() fails with valid APEX - if (!apexd_private::IsMounted(path)) { - LOG(DEBUG) << "Removing corrupt APEX " << path; - if (unlink(path.c_str()) != 0) { - PLOG(ERROR) << "Failed to unlink " << path; - } - } - continue; - } - if (!ShouldActivateApexOnData(*apex)) { - LOG(DEBUG) << "Removing orphaned APEX " << path; + for (const auto& path : all_apex_files) { + if (!apexd_private::IsMounted(path)) { + LOG(INFO) << "Removing inactive data APEX " << path; if (unlink(path.c_str()) != 0) { - PLOG(ERROR) << "Failed to unlink " << path; + PLOG(ERROR) << "Failed to unlink inactive data APEX " << path; } } } } -} // namespace - -void bootCompletedCleanup() { - UnmountDanglingMounts(); - RemoveOrphanedApexes(); +void BootCompletedCleanup() { + RemoveInactiveDataApex(); + ApexSession::DeleteFinalizedSessions(); } -int unmountAll() { - gMountedApexes.PopulateFromMounts(); +int UnmountAll() { + gMountedApexes.PopulateFromMounts(gConfig->active_apex_data_dir, + gConfig->decompression_dir, + gConfig->apex_hash_tree_dir); int ret = 0; gMountedApexes.ForallMountedApexes([&](const std::string& /*package*/, const MountedApexData& data, bool latest) { LOG(INFO) << "Unmounting " << data.full_path << " mounted on " << data.mount_point; - if (latest) { + auto apex = ApexFile::Open(data.full_path); + if (!apex.ok()) { + LOG(ERROR) << "Failed to open " << data.full_path << " : " + << apex.error(); + ret = 1; + return; + } + if (latest && !apex->GetManifest().providesharedapexlibs()) { auto pos = data.mount_point.find('@'); CHECK(pos != std::string::npos); std::string bind_mount = data.mount_point.substr(0, pos); @@ -2167,7 +3030,7 @@ int unmountAll() { ret = 1; } } - if (auto status = Unmount(data); !status.ok()) { + if (auto status = Unmount(data, /* deferred= */ false); !status.ok()) { LOG(ERROR) << "Failed to unmount " << data.mount_point << " : " << status.error(); ret = 1; @@ -2176,12 +3039,7 @@ int unmountAll() { return ret; } -bool isBooting() { - auto status = GetProperty(kApexStatusSysprop, ""); - return status != kApexStatusReady && status != kApexStatusActivated; -} - -Result<void> remountPackages() { +Result<void> RemountPackages() { std::vector<std::string> apexes; gMountedApexes.ForallMountedApexes([&apexes](const std::string& /*package*/, const MountedApexData& data, @@ -2195,7 +3053,7 @@ Result<void> remountPackages() { for (const std::string& apex : apexes) { // Since this is only used during development workflow, we are trying to // remount as many apexes as possible instead of failing fast. - if (auto ret = remountApexFile(apex); !ret) { + if (auto ret = RemountApexFile(apex); !ret.ok()) { LOG(WARNING) << "Failed to remount " << apex << " : " << ret.error(); failed.emplace_back(apex); } @@ -2212,5 +3070,596 @@ Result<void> remountPackages() { return {}; } +// Given a single new APEX incoming via OTA, should we allocate space for it? +Result<bool> ShouldAllocateSpaceForDecompression( + const std::string& new_apex_name, const int64_t new_apex_version, + const ApexFileRepository& instance) { + // An apex at most will have two versions on device: pre-installed and data. + + // Check if there is a pre-installed version for the new apex. + if (!instance.HasPreInstalledVersion(new_apex_name)) { + // We are introducing a new APEX that doesn't exist at all + return true; + } + + // Check if there is a data apex + if (!instance.HasDataVersion(new_apex_name)) { + // Data apex doesn't exist. Compare against pre-installed APEX + auto pre_installed_apex = instance.GetPreInstalledApex(new_apex_name); + if (!pre_installed_apex.get().IsCompressed()) { + // Compressing an existing uncompressed system APEX. + return true; + } + // Since there is no data apex, it means device is using the compressed + // pre-installed version. If new apex has higher version, we are upgrading + // the pre-install version and if new apex has lower version, we are + // downgrading it. So the current decompressed apex should be replaced + // with the new decompressed apex to reflect that. + const int64_t pre_installed_version = + instance.GetPreInstalledApex(new_apex_name) + .get() + .GetManifest() + .version(); + return new_apex_version != pre_installed_version; + } + + // From here on, data apex exists. So we should compare directly against data + // apex. + auto data_apex = instance.GetDataApex(new_apex_name); + // Compare the data apex version with new apex + const int64_t data_version = data_apex.get().GetManifest().version(); + // We only decompress the new_apex if it has higher version than data apex. + return new_apex_version > data_version; +} + +void CollectApexInfoList(std::ostream& os, + const std::vector<ApexFile>& active_apexs, + const std::vector<ApexFile>& inactive_apexs) { + std::vector<com::android::apex::ApexInfo> apex_infos; + + auto convert_to_autogen = [&apex_infos](const ApexFile& apex, + bool is_active) { + auto& instance = ApexFileRepository::GetInstance(); + + auto preinstalled_path = + instance.GetPreinstalledPath(apex.GetManifest().name()); + std::optional<std::string> preinstalled_module_path; + if (preinstalled_path.ok()) { + preinstalled_module_path = *preinstalled_path; + } + + std::optional<int64_t> mtime; + struct stat stat_buf; + if (stat(apex.GetPath().c_str(), &stat_buf) == 0) { + mtime.emplace(stat_buf.st_mtime); + } else { + PLOG(WARNING) << "Failed to stat " << apex.GetPath(); + } + com::android::apex::ApexInfo apex_info( + apex.GetManifest().name(), apex.GetPath(), preinstalled_module_path, + apex.GetManifest().version(), apex.GetManifest().versionname(), + instance.IsPreInstalledApex(apex), is_active, mtime); + apex_infos.emplace_back(apex_info); + }; + for (const auto& apex : active_apexs) { + convert_to_autogen(apex, /* is_active= */ true); + } + for (const auto& apex : inactive_apexs) { + convert_to_autogen(apex, /* is_active= */ false); + } + com::android::apex::ApexInfoList apex_info_list(apex_infos); + com::android::apex::write(os, apex_info_list); +} + +// Reserve |size| bytes in |dest_dir| by creating a zero-filled file. +// Also, we always clean up ota_apex that has been processed as +// part of pre-reboot decompression whenever we reserve space. +Result<void> ReserveSpaceForCompressedApex(int64_t size, + const std::string& dest_dir) { + if (size < 0) { + return Error() << "Cannot reserve negative byte of space"; + } + + // Since we are reserving space, then we must be preparing for a new OTA. + // Clean up any processed ota_apex from previous OTA. + auto ota_apex_files = + FindFilesBySuffix(gConfig->decompression_dir, {kOtaApexPackageSuffix}); + if (!ota_apex_files.ok()) { + return Error() << "Failed to clean up ota_apex: " << ota_apex_files.error(); + } + for (const std::string& ota_apex : *ota_apex_files) { + RemoveFileIfExists(ota_apex); + } + + auto file_path = StringPrintf("%s/full.tmp", dest_dir.c_str()); + if (size == 0) { + LOG(INFO) << "Cleaning up reserved space for compressed APEX"; + // Ota is being cancelled. Clean up reserved space + RemoveFileIfExists(file_path); + return {}; + } + + LOG(INFO) << "Reserving " << size << " bytes for compressed APEX"; + unique_fd dest_fd( + open(file_path.c_str(), O_WRONLY | O_CLOEXEC | O_CREAT, 0644)); + if (dest_fd.get() == -1) { + return ErrnoError() << "Failed to open file for reservation " + << file_path.c_str(); + } + + // Resize to required size + std::error_code ec; + std::filesystem::resize_file(file_path, size, ec); + if (ec) { + RemoveFileIfExists(file_path); + return ErrnoError() << "Failed to resize file " << file_path.c_str() + << " : " << ec.message(); + } + + return {}; +} + +int OnOtaChrootBootstrap() { + auto& instance = ApexFileRepository::GetInstance(); + if (auto status = instance.AddPreInstalledApex(gConfig->apex_built_in_dirs); + !status.ok()) { + LOG(ERROR) << "Failed to scan pre-installed apexes from " + << Join(gConfig->apex_built_in_dirs, ','); + return 1; + } + if (auto status = instance.AddDataApex(gConfig->active_apex_data_dir); + !status.ok()) { + LOG(ERROR) << "Failed to scan upgraded apexes from " + << gConfig->active_apex_data_dir; + // Failing to scan upgraded apexes is not fatal, since we can still try to + // run otapreopt using only pre-installed apexes. Worst case, apps will be + // re-optimized on next boot. + } + + // Create directories for APEX shared libraries. + if (auto status = CreateSharedLibsApexDir(); !status.ok()) { + LOG(ERROR) << "Failed to create /apex/sharedlibs : " << status.ok(); + return 1; + } + + auto activation_list = + SelectApexForActivation(instance.AllApexFilesByName(), instance); + + // TODO(b/179497746): This is the third time we are duplicating this code + // block. This will be easier to dedup once we start opening ApexFiles via + // ApexFileRepository. That way, ProcessCompressedApex can return list of + // ApexFileRef, instead of ApexFile. + + // Process compressed APEX, if any + std::vector<ApexFileRef> compressed_apex; + for (auto it = activation_list.begin(); it != activation_list.end();) { + if (it->get().IsCompressed()) { + compressed_apex.emplace_back(*it); + it = activation_list.erase(it); + } else { + it++; + } + } + std::vector<ApexFile> decompressed_apex; + if (!compressed_apex.empty()) { + decompressed_apex = + ProcessCompressedApex(compressed_apex, /* is_ota_chroot= */ true); + + for (const ApexFile& apex_file : decompressed_apex) { + activation_list.emplace_back(std::cref(apex_file)); + } + } + + auto activate_status = ActivateApexPackages(activation_list, + /* is_ota_chroot= */ true); + if (!activate_status.ok()) { + LOG(ERROR) << "Failed to activate apex packages : " + << activate_status.error(); + auto retry_status = ActivateMissingApexes(activation_list, + /* is_ota_chroot= */ true); + if (!retry_status.ok()) { + LOG(ERROR) << retry_status.error(); + } + } + + // There are a bunch of places that are producing apex-info.xml file. + // We should consolidate the logic in one function and make all other places + // use it. + auto active_apexes = GetActivePackages(); + std::vector<ApexFile> inactive_apexes = GetFactoryPackages(); + auto new_end = std::remove_if( + inactive_apexes.begin(), inactive_apexes.end(), + [&active_apexes](const ApexFile& apex) { + return std::any_of(active_apexes.begin(), active_apexes.end(), + [&apex](const ApexFile& active_apex) { + return apex.GetPath() == active_apex.GetPath(); + }); + }); + inactive_apexes.erase(new_end, inactive_apexes.end()); + std::stringstream xml; + CollectApexInfoList(xml, active_apexes, inactive_apexes); + std::string file_name = StringPrintf("%s/%s", kApexRoot, kApexInfoList); + unique_fd fd(TEMP_FAILURE_RETRY( + open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644))); + if (fd.get() == -1) { + PLOG(ERROR) << "Can't open " << file_name; + return 1; + } + + if (!android::base::WriteStringToFd(xml.str(), fd)) { + PLOG(ERROR) << "Can't write to " << file_name; + return 1; + } + + fd.reset(); + + if (auto status = RestoreconPath(file_name); !status.ok()) { + LOG(ERROR) << "Failed to restorecon " << file_name << " : " + << status.error(); + return 1; + } + + return 0; +} + +int OnOtaChrootBootstrapFlattenedApex() { + LOG(INFO) << "OnOtaChrootBootstrapFlattenedApex"; + + std::vector<com::android::apex::ApexInfo> apex_infos; + + for (const std::string& dir : gConfig->apex_built_in_dirs) { + LOG(INFO) << "Scanning " << dir; + auto dir_content = ReadDir(dir, [](const auto& entry) { + std::error_code ec; + return entry.is_directory(ec); + }); + + if (!dir_content.ok()) { + LOG(ERROR) << "Failed to scan " << dir << " : " << dir_content.error(); + continue; + } + + // Sort to make sure that /apex/apex-info-list.xml generation doesn't depend + // on the unstable directory scan. + std::vector<std::string> entries = std::move(*dir_content); + std::sort(entries.begin(), entries.end()); + + for (const std::string& apex_dir : entries) { + std::string manifest_file = apex_dir + "/" + kManifestFilenamePb; + if (access(manifest_file.c_str(), F_OK) != 0) { + PLOG(ERROR) << "Failed to access " << manifest_file; + continue; + } + + auto manifest = ReadManifest(manifest_file); + if (!manifest.ok()) { + LOG(ERROR) << "Failed to read apex manifest from " << manifest_file + << " : " << manifest.error(); + continue; + } + + std::string mount_point = std::string(kApexRoot) + "/" + manifest->name(); + if (mkdir(mount_point.c_str(), 0755) != 0) { + PLOG(ERROR) << "Failed to mkdir " << mount_point; + continue; + } + + LOG(INFO) << "Bind mounting " << apex_dir << " onto " << mount_point; + if (mount(apex_dir.c_str(), mount_point.c_str(), nullptr, MS_BIND, + nullptr) != 0) { + PLOG(ERROR) << "Failed to bind mount " << apex_dir << " to " + << mount_point; + continue; + } + + apex_infos.emplace_back(manifest->name(), /* modulePath= */ apex_dir, + /* preinstalledModulePath= */ apex_dir, + /* versionCode= */ manifest->version(), + /* versionName= */ manifest->versionname(), + /* isFactory= */ true, /* isActive= */ true, + /* lastUpdateMillis= */ 0); + } + } + + std::string file_name = StringPrintf("%s/%s", kApexRoot, kApexInfoList); + unique_fd fd(TEMP_FAILURE_RETRY( + open(file_name.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644))); + if (fd.get() == -1) { + PLOG(ERROR) << "Can't open " << file_name; + return 1; + } + + std::ostringstream xml; + com::android::apex::ApexInfoList apex_info_list(apex_infos); + com::android::apex::write(xml, apex_info_list); + if (!android::base::WriteStringToFd(xml.str(), fd)) { + PLOG(ERROR) << "Can't write to " << file_name; + return 1; + } + fd.reset(); + + if (auto status = RestoreconPath(file_name); !status.ok()) { + LOG(ERROR) << "Failed to restorecon " << file_name << " : " + << status.error(); + return 1; + } + + return 0; +} + +android::apex::MountedApexDatabase& GetApexDatabaseForTesting() { + return gMountedApexes; +} + +// A version of apex verification that happens during non-staged APEX +// installation. +Result<void> VerifyPackageNonStagedInstall(const ApexFile& apex_file) { + const auto& verify_package_boot_status = VerifyPackageBoot(apex_file); + if (!verify_package_boot_status.ok()) { + return verify_package_boot_status; + } + + auto check_fn = [&apex_file](const std::string& mount_point) -> Result<void> { + auto dirs = GetSubdirs(mount_point); + if (!dirs.ok()) { + return dirs.error(); + } + if (std::find(dirs->begin(), dirs->end(), mount_point + "/app") != + dirs->end()) { + return Error() << apex_file.GetPath() << " contains app inside"; + } + if (std::find(dirs->begin(), dirs->end(), mount_point + "/priv-app") != + dirs->end()) { + return Error() << apex_file.GetPath() << " contains priv-app inside"; + } + return Result<void>{}; + }; + return RunVerifyFnInsideTempMount(apex_file, check_fn, true); +} + +Result<void> CheckSupportsNonStagedInstall(const ApexFile& cur_apex, + const ApexFile& new_apex) { + const auto& cur_manifest = cur_apex.GetManifest(); + const auto& new_manifest = new_apex.GetManifest(); + + if (!new_manifest.supportsrebootlessupdate()) { + return Error() << new_apex.GetPath() + << " does not support non-staged update"; + } + + // Check if update will impact linkerconfig. + + // Updates to shared libs APEXes must be done via staged install flow. + if (new_manifest.providesharedapexlibs()) { + return Error() << new_apex.GetPath() << " is a shared libs APEX"; + } + + // This APEX provides native libs to other parts of the platform. It can only + // be updated via staged install flow. + if (new_manifest.providenativelibs_size() > 0) { + return Error() << new_apex.GetPath() << " provides native libs"; + } + + // This APEX requires libs provided by dynamic common library APEX, hence it + // can only be installed using staged install flow. + if (new_manifest.requiresharedapexlibs_size() > 0) { + return Error() << new_apex.GetPath() << " requires shared apex libs"; + } + + // We don't allow non-staged updates of APEXES that have java libs inside. + if (new_manifest.jnilibs_size() > 0) { + return Error() << new_apex.GetPath() << " requires JNI libs"; + } + + // For requireNativeLibs bit, we only allow updates that don't change list of + // required libs. + + std::vector<std::string> cur_required_libs( + cur_manifest.requirenativelibs().begin(), + cur_manifest.requirenativelibs().end()); + sort(cur_required_libs.begin(), cur_required_libs.end()); + + std::vector<std::string> new_required_libs( + new_manifest.requirenativelibs().begin(), + new_manifest.requirenativelibs().end()); + sort(new_required_libs.begin(), new_required_libs.end()); + + if (cur_required_libs != new_required_libs) { + return Error() << "Set of native libs required by " << new_apex.GetPath() + << " differs from the one required by the currently active " + << cur_apex.GetPath(); + } + + auto expected_public_key = + ApexFileRepository::GetInstance().GetPublicKey(new_manifest.name()); + if (!expected_public_key.ok()) { + return expected_public_key.error(); + } + auto verity_data = new_apex.VerifyApexVerity(*expected_public_key); + if (!verity_data.ok()) { + return verity_data.error(); + } + // Supporting non-staged install of APEXes without a hashtree is additional + // hassle, it's easier not to support it. + if (verity_data->desc->tree_size == 0) { + return Error() << new_apex.GetPath() + << " does not have an embedded hash tree"; + } + return {}; +} + +Result<size_t> ComputePackageIdMinor(const ApexFile& apex) { + static constexpr size_t kMaxVerityDevicesPerApexName = 3u; + DeviceMapper& dm = DeviceMapper::Instance(); + std::vector<DeviceMapper::DmBlockDevice> dm_devices; + if (!dm.GetAvailableDevices(&dm_devices)) { + return Error() << "Failed to list dm devices"; + } + size_t devices = 0; + size_t next_minor = 1; + for (const auto& dm_device : dm_devices) { + std::string_view dm_name(dm_device.name()); + // Format is <module_name>@<version_code>[_<minor>] + if (!ConsumePrefix(&dm_name, apex.GetManifest().name())) { + continue; + } + devices++; + auto pos = dm_name.find_last_of('_'); + if (pos == std::string_view::npos) { + continue; + } + size_t minor; + if (!ParseUint(std::string(dm_name.substr(pos + 1)), &minor)) { + return Error() << "Unexpected dm device name " << dm_device.name(); + } + if (next_minor < minor + 1) { + next_minor = minor + 1; + } + } + if (devices > kMaxVerityDevicesPerApexName) { + return Error() << "There are too many (" << devices + << ") dm block devices associated with package " + << apex.GetManifest().name(); + } + while (true) { + std::string target_file = + StringPrintf("%s/%s_%zu.apex", gConfig->active_apex_data_dir, + GetPackageId(apex.GetManifest()).c_str(), next_minor); + if (access(target_file.c_str(), F_OK) == 0) { + next_minor++; + } else { + break; + } + } + + return next_minor; +} + +Result<void> UpdateApexInfoList() { + std::vector<ApexFile> active(GetActivePackages()); + std::vector<ApexFile> inactive = CalculateInactivePackages(active); + + std::stringstream xml; + CollectApexInfoList(xml, active, inactive); + + std::string name = StringPrintf("%s/.default-%s", kApexRoot, kApexInfoList); + unique_fd fd(TEMP_FAILURE_RETRY( + open(name.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644))); + if (fd.get() == -1) { + return ErrnoError() << "Can't open " << name; + } + if (!WriteStringToFd(xml.str(), fd)) { + return ErrnoError() << "Failed to write to " << name; + } + + return {}; +} + +Result<ApexFile> InstallPackage(const std::string& package_path) { + LOG(INFO) << "Installing " << package_path; + auto temp_apex = ApexFile::Open(package_path); + if (!temp_apex.ok()) { + return temp_apex.error(); + } + + const std::string& module_name = temp_apex->GetManifest().name(); + // Don't allow non-staged update if there are no active versions of this APEX. + auto cur_mounted_data = gMountedApexes.GetLatestMountedApex(module_name); + if (!cur_mounted_data.has_value()) { + return Error() << "No active version found for package " << module_name; + } + + auto cur_apex = ApexFile::Open(cur_mounted_data->full_path); + if (!cur_apex.ok()) { + return cur_apex.error(); + } + + // Do a quick check if this APEX can be installed without a reboot. + // Note that passing this check doesn't guarantee that APEX will be + // successfully installed. + if (auto r = CheckSupportsNonStagedInstall(*cur_apex, *temp_apex); !r.ok()) { + return r.error(); + } + + // 1. Verify that APEX is correct. This is a heavy check that involves + // mounting an APEX on a temporary mount point and reading the entire + // dm-verity block device. + if (auto verify = VerifyPackageNonStagedInstall(*temp_apex); !verify.ok()) { + return verify.error(); + } + + // 2. Compute params for mounting new apex. + auto new_id_minor = ComputePackageIdMinor(*temp_apex); + if (!new_id_minor.ok()) { + return new_id_minor.error(); + } + + std::string new_id = GetPackageId(temp_apex->GetManifest()) + "_" + + std::to_string(*new_id_minor); + + // 2. Unmount currently active APEX. + if (auto res = UnmountPackage(*cur_apex, /* allow_latest= */ true, + /* deferred= */ true); + !res.ok()) { + return res.error(); + } + + // 3. Hard link to final destination. + std::string target_file = + StringPrintf("%s/%s.apex", gConfig->active_apex_data_dir, new_id.c_str()); + + auto guard = android::base::make_scope_guard([&]() { + if (unlink(target_file.c_str()) != 0 && errno != ENOENT) { + PLOG(ERROR) << "Failed to unlink " << target_file; + } + // We can't really rely on the fact that dm-verity device backing up + // previously active APEX is still around. We need to create a new one. + std::string old_new_id = GetPackageId(temp_apex->GetManifest()) + "_" + + std::to_string(*new_id_minor + 1); + if (auto res = ActivatePackageImpl(*cur_apex, old_new_id); !res.ok()) { + // At this point not much we can do... :( + LOG(ERROR) << res.error(); + } + }); + + // At this point it should be safe to hard link |temp_apex| to + // |params->target_file|. In case reboot happens during one of the stages + // below, then on next boot apexd will pick up the new verified APEX. + if (link(package_path.c_str(), target_file.c_str()) != 0) { + return ErrnoError() << "Failed to link " << package_path << " to " + << target_file; + } + + auto new_apex = ApexFile::Open(target_file); + if (!new_apex.ok()) { + return new_apex.error(); + } + + // 4. And activate new one. + if (auto res = ActivatePackageImpl(*new_apex, new_id); !res.ok()) { + return res.error(); + } + + // Accept the install. + guard.Disable(); + + // 4. Now we can unlink old APEX if it's not pre-installed. + if (!ApexFileRepository::GetInstance().IsPreInstalledApex(*cur_apex)) { + if (unlink(cur_mounted_data->full_path.c_str()) != 0) { + PLOG(ERROR) << "Failed to unlink " << cur_mounted_data->full_path; + } + } + + if (auto res = UpdateApexInfoList(); !res.ok()) { + LOG(ERROR) << res.error(); + } + + // Release compressed blocks in case target_file is on f2fs-compressed + // filesystem. + ReleaseF2fsCompressedBlocks(target_file); + + return new_apex; +} + } // namespace apex } // namespace android diff --git a/apexd/apexd.h b/apexd/apexd.h index 36f72beb..2465bde9 100644 --- a/apexd/apexd.h +++ b/apexd/apexd.h @@ -17,6 +17,7 @@ #ifndef ANDROID_APEXD_APEXD_H_ #define ANDROID_APEXD_APEXD_H_ +#include <ostream> #include <string> #include <vector> @@ -24,101 +25,177 @@ #include <android-base/result.h> #include "apex_constants.h" +#include "apex_database.h" #include "apex_file.h" +#include "apex_file_repository.h" +#include "apexd_session.h" namespace android { namespace apex { +// A structure containing all the values that might need to be injected for +// testing (e.g. apexd status property, etc.) +// +// Ideally we want to introduce Apexd class and use dependency injection for +// such values, but that will require a sizeable refactoring. For the time being +// this config should do the trick. +struct ApexdConfig { + const char* apex_status_sysprop; + std::vector<std::string> apex_built_in_dirs; + const char* active_apex_data_dir; + const char* decompression_dir; + const char* ota_reserved_dir; + const char* apex_hash_tree_dir; + const char* staged_session_dir; +}; + +static const ApexdConfig kDefaultConfig = { + kApexStatusSysprop, kApexPackageBuiltinDirs, kActiveApexPackagesDataDir, + kApexDecompressedDir, kOtaReservedDir, kApexHashTreeDir, + kStagedSessionsDir, +}; + class CheckpointInterface; -android::base::Result<void> resumeRevertIfNeeded(); +void SetConfig(const ApexdConfig& config); + +// Exposed only for testing. +android::base::Result<void> Unmount( + const MountedApexDatabase::MountedApexData& data, bool deferred); -// Keep it for now to make otapreopt_chroot keep happy. -// TODO(b/137086602): remove this function. -android::base::Result<void> scanPackagesDirAndActivate( - const char* apex_package_dir); -void scanStagedSessionsDirAndStage(); -android::base::Result<void> preinstallPackages( +android::base::Result<void> ResumeRevertIfNeeded(); + +android::base::Result<void> PreinstallPackages( const std::vector<std::string>& paths) WARN_UNUSED; -android::base::Result<void> postinstallPackages( +android::base::Result<void> PostinstallPackages( const std::vector<std::string>& paths) WARN_UNUSED; -android::base::Result<void> stagePackages( +android::base::Result<void> StagePackages( const std::vector<std::string>& tmpPaths) WARN_UNUSED; -android::base::Result<void> unstagePackages( +android::base::Result<void> UnstagePackages( const std::vector<std::string>& paths) WARN_UNUSED; -android::base::Result<std::vector<ApexFile>> submitStagedSession( +android::base::Result<std::vector<ApexFile>> SubmitStagedSession( const int session_id, const std::vector<int>& child_session_ids, const bool has_rollback_enabled, const bool is_rollback, const int rollback_id) WARN_UNUSED; -android::base::Result<void> markStagedSessionReady(const int session_id) +android::base::Result<void> MarkStagedSessionReady(const int session_id) WARN_UNUSED; -android::base::Result<void> markStagedSessionSuccessful(const int session_id) +android::base::Result<void> MarkStagedSessionSuccessful(const int session_id) WARN_UNUSED; -android::base::Result<void> revertActiveSessions( - const std::string& crashing_native_process); -android::base::Result<void> revertActiveSessionsAndReboot( - const std::string& crashing_native_process); - -android::base::Result<void> activatePackage(const std::string& full_path) +// Only only of the parameters should be passed during revert +android::base::Result<void> RevertActiveSessions( + const std::string& crashing_native_process, + const std::string& error_message); +// Only only of the parameters should be passed during revert +android::base::Result<void> RevertActiveSessionsAndReboot( + const std::string& crashing_native_process, + const std::string& error_message); + +android::base::Result<void> ActivatePackage(const std::string& full_path) WARN_UNUSED; -android::base::Result<void> deactivatePackage(const std::string& full_path) +android::base::Result<void> DeactivatePackage(const std::string& full_path) WARN_UNUSED; -std::vector<ApexFile> getActivePackages(); -android::base::Result<ApexFile> getActivePackage( +std::vector<ApexFile> GetActivePackages(); +android::base::Result<ApexFile> GetActivePackage( const std::string& package_name); -std::vector<ApexFile> getFactoryPackages(); +std::vector<ApexFile> GetFactoryPackages(); -android::base::Result<void> abortStagedSession(const int session_id); -android::base::Result<void> abortActiveSession(); +android::base::Result<void> AbortStagedSession(const int session_id); -android::base::Result<ino_t> snapshotCeData(const int user_id, - const int rollback_id, - const std::string& apex_name); -android::base::Result<void> restoreCeData(const int user_id, +android::base::Result<void> SnapshotCeData(const int user_id, + const int rollback_id, + const std::string& apex_name); +android::base::Result<void> RestoreCeData(const int user_id, const int rollback_id, const std::string& apex_name); -android::base::Result<void> destroyDeSnapshots(const int rollback_id); -android::base::Result<void> destroyCeSnapshotsNotSpecified( + +android::base::Result<void> DestroyDeSnapshots(const int rollback_id); +android::base::Result<void> DestroyCeSnapshots(const int user_id, + const int rollback_id); +android::base::Result<void> DestroyCeSnapshotsNotSpecified( int user_id, const std::vector<int>& retain_rollback_ids); -int onBootstrap(); -// Small helper function to tell if device is currently booting. -bool isBooting(); +int OnBootstrap(); // Sets the values of gVoldService and gInFsCheckpointMode. -void initializeVold(CheckpointInterface* checkpoint_service); +void InitializeVold(CheckpointInterface* checkpoint_service); // Initializes in-memory state (e.g. pre-installed data, activated apexes). // Must be called first before calling any other boot sequence related function. -void initialize(CheckpointInterface* checkpoint_service); +void Initialize(CheckpointInterface* checkpoint_service); +// Initializes data apex as in-memory state. Should be called only if we are +// not booting, since initialization timing is different when booting +void InitializeDataApex(); // Migrates sessions from /data/apex/session to /metadata/session.i // Must only be called during boot (i.e apexd.status is not "ready" or // "activated"). -android::base::Result<void> migrateSessionsDirIfNeeded(); +android::base::Result<void> MigrateSessionsDirIfNeeded(); // Apex activation logic. Scans staged apex sessions and activates apexes. // Must only be called during boot (i.e apexd.status is not "ready" or // "activated"). -void onStart(); +void OnStart(); +// For every package X, there can be at most two APEX, pre-installed vs +// installed on data. We decide which ones should be activated and return them +// as a list +std::vector<ApexFileRef> SelectApexForActivation( + const std::unordered_map<std::string, std::vector<ApexFileRef>>& all_apex, + const ApexFileRepository& instance); +std::vector<ApexFile> ProcessCompressedApex( + const std::vector<ApexFileRef>& compressed_apex, bool is_ota_chroot); +// Validate |apex| is same as |capex| +android::base::Result<void> ValidateDecompressedApex(const ApexFile& capex, + const ApexFile& apex); // Notifies system that apexes are activated by setting apexd.status property to // "activated". // Must only be called during boot (i.e. apexd.status is not "ready" or // "activated"). -void onAllPackagesActivated(); +void OnAllPackagesActivated(bool is_bootstrap); // Notifies system that apexes are ready by setting apexd.status property to // "ready". // Must only be called during boot (i.e. apexd.status is not "ready" or // "activated"). -void onAllPackagesReady(); -void bootCompletedCleanup(); -int snapshotOrRestoreDeUserData(); +void OnAllPackagesReady(); +void OnBootCompleted(); +// Exposed for testing +void RemoveInactiveDataApex(); +void BootCompletedCleanup(); +int SnapshotOrRestoreDeUserData(); + +int UnmountAll(); -int unmountAll(); +android::base::Result<MountedApexDatabase::MountedApexData> +GetTempMountedApexData(const std::string& package); // Optimistically tries to remount as many APEX packages as possible. // For more documentation see corresponding binder call in IApexService.aidl. -android::base::Result<void> remountPackages(); +android::base::Result<void> RemountPackages(); + +// Exposed for unit tests +android::base::Result<bool> ShouldAllocateSpaceForDecompression( + const std::string& new_apex_name, int64_t new_apex_version, + const ApexFileRepository& instance); + +void CollectApexInfoList(std::ostream& os, + const std::vector<ApexFile>& active_apexs, + const std::vector<ApexFile>& inactive_apexs); + +// Reserve |size| bytes in |dest_dir| by creating a zero-filled file +android::base::Result<void> ReserveSpaceForCompressedApex( + int64_t size, const std::string& dest_dir); + +// Activates apexes in otapreot_chroot environment. +// TODO(b/172911822): support compressed apexes. +int OnOtaChrootBootstrap(); + +// Activates flattened apexes in otapreopt_chroot environment. +int OnOtaChrootBootstrapFlattenedApex(); + +android::apex::MountedApexDatabase& GetApexDatabaseForTesting(); + +// Performs a non-staged install of an APEX specified by |package_path|. +// TODO(ioffe): add more documentation. +android::base::Result<ApexFile> InstallPackage(const std::string& package_path); } // namespace apex } // namespace android diff --git a/apexd/apexd_checkpoint.h b/apexd/apexd_checkpoint.h index 7177c047..2b4c3a12 100644 --- a/apexd/apexd_checkpoint.h +++ b/apexd/apexd_checkpoint.h @@ -32,7 +32,7 @@ class CheckpointInterface { virtual android::base::Result<bool> NeedsCheckpoint() = 0; virtual android::base::Result<bool> NeedsRollback() = 0; - virtual android::base::Result<void> StartCheckpoint(int32_t numRetries) = 0; + virtual android::base::Result<void> StartCheckpoint(int32_t num_retries) = 0; virtual android::base::Result<void> AbortChanges(const std::string& msg, bool retry) = 0; diff --git a/apexd/apexd_checkpoint_vold.cpp b/apexd/apexd_checkpoint_vold.cpp index ca29209d..8a8d0ff1 100644 --- a/apexd/apexd_checkpoint_vold.cpp +++ b/apexd/apexd_checkpoint_vold.cpp @@ -31,11 +31,11 @@ namespace android { namespace apex { Result<VoldCheckpointInterface> VoldCheckpointInterface::Create() { - auto voldService = + auto vold_service = defaultServiceManager()->getService(android::String16("vold")); - if (voldService != nullptr) { + if (vold_service != nullptr) { return VoldCheckpointInterface( - android::interface_cast<android::os::IVold>(voldService)); + android::interface_cast<android::os::IVold>(vold_service)); } return Errorf("Failed to retrieve vold service."); } @@ -91,9 +91,10 @@ Result<bool> VoldCheckpointInterface::NeedsRollback() { return false; } -Result<void> VoldCheckpointInterface::StartCheckpoint(int32_t numRetries) { +Result<void> VoldCheckpointInterface::StartCheckpoint(int32_t num_retries) { if (supports_fs_checkpoints_) { - android::binder::Status status = vold_service_->startCheckpoint(numRetries); + android::binder::Status status = + vold_service_->startCheckpoint(num_retries); if (!status.isOk()) { return Error() << status.toString8().c_str(); } diff --git a/apexd/apexd_checkpoint_vold.h b/apexd/apexd_checkpoint_vold.h index dbd190da..f547532e 100644 --- a/apexd/apexd_checkpoint_vold.h +++ b/apexd/apexd_checkpoint_vold.h @@ -44,7 +44,7 @@ class VoldCheckpointInterface : public CheckpointInterface { android::base::Result<void> StartCheckpoint(int32_t retry) override; android::base::Result<void> AbortChanges(const std::string& msg, - bool numRetries) override; + bool num_retries) override; static android::base::Result<VoldCheckpointInterface> Create(); diff --git a/apexd/apexd_prop.cpp b/apexd/apexd_lifecycle.cpp index 31030e10..41c1abef 100644 --- a/apexd/apexd_prop.cpp +++ b/apexd/apexd_lifecycle.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 The Android Open Source Project + * Copyright (C) 2020 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. @@ -16,23 +16,28 @@ #define LOG_TAG "apexd" -#include "apexd_prop.h" +#include "apexd_lifecycle.h" #include <android-base/logging.h> #include <android-base/properties.h> #include "apexd_utils.h" -using android::base::GetBoolProperty; using android::base::GetProperty; using android::base::Result; using android::base::WaitForProperty; namespace android { namespace apex { -void waitForBootStatus(Result<void> (&revert_fn)(const std::string&), - void (&complete_fn)()) { - while (!GetBoolProperty("sys.boot_completed", false)) { + +bool ApexdLifecycle::IsBooting() { + auto status = GetProperty(kApexStatusSysprop, ""); + return status != kApexStatusReady && status != kApexStatusActivated; +} + +void ApexdLifecycle::WaitForBootStatus( + Result<void> (&revert_fn)(const std::string&, const std::string&)) { + while (!boot_completed_) { // Check for change in either crashing property or sys.boot_completed // Wait for updatable_crashing property change for most of the time // (arbitrary 30s), briefly check if boot has completed successfully, @@ -44,7 +49,7 @@ void waitForBootStatus(Result<void> (&revert_fn)(const std::string&), auto name = GetProperty("sys.init.updatable_crashing_process_name", ""); LOG(ERROR) << "Native process '" << (name.empty() ? "[unknown]" : name) << "' is crashing. Attempting a revert"; - auto result = revert_fn(name); + auto result = revert_fn(name, ""); if (!result.ok()) { LOG(ERROR) << "Revert failed : " << result.error(); break; @@ -59,15 +64,9 @@ void waitForBootStatus(Result<void> (&revert_fn)(const std::string&), } } } - // Wait for boot to complete, and then run complete_fn. - // TODO(ioffe): this is a hack, instead we should have a binder call from - // system_server into apexd when boot completes. - if (WaitForProperty("sys.boot_completed", "1", std::chrono::minutes(5))) { - complete_fn(); - return; - } else { - LOG(ERROR) << "Boot never completed"; - } } + +void ApexdLifecycle::MarkBootCompleted() { boot_completed_ = true; } + } // namespace apex } // namespace android diff --git a/apexd/apexd_lifecycle.h b/apexd/apexd_lifecycle.h new file mode 100644 index 00000000..ecdef96e --- /dev/null +++ b/apexd/apexd_lifecycle.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2020 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. + */ + +#ifndef ANDROID_APEXD_APEXD_LIFECYCLE_H_ +#define ANDROID_APEXD_APEXD_LIFECYCLE_H_ + +#include <android-base/result.h> + +namespace android { +namespace apex { + +class ApexdLifecycle { + private: + ApexdLifecycle(){}; + std::atomic<bool> boot_completed_; + + // Non-copyable && non-moveable. + ApexdLifecycle(const ApexdLifecycle&) = delete; + ApexdLifecycle& operator=(const ApexdLifecycle&) = delete; + ApexdLifecycle& operator=(ApexdLifecycle&&) = delete; + + public: + static ApexdLifecycle& GetInstance() { + static ApexdLifecycle instance; + return instance; + } + bool IsBooting(); + void MarkBootCompleted(); + void WaitForBootStatus(android::base::Result<void> (&rollback_fn)( + const std::string&, const std::string&)); +}; +} // namespace apex +} // namespace android + +#endif // ANDROID_APEXD_APEXD_LIFECYCLE_H diff --git a/apexd/apexd_loop.cpp b/apexd/apexd_loop.cpp index 568eb055..08805eaf 100644 --- a/apexd/apexd_loop.cpp +++ b/apexd/apexd_loop.cpp @@ -18,29 +18,49 @@ #include "apexd_loop.h" +#include <mutex> + #include <dirent.h> #include <fcntl.h> #include <linux/fs.h> #include <linux/loop.h> #include <sys/ioctl.h> #include <sys/stat.h> +#include <sys/statfs.h> #include <sys/types.h> #include <unistd.h> #include <android-base/file.h> #include <android-base/logging.h> +#include <android-base/parseint.h> +#include <android-base/properties.h> #include <android-base/stringprintf.h> #include <android-base/strings.h> #include "apexd_utils.h" #include "string_log.h" +using android::base::Basename; +using android::base::ErrnoError; using android::base::Error; +using android::base::GetBoolProperty; +using android::base::ParseUint; using android::base::Result; using android::base::StartsWith; using android::base::StringPrintf; using android::base::unique_fd; +#ifndef LOOP_CONFIGURE +// These can be removed whenever we pull in the Linux v5.8 UAPI headers +struct loop_config { + __u32 fd; + __u32 block_size; + struct loop_info64 info; + __u64 __reserved[8]; +}; +#define LOOP_CONFIGURE 0x4C0A +#endif + namespace android { namespace apex { namespace loop { @@ -66,13 +86,9 @@ void LoopbackDeviceUniqueFd::MaybeCloseBad() { } } -Result<void> configureReadAhead(const std::string& device_path) { - auto pos = device_path.find("/dev/block/"); - if (pos != 0) { - return Error() << "Device path does not start with /dev/block."; - } - pos = device_path.find_last_of('/'); - std::string device_name = device_path.substr(pos + 1, std::string::npos); +Result<void> ConfigureReadAhead(const std::string& device_path) { + CHECK(StartsWith(device_path, "/dev/")); + std::string device_name = Basename(device_path); std::string sysfs_device = StringPrintf("/sys/block/%s/queue/read_ahead_kb", device_name.c_str()); @@ -90,10 +106,10 @@ Result<void> configureReadAhead(const std::string& device_path) { return {}; } -Result<void> preAllocateLoopDevices(size_t num) { - Result<void> loopReady = WaitForFile("/dev/loop-control", 20s); - if (!loopReady.ok()) { - return loopReady; +Result<void> PreAllocateLoopDevices(size_t num) { + Result<void> loop_ready = WaitForFile("/dev/loop-control", 20s); + if (!loop_ready.ok()) { + return loop_ready; } unique_fd ctl_fd( TEMP_FAILURE_RETRY(open("/dev/loop-control", O_RDWR | O_CLOEXEC))); @@ -101,13 +117,30 @@ Result<void> preAllocateLoopDevices(size_t num) { return ErrnoError() << "Failed to open loop-control"; } + bool found = false; + size_t start_id = 0; + constexpr const char* kLoopPrefix = "loop"; + WalkDir("/dev/block", [&](const std::filesystem::directory_entry& entry) { + std::string devname = entry.path().filename().string(); + if (StartsWith(devname, kLoopPrefix)) { + size_t id; + auto parse_ok = ParseUint( + devname.substr(std::char_traits<char>::length(kLoopPrefix)), &id); + if (parse_ok && id > start_id) { + start_id = id; + found = true; + } + } + }); + if (found) ++start_id; + // Assumption: loop device ID [0..num) is valid. // This is because pre-allocation happens during bootstrap. // Anyway Kernel pre-allocated loop devices // as many as CONFIG_BLK_DEV_LOOP_MIN_COUNT, // Within the amount of kernel-pre-allocation, // LOOP_CTL_ADD will fail with EEXIST - for (size_t id = 0ul; id < num; ++id) { + for (size_t id = start_id; id < num + start_id; ++id) { int ret = ioctl(ctl_fd.get(), LOOP_CTL_ADD, id); if (ret < 0 && errno != EEXIST) { return ErrnoError() << "Failed LOOP_CTL_ADD"; @@ -120,25 +153,30 @@ Result<void> preAllocateLoopDevices(size_t num) { // just optimistally hope that they are all created when we actually // access them for activating APEXes. If the dev nodes are not ready // even then, we wait 50ms and warning message will be printed (see below - // createLoopDevice()). + // CreateLoopDevice()). LOG(INFO) << "Pre-allocated " << num << " loopback devices"; return {}; } -Result<LoopbackDeviceUniqueFd> createLoopDevice(const std::string& target, - const int32_t imageOffset, - const size_t imageSize) { - unique_fd ctl_fd(open("/dev/loop-control", O_RDWR | O_CLOEXEC)); - if (ctl_fd.get() == -1) { - return ErrnoError() << "Failed to open loop-control"; - } - - int num = ioctl(ctl_fd.get(), LOOP_CTL_GET_FREE); - if (num == -1) { - return ErrnoError() << "Failed LOOP_CTL_GET_FREE"; - } - - std::string device = StringPrintf("/dev/block/loop%d", num); +Result<void> ConfigureLoopDevice(const int device_fd, const std::string& target, + const int32_t image_offset, + const size_t image_size) { + static bool use_loop_configure; + static std::once_flag once_flag; + std::call_once(once_flag, [&]() { + // LOOP_CONFIGURE is a new ioctl in Linux 5.8 (and backported in Android + // common) that allows atomically configuring a loop device. It is a lot + // faster than the traditional LOOP_SET_FD/LOOP_SET_STATUS64 combo, but + // it may not be available on updating devices, so try once before + // deciding. + struct loop_config config; + memset(&config, 0, sizeof(config)); + config.fd = -1; + if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1 && errno == EBADF) { + // If the IOCTL exists, it will fail with EBADF for the -1 fd + use_loop_configure = true; + } + }); /* * Using O_DIRECT will tell the kernel that we want to use Direct I/O @@ -151,74 +189,151 @@ Result<LoopbackDeviceUniqueFd> createLoopDevice(const std::string& target, */ unique_fd target_fd(open(target.c_str(), O_RDONLY | O_CLOEXEC | O_DIRECT)); if (target_fd.get() == -1) { - return ErrnoError() << "Failed to open " << target; + struct statfs stbuf; + int saved_errno = errno; + // let's give another try with buffered I/O for EROFS and squashfs + if (statfs(target.c_str(), &stbuf) != 0 || + (stbuf.f_type != EROFS_SUPER_MAGIC_V1 && + stbuf.f_type != SQUASHFS_MAGIC && + stbuf.f_type != OVERLAYFS_SUPER_MAGIC)) { + return Error(saved_errno) << "Failed to open " << target; + } + LOG(WARNING) << "Fallback to buffered I/O for " << target; + target_fd.reset(open(target.c_str(), O_RDONLY | O_CLOEXEC)); + if (target_fd.get() == -1) { + return ErrnoError() << "Failed to open " << target; + } + } + + struct loop_info64 li; + memset(&li, 0, sizeof(li)); + strlcpy((char*)li.lo_crypt_name, kApexLoopIdPrefix, LO_NAME_SIZE); + li.lo_offset = image_offset; + li.lo_sizelimit = image_size; + // Automatically free loop device on last close. + li.lo_flags |= LO_FLAGS_AUTOCLEAR; + + if (use_loop_configure) { + struct loop_config config; + memset(&config, 0, sizeof(config)); + li.lo_flags |= LO_FLAGS_DIRECT_IO; + config.fd = target_fd.get(); + config.info = li; + config.block_size = 4096; + + if (ioctl(device_fd, LOOP_CONFIGURE, &config) == -1) { + return ErrnoError() << "Failed to LOOP_CONFIGURE"; + } + + return {}; + } else { + if (ioctl(device_fd, LOOP_SET_FD, target_fd.get()) == -1) { + return ErrnoError() << "Failed to LOOP_SET_FD"; + } + + if (ioctl(device_fd, LOOP_SET_STATUS64, &li) == -1) { + return ErrnoError() << "Failed to LOOP_SET_STATUS64"; + } + + if (ioctl(device_fd, BLKFLSBUF, 0) == -1) { + // This works around a kernel bug where the following happens. + // 1) The device runs with a value of loop.max_part > 0 + // 2) As part of LOOP_SET_FD above, we do a partition scan, which loads + // the first 2 pages of the underlying file into the buffer cache + // 3) When we then change the offset with LOOP_SET_STATUS64, those pages + // are not invalidated from the cache. + // 4) When we try to mount an ext4 filesystem on the loop device, the ext4 + // code will try to find a superblock by reading 4k at offset 0; but, + // because we still have the old pages at offset 0 lying in the cache, + // those pages will be returned directly. However, those pages contain + // the data at offset 0 in the underlying file, not at the offset that + // we configured + // 5) the ext4 driver fails to find a superblock in the (wrong) data, and + // fails to mount the filesystem. + // + // To work around this, explicitly flush the block device, which will + // flush the buffer cache and make sure we actually read the data at the + // correct offset. + return ErrnoError() << "Failed to flush buffers on the loop device"; + } + + // Direct-IO requires the loop device to have the same block size as the + // underlying filesystem. + if (ioctl(device_fd, LOOP_SET_BLOCK_SIZE, 4096) == -1) { + PLOG(WARNING) << "Failed to LOOP_SET_BLOCK_SIZE"; + } } - LoopbackDeviceUniqueFd device_fd; - { - // See comment on kLoopDeviceRetryAttempts. - unique_fd sysfs_fd; - for (size_t i = 0; i != kLoopDeviceRetryAttempts; ++i) { + return {}; +} + +Result<LoopbackDeviceUniqueFd> WaitForDevice(int num) { + std::string opened_device; + const std::vector<std::string> candidate_devices = { + StringPrintf("/dev/block/loop%d", num), + StringPrintf("/dev/loop%d", num), + }; + + // apexd-bootstrap runs in parallel with ueventd to optimize boot time. In + // rare cases apexd would try attempt to mount an apex before ueventd created + // a loop device for it. To work around this we keep polling for loop device + // to be created until ueventd's cold boot sequence is done. + // See comment on kLoopDeviceRetryAttempts. + unique_fd sysfs_fd; + bool cold_boot_done = GetBoolProperty("ro.cold_boot_done", false); + for (size_t i = 0; i != kLoopDeviceRetryAttempts; ++i) { + if (!cold_boot_done) { + cold_boot_done = GetBoolProperty("ro.cold_boot_done", false); + } + for (const auto& device : candidate_devices) { sysfs_fd.reset(open(device.c_str(), O_RDWR | O_CLOEXEC)); if (sysfs_fd.get() != -1) { - break; + return LoopbackDeviceUniqueFd(std::move(sysfs_fd), device); } - PLOG(WARNING) << "Loopback device " << device - << " not ready. Waiting 50ms..."; - usleep(50000); } - if (sysfs_fd.get() == -1) { - return ErrnoError() << "Failed to open " << device; + PLOG(WARNING) << "Loopback device " << num << " not ready. Waiting 50ms..."; + usleep(50000); + if (!cold_boot_done) { + // ueventd hasn't finished cold boot yet, keep trying. + i = 0; } - device_fd = LoopbackDeviceUniqueFd(std::move(sysfs_fd), device); - CHECK_NE(device_fd.get(), -1); } - if (ioctl(device_fd.get(), LOOP_SET_FD, target_fd.get()) == -1) { - return ErrnoError() << "Failed to LOOP_SET_FD"; + return Error() << "Faled to open loopback device " << num; +} + +Result<LoopbackDeviceUniqueFd> CreateLoopDevice(const std::string& target, + const int32_t image_offset, + const size_t image_size) { + unique_fd ctl_fd(open("/dev/loop-control", O_RDWR | O_CLOEXEC)); + if (ctl_fd.get() == -1) { + return ErrnoError() << "Failed to open loop-control"; + } + + static std::mutex mlock; + std::lock_guard lock(mlock); + int num = ioctl(ctl_fd.get(), LOOP_CTL_GET_FREE); + if (num == -1) { + return ErrnoError() << "Failed LOOP_CTL_GET_FREE"; } - struct loop_info64 li; - memset(&li, 0, sizeof(li)); - strlcpy((char*)li.lo_crypt_name, kApexLoopIdPrefix, LO_NAME_SIZE); - li.lo_offset = imageOffset; - li.lo_sizelimit = imageSize; - if (ioctl(device_fd.get(), LOOP_SET_STATUS64, &li) == -1) { - return ErrnoError() << "Failed to LOOP_SET_STATUS64"; - } - - if (ioctl(device_fd.get(), BLKFLSBUF, 0) == -1) { - // This works around a kernel bug where the following happens. - // 1) The device runs with a value of loop.max_part > 0 - // 2) As part of LOOP_SET_FD above, we do a partition scan, which loads - // the first 2 pages of the underlying file into the buffer cache - // 3) When we then change the offset with LOOP_SET_STATUS64, those pages - // are not invalidated from the cache. - // 4) When we try to mount an ext4 filesystem on the loop device, the ext4 - // code will try to find a superblock by reading 4k at offset 0; but, - // because we still have the old pages at offset 0 lying in the cache, - // those pages will be returned directly. However, those pages contain - // the data at offset 0 in the underlying file, not at the offset that - // we configured - // 5) the ext4 driver fails to find a superblock in the (wrong) data, and - // fails to mount the filesystem. - // - // To work around this, explicitly flush the block device, which will flush - // the buffer cache and make sure we actually read the data at the correct - // offset. - return ErrnoError() << "Failed to flush buffers on the loop device"; - } - - // Direct-IO requires the loop device to have the same block size as the - // underlying filesystem. - if (ioctl(device_fd.get(), LOOP_SET_BLOCK_SIZE, 4096) == -1) { - PLOG(WARNING) << "Failed to LOOP_SET_BLOCK_SIZE"; - } - - Result<void> readAheadStatus = configureReadAhead(device); - if (!readAheadStatus.ok()) { - return readAheadStatus.error(); - } - return device_fd; + Result<LoopbackDeviceUniqueFd> loop_device = WaitForDevice(num); + if (!loop_device.ok()) { + return loop_device.error(); + } + CHECK_NE(loop_device->device_fd.get(), -1); + + Result<void> configureStatus = ConfigureLoopDevice( + loop_device->device_fd.get(), target, image_offset, image_size); + if (!configureStatus.ok()) { + return configureStatus.error(); + } + + Result<void> read_ahead_status = ConfigureReadAhead(loop_device->name); + if (!read_ahead_status.ok()) { + return read_ahead_status.error(); + } + + return loop_device; } void DestroyLoopDevice(const std::string& path, const DestroyLoopFn& extra) { diff --git a/apexd/apexd_loop.h b/apexd/apexd_loop.h index 8341675d..c727944a 100644 --- a/apexd/apexd_loop.h +++ b/apexd/apexd_loop.h @@ -38,7 +38,7 @@ struct LoopbackDeviceUniqueFd { : device_fd(std::move(fd)), name(name) {} LoopbackDeviceUniqueFd(LoopbackDeviceUniqueFd&& fd) noexcept - : device_fd(std::move(fd.device_fd)), name(fd.name) {} + : device_fd(std::move(fd.device_fd)), name(std::move(fd.name)) {} LoopbackDeviceUniqueFd& operator=(LoopbackDeviceUniqueFd&& other) noexcept { MaybeCloseBad(); device_fd = std::move(other.device_fd); @@ -52,16 +52,16 @@ struct LoopbackDeviceUniqueFd { void CloseGood() { device_fd.reset(-1); } - int get() { return device_fd.get(); } + int Get() { return device_fd.get(); } }; -android::base::Result<void> configureReadAhead(const std::string& device_path); +android::base::Result<void> ConfigureReadAhead(const std::string& device_path); -android::base::Result<void> preAllocateLoopDevices(size_t num); +android::base::Result<void> PreAllocateLoopDevices(size_t num); -android::base::Result<LoopbackDeviceUniqueFd> createLoopDevice( - const std::string& target, const int32_t imageOffset, - const size_t imageSize); +android::base::Result<LoopbackDeviceUniqueFd> CreateLoopDevice( + const std::string& target, const int32_t image_offset, + const size_t image_size); using DestroyLoopFn = std::function<void(const std::string&, const std::string&)>; diff --git a/apexd/apexd_main.cpp b/apexd/apexd_main.cpp index c60f4faf..26d5732e 100644 --- a/apexd/apexd_main.cpp +++ b/apexd/apexd_main.cpp @@ -17,14 +17,15 @@ #define LOG_TAG "apexd" #include <strings.h> +#include <sys/stat.h> #include <ApexProperties.sysprop.h> #include <android-base/logging.h> #include "apexd.h" #include "apexd_checkpoint_vold.h" +#include "apexd_lifecycle.h" #include "apexd_prepostinstall.h" -#include "apexd_prop.h" #include "apexservice.h" #include <android-base/properties.h> @@ -44,12 +45,17 @@ int HandleSubcommand(char** argv) { if (strcmp("--bootstrap", argv[1]) == 0) { LOG(INFO) << "Bootstrap subcommand detected"; - return android::apex::onBootstrap(); + return android::apex::OnBootstrap(); } if (strcmp("--unmount-all", argv[1]) == 0) { LOG(INFO) << "Unmount all subcommand detected"; - return android::apex::unmountAll(); + return android::apex::UnmountAll(); + } + + if (strcmp("--otachroot-bootstrap", argv[1]) == 0) { + LOG(INFO) << "OTA chroot bootstrap subcommand detected"; + return android::apex::OnOtaChrootBootstrap(); } if (strcmp("--snapshotde", argv[1]) == 0) { @@ -62,16 +68,16 @@ int HandleSubcommand(char** argv) { LOG(ERROR) << "Could not retrieve vold service: " << vold_service_st.error(); } else { - android::apex::initializeVold(&*vold_service_st); + android::apex::InitializeVold(&*vold_service_st); } - int result = android::apex::snapshotOrRestoreDeUserData(); + int result = android::apex::SnapshotOrRestoreDeUserData(); if (result == 0) { // Notify other components (e.g. init) that all APEXs are ready to be used // Note that it's important that the binder service is registered at this // point, since other system services might depend on it. - android::apex::onAllPackagesReady(); + android::apex::OnAllPackagesReady(); } return result; } @@ -98,20 +104,40 @@ void InstallSigtermSignalHandler() { int main(int /*argc*/, char** argv) { android::base::InitLogging(argv, &android::base::KernelLogger); - // TODO: add a -v flag or an external setting to change LogSeverity. - android::base::SetMinimumLogSeverity(android::base::VERBOSE); + // TODO(b/158468454): add a -v flag or an external setting to change severity. + android::base::SetMinimumLogSeverity(android::base::INFO); + + // set umask to 022 so that files/dirs created are accessible to other + // processes e.g.) apex-info-file.xml is supposed to be read by other + // processes + umask(022); InstallSigtermSignalHandler(); + android::apex::SetConfig(android::apex::kDefaultConfig); + + android::apex::ApexdLifecycle& lifecycle = + android::apex::ApexdLifecycle::GetInstance(); + bool booting = lifecycle.IsBooting(); + const bool has_subcommand = argv[1] != nullptr; if (!android::sysprop::ApexProperties::updatable().value_or(false)) { - LOG(INFO) << "This device does not support updatable APEX. Exiting"; if (!has_subcommand) { - // mark apexd as activated so that init can proceed - android::apex::onAllPackagesActivated(); + if (!booting) { + // We've finished booting, but for some reason somebody tried to start + // apexd. Simply exit. + return 0; + } + + LOG(INFO) << "This device does not support updatable APEX. Exiting"; + // Mark apexd as activated so that init can proceed. + android::apex::OnAllPackagesActivated(/*is_bootstrap=*/false); } else if (strcmp("--snapshotde", argv[1]) == 0) { + LOG(INFO) << "This device does not support updatable APEX. Exiting"; // mark apexd as ready - android::apex::onAllPackagesReady(); + android::apex::OnAllPackagesReady(); + } else if (strcmp("--otachroot-bootstrap", argv[1]) == 0) { + return android::apex::OnOtaChrootBootstrapFlattenedApex(); } return 0; } @@ -129,12 +155,21 @@ int main(int /*argc*/, char** argv) { } else { vold_service = &*vold_service_st; } - android::apex::initialize(vold_service); + android::apex::Initialize(vold_service); - bool booting = android::apex::isBooting(); if (booting) { - android::apex::migrateSessionsDirIfNeeded(); - android::apex::onStart(); + if (auto res = android::apex::MigrateSessionsDirIfNeeded(); !res.ok()) { + LOG(ERROR) << "Failed to migrate sessions to /metadata partition : " + << res.error(); + } + android::apex::OnStart(); + } else { + // TODO(b/172911822): Trying to use data apex related ApexFileRepository + // apis without initializing it should throw error. Also, unit tests should + // not pass without initialization. + // TODO(b/172911822): Consolidate this with Initialize() when + // ApexFileRepository can act as cache and re-scanning is not expensive + android::apex::InitializeDataApex(); } android::apex::binder::CreateAndRegisterService(); android::apex::binder::StartThreadPool(); @@ -146,10 +181,8 @@ int main(int /*argc*/, char** argv) { // themselves should wait for the ready status instead, which is set when // the "--snapshotde" subcommand is received and snapshot/restore is // complete. - android::apex::onAllPackagesActivated(); - android::apex::waitForBootStatus( - android::apex::revertActiveSessionsAndReboot, - android::apex::bootCompletedCleanup); + android::apex::OnAllPackagesActivated(/*is_bootstrap=*/false); + lifecycle.WaitForBootStatus(android::apex::RevertActiveSessionsAndReboot); } android::apex::binder::AllowServiceShutdown(); diff --git a/apexd/apexd_prepostinstall.cpp b/apexd/apexd_prepostinstall.cpp index 21b8650a..51b60b4b 100644 --- a/apexd/apexd_prepostinstall.cpp +++ b/apexd/apexd_prepostinstall.cpp @@ -35,6 +35,7 @@ #include "apex_database.h" #include "apex_file.h" +#include "apex_manifest.h" #include "apexd.h" #include "apexd_private.h" #include "apexd_utils.h" @@ -42,6 +43,7 @@ using android::base::Error; using android::base::Result; +using ::apex::proto::ApexManifest; namespace android { namespace apex { @@ -58,14 +60,12 @@ void CloseSTDDescriptors() { close(STDERR_FILENO); } -// Instead of temp mounting inside this fuction, we can make a caller do it. -// This will align with the plan of extending temp mounting to provide a -// way to run additional pre-reboot verification of an APEX. -// TODO(ioffe): pass mount points instead of apex files. template <typename Fn> -Result<void> StageFnInstall(const std::vector<ApexFile>& apexes, Fn fn, +Result<void> StageFnInstall(const std::vector<ApexFile>& apexes, + const std::vector<std::string>& mount_points, Fn fn, const char* arg, const char* name) { - // TODO: Support a session with more than one pre-install hook. + // TODO(b/158470023): consider supporting a session with more than one + // pre-install hook. int hook_idx = -1; for (size_t i = 0; i < apexes.size(); i++) { if (!(apexes[i].GetManifest().*fn)().empty()) { @@ -78,73 +78,32 @@ Result<void> StageFnInstall(const std::vector<ApexFile>& apexes, Fn fn, CHECK(hook_idx != -1); LOG(VERBOSE) << name << " for " << apexes[hook_idx].GetPath(); - std::vector<MountedApexData> mounted_apexes; - std::vector<std::string> activation_dirs; - auto preinstall_guard = android::base::make_scope_guard([&]() { - for (const auto& mount : mounted_apexes) { - Result<void> st = apexd_private::Unmount(mount); - if (!st.ok()) { - LOG(ERROR) << "Failed to unmount " << mount.full_path << " from " - << mount.mount_point << " after " << name << ": " - << st.error(); - } - } - for (const std::string& active_point : activation_dirs) { - if (0 != rmdir(active_point.c_str())) { - PLOG(ERROR) << "Could not delete temporary active point " - << active_point; - } - } - }); - - for (const ApexFile& apex : apexes) { - // 1) Mount the package. - std::string mount_point = - apexd_private::GetPackageTempMountPoint(apex.GetManifest()); - - auto mount_data = apexd_private::TempMountPackage(apex, mount_point); - if (!mount_data.ok()) { - return mount_data.error(); - } - mounted_apexes.push_back(std::move(*mount_data)); - - // Given the fact, that we only allow updates of existing APEXes, all the - // activation points will always be already created. Only scenario, when it - // won't be the case might be apexservice_test. But even then, it might be - // safer to move active_point creation logic to run after unshare. - // TODO(ioffe): move creation of activation points inside RunFnInstall? - // 2) Ensure there is an activation point, and we will clean it up. - std::string active_point = - apexd_private::GetActiveMountPoint(apex.GetManifest()); - if (0 == mkdir(active_point.c_str(), kMkdirMode)) { - activation_dirs.emplace_back(std::move(active_point)); - } else { - int saved_errno = errno; - if (saved_errno != EEXIST) { - return Error() << "Unable to create mount point" << active_point << ": " - << strerror(saved_errno); - } - } - } - - // 3) Create invocation args. + // Create invocation args. std::vector<std::string> args{ "/system/bin/apexd", arg, - mounted_apexes[hook_idx].mount_point, // Make the APEX with hook first. + mount_points[hook_idx] // Make the APEX with hook first. }; - for (size_t i = 0; i < mounted_apexes.size(); i++) { + for (size_t i = 0; i < mount_points.size(); i++) { if ((int)i != hook_idx) { - args.push_back(mounted_apexes[i].mount_point); + args.push_back(mount_points[i]); } } - std::string error_msg; - int res = ForkAndRun(args, &error_msg); - return res == 0 ? Result<void>{} : Error() << error_msg; + return ForkAndRun(args); } template <typename Fn> int RunFnInstall(char** in_argv, Fn fn, const char* name) { + std::vector<std::string> activation_dirs; + auto preinstall_guard = android::base::make_scope_guard([&]() { + for (const std::string& active_point : activation_dirs) { + if (0 != rmdir(active_point.c_str())) { + PLOG(ERROR) << "Could not delete temporary active point " + << active_point; + } + } + }); + // 1) Unshare. if (unshare(CLONE_NEWNS) != 0) { PLOG(ERROR) << "Failed to unshare() for apex " << name; @@ -160,7 +119,8 @@ int RunFnInstall(char** in_argv, Fn fn, const char* name) { std::string hook_path; { - auto bind_fn = [&fn, name](const std::string& mount_point) { + auto bind_fn = [&fn, name, + activation_dirs](const std::string& mount_point) mutable { std::string hook; std::string active_point; { @@ -183,6 +143,14 @@ int RunFnInstall(char** in_argv, Fn fn, const char* name) { const auto& manifest = *manifest_or; hook = (manifest.*fn)(); active_point = apexd_private::GetActiveMountPoint(manifest); + // Ensure there is an activation point. If not, create one and delete + // later. + if (0 == mkdir(active_point.c_str(), kMkdirMode)) { + activation_dirs.push_back(active_point); + } else if (errno != EEXIST) { + PLOG(ERROR) << "Unable to create mount point " << active_point; + _exit(205); + } } // 3) Activate the new apex. @@ -233,17 +201,19 @@ int RunFnInstall(char** in_argv, Fn fn, const char* name) { } // namespace -Result<void> StagePreInstall(const std::vector<ApexFile>& apexes) { - return StageFnInstall(apexes, &ApexManifest::preinstallhook, "--pre-install", - "pre-install"); +Result<void> StagePreInstall(const std::vector<ApexFile>& apexes, + const std::vector<std::string>& mount_points) { + return StageFnInstall(apexes, mount_points, &ApexManifest::preinstallhook, + "--pre-install", "pre-install"); } int RunPreInstall(char** in_argv) { return RunFnInstall(in_argv, &ApexManifest::preinstallhook, "pre-install"); } -Result<void> StagePostInstall(const std::vector<ApexFile>& apexes) { - return StageFnInstall(apexes, &ApexManifest::postinstallhook, +Result<void> StagePostInstall(const std::vector<ApexFile>& apexes, + const std::vector<std::string>& mount_points) { + return StageFnInstall(apexes, mount_points, &ApexManifest::postinstallhook, "--post-install", "post-install"); } diff --git a/apexd/apexd_prepostinstall.h b/apexd/apexd_prepostinstall.h index 66cd2f57..65125bae 100644 --- a/apexd/apexd_prepostinstall.h +++ b/apexd/apexd_prepostinstall.h @@ -27,16 +27,20 @@ namespace apex { class ApexFile; -// Temp mounts given apexes and then forks into: -// apexd --pre-install <mount-point-of-apex-with-hook> [<other-mount-points>] +// Forks into: apexd --pre-install <mount-point-of-apex-with-hook> +// [<other-mount-points>] The caller must pass the temp mount point for each +// apex file. android::base::Result<void> StagePreInstall( - const std::vector<ApexFile>& apexes); + const std::vector<ApexFile>& apexes, + const std::vector<std::string>& mount_points); int RunPreInstall(char** argv); -// Temp mounts given apexes and then forks into: -// apexd --post-install <mount-point-of-apex-with-hook> [<other-mount-points>] +// Forks into: apexd --post-install <mount-point-of-apex-with-hook> +// [<other-mount-points>] The caller must pass the temp mount point for each +// apex file. android::base::Result<void> StagePostInstall( - const std::vector<ApexFile>& apexes); + const std::vector<ApexFile>& apexes, + const std::vector<std::string>& mount_points); int RunPostInstall(char** argv); } // namespace apex diff --git a/apexd/apexd_private.h b/apexd/apexd_private.h index 1706e08b..b3108cff 100644 --- a/apexd/apexd_private.h +++ b/apexd/apexd_private.h @@ -21,6 +21,7 @@ #include <android-base/result.h> #include "apex_database.h" +#include "apex_file.h" #include "apex_manifest.h" namespace android { @@ -32,16 +33,16 @@ static constexpr int kMkdirMode = 0755; namespace apexd_private { -std::string GetPackageMountPoint(const ApexManifest& manifest); -std::string GetPackageTempMountPoint(const ApexManifest& manifest); -std::string GetActiveMountPoint(const ApexManifest& manifest); +std::string GetPackageMountPoint(const ::apex::proto::ApexManifest& manifest); +std::string GetPackageTempMountPoint( + const ::apex::proto::ApexManifest& manifest); +std::string GetActiveMountPoint(const ::apex::proto::ApexManifest& manifest); android::base::Result<void> BindMount(const std::string& target, const std::string& source); -android::base::Result<MountedApexDatabase::MountedApexData> TempMountPackage( - const ApexFile& apex, const std::string& mount_point); -android::base::Result<void> Unmount( - const MountedApexDatabase::MountedApexData& data); +android::base::Result<MountedApexDatabase::MountedApexData> +GetTempMountedApexData(const std::string& package); +android::base::Result<void> UnmountTempMount(const ApexFile& apex); } // namespace apexd_private } // namespace apex diff --git a/apexd/apexd_prop.h b/apexd/apexd_prop.h deleted file mode 100644 index 4b8a7661..00000000 --- a/apexd/apexd_prop.h +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -#ifndef ANDROID_APEXD_APEXD_PROP_H_ -#define ANDROID_APEXD_APEXD_PROP_H_ - -#include <android-base/result.h> - -namespace android { -namespace apex { - -void waitForBootStatus( - android::base::Result<void> (&rollback_fn)(const std::string&), - void (&complete_fn)()); - -} // namespace apex -} // namespace android - -#endif // ANDROID_APEXD_APEXD_PROP_H diff --git a/apexd/apexd_rollback_utils.h b/apexd/apexd_rollback_utils.h index 9990be26..c7fa05ec 100644 --- a/apexd/apexd_rollback_utils.h +++ b/apexd/apexd_rollback_utils.h @@ -25,10 +25,6 @@ #include <android-base/result.h> #include <android-base/scopeguard.h> #include <logwrap/logwrap.h> -#include <selinux/android.h> - -using android::base::Error; -using android::base::Result; namespace android { namespace apex { @@ -40,7 +36,7 @@ static constexpr const char* kCpPath = "/system/bin/cp"; * path. Note that this will fail if run before APEXes are mounted, due to a * dependency on runtime. */ -int32_t copy_directory_recursive(const char* from, const char* to) { +inline int32_t CopyDirectoryRecursive(const char* from, const char* to) { const char* const argv[] = { kCpPath, "-F", /* delete any existing destination file first @@ -62,15 +58,15 @@ int32_t copy_directory_recursive(const char* from, const char* to) { * from from_path into to_path. Note that this must be run after APEXes are * mounted. */ -inline Result<void> ReplaceFiles(const std::string& from_path, - const std::string& to_path) { +inline android::base::Result<void> ReplaceFiles(const std::string& from_path, + const std::string& to_path) { namespace fs = std::filesystem; std::error_code error_code; fs::remove_all(to_path, error_code); if (error_code) { - return Error() << "Failed to delete existing files at " << to_path << " : " - << error_code.message(); + return android::base::Error() << "Failed to delete existing files at " + << to_path << " : " << error_code.message(); } auto deleter = [&] { @@ -83,23 +79,15 @@ inline Result<void> ReplaceFiles(const std::string& from_path, }; auto scope_guard = android::base::make_scope_guard(deleter); - int rc = copy_directory_recursive(from_path.c_str(), to_path.c_str()); + int rc = CopyDirectoryRecursive(from_path.c_str(), to_path.c_str()); if (rc != 0) { - return Error() << "Failed to copy from [" << from_path << "] to [" - << to_path << "]"; + return android::base::Error() << "Failed to copy from [" << from_path + << "] to [" << to_path << "]"; } scope_guard.Disable(); return {}; } -inline Result<void> RestoreconPath(const std::string& path) { - unsigned int seflags = SELINUX_ANDROID_RESTORECON_RECURSE; - if (selinux_android_restorecon(path.c_str(), seflags) < 0) { - return Error() << "Failed to restorecon " << path; - } - return {}; -} - } // namespace apex } // namespace android diff --git a/apexd/apexd_session.cpp b/apexd/apexd_session.cpp index 3d916c48..defef6fa 100644 --- a/apexd/apexd_session.cpp +++ b/apexd/apexd_session.cpp @@ -22,6 +22,7 @@ #include "session_state.pb.h" #include <android-base/logging.h> +#include <android-base/stringprintf.h> #include <dirent.h> #include <sys/stat.h> @@ -32,6 +33,7 @@ using android::base::Error; using android::base::Result; +using android::base::StringPrintf; using apex::proto::SessionState; namespace android { @@ -39,68 +41,57 @@ namespace apex { namespace { -static constexpr const char* kStateFileName = "state"; +// Starting from R, apexd prefers /metadata partition (kNewApexSessionsDir) as +// location for sessions-related information. For devices that don't have +// /metadata partition, apexd will fallback to the /data one +// (kOldApexSessionsDir). +static constexpr const char* kOldApexSessionsDir = "/data/apex/sessions"; +static constexpr const char* kNewApexSessionsDir = "/metadata/apex/sessions"; -std::string getSessionDir(int session_id) { - return kApexSessionsDir + "/" + std::to_string(session_id); -} +static constexpr const char* kStateFileName = "state"; -std::string getSessionStateFilePath(int session_id) { - return getSessionDir(session_id) + "/" + kStateFileName; -} +} // namespace -Result<std::string> createSessionDirIfNeeded(int session_id) { - // create /data/sessions - auto res = createDirIfNeeded(kApexSessionsDir, 0700); - if (!res.ok()) { - return res.error(); - } - // create /data/sessions/session_id - std::string sessionDir = getSessionDir(session_id); - res = createDirIfNeeded(sessionDir, 0700); - if (!res.ok()) { - return res.error(); - } +ApexSession::ApexSession(SessionState state) : state_(std::move(state)) {} - return sessionDir; +std::string ApexSession::GetSessionsDir() { + static std::string result; + static std::once_flag once_flag; + std::call_once(once_flag, [&]() { + auto status = + FindFirstExistingDirectory(kNewApexSessionsDir, kOldApexSessionsDir); + if (!status.ok()) { + LOG(FATAL) << status.error(); + } + result = std::move(*status); + }); + return result; } -Result<void> deleteSessionDir(int session_id) { - std::string session_dir = getSessionDir(session_id); - LOG(DEBUG) << "Deleting " << session_dir; - auto path = std::filesystem::path(session_dir); - std::error_code error_code; - std::filesystem::remove_all(path, error_code); - if (error_code) { - return Error() << "Failed to delete " << session_dir << " : " - << error_code.message(); - } - return {}; +Result<void> ApexSession::MigrateToMetadataSessionsDir() { + return MoveDir(kOldApexSessionsDir, kNewApexSessionsDir); } -} // namespace - -ApexSession::ApexSession(SessionState state) : state_(std::move(state)) {} - Result<ApexSession> ApexSession::CreateSession(int session_id) { SessionState state; // Create session directory - auto sessionPath = createSessionDirIfNeeded(session_id); - if (!sessionPath.ok()) { - return sessionPath.error(); + std::string session_dir = GetSessionsDir() + "/" + std::to_string(session_id); + if (auto status = CreateDirIfNeeded(session_dir, 0700); !status.ok()) { + return status.error(); } state.set_id(session_id); return ApexSession(state); } + Result<ApexSession> ApexSession::GetSessionFromFile(const std::string& path) { SessionState state; - std::fstream stateFile(path, std::ios::in | std::ios::binary); - if (!stateFile) { + std::fstream state_file(path, std::ios::in | std::ios::binary); + if (!state_file) { return Error() << "Failed to open " << path; } - if (!state.ParseFromIstream(&stateFile)) { + if (!state.ParseFromIstream(&state_file)) { return Error() << "Failed to parse " << path; } @@ -108,7 +99,8 @@ Result<ApexSession> ApexSession::GetSessionFromFile(const std::string& path) { } Result<ApexSession> ApexSession::GetSession(int session_id) { - auto path = getSessionStateFilePath(session_id); + auto path = StringPrintf("%s/%d/%s", GetSessionsDir().c_str(), session_id, + kStateFileName); return GetSessionFromFile(path); } @@ -116,19 +108,19 @@ Result<ApexSession> ApexSession::GetSession(int session_id) { std::vector<ApexSession> ApexSession::GetSessions() { std::vector<ApexSession> sessions; - Result<std::vector<std::string>> sessionPaths = ReadDir( - kApexSessionsDir, [](const std::filesystem::directory_entry& entry) { + Result<std::vector<std::string>> session_paths = ReadDir( + GetSessionsDir(), [](const std::filesystem::directory_entry& entry) { std::error_code ec; return entry.is_directory(ec); }); - if (!sessionPaths.ok()) { + if (!session_paths.ok()) { return sessions; } - for (const std::string& sessionDirPath : *sessionPaths) { + for (const std::string& session_dir_path : *session_paths) { // Try to read session state - auto session = GetSessionFromFile(sessionDirPath + "/" + kStateFileName); + auto session = GetSessionFromFile(session_dir_path + "/" + kStateFileName); if (!session.ok()) { LOG(WARNING) << session.error(); continue; @@ -152,20 +144,20 @@ std::vector<ApexSession> ApexSession::GetSessionsInState( std::vector<ApexSession> ApexSession::GetActiveSessions() { auto sessions = GetSessions(); - std::vector<ApexSession> activeSessions; + std::vector<ApexSession> active_sessions; for (const ApexSession& session : sessions) { if (!session.IsFinalized() && session.GetState() != SessionState::UNKNOWN) { - activeSessions.push_back(session); + active_sessions.push_back(session); } } - return activeSessions; + return active_sessions; } SessionState::State ApexSession::GetState() const { return state_.state(); } int ApexSession::GetId() const { return state_.id(); } -std::string ApexSession::GetBuildFingerprint() const { +const std::string& ApexSession::GetBuildFingerprint() const { return state_.expected_build_fingerprint(); } @@ -189,10 +181,14 @@ bool ApexSession::IsRollback() const { return state_.is_rollback(); } int ApexSession::GetRollbackId() const { return state_.rollback_id(); } -std::string ApexSession::GetCrashingNativeProcess() const { +const std::string& ApexSession::GetCrashingNativeProcess() const { return state_.crashing_native_process(); } +const std::string& ApexSession::GetErrorMessage() const { + return state_.error_message(); +} + const google::protobuf::RepeatedField<int> ApexSession::GetChildSessionIds() const { return state_.child_session_ids(); @@ -230,6 +226,10 @@ void ApexSession::SetCrashingNativeProcess( state_.set_crashing_native_process(crashing_process); } +void ApexSession::SetErrorMessage(const std::string& error_message) { + state_.set_error_message(error_message); +} + void ApexSession::AddApexName(const std::string& apex_name) { state_.add_apex_names(apex_name); } @@ -238,19 +238,29 @@ Result<void> ApexSession::UpdateStateAndCommit( const SessionState::State& session_state) { state_.set_state(session_state); - auto stateFilePath = getSessionStateFilePath(state_.id()); + auto state_file_path = StringPrintf("%s/%d/%s", GetSessionsDir().c_str(), + state_.id(), kStateFileName); - std::fstream stateFile(stateFilePath, - std::ios::out | std::ios::trunc | std::ios::binary); - if (!state_.SerializeToOstream(&stateFile)) { - return Error() << "Failed to write state file " << stateFilePath; + std::fstream state_file(state_file_path, + std::ios::out | std::ios::trunc | std::ios::binary); + if (!state_.SerializeToOstream(&state_file)) { + return Error() << "Failed to write state file " << state_file_path; } return {}; } Result<void> ApexSession::DeleteSession() const { - return deleteSessionDir(GetId()); + std::string session_dir = GetSessionsDir() + "/" + std::to_string(GetId()); + LOG(INFO) << "Deleting " << session_dir; + auto path = std::filesystem::path(session_dir); + std::error_code error_code; + std::filesystem::remove_all(path, error_code); + if (error_code) { + return Error() << "Failed to delete " << session_dir << " : " + << error_code.message(); + } + return {}; } std::ostream& operator<<(std::ostream& out, const ApexSession& session) { @@ -259,5 +269,18 @@ std::ostream& operator<<(std::ostream& out, const ApexSession& session) { << "]"; } +void ApexSession::DeleteFinalizedSessions() { + auto sessions = GetSessions(); + for (const ApexSession& session : sessions) { + if (!session.IsFinalized()) { + continue; + } + auto result = session.DeleteSession(); + if (!result.ok()) { + LOG(WARNING) << "Failed to delete finalized session: " << session.GetId(); + } + } +} + } // namespace apex } // namespace android diff --git a/apexd/apexd_session.h b/apexd/apexd_session.h index e0cea91c..5c1f1e3e 100644 --- a/apexd/apexd_session.h +++ b/apexd/apexd_session.h @@ -28,10 +28,18 @@ namespace android { namespace apex { -static const std::string kApexSessionsDir = "/metadata/apex/sessions"; - class ApexSession { public: + // Returns top-level directory to store sessions metadata in. + // If device has /metadata partition, this will return + // /metadata/apex/sessions, on all other devices it will return + // /data/apex/sessions. + static std::string GetSessionsDir(); + // Migrates content of /data/apex/sessions to /metadata/apex/sessions. + // If device doesn't have /metadata partition this call will be a no-op. + // If /data/apex/sessions this call will also be a no-op. + static android::base::Result<void> MigrateToMetadataSessionsDir(); + static android::base::Result<ApexSession> CreateSession(int session_id); static android::base::Result<ApexSession> GetSession(int session_id); static std::vector<ApexSession> GetSessions(); @@ -44,8 +52,9 @@ class ApexSession { const google::protobuf::RepeatedField<int> GetChildSessionIds() const; ::apex::proto::SessionState::State GetState() const; int GetId() const; - std::string GetBuildFingerprint() const; - std::string GetCrashingNativeProcess() const; + const std::string& GetBuildFingerprint() const; + const std::string& GetCrashingNativeProcess() const; + const std::string& GetErrorMessage() const; bool IsFinalized() const; bool HasRollbackEnabled() const; bool IsRollback() const; @@ -58,15 +67,17 @@ class ApexSession { void SetIsRollback(const bool is_rollback); void SetRollbackId(const int rollback_id); void SetCrashingNativeProcess(const std::string& crashing_process); + void SetErrorMessage(const std::string& error_message); void AddApexName(const std::string& apex_name); android::base::Result<void> UpdateStateAndCommit( const ::apex::proto::SessionState::State& state); android::base::Result<void> DeleteSession() const; + static void DeleteFinalizedSessions(); private: - ApexSession(::apex::proto::SessionState state); + explicit ApexSession(::apex::proto::SessionState state); ::apex::proto::SessionState state_; static android::base::Result<ApexSession> GetSessionFromFile( diff --git a/apexd/apexd_session_test.cpp b/apexd/apexd_session_test.cpp new file mode 100644 index 00000000..50d0f858 --- /dev/null +++ b/apexd/apexd_session_test.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2020 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 <filesystem> +#include <fstream> +#include <string> + +#include <errno.h> + +#include <android-base/file.h> +#include <android-base/result.h> +#include <android-base/scopeguard.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> + +#include "apexd_session.h" +#include "apexd_test_utils.h" +#include "apexd_utils.h" +#include "session_state.pb.h" + +namespace android { +namespace apex { +namespace { + +using android::apex::testing::IsOk; +using android::base::Join; +using android::base::make_scope_guard; + +// TODO(b/170329726): add unit tests for apexd_sessions.h + +TEST(ApexdSessionTest, GetSessionsDirSessionsStoredInMetadata) { + if (access("/metadata", F_OK) != 0) { + GTEST_SKIP() << "Device doesn't have /metadata partition"; + } + + std::string result = ApexSession::GetSessionsDir(); + ASSERT_EQ(result, "/metadata/apex/sessions"); +} + +TEST(ApexdSessionTest, GetSessionsDirNoMetadataPartitionFallbackToData) { + if (access("/metadata", F_OK) == 0) { + GTEST_SKIP() << "Device has /metadata partition"; + } + + std::string result = ApexSession::GetSessionsDir(); + ASSERT_EQ(result, "/data/apex/sessions"); +} + +TEST(ApexdSessionTest, MigrateToMetadataSessionsDir) { + namespace fs = std::filesystem; + + if (access("/metadata", F_OK) != 0) { + GTEST_SKIP() << "Device doesn't have /metadata partition"; + } + + // This is ugly, but does the job. To have a truly hermetic unit tests we + // need to refactor ApexSession class. + for (const auto& entry : fs::directory_iterator("/metadata/apex/sessions")) { + fs::remove_all(entry.path()); + } + + // This is a very ugly test set up, but to have something better we need to + // refactor ApexSession class. + class TestApexSession { + public: + TestApexSession(int id, const SessionState::State& state) { + path_ = "/data/apex/sessions/" + std::to_string(id); + if (auto status = CreateDirIfNeeded(path_, 0700); !status.ok()) { + ADD_FAILURE() << "Failed to create " << path_ << " : " + << status.error(); + } + SessionState session; + session.set_id(id); + session.set_state(state); + std::fstream state_file( + path_ + "/state", std::ios::out | std::ios::trunc | std::ios::binary); + if (!session.SerializeToOstream(&state_file)) { + ADD_FAILURE() << "Failed to write to " << path_; + } + } + + ~TestApexSession() { fs::remove_all(path_); } + + private: + std::string path_; + }; + + auto deleter = make_scope_guard([&]() { + fs::remove_all("/metadata/apex/sessions/239"); + fs::remove_all("/metadata/apex/sessions/1543"); + }); + + TestApexSession session1(239, SessionState::SUCCESS); + TestApexSession session2(1543, SessionState::ACTIVATION_FAILED); + + ASSERT_TRUE(IsOk(ApexSession::MigrateToMetadataSessionsDir())); + + auto sessions = ApexSession::GetSessions(); + ASSERT_EQ(2u, sessions.size()) << Join(sessions, ','); + + auto migrated_session_1 = ApexSession::GetSession(239); + ASSERT_TRUE(IsOk(migrated_session_1)); + ASSERT_EQ(SessionState::SUCCESS, migrated_session_1->GetState()); + + auto migrated_session_2 = ApexSession::GetSession(1543); + ASSERT_TRUE(IsOk(migrated_session_2)); + ASSERT_EQ(SessionState::ACTIVATION_FAILED, migrated_session_2->GetState()); +} + +} // namespace +} // namespace apex +} // namespace android diff --git a/apexd/apexd_test.cpp b/apexd/apexd_test.cpp new file mode 100644 index 00000000..6e62a7f3 --- /dev/null +++ b/apexd/apexd_test.cpp @@ -0,0 +1,3524 @@ +/* + * 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 <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/properties.h> +#include <android-base/scopeguard.h> +#include <android-base/stringprintf.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> + +#include "apex_database.h" +#include "apex_file_repository.h" +#include "apexd.h" +#include "apexd_checkpoint.h" +#include "apexd_session.h" +#include "apexd_test_utils.h" +#include "apexd_utils.h" + +#include "apex_manifest.pb.h" +#include "com_android_apex.h" +#include "gmock/gmock-matchers.h" + +namespace android { +namespace apex { + +namespace fs = std::filesystem; + +using MountedApexData = MountedApexDatabase::MountedApexData; +using android::apex::testing::ApexFileEq; +using android::apex::testing::IsOk; +using android::base::GetExecutableDirectory; +using android::base::GetProperty; +using android::base::make_scope_guard; +using android::base::RemoveFileIfExists; +using android::base::Result; +using android::base::StringPrintf; +using android::base::unique_fd; +using android::base::WriteStringToFile; +using com::android::apex::testing::ApexInfoXmlEq; +using ::testing::ByRef; +using ::testing::HasSubstr; +using ::testing::IsEmpty; +using ::testing::StartsWith; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +static std::string GetTestDataDir() { return GetExecutableDirectory(); } +static std::string GetTestFile(const std::string& name) { + return GetTestDataDir() + "/" + name; +} + +static int64_t GetMTime(const std::string& path) { + struct stat st_buf; + if (stat(path.c_str(), &st_buf) != 0) { + PLOG(ERROR) << "Failed to stat " << path; + return 0; + } + return st_buf.st_mtime; +} + +// A very basic mock of CheckpointInterface. +class MockCheckpointInterface : public CheckpointInterface { + public: + Result<bool> SupportsFsCheckpoints() override { return {}; } + + Result<bool> NeedsCheckpoint() override { return false; } + + Result<bool> NeedsRollback() override { return false; } + + Result<void> StartCheckpoint(int32_t num_retries) override { return {}; } + + Result<void> AbortChanges(const std::string& msg, bool retry) override { + return {}; + } +}; + +static constexpr const char* kTestApexdStatusSysprop = "apexd.status.test"; + +// A test fixture that provides frequently required temp directories for tests +class ApexdUnitTest : public ::testing::Test { + public: + ApexdUnitTest() { + built_in_dir_ = StringPrintf("%s/pre-installed-apex", td_.path); + data_dir_ = StringPrintf("%s/data-apex", td_.path); + decompression_dir_ = StringPrintf("%s/decompressed-apex", td_.path); + ota_reserved_dir_ = StringPrintf("%s/ota-reserved", td_.path); + hash_tree_dir_ = StringPrintf("%s/apex-hash-tree", td_.path); + staged_session_dir_ = StringPrintf("%s/staged-session-dir", td_.path); + config_ = {kTestApexdStatusSysprop, {built_in_dir_}, + data_dir_.c_str(), decompression_dir_.c_str(), + ota_reserved_dir_.c_str(), hash_tree_dir_.c_str(), + staged_session_dir_.c_str()}; + } + + const std::string& GetBuiltInDir() { return built_in_dir_; } + const std::string& GetDataDir() { return data_dir_; } + const std::string& GetDecompressionDir() { return decompression_dir_; } + const std::string& GetOtaReservedDir() { return ota_reserved_dir_; } + const std::string& GetHashTreeDir() { return hash_tree_dir_; } + const std::string GetStagedDir(int session_id) { + return StringPrintf("%s/session_%d", staged_session_dir_.c_str(), + session_id); + } + + 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()); + } + + // Copies the compressed apex to |built_in_dir| and decompresses it to + // |decompressed_dir| and then hard links to |target_dir| + std::string PrepareCompressedApex(const std::string& name, + const std::string& built_in_dir) { + fs::copy(GetTestFile(name), built_in_dir); + auto compressed_apex = ApexFile::Open( + StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str())); + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex)); + auto return_value = + ProcessCompressedApex(compressed_apex_list, /*is_ota_chroot*/ false); + return StringPrintf("%s/%s", built_in_dir.c_str(), name.c_str()); + } + + std::string PrepareCompressedApex(const std::string& name) { + return PrepareCompressedApex(name, built_in_dir_); + } + + Result<ApexSession> CreateStagedSession(const std::string& apex_name, + int session_id) { + CreateDirIfNeeded(GetStagedDir(session_id), 0755); + fs::copy(GetTestFile(apex_name), GetStagedDir(session_id)); + auto result = ApexSession::CreateSession(session_id); + result->SetBuildFingerprint(GetProperty("ro.build.fingerprint", "")); + return result; + } + + protected: + void SetUp() override { + SetConfig(config_); + ApexFileRepository::GetInstance().Reset(decompression_dir_); + ASSERT_EQ(mkdir(built_in_dir_.c_str(), 0755), 0); + ASSERT_EQ(mkdir(data_dir_.c_str(), 0755), 0); + ASSERT_EQ(mkdir(decompression_dir_.c_str(), 0755), 0); + ASSERT_EQ(mkdir(ota_reserved_dir_.c_str(), 0755), 0); + ASSERT_EQ(mkdir(hash_tree_dir_.c_str(), 0755), 0); + ASSERT_EQ(mkdir(staged_session_dir_.c_str(), 0755), 0); + + DeleteDirContent(ApexSession::GetSessionsDir()); + } + + void TearDown() override { DeleteDirContent(ApexSession::GetSessionsDir()); } + + private: + TemporaryDir td_; + std::string built_in_dir_; + std::string data_dir_; + std::string decompression_dir_; + std::string ota_reserved_dir_; + std::string hash_tree_dir_; + std::string staged_session_dir_; + ApexdConfig config_; +}; + +// Apex that does not have pre-installed version, does not get selected +TEST_F(ApexdUnitTest, ApexMustHavePreInstalledVersionForSelection) { + AddPreInstalledApex("apex.apexd_test.apex"); + AddPreInstalledApex("com.android.apex.cts.shim.apex"); + auto shared_lib_1 = ApexFile::Open(AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); + auto& instance = ApexFileRepository::GetInstance(); + // Pre-installed data needs to be present so that we can add data apex + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + auto apexd_test_file = ApexFile::Open(AddDataApex("apex.apexd_test.apex")); + auto shim_v1 = ApexFile::Open(AddDataApex("com.android.apex.cts.shim.apex")); + auto shared_lib_2 = ApexFile::Open( + AddDataApex("com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + + const auto all_apex = instance.AllApexFilesByName(); + // Pass a blank instance so that the data apex files are not considered + // pre-installed + const ApexFileRepository instance_blank; + auto result = SelectApexForActivation(all_apex, instance_blank); + ASSERT_EQ(result.size(), 0u); + // When passed proper instance they should get selected + result = SelectApexForActivation(all_apex, instance); + ASSERT_EQ(result.size(), 4u); + ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file)), + ApexFileEq(ByRef(*shim_v1)), + ApexFileEq(ByRef(*shared_lib_1)), + ApexFileEq(ByRef(*shared_lib_2)))); +} + +// Higher version gets priority when selecting for activation +TEST_F(ApexdUnitTest, HigherVersionOfApexIsSelected) { + auto apexd_test_file_v2 = + ApexFile::Open(AddPreInstalledApex("apex.apexd_test_v2.apex")); + AddPreInstalledApex("com.android.apex.cts.shim.apex"); + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + TemporaryDir data_dir; + AddDataApex("apex.apexd_test.apex"); + auto shim_v2 = + ApexFile::Open(AddDataApex("com.android.apex.cts.shim.v2.apex")); + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + + auto all_apex = instance.AllApexFilesByName(); + auto result = SelectApexForActivation(all_apex, instance); + ASSERT_EQ(result.size(), 2u); + + ASSERT_THAT(result, + UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file_v2)), + ApexFileEq(ByRef(*shim_v2)))); +} + +// When versions are equal, non-pre-installed version gets priority +TEST_F(ApexdUnitTest, DataApexGetsPriorityForSameVersions) { + AddPreInstalledApex("apex.apexd_test.apex"); + AddPreInstalledApex("com.android.apex.cts.shim.apex"); + // Initialize pre-installed APEX information + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + auto apexd_test_file = ApexFile::Open(AddDataApex("apex.apexd_test.apex")); + auto shim_v1 = ApexFile::Open(AddDataApex("com.android.apex.cts.shim.apex")); + // Initialize ApexFile repo + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + + auto all_apex = instance.AllApexFilesByName(); + auto result = SelectApexForActivation(all_apex, instance); + ASSERT_EQ(result.size(), 2u); + + ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*apexd_test_file)), + ApexFileEq(ByRef(*shim_v1)))); +} + +// Both versions of shared libs can be selected +TEST_F(ApexdUnitTest, SharedLibsCanHaveBothVersionSelected) { + auto shared_lib_v1 = ApexFile::Open(AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex")); + // Initialize pre-installed APEX information + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + auto shared_lib_v2 = ApexFile::Open( + AddDataApex("com.android.apex.test.sharedlibs_generated.v2.libvY.apex")); + // Initialize data APEX information + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + + auto all_apex = instance.AllApexFilesByName(); + auto result = SelectApexForActivation(all_apex, instance); + ASSERT_EQ(result.size(), 2u); + + ASSERT_THAT(result, UnorderedElementsAre(ApexFileEq(ByRef(*shared_lib_v1)), + ApexFileEq(ByRef(*shared_lib_v2)))); +} + +TEST_F(ApexdUnitTest, ProcessCompressedApex) { + auto compressed_apex = ApexFile::Open( + AddPreInstalledApex("com.android.apex.compressed.v1.capex")); + + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex)); + auto return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + + std::string decompressed_file_path = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + // Assert output path is not empty + auto exists = PathExists(decompressed_file_path); + ASSERT_TRUE(IsOk(exists)); + ASSERT_TRUE(*exists) << decompressed_file_path << " does not exist"; + + // Assert that decompressed apex is same as original apex + const std::string original_apex_file_path = + GetTestFile("com.android.apex.compressed.v1_original.apex"); + auto comparison_result = + CompareFiles(original_apex_file_path, decompressed_file_path); + ASSERT_TRUE(IsOk(comparison_result)); + ASSERT_TRUE(*comparison_result); + + // Assert that return value contains decompressed APEX + auto decompressed_apex = ApexFile::Open(decompressed_file_path); + ASSERT_THAT(return_value, + UnorderedElementsAre(ApexFileEq(ByRef(*decompressed_apex)))); +} + +TEST_F(ApexdUnitTest, ProcessCompressedApexRunsVerification) { + auto compressed_apex_mismatch_key = ApexFile::Open(AddPreInstalledApex( + "com.android.apex.compressed_key_mismatch_with_original.capex")); + auto compressed_apex_version_mismatch = ApexFile::Open( + AddPreInstalledApex("com.android.apex.compressed.v1_with_v2_apex.capex")); + + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex_mismatch_key)); + compressed_apex_list.emplace_back( + std::cref(*compressed_apex_version_mismatch)); + auto return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + ASSERT_EQ(return_value.size(), 0u); +} + +TEST_F(ApexdUnitTest, ValidateDecompressedApex) { + auto capex = ApexFile::Open( + AddPreInstalledApex("com.android.apex.compressed.v1.capex")); + auto decompressed_v1 = ApexFile::Open( + AddDataApex("com.android.apex.compressed.v1_original.apex")); + + auto result = + ValidateDecompressedApex(std::cref(*capex), std::cref(*decompressed_v1)); + ASSERT_TRUE(IsOk(result)); + + // 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_FALSE(IsOk(result)); + ASSERT_THAT( + result.error().message(), + 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_FALSE(IsOk(result)); + ASSERT_THAT(result.error().message(), + 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_FALSE(IsOk(result)); + ASSERT_THAT( + result.error().message(), + HasSubstr("Public key of compressed APEX is different than original")); +} + +TEST_F(ApexdUnitTest, ProcessCompressedApexCanBeCalledMultipleTimes) { + auto compressed_apex = ApexFile::Open( + AddPreInstalledApex("com.android.apex.compressed.v1.capex")); + + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex)); + auto return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + ASSERT_EQ(return_value.size(), 1u); + + // Capture the creation time of the decompressed APEX + std::error_code ec; + auto decompressed_apex_path = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + auto last_write_time_1 = fs::last_write_time(decompressed_apex_path, ec); + ASSERT_FALSE(ec) << "Failed to capture last write time of " + << decompressed_apex_path; + + // Now try to decompress the same capex again. It should not fail. + return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + ASSERT_EQ(return_value.size(), 1u); + + // Ensure the decompressed APEX file did not change + auto last_write_time_2 = fs::last_write_time(decompressed_apex_path, ec); + ASSERT_FALSE(ec) << "Failed to capture last write time of " + << decompressed_apex_path; + ASSERT_EQ(last_write_time_1, last_write_time_2); +} + +// Test behavior of ProcessCompressedApex when is_ota_chroot is true +TEST_F(ApexdUnitTest, ProcessCompressedApexOnOtaChroot) { + auto compressed_apex = ApexFile::Open( + AddPreInstalledApex("com.android.apex.compressed.v1.capex")); + + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex)); + auto return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ true); + ASSERT_EQ(return_value.size(), 1u); + + // Decompressed APEX should be located in decompression_dir + std::string decompressed_file_path = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + // Assert output path is not empty + auto exists = PathExists(decompressed_file_path); + ASSERT_TRUE(IsOk(exists)); + ASSERT_TRUE(*exists) << decompressed_file_path << " does not exist"; + + // Assert that decompressed apex is same as original apex + const std::string original_apex_file_path = + GetTestFile("com.android.apex.compressed.v1_original.apex"); + auto comparison_result = + CompareFiles(original_apex_file_path, decompressed_file_path); + ASSERT_TRUE(IsOk(comparison_result)); + ASSERT_TRUE(*comparison_result); + + // Assert that return value contains the decompressed APEX + auto apex_file = ApexFile::Open(decompressed_file_path); + ASSERT_THAT(return_value, + UnorderedElementsAre(ApexFileEq(ByRef(*apex_file)))); +} + +// When decompressing APEX, reuse existing OTA APEX +TEST_F(ApexdUnitTest, ProcessCompressedApexReuseOtaApex) { + // Push a compressed APEX that will fail to decompress + auto compressed_apex = ApexFile::Open(AddPreInstalledApex( + "com.android.apex.compressed.v1_not_decompressible.capex")); + + std::vector<ApexFileRef> compressed_apex_list; + compressed_apex_list.emplace_back(std::cref(*compressed_apex)); + + // If we try to decompress capex directly, it should fail since the capex + // pushed is faulty and cannot be decompressed + auto return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + ASSERT_EQ(return_value.size(), 0u); + + // But, if there is an ota_apex present for reuse, it should reuse that + // and avoid decompressing the faulty capex + + // Push an OTA apex that should be reused to skip decompression + auto ota_apex_path = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + fs::copy(GetTestFile("com.android.apex.compressed.v1_original.apex"), + ota_apex_path); + return_value = + ProcessCompressedApex(compressed_apex_list, /* is_ota_chroot= */ false); + ASSERT_EQ(return_value.size(), 1u); + + // Ota Apex should be cleaned up + ASSERT_FALSE(*PathExists(ota_apex_path)); + ASSERT_EQ(return_value[0].GetPath(), + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix)); +} + +TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompressionNewApex) { + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + // A brand new compressed APEX is being introduced: selected + auto result = + ShouldAllocateSpaceForDecompression("com.android.brand.new", 1, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result); +} + +TEST_F(ApexdUnitTest, + ShouldAllocateSpaceForDecompressionWasNotCompressedBefore) { + // Prepare fake pre-installed apex + AddPreInstalledApex("apex.apexd_test.apex"); + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + + // An existing pre-installed APEX is now compressed in the OTA: selected + { + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.test_package", 1, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result); + } + + // Even if there is a data apex (lower version) + // Include data apex within calculation now + AddDataApex("apex.apexd_test_v2.apex"); + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + { + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.test_package", 3, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result); + } + + // But not if data apex has equal or higher version + { + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.test_package", 2, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_FALSE(*result); + } +} + +TEST_F(ApexdUnitTest, ShouldAllocateSpaceForDecompressionVersionCompare) { + // Prepare fake pre-installed apex + PrepareCompressedApex("com.android.apex.compressed.v1.capex"); + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_TRUE(IsOk(instance.AddPreInstalledApex({GetBuiltInDir()}))); + ASSERT_TRUE(IsOk(instance.AddDataApex(GetDataDir()))); + + { + // New Compressed apex has higher version than decompressed data apex: + // selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 2, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result) + << "Higher version test with decompressed data returned false"; + } + + // Compare against decompressed data apex + { + // New Compressed apex has same version as decompressed data apex: not + // selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 1, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_FALSE(*result) + << "Same version test with decompressed data returned true"; + } + + { + // New Compressed apex has lower version than decompressed data apex: + // selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 0, instance); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result) + << "lower version test with decompressed data returned false"; + } + + // Replace decompressed data apex with a higher version + ApexFileRepository instance_new(GetDecompressionDir()); + ASSERT_TRUE(IsOk(instance_new.AddPreInstalledApex({GetBuiltInDir()}))); + TemporaryDir data_dir_new; + fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), + data_dir_new.path); + ASSERT_TRUE(IsOk(instance_new.AddDataApex(data_dir_new.path))); + + { + // New Compressed apex has higher version as data apex: selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 3, instance_new); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(*result) << "Higher version test with new data returned false"; + } + + { + // New Compressed apex has same version as data apex: not selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 2, instance_new); + ASSERT_TRUE(IsOk(result)); + ASSERT_FALSE(*result) << "Same version test with new data returned true"; + } + + { + // New Compressed apex has lower version than data apex: not selected + auto result = ShouldAllocateSpaceForDecompression( + "com.android.apex.compressed", 1, instance_new); + ASSERT_TRUE(IsOk(result)); + 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_TRUE(IsOk(ReserveSpaceForCompressedApex(100, dest_dir.path))); + auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + EXPECT_EQ(fs::file_size((*files)[0]), 100u); +} + +TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexSafeToCallMultipleTimes) { + TemporaryDir dest_dir; + // Calling ReserveSpaceForCompressedApex multiple times should still create + // a single file + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(100, dest_dir.path))); + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(100, dest_dir.path))); + auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + EXPECT_EQ(fs::file_size((*files)[0]), 100u); +} + +TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexShrinkAndGrow) { + TemporaryDir dest_dir; + + // Create a 100 byte file + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(100, dest_dir.path))); + + // Should be able to shrink and grow the reserved space + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(1000, dest_dir.path))); + auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + EXPECT_EQ(fs::file_size((*files)[0]), 1000u); + + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(10, dest_dir.path))); + files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + EXPECT_EQ(fs::file_size((*files)[0]), 10u); +} + +TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexDeallocateIfPassedZero) { + TemporaryDir dest_dir; + + // Create a file first + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(100, dest_dir.path))); + auto files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + + // Should delete the reserved file if size passed is 0 + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(0, dest_dir.path))); + files = ReadDir(dest_dir.path, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + 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_original.apex"), + ota_apex_path); + auto path_exists = PathExists(ota_apex_path); + ASSERT_TRUE(*path_exists); + }; + create_ota_apex(); + + // Should not delete the reserved file if size passed is negative + ASSERT_FALSE(IsOk(ReserveSpaceForCompressedApex(-1, dest_dir.path))); + auto path_exists = PathExists(ota_apex_path); + ASSERT_TRUE(*path_exists); + + // Should delete the reserved file if size passed is 0 + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(0, dest_dir.path))); + path_exists = PathExists(ota_apex_path); + ASSERT_FALSE(*path_exists); + + create_ota_apex(); + // Should delete the reserved file if size passed is positive + ASSERT_TRUE(IsOk(ReserveSpaceForCompressedApex(10, dest_dir.path))); + path_exists = PathExists(ota_apex_path); + ASSERT_FALSE(*path_exists); +} + +TEST_F(ApexdUnitTest, ReserveSpaceForCompressedApexErrorForNegativeValue) { + TemporaryDir dest_dir; + // Should return error if negative value is passed + ASSERT_FALSE(IsOk(ReserveSpaceForCompressedApex(-1, dest_dir.path))); +} + +// A test fixture to use for tests that mount/unmount apexes. +class ApexdMountTest : public ApexdUnitTest { + public: + + void UnmountOnTearDown(const std::string& apex_file) { + to_unmount_.push_back(apex_file); + } + + protected: + void SetUp() final { + ApexdUnitTest::SetUp(); + GetApexDatabaseForTesting().Reset(); + ASSERT_TRUE(IsOk(SetUpApexTestEnvironment())); + } + + void TearDown() final { + ApexdUnitTest::TearDown(); + for (const auto& apex : to_unmount_) { + if (auto status = DeactivatePackage(apex); !status.ok()) { + LOG(ERROR) << "Failed to unmount " << apex << " : " << status.error(); + } + } + } + + private: + MountNamespaceRestorer restorer_; + std::vector<std::string> to_unmount_; +}; + +// TODO(b/187864524): cover other negative scenarios. +TEST_F(ApexdMountTest, InstallPackageRejectsApexWithoutRebootlessSupport) { + std::string file_path = AddPreInstalledApex("apex.apexd_test.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage(GetTestFile("apex.apexd_test.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), + HasSubstr("does not support non-staged update")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsNoPreInstalledApex) { + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT( + ret.error().message(), + HasSubstr("No active version found for package test.apex.rebootless")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsNoHashtree) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = + InstallPackage(GetTestFile("test.rebootless_apex_v2_no_hashtree.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), + HasSubstr(" does not have an embedded hash tree")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsNoActiveApex) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT( + ret.error().message(), + HasSubstr("No active version found for package test.apex.rebootless")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsManifestMismatch) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage( + GetTestFile("test.rebootless_apex_manifest_mismatch.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT( + ret.error().message(), + HasSubstr( + "Manifest inside filesystem does not match manifest outside it")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsCorrupted) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_corrupted.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr("Can't verify /dev/block/dm-")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsProvidesSharedLibs) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage( + GetTestFile("test.rebootless_apex_provides_sharedlibs.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr(" is a shared libs APEX")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsProvidesNativeLibs) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage( + GetTestFile("test.rebootless_apex_provides_native_libs.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr(" provides native libs")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsRequiresSharedApexLibs) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage( + GetTestFile("test.rebootless_apex_requires_shared_apex_libs.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr(" requires shared apex libs")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsJniLibs) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_jni_libs.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr(" requires JNI libs")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsAddRequiredNativeLib) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = + InstallPackage(GetTestFile("test.rebootless_apex_add_native_lib.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), + HasSubstr("Set of native libs required by")); + ASSERT_THAT( + ret.error().message(), + HasSubstr("differs from the one required by the currently active")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsRemovesRequiredNativeLib) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = InstallPackage( + GetTestFile("test.rebootless_apex_remove_native_lib.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), + HasSubstr("Set of native libs required by")); + ASSERT_THAT( + ret.error().message(), + HasSubstr("differs from the one required by the currently active")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsAppInApex) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = + InstallPackage(GetTestFile("test.rebootless_apex_app_in_apex.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr("contains app inside")); +} + +TEST_F(ApexdMountTest, InstallPackageRejectsPrivAppInApex) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto ret = + InstallPackage(GetTestFile("test.rebootless_apex_priv_app_in_apex.apex")); + ASSERT_FALSE(IsOk(ret)); + ASSERT_THAT(ret.error().message(), HasSubstr("contains priv-app inside")); +} + +TEST_F(ApexdMountTest, InstallPackagePreInstallVersionActive) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + } + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/test.apex.rebootless", + "/apex/test.apex.rebootless@2")); + + // Check that /apex/test.apex.rebootless is a bind mount of + // /apex/test.apex.rebootless@2. + auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); + ASSERT_TRUE(IsOk(manifest)); + ASSERT_EQ(2u, manifest->version()); + + // Check that GetActivePackage correctly reports upgraded version. + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); + + // Check that pre-installed APEX is still around + ASSERT_EQ(0, access(file_path.c_str(), F_OK)) + << "Can't access " << file_path << " : " << strerror(errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, ret->GetPath()); + ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); + }); +} + +TEST_F(ApexdMountTest, InstallPackagePreInstallVersionActiveSamegrade) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + } + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/test.apex.rebootless", + "/apex/test.apex.rebootless@1")); + + // Check that GetActivePackage correctly reports upgraded version. + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); + + // Check that pre-installed APEX is still around + ASSERT_EQ(0, access(file_path.c_str(), F_OK)) + << "Can't access " << file_path << " : " << strerror(errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, ret->GetPath()); + ASSERT_EQ(data.device_name, "test.apex.rebootless@1_1"); + }); +} + +TEST_F(ApexdMountTest, InstallPackageDataVersionActive) { + AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + std::string file_path = AddDataApex("test.rebootless_apex_v1.apex"); + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + } + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/test.apex.rebootless", + "/apex/test.apex.rebootless@2")); + + // Check that /apex/test.apex.rebootless is a bind mount of + // /apex/test.apex.rebootless@2. + auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); + ASSERT_TRUE(IsOk(manifest)); + ASSERT_EQ(2u, manifest->version()); + + // Check that GetActivePackage correctly reports upgraded version. + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); + + // Check that previously active APEX was deleted. + ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); + ASSERT_EQ(ENOENT, errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, ret->GetPath()); + ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); + }); +} + +TEST_F(ApexdMountTest, InstallPackageResolvesPathCollision) { + AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + std::string file_path = AddDataApex("test.rebootless_apex_v1.apex", + "test.apex.rebootless@1_1.apex"); + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + } + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v1.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/test.apex.rebootless", + "/apex/test.apex.rebootless@1")); + + // Check that /apex/test.apex.rebootless is a bind mount of + // /apex/test.apex.rebootless@2. + auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); + ASSERT_TRUE(IsOk(manifest)); + ASSERT_EQ(1u, manifest->version()); + + // Check that GetActivePackage correctly reports upgraded version. + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); + + // Check that we correctly resolved active apex path collision. + ASSERT_EQ(active_apex->GetPath(), + GetDataDir() + "/test.apex.rebootless@1_2.apex"); + + // Check that previously active APEX was deleted. + ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); + ASSERT_EQ(ENOENT, errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, ret->GetPath()); + ASSERT_EQ(data.device_name, "test.apex.rebootless@1_2"); + }); +} + +TEST_F(ApexdMountTest, InstallPackageDataVersionActiveSamegrade) { + AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + std::string file_path = AddDataApex("test.rebootless_apex_v2.apex"); + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + } + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/test.apex.rebootless", + "/apex/test.apex.rebootless@2")); + + // Check that /apex/test.apex.rebootless is a bind mount of + // /apex/test.apex.rebootless@2. + auto manifest = ReadManifest("/apex/test.apex.rebootless/apex_manifest.pb"); + ASSERT_TRUE(IsOk(manifest)); + ASSERT_EQ(2u, manifest->version()); + + // Check that GetActivePackage correctly reports upgraded version. + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), ret->GetPath()); + + // Check that previously active APEX was deleted. + ASSERT_EQ(-1, access(file_path.c_str(), F_OK)); + ASSERT_EQ(ENOENT, errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, ret->GetPath()); + ASSERT_EQ(data.device_name, "test.apex.rebootless@2_1"); + }); +} + +TEST_F(ApexdMountTest, InstallPackageUnmountFailsPreInstalledApexActive) { + std::string file_path = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + 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")); + ASSERT_FALSE(IsOk(ret)); + + 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_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + + // Check that old APEX is still around + ASSERT_EQ(0, access(file_path.c_str(), F_OK)) + << "Can't access " << file_path << " : " << strerror(errno); + + auto& db = GetApexDatabaseForTesting(); + // Check that upgraded APEX is mounted on top of dm-verity device. + db.ForallMountedApexes("test.apex.rebootless", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, file_path); + }); +} + +TEST_F(ApexdMountTest, InstallPackageUnmountFailedUpdatedApexActive) { + AddPreInstalledApex("test.rebootless_apex_v1.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + std::string file_path = AddDataApex("test.rebootless_apex_v1.apex"); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + { + auto active_apex = GetActivePackage("test.apex.rebootless"); + ASSERT_TRUE(IsOk(active_apex)); + 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")); + ASSERT_FALSE(IsOk(ret)); + + 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_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + + // Check that old APEX is still around + ASSERT_EQ(0, access(file_path.c_str(), F_OK)) + << "Can't access " << file_path << " : " << strerror(errno); + + auto& db = GetApexDatabaseForTesting(); + db.ForallMountedApexes( + "test.apex.rebootless", [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, file_path); + ASSERT_EQ(data.device_name, "test.apex.rebootless@1"); + }); +} + +TEST_F(ApexdMountTest, InstallPackageUpdatesApexInfoList) { + auto apex_1 = AddPreInstalledApex("test.rebootless_apex_v1.apex"); + auto apex_2 = AddPreInstalledApex("apex.apexd_test.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + UnmountOnTearDown(apex_1); + UnmountOnTearDown(apex_2); + ASSERT_TRUE(IsOk(ActivatePackage(apex_1))); + ASSERT_TRUE(IsOk(ActivatePackage(apex_2))); + + // Call OnAllPackagesActivated to create /apex/apex-info-list.xml. + OnAllPackagesActivated(/* is_bootstrap= */ false); + // Check /apex/apex-info-list.xml was created. + ASSERT_EQ(0, access("/apex/apex-info-list.xml", F_OK)); + + auto ret = InstallPackage(GetTestFile("test.rebootless_apex_v2.apex")); + ASSERT_TRUE(IsOk(ret)); + UnmountOnTearDown(ret->GetPath()); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "test.apex.rebootless", + /* modulePath= */ apex_1, + /* preinstalledModulePath= */ apex_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_1)); + 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)); + 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())); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2), + ApexInfoXmlEq(apex_info_xml_3))); +} + +TEST_F(ApexdMountTest, ActivatePackage) { + std::string file_path = AddPreInstalledApex("apex.apexd_test.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + UnmountOnTearDown(file_path); + + auto active_apex = GetActivePackage("com.android.apex.test_package"); + ASSERT_TRUE(IsOk(active_apex)); + 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_TRUE(IsOk(DeactivatePackage(file_path))); + ASSERT_FALSE(IsOk(GetActivePackage("com.android.apex.test_package"))); + + auto new_apex_mounts = GetApexMounts(); + ASSERT_EQ(new_apex_mounts.size(), 0u); +} + +TEST_F(ApexdMountTest, ActivateDeactivateSharedLibsApex) { + ASSERT_EQ(mkdir("/apex/sharedlibs", 0755), 0); + ASSERT_EQ(mkdir("/apex/sharedlibs/lib", 0755), 0); + ASSERT_EQ(mkdir("/apex/sharedlibs/lib64", 0755), 0); + auto deleter = make_scope_guard([]() { + std::error_code ec; + fs::remove_all("/apex/sharedlibs", ec); + if (ec) { + LOG(ERROR) << "Failed to delete /apex/sharedlibs : " << ec; + } + }); + + std::string file_path = AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + + UnmountOnTearDown(file_path); + ASSERT_TRUE(IsOk(ActivatePackage(file_path))); + + auto active_apex = GetActivePackage("com.android.apex.test.sharedlibs"); + ASSERT_TRUE(IsOk(active_apex)); + ASSERT_EQ(active_apex->GetPath(), file_path); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test.sharedlibs@1")); + + ASSERT_TRUE(IsOk(DeactivatePackage(file_path))); + ASSERT_FALSE(IsOk(GetActivePackage("com.android.apex.test.sharedlibs"))); + + auto new_apex_mounts = GetApexMounts(); + ASSERT_EQ(new_apex_mounts.size(), 0u); +} + +TEST_F(ApexdMountTest, RemoveInactiveDataApex) { + AddPreInstalledApex("com.android.apex.compressed.v2.capex"); + // Add a decompressed apex that will not be mounted, so should be removed + auto decompressed_apex = StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + fs::copy(GetTestFile("com.android.apex.compressed.v1_original.apex"), + decompressed_apex); + // Add a decompressed apex that will be mounted, so should be not be removed + auto active_decompressed_apex = StringPrintf( + "%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), + active_decompressed_apex); + // Apex that do not have kDecompressedApexPackageSuffix, should not be removed + // from decompression_dir + auto decompressed_different_suffix = + StringPrintf("%s/com.android.apex.compressed@2%s", + GetDecompressionDir().c_str(), kApexPackageSuffix); + fs::copy(GetTestFile("com.android.apex.compressed.v2_original.apex"), + decompressed_different_suffix); + + AddPreInstalledApex("apex.apexd_test.apex"); + auto data_apex = AddDataApex("apex.apexd_test.apex"); + auto active_data_apex = AddDataApex("apex.apexd_test_v2.apex"); + + // Activate some of the apex + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()}); + UnmountOnTearDown(active_decompressed_apex); + UnmountOnTearDown(active_data_apex); + ASSERT_TRUE(IsOk(ActivatePackage(active_decompressed_apex))); + ASSERT_TRUE(IsOk(ActivatePackage(active_data_apex))); + // 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(), 0); + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + 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)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2))); +} + +TEST_F(ApexdMountTest, OnOtaChrootBootstrapFailsToScanPreInstalledApexes) { + AddPreInstalledApex("apex.apexd_test.apex"); + AddPreInstalledApex("apex.apexd_test_corrupt_superblock_apex.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 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(), 0); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@2", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1)); + 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)); + 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)); + 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(), 0); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1)); + 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)); + 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)); + 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(), 0); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@2", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 2, /* versionName= */ "2", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + 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)); + + 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(), 0); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + 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)); + + 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_TRUE(IsOk(apex)); + ASSERT_EQ(static_cast<uint64_t>(apex->GetManifest().version()), 2ULL); + } + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + 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)); + + 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(), 0); + + UnmountOnTearDown(apex_path_1); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1))); +} + +TEST_F(ApexdMountTest, OnOtaChrootBootstrapPreInstalledSharedLibsApex) { + std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); + std::string apex_path_2 = AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); + std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@2", + "/apex/com.android.apex.test.sharedlibs@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1)); + auto apex_info_xml_2 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test.sharedlibs", + /* modulePath= */ apex_path_2, + /* preinstalledModulePath= */ apex_path_2, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_2)); + 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)); + + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2), + ApexInfoXmlEq(apex_info_xml_3))); + + ASSERT_EQ(access("/apex/sharedlibs", F_OK), 0); + + // Check /apex/sharedlibs is populated properly. + std::vector<std::string> sharedlibs; + for (const auto& p : fs::recursive_directory_iterator("/apex/sharedlibs")) { + if (fs::is_symlink(p)) { + auto src = fs::read_symlink(p.path()); + ASSERT_EQ(p.path().filename(), src.filename()); + sharedlibs.push_back(p.path().parent_path().string() + "->" + + src.parent_path().string()); + } + } + + std::vector<std::string> expected = { + "/apex/sharedlibs/lib/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib/libsharedlibtest.so", + "/apex/sharedlibs/lib/libc++.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib/libc++.so", + }; + + // On 64bit devices we also have lib64. + if (!GetProperty("ro.product.cpu.abilist64", "").empty()) { + expected.push_back( + "/apex/sharedlibs/lib64/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib64/libsharedlibtest.so"); + expected.push_back( + "/apex/sharedlibs/lib64/libc++.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib64/libc++.so"); + } + ASSERT_THAT(sharedlibs, UnorderedElementsAreArray(expected)); +} + +TEST_F(ApexdMountTest, OnOtaChrootBootstrapSharedLibsApexBothVersions) { + std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); + std::string apex_path_2 = AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); + std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); + std::string apex_path_4 = + AddDataApex("com.android.apex.test.sharedlibs_generated.v2.libvY.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + UnmountOnTearDown(apex_path_4); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@2", + "/apex/com.android.apex.test.sharedlibs@1", + "/apex/com.android.apex.test.sharedlibs@2")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_1)); + auto apex_info_xml_2 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test.sharedlibs", + /* modulePath= */ apex_path_2, + /* preinstalledModulePath= */ apex_path_2, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ false, GetMTime(apex_path_2)); + 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)); + auto apex_info_xml_4 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test.sharedlibs", + /* modulePath= */ apex_path_4, + /* preinstalledModulePath= */ apex_path_2, + /* versionCode= */ 2, /* versionName= */ "2", + /* isFactory= */ false, /* isActive= */ true, GetMTime(apex_path_4)); + + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2), + ApexInfoXmlEq(apex_info_xml_3), + ApexInfoXmlEq(apex_info_xml_4))); + + ASSERT_EQ(access("/apex/sharedlibs", F_OK), 0); + + // Check /apex/sharedlibs is populated properly. + // Because we don't want to hardcode full paths (they are pretty long and have + // a hash in them which might change if new prebuilts are dropped in), the + // assertion logic is a little bit clunky. + std::vector<std::string> sharedlibs; + for (const auto& p : fs::recursive_directory_iterator("/apex/sharedlibs")) { + if (fs::is_symlink(p)) { + auto src = fs::read_symlink(p.path()); + ASSERT_EQ(p.path().filename(), src.filename()); + sharedlibs.push_back(p.path().parent_path().string() + "->" + + src.parent_path().string()); + } + } + + std::vector<std::string> expected = { + "/apex/sharedlibs/lib/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@2/lib/libsharedlibtest.so", + "/apex/sharedlibs/lib/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib/libsharedlibtest.so", + "/apex/sharedlibs/lib/libc++.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib/libc++.so", + }; + // On 64bit devices we also have lib64. + if (!GetProperty("ro.product.cpu.abilist64", "").empty()) { + expected.push_back( + "/apex/sharedlibs/lib64/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@2/lib64/libsharedlibtest.so"); + expected.push_back( + "/apex/sharedlibs/lib64/libsharedlibtest.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib64/libsharedlibtest.so"); + expected.push_back( + "/apex/sharedlibs/lib64/libc++.so->" + "/apex/com.android.apex.test.sharedlibs@1/lib64/libc++.so"); + } + + ASSERT_THAT(sharedlibs, UnorderedElementsAreArray(expected)); +} + +// Test when we move from uncompressed APEX to CAPEX via ota +TEST_F(ApexdMountTest, OnOtaChrootBootstrapOnlyCompressedApexes) { + std::string apex_path = + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // Decompressed APEX should be mounted from decompression_dir + std::string decompressed_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(decompressed_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@1.chroot"); + }); +} + +// Test we decompress only once even if OnOtaChrootBootstrap is called multiple +// times +TEST_F(ApexdMountTest, OnOtaChrootBootstrapDecompressOnlyOnceMultipleCalls) { + std::string apex_path = + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // Decompressed OTA APEX should be mounted + std::string decompressed_ota_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_ota_apex); + + // Capture the creation time of the OTA APEX + std::error_code ec; + auto last_write_time_1 = fs::last_write_time(decompressed_ota_apex, ec); + ASSERT_FALSE(ec) << "Failed to capture last write time of " + << decompressed_ota_apex; + + // Call OnOtaChrootBootstrap again. Since we do not hardlink decompressed APEX + // to /data/apex/active directory when in chroot, when selecting apex for + // activation, we will end up selecting compressed APEX again. + ASSERT_EQ(OnOtaChrootBootstrap(), 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(), 0); + + // Upgraded decompressed APEX should be mounted from decompression dir + std::string decompressed_active_apex = + StringPrintf("%s/com.android.apex.compressed@2%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@2")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_active_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 2, /* versionName= */ "2", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_active_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_active_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@2.chroot"); + }); +} + +// Test when we update existing CAPEX to same version via OTA +TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapex) { + TemporaryDir previous_built_in_dir; + PrepareCompressedApex("com.android.apex.compressed.v1.capex", + previous_built_in_dir.path); + // Place a same version capex in current built_in_dir, under a different name + auto apex_path = + StringPrintf("%s/different-name.capex", GetBuiltInDir().c_str()); + fs::copy(GetTestFile("com.android.apex.compressed.v1.capex"), apex_path); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // Previously decompressed APEX should be mounted from decompression_dir + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_active_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_active_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_active_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@1.chroot"); + }); +} + +// Test when we update existing CAPEX to same version, but different digest +TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapexDifferentDigest) { + TemporaryDir previous_built_in_dir; + auto different_digest_apex_path = PrepareCompressedApex( + "com.android.apex.compressed.v1_different_digest.capex", + previous_built_in_dir.path); + // Place a same version capex in current built_in_dir, which has different + // digest + auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // New decompressed ota APEX should be mounted with kOtaApexPackageSuffix + std::string decompressed_ota_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_ota_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_ota_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_ota_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_ota_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@1.chroot"); + }); + + // Ensure decompressed apex has same digest as pre-installed + auto pre_installed_apex = ApexFile::Open(apex_path); + auto decompressed_apex = ApexFile::Open(decompressed_ota_apex); + auto different_digest_apex = ApexFile::Open(different_digest_apex_path); + ASSERT_EQ( + pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(), + GetRootDigest(*decompressed_apex)); + ASSERT_NE( + pre_installed_apex->GetManifest().capexmetadata().originalapexdigest(), + GetRootDigest(*different_digest_apex)); + + // Ensure we didn't remove previous decompressed APEX + std::string previous_decompressed_apex = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + auto path_exists = PathExists(previous_decompressed_apex); + ASSERT_TRUE(*path_exists); +} + +// Test when we update existing CAPEX to same version, but different key via OTA +TEST_F(ApexdMountTest, OnOtaChrootBootstrapSamegradeCapexDifferentKey) { + TemporaryDir previous_built_in_dir; + PrepareCompressedApex("com.android.apex.compressed_different_key.capex", + previous_built_in_dir.path); + // Place a same version capex in current built_in_dir, which has different key + auto apex_path = AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // New decompressed APEX should be mounted from ota_reserved directory + std::string decompressed_active_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_active_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_active_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_active_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@1.chroot"); + }); +} + +// Test when we remove CAPEX via OTA +TEST_F(ApexdMountTest, OnOtaChrootBootstrapCapexToApex) { + TemporaryDir previous_built_in_dir; + PrepareCompressedApex("com.android.apex.compressed.v1.capex", + previous_built_in_dir.path); + // Place a uncompressed version apex in current built_in_dir + std::string apex_path = + AddPreInstalledApex("com.android.apex.compressed.v1_original.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // New uncompressed APEX should be mounted + UnmountOnTearDown(apex_path); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_uncompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ apex_path, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path)); + 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(), 0); + + // Pre-installed CAPEX should be decompressed again and mounted from + // decompression_dir + std::string decompressed_active_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_active_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_active_apex)); + 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(), 0); + + // Data APEX should be mounted + UnmountOnTearDown(data_apex_path); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@2")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_data = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ data_apex_path, + /* preinstalledModulePath= */ system_apex_path, + /* versionCode= */ 2, /* versionName= */ "2", + /* isFactory= */ false, /* isActive= */ true, GetMTime(data_apex_path)); + 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)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_data), + ApexInfoXmlEq(apex_info_xml_system))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, data_apex_path); + ASSERT_EQ(data.device_name, + "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_original.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 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); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@2")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml = 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)); + 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.device_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_original.apex"); + + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + // Data APEX should be mounted + UnmountOnTearDown(data_apex_path); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@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)); + 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)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_data), + ApexInfoXmlEq(apex_info_xml_system))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, data_apex_path); + ASSERT_EQ(data.device_name, + "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(), 0); + + // New decompressed APEX should be mounted from ota_reserved directory + std::string decompressed_active_apex = + StringPrintf("%s/com.android.apex.compressed@1%s", + GetDecompressionDir().c_str(), kOtaApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.compressed", + "/apex/com.android.apex.compressed@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_decompressed = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.compressed", + /* modulePath= */ decompressed_active_apex, + /* preinstalledModulePath= */ apex_path, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + GetMTime(decompressed_active_apex)); + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_decompressed))); + auto& db = GetApexDatabaseForTesting(); + // Check that it was mounted from decompressed apex. It should also be mounted + // on dm-verity device. + db.ForallMountedApexes("com.android.apex.compressed", + [&](const MountedApexData& data, bool latest) { + ASSERT_TRUE(latest); + ASSERT_EQ(data.full_path, decompressed_active_apex); + ASSERT_EQ(data.device_name, + "com.android.apex.compressed@1.chroot"); + }); +} + +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 = AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); + std::string apex_path_3 = AddDataApex("apex.apexd_test_v2.apex"); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + ASSERT_EQ(OnOtaChrootBootstrap(), 0); + + EXPECT_EQ(GetSelinuxContext("/apex/apex-info-list.xml"), + "u:object_r:apex_info_file:s0"); + + EXPECT_EQ(GetSelinuxContext("/apex/sharedlibs"), + "u:object_r:apex_mnt_dir: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(), 0); + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + 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.device_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.device_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(), 0); + UnmountOnTearDown(apex_path_2); + + 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)); + 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)); + + 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(), 0); + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test_package", + "/apex/com.android.apex.test_package@1", + "/apex/com.android.apex.test_package_2", + "/apex/com.android.apex.test_package_2@1")); + + ASSERT_EQ(access("/apex/apex-info-list.xml", F_OK), 0); + auto info_list = + com::android::apex::readApexInfoList("/apex/apex-info-list.xml"); + ASSERT_TRUE(info_list.has_value()); + auto apex_info_xml_1 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package", + /* modulePath= */ apex_path_1, + /* preinstalledModulePath= */ apex_path_1, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, GetMTime(apex_path_1)); + 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)); + + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2))); +} + +TEST_F(ApexdMountTest, OnOtaChrootBootstrapFlattenedApex) { + std::string apex_dir_1 = GetBuiltInDir() + "/com.android.apex.test_package"; + std::string apex_dir_2 = GetBuiltInDir() + "/com.android.apex.test_package_2"; + + ASSERT_EQ(mkdir(apex_dir_1.c_str(), 0755), 0); + ASSERT_EQ(mkdir(apex_dir_2.c_str(), 0755), 0); + + auto write_manifest_fn = [&](const std::string& apex_dir, + const std::string& module_name, int version) { + using ::apex::proto::ApexManifest; + + ApexManifest manifest; + manifest.set_name(module_name); + manifest.set_version(version); + manifest.set_versionname(std::to_string(version)); + + std::string out; + manifest.SerializeToString(&out); + ASSERT_TRUE(WriteStringToFile(out, apex_dir + "/apex_manifest.pb")); + }; + + write_manifest_fn(apex_dir_1, "com.android.apex.test_package", 2); + write_manifest_fn(apex_dir_2, "com.android.apex.test_package_2", 1); + + ASSERT_EQ(OnOtaChrootBootstrapFlattenedApex(), 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); + ASSERT_EQ(GetSelinuxContext("/apex/apex-info-list.xml"), + "u:object_r:apex_info_file:s0"); + + 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_dir_1, + /* preinstalledModulePath= */ apex_dir_1, + /* versionCode= */ 2, /* versionName= */ "2", + /* isFactory= */ true, /* isActive= */ true, + /* lastUpdateMillis= */ 0); + auto apex_info_xml_2 = com::android::apex::ApexInfo( + /* moduleName= */ "com.android.apex.test_package_2", + /* modulePath= */ apex_dir_2, + /* preinstalledModulePath= */ apex_dir_2, + /* versionCode= */ 1, /* versionName= */ "1", + /* isFactory= */ true, /* isActive= */ true, + /* lastUpdateMillis= */ 0); + + ASSERT_THAT(info_list->getApexInfo(), + UnorderedElementsAre(ApexInfoXmlEq(apex_info_xml_1), + ApexInfoXmlEq(apex_info_xml_2))); +} + +TEST_F(ApexdMountTest, OnStartOnlyPreInstalledApexes) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + std::string apex_path_1 = AddPreInstalledApex("apex.apexd_test.apex"); + std::string apex_path_2 = + AddPreInstalledApex("apex.apexd_test_different_app.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + std::string apex_path = AddPreInstalledApex("com.android.apex.cts.shim.apex"); + AddDataApex("com.android.apex.cts.shim.v2_wrong_sha.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + UnmountOnTearDown(apex_path); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_TRUE(IsOk(apex)); + ASSERT_EQ(static_cast<uint64_t>(apex->GetManifest().version()), 2ULL); + } + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + std::string apex_path_1 = + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Decompressed APEX should be mounted + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@1"); + }); +} + +TEST_F(ApexdMountTest, OnStartDataHasHigherVersionThanCapex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + std::string apex_path_2 = + AddDataApex("com.android.apex.compressed.v2_original.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@2"); + }); +} + +TEST_F(ApexdMountTest, OnStartDataHasSameVersionAsCapex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + std::string apex_path_2 = + AddDataApex("com.android.apex.compressed.v1_original.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Data APEX should be mounted + UnmountOnTearDown(apex_path_2); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@1"); + }); +} + +TEST_F(ApexdMountTest, OnStartSystemHasHigherVersionCapexThanData) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + std::string apex_path_1 = + AddPreInstalledApex("com.android.apex.compressed.v2.capex"); + AddDataApex("com.android.apex.compressed.v1_original.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Decompressed APEX should be mounted + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@2"); + }); +} + +TEST_F(ApexdMountTest, OnStartFailsToActivateApexOnDataFallsBackToCapex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + AddPreInstalledApex("com.android.apex.compressed.v1.capex"); + AddDataApex("com.android.apex.compressed.v2_manifest_mismatch.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Decompressed APEX should be mounted + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@1"); + }); +} + +// Test scenario when we fallback to capex but it already has a decompressed +// version on data +TEST_F(ApexdMountTest, OnStartFallbackToAlreadyDecompressedCapex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + PrepareCompressedApex("com.android.apex.compressed.v1.capex"); + AddDataApex("com.android.apex.compressed.v2_manifest_mismatch.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Decompressed APEX should be mounted + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@1%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@1"); + }); +} + +// Test scenario when we fallback to capex but it has same version as corrupt +// data apex +TEST_F(ApexdMountTest, OnStartFallbackToCapexSameVersion) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Decompressed APEX should be mounted + std::string decompressed_active_apex = StringPrintf( + "%s/com.android.apex.compressed@2%s", GetDecompressionDir().c_str(), + kDecompressedApexPackageSuffix); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@2"); + }); +} + +TEST_F(ApexdMountTest, OnStartCapexToApex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_original.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Uncompressed APEX should be mounted + UnmountOnTearDown(apex_path); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, IsEmpty()); + }); +} + +// Test to ensure we do not mount decompressed APEX from /data/apex/active +TEST_F(ApexdMountTest, OnStartOrphanedDecompressedApexInActiveDirectory) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + // 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_original.apex"), + decompressed_apex_in_active_dir); + auto apex_path = + AddPreInstalledApex("com.android.apex.compressed.v1_original.apex"); + + ASSERT_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Pre-installed APEX should be mounted + UnmountOnTearDown(apex_path); + 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.device_name, IsEmpty()); + }); +} + +// Test scenario when decompressed version has different version than +// pre-installed CAPEX +TEST_F(ApexdMountTest, OnStartDecompressedApexVersionDifferentThanCapex) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + 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); + UnmountOnTearDown(decompressed_active_apex); + + ASSERT_EQ(GetProperty(kTestApexdStatusSysprop, ""), "starting"); + 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.device_name, + "com.android.apex.compressed@1"); + }); +} + +// Test that ota_apex is persisted until slot switch +TEST_F(ApexdMountTest, OnStartOtaApexKeptUntilSlotSwitch) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + // 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + // 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); + UnmountOnTearDown(old_decompressed_apex); + UnmountOnTearDown(new_decompressed_apex); + + // 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + // 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + OnStart(); + + // Existing same version decompressed APEX with different root digest should + // be ignored and the pre-installed CAPEX should be decompressed again. + UnmountOnTearDown(decompressed_apex_path); + + // 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) { + MockCheckpointInterface checkpoint_interface; + // Need to call InitializeVold before calling OnStart + InitializeVold(&checkpoint_interface); + + 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_RESULT_OK( + ApexFileRepository::GetInstance().AddPreInstalledApex({GetBuiltInDir()})); + + 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); + UnmountOnTearDown(decompressed_active_apex); + + // 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, PopulateFromMountsChecksPathPrefix) { + AddPreInstalledApex("apex.apexd_test.apex"); + std::string apex_path = 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()); + + // Mount an apex from some other directory + TemporaryDir td; + AddPreInstalledApex("apex.apexd_test_different_app.apex"); + fs::copy(GetTestFile("apex.apexd_test_different_app.apex"), td.path); + std::string other_apex = + StringPrintf("%s/apex.apexd_test_different_app.apex", td.path); + + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_RESULT_OK(instance.AddPreInstalledApex({GetBuiltInDir()})); + + ASSERT_TRUE(IsOk(ActivatePackage(apex_path))); + ASSERT_TRUE(IsOk(ActivatePackage(decompressed_apex))); + ASSERT_TRUE(IsOk(ActivatePackage(other_apex))); + + auto& db = GetApexDatabaseForTesting(); + // Remember mount information for |other_apex|, since it won't be available in + // the database. We will need to tear it down manually. + std::optional<MountedApexData> other_apex_mount_data; + db.ForallMountedApexes( + "com.android.apex.test_package_2", + [&other_apex_mount_data](const MountedApexData& data, bool latest) { + if (latest) { + other_apex_mount_data.emplace(data); + } + }); + UnmountOnTearDown(apex_path); + UnmountOnTearDown(decompressed_apex); + ASSERT_TRUE(other_apex_mount_data.has_value()); + auto deleter = make_scope_guard([&other_apex_mount_data]() { + if (!other_apex_mount_data.has_value()) { + return; + } + if (umount2("/apex/com.android.apex.test_package_2", 0) != 0) { + PLOG(ERROR) << "Failed to unmount /apex/com.android.apex.test_package_2"; + } + auto res = Unmount(*other_apex_mount_data, /* deferred= */ false); + if (!res.ok()) { + LOG(ERROR) << res.error(); + } + }); + + 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")); + + // Clear the database before calling PopulateFromMounts + db.Reset(); + + // Populate from mount + db.PopulateFromMounts(GetDataDir(), GetDecompressionDir(), GetHashTreeDir()); + + // Count number of package and collect package names + int package_count = 0; + std::vector<std::string> mounted_paths; + db.ForallMountedApexes([&](const std::string& package, + const MountedApexData& data, bool latest) { + package_count++; + mounted_paths.push_back(data.full_path); + }); + ASSERT_EQ(package_count, 2); + ASSERT_THAT(mounted_paths, + UnorderedElementsAre(apex_path, decompressed_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_RESULT_OK(instance.AddPreInstalledApex({GetBuiltInDir()})); + + ASSERT_TRUE(IsOk(ActivatePackage(apex_path_2))); + ASSERT_TRUE(IsOk(ActivatePackage(apex_path_3))); + ASSERT_TRUE(IsOk(ActivatePackage(decompressed_apex))); + UnmountOnTearDown(apex_path_2); + UnmountOnTearDown(apex_path_3); + UnmountOnTearDown(decompressed_apex); + + 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")); + + auto& db = GetApexDatabaseForTesting(); + // UnmountAll expects apex database to empty, hence this reset. + db.Reset(); + + ASSERT_EQ(0, UnmountAll()); + + auto new_apex_mounts = GetApexMounts(); + ASSERT_EQ(new_apex_mounts.size(), 0u); +} + +TEST_F(ApexdMountTest, UnmountAllSharedLibsApex) { + ASSERT_EQ(mkdir("/apex/sharedlibs", 0755), 0); + ASSERT_EQ(mkdir("/apex/sharedlibs/lib", 0755), 0); + ASSERT_EQ(mkdir("/apex/sharedlibs/lib64", 0755), 0); + auto deleter = make_scope_guard([]() { + std::error_code ec; + fs::remove_all("/apex/sharedlibs", ec); + if (ec) { + LOG(ERROR) << "Failed to delete /apex/sharedlibs : " << ec; + } + }); + + std::string apex_path_1 = AddPreInstalledApex( + "com.android.apex.test.sharedlibs_generated.v1.libvX.apex"); + std::string apex_path_2 = + AddDataApex("com.android.apex.test.sharedlibs_generated.v2.libvY.apex"); + + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_RESULT_OK(instance.AddPreInstalledApex({GetBuiltInDir()})); + + ASSERT_TRUE(IsOk(ActivatePackage(apex_path_1))); + ASSERT_TRUE(IsOk(ActivatePackage(apex_path_2))); + UnmountOnTearDown(apex_path_1); + UnmountOnTearDown(apex_path_2); + + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, + UnorderedElementsAre("/apex/com.android.apex.test.sharedlibs@1", + "/apex/com.android.apex.test.sharedlibs@2")); + + auto& db = GetApexDatabaseForTesting(); + // UnmountAll expects apex database to empty, hence this reset. + db.Reset(); + + ASSERT_EQ(0, UnmountAll()); + + auto new_apex_mounts = GetApexMounts(); + ASSERT_EQ(new_apex_mounts.size(), 0u); +} + +class ApexActivationFailureTests : public ApexdMountTest {}; + +TEST_F(ApexActivationFailureTests, BuildFingerprintDifferent) { + auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); + apex_session->SetBuildFingerprint("wrong fingerprint"); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("APEX build fingerprint has changed")); +} + +TEST_F(ApexActivationFailureTests, ApexFileMissingInStagingDirectory) { + auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + // Delete the apex file in staging directory + DeleteDirContent(GetStagedDir(123)); + + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("No APEX packages found")); +} + +TEST_F(ApexActivationFailureTests, MultipleApexFileInStagingDirectory) { + auto apex_session = CreateStagedSession("apex.apexd_test.apex", 123); + CreateStagedSession("com.android.apex.compressed.v1_original.apex", 123); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("More than one APEX package found")); +} + +TEST_F(ApexActivationFailureTests, PostInstallFailsForApex) { + auto apex_session = + CreateStagedSession("apex.apexd_test_corrupt_superblock_apex.apex", 123); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("Postinstall failed for session")); +} + +TEST_F(ApexActivationFailureTests, CorruptedApexCannotBeStaged) { + auto apex_session = CreateStagedSession("corrupted_b146895998.apex", 123); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("Activation failed for packages")); +} + +TEST_F(ApexActivationFailureTests, ActivatePackageImplFails) { + auto shim_path = AddPreInstalledApex("com.android.apex.cts.shim.apex"); + auto& instance = ApexFileRepository::GetInstance(); + ASSERT_RESULT_OK(instance.AddPreInstalledApex({GetBuiltInDir()})); + + auto apex_session = + CreateStagedSession("com.android.apex.cts.shim.v2_wrong_sha.apex", 123); + apex_session->UpdateStateAndCommit(SessionState::STAGED); + + UnmountOnTearDown(shim_path); + OnStart(); + + apex_session = ApexSession::GetSession(123); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("Failed to activate packages")); + ASSERT_THAT(apex_session->GetErrorMessage(), + HasSubstr("has unexpected SHA512 hash")); +} + +} // namespace apex +} // namespace android diff --git a/apexd/apexd_test_utils.h b/apexd/apexd_test_utils.h index 0e43477d..5e3689d3 100644 --- a/apexd/apexd_test_utils.h +++ b/apexd/apexd_test_utils.h @@ -14,14 +14,34 @@ * limitations under the License. */ +#include <filesystem> +#include <fstream> + +#include <asm-generic/errno-base.h> +#include <gmock/gmock.h> +#include <gtest/gtest.h> +#include <sched.h> +#include <sys/mount.h> + +#include <android-base/errors.h> +#include <android-base/logging.h> +#include <android-base/macros.h> +#include <android-base/result.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <android-base/unique_fd.h> #include <android/apex/ApexInfo.h> #include <android/apex/ApexSessionInfo.h> #include <binder/IServiceManager.h> -#include <gmock/gmock.h> -#include <gtest/gtest.h> +#include <selinux/android.h> +#include "apex_file.h" #include "session_state.pb.h" +#include "com_android_apex.h" + +using android::base::Error; +using android::base::Result; using apex::proto::SessionState; namespace android { @@ -32,6 +52,7 @@ using ::testing::AllOf; using ::testing::Eq; using ::testing::ExplainMatchResult; using ::testing::Field; +using ::testing::Property; template <typename T> inline ::testing::AssertionResult IsOk(const android::base::Result<T>& result) { @@ -85,6 +106,22 @@ MATCHER_P(ApexInfoEq, other, "") { arg, result_listener); } +MATCHER_P(ApexFileEq, other, "") { + return ExplainMatchResult( + AllOf(Property("path", &ApexFile::GetPath, Eq(other.get().GetPath())), + Property("image_offset", &ApexFile::GetImageOffset, + Eq(other.get().GetImageOffset())), + Property("image_size", &ApexFile::GetImageSize, + Eq(other.get().GetImageSize())), + Property("fs_type", &ApexFile::GetFsType, + Eq(other.get().GetFsType())), + Property("public_key", &ApexFile::GetBundledPublicKey, + Eq(other.get().GetBundledPublicKey())), + Property("is_compressed", &ApexFile::IsCompressed, + Eq(other.get().IsCompressed()))), + arg, result_listener); +} + inline ApexSessionInfo CreateSessionInfo(int session_id) { ApexSessionInfo info; info.sessionId = session_id; @@ -128,5 +165,194 @@ inline void PrintTo(const ApexInfo& apex, std::ostream* os) { *os << "}"; } +inline Result<bool> CompareFiles(const std::string& filename1, + const std::string& filename2) { + std::ifstream file1(filename1, std::ios::binary); + std::ifstream file2(filename2, std::ios::binary); + + if (file1.bad() || file2.bad()) { + return Error() << "Could not open one of the file"; + } + + std::istreambuf_iterator<char> begin1(file1); + std::istreambuf_iterator<char> begin2(file2); + + return std::equal(begin1, std::istreambuf_iterator<char>(), begin2); +} + +inline android::base::Result<std::string> GetCurrentMountNamespace() { + std::string result; + if (!android::base::Readlink("/proc/self/ns/mnt", &result)) { + return android::base::ErrnoError() << "Failed to read /proc/self/ns/mnt"; + } + return result; +} + +// A helper class to switch back to the original mount namespace of a process +// upon exiting current scope. +class MountNamespaceRestorer final { + public: + explicit MountNamespaceRestorer() { + original_namespace_.reset(open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC)); + if (original_namespace_.get() < 0) { + PLOG(ERROR) << "Failed to open /proc/self/ns/mnt"; + } + } + + ~MountNamespaceRestorer() { + if (original_namespace_.get() != -1) { + if (setns(original_namespace_.get(), CLONE_NEWNS) == -1) { + PLOG(ERROR) << "Failed to switch back to " << original_namespace_.get(); + } + } + } + + private: + android::base::unique_fd original_namespace_; + DISALLOW_COPY_AND_ASSIGN(MountNamespaceRestorer); +}; + +inline std::vector<std::string> GetApexMounts() { + std::vector<std::string> apex_mounts; + std::string mount_info; + if (!android::base::ReadFileToString("/proc/self/mountinfo", &mount_info)) { + return apex_mounts; + } + for (const auto& line : android::base::Split(mount_info, "\n")) { + std::vector<std::string> tokens = android::base::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 && android::base::StartsWith(tokens[4], "/apex/")) { + apex_mounts.push_back(tokens[4]); + } + } + return apex_mounts; +} + +// Sets up a test environment for unit testing logic around mounting/unmounting +// apexes. For examples of usage see apexd_test.cpp +inline android::base::Result<void> SetUpApexTestEnvironment() { + using android::base::ErrnoError; + + // 1. Switch to new mount namespace. + if (unshare(CLONE_NEWNS) != 0) { + return ErrnoError() << "Failed to unshare"; + } + + // 2. Make everything private, so that changes don't propagate. + if (mount(nullptr, "/", nullptr, MS_PRIVATE | MS_REC, nullptr) == -1) { + return ErrnoError() << "Failed to mount / as private"; + } + + // 3. Unmount all apexes. This needs to happen in two phases: + // Note: unlike regular unmount flow in apexd, we don't destroy dm and loop + // devices, since that would've propagated outside of the test environment. + std::vector<std::string> apex_mounts = GetApexMounts(); + + // 3a. First unmount all bind mounds (without @version_code). + for (const auto& mount : apex_mounts) { + if (mount.find('@') == std::string::npos) { + if (umount2(mount.c_str(), 0) != 0) { + return ErrnoError() << "Failed to unmount " << mount; + } + } + } + + // 3.b Now unmount versioned mounts. + for (const auto& mount : apex_mounts) { + if (mount.find('@') != std::string::npos) { + if (umount2(mount.c_str(), 0) != 0) { + return ErrnoError() << "Failed to unmount " << mount; + } + } + } + + static constexpr const char* kApexMountForTest = "/mnt/scratch/apex"; + + // Clean up in case previous test left directory behind. + if (access(kApexMountForTest, F_OK) == 0) { + if (umount2(kApexMountForTest, MNT_FORCE | UMOUNT_NOFOLLOW) != 0) { + PLOG(WARNING) << "Failed to unmount " << kApexMountForTest; + } + if (rmdir(kApexMountForTest) != 0) { + return ErrnoError() << "Failed to rmdir " << kApexMountForTest; + } + } + + // 4. Create an empty tmpfs that will substitute /apex in tests. + if (mkdir(kApexMountForTest, 0755) != 0) { + return ErrnoError() << "Failed to mkdir " << kApexMountForTest; + } + + if (mount("tmpfs", kApexMountForTest, "tmpfs", 0, nullptr) == -1) { + return ErrnoError() << "Failed to mount " << kApexMountForTest; + } + + // 5. Overlay it over /apex via bind mount. + if (mount(kApexMountForTest, "/apex", nullptr, MS_BIND, nullptr) == -1) { + return ErrnoError() << "Failed to bind mount " << kApexMountForTest + << " over /apex"; + } + + // Just in case, run restorecon -R on /apex. + if (selinux_android_restorecon("/apex", SELINUX_ANDROID_RESTORECON_RECURSE) < + 0) { + return android::base::ErrnoError() << "Failed to restorecon /apex"; + } + + return {}; +} + +} // namespace apex +} // namespace android + +namespace com { +namespace android { +namespace apex { + +namespace testing { +MATCHER_P(ApexInfoXmlEq, other, "") { + using ::testing::AllOf; + using ::testing::Eq; + using ::testing::ExplainMatchResult; + using ::testing::Field; + using ::testing::Property; + + return ExplainMatchResult( + AllOf( + Property("moduleName", &ApexInfo::getModuleName, + Eq(other.getModuleName())), + Property("modulePath", &ApexInfo::getModulePath, + Eq(other.getModulePath())), + Property("preinstalledModulePath", + &ApexInfo::getPreinstalledModulePath, + Eq(other.getPreinstalledModulePath())), + Property("versionCode", &ApexInfo::getVersionCode, + Eq(other.getVersionCode())), + Property("isFactory", &ApexInfo::getIsFactory, + Eq(other.getIsFactory())), + Property("isActive", &ApexInfo::getIsActive, Eq(other.getIsActive())), + Property("lastUpdateMillis", &ApexInfo::getLastUpdateMillis, + Eq(other.getLastUpdateMillis()))), + arg, result_listener); +} + +} // namespace testing + +// Must be in com::android::apex namespace for gtest to pick it up. +inline void PrintTo(const ApexInfo& apex, std::ostream* os) { + *os << "apex_info: {\n"; + *os << " moduleName : " << apex.getModuleName() << "\n"; + *os << " modulePath : " << apex.getModulePath() << "\n"; + *os << " preinstalledModulePath : " << apex.getPreinstalledModulePath() + << "\n"; + *os << " versionCode : " << apex.getVersionCode() << "\n"; + *os << " isFactory : " << apex.getIsFactory() << "\n"; + *os << " isActive : " << apex.getIsActive() << "\n"; + *os << "}"; +} + } // namespace apex } // namespace android +} // namespace com diff --git a/apexd/apexd_testdata/Android.bp b/apexd/apexd_testdata/Android.bp index 1f74a532..0e7039a0 100644 --- a/apexd/apexd_testdata/Android.bp +++ b/apexd/apexd_testdata/Android.bp @@ -15,6 +15,10 @@ // These apex definitions will generate the prebuilt test data. The modules // are disabled so as not to pollute the build. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + apex_key { name: "com.android.apex.test_package.key", public_key: "com.android.apex.test_package.avbpubkey", @@ -22,6 +26,13 @@ apex_key { installable: false, } +apex_key { + name: "com.android.apex.compressed.key", + public_key: "com.android.apex.compressed.avbpubkey", + private_key: "com.android.apex.compressed.pem", + installable: false, +} + apex { name: "apex.apexd_test", manifest: "manifest.json", @@ -33,13 +44,173 @@ apex { } apex { + name: "com.android.apex.compressed.v1", + manifest: "manifest_compressed.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.compressed.key", + installable: false, + test_only_force_compression: true, + updatable: false, +} + +apex { + name: "com.android.apex.compressed.v1_different_digest", + manifest: "manifest_compressed.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["hash_of_dev_null"], + key: "com.android.apex.compressed.key", + installable: false, + test_only_force_compression: true, + updatable: false, +} + +genrule { + // Generates a compressed apex which doesn't have an original_apex file in it + name: "gen_capex_without_apex", + out: ["com.android.apex.compressed.v1_without_apex.capex"], + srcs: [":com.android.apex.compressed.v1"], + tools: ["soong_zip"], + cmd: "unzip -q $(in) -d $(genDir) && rm $(genDir)/original_apex && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) -L 9 " + + "-o $(genDir)/com.android.apex.compressed.v1_without_apex.capex" +} + +genrule { + // Generates a compressed apex which has different version of original_apex in it + name: "gen_capex_with_v2_apex", + out: ["com.android.apex.compressed.v1_with_v2_apex.capex"], + srcs: [":com.android.apex.compressed.v2"], + tools: ["soong_zip", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop version 1 $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) -L 9 " + + "-o $(genDir)/com.android.apex.compressed.v1_with_v2_apex.capex" +} + +genrule { + // Generates a compressed apex which can be opened but not decompressed + name: "gen_capex_not_decompressible", + out: ["com.android.apex.compressed.v1_not_decompressible.capex"], + srcs: [":com.android.apex.compressed.v1"], + tools: ["soong_zip", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && echo '' > $(genDir)/original_apex && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) -L 9 " + + "-o $(genDir)/com.android.apex.compressed.v1_not_decompressible.capex" +} + +genrule { + // Generates a capex which has same module name as com.android.apex.compressed, but + // is contains a different public key. + name: "gen_key_mismatch_capex", + out: ["com.android.apex.compressed_different_key.capex"], + srcs: [":apex.apexd_test_no_inst_key"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest", "apex_compression_tool"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop name com.android.apex.compressed $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/com.android.apex.compressed_different_key.apex && " + + "$(location apex_compression_tool) compress " + + "--apex_compression_tool_path='out/soong/host/linux-x86/bin:prebuilts/sdk/tools/linux/bin' " + + "--input=$(genDir)/com.android.apex.compressed_different_key.apex " + + "--output=$(genDir)/com.android.apex.compressed_different_key.capex" +} + +genrule { + // Generates a capex which has a different public key than original_apex + name: "gen_key_mismatch_with_original_capex", + out: ["com.android.apex.compressed_key_mismatch_with_original.capex"], + srcs: [":com.android.apex.compressed.v1"], + tools: ["soong_zip"], + cmd: "unzip -q $(in) -d $(genDir) && " + // unzip input + "echo 'different-key' >> $(genDir)/apex_pubkey && " + // modify the public key + "$(location soong_zip) -d -C $(genDir) -D $(genDir) -L 9 " +// repack the compressed APEX + "-o $(genDir)/com.android.apex.compressed_key_mismatch_with_original.capex", +} + +genrule { + // Generates an apex which has a different manifest outside the filesystem + // image. + name: "gen_manifest_mismatch_compressed_apex_v2", + out: ["com.android.apex.compressed.v2_manifest_mismatch.apex"], + srcs: [":com.android.apex.compressed.v2_original"], + tools: ["soong_zip", "zipalign", "conv_apex_manifest"], + cmd: "unzip -q $(in) -d $(genDir) && " + + "$(location conv_apex_manifest) setprop version 137 $(genDir)/apex_manifest.pb && " + + "$(location soong_zip) -d -C $(genDir) -D $(genDir) " + + "-s apex_manifest.pb -s apex_payload.img -s apex_pubkey " + + "-o $(genDir)/unaligned.apex && " + + "$(location zipalign) -f 4096 $(genDir)/unaligned.apex " + + "$(genDir)/com.android.apex.compressed.v2_manifest_mismatch.apex" +} + +apex { + name: "com.android.apex.compressed.v1_original", + manifest: "manifest_compressed.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.compressed.key", + installable: false, + compressible: false, + updatable: false, +} + +apex { + name: "com.android.apex.compressed.v1_different_digest_original", + manifest: "manifest_compressed.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["hash_of_dev_null"], + key: "com.android.apex.compressed.key", + installable: false, + compressible: false, + updatable: false, +} + +apex { + name: "com.android.apex.compressed.v2", + manifest: "manifest_compressed_v2.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.compressed.key", + installable: false, + test_only_force_compression: true, + updatable: false, +} + +apex { + name: "com.android.apex.compressed.v2_original", + manifest: "manifest_compressed_v2.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.compressed.key", + installable: false, + compressible: false, + updatable: false, +} + +apex { + name: "apex.apexd_test_f2fs", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.test_package.key", + installable: false, + min_sdk_version: "current", + payload_fs_type: "f2fs", +} + +apex { name: "apex.apexd_test_no_hashtree", manifest: "manifest.json", file_contexts: ":apex.test-file_contexts", prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package.key", installable: false, - test_only_no_hashtree: true, + generate_hashtree: false, + updatable: false, } // This APEX has same name and version as apex.apexd_test_no_hashtree, but has @@ -55,7 +226,8 @@ apex { ], key: "com.android.apex.test_package.key", installable: false, - test_only_no_hashtree: true, + generate_hashtree: false, + updatable: false, } apex { @@ -65,6 +237,7 @@ apex { prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package.key", installable: false, + updatable: false, } apex { @@ -92,6 +265,7 @@ apex { prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package.key", installable: false, + updatable: false, } apex_key { @@ -109,6 +283,7 @@ apex { key: "com.android.apex.test_package.preinstall.key", binaries: ["apex_test_preInstallHook"], installable: false, + updatable: false, } apex_key { @@ -126,6 +301,7 @@ apex { key: "com.android.apex.test_package.postinstall.key", binaries: ["apex_test_postInstallHook"], installable: false, + updatable: false, } apex_key { @@ -143,6 +319,7 @@ apex { key: "com.android.apex.test_package.prepostinstall.fail.key", binaries: ["apex_test_prePostInstallHookFail"], installable: false, + updatable: false, } apex_key { @@ -159,6 +336,18 @@ apex { prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package.no_inst_key.key", installable: false, + updatable: false, +} + +apex { + name: "apex.apexd_test_f2fs_no_inst_key", + manifest: "manifest_no_inst_key.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.test_package.no_inst_key.key", + installable: false, + payload_fs_type: "f2fs", + updatable: false, } apex_key { @@ -175,6 +364,7 @@ apex { prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package_2.key", installable: false, + updatable: false, } sh_binary { @@ -199,6 +389,7 @@ apex { prebuilts: ["sample_prebuilt_file"], key: "com.android.apex.test_package.key", installable: false, + updatable: false, } prebuilt_etc { @@ -212,3 +403,156 @@ prebuilt_apex { filename: "corrupted_b146895998.apex", installable: false, } + +// APEX for banned name test cannot be generated at build time. +// This file can be generated manually by creating new apex target +// with manifest name 'sharedlibs', and modify aapt2 to skip validating +// package name from aapt::util::IsAndroidPackageName(). +prebuilt_apex { + name: "apex.banned_name", + src: "sharedlibs.apex", + filename: "sharedlibs.apex", + installable: false, +} + +// A compressed apex that also provides shared libs. +// Should be declined by ApexFile::Open. +apex { + name: "com.android.apex.compressed_sharedlibs", + manifest: "manifest_compressed_sharedlibs.json", + file_contexts: ":apex.test-file_contexts", + prebuilts: ["sample_prebuilt_file"], + key: "com.android.apex.compressed.key", + installable: false, + test_only_force_compression: true, + updatable: false, +} + +apex { + name: "test.rebootless_apex_v1", + manifest: "manifest_rebootless.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, + // TODO(ioffe): we should have a separate field to hashtree presence. + min_sdk_version: "29", // test requires hashtree to be present. +} + +apex { + name: "test.rebootless_apex_v2", + manifest: "manifest_rebootless_v2.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, + // TODO(ioffe): we should have a separate field to hashtree presence. + min_sdk_version: "29", // test requires hashtree to be present. +} + +apex { + name: "test.rebootless_apex_v2_no_hashtree", + manifest: "manifest_rebootless_v2.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, + generate_hashtree: false, +} + +apex { + name: "test.rebootless_apex_provides_sharedlibs", + manifest: "manifest_rebootless_provides_sharedlibs.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_provides_native_libs", + manifest: "manifest_rebootless_provides_native_libs.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_requires_shared_apex_libs", + manifest: "manifest_rebootless_requires_shared_apex_libs.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_jni_libs", + manifest: "manifest_rebootless_jni_libs.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_add_native_lib", + manifest: "manifest_rebootless_add_native_lib.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_remove_native_lib", + manifest: "manifest_rebootless_remove_native_lib.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, +} + +apex { + name: "test.rebootless_apex_app_in_apex", + manifest: "manifest_rebootless_v2.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, + apps: ["AppInRebootlessApex"], + // TODO(ioffe): we should have a separate field to hashtree presence. + min_sdk_version: "29", // test requires hashtree to be present. +} + +apex { + name: "test.rebootless_apex_priv_app_in_apex", + manifest: "manifest_rebootless_v2.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test_package.key", + installable: false, + updatable: false, + apps: ["PrivAppInRebootlessApex"], + // TODO(ioffe): we should have a separate field to hashtree presence. + min_sdk_version: "29", // test requires hashtree to be present. +} + +android_app { + name: "AppInRebootlessApex", + sdk_version: "29", + manifest: "AppInRebootlessApex_AndroidManifest.xml", + apex_available: [ + "test.rebootless_apex_app_in_apex", + ], +} + +android_app { + name: "PrivAppInRebootlessApex", + sdk_version: "29", + privileged: true, + manifest: "AppInRebootlessApex_AndroidManifest.xml", + apex_available: [ + "test.rebootless_apex_priv_app_in_apex", + ], +} diff --git a/tests/src/com/android/tests/apex/CellbroadcastHostTest.java b/apexd/apexd_testdata/AppInRebootlessApex_AndroidManifest.xml index 97230422..242d1be4 100644 --- a/tests/src/com/android/tests/apex/CellbroadcastHostTest.java +++ b/apexd/apexd_testdata/AppInRebootlessApex_AndroidManifest.xml @@ -1,5 +1,6 @@ -/* - * Copyright (C) 2019 The Android Open Source Project +<?xml version="1.0" encoding="utf-8"?> +<!-- + * Copyright (C) 2017 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. @@ -12,17 +13,8 @@ * 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. - */ + --> -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class CellbroadcastHostTest extends ApexE2EBaseHostTest { -}
\ No newline at end of file +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.apex.rebootless_tests.app"> +</manifest> diff --git a/apexd/apexd_testdata/com.android.apex.compressed.avbpubkey b/apexd/apexd_testdata/com.android.apex.compressed.avbpubkey Binary files differnew file mode 100644 index 00000000..92767eac --- /dev/null +++ b/apexd/apexd_testdata/com.android.apex.compressed.avbpubkey diff --git a/apexd/apexd_testdata/com.android.apex.compressed.pem b/apexd/apexd_testdata/com.android.apex.compressed.pem new file mode 100644 index 00000000..bc2c0a72 --- /dev/null +++ b/apexd/apexd_testdata/com.android.apex.compressed.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAqvAUWzu1+JmgUDTMx22v3Wcdv05CwE3y7roVbIhIFtfQjvjY +NNStwnl5r+2er0BmlNDWAPC20x46uF2kfSecIjoWMOAjMtkgWHYX6nZqE2BXo6Mj +KnxC+D+zdj6FO+QCbAZ1JKlfy/zGyq37VTIQu1OF9a3FyusOP2qapWr4jYiZ2WLr +gHhk/qvurN2GhpeTX7TMSEOVfg2Q7kKTxxXE5blw5lRaF9FzX4FMfU296qyXJ1qE +8pLjcVwVjTjrAO69AQ3S+lZZXA5WJwbHqU5WaRL5k+XjXASyoxTRvOHOsQ3mxQ+B +Ad227pk/x5lZLnnuc7d3Qe5cB2yJNDaENEptCA5VKaBG1QbRBL8ykD45w2OwIP7p +2H58b/2tEgtjA4bpWEFqwVa8LXMBg2x56Sbo8YWmQL4FE14XAYyI3eAs171JZV6e +nf9/ll+45oIXI2wY0jttfnYyGErLlbCIWYYByAnV5mwZ9eBrXl/xu65+MUKVnZOA +2y1KJch1eXUjLqMRAW6DBwOmAjsJhuiF8u2gZlF/zgnzPzWbbvfMWi/aztdCV2Yb +rRhKZT6i2A0a9o3lghwGpHJjXwJ23ELWmfWI6mxGGJyhrVRLvUlRp2a8QRmw6mcc +sWVWdichcjvx20fIPHanzC/TtgKe/smQjZ1GQClPbqRvbhoxleklLDk+NWkCAwEA +AQKCAgAJLwbY8/dN2OsdBAkwebsmGQEnIwxBCq8Pll3KS/QbjhK9a68p/3cqaJ5a +DlklLz+TpTiqKkSYSRp8h0NsGfGwgRBqJdCeTb4IIqgcR6phSh7LQtuDz9NFRR7e +LnO4CQL8TMiEZLkp23XOs9Q5+oudlNowndpvkXtdetu+IWYjLICfhkoCx/UdHZ2g +GfK9Tm1zieIjy8W/VPlBY8BOxOOkN/dR2JxShUU+j1LNtJyMfCNO0PUtlyizEXBy ++ujdTvZGlTiNjJAWewqz5BBcD0JjOMrB4Gr7qaDzVA4EqNkS+B063x9eO0w1u3QK +xDlfXtupyeSVXToCiBlC/d7SdGExJBqpebCKWLvdIXxDbgU5tNeKXcbHNp9zOduV +RTulPxseqKgvO/wDc27F1AChG68GYyWcEm7kWQd6BVmOOwxRurssrTgchFk/HrY+ +Vzlbqs+CTyrYoyZuMF8W7E0U7pQtLx4VMv1AEfHyzLmbPgsdwmlPZ4On+yks4rGt +jHGtUergxsAiiYBHZqXbmuK7UTg1wi5u8p4QgTUxC5jkKMetkgqGajWf5BDpf02E ++0P+58VGHkd3ACPFG4PcS8gnmjpx/EXicWq855w+nshr8Ff4Gs9LbUv0pg8s9mZA +2JGfGyjxp8h9/06OrKq5pM5HVuOCZ0AgEHEf++AeBSq24mrgkQKCAQEA28JaiH83 +7NCiK8XJebgPgVGl/wwugsA+Ea3Kv/prQGmFW/IVJNAbMAREkzDi4IaVGySPHDpo +OaDFFPOiC9zyveK8LdJyqaInEdN6nerGuM4ubA27DKP52MHfMBr6FDYgru5hcskw +4gHGZAWhD242rzhFGgfTTDbRXRhqn93f/6+yHbjyjybs5vPM2gU/qPlwdjUPxXFo +g3Nrx3TyrMtfUL/dhHniF+SiiVfu8hNqi4EuQ295jGtwMZl8U0/UVuQ6UQUMe6cT +VfKuunxyCq2AOvd4XLODZ9BcZDtTDLIsXaPyVzJUPGxugqlid/i7SuV+c3unT0dD +Ma2+7n6gdStuPQKCAQEAxyCeECT3Yhx1rB3SvloGVCMQY3ajs4/a3qIPLa18X8ZL +K1qUevlq7mpJVbnI0KmbuyNzPyGkPbQtuGU0g8kvchD7nH3Pi9SabB2ereIi9YO0 +s5a+ZVVPdtD69oCfhhKh9BPrjsSDnci2ZjCYkxzKHVtSvlJQmWAaxcUCwUKPvXen +LBQ4QNC9bnVj6v9XJwWy3YgrwpRetdBPU/jnSAjLOpjWKlXz4pokHpIR1kAsRY9O +In2gkP27PwSBMEZ4qxKa0uujQ5L4odqtDm4O15fF1IQ0/y+sNTrZArg54X3MYG6J +UyR9ReKXUL5nWYYSEsVJ6D9J8fiow21hiYnSt82inQKCAQEAlrW+QtgEcYtPfHeD +Sc96CrUFA1nGV/MhXhxy7J//h8gWJk1qRLnXu2Fh6hPftB14CopS/wfrTII+RrUj +D8GyU5k4drBZ5I4I/0eqUrydFkaIPaBZBD08bnPe7W3CzbOlTHK9L+xcctLGzPez +UhLCu/36HfT56s25XYAON58BLKfAnnOlHZmTZHwUo/xvgSG4B8kyDLVO+L9iTgKd +HvXGY2mBsIWqEbrB4TEF9MxuCEhKgwLjN/LCmbFqOvSHaiPQ/plYy0B0mT/6pngL ++ditFUN8Lw8JclvJ0Q+CUDWtCXcTDsu8S0gNrdweZKqXP7ENvIMz5cG4ikxeoc8D +mfdz/QKCAQBZkzGnX4mtJ9JDV7Maj9Ky/Ib9xzvCpZ62cb5UNOtzBfeAjCGo5BQX +JdbRal6Mhw+X8k2Ag7inTSsX/ObPtavTKxKUhf/cDgpdQkHERKqnONULyG7jlKnH +cCDEzH12SWFzM5bORVZTnxt2ArxPyS6eYBtrpAm/xPymJIaluzR/7ZhU+s+HUJ82 +VjZZWv2wfx7ECuJsiGPGc+uLgbdArzwEowYMS1gHgoFnAxxk/b8sl1d1qn+VWZ5m +rbcTqU/U3Oyqnvd1iWKxJHaevCGPGCYVAFf6x043L765O3hGGFncszyxGwQDcPfS +iaRiIC53JSqm//lakRoRt12eClKw/h79AoIBAQC0uu/kwB//HdQbzJkejoBZDWpu +2eTZKnLs/5hsU/CPfJnL2cJfStJIFh6WgutNoW++Xi79OfagEQx6hAyfl5HexV8V +2PNDMgcgxauAObhkZtg8x5ZDAyrOiXVHR+T3cm8qQbVtrHdiX97hrg9tTj923baK +TpI9qQw+2Jg627BR2t/rYIrZwdL2oqbznihIIPg6FkCT/53AfX8W1U9GWPnUyIkn +KvRdtY6osOS/C/Dju/Eg6710umACRPLa5928A5q+1jY6TntRIG8Q/agL4IXjR/0E +O0OkoRArOFt2NGN88Vqy7ses0VfP2UIpjjRP3/8xwUFmUo8vdoR5oy4kC/Sz +-----END RSA PRIVATE KEY----- diff --git a/apexd/apexd_testdata/manifest_compressed.json b/apexd/apexd_testdata/manifest_compressed.json new file mode 100644 index 00000000..8c07cc82 --- /dev/null +++ b/apexd/apexd_testdata/manifest_compressed.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.compressed", + "version": 1 +} diff --git a/apexd/apexd_testdata/manifest_compressed_sharedlibs.json b/apexd/apexd_testdata/manifest_compressed_sharedlibs.json new file mode 100644 index 00000000..30d2835f --- /dev/null +++ b/apexd/apexd_testdata/manifest_compressed_sharedlibs.json @@ -0,0 +1,5 @@ +{ + "name": "com.android.apex.compressed", + "version": 1, + "provideSharedApexLibs": true +} diff --git a/apexd/apexd_testdata/manifest_compressed_v2.json b/apexd/apexd_testdata/manifest_compressed_v2.json new file mode 100644 index 00000000..c5adb514 --- /dev/null +++ b/apexd/apexd_testdata/manifest_compressed_v2.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.compressed", + "version": 2 +} diff --git a/apexd/apexd_testdata/manifest_rebootless.json b/apexd/apexd_testdata/manifest_rebootless.json new file mode 100644 index 00000000..a37fa005 --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless.json @@ -0,0 +1,9 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_add_native_lib.json b/apexd/apexd_testdata/manifest_rebootless_add_native_lib.json new file mode 100644 index 00000000..eab172c9 --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_add_native_lib.json @@ -0,0 +1,10 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar", + "libnew" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_jni_libs.json b/apexd/apexd_testdata/manifest_rebootless_jni_libs.json new file mode 100644 index 00000000..bf88d2eb --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_jni_libs.json @@ -0,0 +1,12 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ], + "jniLibs": [ + "libjniwow" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_provides_native_libs.json b/apexd/apexd_testdata/manifest_rebootless_provides_native_libs.json new file mode 100644 index 00000000..ef829b07 --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_provides_native_libs.json @@ -0,0 +1,12 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ], + "provideNativeLibs": [ + "libbaz" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_provides_sharedlibs.json b/apexd/apexd_testdata/manifest_rebootless_provides_sharedlibs.json new file mode 100644 index 00000000..d91c21e4 --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_provides_sharedlibs.json @@ -0,0 +1,10 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ], + "provideSharedApexLibs": true +} diff --git a/apexd/apexd_testdata/manifest_rebootless_remove_native_lib.json b/apexd/apexd_testdata/manifest_rebootless_remove_native_lib.json new file mode 100644 index 00000000..0926e70f --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_remove_native_lib.json @@ -0,0 +1,8 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libbar" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_requires_shared_apex_libs.json b/apexd/apexd_testdata/manifest_rebootless_requires_shared_apex_libs.json new file mode 100644 index 00000000..faaadfda --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_requires_shared_apex_libs.json @@ -0,0 +1,12 @@ +{ + "name": "test.apex.rebootless", + "version": 1, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ], + "requireSharedApexLibs": [ + "libabc" + ] +} diff --git a/apexd/apexd_testdata/manifest_rebootless_v2.json b/apexd/apexd_testdata/manifest_rebootless_v2.json new file mode 100644 index 00000000..dfcb21fe --- /dev/null +++ b/apexd/apexd_testdata/manifest_rebootless_v2.json @@ -0,0 +1,9 @@ +{ + "name": "test.apex.rebootless", + "version": 2, + "supportsRebootlessUpdate": true, + "requireNativeLibs": [ + "libfoo", + "libbar" + ] +} diff --git a/apexd/apexd_testdata/sharedlibs.apex b/apexd/apexd_testdata/sharedlibs.apex Binary files differnew file mode 100644 index 00000000..cde11d51 --- /dev/null +++ b/apexd/apexd_testdata/sharedlibs.apex diff --git a/apexd/apexd_utils.h b/apexd/apexd_utils.h index 2759f9aa..23194ebf 100644 --- a/apexd/apexd_utils.h +++ b/apexd/apexd_utils.h @@ -18,9 +18,11 @@ #define ANDROID_APEXD_APEXD_UTILS_H_ #include <chrono> +#include <cstdint> #include <filesystem> #include <string> #include <thread> +#include <type_traits> #include <vector> #include <dirent.h> @@ -30,17 +32,14 @@ #include <android-base/chrono_utils.h> #include <android-base/logging.h> +#include <android-base/properties.h> #include <android-base/result.h> #include <android-base/scopeguard.h> #include <android-base/strings.h> #include <cutils/android_reboot.h> +#include <selinux/android.h> #include "apex_constants.h" -#include "string_log.h" - -using android::base::ErrnoError; -using android::base::Error; -using android::base::Result; namespace android { namespace apex { @@ -61,21 +60,18 @@ inline int WaitChild(pid_t pid) { } } -// TODO(ioffe): change to Result<void>? -inline int ForkAndRun(const std::vector<std::string>& args, - std::string* error_msg) { +inline android::base::Result<void> ForkAndRun( + const std::vector<std::string>& args) { LOG(DEBUG) << "Forking : " << android::base::Join(args, " "); std::vector<const char*> argv; argv.resize(args.size() + 1, nullptr); std::transform(args.begin(), args.end(), argv.begin(), [](const std::string& in) { return in.c_str(); }); - // 3) Fork. pid_t pid = fork(); if (pid == -1) { // Fork failed. - *error_msg = PStringLog() << "Unable to fork"; - return -1; + return android::base::ErrnoError() << "Unable to fork"; } if (pid == 0) { @@ -86,13 +82,13 @@ inline int ForkAndRun(const std::vector<std::string>& args, int rc = WaitChild(pid); if (rc != 0) { - *error_msg = StringLog() << "Failed run: status=" << rc; + return android::base::Error() << "Failed run: status=" << rc; } - return rc; + return {}; } template <typename Fn> -Result<void> WalkDir(const std::string& path, Fn fn) { +android::base::Result<void> WalkDir(const std::string& path, Fn fn) { namespace fs = std::filesystem; std::error_code ec; auto it = fs::directory_iterator(path, ec); @@ -102,14 +98,15 @@ Result<void> WalkDir(const std::string& path, Fn fn) { it.increment(ec); } if (ec) { - return Error() << "Can't open " << path - << " for reading : " << ec.message(); + return android::base::Error() + << "Can't open " << path << " for reading : " << ec.message(); } return {}; } template <typename FilterFn> -Result<std::vector<std::string>> ReadDir(const std::string& path, FilterFn fn) { +android::base::Result<std::vector<std::string>> ReadDir(const std::string& path, + FilterFn fn) { namespace fs = std::filesystem; std::vector<std::string> ret; @@ -129,72 +126,70 @@ inline bool IsEmptyDirectory(const std::string& path) { return res.ok() && res->empty(); } -inline Result<void> createDirIfNeeded(const std::string& path, mode_t mode) { +inline android::base::Result<void> CreateDirIfNeeded(const std::string& path, + mode_t mode) { struct stat stat_data; if (stat(path.c_str(), &stat_data) != 0) { if (errno == ENOENT) { if (mkdir(path.c_str(), mode) != 0) { - return ErrnoError() << "Could not mkdir " << path; + return android::base::ErrnoError() << "Could not mkdir " << path; } } else { - return ErrnoError() << "Could not stat " << path; + return android::base::ErrnoError() << "Could not stat " << path; } } else { if (!S_ISDIR(stat_data.st_mode)) { - return Error() << path << " exists and is not a directory."; + return android::base::Error() + << path << " exists and is not a directory."; } } // Need to manually call chmod because mkdir will create a folder with // permissions mode & ~umask. if (chmod(path.c_str(), mode) != 0) { - return ErrnoError() << "Could not chmod " << path; + return android::base::ErrnoError() << "Could not chmod " << path; } return {}; } -inline Result<void> DeleteDirContent(const std::string& path) { +inline android::base::Result<void> DeleteDirContent(const std::string& path) { auto files = ReadDir(path, [](auto _) { return true; }); if (!files.ok()) { - return Error() << "Failed to delete " << path << " : " << files.error(); + return android::base::Error() + << "Failed to delete " << path << " : " << files.error(); } for (const std::string& file : *files) { - if (unlink(file.c_str()) != 0) { - return ErrnoError() << "Failed to delete " << file; + std::error_code ec; + std::filesystem::remove_all(file, ec); + if (ec) { + return android::base::Error() + << "Failed to delete path " << file << " : " << ec.message(); } } return {}; } -inline Result<void> DeleteDir(const std::string& path) { +inline android::base::Result<void> DeleteDir(const std::string& path) { namespace fs = std::filesystem; std::error_code ec; fs::remove_all(path, ec); if (ec) { - return Error() << "Failed to delete path " << path << " : " << ec.message(); + return android::base::Error() + << "Failed to delete path " << path << " : " << ec.message(); } return {}; } -inline Result<ino_t> get_path_inode(const std::string& path) { - struct stat buf; - memset(&buf, 0, sizeof(buf)); - if (stat(path.c_str(), &buf) != 0) { - return ErrnoError() << "Failed to stat " << path; - } else { - return buf.st_ino; - } -} - -inline Result<bool> PathExists(const std::string& path) { +inline android::base::Result<bool> PathExists(const std::string& path) { namespace fs = std::filesystem; std::error_code ec; if (!fs::exists(fs::path(path), ec)) { if (ec) { - return Error() << "Failed to access " << path << " : " << ec.message(); + return android::base::Error() + << "Failed to access " << path << " : " << ec.message(); } else { return false; } @@ -209,8 +204,8 @@ inline void Reboot() { } } -inline Result<void> WaitForFile(const std::string& path, - std::chrono::nanoseconds timeout) { +inline android::base::Result<void> WaitForFile( + const std::string& path, std::chrono::nanoseconds timeout) { android::base::Timer t; bool has_slept = false; while (t.duration() < timeout) { @@ -224,10 +219,12 @@ inline Result<void> WaitForFile(const std::string& path, std::this_thread::sleep_for(5ms); has_slept = true; } - return ErrnoError() << "wait for '" << path << "' timed out and took " << t; + return android::base::ErrnoError() + << "wait for '" << path << "' timed out and took " << t; } -inline Result<std::vector<std::string>> GetSubdirs(const std::string& path) { +inline android::base::Result<std::vector<std::string>> GetSubdirs( + const std::string& path) { namespace fs = std::filesystem; auto filter_fn = [](const std::filesystem::directory_entry& entry) { std::error_code ec; @@ -241,10 +238,140 @@ inline Result<std::vector<std::string>> GetSubdirs(const std::string& path) { return ReadDir(path, filter_fn); } -inline Result<std::vector<std::string>> GetDeUserDirs() { +inline android::base::Result<std::vector<std::string>> GetDeUserDirs() { return GetSubdirs(kDeNDataDir); } +inline android::base::Result<std::vector<std::string>> FindFilesBySuffix( + const std::string& path, const std::vector<std::string>& suffix_list) { + auto filter_fn = + [&suffix_list](const std::filesystem::directory_entry& entry) { + for (const std::string& suffix : suffix_list) { + std::error_code ec; + auto name = entry.path().filename().string(); + if (entry.is_regular_file(ec) && + android::base::EndsWith(name, suffix)) { + return true; // suffix matches, take. + } + } + return false; + }; + return ReadDir(path, filter_fn); +} + +inline android::base::Result<std::vector<std::string>> FindApexes( + const std::vector<std::string>& paths) { + std::vector<std::string> result; + for (const auto& path : paths) { + auto exist = PathExists(path); + if (!exist.ok()) { + return exist.error(); + } + if (!*exist) continue; + + const auto& apexes = FindFilesBySuffix(path, {kApexPackageSuffix}); + if (!apexes.ok()) { + return apexes; + } + + result.insert(result.end(), apexes->begin(), apexes->end()); + } + return result; +} + +// Returns first path between |first_dir| and |second_dir| that correspond to a +// existing directory. Returns error if neither |first_dir| nor |second_dir| +// correspond to an existing directory. +inline android::base::Result<std::string> FindFirstExistingDirectory( + const std::string& first_dir, const std::string& second_dir) { + struct stat stat_buf; + if (stat(first_dir.c_str(), &stat_buf) != 0) { + PLOG(WARNING) << "Failed to stat " << first_dir; + if (stat(second_dir.c_str(), &stat_buf) != 0) { + return android::base::ErrnoError() << "Failed to stat " << second_dir; + } + if (!S_ISDIR(stat_buf.st_mode)) { + return android::base::Error() << second_dir << " is not a directory"; + } + return second_dir; + } + + if (S_ISDIR(stat_buf.st_mode)) { + return first_dir; + } + LOG(WARNING) << first_dir << " is not a directory"; + + if (stat(second_dir.c_str(), &stat_buf) != 0) { + return android::base::ErrnoError() << "Failed to stat " << second_dir; + } + if (!S_ISDIR(stat_buf.st_mode)) { + return android::base::Error() << second_dir << " is not a directory"; + } + return second_dir; +} + +// Copies all entries under |from| directory to |to| directory, and then them. +// Leaving |from| empty. +inline android::base::Result<void> MoveDir(const std::string& from, + const std::string& to) { + struct stat stat_buf; + if (stat(to.c_str(), &stat_buf) != 0) { + return android::base::ErrnoError() << "Failed to stat " << to; + } + if (!S_ISDIR(stat_buf.st_mode)) { + return android::base::Error() << to << " is not a directory"; + } + + namespace fs = std::filesystem; + std::error_code ec; + auto it = fs::directory_iterator(from, ec); + if (ec) { + return android::base::Error() + << "Can't read " << from << " : " << ec.message(); + } + + for (const auto& end = fs::directory_iterator(); it != end;) { + auto from_path = it->path(); + it.increment(ec); + if (ec) { + return android::base::Error() + << "Can't read " << from << " : " << ec.message(); + } + auto to_path = to / from_path.filename(); + fs::copy(from_path, to_path, fs::copy_options::recursive, ec); + if (ec) { + return android::base::Error() << "Failed to copy " << from_path << " to " + << to_path << " : " << ec.message(); + } + fs::remove_all(from_path, ec); + if (ec) { + return android::base::Error() + << "Failed to delete " << from_path << " : " << ec.message(); + } + } + return {}; +} + +inline android::base::Result<uintmax_t> GetFileSize( + const std::string& file_path) { + std::error_code ec; + auto value = std::filesystem::file_size(file_path, ec); + if (ec) { + return android::base::Error() << "Failed to get file size of " << file_path + << " : " << ec.message(); + } + + return value; +} + +inline android::base::Result<void> RestoreconPath(const std::string& path) { + unsigned int seflags = SELINUX_ANDROID_RESTORECON_RECURSE; + if (selinux_android_restorecon(path.c_str(), seflags) < 0) { + return android::base::ErrnoError() << "Failed to restorecon " << path; + } + return {}; +} + } // namespace apex } // namespace android diff --git a/apexd/apexd_utils_test.cpp b/apexd/apexd_utils_test.cpp new file mode 100644 index 00000000..cbbc45e1 --- /dev/null +++ b/apexd/apexd_utils_test.cpp @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2020 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 <filesystem> +#include <fstream> +#include <new> +#include <string> + +#include <errno.h> + +#include <android-base/file.h> +#include <android-base/result.h> +#include <android-base/stringprintf.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> + +#include "apexd.h" +#include "apexd_test_utils.h" +#include "apexd_utils.h" + +namespace android { +namespace apex { +namespace { + +namespace fs = std::filesystem; + +using android::apex::testing::IsOk; +using android::base::Basename; +using android::base::Join; +using android::base::StringPrintf; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +// TODO(b/170327382): add unit tests for apexd_utils.h + +TEST(ApexdUtilTest, DeleteDirContent) { + TemporaryDir root_dir; + TemporaryFile child_file_1(root_dir.path); + TemporaryFile child_file_2(root_dir.path); + std::string child_dir = StringPrintf("%s/child-dir", root_dir.path); + CreateDirIfNeeded(child_dir, 0755); + TemporaryFile nested_file(child_dir); + + auto content = ReadDir(root_dir.path, [](auto _) { return true; }); + IsOk(content); + ASSERT_EQ(content->size(), 3u); + + auto del_result = DeleteDirContent(root_dir.path); + IsOk(del_result); + content = ReadDir(root_dir.path, [](auto _) { return true; }); + IsOk(content); + ASSERT_EQ(content->size(), 0u); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryBothExist) { + TemporaryDir first_dir; + TemporaryDir second_dir; + auto result = FindFirstExistingDirectory(first_dir.path, second_dir.path); + ASSERT_TRUE(IsOk(result)); + ASSERT_EQ(*result, first_dir.path); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryOnlyFirstExist) { + TemporaryDir first_dir; + auto second_dir = "/data/local/tmp/does/not/exist"; + auto result = FindFirstExistingDirectory(first_dir.path, second_dir); + ASSERT_TRUE(IsOk(result)); + ASSERT_EQ(*result, first_dir.path); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryOnlySecondExist) { + auto first_dir = "/data/local/tmp/does/not/exist"; + TemporaryDir second_dir; + auto result = FindFirstExistingDirectory(first_dir, second_dir.path); + ASSERT_TRUE(IsOk(result)); + ASSERT_EQ(*result, second_dir.path); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryNoneExist) { + auto first_dir = "/data/local/tmp/does/not/exist"; + auto second_dir = "/data/local/tmp/also/does/not/exist"; + auto result = FindFirstExistingDirectory(first_dir, second_dir); + ASSERT_FALSE(IsOk(result)); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryFirstFileSecondDir) { + TemporaryFile first_file; + TemporaryDir second_dir; + auto result = FindFirstExistingDirectory(first_file.path, second_dir.path); + ASSERT_TRUE(IsOk(result)); + ASSERT_EQ(*result, second_dir.path); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryFirstDirSecondFile) { + TemporaryDir first_dir; + TemporaryFile second_file; + auto result = FindFirstExistingDirectory(first_dir.path, second_file.path); + ASSERT_TRUE(IsOk(result)); + ASSERT_EQ(*result, first_dir.path); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryBothFiles) { + TemporaryFile first_file; + TemporaryFile second_file; + auto result = FindFirstExistingDirectory(first_file.path, second_file.path); + ASSERT_FALSE(IsOk(result)); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryFirstFileSecondDoesNotExist) { + TemporaryFile first_file; + auto second_dir = "/data/local/tmp/does/not/exist"; + auto result = FindFirstExistingDirectory(first_file.path, second_dir); + ASSERT_FALSE(IsOk(result)); +} + +TEST(ApexdUtilTest, FindFirstExistingDirectoryFirsDoesNotExistSecondFile) { + auto first_dir = "/data/local/tmp/does/not/exist"; + TemporaryFile second_file; + auto result = FindFirstExistingDirectory(first_dir, second_file.path); + ASSERT_FALSE(IsOk(result)); +} + +TEST(ApexdUtilTest, MoveDir) { + TemporaryDir from; + TemporaryDir to; + + TemporaryFile from_1(from.path); + auto from_subdir = StringPrintf("%s/subdir", from.path); + if (mkdir(from_subdir.c_str(), 07000) != 0) { + FAIL() << "Failed to mkdir " << from_subdir << " : " << strerror(errno); + } + TemporaryFile from_2(from_subdir); + + auto result = MoveDir(from.path, to.path); + ASSERT_TRUE(IsOk(result)); + ASSERT_TRUE(fs::is_empty(from.path)); + + std::vector<std::string> content; + for (const auto& it : fs::recursive_directory_iterator(to.path)) { + content.push_back(it.path()); + } + + static const std::vector<std::string> expected = { + StringPrintf("%s/%s", to.path, Basename(from_1.path).c_str()), + StringPrintf("%s/subdir", to.path), + StringPrintf("%s/subdir/%s", to.path, Basename(from_2.path).c_str()), + }; + ASSERT_THAT(content, UnorderedElementsAreArray(expected)); +} + +TEST(ApexdUtilTest, MoveDirFromIsNotDirectory) { + TemporaryFile from; + TemporaryDir to; + ASSERT_FALSE(IsOk(MoveDir(from.path, to.path))); +} + +TEST(ApexdUtilTest, MoveDirToIsNotDirectory) { + TemporaryDir from; + TemporaryFile to; + TemporaryFile from_1(from.path); + ASSERT_FALSE(IsOk(MoveDir(from.path, to.path))); +} + +TEST(ApexdUtilTest, MoveDirFromDoesNotExist) { + TemporaryDir to; + ASSERT_FALSE(IsOk(MoveDir("/data/local/tmp/does/not/exist", to.path))); +} + +TEST(ApexdUtilTest, MoveDirToDoesNotExist) { + namespace fs = std::filesystem; + + TemporaryDir from; + TemporaryFile from_1(from.path); + auto from_subdir = StringPrintf("%s/subdir", from.path); + if (mkdir(from_subdir.c_str(), 07000) != 0) { + FAIL() << "Failed to mkdir " << from_subdir << " : " << strerror(errno); + } + TemporaryFile from_2(from_subdir); + + ASSERT_FALSE(IsOk(MoveDir(from.path, "/data/local/tmp/does/not/exist"))); + + // Check that |from| directory is not empty. + std::vector<std::string> content; + for (const auto& it : fs::recursive_directory_iterator(from.path)) { + content.push_back(it.path()); + } + + ASSERT_THAT(content, + UnorderedElementsAre(from_1.path, from_subdir, from_2.path)); +} + +TEST(ApexdUtilTest, FindFilesBySuffix) { + TemporaryDir td; + + // create files with different suffix + const std::string first_filename = StringPrintf("%s/first.a", td.path); + const std::string second_filename = StringPrintf("%s/second.b", td.path); + const std::string third_filename = StringPrintf("%s/third.c", td.path); + const std::string fourth_filename = StringPrintf("%s/fourth.c", td.path); + + std::ofstream first_file(first_filename); + std::ofstream second_file(second_filename); + std::ofstream third_file(third_filename); + std::ofstream fourth_file(fourth_filename); + + auto result = FindFilesBySuffix(td.path, {".b", ".c"}); + ASSERT_TRUE(IsOk(result)); + ASSERT_THAT(*result, UnorderedElementsAre(second_filename, third_filename, + fourth_filename)); +} + +TEST(ApexdTestUtilsTest, MountNamespaceRestorer) { + auto original_namespace = GetCurrentMountNamespace(); + ASSERT_RESULT_OK(original_namespace); + { + MountNamespaceRestorer restorer; + // Switch to new mount namespace. + ASSERT_NE(-1, unshare(CLONE_NEWNS)); + auto current_namespace = GetCurrentMountNamespace(); + ASSERT_RESULT_OK(current_namespace); + ASSERT_NE(original_namespace, current_namespace); + } + // Check that we switched back to the original namespace upon exiting the + // scope. + auto current_namespace = GetCurrentMountNamespace(); + ASSERT_RESULT_OK(current_namespace); + ASSERT_EQ(*original_namespace, *current_namespace); +} + +TEST(ApexdTestUtilsTest, SetUpApexTestEnvironment) { + auto original_apex_mounts = GetApexMounts(); + ASSERT_GT(original_apex_mounts.size(), 0u); + auto original_dir_content = ReadDir("/apex", [](auto _) { return true; }); + ASSERT_TRUE(IsOk(original_dir_content)); + { + MountNamespaceRestorer restorer; + ASSERT_TRUE(IsOk(SetUpApexTestEnvironment())); + // Check /apex is apex_mnt_dir. + char* context; + ASSERT_GT(getfilecon("/apex", &context), 0); + EXPECT_EQ(std::string(context), "u:object_r:apex_mnt_dir:s0"); + freecon(context); + // Check no apexes are mounted in our test environment. + auto new_apex_mounts = GetApexMounts(); + ASSERT_EQ(new_apex_mounts.size(), 0u); + // Check that /apex is empty. + auto dir_content = ReadDir("/apex", [](auto _) { return true; }); + ASSERT_TRUE(IsOk(dir_content)); + ASSERT_EQ(dir_content->size(), 0u) + << "Found following entries: " << Join(*dir_content, ','); + // Check that we can still access /data. + std::string test_dir = android::base::GetExecutableDirectory(); + ASSERT_TRUE(android::base::StartsWith(test_dir, "/data")); + TemporaryFile tf(test_dir); + // Check that we can write. + ASSERT_TRUE(android::base::WriteStringToFile("secret", tf.path)); + // And check that we can still read it + std::string content; + ASSERT_TRUE(android::base::ReadFileToString(tf.path, &content)); + ASSERT_EQ(content, "secret"); + } + auto apex_mounts = GetApexMounts(); + ASSERT_THAT(apex_mounts, UnorderedElementsAreArray(original_apex_mounts)); + auto apex_dir_content = ReadDir("/apex", [](auto _) { return true; }); + ASSERT_TRUE(IsOk(apex_dir_content)); + ASSERT_EQ(apex_dir_content->size(), original_dir_content->size()); +} + +} // namespace +} // namespace apex +} // namespace android diff --git a/apexd/apexd_verity.cpp b/apexd/apexd_verity.cpp index a4be3363..0008f6d2 100644 --- a/apexd/apexd_verity.cpp +++ b/apexd/apexd_verity.cpp @@ -29,6 +29,7 @@ #include "apex_file.h" #include "apexd_utils.h" +using android::base::Dirname; using android::base::ErrnoError; using android::base::Error; using android::base::ReadFully; @@ -59,7 +60,8 @@ std::vector<uint8_t> HexToBin(const std::string& hex) { Result<void> GenerateHashTree(const ApexFile& apex, const ApexVerityData& verity_data, const std::string& hashtree_file) { - unique_fd fd(TEMP_FAILURE_RETRY(open(apex.GetPath().c_str(), O_RDONLY))); + unique_fd fd( + TEMP_FAILURE_RETRY(open(apex.GetPath().c_str(), O_RDONLY | O_CLOEXEC))); if (fd.get() == -1) { return ErrnoError() << "Failed to open " << apex.GetPath(); } @@ -78,7 +80,10 @@ Result<void> GenerateHashTree(const ApexFile& apex, return Error() << "Invalid image size " << image_size; } - if (lseek(fd, apex.GetImageOffset(), SEEK_SET) == -1) { + if (!apex.GetImageOffset()) { + return Error() << "Cannot generate HashTree without image offset"; + } + if (lseek(fd, apex.GetImageOffset().value(), SEEK_SET) == -1) { return ErrnoError() << "Failed to seek"; } @@ -105,8 +110,8 @@ Result<void> GenerateHashTree(const ApexFile& apex, return Error() << "Failed to build hashtree: root digest mismatch"; } - unique_fd out_fd(TEMP_FAILURE_RETRY( - open(hashtree_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600))); + unique_fd out_fd(TEMP_FAILURE_RETRY(open( + hashtree_file.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600))); if (!builder->WriteHashTreeToFd(out_fd, 0)) { return Error() << "Failed to write hashtree to " << hashtree_file; } @@ -115,7 +120,8 @@ Result<void> GenerateHashTree(const ApexFile& apex, Result<std::string> CalculateRootDigest(const std::string& hashtree_file, const ApexVerityData& verity_data) { - unique_fd fd(TEMP_FAILURE_RETRY(open(hashtree_file.c_str(), O_RDONLY))); + unique_fd fd( + TEMP_FAILURE_RETRY(open(hashtree_file.c_str(), O_RDONLY | O_CLOEXEC))); if (fd.get() == -1) { return ErrnoError() << "Failed to open " << hashtree_file; } @@ -149,7 +155,11 @@ Result<std::string> CalculateRootDigest(const std::string& hashtree_file, Result<PrepareHashTreeResult> PrepareHashTree( const ApexFile& apex, const ApexVerityData& verity_data, const std::string& hashtree_file) { - if (auto st = createDirIfNeeded(kApexHashTreeDir, 0700); !st.ok()) { + if (apex.IsCompressed()) { + return Error() << "Cannot prepare HashTree of compressed APEX"; + } + + if (auto st = CreateDirIfNeeded(Dirname(hashtree_file), 0700); !st.ok()) { return st.error(); } bool should_regenerate_hashtree = false; diff --git a/apexd/apexd_verity_test.cpp b/apexd/apexd_verity_test.cpp index cf2317c8..4e53ccbf 100644 --- a/apexd/apexd_verity_test.cpp +++ b/apexd/apexd_verity_test.cpp @@ -25,7 +25,6 @@ #include <gtest/gtest.h> #include "apex_file.h" -#include "apex_preinstalled_data.h" #include "apexd_test_utils.h" #include "apexd_verity.h" @@ -45,12 +44,11 @@ static std::string GetTestFile(const std::string& name) { } TEST(ApexdVerityTest, ReusesHashtree) { - ASSERT_TRUE(IsOk(collectPreinstalledData({"/system_ext/apex"}))); TemporaryDir td; auto apex = ApexFile::Open(GetTestFile("apex.apexd_test_no_hashtree.apex")); ASSERT_TRUE(IsOk(apex)); - auto verity_data = apex->VerifyApexVerity(); + auto verity_data = apex->VerifyApexVerity(apex->GetBundledPublicKey()); ASSERT_TRUE(IsOk(verity_data)); auto hashtree_file = StringPrintf("%s/hashtree", td.path); @@ -78,12 +76,11 @@ TEST(ApexdVerityTest, ReusesHashtree) { } TEST(ApexdVerityTest, RegenerateHashree) { - ASSERT_TRUE(IsOk(collectPreinstalledData({"/system_ext/apex"}))); TemporaryDir td; auto apex = ApexFile::Open(GetTestFile("apex.apexd_test_no_hashtree.apex")); ASSERT_TRUE(IsOk(apex)); - auto verity_data = apex->VerifyApexVerity(); + auto verity_data = apex->VerifyApexVerity(apex->GetBundledPublicKey()); ASSERT_TRUE(IsOk(verity_data)); auto hashtree_file = StringPrintf("%s/hashtree", td.path); @@ -98,7 +95,7 @@ TEST(ApexdVerityTest, RegenerateHashree) { auto apex2 = ApexFile::Open(GetTestFile("apex.apexd_test_no_hashtree_2.apex")); ASSERT_TRUE(IsOk(apex2)); - auto verity_data2 = apex2->VerifyApexVerity(); + auto verity_data2 = apex2->VerifyApexVerity(apex2->GetBundledPublicKey()); ASSERT_TRUE(IsOk(verity_data2)); // Now call PrepareHashTree again. Since digest doesn't match, hashtree @@ -115,5 +112,20 @@ TEST(ApexdVerityTest, RegenerateHashree) { ASSERT_NE(first_hashtree, second_hashtree) << hashtree_file << " was reused"; } +TEST(ApexdVerityTest, CannotPrepareHashTreeForCompressedApex) { + TemporaryDir td; + + auto apex = + ApexFile::Open(GetTestFile("com.android.apex.compressed.v1.capex")); + ASSERT_TRUE(IsOk(apex)); + std::string hash_tree; + ApexVerityData verity_data; + auto result = PrepareHashTree(*apex, verity_data, hash_tree); + ASSERT_FALSE(IsOk(result)); + ASSERT_THAT( + result.error().message(), + ::testing::HasSubstr("Cannot prepare HashTree of compressed APEX")); +} + } // namespace apex } // namespace android diff --git a/apexd/apexservice.cpp b/apexd/apexservice.cpp index b8aced60..4f3d2fc3 100644 --- a/apexd/apexservice.cpp +++ b/apexd/apexservice.cpp @@ -36,13 +36,14 @@ #include <utils/String16.h> #include "apex_file.h" -#include "apex_preinstalled_data.h" +#include "apex_file_repository.h" #include "apexd.h" #include "apexd_session.h" #include "string_log.h" #include <android/apex/BnApexService.h> +using android::base::Join; using android::base::Result; namespace android { @@ -78,10 +79,10 @@ class ApexService : public BnApexService { BinderStatus getSessions(std::vector<ApexSessionInfo>* aidl_return) override; BinderStatus getStagedSessionInfo( int session_id, ApexSessionInfo* apex_session_info) override; - BinderStatus activatePackage(const std::string& packagePath) override; - BinderStatus deactivatePackage(const std::string& packagePath) override; + BinderStatus activatePackage(const std::string& package_path) override; + BinderStatus deactivatePackage(const std::string& package_path) override; BinderStatus getActivePackages(std::vector<ApexInfo>* aidl_return) override; - BinderStatus getActivePackage(const std::string& packageName, + BinderStatus getActivePackage(const std::string& package_name, ApexInfo* aidl_return) override; BinderStatus getAllPackages(std::vector<ApexInfo>* aidl_return) override; BinderStatus preinstallPackages( @@ -92,14 +93,26 @@ class ApexService : public BnApexService { BinderStatus revertActiveSessions() override; BinderStatus resumeRevertIfNeeded() override; BinderStatus snapshotCeData(int user_id, int rollback_id, - const std::string& apex_name, - int64_t* _aidl_return) override; + const std::string& apex_name) override; BinderStatus restoreCeData(int user_id, int rollback_id, const std::string& apex_name) override; BinderStatus destroyDeSnapshots(int rollback_id) override; + BinderStatus destroyCeSnapshots(int user_id, int rollback_id) override; BinderStatus destroyCeSnapshotsNotSpecified( int user_id, const std::vector<int>& retain_rollback_ids) override; BinderStatus remountPackages() override; + BinderStatus recollectPreinstalledData( + const std::vector<std::string>& paths) override; + BinderStatus recollectDataApex(const std::string& path, + const std::string& decompression_dir) override; + BinderStatus markBootCompleted() override; + BinderStatus calculateSizeForCompressedApex( + const CompressedApexInfoList& compressed_apex_info_list, + int64_t* required_size) override; + BinderStatus reserveSpaceForCompressedApex( + const CompressedApexInfoList& compressed_apex_info_list) override; + BinderStatus installAndActivatePackage(const std::string& package_path, + ApexInfo* aidl_return) override; status_t dump(int fd, const Vector<String16>& args) override; @@ -120,39 +133,37 @@ BinderStatus CheckDebuggable(const std::string& name) { } BinderStatus ApexService::stagePackages(const std::vector<std::string>& paths) { - BinderStatus debugCheck = CheckDebuggable("stagePackages"); - if (!debugCheck.isOk()) { - return debugCheck; + BinderStatus debug_check = CheckDebuggable("stagePackages"); + if (!debug_check.isOk()) { + return debug_check; } LOG(DEBUG) << "stagePackages() received by ApexService, paths " << android::base::Join(paths, ','); - Result<void> res = ::android::apex::stagePackages(paths); + Result<void> res = ::android::apex::StagePackages(paths); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. LOG(ERROR) << "Failed to stage " << android::base::Join(paths, ',') << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } BinderStatus ApexService::unstagePackages( const std::vector<std::string>& paths) { - Result<void> res = ::android::apex::unstagePackages(paths); + Result<void> res = ::android::apex::UnstagePackages(paths); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. LOG(ERROR) << "Failed to unstage " << android::base::Join(paths, ',') << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } @@ -162,7 +173,7 @@ BinderStatus ApexService::submitStagedSession(const ApexSessionParams& params, << params.sessionId << " child sessions: [" << android::base::Join(params.childSessionIds, ',') << "]"; - Result<std::vector<ApexFile>> packages = ::android::apex::submitStagedSession( + Result<std::vector<ApexFile>> packages = ::android::apex::SubmitStagedSession( params.sessionId, params.childSessionIds, params.hasRollbackEnabled, params.isRollback, params.rollbackId); if (!packages.ok()) { @@ -186,7 +197,7 @@ BinderStatus ApexService::submitStagedSession(const ApexSessionParams& params, BinderStatus ApexService::markStagedSessionReady(int session_id) { LOG(DEBUG) << "markStagedSessionReady() received by ApexService, session id " << session_id; - Result<void> success = ::android::apex::markStagedSessionReady(session_id); + Result<void> success = ::android::apex::MarkStagedSessionReady(session_id); if (!success.ok()) { LOG(ERROR) << "Failed to mark session id " << session_id << " as ready: " << success.error(); @@ -201,7 +212,7 @@ BinderStatus ApexService::markStagedSessionSuccessful(int session_id) { LOG(DEBUG) << "markStagedSessionSuccessful() received by ApexService, session id " << session_id; - Result<void> ret = ::android::apex::markStagedSessionSuccessful(session_id); + Result<void> ret = ::android::apex::MarkStagedSessionSuccessful(session_id); if (!ret.ok()) { LOG(ERROR) << "Failed to mark session " << session_id << " as SUCCESS: " << ret.error(); @@ -212,6 +223,43 @@ BinderStatus ApexService::markStagedSessionSuccessful(int session_id) { return BinderStatus::ok(); } +BinderStatus ApexService::markBootCompleted() { + ::android::apex::OnBootCompleted(); + return BinderStatus::ok(); +} + +BinderStatus ApexService::calculateSizeForCompressedApex( + const CompressedApexInfoList& compressed_apex_info_list, + int64_t* required_size) { + *required_size = 0; + const auto& instance = ApexFileRepository::GetInstance(); + for (const auto& apex_info : compressed_apex_info_list.apexInfos) { + auto should_allocate_space = ShouldAllocateSpaceForDecompression( + apex_info.moduleName, apex_info.versionCode, instance); + if (!should_allocate_space.ok() || *should_allocate_space) { + *required_size += apex_info.decompressedSize; + } + } + return BinderStatus::ok(); +} + +BinderStatus ApexService::reserveSpaceForCompressedApex( + const CompressedApexInfoList& compressed_apex_info_list) { + int64_t required_size; + if (auto res = calculateSizeForCompressedApex(compressed_apex_info_list, + &required_size); + !res.isOk()) { + return res; + } + if (auto res = ReserveSpaceForCompressedApex(required_size, kOtaReservedDir); + !res.ok()) { + return BinderStatus::fromExceptionCode( + BinderStatus::EX_SERVICE_SPECIFIC, + String8(res.error().message().c_str())); + } + return BinderStatus::ok(); +} + static void ClearSessionInfo(ApexSessionInfo* session_info) { session_info->sessionId = -1; session_info->isUnknown = false; @@ -225,13 +273,14 @@ static void ClearSessionInfo(ApexSessionInfo* session_info) { session_info->isRevertFailed = false; } -void convertToApexSessionInfo(const ApexSession& session, +void ConvertToApexSessionInfo(const ApexSession& session, ApexSessionInfo* session_info) { using SessionState = ::apex::proto::SessionState; ClearSessionInfo(session_info); session_info->sessionId = session.GetId(); session_info->crashingNativeProcess = session.GetCrashingNativeProcess(); + session_info->errorMessage = session.GetErrorMessage(); switch (session.GetState()) { case SessionState::VERIFIED: @@ -265,23 +314,24 @@ void convertToApexSessionInfo(const ApexSession& session, } } -static ApexInfo getApexInfo(const ApexFile& package) { +static ApexInfo GetApexInfo(const ApexFile& package) { + auto& instance = ApexFileRepository::GetInstance(); ApexInfo out; out.moduleName = package.GetManifest().name(); out.modulePath = package.GetPath(); out.versionCode = package.GetManifest().version(); out.versionName = package.GetManifest().versionname(); - out.isFactory = package.IsBuiltin(); + out.isFactory = instance.IsPreInstalledApex(package); out.isActive = false; - Result<std::string> preinstalledPath = - getApexPreinstalledPath(package.GetManifest().name()); - if (preinstalledPath.ok()) { - out.preinstalledModulePath = *preinstalledPath; + Result<std::string> preinstalled_path = + instance.GetPreinstalledPath(package.GetManifest().name()); + if (preinstalled_path.ok()) { + out.preinstalledModulePath = *preinstalled_path; } return out; } -static std::string toString(const ApexInfo& package) { +static std::string ToString(const ApexInfo& package) { std::string msg = StringLog() << "Module: " << package.moduleName << " Version: " << package.versionCode @@ -297,9 +347,9 @@ BinderStatus ApexService::getSessions( std::vector<ApexSessionInfo>* aidl_return) { auto sessions = ApexSession::GetSessions(); for (const auto& session : sessions) { - ApexSessionInfo sessionInfo; - convertToApexSessionInfo(session, &sessionInfo); - aidl_return->push_back(sessionInfo); + ApexSessionInfo session_info; + ConvertToApexSessionInfo(session, &session_info); + aidl_return->push_back(session_info); } return BinderStatus::ok(); @@ -317,82 +367,80 @@ BinderStatus ApexService::getStagedSessionInfo( return BinderStatus::ok(); } - convertToApexSessionInfo(*session, apex_session_info); + ConvertToApexSessionInfo(*session, apex_session_info); return BinderStatus::ok(); } -BinderStatus ApexService::activatePackage(const std::string& packagePath) { - BinderStatus debugCheck = CheckDebuggable("activatePackage"); - if (!debugCheck.isOk()) { - return debugCheck; +BinderStatus ApexService::activatePackage(const std::string& package_path) { + BinderStatus debug_check = CheckDebuggable("activatePackage"); + if (!debug_check.isOk()) { + return debug_check; } LOG(DEBUG) << "activatePackage() received by ApexService, path " - << packagePath; + << package_path; - Result<void> res = ::android::apex::activatePackage(packagePath); + Result<void> res = ::android::apex::ActivatePackage(package_path); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. - LOG(ERROR) << "Failed to activate " << packagePath << ": " << res.error(); + LOG(ERROR) << "Failed to activate " << package_path << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } -BinderStatus ApexService::deactivatePackage(const std::string& packagePath) { - BinderStatus debugCheck = CheckDebuggable("deactivatePackage"); - if (!debugCheck.isOk()) { - return debugCheck; +BinderStatus ApexService::deactivatePackage(const std::string& package_path) { + BinderStatus debug_check = CheckDebuggable("deactivatePackage"); + if (!debug_check.isOk()) { + return debug_check; } LOG(DEBUG) << "deactivatePackage() received by ApexService, path " - << packagePath; + << package_path; - Result<void> res = ::android::apex::deactivatePackage(packagePath); + Result<void> res = ::android::apex::DeactivatePackage(package_path); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. - LOG(ERROR) << "Failed to deactivate " << packagePath << ": " << res.error(); + LOG(ERROR) << "Failed to deactivate " << package_path << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } BinderStatus ApexService::getActivePackages( std::vector<ApexInfo>* aidl_return) { - auto packages = ::android::apex::getActivePackages(); + auto packages = ::android::apex::GetActivePackages(); for (const auto& package : packages) { - ApexInfo apexInfo = getApexInfo(package); - apexInfo.isActive = true; - aidl_return->push_back(std::move(apexInfo)); + ApexInfo apex_info = GetApexInfo(package); + apex_info.isActive = true; + aidl_return->push_back(std::move(apex_info)); } return BinderStatus::ok(); } -BinderStatus ApexService::getActivePackage(const std::string& packageName, +BinderStatus ApexService::getActivePackage(const std::string& package_name, ApexInfo* aidl_return) { - Result<ApexFile> apex = ::android::apex::getActivePackage(packageName); + Result<ApexFile> apex = ::android::apex::GetActivePackage(package_name); if (apex.ok()) { - *aidl_return = getApexInfo(*apex); + *aidl_return = GetApexInfo(*apex); aidl_return->isActive = true; } return BinderStatus::ok(); } BinderStatus ApexService::getAllPackages(std::vector<ApexInfo>* aidl_return) { - const auto& active = ::android::apex::getActivePackages(); - const auto& factory = ::android::apex::getFactoryPackages(); + const auto& active = ::android::apex::GetActivePackages(); + const auto& factory = ::android::apex::GetFactoryPackages(); for (const ApexFile& pkg : active) { - ApexInfo apex_info = getApexInfo(pkg); + ApexInfo apex_info = GetApexInfo(pkg); apex_info.isActive = true; aidl_return->push_back(std::move(apex_info)); } @@ -401,55 +449,70 @@ BinderStatus ApexService::getAllPackages(std::vector<ApexInfo>* aidl_return) { return o.GetPath() == pkg.GetPath(); }; if (std::find_if(active.begin(), active.end(), same_path) == active.end()) { - aidl_return->push_back(getApexInfo(pkg)); + aidl_return->push_back(GetApexInfo(pkg)); } } return BinderStatus::ok(); } +BinderStatus ApexService::installAndActivatePackage( + const std::string& package_path, ApexInfo* aidl_return) { + LOG(DEBUG) << "installAndActivatePackage() received by ApexService, path: " + << package_path; + auto res = InstallPackage(package_path); + if (!res.ok()) { + LOG(ERROR) << "Failed to install package " << package_path << " : " + << res.error(); + return BinderStatus::fromExceptionCode( + BinderStatus::EX_SERVICE_SPECIFIC, + String8(res.error().message().c_str())); + } + *aidl_return = GetApexInfo(*res); + aidl_return->isActive = true; + return BinderStatus::ok(); +} + BinderStatus ApexService::preinstallPackages( const std::vector<std::string>& paths) { - BinderStatus debugCheck = CheckDebuggable("preinstallPackages"); - if (!debugCheck.isOk()) { - return debugCheck; + BinderStatus debug_check = CheckDebuggable("preinstallPackages"); + if (!debug_check.isOk()) { + return debug_check; } - Result<void> res = ::android::apex::preinstallPackages(paths); + Result<void> res = ::android::apex::PreinstallPackages(paths); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. LOG(ERROR) << "Failed to preinstall packages " << android::base::Join(paths, ',') << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } BinderStatus ApexService::postinstallPackages( const std::vector<std::string>& paths) { - BinderStatus debugCheck = CheckDebuggable("postinstallPackages"); - if (!debugCheck.isOk()) { - return debugCheck; + BinderStatus debug_check = CheckDebuggable("postinstallPackages"); + if (!debug_check.isOk()) { + return debug_check; } - Result<void> res = ::android::apex::postinstallPackages(paths); + Result<void> res = ::android::apex::PostinstallPackages(paths); if (res.ok()) { return BinderStatus::ok(); } - // TODO: Get correct binder error status. LOG(ERROR) << "Failed to postinstall packages " << android::base::Join(paths, ',') << ": " << res.error(); return BinderStatus::fromExceptionCode( - BinderStatus::EX_ILLEGAL_ARGUMENT, + BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } BinderStatus ApexService::abortStagedSession(int session_id) { LOG(DEBUG) << "abortStagedSession() received by ApexService."; - Result<void> res = ::android::apex::abortStagedSession(session_id); + Result<void> res = ::android::apex::AbortStagedSession(session_id); if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_ILLEGAL_ARGUMENT, @@ -460,7 +523,7 @@ BinderStatus ApexService::abortStagedSession(int session_id) { BinderStatus ApexService::revertActiveSessions() { LOG(DEBUG) << "revertActiveSessions() received by ApexService."; - Result<void> res = ::android::apex::revertActiveSessions(""); + Result<void> res = ::android::apex::RevertActiveSessions("", ""); if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_ILLEGAL_ARGUMENT, @@ -470,13 +533,13 @@ BinderStatus ApexService::revertActiveSessions() { } BinderStatus ApexService::resumeRevertIfNeeded() { - BinderStatus debugCheck = CheckDebuggable("resumeRevertIfNeeded"); - if (!debugCheck.isOk()) { - return debugCheck; + BinderStatus debug_check = CheckDebuggable("resumeRevertIfNeeded"); + if (!debug_check.isOk()) { + return debug_check; } LOG(DEBUG) << "resumeRevertIfNeeded() received by ApexService."; - Result<void> res = ::android::apex::resumeRevertIfNeeded(); + Result<void> res = ::android::apex::ResumeRevertIfNeeded(); if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_ILLEGAL_ARGUMENT, @@ -486,17 +549,15 @@ BinderStatus ApexService::resumeRevertIfNeeded() { } BinderStatus ApexService::snapshotCeData(int user_id, int rollback_id, - const std::string& apex_name, - int64_t* _aidl_return) { + const std::string& apex_name) { LOG(DEBUG) << "snapshotCeData() received by ApexService."; - Result<ino_t> res = - ::android::apex::snapshotCeData(user_id, rollback_id, apex_name); + Result<void> res = + ::android::apex::SnapshotCeData(user_id, rollback_id, apex_name); if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); } - *_aidl_return = static_cast<uint64_t>(*res); return BinderStatus::ok(); } @@ -504,7 +565,7 @@ BinderStatus ApexService::restoreCeData(int user_id, int rollback_id, const std::string& apex_name) { LOG(DEBUG) << "restoreCeData() received by ApexService."; Result<void> res = - ::android::apex::restoreCeData(user_id, rollback_id, apex_name); + ::android::apex::RestoreCeData(user_id, rollback_id, apex_name); if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_SERVICE_SPECIFIC, @@ -515,8 +576,19 @@ BinderStatus ApexService::restoreCeData(int user_id, int rollback_id, BinderStatus ApexService::destroyDeSnapshots(int rollback_id) { LOG(DEBUG) << "destroyDeSnapshots() received by ApexService."; - Result<void> res = ::android::apex::destroyDeSnapshots(rollback_id); - if (!res) { + Result<void> res = ::android::apex::DestroyDeSnapshots(rollback_id); + if (!res.ok()) { + return BinderStatus::fromExceptionCode( + BinderStatus::EX_SERVICE_SPECIFIC, + String8(res.error().message().c_str())); + } + return BinderStatus::ok(); +} + +BinderStatus ApexService::destroyCeSnapshots(int user_id, int rollback_id) { + LOG(DEBUG) << "destroyCeSnapshots() received by ApexService."; + Result<void> res = ::android::apex::DestroyCeSnapshots(user_id, rollback_id); + if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); @@ -527,9 +599,9 @@ BinderStatus ApexService::destroyDeSnapshots(int rollback_id) { BinderStatus ApexService::destroyCeSnapshotsNotSpecified( int user_id, const std::vector<int>& retain_rollback_ids) { LOG(DEBUG) << "destroyCeSnapshotsNotSpecified() received by ApexService."; - Result<void> res = ::android::apex::destroyCeSnapshotsNotSpecified( + Result<void> res = ::android::apex::DestroyCeSnapshotsNotSpecified( user_id, retain_rollback_ids); - if (!res) { + if (!res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); @@ -545,7 +617,47 @@ BinderStatus ApexService::remountPackages() { if (auto root = CheckCallerIsRoot("remountPackages"); !root.isOk()) { return root; } - if (auto res = ::android::apex::remountPackages(); !res.ok()) { + if (auto res = ::android::apex::RemountPackages(); !res.ok()) { + return BinderStatus::fromExceptionCode( + BinderStatus::EX_SERVICE_SPECIFIC, + String8(res.error().message().c_str())); + } + return BinderStatus::ok(); +} + +BinderStatus ApexService::recollectPreinstalledData( + const std::vector<std::string>& paths) { + LOG(DEBUG) << "recollectPreinstalledData() received by ApexService, paths: " + << Join(paths, ','); + if (auto debug = CheckDebuggable("recollectPreinstalledData"); + !debug.isOk()) { + return debug; + } + if (auto root = CheckCallerIsRoot("recollectPreinstalledData"); + !root.isOk()) { + return root; + } + ApexFileRepository& instance = ApexFileRepository::GetInstance(); + if (auto res = instance.AddPreInstalledApex(paths); !res.ok()) { + return BinderStatus::fromExceptionCode( + BinderStatus::EX_SERVICE_SPECIFIC, + String8(res.error().message().c_str())); + } + return BinderStatus::ok(); +} + +BinderStatus ApexService::recollectDataApex( + const std::string& path, const std::string& decompression_dir) { + LOG(DEBUG) << "recollectDataApex() received by ApexService, paths " << path + << " and " << decompression_dir; + if (auto debug = CheckDebuggable("recollectDataApex"); !debug.isOk()) { + return debug; + } + if (auto root = CheckCallerIsRoot("recollectDataApex"); !root.isOk()) { + return root; + } + ApexFileRepository& instance = ApexFileRepository::GetInstance(); + if (auto res = instance.AddDataApex(path); !res.ok()) { return BinderStatus::fromExceptionCode( BinderStatus::EX_SERVICE_SPECIFIC, String8(res.error().message().c_str())); @@ -565,16 +677,18 @@ status_t ApexService::onTransact(uint32_t _aidl_code, const Parcel& _aidl_data, for (int i = 0; i < argc && _aidl_data.dataAvail() > 0; i++) { args.add(_aidl_data.readString16()); } - sp<IBinder> unusedCallback; - sp<IResultReceiver> resultReceiver; + sp<IBinder> unused_callback; + sp<IResultReceiver> result_receiver; status_t status; - if ((status = _aidl_data.readNullableStrongBinder(&unusedCallback)) != OK) + if ((status = _aidl_data.readNullableStrongBinder(&unused_callback)) != + OK) return status; - if ((status = _aidl_data.readNullableStrongBinder(&resultReceiver)) != OK) + if ((status = _aidl_data.readNullableStrongBinder(&result_receiver)) != + OK) return status; status = shellCommand(in, out, err, args); - if (resultReceiver != nullptr) { - resultReceiver->send(status); + if (result_receiver != nullptr) { + result_receiver->send(status); } return OK; } @@ -593,7 +707,7 @@ status_t ApexService::dump(int fd, const Vector<String16>& /*args*/) { return BAD_VALUE; } else { for (const auto& item : list) { - std::string msg = toString(item); + std::string msg = ToString(item); dprintf(fd, "%s", msg.c_str()); } } @@ -611,14 +725,19 @@ status_t ApexService::dump(int fd, const Vector<String16>& /*args*/) { } } std::string revert_reason = ""; - std::string crashing_native_process = session.GetCrashingNativeProcess(); + const auto& crashing_native_process = session.GetCrashingNativeProcess(); if (!crashing_native_process.empty()) { revert_reason = " Revert Reason: " + crashing_native_process; } + std::string error_message_dump = ""; + const auto& error_message = session.GetErrorMessage(); + if (!error_message.empty()) { + error_message_dump = " Error Message: " + error_message; + } std::string msg = StringLog() << "Session ID: " << session.GetId() << child_ids_str << " State: " << SessionState_State_Name(session.GetState()) - << revert_reason << std::endl; + << revert_reason << error_message_dump << std::endl; dprintf(fd, "%s", msg.c_str()); } @@ -637,25 +756,25 @@ status_t ApexService::shellCommand(int in, int out, int err, } log << "ApexService:" << std::endl << " help - display this help" << std::endl - << " stagePackages [packagePath1] ([packagePath2]...) - stage " + << " stagePackages [package_path1] ([package_path2]...) - stage " "multiple packages from the given path" << std::endl - << " getActivePackage [packageName] - return info for active package " + << " getActivePackage [package_name] - return info for active package " "with given name, if present" << std::endl << " getAllPackages - return the list of all packages" << std::endl << " getActivePackages - return the list of active packages" << std::endl - << " activatePackage [packagePath] - activate package from the " + << " activatePackage [package_path] - activate package from the " "given path" << std::endl - << " deactivatePackage [packagePath] - deactivate package from the " + << " deactivatePackage [package_path] - deactivate package from the " "given path" << std::endl - << " preinstallPackages [packagePath1] ([packagePath2]...) - run " + << " preinstallPackages [package_path1] ([package_path2]...) - run " "pre-install hooks of the given packages" << std::endl - << " postinstallPackages [packagePath1] ([packagePath2]...) - run " + << " postinstallPackages [package_path1] ([package_path2]...) - run " "post-install hooks of the given packages" << std::endl << " getStagedSessionInfo [sessionId] - displays information about a " @@ -687,7 +806,7 @@ status_t ApexService::shellCommand(int in, int out, int err, if (cmd == String16("stagePackages")) { if (args.size() < 2) { - print_help(err, "stagePackages requires at least one packagePath"); + print_help(err, "stagePackages requires at least one package_path"); return BAD_VALUE; } std::vector<std::string> pkgs; @@ -713,7 +832,7 @@ status_t ApexService::shellCommand(int in, int out, int err, BinderStatus status = getAllPackages(&list); if (status.isOk()) { for (const auto& item : list) { - std::string msg = toString(item); + std::string msg = ToString(item); dprintf(out, "%s", msg.c_str()); } return OK; @@ -733,7 +852,7 @@ status_t ApexService::shellCommand(int in, int out, int err, BinderStatus status = getActivePackages(&list); if (status.isOk()) { for (const auto& item : list) { - std::string msg = toString(item); + std::string msg = ToString(item); dprintf(out, "%s", msg.c_str()); } return OK; @@ -753,7 +872,7 @@ status_t ApexService::shellCommand(int in, int out, int err, ApexInfo package; BinderStatus status = getActivePackage(String8(args[1]).string(), &package); if (status.isOk()) { - std::string msg = toString(package); + std::string msg = ToString(package); dprintf(out, "%s", msg.c_str()); return OK; } @@ -768,7 +887,7 @@ status_t ApexService::shellCommand(int in, int out, int err, if (cmd == String16("activatePackage")) { if (args.size() != 2) { - print_help(err, "activatePackage requires one packagePath"); + print_help(err, "activatePackage requires one package_path"); return BAD_VALUE; } BinderStatus status = activatePackage(String8(args[1]).string()); @@ -783,7 +902,7 @@ status_t ApexService::shellCommand(int in, int out, int err, if (cmd == String16("deactivatePackage")) { if (args.size() != 2) { - print_help(err, "deactivatePackage requires one packagePath"); + print_help(err, "deactivatePackage requires one package_path"); return BAD_VALUE; } BinderStatus status = deactivatePackage(String8(args[1]).string()); @@ -856,7 +975,7 @@ status_t ApexService::shellCommand(int in, int out, int err, BinderStatus status = submitStagedSession(params, &list); if (status.isOk()) { for (const auto& item : list.apexInfos) { - std::string msg = toString(item); + std::string msg = ToString(item); dprintf(out, "%s", msg.c_str()); } return OK; @@ -872,7 +991,7 @@ status_t ApexService::shellCommand(int in, int out, int err, if (args.size() < 2) { print_help(err, "preinstallPackages/postinstallPackages requires at least" - " one packagePath"); + " one package_path"); return BAD_VALUE; } std::vector<std::string> pkgs; @@ -929,15 +1048,15 @@ void CreateAndRegisterService() { sp<ProcessState> ps(ProcessState::self()); // Create binder service and register with LazyServiceRegistrar - sp<ApexService> apexService = new ApexService(); - auto lazyRegistrar = LazyServiceRegistrar::getInstance(); - lazyRegistrar.forcePersist(true); - lazyRegistrar.registerService(apexService, kApexServiceName); + sp<ApexService> apex_service = sp<ApexService>::make(); + auto lazy_registrar = LazyServiceRegistrar::getInstance(); + lazy_registrar.forcePersist(true); + lazy_registrar.registerService(apex_service, kApexServiceName); } void AllowServiceShutdown() { - auto lazyRegistrar = LazyServiceRegistrar::getInstance(); - lazyRegistrar.forcePersist(false); + auto lazy_registrar = LazyServiceRegistrar::getInstance(); + lazy_registrar.forcePersist(false); } void StartThreadPool() { diff --git a/apexd/apexservice_test.cpp b/apexd/apexservice_test.cpp index 4c054f10..d0d4633e 100644 --- a/apexd/apexservice_test.cpp +++ b/apexd/apexservice_test.cpp @@ -59,8 +59,8 @@ #include "apexd_session.h" #include "apexd_test_utils.h" #include "apexd_utils.h" - #include "session_state.pb.h" +#include "string_log.h" using apex::proto::SessionState; @@ -73,6 +73,7 @@ using android::apex::testing::ApexInfoEq; using android::apex::testing::CreateSessionInfo; using android::apex::testing::IsOk; using android::apex::testing::SessionInfoEq; +using android::base::ErrnoError; using android::base::Join; using android::base::ReadFully; using android::base::StartsWith; @@ -82,6 +83,7 @@ using android::dm::DeviceMapper; using android::fs_mgr::Fstab; using android::fs_mgr::GetEntryForMountPoint; using android::fs_mgr::ReadFstabFromFile; +using ::apex::proto::ApexManifest; using ::testing::Contains; using ::testing::EndsWith; using ::testing::HasSubstr; @@ -123,6 +125,7 @@ class ApexServiceTest : public ::testing::Test { vold_service_->supportsCheckpoint(&supports_fs_checkpointing_); ASSERT_TRUE(IsOk(status)); CleanUp(); + service_->recollectPreinstalledData(kApexPackageBuiltinDirs); } void TearDown() override { CleanUp(); } @@ -296,9 +299,8 @@ class ApexServiceTest : public ::testing::Test { "-f", file, }; - std::string error_msg; - int res = ForkAndRun(args, &error_msg); - CHECK_EQ(0, res) << error_msg; + auto res = ForkAndRun(args); + CHECK(res.ok()) << res.error(); std::string data; CHECK(android::base::ReadFileToString(file, &data)); @@ -447,21 +449,10 @@ class ApexServiceTest : public ::testing::Test { private: void CleanUp() { - auto status = WalkDir(kApexDataDir, [](const fs::directory_entry& p) { - std::error_code ec; - fs::file_status status = p.status(ec); - ASSERT_FALSE(ec) << "Failed to stat " << p.path() << " : " - << ec.message(); - if (fs::is_directory(status)) { - fs::remove_all(p.path(), ec); - } else { - fs::remove(p.path(), ec); - } - ASSERT_FALSE(ec) << "Failed to delete " << p.path() << " : " - << ec.message(); - }); - fs::remove_all(kApexSessionsDir); - ASSERT_TRUE(IsOk(status)); + DeleteDirContent(kActiveApexPackagesDataDir); + DeleteDirContent(kApexBackupDir); + DeleteDirContent(kApexHashTreeDir); + DeleteDirContent(ApexSession::GetSessionsDir()); DeleteIfExists("/data/misc_ce/0/apexdata/apex.apexd_test"); DeleteIfExists("/data/misc_ce/0/apexrollback/123456"); @@ -526,7 +517,8 @@ Result<void> ReadDevice(const std::string& block_device) { static constexpr size_t kBufSize = 1024 * kBlockSize; std::vector<uint8_t> buffer(kBufSize); - unique_fd fd(TEMP_FAILURE_RETRY(open(block_device.c_str(), O_RDONLY))); + unique_fd fd( + TEMP_FAILURE_RETRY(open(block_device.c_str(), O_RDONLY | O_CLOEXEC))); if (fd.get() == -1) { return ErrnoError() << "Can't open " << block_device; } @@ -642,7 +634,7 @@ TEST_F(ApexServiceTest, StageFailKey) { // May contain one of two errors. std::string error = st.exceptionMessage().c_str(); - ASSERT_THAT(error, HasSubstr("No preinstalled data found for package " + ASSERT_THAT(error, HasSubstr("No preinstalled apex found for package " "com.android.apex.test_package.no_inst_key")); } @@ -718,7 +710,7 @@ TEST_F(ApexServiceTest, SubmitStagedSessionFailDoesNotLeakTempVerityDevices) { } } -TEST_F(ApexServiceTest, StageSuccess_ClearsPreviouslyActivePackage) { +TEST_F(ApexServiceTest, StageSuccessClearsPreviouslyActivePackage) { PrepareTestApexForInstall installer1(GetTestFile("apex.apexd_test_v2.apex")); PrepareTestApexForInstall installer2( GetTestFile("apex.apexd_test_different_app.apex")); @@ -782,7 +774,6 @@ TEST_F(ApexServiceTest, MultiStageSuccess) { } ASSERT_EQ(std::string("com.android.apex.test_package"), installer.package); - // TODO: Add second test. Right now, just use a separate version. PrepareTestApexForInstall installer2(GetTestFile("apex.apexd_test_v2.apex")); if (!installer2.Prepare()) { return; @@ -840,18 +831,10 @@ TEST_F(ApexServiceTest, SnapshotCeData) { ASSERT_TRUE( RegularFileExists("/data/misc_ce/0/apexdata/apex.apexd_test/hello.txt")); - int64_t result; - service_->snapshotCeData(0, 123456, "apex.apexd_test", &result); + service_->snapshotCeData(0, 123456, "apex.apexd_test"); ASSERT_TRUE(RegularFileExists( "/data/misc_ce/0/apexrollback/123456/apex.apexd_test/hello.txt")); - - // Check that the return value is the inode of the snapshot directory. - struct stat buf; - memset(&buf, 0, sizeof(buf)); - ASSERT_EQ(0, - stat("/data/misc_ce/0/apexrollback/123456/apex.apexd_test", &buf)); - ASSERT_EQ(int64_t(buf.st_ino), result); } TEST_F(ApexServiceTest, RestoreCeData) { @@ -878,7 +861,7 @@ TEST_F(ApexServiceTest, RestoreCeData) { DirExists("/data/misc_ce/0/apexrollback/123456/apex.apexd_test")); } -TEST_F(ApexServiceTest, DestroyDeSnapshots_DeSys) { +TEST_F(ApexServiceTest, DestroyDeSnapshotsDeSys) { CreateDir("/data/misc/apexrollback/123456"); CreateDir("/data/misc/apexrollback/123456/my.apex"); CreateFile("/data/misc/apexrollback/123456/my.apex/hello.txt"); @@ -896,7 +879,7 @@ TEST_F(ApexServiceTest, DestroyDeSnapshots_DeSys) { ASSERT_FALSE(DirExists("/data/misc/apexrollback/123456")); } -TEST_F(ApexServiceTest, DestroyDeSnapshots_DeUser) { +TEST_F(ApexServiceTest, DestroyDeSnapshotsDeUser) { CreateDir("/data/misc_de/0/apexrollback/123456"); CreateDir("/data/misc_de/0/apexrollback/123456/my.apex"); CreateFile("/data/misc_de/0/apexrollback/123456/my.apex/hello.txt"); @@ -914,6 +897,31 @@ TEST_F(ApexServiceTest, DestroyDeSnapshots_DeUser) { ASSERT_FALSE(DirExists("/data/misc_de/0/apexrollback/123456")); } +TEST_F(ApexServiceTest, DestroyCeSnapshots) { + CreateDir("/data/misc_ce/0/apexrollback/123456"); + CreateDir("/data/misc_ce/0/apexrollback/123456/apex.apexd_test"); + CreateFile("/data/misc_ce/0/apexrollback/123456/apex.apexd_test/file.txt"); + + CreateDir("/data/misc_ce/0/apexrollback/77777"); + CreateDir("/data/misc_ce/0/apexrollback/77777/apex.apexd_test"); + CreateFile("/data/misc_ce/0/apexrollback/77777/apex.apexd_test/thing.txt"); + + ASSERT_TRUE(RegularFileExists( + "/data/misc_ce/0/apexrollback/123456/apex.apexd_test/file.txt")); + ASSERT_TRUE(RegularFileExists( + "/data/misc_ce/0/apexrollback/77777/apex.apexd_test/thing.txt")); + + android::binder::Status st = service_->destroyCeSnapshots(0, 123456); + ASSERT_TRUE(IsOk(st)); + // Should be OK if the directory doesn't exist. + st = service_->destroyCeSnapshots(1, 123456); + ASSERT_TRUE(IsOk(st)); + + ASSERT_TRUE(RegularFileExists( + "/data/misc_ce/0/apexrollback/77777/apex.apexd_test/thing.txt")); + ASSERT_FALSE(DirExists("/data/misc_ce/0/apexrollback/123456")); +} + TEST_F(ApexServiceTest, DestroyCeSnapshotsNotSpecified) { CreateDir("/data/misc_ce/0/apexrollback/123456"); CreateDir("/data/misc_ce/0/apexrollback/123456/apex.apexd_test"); @@ -1131,7 +1139,8 @@ TEST_F(ApexServiceActivationSuccessTest, ShowsUpInMountedApexDatabase) { << GetDebugStr(installer_.get()); MountedApexDatabase db; - db.PopulateFromMounts(); + db.PopulateFromMounts(kActiveApexPackagesDataDir, kApexDecompressedDir, + kApexHashTreeDir); std::optional<MountedApexData> mounted_apex; db.ForallMountedApexes(installer_->package, @@ -1257,7 +1266,8 @@ TEST_F(ApexServiceNoHashtreeApexActivationTest, ShowsUpInMountedApexDatabase) { << GetDebugStr(installer_.get()); MountedApexDatabase db; - db.PopulateFromMounts(); + db.PopulateFromMounts(kActiveApexPackagesDataDir, kApexDecompressedDir, + kApexHashTreeDir); std::optional<MountedApexData> mounted_apex; db.ForallMountedApexes(installer_->package, @@ -1327,7 +1337,7 @@ TEST_F(ApexServiceTest, NoHashtreeApexStagePackagesMovesHashtree) { auto read_fn = [](const std::string& path) -> std::vector<uint8_t> { static constexpr size_t kBufSize = 4096; std::vector<uint8_t> buffer(kBufSize); - unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY))); + unique_fd fd(TEMP_FAILURE_RETRY(open(path.c_str(), O_RDONLY | O_CLOEXEC))); if (fd.get() == -1) { PLOG(ERROR) << "Failed to open " << path; ADD_FAILURE(); @@ -1379,53 +1389,78 @@ TEST_F(ApexServiceTest, NoHashtreeApexStagePackagesMovesHashtree) { } TEST_F(ApexServiceTest, GetFactoryPackages) { - Result<std::vector<ApexInfo>> factoryPackages = GetFactoryPackages(); - ASSERT_TRUE(IsOk(factoryPackages)); - ASSERT_TRUE(factoryPackages->size() > 0); + Result<std::vector<ApexInfo>> factory_packages = GetFactoryPackages(); + ASSERT_TRUE(IsOk(factory_packages)); + ASSERT_TRUE(factory_packages->size() > 0); + + std::vector<std::string> builtin_dirs; + for (const auto& d : kApexPackageBuiltinDirs) { + std::string realpath; + if (android::base::Realpath(d, &realpath)) { + builtin_dirs.push_back(realpath); + } + // realpath might fail in case when dir is a non-existing path. We can + // ignore non-existing paths. + } + + // Decompressed APEX is also considred factory package + builtin_dirs.push_back(kApexDecompressedDir); - for (const ApexInfo& package : *factoryPackages) { - ASSERT_TRUE(isPathForBuiltinApexes(package.modulePath)); + for (const ApexInfo& package : *factory_packages) { + bool is_builtin = false; + for (const auto& dir : builtin_dirs) { + if (StartsWith(package.modulePath, dir)) { + is_builtin = true; + } + } + ASSERT_TRUE(is_builtin); } } TEST_F(ApexServiceTest, NoPackagesAreBothActiveAndInactive) { - Result<std::vector<ApexInfo>> activePackages = GetActivePackages(); - ASSERT_TRUE(IsOk(activePackages)); - ASSERT_TRUE(activePackages->size() > 0); - Result<std::vector<ApexInfo>> inactivePackages = GetInactivePackages(); - ASSERT_TRUE(IsOk(inactivePackages)); - std::vector<std::string> activePackagesStrings = - GetPackagesStrings(*activePackages); - std::vector<std::string> inactivePackagesStrings = - GetPackagesStrings(*inactivePackages); - std::sort(activePackagesStrings.begin(), activePackagesStrings.end()); - std::sort(inactivePackagesStrings.begin(), inactivePackagesStrings.end()); + Result<std::vector<ApexInfo>> active_packages = GetActivePackages(); + ASSERT_TRUE(IsOk(active_packages)); + ASSERT_TRUE(active_packages->size() > 0); + Result<std::vector<ApexInfo>> inactive_packages = GetInactivePackages(); + ASSERT_TRUE(IsOk(inactive_packages)); + std::vector<std::string> active_packages_strings = + GetPackagesStrings(*active_packages); + std::vector<std::string> inactive_packages_strings = + GetPackagesStrings(*inactive_packages); + std::sort(active_packages_strings.begin(), active_packages_strings.end()); + std::sort(inactive_packages_strings.begin(), inactive_packages_strings.end()); std::vector<std::string> intersection; std::set_intersection( - activePackagesStrings.begin(), activePackagesStrings.end(), - inactivePackagesStrings.begin(), inactivePackagesStrings.end(), + active_packages_strings.begin(), active_packages_strings.end(), + inactive_packages_strings.begin(), inactive_packages_strings.end(), std::back_inserter(intersection)); ASSERT_THAT(intersection, SizeIs(0)); } TEST_F(ApexServiceTest, GetAllPackages) { - Result<std::vector<ApexInfo>> allPackages = GetAllPackages(); - ASSERT_TRUE(IsOk(allPackages)); - ASSERT_TRUE(allPackages->size() > 0); - Result<std::vector<ApexInfo>> activePackages = GetActivePackages(); - std::vector<std::string> activeStrings = GetPackagesStrings(*activePackages); - Result<std::vector<ApexInfo>> factoryPackages = GetFactoryPackages(); - std::vector<std::string> factoryStrings = - GetPackagesStrings(*factoryPackages); - for (ApexInfo& apexInfo : *allPackages) { - std::string packageString = GetPackageString(apexInfo); - bool shouldBeActive = std::find(activeStrings.begin(), activeStrings.end(), - packageString) != activeStrings.end(); - bool shouldBeFactory = - std::find(factoryStrings.begin(), factoryStrings.end(), - packageString) != factoryStrings.end(); - ASSERT_EQ(shouldBeActive, apexInfo.isActive); - ASSERT_EQ(shouldBeFactory, apexInfo.isFactory); + Result<std::vector<ApexInfo>> all_packages = GetAllPackages(); + ASSERT_TRUE(IsOk(all_packages)); + ASSERT_TRUE(all_packages->size() > 0); + Result<std::vector<ApexInfo>> active_packages = GetActivePackages(); + std::vector<std::string> active_strings = + GetPackagesStrings(*active_packages); + Result<std::vector<ApexInfo>> factory_packages = GetFactoryPackages(); + std::vector<std::string> factory_strings = + GetPackagesStrings(*factory_packages); + for (ApexInfo& apexInfo : *all_packages) { + std::string package_string = GetPackageString(apexInfo); + bool should_be_active = + std::find(active_strings.begin(), active_strings.end(), + package_string) != active_strings.end(); + bool should_be_factory = + std::find(factory_strings.begin(), factory_strings.end(), + package_string) != factory_strings.end(); + ASSERT_EQ(should_be_active, apexInfo.isActive) + << package_string << " should " << (should_be_active ? "" : "not ") + << "be active"; + ASSERT_EQ(should_be_factory, apexInfo.isFactory) + << package_string << " should " << (should_be_factory ? "" : "not ") + << "be factory"; } } @@ -1983,6 +2018,42 @@ TEST_F(ApexServiceTest, AbortStagedSessionActivatedFail) { SessionInfoEq(expected2))); } +// Only finalized sessions should be deleted on DeleteFinalizedSessions() +TEST_F(ApexServiceTest, DeleteFinalizedSessions) { + // Fetch list of all session state + std::vector<SessionState::State> states; + for (int i = SessionState::State_MIN; i < SessionState::State_MAX; i++) { + if (!SessionState::State_IsValid(i)) { + continue; + } + states.push_back(SessionState::State(i)); + } + + // For every session state, create a new session. This is to verify we only + // delete sessions in final state. + auto nonFinalSessions = 0u; + for (auto i = 0u; i < states.size(); i++) { + auto session = ApexSession::CreateSession(230 + i); + SessionState::State state = states[i]; + ASSERT_TRUE(IsOk(session->UpdateStateAndCommit(state))); + if (!session->IsFinalized()) { + nonFinalSessions++; + } + } + std::vector<ApexSession> sessions = ApexSession::GetSessions(); + ASSERT_EQ(states.size(), sessions.size()); + + // Now try cleaning up all finalized sessions + ApexSession::DeleteFinalizedSessions(); + sessions = ApexSession::GetSessions(); + ASSERT_EQ(nonFinalSessions, sessions.size()); + + // Verify only finalized sessions have been deleted + for (auto& session : sessions) { + ASSERT_FALSE(session.IsFinalized()); + } +} + TEST_F(ApexServiceTest, BackupActivePackages) { if (supports_fs_checkpointing_) { GTEST_SKIP() << "Can't run if filesystem checkpointing is enabled"; @@ -2039,7 +2110,7 @@ TEST_F(ApexServiceTest, BackupActivePackagesClearsPreviousBackup) { } // Make sure /data/apex/backups exists. - ASSERT_TRUE(IsOk(createDirIfNeeded(std::string(kApexBackupDir), 0700))); + ASSERT_TRUE(IsOk(CreateDirIfNeeded(std::string(kApexBackupDir), 0700))); // Create some bogus files in /data/apex/backups. std::ofstream old_backup(StringPrintf("%s/file1", kApexBackupDir)); ASSERT_TRUE(old_backup.good()); @@ -2083,7 +2154,7 @@ TEST_F(ApexServiceTest, BackupActivePackagesZeroActivePackages) { // Make sure that /data/apex/active exists and is empty ASSERT_TRUE( - IsOk(createDirIfNeeded(std::string(kActiveApexPackagesDataDir), 0755))); + IsOk(CreateDirIfNeeded(std::string(kActiveApexPackagesDataDir), 0755))); auto active_pkgs = ReadEntireDir(kActiveApexPackagesDataDir); ASSERT_TRUE(IsOk(active_pkgs)); ASSERT_EQ(0u, active_pkgs->size()); @@ -2098,7 +2169,7 @@ TEST_F(ApexServiceTest, BackupActivePackagesZeroActivePackages) { ASSERT_EQ(0u, backups->size()); } -TEST_F(ApexServiceTest, ActivePackagesFolderDoesNotExist) { +TEST_F(ApexServiceTest, ActivePackagesDirEmpty) { PrepareTestApexForInstall installer(GetTestFile("apex.apexd_test_v2.apex"), "/data/app-staging/session_41", "staging_data_file"); @@ -2107,10 +2178,8 @@ TEST_F(ApexServiceTest, ActivePackagesFolderDoesNotExist) { return; } - // Make sure that /data/apex/active does not exist - std::error_code ec; - fs::remove_all(fs::path(kActiveApexPackagesDataDir), ec); - ASSERT_FALSE(ec) << "Failed to delete " << kActiveApexPackagesDataDir; + // Make sure that /data/apex/active is empty + DeleteDirContent(kActiveApexPackagesDataDir); ApexInfoList list; ApexSessionParams params; @@ -2167,12 +2236,24 @@ TEST_F(ApexServiceTest, UnstagePackagesFail) { UnorderedElementsAre(installer1.test_installed_file)); } +TEST_F(ApexServiceTest, UnstagePackagesFailPreInstalledApex) { + auto status = service_->unstagePackages( + {"/system/apex/com.android.apex.cts.shim.apex"}); + ASSERT_FALSE(IsOk(status)); + const std::string& error_message = + std::string(status.exceptionMessage().c_str()); + ASSERT_THAT(error_message, + HasSubstr("Can't uninstall pre-installed apex " + "/system/apex/com.android.apex.cts.shim.apex")); + ASSERT_TRUE(RegularFileExists("/system/apex/com.android.apex.cts.shim.apex")); +} + class ApexServiceRevertTest : public ApexServiceTest { protected: void SetUp() override { ApexServiceTest::SetUp(); } void PrepareBackup(const std::vector<std::string>& pkgs) { - ASSERT_TRUE(IsOk(createDirIfNeeded(std::string(kApexBackupDir), 0700))); + ASSERT_TRUE(IsOk(CreateDirIfNeeded(std::string(kApexBackupDir), 0700))); for (const auto& pkg : pkgs) { PrepareTestApexForInstall installer(pkg); ASSERT_TRUE(installer.Prepare()) << " failed to prepare " << pkg; @@ -2383,7 +2464,10 @@ TEST_F(ApexServiceRevertTest, RevertStoresCrashingNativeProcess) { // Make sure /data/apex/active is non-empty. ASSERT_TRUE(IsOk(service_->stagePackages({installer.test_file}))); std::string native_process = "test_process"; - Result<void> res = ::android::apex::revertActiveSessions(native_process); + // TODO(ioffe): this is calling into internals of apexd which makes test quite + // britle. With some refactoring we should be able to call binder api, or + // make this a unit test of apexd.cpp. + Result<void> res = ::android::apex::RevertActiveSessions(native_process, ""); session = ApexSession::GetSession(1543); ASSERT_EQ(session->GetCrashingNativeProcess(), native_process); } @@ -2571,21 +2655,6 @@ TEST_F(ApexShimUpdateTest, UpdateToV2Success) { ASSERT_TRUE(IsOk(service_->stagePackages({installer.test_file}))); } -TEST_F(ApexShimUpdateTest, UpdateToV2FailureWrongSHA512) { - PrepareTestApexForInstall installer( - GetTestFile("com.android.apex.cts.shim.v2_wrong_sha.apex")); - - if (!installer.Prepare()) { - FAIL() << GetDebugStr(&installer); - } - - const auto& status = service_->stagePackages({installer.test_file}); - ASSERT_FALSE(IsOk(status)); - const std::string& error_message = - std::string(status.exceptionMessage().c_str()); - ASSERT_THAT(error_message, HasSubstr("has unexpected SHA512 hash")); -} - TEST_F(ApexShimUpdateTest, SubmitStagedSessionFailureHasPreInstallHook) { PrepareTestApexForInstall installer( GetTestFile("com.android.apex.cts.shim.v2_with_pre_install_hook.apex"), @@ -2684,7 +2753,7 @@ TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFails) { ASSERT_FALSE(IsOk(service_->submitStagedSession(params, &list))); } -TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFails_b146895998) { +TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFailsB146895998) { PrepareTestApexForInstall installer(GetTestFile("corrupted_b146895998.apex"), "/data/app-staging/session_71", "staging_data_file"); @@ -2699,7 +2768,7 @@ TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFails_b146895998) { ASSERT_FALSE(IsOk(service_->submitStagedSession(params, &list))); } -TEST_F(ApexServiceTest, StageCorruptApexFails_b146895998) { +TEST_F(ApexServiceTest, StageCorruptApexFailsB146895998) { PrepareTestApexForInstall installer(GetTestFile("corrupted_b146895998.apex")); if (!installer.Prepare()) { @@ -2756,8 +2825,10 @@ TEST_F(ApexServiceTest, RemountPackagesPackageOnSystemChanged) { auto active_apex = GetActivePackage("com.android.apex.test_package"); ASSERT_RESULT_OK(active_apex); ASSERT_EQ(2u, active_apex->versionCode); - // Sanity check that module path didn't change. - ASSERT_EQ(kSystemPath, active_apex->modulePath); + // Check that module path didn't change, modulo symlink. + std::string realSystemPath; + ASSERT_TRUE(android::base::Realpath(kSystemPath, &realSystemPath)); + ASSERT_EQ(realSystemPath, active_apex->modulePath); } TEST_F(ApexServiceActivationSuccessTest, RemountPackagesPackageOnDataChanged) { @@ -2783,6 +2854,25 @@ TEST_F(ApexServiceActivationSuccessTest, RemountPackagesPackageOnDataChanged) { ASSERT_EQ(installer_->test_installed_file, active_apex->modulePath); } +TEST_F(ApexServiceTest, + SubmitStagedSessionFailsManifestMismatchCleansUpHashtree) { + PrepareTestApexForInstall installer( + GetTestFile("apex.apexd_test_no_hashtree_manifest_mismatch.apex"), + "/data/app-staging/session_83", "staging_data_file"); + if (!installer.Prepare()) { + return; + } + + ApexInfoList list; + ApexSessionParams params; + params.sessionId = 83; + ASSERT_FALSE(IsOk(service_->submitStagedSession(params, &list))); + std::string hashtree_file = std::string(kApexHashTreeDir) + "/" + + installer.package + "@" + + std::to_string(installer.version) + ".new"; + ASSERT_FALSE(RegularFileExists(hashtree_file)); +} + class LogTestToLogcat : public ::testing::EmptyTestEventListener { void OnTestStart(const ::testing::TestInfo& test_info) override { #ifdef __ANDROID__ @@ -2791,7 +2881,7 @@ class LogTestToLogcat : public ::testing::EmptyTestEventListener { using base::StringPrintf; base::LogdLogger l; std::string msg = - StringPrintf("=== %s::%s (%s:%d)", test_info.test_case_name(), + 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()); @@ -2838,11 +2928,164 @@ TEST_F(ApexServiceActivationNoCode, NoCodeApexIsNotExecutable) { EXPECT_TRUE(found_apex_mountpoint); } +struct BannedNameProvider { + static std::string GetTestName() { return "sharedlibs.apex"; } + static std::string GetPackageName() { return "sharedlibs"; } +}; + +class ApexServiceActivationBannedName + : public ApexServiceActivationTest<BannedNameProvider> { + public: + ApexServiceActivationBannedName() : ApexServiceActivationTest(false) {} +}; + +TEST_F(ApexServiceActivationBannedName, ApexWithBannedNameCannotBeActivated) { + ASSERT_FALSE( + IsOk(service_->activatePackage(installer_->test_installed_file))); +} + +namespace { +void PrepareCompressedTestApex(const std::string& input_apex, + const std::string& builtin_dir, + const std::string& decompressed_dir, + const std::string& active_apex_dir) { + const Result<ApexFile>& apex_file = ApexFile::Open(input_apex); + ASSERT_TRUE(apex_file.ok()); + ASSERT_TRUE(apex_file->IsCompressed()) << "Not a compressed APEX"; + + auto prebuilt_file_path = + builtin_dir + "/" + android::base::Basename(input_apex); + fs::copy(input_apex, prebuilt_file_path); + + const ApexManifest& manifest = apex_file->GetManifest(); + const std::string& package = manifest.name(); + const int64_t& version = manifest.version(); + + auto decompressed_file_path = decompressed_dir + "/" + package + "@" + + std::to_string(version) + ".apex"; + auto result = apex_file->Decompress(decompressed_file_path); + ASSERT_TRUE(result.ok()) << "Failed to decompress " << result.error(); + auto active_apex_file_path = + active_apex_dir + "/" + package + "@" + std::to_string(version) + ".apex"; + auto error = + link(decompressed_file_path.c_str(), active_apex_file_path.c_str()); + ASSERT_EQ(error, 0) << "Failed to hardlink decompressed APEX"; +} + +CompressedApexInfo CreateCompressedApex(const std::string& name, + const int version, const int size) { + CompressedApexInfo result; + result.moduleName = name; + result.versionCode = version; + result.decompressedSize = size; + return result; +} +} // namespace + +class ApexServiceTestForCompressedApex : public ApexServiceTest { + public: + static constexpr const char* kTempPrebuiltDir = "/data/apex/temp_prebuilt"; + + void SetUp() override { + ApexServiceTest::SetUp(); + ASSERT_NE(nullptr, service_.get()); + + TemporaryDir decompression_dir, active_apex_dir; + if (0 != mkdir(kTempPrebuiltDir, 0777)) { + int saved_errno = errno; + ASSERT_EQ(saved_errno, EEXIST) + << kTempPrebuiltDir << ":" << strerror(saved_errno); + } + PrepareCompressedTestApex( + GetTestFile("com.android.apex.compressed.v1.capex"), kTempPrebuiltDir, + kApexDecompressedDir, kActiveApexPackagesDataDir); + service_->recollectPreinstalledData({kTempPrebuiltDir}); + service_->recollectDataApex(kActiveApexPackagesDataDir, + kApexDecompressedDir); + } + + void TearDown() override { + ApexServiceTest::TearDown(); + DeleteDirContent(kTempPrebuiltDir); + rmdir(kTempPrebuiltDir); + DeleteDirContent(kApexDecompressedDir); + DeleteDirContent(kActiveApexPackagesDataDir); + } +}; + +TEST_F(ApexServiceTestForCompressedApex, CalculateSizeForCompressedApex) { + int64_t result; + // Empty list of compressed apex info + { + CompressedApexInfoList empty_list; + ASSERT_TRUE( + IsOk(service_->calculateSizeForCompressedApex(empty_list, &result))); + ASSERT_EQ(result, 0ll); + } + + // Multiple compressed APEX should get summed + { + CompressedApexInfoList non_empty_list; + CompressedApexInfo new_apex = CreateCompressedApex("new_apex", 1, 1); + CompressedApexInfo new_apex_2 = CreateCompressedApex("new_apex_2", 1, 2); + CompressedApexInfo compressed_apex_same_version = + CreateCompressedApex("com.android.apex.compressed", 1, 4); + CompressedApexInfo compressed_apex_higher_version = + CreateCompressedApex("com.android.apex.compressed", 2, 8); + non_empty_list.apexInfos.push_back(new_apex); + non_empty_list.apexInfos.push_back(new_apex_2); + non_empty_list.apexInfos.push_back(compressed_apex_same_version); + non_empty_list.apexInfos.push_back(compressed_apex_higher_version); + ASSERT_TRUE(IsOk( + service_->calculateSizeForCompressedApex(non_empty_list, &result))); + ASSERT_EQ(result, 11ll); // 1+2+8. compressed_apex_same_version is ignored + } +} + +TEST_F(ApexServiceTestForCompressedApex, ReserveSpaceForCompressedApex) { + // Multiple compressed APEX should reserve equal to + // CalculateSizeForCompressedApex + { + CompressedApexInfoList non_empty_list; + CompressedApexInfo new_apex = CreateCompressedApex("new_apex", 1, 1); + CompressedApexInfo new_apex_2 = CreateCompressedApex("new_apex_2", 1, 2); + CompressedApexInfo compressed_apex_same_version = + CreateCompressedApex("com.android.apex.compressed", 1, 4); + CompressedApexInfo compressed_apex_higher_version = + CreateCompressedApex("com.android.apex.compressed", 2, 8); + non_empty_list.apexInfos.push_back(new_apex); + non_empty_list.apexInfos.push_back(new_apex_2); + non_empty_list.apexInfos.push_back(compressed_apex_same_version); + non_empty_list.apexInfos.push_back(compressed_apex_higher_version); + int64_t required_size; + ASSERT_TRUE(IsOk(service_->calculateSizeForCompressedApex(non_empty_list, + &required_size))); + ASSERT_EQ(required_size, + 11ll); // 1+2+8. compressed_apex_same_version is ignored + + ASSERT_TRUE(IsOk(service_->reserveSpaceForCompressedApex(non_empty_list))); + auto files = ReadDir(kOtaReservedDir, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 1u); + EXPECT_EQ((int64_t)fs::file_size((*files)[0]), required_size); + } + + // Sending empty list should delete reserved file + { + CompressedApexInfoList empty_list; + ASSERT_TRUE(IsOk(service_->reserveSpaceForCompressedApex(empty_list))); + auto files = ReadDir(kOtaReservedDir, [](auto _) { return true; }); + ASSERT_TRUE(IsOk(files)); + ASSERT_EQ(files->size(), 0u); + } +} + } // namespace apex } // namespace android int main(int argc, char** argv) { android::base::InitLogging(argv, &android::base::StderrLogger); + android::base::SetMinimumLogSeverity(android::base::VERBOSE); ::testing::InitGoogleTest(&argc, argv); ::testing::UnitTest::GetInstance()->listeners().Append( new android::apex::LogTestToLogcat()); diff --git a/apexd/sysprop/Android.bp b/apexd/sysprop/Android.bp index 7e3ae611..0b81efd9 100644 --- a/apexd/sysprop/Android.bp +++ b/apexd/sysprop/Android.bp @@ -1,3 +1,7 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + sysprop_library { name: "com.android.sysprop.apex", srcs: ["ApexProperties.sysprop"], diff --git a/apexd/sysprop/ApexProperties.sysprop b/apexd/sysprop/ApexProperties.sysprop index bd768303..8baf2730 100644 --- a/apexd/sysprop/ApexProperties.sysprop +++ b/apexd/sysprop/ApexProperties.sysprop @@ -22,3 +22,19 @@ prop { access: Readonly prop_name: "ro.apex.updatable" } + +prop { + api_name: "dm_delete_timeout" + type: UInt + scope: Internal + access: Readonly + prop_name: "apexd.config.dm_delete.timeout" +} + +prop { + api_name: "dm_create_timeout" + type: UInt + scope: Internal + access: Readonly + prop_name: "apexd.config.dm_create.timeout" +} diff --git a/apexd/sysprop/api/com.android.sysprop.apex-current.txt b/apexd/sysprop/api/com.android.sysprop.apex-current.txt index 17983127..e69de29b 100644 --- a/apexd/sysprop/api/com.android.sysprop.apex-current.txt +++ b/apexd/sysprop/api/com.android.sysprop.apex-current.txt @@ -1,8 +0,0 @@ -props { - module: "android.sysprop.ApexProperties" - prop { - api_name: "updatable" - scope: Internal - prop_name: "ro.apex.updatable" - } -} diff --git a/apexer/Android.bp b/apexer/Android.bp index a33aade4..29be7d1c 100644 --- a/apexer/Android.bp +++ b/apexer/Android.bp @@ -12,22 +12,32 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + apexer_tools = [ "apexer", "aapt2", "avbtool", "e2fsdroid", - "merge_zips", "mke2fs", "resize2fs", "sefcontext_compile", - "soong_zip", "zipalign", + "make_f2fs", + "sload_f2fs", // TODO(b/124476339) apex doesn't follow 'required' dependencies so we need to include this // manually for 'avbtool'. "fec", ] +// TODO(b/157110982): cannot specify "required" dependency on go binary +apexer_go_tools = [ + "merge_zips", + "soong_zip", +] + python_library_host { name: "apex_manifest", srcs: [ @@ -51,6 +61,10 @@ python_binary_host { srcs: [ "apexer.py", ], + // TODO(b/157625953) mke2fs.conf can't embedded directly. + data: [ + ":mke2fs_conf", + ], version: { py2: { enabled: true, @@ -103,7 +117,8 @@ apex_test { manifest: "etc/manifest.json", ignore_system_library_special_case: true, key: "com.android.support.apexer.key", - binaries: apexer_tools, + binaries: apexer_tools + apexer_go_tools, + updatable: false, } // TODO(b/148659029): this test can't run in TEST_MAPPING. @@ -118,6 +133,7 @@ python_test_host { ":com.android.example-legacy.apex", ":com.android.example-logging_parent.apex", ":com.android.example-overridden_package_name.apex", + ":apexer_test_host_tools", "testdata/com.android.example.apex.avbpubkey", "testdata/com.android.example.apex.pem", "testdata/com.android.example.apex.pk8", @@ -135,8 +151,59 @@ python_test_host { libs: [ "apex_manifest", ], - required: [ - "apexer", - "deapexer" +} + +apexer_deps_minus_go_tools = apexer_tools + [ + "deapexer", + "debugfs_static", +] + +apexer_deps_tools = apexer_deps_minus_go_tools + apexer_go_tools + +genrule_defaults { + name: "apexer_test_host_tools_list", + tools: apexer_deps_tools + [ + "signapk", + ], +} + +genrule { + name: "apexer_test_host_tools", + srcs: [ + ":current_android_jar", + ], + out: ["apexer_test_host_tools.zip"], + tools: apexer_deps_tools + [ + // To force signapk.jar generated in out/soong/host + "signapk", ], + cmd: "HOST_OUT_BIN=$$(dirname $(location apexer)) && " + + "HOST_SOONG_OUT=$$(dirname $$HOST_OUT_BIN) && " + + "SIGNAPK_JAR=$$(find $${HOST_SOONG_OUT}/framework -name \"signapk*\") && " + + "LIBCPLUSPLUS=$$(find $${HOST_SOONG_OUT}/lib64 -name \"libc++.*\") && " + + "LIBCONSCRYPT_OPENJDK_JNI=$$(find $${HOST_SOONG_OUT}/lib64 -name \"libconscrypt_openjdk_jni.*\") && " + + "BASE=$(genDir)/binary_files && " + + "BIN=$$BASE/bin && " + + "LIB=$$BASE/lib64 && " + + "mkdir -p $$BIN && " + + "mkdir -p $$LIB && " + + "cp $(in) $$BIN && " + + "cp $(location apexer) $$BIN && " + + "cp $(location deapexer) $$BIN && " + + "cp $(location avbtool) $$BIN && " + + "cp $(location aapt2) $$BIN && " + + "cp $(location e2fsdroid) $$BIN && " + + "cp $(location merge_zips) $$BIN && " + + "cp $(location mke2fs) $$BIN && " + + "cp $(location resize2fs) $$BIN && " + + "cp $(location sefcontext_compile) $$BIN && " + + "cp $(location soong_zip) $$BIN && " + + "cp $(location fec) $$BIN && " + + "cp $(location zipalign) $$BIN && " + + "cp $(location debugfs_static) $$BIN && " + + "cp $$SIGNAPK_JAR $$BIN && " + + "cp $$LIBCPLUSPLUS $$LIB && " + + "cp $$LIBCONSCRYPT_OPENJDK_JNI $$LIB && " + + "$(location soong_zip) -C $$BASE -D $$BASE -o $(out) && " + + "rm -rf $$BASE", } diff --git a/apexer/TEST_MAPPING b/apexer/TEST_MAPPING index cbb6a858..1ad30781 100644 --- a/apexer/TEST_MAPPING +++ b/apexer/TEST_MAPPING @@ -1,4 +1,9 @@ { + "presubmit": [ + { + "name": "apexer_test" + } + ], "imports": [ { "path": "system/apex/tests" diff --git a/apexer/apexer.py b/apexer/apexer.py index f698c8c8..f0c17f43 100644 --- a/apexer/apexer.py +++ b/apexer/apexer.py @@ -23,6 +23,7 @@ import apex_build_info_pb2 import argparse import hashlib import os +import pkgutil import re import shlex import shutil @@ -101,6 +102,13 @@ def ParseArgs(argv): choices=['zip', 'image'], help='type of APEX payload being built "zip" or "image"') parser.add_argument( + '--payload_fs_type', + metavar='FS_TYPE', + required=False, + default='ext4', + choices=['ext4', 'f2fs'], + help='type of filesystem being used for payload image "ext4" or "f2fs"') + parser.add_argument( '--override_apk_package_name', required=False, help='package name of the APK container. Default is the apex name in --manifest.' @@ -162,7 +170,7 @@ def ParseArgs(argv): parser.add_argument( '--unsigned_payload_only', action='store_true', - help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies + help="""Outputs the unsigned payload image/zip only. Also, setting this flag implies --payload_only is set too.""" ) parser.add_argument( @@ -182,7 +190,7 @@ def FindBinaryPath(binary): ':'.join(tool_path_list)) -def RunCommand(cmd, verbose=False, env=None): +def RunCommand(cmd, verbose=False, env=None, expected_return_values={0}): env = env or {} env.update(os.environ.copy()) @@ -194,10 +202,10 @@ def RunCommand(cmd, verbose=False, env=None): cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) output, _ = p.communicate() - if verbose or p.returncode is not 0: + if verbose or p.returncode not in expected_return_values: print(output.rstrip()) - assert p.returncode is 0, 'Failed to execute: ' + ' '.join(cmd) + assert p.returncode in expected_return_values, 'Failed to execute: ' + ' '.join(cmd) return (output, p.returncode) @@ -378,6 +386,9 @@ def GenerateBuildInfo(args): if args.logging_parent: build_info.logging_parent = args.logging_parent + if args.payload_type == 'image': + build_info.payload_fs_type = args.payload_fs_type + return build_info def AddLoggingParent(android_manifest, logging_parent_value): @@ -450,9 +461,8 @@ def CreateApex(args, work_dir): print("Cannot read manifest file: '" + args.manifest + "'") return False - # create an empty ext4 image that is sufficiently big - # sufficiently big = size + 16MB margin - size_in_mb = (GetDirSize(args.input_dir) / (1024 * 1024)) + 16 + # create an empty image that is sufficiently big + size_in_mb = (GetDirSize(args.input_dir) / (1024 * 1024)) content_dir = os.path.join(work_dir, 'content') os.mkdir(content_dir) @@ -473,67 +483,112 @@ def CreateApex(args, work_dir): else: key_name = os.path.basename(os.path.splitext(args.key)[0]) - if manifest_apex.name != key_name: - print("package name '" + manifest_apex.name + - "' does not match with key name '" + key_name + "'") - return False img_file = os.path.join(content_dir, 'apex_payload.img') - # margin is for files that are not under args.input_dir. this consists of - # n inodes for apex_manifest files and 11 reserved inodes for ext4. - # TOBO(b/122991714) eliminate these details. use build_image.py which - # determines the optimal inode count by first building an image and then - # count the inodes actually used. - inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11 - inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin - - cmd = ['mke2fs'] - cmd.extend(['-O', '^has_journal']) # because image is read-only - cmd.extend(['-b', str(BLOCK_SIZE)]) - cmd.extend(['-m', '0']) # reserved block percentage - cmd.extend(['-t', 'ext4']) - cmd.extend(['-I', '256']) # inode size - cmd.extend(['-N', str(inode_num)]) - uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) - cmd.extend(['-U', uu]) - cmd.extend(['-E', 'hash_seed=' + uu]) - cmd.append(img_file) - cmd.append(str(size_in_mb) + 'M') - RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) - - # Compile the file context into the binary form - compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin') - cmd = ['sefcontext_compile'] - cmd.extend(['-o', compiled_file_contexts]) - cmd.append(args.file_contexts) - RunCommand(cmd, args.verbose) + if args.payload_fs_type == 'ext4': + # sufficiently big = size + 16MB margin + size_in_mb += 16 + + # margin is for files that are not under args.input_dir. this consists of + # n inodes for apex_manifest files and 11 reserved inodes for ext4. + # TOBO(b/122991714) eliminate these details. use build_image.py which + # determines the optimal inode count by first building an image and then + # count the inodes actually used. + inode_num_margin = GetFilesAndDirsCount(manifests_dir) + 11 + inode_num = GetFilesAndDirsCount(args.input_dir) + inode_num_margin + + cmd = ['mke2fs'] + cmd.extend(['-O', '^has_journal']) # because image is read-only + cmd.extend(['-b', str(BLOCK_SIZE)]) + cmd.extend(['-m', '0']) # reserved block percentage + cmd.extend(['-t', 'ext4']) + cmd.extend(['-I', '256']) # inode size + cmd.extend(['-N', str(inode_num)]) + uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) + cmd.extend(['-U', uu]) + cmd.extend(['-E', 'hash_seed=' + uu]) + cmd.append(img_file) + cmd.append(str(size_in_mb) + 'M') + with tempfile.NamedTemporaryFile(dir=work_dir, suffix="mke2fs.conf") as conf_file: + conf_data = pkgutil.get_data('apexer', 'mke2fs.conf') + conf_file.write(conf_data) + conf_file.flush() + RunCommand(cmd, args.verbose, + {"MKE2FS_CONFIG": conf_file.name, 'E2FSPROGS_FAKE_TIME': '1'}) + + # Compile the file context into the binary form + compiled_file_contexts = os.path.join(work_dir, 'file_contexts.bin') + cmd = ['sefcontext_compile'] + cmd.extend(['-o', compiled_file_contexts]) + cmd.append(args.file_contexts) + RunCommand(cmd, args.verbose) + + # Add files to the image file + cmd = ['e2fsdroid'] + cmd.append('-e') # input is not android_sparse_file + cmd.extend(['-f', args.input_dir]) + cmd.extend(['-T', '0']) # time is set to epoch + cmd.extend(['-S', compiled_file_contexts]) + cmd.extend(['-C', args.canned_fs_config]) + cmd.append('-s') # share dup blocks + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + cmd = ['e2fsdroid'] + cmd.append('-e') # input is not android_sparse_file + cmd.extend(['-f', manifests_dir]) + cmd.extend(['-T', '0']) # time is set to epoch + cmd.extend(['-S', compiled_file_contexts]) + cmd.extend(['-C', args.canned_fs_config]) + cmd.append('-s') # share dup blocks + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + # Resize the image file to save space + cmd = ['resize2fs'] + cmd.append('-M') # shrink as small as possible + cmd.append(img_file) + RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + + elif args.payload_fs_type == 'f2fs': + # F2FS requires a ~100M minimum size (necessary for ART, could be reduced a bit for other) + # TODO(b/158453869): relax these requirements for readonly devices + size_in_mb += 100 + + # Create an empty image + cmd = ['/usr/bin/fallocate'] + cmd.extend(['-l', str(size_in_mb)+'M']) + cmd.append(img_file) + RunCommand(cmd, args.verbose) + + # Format the image to F2FS + cmd = ['make_f2fs'] + cmd.extend(['-g', 'android']) + uu = str(uuid.uuid5(uuid.NAMESPACE_URL, 'www.android.com')) + cmd.extend(['-U', uu]) + cmd.extend(['-T', '0']) + cmd.append('-r') # sets checkpointing seed to 0 to remove random bits + cmd.append(img_file) + RunCommand(cmd, args.verbose) - # Add files to the image file - cmd = ['e2fsdroid'] - cmd.append('-e') # input is not android_sparse_file - cmd.extend(['-f', args.input_dir]) - cmd.extend(['-T', '0']) # time is set to epoch - cmd.extend(['-S', compiled_file_contexts]) - cmd.extend(['-C', args.canned_fs_config]) - cmd.append('-s') # share dup blocks - cmd.append(img_file) - RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) - - cmd = ['e2fsdroid'] - cmd.append('-e') # input is not android_sparse_file - cmd.extend(['-f', manifests_dir]) - cmd.extend(['-T', '0']) # time is set to epoch - cmd.extend(['-S', compiled_file_contexts]) - cmd.extend(['-C', args.canned_fs_config]) - cmd.append('-s') # share dup blocks - cmd.append(img_file) - RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) - - # Resize the image file to save space - cmd = ['resize2fs'] - cmd.append('-M') # shrink as small as possible - cmd.append(img_file) - RunCommand(cmd, args.verbose, {'E2FSPROGS_FAKE_TIME': '1'}) + # Add files to the image + cmd = ['sload_f2fs'] + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-f', manifests_dir]) + cmd.extend(['-s', args.file_contexts]) + cmd.extend(['-T', '0']) + cmd.append(img_file) + RunCommand(cmd, args.verbose, expected_return_values={0,1}) + + cmd = ['sload_f2fs'] + cmd.extend(['-C', args.canned_fs_config]) + cmd.extend(['-f', args.input_dir]) + cmd.extend(['-s', args.file_contexts]) + cmd.extend(['-T', '0']) + cmd.append(img_file) + RunCommand(cmd, args.verbose, expected_return_values={0,1}) + + # TODO(b/158453869): resize the image file to save space if args.unsigned_payload_only: shutil.copyfile(img_file, args.output) diff --git a/apexer/apexer_test.py b/apexer/apexer_test.py index 176b994c..7fec4ec5 100644 --- a/apexer/apexer_test.py +++ b/apexer/apexer_test.py @@ -21,6 +21,7 @@ import logging import os import re import shutil +import stat import subprocess import tempfile import unittest @@ -161,6 +162,7 @@ DEBUG_TEST = False class ApexerRebuildTest(unittest.TestCase): def setUp(self): self._to_cleanup = [] + self._get_host_tools(os.path.join(get_current_dir(), "apexer_test_host_tools.zip")) def tearDown(self): if not DEBUG_TEST: @@ -173,6 +175,37 @@ class ApexerRebuildTest(unittest.TestCase): else: print(self._to_cleanup) + def _get_host_tools(self, host_tools_file_path): + dir_name = tempfile.mkdtemp(prefix=self._testMethodName+"_host_tools_") + self._to_cleanup.append(dir_name) + if os.path.isfile(host_tools_file_path): + with ZipFile(host_tools_file_path, 'r') as zip_obj: + zip_obj.extractall(path=dir_name) + + files = {} + for i in ["apexer", "deapexer", "avbtool", "mke2fs", "sefcontext_compile", "e2fsdroid", + "resize2fs", "soong_zip", "aapt2", "merge_zips", "zipalign", "debugfs_static", + "signapk.jar", "android.jar"]: + file_path = os.path.join(dir_name, "bin", i) + if os.path.exists(file_path): + os.chmod(file_path, stat.S_IRUSR | stat.S_IXUSR); + files[i] = file_path + else: + files[i] = i + self.host_tools = files + self.host_tools_path = os.path.join(dir_name, "bin") + + path = os.path.join(dir_name, "bin") + if "PATH" in os.environ: + path += ":" + os.environ["PATH"] + os.environ["PATH"] = path + + ld_library_path = os.path.join(dir_name, "lib64") + if "LD_LIBRARY_PATH" in os.environ: + ld_library_path += ":" + os.environ["LD_LIBRARY_PATH"] + if "ANDROID_HOST_OUT" in os.environ: + ld_library_path += ":" + os.path.join(os.environ["ANDROID_HOST_OUT"], "lib64") + os.environ["LD_LIBRARY_PATH"] = ld_library_path def _get_container_files(self, apex_file_path): dir_name = tempfile.mkdtemp(prefix=self._testMethodName+"_container_files_") @@ -216,7 +249,8 @@ class ApexerRebuildTest(unittest.TestCase): def _extract_payload(self, apex_file_path): dir_name = tempfile.mkdtemp(prefix=self._testMethodName+"_extracted_payload_") self._to_cleanup.append(dir_name) - cmd = ["deapexer", "extract", apex_file_path, dir_name] + cmd = ["deapexer", "--debugfs_path", self.host_tools["debugfs_static"], + "extract", apex_file_path, dir_name] run_host_command(cmd) # Remove payload files added by apexer and e2fs tools. @@ -235,9 +269,13 @@ class ApexerRebuildTest(unittest.TestCase): if unsigned_payload_only or "--payload_only" in args: payload_only = True - os.environ["APEXER_TOOL_PATH"] = ( - "out/soong/host/linux-x86/bin:prebuilts/sdk/tools/linux/bin") + os.environ["APEXER_TOOL_PATH"] = (self.host_tools_path + + ":out/soong/host/linux-x86/bin:prebuilts/sdk/tools/linux/bin") cmd = ["apexer", "--force", "--include_build_info", "--do_not_check_keyname"] + if DEBUG_TEST: + cmd.append('-v') + cmd.extend(["--apexer_tool_path", os.environ["APEXER_TOOL_PATH"]]) + cmd.extend(["--android_jar_path", self.host_tools["android.jar"]]) cmd.extend(["--manifest", container_files["apex_manifest.pb"]]) if "apex_manifest.json" in container_files: cmd.extend(["--manifest_json", container_files["apex_manifest.json"]]) @@ -261,14 +299,35 @@ class ApexerRebuildTest(unittest.TestCase): run_host_command(cmd) return fn + def _get_java_toolchain(self): + java_toolchain = "java" + if os.path.isfile("prebuilts/jdk/jdk11/linux-x86/bin/java"): + java_toolchain = "prebuilts/jdk/jdk11/linux-x86/bin/java" + elif "ANDROID_JAVA_TOOLCHAIN" in os.environ: + java_toolchain = os.path.join(os.environ["ANDROID_JAVA_TOOLCHAIN"], "java") + elif "ANDROID_JAVA_HOME" in os.environ: + java_toolchain = os.path.join(os.environ["ANDROID_JAVA_HOME"], "bin", "java") + elif "JAVA_HOME" in os.environ: + java_toolchain = os.path.join(os.environ["JAVA_HOME"], "bin", "java") + + java_dep_lib = os.environ["LD_LIBRARY_PATH"] + if "ANDROID_HOST_OUT" in os.environ: + java_dep_lib += ":" + os.path.join(os.environ["ANDROID_HOST_OUT"], "lib64") + if "ANDROID_BUILD_TOP" in os.environ: + java_dep_lib += ":" + os.path.join(os.environ["ANDROID_BUILD_TOP"], + "out/soong/host/linux-x86/lib64") + + return [java_toolchain, java_dep_lib] + def _sign_apk_container(self, unsigned_apex): fd, fn = tempfile.mkstemp(prefix=self._testMethodName+"_repacked_", suffix=".apex") os.close(fd) self._to_cleanup.append(fn) + java_toolchain, java_dep_lib = self._get_java_toolchain() cmd = [ - "prebuilts/jdk/jdk11/linux-x86/bin/java", - "-Djava.library.path=out/soong/host/linux-x86/lib64", - "-jar", "out/soong/host/linux-x86/framework/signapk.jar", + java_toolchain, + "-Djava.library.path=" + java_dep_lib, + "-jar", self.host_tools['signapk.jar'], "-a", "4096", os.path.join(get_current_dir(), TEST_X509_KEY), os.path.join(get_current_dir(), TEST_PK8_KEY), diff --git a/apexer/runtests.sh b/apexer/runtests.sh index 7a499c75..e2eacc90 100755 --- a/apexer/runtests.sh +++ b/apexer/runtests.sh @@ -28,10 +28,12 @@ m -j apexer export APEXER_TOOL_PATH="${ANDROID_BUILD_TOP}/out/soong/host/linux-x86/bin:${ANDROID_BUILD_TOP}/prebuilts/sdk/tools/linux/bin" PATH+=":${ANDROID_BUILD_TOP}/prebuilts/sdk/tools/linux/bin" +for fs_type in ext4 f2fs +do input_dir=$(mktemp -d) output_dir=$(mktemp -d) -function finish { +function cleanup { sudo umount /dev/loop10 sudo losetup --detach /dev/loop10 @@ -39,7 +41,7 @@ function finish { rm -rf ${output_dir} } -trap finish EXIT +trap cleanup ERR ############################################# # prepare the inputs ############################################# @@ -55,7 +57,7 @@ ln -s file1 ${input_dir}/sym1 manifest_dir=$(mktemp -d) manifest_file=${manifest_dir}/apex_manifest.pb echo '{"name": "com.android.example.apex", "version": 1}' > ${manifest_dir}/apex_manifest.json -${ANDROID_HOST_OUT}/bin/conv_apex_manifest proto ${manifest_dir}/apex_manifest.json -o ${manifest_file} +${ANDROID_BUILD_TOP}/out/soong/host/linux-x86/bin/conv_apex_manifest proto ${manifest_dir}/apex_manifest.json -o ${manifest_file} # Create the file_contexts file file_contexts_file=$(mktemp) @@ -82,7 +84,9 @@ output_file=${output_dir}/test.apex ${ANDROID_HOST_OUT}/bin/apexer --verbose --manifest ${manifest_file} \ --file_contexts ${file_contexts_file} \ --canned_fs_config ${canned_fs_config_file} \ + --payload_fs_type ${fs_type} \ --key ${ANDROID_BUILD_TOP}/system/apex/apexer/testdata/com.android.example.apex.pem \ + --android_jar_path ${ANDROID_BUILD_TOP}/prebuilts/sdk/current/public/android.jar \ ${input_dir} ${output_file} ############################################# @@ -131,4 +135,8 @@ sudo diff ${input_dir}/sub/file3 ${output_dir}/mnt/sub/file3 # check the android manifest aapt dump xmltree ${output_file} AndroidManifest.xml -echo Passed +echo "Passed for ${fs_type}" +cleanup +done + +echo "Passed for all fs types" diff --git a/apexer/testdata/Android.bp b/apexer/testdata/Android.bp index 5f72dc9b..951f849e 100644 --- a/apexer/testdata/Android.bp +++ b/apexer/testdata/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + android_app_certificate { name: "com.android.example.apex.certificate", // This will use com.android.my.apex.x509.pem (the cert) and @@ -33,6 +37,8 @@ apex { key: "com.android.example.apex.key", certificate: ":com.android.example.apex.certificate", installable: false, + updatable:false, + generate_hashtree: false, } apex { @@ -44,6 +50,7 @@ apex { certificate: ":com.android.example.apex.certificate", min_sdk_version: "29", installable: false, + generate_hashtree: false, } apex { @@ -55,6 +62,7 @@ apex { certificate: ":com.android.example.apex.certificate", installable: false, logging_parent: "foobar", + updatable: false, } apex { @@ -66,4 +74,5 @@ apex { certificate: ":com.android.example.apex.certificate", installable: false, package_name: "com.android.overridden.example.apex", + updatable: false, } diff --git a/docs/README.md b/docs/README.md index 5ba0ab9c..64867a2b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,15 +1,19 @@ # APEX File Format -Android Pony EXpress (APEX) is a container format introduced in Android Q -that is used in the install flow for lower-level system -modules. This format facilitates the updates of system components that don't fit -into the standard Android application model. Some example components are native -services and libraries, hardware abstraction layers +Android Pony EXpress (APEX) is a container format introduced in Android Q that +is used in the install flow for lower-level system modules. This format +facilitates the updates of system components that don't fit into the standard +Android application model. Some example components are native services and +libraries, hardware abstraction layers ([HALs](/https://source.android.com/devices/architecture/hal-types)), runtime ([ART](/https://source.android.com/devices/tech/dalvik)), and class libraries. The term "APEX" can also refer to an APEX file. +This document describes technical details of the APEX file format. If you are +looking at how to build an APEX package, kindly refer to [this how-to](howto.md) +document. + ## Background Although Android supports updates of modules that fit within the standard app @@ -29,7 +33,7 @@ has the following drawbacks: This section describes the high-level design of the APEX file format and the APEX manager, which is a service that manages APEX files. -### APEX format +### APEX format {#apex-format} This is the format of an APEX file. @@ -55,8 +59,8 @@ infrastructure such as ADB, PackageManager, and package installer apps (such as Play Store). For example, the APEX file can use an existing tool such as `aapt` to inspect basic metadata from the file. The file contains package name and version information. This information is generally also available in -`apex_manifest.json`. `AndroidManifest.xml` might contain additional -targeting information that can be used by the existing app publishing tools. +`apex_manifest.json`. `AndroidManifest.xml` might contain additional targeting +information that can be used by the existing app publishing tools. `apex_manifest.json` is recommended over `AndroidManifest.xml` for new code and systems that deal with APEX. @@ -68,16 +72,16 @@ metadata block are created using libavb. The file system payload isn't parsed inside the `apex_payload.img` file. `apex_pubkey` is the public key used to sign the file system image. At runtime, -this key ensures that the downloaded APEX is signed with the same entity -that signs the same APEX in the built-in partitions. +this key ensures that the downloaded APEX is signed with the same entity that +signs the same APEX in the built-in partitions. ### APEX manager -The APEX manager (or `apexd`) is a native daemon responsible for -verifying, installing, and uninstalling APEX files. This process is launched and -is ready early in the boot sequence. APEX files are normally pre-installed on -the device under `/system/apex`. The APEX manager defaults to using these -packages if no updates are available. +The APEX manager (or `apexd`) is a native daemon responsible for verifying, +installing, and uninstalling APEX files. This process is launched and is ready +early in the boot sequence. APEX files are normally pre-installed on the device +under `/system/apex`. The APEX manager defaults to using these packages if no +updates are available. The update sequence of an APEX uses the [PackageManager class](https://developer.android.com/reference/android/content/pm/PackageManager) @@ -132,17 +136,17 @@ The APEX format supports these file types: - Data files - Config files -The APEX format can only update some of these file types. Whether a file -type can be updated depends on the platform and how stable the interfaces for -the files types are defined. +The APEX format can only update some of these file types. Whether a file type +can be updated depends on the platform and how stable the interfaces for the +files types are defined. ### Signing APEX files are signed in two ways. First, the `apex_payload.img` (specifically, the vbmeta descriptor appended to `apex_payload.img`) file is signed with a key. Then, the entire APEX is signed using the -[APK signature scheme v3](/https://source.android.com/security/apksigning/v3). Two different keys are used -in this process. +[APK signature scheme v3](/https://source.android.com/security/apksigning/v3). +Two different keys are used in this process. On the device side, a public key corresponding to the private key used to sign the vbmeta descriptor is installed. The APEX manager uses the public key to @@ -152,9 +156,8 @@ different keys and is enforced both at build time and runtime. ### APEX in built-in partitions APEX files can be located in built-in partitions such as `/system`. The -partition is -already over dm-verity, so the APEX files are mounted directly over the loop -device. +partition is already over dm-verity, so the APEX files are mounted directly over +the loop device. If an APEX is present in a built-in partition, the APEX can be updated by providing an APEX package with the same package name and a higher version code. @@ -165,18 +168,18 @@ the newer version of the APEX is only activated after reboot. ## Kernel requirements To support APEX mainline modules on an Android device, the following Linux -kernel features are required: the loop driver and dm-verity. The loop -driver mounts the file system image in an APEX module and dm-verity verifies the -APEX module. +kernel features are required: the loop driver and dm-verity. The loop driver +mounts the file system image in an APEX module and dm-verity verifies the APEX +module. -The performance of the loop driver and dm-verity is important in achieving -good system performance when using APEX modules. +The performance of the loop driver and dm-verity is important in achieving good +system performance when using APEX modules. ### Supported kernel versions APEX mainline modules are supported on devices using kernel versions 4.4 or -higher. New devices launching with Android Q or higher -must use kernel version 4.9 or higher to support APEX modules. +higher. New devices launching with Android Q or higher must use kernel version +4.9 or higher to support APEX modules. ### Required kernel patches @@ -187,10 +190,9 @@ of the Android common tree. #### Kernel version 4.4 This version is only supported for devices that are upgraded from Android 9 to -Android Q and want to support APEX modules. To get the -required patches, a down-merge from the `android-4.4` branch is strongly -recommended. The following is a list of the required individual patches -for kernel version 4.4. +Android Q and want to support APEX modules. To get the required patches, a +down-merge from the `android-4.4` branch is strongly recommended. The following +is a list of the required individual patches for kernel version 4.4. - UPSTREAM: loop: add ioctl for changing logical block size ([4.4](https://android-review.googlesource.com/c/kernel/common/+/777013){: .external}) @@ -216,9 +218,9 @@ the `android-common` branch. ### Required kernel configuration options -The following list shows the base configuration requirements for supporting -APEX modules that were introduced in Android Q. The -items with an asterisk (\*) are existing requirements from Android 9 and lower. +The following list shows the base configuration requirements for supporting APEX +modules that were introduced in Android Q. The items with an asterisk (\*) are +existing requirements from Android 9 and lower. ``` (*) CONFIG_AIO=Y # AIO support (for direct I/O on loop devices) @@ -242,8 +244,8 @@ requirements. Note: Because the implementation details for APEX are still under development, the content in this section is subject to change. -This section describes how to build an APEX using the Android build system. -The following is an example of `Android.bp` for an APEX named `apex.test`. +This section describes how to build an APEX using the Android build system. The +following is an example of `Android.bp` for an APEX named `apex.test`. ``` apex { @@ -280,12 +282,12 @@ apex { #### File types and locations in APEX -|File type |Location in APEX | -|-------------------|--------------------------------------------------------------| -|Shared libraries |`/lib` and `/lib64` (`/lib/arm` for translated arm in x86) | -|Executables |`/bin` | -|Java libraries |`/javalib` | -|Prebuilts |`/etc` | +File type | Location in APEX +---------------- | ---------------------------------------------------------- +Shared libraries | `/lib` and `/lib64` (`/lib/arm` for translated arm in x86) +Executables | `/bin` +Java libraries | `/javalib` +Prebuilts | `/etc` ### Transitive dependencies @@ -376,6 +378,7 @@ $ avbtool extract_public_key --key foo.pem --output foo.avbpubkey ``` In Android.bp: + ``` apex_key { name: "apex.test.key", @@ -507,21 +510,21 @@ kernel to fully support APEX. For example, the kernel might have been built without `CONFIG_BLK_DEV_LOOP=Y`, which is crucial for mounting the file system image inside an APEX. -Flattened APEX is a specially built APEX that can be activated on devices with -a legacy kernel. Files in a flattened APEX are directly installed to a directory +Flattened APEX is a specially built APEX that can be activated on devices with a +legacy kernel. Files in a flattened APEX are directly installed to a directory under the built-in partition. For example, `lib/libFoo.so` in a flattend APEX `my.apex` is installed to `/system/apex/my.apex/lib/libFoo.so`. Activating a flattened APEX doesn't involve the loop device. The entire directory `/system/apex/my.apex` is directly bind-mounted to `/apex/name@ver`. -Flattened APEXs can't be updated by downloading updated versions -of the APEXs from network because the downloaded APEXs can't be flattened. -Flattened APEXs can be updated only via a regular OTA. +Flattened APEXs can't be updated by downloading updated versions of the APEXs +from network because the downloaded APEXs can't be flattened. Flattened APEXs +can be updated only via a regular OTA. Note that flattened APEX is the default configuration for now. This means all -APEXes are by default flattened unless you explicitly configure your device -to support updatable APEX (explained above). +APEXes are by default flattened unless you explicitly configure your device to +support updatable APEX (explained above). Also note that, mixing flattened and non-flattened APEXes in a device is NOT supported. It should be either all non-flattened or all flattened. This is @@ -530,22 +533,168 @@ like Mainline. APEXes that are not pre-signed (i.e. built from the source) should also be non-flattened and signed with proper keys in that case. The device should inherit from `updatable_apex.mk` as explained above. +## Compressed apexes {#compressed-apex} + +APEX compression is a new feature introduced in Android S. Its main purpose is +to reduce the storage impact of updatable APEX packages: after an update to an +APEX is installed, its pre-installed version is not used anymore, and space that +is taken by it effectively becomes a dead weight. + +APEX compression minimizes the storage impact by using a highly-compressed +set of APEX files on read-only partitions (e.g. `/system`). In Android S a +DEFLATE zip compression is used. + +Note: compression doesn't provide any optimization in the following scenarios: + +* Bootstrap apexes that are required to be mounted very early in the boot + sequence. List of bootstrap apexes is configured in `kBootstrapApexes` + constant in `system/apex/apexd/apexd.cpp`. +* Non-updatable apexes. Compression is only beneficial in case an updated + version of an apex is installed on `/data partition`. + Full list of updatable apexes is available at + https://source.android.com/devices/architecture/modular-system. +* Dynamic shared libs apexes. Since `apexd` will always activate both versions + of such apexes (pre-installed and upgraded), compressing them doesn't provide + any value. + +### Compressed APEX file format + +This is the format of a compressed APEX file. + +![Compressed APEX file format](compressed-apex-format.png) + +**Figure 2.** Compressed APEX file format + +At the top level, a compressed APEX file is a zip file containing the original apex in deflated +form with compression level of 9 and other files stored uncompressed. + +The four files in an APEX file are: + +* `original_apex`: deflated with compression level of 9 +* `apex_manifest.pb`: stored only +* `AndroidManifest.xml`: stored only +* `apex_pubkey`: stored only + + +`original_apex` is the original uncompressed [APEX file](#apex-format). + +`apex_manifest.pb` `AndroidManifest.xml` `apex_pubkey` are copies of the +corresponding files from `original_apex`. + + +### Building compressed apex + +Compressed apex can be built using `apex_compression_tool.py` located at +`system/apex/tools`. + +Note: the outer apk container of the produced compressed apex file won't be +automatically signed. You will need to manually sign it with using the correct +certificate. See [Signing Builds for Release]( +https://source.android.com/devices/tech/ota/sign_builds#apex-signing-key-replacement). + +There are a few different parameters related to APEX compression available in +the build system. + +In `Android.bp` whether an apex is compressible is controlled by `compressible` +property: + +``` +apex { + name: "apex.test", + manifest: "apex_manifest.json", + file_contexts: "file_contexts", + compressible: true, +} +``` + +Note: this only serves as a hint to build system that this apex can be +compressed. Such property is required due to the fact that not all apexes are +compressible as mentioned in the [section above](#compressed-apex). + +TODO(b/183208430): add docs on how this works for prebuilts. + +A `PRODUCT_COMPRESSED_APEX` product flag is used to control whether a system +image built from source should contain compressed apexes or not. + +For local experimentation you can force a build to compress apexes by setting +`OVERRIDE_PRODUCT_COMPRESSED_APEX=true`. + +Compressed APEX files generated by the build system will have `.capex` +extension. It makes it easier to distinguish between compressed and uncompressed +versions of an APEX. + +### Supported compression algorithms + +Android S only supports deflate zip compression. + +### Activating compressed apex during boot + +Before activating a compressed APEX, `original_apex` inside it will be +decompressed into `/data/apex/decompressed` directory. The resulting +decompressed APEX will be hard linked to the `/data/apex/active` directory. + +Note: because of the hard link step above, it's important that files under +`/data/apex/decompressed` have the same SELinux label as files under +`/data/apex/active`. + +Consider following example as an illustration of the process described above. + +Let's assume that `/system/apex/com.android.foo.capex` is a compressed APEX +being activated, and it's `versionCode` is `37`. + +1. First `original_apex` inside `/system/apex/com.android.foo.capex` is + decompressed into `/data/apex/decompressed/com.android.foo@37.apex`. +2. After that `restorecon /data/apex/decompressed/com.android.foo@37.apex` is + performed to make sure that it has a correct SELinux label. +3. Verification checks are performed on + `/data/apex/decompressed/com.android.foo@37.apex` to ensure it's validity: + * `apexd` checks that public key bundled in + `/data/apex/decompressed/com.android.foo@37.apex` is equal to the one + bundled in `/system/apex/com.android.foo.capex` +4. Next `/data/apex/decompressed/com.android.foo@37.apex` is hard linked to + `/data/apex/active/com.android.foo@37.apex`. +5. Finally, regular activation logic for uncompressed APEX files is performed + for `/data/apex/active/com.android.foo@37.apex`. + +For more information see implementation of `OnStart` function in +`system/apex/apexd/apexd.cpp`. + +### Interaction with OTA + +Compressed APEX files have some implications on the OTA delivery and +application. Since an OTA might contain a compressed APEX file with higher +version compared to what is currently active on the device, some free space must +be reserved before rebooting a device to apply an OTA. + +To help OTA system, two new binder APIs are exposed by apexd: + +* `calculateSizeForCompressedApex` - calculates size required for decompressing + APEX files in OTA package. It can be used to check if device has enough space + before downloading an OTA. +* `reserveSpaceForCompressedApex` - reserves space on the disk that in the + future will be used by apexd for decompression of compressed APEX files inside + the OTA package. + + +In case of A/B OTA, `apexd` will attempt decompression in the background as part +of the postinstall OTA routine. If decompression fails, `apexd` will fallback to +decompressing during the boot that applies the OTA. + ## Alternatives considered when developing APEX -Here are some options that we considered when designing the APEX file -format, and why we included or excluded them. +Here are some options that we considered when designing the APEX file format, +and why we included or excluded them. ### Regular package management systems -Linux distributions have package management systems like `dpkg` and `rpm`, -which are powerful, mature and robust. However, they weren't -adopted for APEX because they can't protect the packages after -installation. Verification is done only when packages are being installed. -Attackers can break the integrity of the installed packages unnoticed. This is -a regression for Android where all system components were stored in read-only -file systems whose integrity is protected by dm-verity for every I/O. Any -tampering to system components must be prohibited, or be detectable so that -the device can refuse to boot if compromised. +Linux distributions have package management systems like `dpkg` and `rpm`, which +are powerful, mature and robust. However, they weren't adopted for APEX because +they can't protect the packages after installation. Verification is done only +when packages are being installed. Attackers can break the integrity of the +installed packages unnoticed. This is a regression for Android where all system +components were stored in read-only file systems whose integrity is protected by +dm-verity for every I/O. Any tampering to system components must be prohibited, +or be detectable so that the device can refuse to boot if compromised. ### dm-crypt for integrity @@ -573,15 +722,15 @@ partition, they were accessible via paths such as `/system/lib/libfoo.so`. A client of an APEX file (other APEX files or the platform) should use the new paths. This change in paths might require updates to the existing code. -One way to avoid the path change is to overlay the file contents in an APEX -file over the `/system` partition. However, we decided not to overlay files over -the `/system` partition because we believed this would negatively affect -performance as the number of files being overlayed (possibly even stacked one -after another) increases. +One way to avoid the path change is to overlay the file contents in an APEX file +over the `/system` partition. However, we decided not to overlay files over the +`/system` partition because we believed this would negatively affect performance +as the number of files being overlayed (possibly even stacked one after another) +increases. Another option was to hijack file access functions such as `open`, `stat`, and `readlink`, so that paths that start with `/system` are redirected to their corresponding paths under `/apex`. We discarded this option because it's -practically infeasible to change all functions that accept paths. For -example, some apps statically link Bionic, which implements the functions. In -that case, the redirection won't happen for the app. +practically infeasible to change all functions that accept paths. For example, +some apps statically link Bionic, which implements the functions. In that case, +the redirection won't happen for the app. diff --git a/docs/compressed-apex-format.png b/docs/compressed-apex-format.png Binary files differnew file mode 100644 index 00000000..d67efcbc --- /dev/null +++ b/docs/compressed-apex-format.png diff --git a/docs/howto.md b/docs/howto.md new file mode 100644 index 00000000..a7042493 --- /dev/null +++ b/docs/howto.md @@ -0,0 +1,556 @@ +# How To APEX + +[go/android-apex-howto](http://go/android-apex-howto) (internal link) + +This doc reflects the current implementation status, and thus is expected to +change regularly. + +## Reference + +To understand the design rationale, visit this +[public doc](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/docs/README.md#alternatives-considered-when-developing-apex) +and [go/android-apex](http://go/android-apex) (internal). + +## Building an APEX + +A cheat sheet: + +``` +apex { + name: "com.android.my.apex", + + manifest: "apex_manifest.json", + + // optional. if unspecified, a default one is auto-generated + androidManifest: "AndroidManifest.xml", + + // libc.so and libcutils.so are included in the apex + native_shared_libs: ["libc", "libcutils"], + binaries: ["vold"], + java_libs: ["core-all"], + apps: ["myapk"], + prebuilts: ["my_prebuilt"], + + compile_multilib: "both", + + key: "com.android.my.apex.key", + certificate: ":com.android.my.apex.certificate", +} +``` + +`apex_manifest.json` should look like: + +``` +{ + "name": "com.android.my.apex", + "version": 1 +} +``` + +The file contexts files should be created at +`/system/sepolicy/apex/com.android.my.apex-file_contexts`: + +``` +(/.*)? u:object_r:system_file:s0 +/sub(/.*)? u:object_r:sub_file:s0 +/sub/file3 u:object_r:file3_file:s0 +``` + +The file should describe the contents of your apex. Note that the file is +amended by the build system so that the `apexd` can access the root directory of +your apex and the `apex_manifest.pb` file. (Technically, they are labeled as +`system_file`.) So if you're +[building the apex without Soong](#building-apex-without-soong), please be sure +that `apexd` can access the root directory and the `apex_manifest.pb` file. (In +the example above, the first line does that.) + +#### A script to create a skeleton of APEX + +For convenience, you might want to use a +[script](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/tools/create_apex_skeleton.sh) +that creates a skeleton (`Android.bp`, keys, etc.) of an APEX for you. You only +need to adjust the `APEX_NAME` variable to be your actual APEX name. + +#### File types and places where they are installed in apex + +file type | place in apex +-------------- | ---------------------------------------------------------- +shared libs | `/lib` and `/lib64` (`/lib/arm` for translated arm in x86) +executables | `/bin` +java libraries | `/javalib` +android apps | `/app` or `/priv-app` +prebuilts | `/etc` + +### Transitive dependencies + +Transitive dependencies of a native shared lib or an executable are +automatically included in the APEX. For example, if `libFoo` depends on +`libBar`, then the two libs are included even when only `libFoo` is listed in +`native_shared_libs` property. + +However, if a transitive dependency has a stable ABI, it is not included +transitively. It can be included in an APEX only by directly being referenced. +Currently (2019/08/05), the only module type that can provide stable ABI is +`cc_library`. To do so, add `stubs.*` property as shown below: + +``` +cc_library { + name: "foo", + srcs: [...], + stubs: { + symbol_file: "foo.map.txt", + versions: ["29", "30"], + }, +} +``` + +Use this when a lib has to be accessed across the APEX boundary, e.g. between +APEXes or between an APEX and the platform. + +### apex_available + +Any module that is “included” (not just referenced) in an APEX either via the +direct dependency or the transitive dependency has to correctly set the +`apex_available` property in its `Android.bp` file. The property can have one or +more of the following values: + +* `<name_of_an_apex>`: Like `com.android.adbd`. By specifying the APEX names + explicitly, the module is guaranteed to be included in those APEXes. This is + useful when a module has to be kept as an implementation detail of an APEX + and therefore shouldn’t be used from outside. +* `//apex_available:anyapex`: This means that the module can be included in + any APEX. This is useful for general-purpose utility libraries like + `libbase`, `libcutils`, etc. +* `//apex_available:platform`: The module can be installed to the platform, + outside of APEXes. This is the default value. However, `if apex_available` + is set to either of `<name_of_an_apex` or `//apex_available:anyapex`, the + default is removed. If a module has to be included in both APEX and the + platform, `//apex_available:platform` and`//apex_available:anyapex` should + be specified together. + +The act of adding an APEX name to the `apex_available` property of a module has +to be done or be reviewed by the author(s) of the module. Being included in an +APEX means that the module will be portable, i.e., running on multiple versions +of the current and previous platforms, whereas it usually was expected to run on +the current (the up-to-date) platform. Therefore, the module might have to be +prepared to not have version-specific dependencies to the platform, like the +existence of a dev node, a system call, etc. + +### Handling multiple ABIs + +`compile_multilib`: specifies the ABI(s) that this APEX will compile native +modules for. Can be either of `both`, `first`, `32`, `64`, `prefer32`. For most +of the cases, this should be `both`. + +`native_shared_libs`: installed for **_both_** primary and secondary ABIs of the +device. Of course, if the APEX is built for a target having single ABI (i.e. +32-bit only or 64-bit only), only libraries with the corresponding ABI are +installed. + +`binaries`: installed only for the **_primary_** ABI of the device. In other +words, + +* If the device is 32-bit only, only the 32-bit variant of the binary is + installed. +* If the device supports both 32/64 ABIs, but with + `TARGET_PREFER_32_BIT_EXECUTABLES=true`, then only the 32-bit variant of the + binary is installed. +* If the device is 64-bit only, then only the 64-bit variant of the binary is + installed. +* If the device supports both 32/64 ABIs, but without + `TARGET_PREFER_32_BIT_EXECUTABLES=true`, then only the 64-bit variant of the + binary is installed. + +In order to fine control the ABIs of the native libraries and binaries to be +installed, use +`multilib.[first|lib32|lib64|prefer32|both].[native_shared_libs|binaries]` +properties. + +* `first`: matches with the primary ABI of the device. This is the default for + `binaries`. +* `lib32`: matches with the 32-bit ABI of the device, if supported +* `lib64`: matches with the 64-bit ABI of the device, it supported +* `prefer32`: matches with the 32-bit ABI of the device, if support. If 32-bit + ABI is not supported, it is matched with the 64-bit ABI. +* `both`: matches with the both ABIs. This is the default for + `native_shared_libraries`. +* `java libraries` and `prebuilts`: ABI-agnostic + +Example: (let’s assume that the device supports 32/64 and does not prefer32) + +``` +apex { + // other properties are omitted + compile_multilib: "both", + native_shared_libs: ["libFoo"], // installed for 32 and 64 + binaries: ["exec1"], // installed for 64, but not for 32 + multilib: { + first: { + native_shared_libs: ["libBar"], // installed for 64, but not for 32 + binaries: ["exec2"], // same as binaries without multilib.first + }, + both: { + native_shared_libs: ["libBaz"], // same as native_shared_libs without multilib + binaries: ["exec3"], // installed for 32 and 64 + }, + prefer32: { + native_shared_libs: ["libX"], // installed for 32, but not for 64 + }, + lib64: { + native_shared_libs: ["libY"], // installed for 64, but not for 32 + }, + }, +} +``` + +### APEX image signing + +**Note**: the APEX skeleton creation +[script](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/tools/create_apex_skeleton.sh) +automates this step. + +Each APEX must be signed with different keys. There is no concept of the +platform key. `apexd` in the future might reject if multiple APEXes are signed +with the same key. When a new key is needed, create a public-private key pair +and make an `apex_key` module. Use `key` property to sign an APEX using the key. +The public key is included in the zip container of the APEX as a file entry +`apex_pubkey`. + +How to generate the key pair: + +``` +# create an rsa key pair +$ openssl genrsa -out com.android.my.apex.pem 4096 + +# extract the public key from the key pair +$ avbtool extract_public_key --key com.android.my.apex.pem \ +--output com.android.my.apex.avbpubkey + +# in Android.bp +apex_key { + name: "com.android.my.apex.key", + public_key: "com.android.my.apex.avbpubkey", + private_key: "com.android.my.apex.pem", +} +``` + +Important: In the above example, the name of the public key (that is +`com.android.my.apex`) becomes the ID of the key. The ID of the key used to sign +an APEX is recorded in the APEX. At runtime, a public key with the same ID in +the device is used to verify the APEX. + +### APK (APEX container) signing + +**Note**: the APEX skeleton creation +[script](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/tools/create_apex_skeleton.sh) +automates this step. + +An APEX should also be signed just like APKs. So, an APEX is signed twice; once +for the mini file system (`apex_payload.img` file) and once for the entire file. + +Just like APK, the file-level signing is done via the `certificate` property. It +can be set in three ways. + +* not set: if unset, the APEX is signed with the certificate located at + `PRODUCT_DEFAULT_DEV_CERTIFICATE`. If the flag is also unset, it defaults to + `build/target/product/security/testkey` +* `<name>`: the APEX is signed with the certificate named `<name>` in the same + directory as `PRODUCT_DEFAULT_DEV_CERTIFICATE` +* `<name>`: the APEX signed with the certificate which is defined by a + Soong module named `<name>`. The certificate module can be defined as + follows. + +``` +android_app_certificate { + name: "com.android.my.apex.certificate", + // This will use com.android.my.apex.x509.pem (the cert) and + // com.android.my.apex.pk8 (the private key) + certificate: "com.android.my.apex", +} +``` + +How to generate the certificate/private key pair: + +``` +# Create certificate and private in PEM form +$ openssl req -x509 -newkey rsa:4096 -nodes -days 999999 -keyout key.pem -out com.android.my.apex.x509.pem + +# Enter following info via the interactive prompts +# Country Name: US +# State: California +# Locality Name: Mountain View +# Organization Name: Android +# Organization Unit Name: Android +# Common Name: <your-apk-name> +# Email address: android@android.com + +# Convert the private to pkcs8 format +$ openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -out com.android.my.apex.pk8 -nocrypt +``` + +### Signing APEXs with release keys + +The procedures described in the [APEX image signing](#apex-image-signing) and +[APK (APEX container) signing](#apk-apex-container_signing) sections require the +private keys to be present in the tree. This is not suitable for public release. +Please refer to the +[APEX signing key replacement](https://source.android.com/devices/tech/ota/sign_builds#apex-signing-key-replacement) +documentation to prepare the APEX packages for release. + +For the Google-specific procedure for release keys, the documentation is +available at +[go/android-apex-howto-internal](http://go/android-apex-howto-internal) +(internal only). + +### Linker namespaces for native libraries and binaries + +The linker needs to be set up with separate namespaces for each APEX, for +isolation. It is done through `ld.config.txt` files, which are autogenerated by +`linkerconfig`. Normally you only need to ensure that the APEX manifest +correctly lists the native libraries it requires (from platform or other APEXes) +and provides, which by default is taken from the build system. + +Refer to the [design doc](go/linker-config-apex) for more information about +linkerconfig and apex. + +## Installing an APEX + +Use + +``` +adb install --staged <path_to_apex> && adb reboot +``` + +The `adb install --staged` command triggers a verification for the staged APEX +which might fail when the APEX is signed incorrectly. + +Note that on Q devices when the `adb install --staged` command completes you +still will have to wait until the verification for the staged APEX is finished +before issuing `adb reboot`. + +On R devices we added the `--wait` option to `adb install` to wait until the +verification is completed before returning. On S devices the `--wait` option is +implicit. + +## Hot swapping an APEX (development only) + +Use + +``` +adb sync && adb shell cmd -w apexservice remountPackages +``` + +Note that for this command to remount your APEX, you must ensure that all +processes that have reference to your APEX are killed. E.g. if you are +developing an APEX that contributes to system\_server, you can use the +following: + +``` +adb root +adb remount +adb shell stop +adb sync +adb shell cmd -w apexservice remountPackages +adb shell start +``` + +## Using an APEX + +After the reboot, the apex will be mounted at `/apex/<apex_name>@<version>` +directory. Multiple versions of the same APEX can be mounted at the same time. A +mount point that always points to the latest version of an APEX is provided: +`/apex/<apex_name>`. + +Clients can use the latter path to read or execute something from APEX. + +So, typical usage of APEX is as follows. + +1. an APEX is pre-loaded under `/system/apex`when the device is shipped. +2. Files in it are accessed via the `/apex/<apex_name>/`path. +3. When an updated version of the APEX is installed in `/data/apex/active`, the + path will point to the new APEX after the reboot. + +## Updating service with APEX + +Using APEX, you can update a service. To do so, you need … + +1) Mark the service in system partition as updatable. Add the new option +‘updatable’ to the service definition. + +``` +/system/etc/init/myservice.rc: + +service myservice /system/bin/myservice + class core + user system + … + updatable +``` + +2) Create a new `.rc` file for the updated service. Use ‘`override`’ option to +redefine the existing service. + +``` +/apex/my.apex/etc/init.rc: + +service myservice /apex/my.apex/bin/myservice + class core + user system + … + override +``` + +Note that you can only have service definitions in the rc file in APEX. You +cannot have action triggers in APEXes. + +Also note that if a service marked as updatable is started before APEXes are +activated, the start is delayed until the activation of APEXes is finished. + +## Configuring system to support APEX updates + +Set the following system property to true to support APEX file updates. + +``` +<device.mk>: + +PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=true + +BoardConfig.mk: +TARGET_FLATTEN_APEX := false + +or just +<device.mk>: + +$(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk) +``` + +## Flattened APEX + +For legacy devices, it is sometimes impossible or infeasible to update the old +kernel to fully support APEX. For example, the kernel might have been built +without `CONFIG_BLK_DEV_LOOP=Y`, which is crucial for mounting the file system +image inside an APEX. + +Flattened APEX is a specially built APEX that can be activated on devices with a +legacy kernel. Files in a flattened APEX are directly installed to a directory +under the built-in partition. For example, `lib/libFoo.so` in a flattened APEX +my.apex is installed to `/system/apex/my.apex/lib/libFoo.so`. + +Activating a flattened APEX doesn't involve the loop device. The entire +directory `/system/apex/my.apex` is directly bind-mounted to `/apex/name@ver`. + +Flattened APEXs can‘t be updated by downloading updated versions of the APEXs +from network because the downloaded APEXs can’t be flattened. Flattened APEXs +can be updated only via a regular OTA. + +Note that flattened APEX is the default configuration for now (2019/Aug). This +means all APEXes are by default flattened unless you explicitly configure your +device to support updatable APEX (explained above). + +Also note that, mixing flattened and non-flattened APEXes in a device is NOT +supported. It should be either all non-flattened or all flattened. This is +especially important when shipping pre-signed APEX prebuilts for the projects +like Mainline. APEXes that are not pre-signed (i.e. built from the source) +should also be non-flattened and signed with proper keys in that case. The +device should inherit from `updatable_apex.mk` as explained above. + +## Building APEX without Soong + +An APEX can be built without relying on the build commands generated by Soong. + +1) Prepare following files: + +- APEX manifest file (in JSON) + +- AndroidManifest file (in XML, optional) + +- AVB private key + +- APK certificate (`*.x509.pem`) + +- APK private key (`*.pk8`) + +- `file_contexts` file + +- files to be packaged into the APEX + +2) Create `canned_fs_config` file + +It is a file that specifies access bits and uid/gid of each file in the APEX. + +``` +/ 1000 1000 0755 +/apex_manifest.json 1000 1000 0644 +/apex_manifest.pb 1000 1000 0644 +/file1 1000 1000 0644 +/file2 1000 1000 0644 +/dir 0 2000 0755 +/dir/file3 1000 1000 0644 +... +``` + +Note that ALL files AND directories must be specified. And don’t forget to have +a line for `/`and `/apex_manifest.pb`. (`/apex_manifest.json` line is for +Q-targeting modules) + +3) Invoke `apexer` + +``` +$ apexer \ + --manifest <apex_manifest_file> \ + --file_contexts <file_contexts_file> \ + --canned_fs_config <canned_fs_config_file> \ + --key <avb_private_key_file> \ + --payload_type image \ + --android_manifest <android_manifest_file> \ + --override_apk_package_name com.google.foo \ + <input_directory> \ + <output_apex_file> +``` + +`--android_manifest` and -`-override_apk_package` are optional arguments and +thus can be omitted if not needed. + +Note: The `<apex_manifest_file>` shouldn’t be under `<input_directory>`. + +4) Sign it + +`apexer` signs the `apex_payload.img` file only. The entire apex (which is a zip +file) has to be signed with `Signapk`. + +``` +$ java \ + -Djava.library.path=$(dirname out/soong/host/linux-x86/lib64/libconscrypt_openjdk_jni.so)\ + -jar out/soong/host/linux-x86/framework/signapk.jar \ + -a 4096 \ + <apk_certificate_file> \ + <apk_private_key_file> \ + <unsigned_input_file> \ + <signed_output_file> +``` + +This will sign the input file with the cert/privkey pairs to produce the output +file. + +## Re-packaging an existing APEX + +If an APEX has been build by passing `--include_build_info` to `apexer` (this is +the default when building via Soong), it will then include a file named +`apex_build_info.pb` which will store as much information as possible about how +the apex was built (see the `ApexBuildInfo` proto +[definition](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/proto/apex_build_info.proto) +for more info) with the exception of the signing keys. + +We also provide a tool named `deapexer` to extract the payload content of an +APEX in a local directory. + +By using these tools, you can then adapt the procedure described in the +[building the apex without Soong](#building-apex-without-soong) section and pass +the `--build_info apex_build_info.pb` file where `apex_build_info.pb` contains +all the build parameters that you would otherwise pass via flag to `apexer`. + +We do this programmatically in some unit test code to generate "unusual" APEX +files, see for example +[here](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/apexer/apexer_test.py) +and +[here](https://android.googlesource.com/platform/system/apex/+/refs/heads/master/tests/testdata/sharedlibs/build/shared_libs_repack.py). diff --git a/library_linking_strategy.cc b/library_linking_strategy.cc new file mode 100644 index 00000000..a56d92e2 --- /dev/null +++ b/library_linking_strategy.cc @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2020 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. + */ + +int main() { return 0; } diff --git a/libs/libapexutil/Android.bp b/libs/libapexutil/Android.bp index edf66f2f..5ba3181d 100644 --- a/libs/libapexutil/Android.bp +++ b/libs/libapexutil/Android.bp @@ -14,6 +14,10 @@ * limitations under the License. */ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + cc_defaults { name: "libapexutil-deps", static_libs: [ @@ -30,6 +34,10 @@ cc_library_static { export_include_dirs: ["."], srcs: ["apexutil.cpp"], host_supported: true, + apex_available: [ + "//apex_available:platform", + "com.android.runtime", + ], } cc_test { @@ -45,4 +53,4 @@ cc_test { "general-tests", ], host_supported: true, -}
\ No newline at end of file +} diff --git a/libs/libapexutil/apexutil.cpp b/libs/libapexutil/apexutil.cpp index e0b0787b..6203a3b4 100644 --- a/libs/libapexutil/apexutil.cpp +++ b/libs/libapexutil/apexutil.cpp @@ -18,6 +18,7 @@ #include "apexutil.h" #include <dirent.h> +#include <string.h> #include <memory> @@ -67,6 +68,8 @@ GetActivePackages(const std::string &apex_root) { continue; if (strchr(entry->d_name, '@') != nullptr) continue; + if (strcmp(entry->d_name, "sharedlibs") == 0) + continue; std::string apex_path = apex_root + "/" + entry->d_name; auto manifest = ParseApexManifest(apex_path + "/apex_manifest.pb"); if (manifest.ok()) { diff --git a/proto/Android.bp b/proto/Android.bp index be9966a4..30b24e33 100644 --- a/proto/Android.bp +++ b/proto/Android.bp @@ -14,6 +14,10 @@ * limitations under the License. */ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + cc_library_static { name: "lib_apex_manifest_proto", host_supported: true, @@ -33,6 +37,10 @@ cc_library_static { type: "lite", }, srcs: ["apex_manifest.proto"], + apex_available: [ + "//apex_available:platform", + "com.android.runtime", + ], } python_library_host { @@ -90,3 +98,19 @@ cc_library_static { }, srcs: ["session_state.proto"], } + +genrule { + name: "apex-protos", + tools: ["soong_zip"], + dist: { + targets: ["apexer_tools"], + }, + srcs: [ + "apex_manifest.proto", + "apex_build_info.proto", + ], + out: ["apex-protos.zip"], + cmd: "mkdir $(genDir)/protos && " + + "cp $(in) $(genDir)/protos && " + + "$(location soong_zip) -o $(out) -C $(genDir)/protos -D $(genDir)/protos", +} diff --git a/proto/apex_build_info.proto b/proto/apex_build_info.proto index e25baa20..fd8a3499 100644 --- a/proto/apex_build_info.proto +++ b/proto/apex_build_info.proto @@ -48,4 +48,7 @@ message ApexBuildInfo { // Value of --logging_parent passed at build time. string logging_parent = 9; + + // Value of --payload_fs_type passed at build time. + string payload_fs_type = 10; } diff --git a/proto/apex_manifest.proto b/proto/apex_manifest.proto index dfbf4d23..22fb7d79 100644 --- a/proto/apex_manifest.proto +++ b/proto/apex_manifest.proto @@ -48,4 +48,35 @@ message ApexManifest { // List of native libs which this apex uses from other apexes or system. repeated string requireNativeLibs = 8; + + // List of JNI libs. + // linkerconfig/libnativeloader use this field so that java libraries can + // load JNI libraries in the same apex. + // This is supposed to be filled by the build system with libraries which are + // marked as "is_jni: true" from the list of "native_shared_libs". + repeated string jniLibs = 9; + + // List of libs required that are located in a shared libraries APEX. + // Format of the content is 'library:hash'. + // Example) libc++.so:83d8f50... + repeated string requireSharedApexLibs = 10; + + // Whether this APEX provides libraries to be shared with other APEXs. This + // causes libraries contained in the APEX to be made available under + // /apex/sharedlibs . + bool provideSharedApexLibs = 11; + + message CompressedApexMetadata { + + // Valid only for compressed APEX. This field contains the root digest of + // the original_apex contained inside CAPEX. + string originalApexDigest = 1; + } + + // Exists only for compressed APEX + CompressedApexMetadata capexMetadata = 12; + + // Indicates that this APEX can be updated without rebooting device. + bool supportsRebootlessUpdate = 13; } + diff --git a/proto/session_state.proto b/proto/session_state.proto index 35319cf7..cebf0adb 100644 --- a/proto/session_state.proto +++ b/proto/session_state.proto @@ -58,4 +58,7 @@ message SessionState { // The names of the apexes within this session. Only populated for sessions // that have been activated. repeated string apex_names = 9; + + // Populated with error details when session fails to activate + string error_message = 10; } diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..88750048 --- /dev/null +++ b/pylintrc @@ -0,0 +1,447 @@ +# This Pylint rcfile contains a best-effort configuration to uphold the +# best-practices and style described in the Google Python style guide: +# https://google.github.io/styleguide/pyguide.html +# +# Its canonical open-source location is: +# https://google.github.io/styleguide/pylintrc + +[MASTER] + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=third_party + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=abstract-method, + apply-builtin, + arguments-differ, + attribute-defined-outside-init, + backtick, + bad-option-value, + basestring-builtin, + buffer-builtin, + c-extension-no-member, + consider-using-enumerate, + cmp-builtin, + cmp-method, + coerce-builtin, + coerce-method, + delslice-method, + div-method, + duplicate-code, + eq-without-hash, + execfile-builtin, + file-builtin, + filter-builtin-not-iterating, + fixme, + getslice-method, + global-statement, + hex-method, + idiv-method, + implicit-str-concat-in-sequence, + import-error, + import-self, + import-star-module-level, + inconsistent-return-statements, + input-builtin, + intern-builtin, + invalid-str-codec, + locally-disabled, + long-builtin, + long-suffix, + map-builtin-not-iterating, + misplaced-comparison-constant, + missing-function-docstring, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + no-else-break, + no-else-continue, + no-else-raise, + no-else-return, + no-init, # added + no-member, + no-name-in-module, + no-self-use, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + parameter-unpacking, + print-statement, + raising-string, + range-builtin-not-iterating, + raw_input-builtin, + rdiv-method, + reduce-builtin, + relative-import, + reload-builtin, + round-builtin, + setslice-method, + signature-differs, + standarderror-builtin, + suppressed-message, + sys-max-int, + too-few-public-methods, + too-many-ancestors, + too-many-arguments, + too-many-boolean-expressions, + too-many-branches, + too-many-instance-attributes, + too-many-locals, + too-many-nested-blocks, + too-many-public-methods, + too-many-return-statements, + too-many-statements, + trailing-newlines, + unichr-builtin, + unicode-builtin, + unnecessary-pass, + unpacking-in-except, + useless-else-on-loop, + useless-object-inheritance, + useless-suppression, + using-cmp-argument, + wrong-import-order, + xrange-builtin, + zip-builtin-not-iterating, + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=main,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl + +# Regular expression matching correct function names +function-rgx=^(?:(?P<exempt>setUp|tearDown|setUpModule|tearDownModule)|(?P<camel_case>_?[A-Z][a-zA-Z0-9]*)|(?P<snake_case>_?[a-z][a-z0-9_]*))$ + +# Regular expression matching correct variable names +variable-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct constant names +const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct attribute names +attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ + +# Regular expression matching correct argument names +argument-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=^[a-z][a-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=^_?[A-Z][a-zA-Z0-9]*$ + +# Regular expression matching correct module names +module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ + +# Regular expression matching correct method names +method-rgx=(?x)^(?:(?P<exempt>_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P<camel_case>_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P<snake_case>_{0,2}[a-z][a-z0-9_]*))$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=10 + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# TODO(https://github.com/PyCQA/pylint/issues/3352): Direct pylint to exempt +# lines made too long by directives to pytype. + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=(?x)( + ^\s*(\#\ )?<?https?://\S+>?$| + ^\s*(from\s+\S+\s+)?import\s+.+$) + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=yes + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check= + +# Maximum number of lines in a module +max-module-lines=99999 + +# String used as indentation unit. The internal Google style guide mandates 2 +# spaces. Google's externaly-published style guide says 4, consistent with +# PEP 8. Here, we use 2 spaces, for conformity with many open-sourced Google +# projects (like TensorFlow). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=TODO + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=yes + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging,absl.logging,tensorflow.io.logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec, + sets + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant, absl + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls, + class_ + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=StandardError, + Exception, + BaseException diff --git a/shim/Android.bp b/shim/Android.bp index 351e754f..b9952664 100644 --- a/shim/Android.bp +++ b/shim/Android.bp @@ -15,6 +15,10 @@ // TODO: consider removing _prebuilt suffix from module names and make use of // 'prefer: true' +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + prebuilt_apex { name: "com.android.apex.cts.shim.v1_prebuilt", overrides: ["CtsShimPrebuilt", "CtsShimPrivPrebuilt"], @@ -176,3 +180,43 @@ prebuilt_apex { filename: "com.android.apex.cts.shim.v3.apex", installable: false, } + +prebuilt_apex { + name: "com.android.apex.cts.shim.v2_different_certificate_prebuilt", + arch: { + arm: { + src: "prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex", + }, + arm64: { + src: "prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex", + }, + x86: { + src: "prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex", + }, + x86_64: { + src: "prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex", + }, + }, + filename: "com.android.apex.cts.shim.v2_different_certificate.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.cts.shim.v2_unsigned_apk_container_prebuilt", + arch: { + arm: { + src: "prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_apk_container.apex", + }, + arm64: { + src: "prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_apk_container.apex", + }, + x86: { + src: "prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_apk_container.apex", + }, + x86_64: { + src: "prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_apk_container.apex", + }, + }, + filename: "com.android.apex.cts.shim.v2_unsigned_apk_container.apex", + installable: false, +} diff --git a/shim/build/Android.bp b/shim/build/Android.bp index 2667984b..830cbe16 100644 --- a/shim/build/Android.bp +++ b/shim/build/Android.bp @@ -14,6 +14,10 @@ // Build rules to build shim apexes. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + genrule { name: "com.android.apex.cts.shim.pem", out: ["com.android.apex.cts.shim.pem"], @@ -36,6 +40,27 @@ apex_key { } genrule { + name: "com.android.apex.cts.shim.debug.pem", + out: ["com.android.apex.cts.shim.debug.pem"], + cmd: "openssl genrsa -out $(out) 4096", +} + +genrule { + name: "com.android.apex.cts.shim.debug.pubkey", + srcs: [":com.android.apex.cts.shim.debug.pem"], + out: ["com.android.apex.cts.shim.debug.pubkey"], + tools: ["avbtool"], + cmd: "$(location avbtool) extract_public_key --key $(in) --output $(out)", +} + +apex_key { + name: "com.android.apex.cts.shim.debug.key", + private_key: ":com.android.apex.cts.shim.debug.pem", + public_key: ":com.android.apex.cts.shim.debug.pubkey", + installable: false, +} + +genrule { name: "generate_hash_of_dev_null", out: ["hash.txt"], cmd: "sha512sum -b /dev/null | cut -d' ' -f1 | tee $(out)", @@ -58,6 +83,7 @@ apex { apps: ["CtsShim", "CtsShimPriv"], installable: false, allowed_files: "default_shim_allowed_list.txt", + updatable: false, } apex { @@ -70,6 +96,19 @@ apex { apps: ["CtsShim", "CtsShimPriv"], installable: false, allowed_files: "default_shim_allowed_list.txt", + updatable: false, +} + +apex { + name: "com.android.apex.cts.shim.v2_sign_payload_with_different_key", + manifest: "manifest_v2.json", + androidManifest: "AndroidManifest.xml", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.cts.shim.debug.key", + prebuilts: ["hash_of_dev_null"], + installable: false, + allowed_files: "default_shim_allowed_list.txt", + updatable: false, } apex { @@ -81,6 +120,7 @@ apex { prebuilts: ["hash_of_dev_null"], installable: false, allowed_files: "default_shim_allowed_list.txt", + updatable: false, } apex { @@ -93,7 +133,8 @@ apex { apps: ["CtsShim", "CtsShimPriv"], installable: false, allowed_files: "default_shim_allowed_list.txt", - test_only_no_hashtree: true, + generate_hashtree: false, + updatable: false, } apex { @@ -107,6 +148,7 @@ apex { installable: false, allowed_files: "default_shim_allowed_list.txt", test_only_unsigned_payload: true, + updatable: false, } override_apex { @@ -138,6 +180,7 @@ apex { key: "com.android.apex.cts.shim.key", prebuilts: ["empty_hash"], installable: false, + updatable: false, } prebuilt_etc { @@ -155,6 +198,7 @@ apex { key: "com.android.apex.cts.shim.key", prebuilts: ["hash_of_dev_null", "apex_shim_additional_file"], installable: false, + updatable: false, } prebuilt_etc { @@ -173,6 +217,7 @@ apex { key: "com.android.apex.cts.shim.key", prebuilts: ["hash_of_dev_null", "apex_shim_additional_folder"], installable: false, + updatable: false, } apex { @@ -183,6 +228,7 @@ apex { key: "com.android.apex.cts.shim.key", prebuilts: ["hash_of_dev_null"], installable: false, + updatable: false, } apex { @@ -193,6 +239,7 @@ apex { key: "com.android.apex.cts.shim.key", prebuilts: ["hash_of_dev_null"], installable: false, + updatable: false, } genrule { @@ -212,6 +259,7 @@ genrule { ":com.android.apex.cts.shim.v2_with_post_install_hook", ":com.android.apex.cts.shim.v2_sdk_target_p", ":com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p", + ":com.android.apex.cts.shim.v2_rebootless", ":com.android.apex.cts.shim.v3", ":com.android.apex.cts.shim.v3_signed_bob", ":com.android.apex.cts.shim.v3_signed_bob_rot", @@ -236,6 +284,7 @@ apex { prebuilts: ["hash_v1"], apps: ["CtsShim", "CtsShimPriv"], allowed_files: "default_shim_allowed_list.txt", + updatable: false, } // This is to install the flattened version of com.android.apex.cts.shim. @@ -276,10 +325,12 @@ apex_key { apex { name: "com.android.apex.cts.shim_not_pre_installed", manifest: "manifest_not_pre_installed.json", + androidManifest: "AndroidManifestNotPreInstalled.xml", file_contexts: ":apex.test-file_contexts", key: "com.android.apex.cts.shim_not_pre_installed.key", prebuilts: ["hash_of_dev_null"], installable: false, + updatable: false, } apex { @@ -291,6 +342,7 @@ apex { prebuilts: ["hash_of_dev_null"], installable: false, certificate: ":com.android.apex.cts.shim.debug.cert", + updatable: false, } android_app_certificate { @@ -455,6 +507,7 @@ apex { apps: ["CtsShim", "CtsShimPriv"], installable: false, min_sdk_version: "29", + updatable: false, } genrule { @@ -475,6 +528,7 @@ apex { prebuilts: ["hash_of_dev_null"], installable: false, apps: ["CtsShim", "CtsShimPriv"], + updatable: false, } // Apex shim with apk-in-apex that targets sdk P @@ -487,6 +541,7 @@ apex { prebuilts: ["hash_of_dev_null"], apps: ["CtsShimTargetPSdk"], installable: false, + updatable: false, } // Apex shim with unsigned apk @@ -495,7 +550,25 @@ genrule { srcs: [":com.android.apex.cts.shim.v2"], out: ["com.android.apex.cts.shim.v2_unsigned_apk_container.apex"], cmd: "cp -v $(in) $(out) && zip -d $(out) META-INF*", - dist: { - targets: ["apps_only"], - } +} + +// Apex shim for testing rebootless updates +apex { + name: "com.android.apex.cts.shim.v2_rebootless", + manifest: "manifest_v2_rebootless.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.cts.shim.key", + prebuilts: ["hash_of_dev_null"], + installable: false, + updatable: false, +} + +apex { + name: "com.android.apex.cts.shim.v3_rebootless", + manifest: "manifest_v3_rebootless.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.cts.shim.key", + prebuilts: ["hash_of_dev_null"], + installable: false, + updatable: false, } diff --git a/shim/build/AndroidManifestNotPreInstalled.xml b/shim/build/AndroidManifestNotPreInstalled.xml new file mode 100644 index 00000000..82955f77 --- /dev/null +++ b/shim/build/AndroidManifestNotPreInstalled.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.apex.cts.shim_not_pre_installed"> + <!-- APEX does not have classes.dex --> + <application android:hasCode="false" /> + <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> +</manifest> + diff --git a/shim/build/manifest_v2_rebootless.json b/shim/build/manifest_v2_rebootless.json new file mode 100644 index 00000000..5f6adfdb --- /dev/null +++ b/shim/build/manifest_v2_rebootless.json @@ -0,0 +1,5 @@ +{ + "name": "com.android.apex.cts.shim", + "version": 2, + "supportsRebootlessUpdate": true +} diff --git a/shim/build/manifest_v3_rebootless.json b/shim/build/manifest_v3_rebootless.json new file mode 100644 index 00000000..61e63ff0 --- /dev/null +++ b/shim/build/manifest_v3_rebootless.json @@ -0,0 +1,5 @@ +{ + "name": "com.android.apex.cts.shim", + "version": 3, + "supportsRebootlessUpdate": true +} diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v1.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v1.apex Binary files differindex 7b5758dc..41a0c9cd 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v1.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v1.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2.apex Binary files differindex dc2c8c9d..96669d89 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_file.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_file.apex Binary files differindex 3cf81757..1aa561c9 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_file.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_file.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_folder.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_folder.apex Binary files differindex 53358189..789230e6 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_folder.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_additional_folder.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex Binary files differindex 4f0edff6..11255a54 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex Binary files differindex af89d200..f5e7f034 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_certificate.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_package_name.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_package_name.apex Binary files differindex 12b85e43..ba10726d 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_package_name.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_different_package_name.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_no_hashtree.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_no_hashtree.apex Binary files differindex 00c0c197..8cc3bdcd 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_no_hashtree.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_no_hashtree.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_rebootless.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_rebootless.apex Binary files differnew file mode 100644 index 00000000..7556ac7e --- /dev/null +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_rebootless.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sdk_target_p.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sdk_target_p.apex Binary files differindex 50e77bdd..7c75442f 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sdk_target_p.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sdk_target_p.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex Binary files differnew file mode 100644 index 00000000..1cb49705 --- /dev/null +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob.apex Binary files differindex 3bde9ba5..b05306d2 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot.apex Binary files differindex 26c3f27a..14769b57 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex Binary files differindex 81aaecfc..8ac1ff9d 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_payload.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_payload.apex Binary files differindex d2be4a2a..adaff7bc 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_payload.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_unsigned_payload.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_post_install_hook.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_post_install_hook.apex Binary files differindex e1f904e6..3ad60002 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_post_install_hook.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_post_install_hook.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_pre_install_hook.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_pre_install_hook.apex Binary files differindex c8aa9d98..ba1755d8 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_pre_install_hook.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_with_pre_install_hook.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex Binary files differindex 41c29bfd..82e4754d 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_without_apk_in_apex.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_wrong_sha.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_wrong_sha.apex Binary files differindex 1447e7d2..5ef089a6 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v2_wrong_sha.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v2_wrong_sha.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v3.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v3.apex Binary files differindex e538f831..4b2b49ef 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v3.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v3.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v3_rebootless.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_rebootless.apex Binary files differnew file mode 100644 index 00000000..802cfce8 --- /dev/null +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_rebootless.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob.apex Binary files differindex 86b418e0..c2cb4e23 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob_rot.apex b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob_rot.apex Binary files differindex 29a0877b..75acc54e 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob_rot.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim.v3_signed_bob_rot.apex diff --git a/shim/prebuilts/arm/com.android.apex.cts.shim_not_pre_installed.apex b/shim/prebuilts/arm/com.android.apex.cts.shim_not_pre_installed.apex Binary files differindex e8705c7f..61257d6d 100644 --- a/shim/prebuilts/arm/com.android.apex.cts.shim_not_pre_installed.apex +++ b/shim/prebuilts/arm/com.android.apex.cts.shim_not_pre_installed.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v1.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v1.apex Binary files differindex 04ec92da..f8b1dc57 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v1.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v1.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2.apex Binary files differindex ff547892..2500feb2 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_file.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_file.apex Binary files differindex 3cf81757..1aa561c9 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_file.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_file.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_folder.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_folder.apex Binary files differindex 53358189..789230e6 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_folder.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_additional_folder.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex Binary files differindex 4f0edff6..11255a54 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_apk_in_apex_sdk_target_p.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex Binary files differindex af89d200..f5e7f034 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_certificate.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_package_name.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_package_name.apex Binary files differindex eca1c375..8a6cc222 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_package_name.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_different_package_name.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_no_hashtree.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_no_hashtree.apex Binary files differindex 19da8646..1ebb069f 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_no_hashtree.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_no_hashtree.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_rebootless.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_rebootless.apex Binary files differnew file mode 100644 index 00000000..7556ac7e --- /dev/null +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_rebootless.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sdk_target_p.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sdk_target_p.apex Binary files differindex 6b7436b4..1189d97e 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sdk_target_p.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sdk_target_p.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex Binary files differnew file mode 100644 index 00000000..1cb49705 --- /dev/null +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_sign_payload_with_different_key.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob.apex Binary files differindex 659f4625..31307155 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot.apex Binary files differindex c5e22108..b1f1db8e 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex Binary files differindex 3ff88a91..3f2059da 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_signed_bob_rot_rollback.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_payload.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_payload.apex Binary files differindex cd702268..aec79145 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_payload.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_unsigned_payload.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_post_install_hook.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_post_install_hook.apex Binary files differindex e1f904e6..3ad60002 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_post_install_hook.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_post_install_hook.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_pre_install_hook.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_pre_install_hook.apex Binary files differindex c8aa9d98..ba1755d8 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_pre_install_hook.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_with_pre_install_hook.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex Binary files differindex 41c29bfd..82e4754d 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_without_apk_in_apex.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_wrong_sha.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_wrong_sha.apex Binary files differindex 1447e7d2..5ef089a6 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v2_wrong_sha.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v2_wrong_sha.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v3.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v3.apex Binary files differindex b3ecef76..e3782eab 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v3.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v3.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v3_rebootless.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_rebootless.apex Binary files differnew file mode 100644 index 00000000..802cfce8 --- /dev/null +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_rebootless.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob.apex Binary files differindex 9281ee39..bec8ad0b 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob_rot.apex b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob_rot.apex Binary files differindex 23b23789..2a1ffb89 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob_rot.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim.v3_signed_bob_rot.apex diff --git a/shim/prebuilts/x86/com.android.apex.cts.shim_not_pre_installed.apex b/shim/prebuilts/x86/com.android.apex.cts.shim_not_pre_installed.apex Binary files differindex e8705c7f..61257d6d 100644 --- a/shim/prebuilts/x86/com.android.apex.cts.shim_not_pre_installed.apex +++ b/shim/prebuilts/x86/com.android.apex.cts.shim_not_pre_installed.apex diff --git a/tests/Android.bp b/tests/Android.bp index 96fd30b4..425f2bc2 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + java_defaults { name: "apex_e2e_test_defaults", libs: [ @@ -24,17 +28,6 @@ java_defaults { } java_test_host { - name: "adbd_e2e_tests", - srcs: ["src/**/AdbdHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.adbd", - ], - test_config: "adbd-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { name: "timezone_data_e2e_tests", srcs: ["src/**/TimezoneDataHostTest.java"], defaults: ["apex_e2e_test_defaults"], @@ -49,17 +42,6 @@ java_test_host { } java_test_host { - name: "statsd_e2e_tests", - srcs: ["src/**/StatsdHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.os.statsd", - ], - test_config: "statsd-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { name: "media_e2e_tests", srcs: ["src/**/MediaHostTest.java"], defaults: ["apex_e2e_test_defaults"], @@ -82,95 +64,6 @@ java_test_host { } java_test_host { - name: "mediaprovider_e2e_tests", - srcs: ["src/**/MediaProviderHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.mediaprovider", - ], - test_config: "mediaprovider-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "conscrypt_e2e_tests", - srcs: ["src/**/ConscryptHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.conscrypt", - ], - test_config: "conscrypt-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "neuralnetworks_e2e_tests", - srcs: ["src/**/NeuralNetworksHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.neuralnetworks", - ], - test_config: "neuralnetworks-e2e-tests.xml", - test_suites: ["device-tests"], -} - - -java_test_host { - name: "cellbroadcast_e2e_tests", - srcs: ["src/**/CellbroadcastHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.cellbroadcast", - ], - test_config: "cellbroadcast-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "ipsec_e2e_tests", - srcs: ["src/**/IpSecHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.ipsec", - ], - test_config: "ipsec-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "permission_e2e_tests", - srcs: ["src/**/PermissionHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.permission", - ], - test_config: "permission-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "wifi_e2e_tests", - srcs: ["src/**/WifiHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.wifi", - ], - test_config: "wifi-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { - name: "extservices_e2e_tests", - srcs: ["src/**/ExtServicesHostTest.java"], - defaults: ["apex_e2e_test_defaults"], - data: [ - ":test_com.android.extservices", - ], - test_config: "extservices-e2e-tests.xml", - test_suites: ["device-tests"], -} - -java_test_host { name: "apex_targetprep_tests", libs: ["tradefed"], srcs: ["src/**/ApexTargetPrepTest.java"], @@ -183,19 +76,14 @@ java_library_host { name: "apex_e2e_base_test", srcs: ["src/**/ApexE2EBaseHostTest.java"], static_libs: [ - "module_test_util", + "frameworks-base-hostutils", + "cts-install-lib-host", ], libs: [ "tradefed", ], } -java_library_host { - name: "module_test_util", - srcs: ["util/**/ModuleTestUtils.java"], - libs: ["tradefed", "truth-prebuilt"], -} - apex { name: "apex.test", manifest: "testdata/apex_manifest.json", @@ -203,6 +91,7 @@ apex { key: "apex.test.key", certificate: ":apex.test.certificate", installable: false, + updatable: false, } apex_key { @@ -237,21 +126,11 @@ prebuilt_etc { installable: false, } -cc_binary { - name: "sample_prefer32_binary", - srcs: ["sample_prefer32_binary.cc"], - target: { - android: { - compile_multilib: "prefer32", - }, - }, -} - java_test_host { name: "apex_rollback_tests", - srcs: ["src/**/ApexRollbackTests.java", "src/**/ApexTestUtils.java"], + srcs: ["src/**/ApexRollbackTests.java"], libs: ["tradefed", "truth-prebuilt"], - static_libs: ["module_test_util"], + static_libs: ["frameworks-base-hostutils", "cts-install-lib-host"], test_config: "apex-rollback-tests.xml", test_suites: ["general-tests"], @@ -265,23 +144,17 @@ java_test_host { } java_test_host { - name: "module_test_utils_tests", - srcs: ["src/**/ModuleTestUtilsTest.java"], - libs: ["tradefed", "truth-prebuilt"], - static_libs: ["module_test_util"], - test_config: "module-test-utils-tests.xml", - test_suites: ["general-tests"], - data: [":com.android.apex.cts.shim.v2_prebuilt"], -} - -java_test_host { name: "apexd_host_tests", - srcs: ["src/**/ApexdHostTest.java"], + srcs: [ + "src/**/ApexdHostTest.java", + ":apex-info-list", + ], libs: ["tradefed"], static_libs: [ - "module_test_util", "truth-prebuilt", "apex_manifest_proto_java", + "frameworks-base-hostutils", + "cts-install-lib-host" ], test_config: "apexd-host-tests.xml", test_suites: ["general-tests"], @@ -292,5 +165,67 @@ java_test_host { ":apex.apexd_test_v3", ":com.android.apex.cts.shim.v2_prebuilt", ":com.android.apex.cts.shim.v2_no_pb", + ":com.android.apex.cts.shim.v2_additional_file_prebuilt", + ], +} + +java_test_host { + name: "sharedlibs_host_tests", + srcs: [ + "src/**/SharedLibsApexTest.java" + ], + libs: ["tradefed"], + java_resources: [ + ":com.android.apex.test.bar_stripped.v1.libvX_prebuilt", + ":com.android.apex.test.bar_stripped.v2.libvY_prebuilt", + ":com.android.apex.test.bar.v1.libvX_prebuilt", + ":com.android.apex.test.bar.v2.libvY_prebuilt", + ":com.android.apex.test.baz_stripped.v1.libvX_prebuilt", + ":com.android.apex.test.foo_stripped.v1.libvX_prebuilt", + ":com.android.apex.test.foo_stripped.v2.libvY_prebuilt", + ":com.android.apex.test.foo.v1.libvX_prebuilt", + ":com.android.apex.test.foo.v2.libvY_prebuilt", + ":com.android.apex.test.pony_stripped.v1.libvZ_prebuilt", + ":com.android.apex.test.pony.v1.libvZ_prebuilt", + ":com.android.apex.test.sharedlibs_generated.v1.libvX_prebuilt", + ":com.android.apex.test.sharedlibs_generated.v2.libvY_prebuilt", + ":com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ_prebuilt", ], + static_libs: [ + "compatibility-host-util", + "cts-install-lib-host", + "frameworks-base-hostutils", + "truth-prebuilt" + ], + test_config: "shared-libs-apex-tests.xml", + test_suites: ["general-tests"], +} + +java_test_host { + name: "apex_compression_platform_tests", + srcs: ["src/**/ApexCompressionTests.java"], + libs: ["tradefed", "truth-prebuilt"], + static_libs: ["cts-install-lib-host", "testng"], + test_config: "apex_compression_platform_tests.xml", + test_suites: ["general-tests"], + data: [ + ":com.android.apex.compressed.v1", + ":com.android.apex.compressed.v1_original", + ":com.android.apex.compressed.v1_different_digest", + ":com.android.apex.compressed.v2", + ":gen_manifest_mismatch_compressed_apex_v2", + ":apex_compression_tests_app", + ], +} + +android_test_helper_app { + name: "apex_compression_tests_app", + manifest: "app/AndroidManifest.xml", + srcs: ["app/src/**/*.java"], + static_libs: ["androidx.test.rules", "cts-install-lib", "cts-rollback-lib", "testng"], + test_suites: ["general-tests"], + java_resources: [ + ":com.android.apex.compressed.v1_original", + ":com.android.apex.compressed.v2_original", + ] } diff --git a/tests/TEST_MAPPING b/tests/TEST_MAPPING index cd488fb2..d3177a80 100644 --- a/tests/TEST_MAPPING +++ b/tests/TEST_MAPPING @@ -1,49 +1,25 @@ { "presubmit": [ { - "name": "apex_rollback_tests" - }, - { "name": "apex_targetprep_tests" }, { - "name": "adbd_e2e_tests" - }, - { - "name": "apexd_host_tests" - }, - { - "name": "conscrypt_e2e_tests" - }, - { - "name": "extservices_e2e_tests" - }, - { - "name": "neuralnetworks_e2e_tests" - }, - { - "name": "permission_e2e_tests" - }, - { - "name": "sdkextensions_e2e_tests" - }, - { - "name": "statsd_e2e_tests" - }, - { "name": "timezone_data_e2e_tests" }, { - "name": "ipsec_e2e_tests" - }, - { - "name": "wifi_e2e_tests" - }, + "name": "CtsApexSharedLibrariesTestCases" + } + ], + "presubmit-large": [ + // TODO(b/190710217): uncomment this. + //{ + // "name": "apex_rollback_tests" + //}, { - "name": "mediaprovider_e2e_tests" + "name": "sdkextensions_e2e_tests" }, { - "name": "cellbroadcast_e2e_tests" + "name": "sharedlibs_host_tests" } ], "postsubmit": [ @@ -58,57 +34,16 @@ "name": "media_swcodec_e2e_tests", "keywords": ["primary-device"] }, - // The following changes are in post-submit to restrict to physical - // devices (currently userspace reboot fails on cuttlefish). - // TODO(b/147726967): Remove when Userspace reboot works on cuttlefish { - "name": "adbd_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "conscrypt_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "extservices_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "neuralnetworks_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "permission_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "sdkextensions_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "timezone_data_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "ipsec_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "wifi_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "mediaprovider_e2e_tests", - "keywords": ["primary-device"] - }, - { - "name": "cellbroadcast_e2e_tests", - "keywords": ["primary-device"] + "name": "apex_compression_platform_tests" } ], "imports": [ { "path": "cts/hostsidetests/stagedinstall" + }, + { + "path": "frameworks/base/tests/StagedInstallTest" } ] } diff --git a/tests/adbd-e2e-tests.xml b/tests/adbd-e2e-tests.xml deleted file mode 100644 index a144d834..00000000 --- a/tests/adbd-e2e-tests.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2020 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. ---> -<configuration description="Configuration for adbd module e2e tests"> - <option name="test-suite-tag" value="adbd_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="adbd_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.adbd.apex" /> - </test> -</configuration> diff --git a/tests/apex_compression_platform_tests.xml b/tests/apex_compression_platform_tests.xml new file mode 100644 index 00000000..a837fbad --- /dev/null +++ b/tests/apex_compression_platform_tests.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<configuration description="Runs the apex compression test cases"> + <option name="test-suite-tag" value="apex_compression_platform_tests" /> + <option name="test-suite-tag" value="apct" /> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="apex_compression_tests_app.apk" /> + </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <option name="run-command" value="setprop persist.rollback.is_test 1" /> + <option name="teardown-command" value="setprop persist.rollback.is_test 0" /> + </target_preparer> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="class" + value="com.android.tests.apex.host.ApexCompressionTests" /> + </test> +</configuration> diff --git a/tests/app/AndroidManifest.xml b/tests/app/AndroidManifest.xml new file mode 100644 index 00000000..0644ba7e --- /dev/null +++ b/tests/app/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.tests.apex.app" > + + <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" /> + <application> + <receiver android:name="com.android.cts.install.lib.LocalIntentSender" + android:exported="true" /> + <uses-library android:name="android.test.runner" /> + </application> + + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.tests.apex.app" + android:label="ApexCompression Test"/> + +</manifest> diff --git a/tests/app/src/com/android/tests/apex/app/ApexCompressionTests.java b/tests/app/src/com/android/tests/apex/app/ApexCompressionTests.java new file mode 100644 index 00000000..a97293ce --- /dev/null +++ b/tests/app/src/com/android/tests/apex/app/ApexCompressionTests.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.tests.apex.app; + +import static com.google.common.truth.Truth.assertThat; + +import android.Manifest; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.rollback.RollbackInfo; + +import androidx.test.InstrumentationRegistry; + +import com.android.cts.install.lib.Install; +import com.android.cts.install.lib.InstallUtils; +import com.android.cts.install.lib.TestApp; +import com.android.cts.rollback.lib.RollbackUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ApexCompressionTests { + private static final String COMPRESSED_APEX_PACKAGE_NAME = "com.android.apex.compressed"; + private final Context mContext = InstrumentationRegistry.getContext(); + private final PackageManager mPm = mContext.getPackageManager(); + + private static final TestApp UNCOMPRESSED_APEX_V1 = new TestApp( + "TestAppUncompressedApexV1", COMPRESSED_APEX_PACKAGE_NAME, 2, /*isApex*/ true, + "com.android.apex.compressed.v1_original.apex"); + private static final TestApp UNCOMPRESSED_APEX_V2 = new TestApp( + "TestAppUncompressedApexV2", COMPRESSED_APEX_PACKAGE_NAME, 2, /*isApex*/ true, + "com.android.apex.compressed.v2_original.apex"); + + @Before + public void adoptShellPermissions() { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity( + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.DELETE_PACKAGES, + Manifest.permission.TEST_MANAGE_ROLLBACKS); + } + + @After + public void dropShellPermissions() { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + @Test + public void testDecompressedApexIsConsideredFactory() throws Exception { + // Only retrieve active apex package + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi).isNotNull(); + assertThat(pi.isApex).isTrue(); + assertThat(pi.packageName).isEqualTo(COMPRESSED_APEX_PACKAGE_NAME); + assertThat(pi.getLongVersionCode()).isEqualTo(1); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isTrue(); + } + + @Test + public void testUnusedDecompressedApexIsCleanedUp_HigherVersion() throws Exception { + Install.single(UNCOMPRESSED_APEX_V2).setStaged().commit(); + } + + @Test + public void testUnusedDecompressedApexIsCleanedUp_SameVersion() throws Exception { + Install.single(UNCOMPRESSED_APEX_V1).setStaged().commit(); + } + + + @Test + public void testCapexToApexSwitch() throws Exception { + // Only retrieve active apex package + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi).isNotNull(); + assertThat(pi.isApex).isTrue(); + assertThat(pi.packageName).isEqualTo(COMPRESSED_APEX_PACKAGE_NAME); + assertThat(pi.getLongVersionCode()).isEqualTo(1); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isTrue(); + assertThat(pi.applicationInfo.sourceDir).startsWith("/system/apex"); + } + + @Test + public void testDecompressedApexVersionAlwaysHasSameVersionAsCapex() throws Exception { + // Only retrieve active apex package + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi).isNotNull(); + assertThat(pi.isApex).isTrue(); + assertThat(pi.packageName).isEqualTo(COMPRESSED_APEX_PACKAGE_NAME); + assertThat(pi.getLongVersionCode()).isEqualTo(1); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isTrue(); + } + + @Test + public void testCompressedApexCanBeRolledBack_Commit() throws Exception { + // Verify before updating + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi.getLongVersionCode()).isEqualTo(1); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isTrue(); + assertThat(pi.applicationInfo.sourceDir).startsWith("/data/apex/decompressed/"); + + Install.single(UNCOMPRESSED_APEX_V2).setStaged().setEnableRollback().commit(); + } + + @Test + public void testCompressedApexCanBeRolledBack_Rollback() throws Exception { + // Verify before rollback + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi.getLongVersionCode()).isEqualTo(2); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isFalse(); + assertThat(pi.applicationInfo.sourceDir).startsWith("/data/apex/active/"); + + // Trigger rollback + RollbackInfo available = RollbackUtils.getAvailableRollback(COMPRESSED_APEX_PACKAGE_NAME); + RollbackUtils.rollback(available.getRollbackId(), UNCOMPRESSED_APEX_V2); + RollbackInfo committed = RollbackUtils.getCommittedRollbackById(available.getRollbackId()); + InstallUtils.waitForSessionReady(committed.getCommittedSessionId()); + } + + @Test + public void testCompressedApexCanBeRolledBack_Verify() throws Exception { + // Verify rollback worked + PackageInfo pi = mPm.getPackageInfo( + COMPRESSED_APEX_PACKAGE_NAME, PackageManager.MATCH_APEX); + assertThat(pi.getLongVersionCode()).isEqualTo(1); + boolean isFactoryPackage = (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + assertThat(isFactoryPackage).isFalse(); + assertThat(pi.applicationInfo.sourceDir).startsWith("/data/apex/active/"); + } +} diff --git a/tests/cellbroadcast-e2e-tests.xml b/tests/cellbroadcast-e2e-tests.xml deleted file mode 100644 index 368a88fa..00000000 --- a/tests/cellbroadcast-e2e-tests.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for Cellbroadcast module e2e test cases"> - <option name="test-suite-tag" value="cellbroadcast_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="cellbroadcast_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.cellbroadcast.apex" /> - </test> -</configuration>
\ No newline at end of file diff --git a/tests/conscrypt-e2e-tests.xml b/tests/conscrypt-e2e-tests.xml deleted file mode 100644 index 77f781a2..00000000 --- a/tests/conscrypt-e2e-tests.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for Conscrypt module e2e test cases"> - <option name="test-suite-tag" value="conscrypt_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="conscrypt_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.conscrypt.apex" /> - </test> -</configuration> - diff --git a/tests/extservices-e2e-tests.xml b/tests/extservices-e2e-tests.xml deleted file mode 100644 index c45392e6..00000000 --- a/tests/extservices-e2e-tests.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2020 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. ---> -<configuration description="Config for extservices apex e2e testing"> - <option name="test-suite-tag" value="extservices_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="extservices_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.extservices.apex" /> - </test> -</configuration> diff --git a/tests/ipsec-e2e-tests.xml b/tests/ipsec-e2e-tests.xml deleted file mode 100644 index 0030bd34..00000000 --- a/tests/ipsec-e2e-tests.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for ipsec module e2e testing"> - <option name="test-suite-tag" value="ipsec_e2e_test" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" /> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="ipsec_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.ipsec.apex" /> - </test> -</configuration> - diff --git a/tests/mediaprovider-e2e-tests.xml b/tests/mediaprovider-e2e-tests.xml deleted file mode 100644 index e1212622..00000000 --- a/tests/mediaprovider-e2e-tests.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for MediaProvider module e2e test cases"> - <option name="test-suite-tag" value="mediaprovider_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="mediaprovider_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.mediaprovider.apex" /> - </test> -</configuration> diff --git a/tests/module-test-utils-tests.xml b/tests/module-test-utils-tests.xml deleted file mode 100644 index 6337ab92..00000000 --- a/tests/module-test-utils-tests.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2019 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. - --> -<configuration description="Unit tests for ModuleTestUtils"> - <option name="test-suite-tag" value="module_test_utils_tests" /> - <option name="test-suite-tag" value="apct" /> - <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="module_test_utils_tests.jar" /> - </test> -</configuration> diff --git a/tests/native/.clang-format b/tests/native/.clang-format new file mode 100644 index 00000000..f6cb8ad9 --- /dev/null +++ b/tests/native/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/tests/native/Android.bp b/tests/native/Android.bp new file mode 100644 index 00000000..79152223 --- /dev/null +++ b/tests/native/Android.bp @@ -0,0 +1,55 @@ +// 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. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_test { + name: "CtsApexSharedLibrariesTestCases", + test_suites: [ + "cts", + "device-tests", + "mts", + ], + compile_multilib: "both", + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + + shared_libs: [ + "libbase", + "liblog", + ], + + static_libs: [ + "libc++fs", + "libfs_mgr", + ], + + srcs: [ + "apex_shared_libraries_test.cpp", + ], + + cflags: [ + "-Wall", + "-Wextra", + "-Werror", + ], +} diff --git a/tests/native/AndroidTest.xml b/tests/native/AndroidTest.xml new file mode 100644 index 00000000..fa189d5e --- /dev/null +++ b/tests/native/AndroidTest.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<configuration description="Config for CTS apex_shared_libraries test cases"> + <option name="test-suite-tag" value="cts" /> + <option name="config-descriptor:metadata" key="component" value="systems" /> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher"> + <option name="cleanup" value="true" /> + <option name="push" value="CtsApexSharedLibrariesTestCases->/data/local/tmp/CtsApexSharedLibrariesTestCases" /> + <option name="append-bitness" value="true" /> + </target_preparer> + <test class="com.android.tradefed.testtype.GTest" > + <option name="native-test-device-path" value="/data/local/tmp" /> + <option name="module-name" value="CtsApexSharedLibrariesTestCases" /> + <option name="runtime-hint" value="65s" /> + </test> +</configuration> diff --git a/tests/native/apex_shared_libraries_test.cpp b/tests/native/apex_shared_libraries_test.cpp new file mode 100644 index 00000000..9f536dc6 --- /dev/null +++ b/tests/native/apex_shared_libraries_test.cpp @@ -0,0 +1,142 @@ +/* + * 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. + */ +#define LOG_TAG "apex_shared_libraries_test" + +#include <android-base/logging.h> +#include <android-base/properties.h> +#include <android-base/scopeguard.h> +#include <android-base/strings.h> +#include <dlfcn.h> +#include <fstab/fstab.h> +#include <gtest/gtest.h> +#include <link.h> + +#include <filesystem> +#include <fstream> +#include <string> + +using android::base::GetBoolProperty; +using android::base::Split; +using android::base::StartsWith; +using android::fs_mgr::Fstab; +using android::fs_mgr::ReadFstabFromFile; + +namespace fs = std::filesystem; + +static constexpr const char* kApexRoot = "/apex"; +static constexpr const char* kApexSharedLibsRoot = "/apex/sharedlibs"; + +TEST(apex_shared_libraries, symlink_libraries_loadable) { + if (!GetBoolProperty("ro.apex.updatable", false)) { + GTEST_SKIP() << "Skipping test because device doesn't support APEX"; + } + + Fstab fstab; + ASSERT_TRUE(ReadFstabFromFile("/proc/mounts", &fstab)); + + // Traverse mount points to identify apexs. + for (auto& entry : fstab) { + if (fs::path(entry.mount_point).parent_path() != kApexRoot) { + continue; + } + + // Focus on "active" apexs. + if (entry.mount_point.find('@') != std::string::npos) { + continue; + } + std::string dev_file = fs::path(entry.blk_device).filename(); + + // Filter out any mount irrelevant (e.g. tmpfs) + if (!StartsWith(dev_file, "loop") && !StartsWith(dev_file, "dm-")) { + continue; + } + +#if !defined(__LP64__) + auto lib = fs::path(entry.mount_point) / "lib"; +#else // !__LP64__ + auto lib = fs::path(entry.mount_point) / "lib64"; +#endif // !__LP64__ + if (!fs::is_directory(lib)) { + continue; + } + for (auto& p : fs::directory_iterator(lib)) { + std::error_code ec; + if (!fs::is_symlink(p, ec)) { + continue; + } + + // We are only checking libraries pointing at a location inside + // /apex/sharedlibs. + auto target = fs::read_symlink(p.path(), ec); + if (ec || !StartsWith(target.string(), kApexSharedLibsRoot)) { + continue; + } + + // Symlink validity check. + auto dest = fs::canonical(p.path(), ec); + EXPECT_FALSE(ec) << "Failed to resolve " << p.path() << " (symlink to " + << target << "): " << ec; + if (ec) { + continue; + } + + // Library loading validity check. + dlerror(); // Clear any pending errors. + void* handle = dlopen(p.path().c_str(), RTLD_NOW); + EXPECT_TRUE(handle != nullptr) << dlerror(); + if (handle == nullptr) { + continue; + } + auto guard = android::base::make_scope_guard([&]() { dlclose(handle); }); + + // Check that library is loaded and pointing to the realpath of the + // library. + auto dl_callback = [](dl_phdr_info* info, size_t /* size */, void* data) { + auto dest = *static_cast<fs::path*>(data); + if (info->dlpi_name == nullptr) { + // This is linker imposing as libdl.so - skip it + return 0; + } + int j; + for (j = 0; j < info->dlpi_phnum; j++) { + void* addr = (void*)(info->dlpi_addr + info->dlpi_phdr[j].p_vaddr); + Dl_info dl_info; + int rc = dladdr(addr, &dl_info); + if (rc == 0) { + continue; + } + if (dl_info.dli_fname) { + auto libpath = fs::path(dl_info.dli_fname); + if (libpath == dest) { + // Library found! + return 1; + } + } + } + + return 0; + }; + bool found = (dl_iterate_phdr(dl_callback, &dest) == 1); + EXPECT_TRUE(found) << "Error verifying library symlink " << p.path() + << " which points to " << target + << " which resolves to file " << dest; + if (found) { + LOG(INFO) << "Verified that " << p.path() + << " correctly loads as library " << dest; + } + } + } +} diff --git a/tests/neuralnetworks-e2e-tests.xml b/tests/neuralnetworks-e2e-tests.xml deleted file mode 100644 index 63c38a68..00000000 --- a/tests/neuralnetworks-e2e-tests.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for neuralnetworks module e2e test cases"> - <option name="test-suite-tag" value="neuralnetworks_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="neuralnetworks_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.neuralnetworks.apex" /> - </test> -</configuration> - diff --git a/tests/permission-e2e-tests.xml b/tests/permission-e2e-tests.xml deleted file mode 100644 index e1605abe..00000000 --- a/tests/permission-e2e-tests.xml +++ /dev/null @@ -1,26 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> - -<!-- Copyright (C) 2018 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. ---> - -<configuration description="Configuration for permission module e2e test cases"> - <option name="test-suite-tag" value="permission_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="permission_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.permission.apex" /> - </test> -</configuration> diff --git a/tests/sample_prefer32_binary.cc b/tests/sample_prefer32_binary.cc deleted file mode 100644 index 76e81970..00000000 --- a/tests/sample_prefer32_binary.cc +++ /dev/null @@ -1 +0,0 @@ -int main() { return 0; } diff --git a/tests/shared-libs-apex-tests.xml b/tests/shared-libs-apex-tests.xml new file mode 100644 index 00000000..d41b8916 --- /dev/null +++ b/tests/shared-libs-apex-tests.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2020 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. +--> +<configuration description="Runs the shared libs apex host side test cases"> + <option name="test-suite-tag" value="sharedlibs_host_tests" /> + <option name="test-suite-tag" value="apct" /> + <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> + <test class="com.android.tradefed.testtype.HostTest" > + <option name="jar" value="sharedlibs_host_tests.jar" /> + </test> +</configuration> diff --git a/tests/src/com/android/tests/apex/ApexE2EBaseHostTest.java b/tests/src/com/android/tests/apex/ApexE2EBaseHostTest.java index 0812c6ac..e0871df5 100644 --- a/tests/src/com/android/tests/apex/ApexE2EBaseHostTest.java +++ b/tests/src/com/android/tests/apex/ApexE2EBaseHostTest.java @@ -21,9 +21,10 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assume.assumeTrue; +import android.cts.install.lib.host.InstallUtilsHost; import android.platform.test.annotations.RequiresDevice; -import com.android.tests.util.ModuleTestUtils; +import com.android.tests.rollback.host.AbandonSessionsRule; import com.android.tradefed.config.Option; import com.android.tradefed.config.Option.Importance; import com.android.tradefed.device.ITestDevice.ApexInfo; @@ -33,6 +34,7 @@ import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; import org.junit.After; import org.junit.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import java.io.File; @@ -52,8 +54,11 @@ public abstract class ApexE2EBaseHostTest extends BaseHostJUnit4Test { private static final String USERSPACE_REBOOT_SUPPORTED_PROP = "init.userspace_reboot.is_supported"; - /* protected so that derived tests can have access to test utils automatically */ - protected final ModuleTestUtils mUtils = new ModuleTestUtils(this); + // Protected so that derived tests can have access to test utils automatically + protected final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); + + @Rule + public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); @Option(name = OPTION_APEX_FILE_NAME, description = "The file name of the apex module.", @@ -64,15 +69,13 @@ public abstract class ApexE2EBaseHostTest extends BaseHostJUnit4Test { @Before public void setUp() throws Exception { - assumeTrue("Updating APEX is not supported", mUtils.isApexUpdateSupported()); - mUtils.abandonActiveStagedSession(); + assumeTrue("Updating APEX is not supported", mHostUtils.isApexUpdateSupported()); uninstallAllApexes(); } @After public void tearDown() throws Exception { - assumeTrue("Updating APEX is not supported", mUtils.isApexUpdateSupported()); - mUtils.abandonActiveStagedSession(); + assumeTrue("Updating APEX is not supported", mHostUtils.isApexUpdateSupported()); uninstallAllApexes(); } @@ -105,35 +108,23 @@ public abstract class ApexE2EBaseHostTest extends BaseHostJUnit4Test { private void uninstallAllApexes() throws Exception { for (String filename : getAllApexFilenames()) { - ApexInfo apex = mUtils.getApexInfo(mUtils.getTestFile(filename)); + ApexInfo apex = mHostUtils.getApexInfo(mHostUtils.getTestFile(filename)); uninstallApex(apex.name); } } protected final ApexInfo installApex(String filename) throws Exception { - File testAppFile = mUtils.getTestFile(filename); + File testAppFile = mHostUtils.getTestFile(filename); - String installResult = getDevice().installPackage(testAppFile, false, "--wait"); + String installResult = mHostUtils.installStagedPackage(testAppFile); assertWithMessage("failed to install test app %s. Reason: %s", filename, installResult) .that(installResult).isNull(); - ApexInfo testApexInfo = mUtils.getApexInfo(testAppFile); + ApexInfo testApexInfo = mHostUtils.getApexInfo(testAppFile); Assert.assertNotNull(testApexInfo); return testApexInfo; } - protected final void installApexes(String... filenames) throws Exception { - // We don't use the installApex method from the super class here, because that won't install - // the two apexes into the same session. - String[] args = new String[filenames.length + 1]; - args[0] = "install-multi-package"; - for (int i = 0; i < filenames.length; i++) { - args[i + 1] = mUtils.getTestFile(filenames[i]).getAbsolutePath(); - } - String stdout = getDevice().executeAdbCommand(args); - assertThat(stdout).isNotNull(); - } - protected final void reboot(boolean userspaceReboot) throws Exception { if (userspaceReboot) { assertThat(getDevice().setProperty("test.userspace_reboot.requested", "1")).isTrue(); diff --git a/tests/src/com/android/tests/apex/ApexRollbackTests.java b/tests/src/com/android/tests/apex/ApexRollbackTests.java index 6cf54e1f..68f82ca6 100644 --- a/tests/src/com/android/tests/apex/ApexRollbackTests.java +++ b/tests/src/com/android/tests/apex/ApexRollbackTests.java @@ -22,16 +22,18 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; -import com.android.tests.util.ModuleTestUtils; +import android.cts.install.lib.host.InstallUtilsHost; + +import com.android.tests.rollback.host.AbandonSessionsRule; +import com.android.tradefed.device.DeviceNotAvailableException; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.device.ITestDevice.ApexInfo; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; -import com.android.tradefed.util.CommandResult; -import com.android.tradefed.util.CommandStatus; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,13 +46,20 @@ import java.util.Set; */ @RunWith(DeviceJUnit4ClassRunner.class) public class ApexRollbackTests extends BaseHostJUnit4Test { - private final ModuleTestUtils mUtils = new ModuleTestUtils(this); + private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); + @Rule + public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); + + private boolean mWasAdbRoot = false; @Before public void setUp() throws Exception { - mUtils.abandonActiveStagedSession(); - mUtils.uninstallShimApexIfNecessary(); + mHostUtils.uninstallShimApexIfNecessary(); resetProperties(); + mWasAdbRoot = getDevice().isAdbRoot(); + if (!mWasAdbRoot) { + assumeTrue("Requires root", getDevice().enableAdbRoot()); + } } /** @@ -59,9 +68,11 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @After public void tearDown() throws Exception { - mUtils.abandonActiveStagedSession(); - mUtils.uninstallShimApexIfNecessary(); + mHostUtils.uninstallShimApexIfNecessary(); resetProperties(); + if (!mWasAdbRoot) { + getDevice().disableAdbRoot(); + } } private void resetProperties() throws Exception { @@ -81,9 +92,15 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testAutomaticBootLoopRecovery() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); - - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + ITestDevice device = getDevice(); + // Skip this test if there is already crashing process on device + boolean hasCrashingProcess = + device.getBooleanProperty("sys.init.updatable_crashing", false); + String crashingProcess = device.getProperty("sys.init.updatable_crashing_process_name"); + assumeFalse( + "Device already has a crashing process: " + crashingProcess, hasCrashingProcess); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); // To simulate an apex update that causes a boot loop, we install a // trigger_watchdog.rc file that arranges for a trigger_watchdog.sh @@ -92,10 +109,9 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { // persist.debug.trigger_watchdog.apex is installed. If so, // trigger_watchdog.sh repeatedly kills the system server causing a // boot loop. - ITestDevice device = getDevice(); assertThat(device.setProperty("persist.debug.trigger_watchdog.apex", "com.android.apex.cts.shim@2")).isTrue(); - String error = device.installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertThat(error).isNull(); String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " @@ -129,10 +145,10 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testSessionNotRevertedWithCheckpointingDisabled() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); - assumeFalse("Fs checkpointing is enabled", supportsFsCheckpointing()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeFalse("Fs checkpointing is enabled", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); ITestDevice device = getDevice(); assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", @@ -140,7 +156,7 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { assertThat(device.setProperty("debug.trigger_reboot_once_after_activation", "1")).isTrue(); - String error = device.installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertThat(error).isNull(); String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " @@ -153,12 +169,8 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); - String stagedSessionsInfo = device.executeShellCommand("pm get-stagedsessions"); - for (String line: stagedSessionsInfo.split("[\\r\\n]+")) { - if (line.contains(sessionIdToCheck)) { - assertThat(line).contains("isApplied = true"); - } - } + String stagedSessionInfo = getStagedSession(sessionIdToCheck); + assertThat(stagedSessionInfo).contains("isApplied = true"); Set<ApexInfo> activatedApexes = device.getActiveApexes(); assertThat(activatedApexes).contains(ctsShimV2); @@ -171,17 +183,17 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testCheckpointingRevertsSession() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); - assumeTrue("Device doesn't support fs checkpointing", supportsFsCheckpointing()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); ITestDevice device = getDevice(); assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", "com.android.apex.cts.shim@2.apex")).isTrue(); assertThat(device.setProperty("persist.debug.trigger_reboot_twice_after_activation", "1")).isTrue(); - String error = device.installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertThat(error).isNull(); String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " @@ -195,12 +207,8 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); - String stagedSessionsInfo = device.executeShellCommand("pm get-stagedsessions"); - for (String line: stagedSessionsInfo.split("[\\r\\n]+")) { - if (line.contains(sessionIdToCheck)) { - assertThat(line).contains("isFailed = true"); - } - } + String stagedSessionInfo = getStagedSession(sessionIdToCheck); + assertThat(stagedSessionInfo).contains("isFailed = true"); Set<ApexInfo> activatedApexes = device.getActiveApexes(); assertThat(activatedApexes).contains(ctsShimV1); @@ -213,17 +221,17 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testRebootingOnceDoesNotRevertSession() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); - assumeTrue("Device doesn't support fs checkpointing", supportsFsCheckpointing()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); ITestDevice device = getDevice(); assertThat(device.setProperty("persist.debug.trigger_reboot_after_activation", "com.android.apex.cts.shim@2.apex")).isTrue(); assertThat(device.setProperty("debug.trigger_reboot_once_after_activation", "1")).isTrue(); - String error = device.installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertThat(error).isNull(); String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions --only-ready " @@ -237,12 +245,8 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); - String stagedSessionsInfo = device.executeShellCommand("pm get-stagedsessions"); - for (String line: stagedSessionsInfo.split("[\\r\\n]+")) { - if (line.contains(sessionIdToCheck)) { - assertThat(line).contains("isApplied = true"); - } - } + String stagedSessionInfo = getStagedSession(sessionIdToCheck); + assertThat(stagedSessionInfo).contains("isApplied = true"); Set<ApexInfo> activatedApexes = device.getActiveApexes(); assertThat(activatedApexes).contains(ctsShimV2); @@ -257,7 +261,7 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testApexdDoesNotBootLoopDeviceIfThereIsNothingToRevert() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); // On next boot trigger setprop sys.init.updatable_crashing 1, which will trigger a // revert mechanism in apexd. Since there is nothing to revert, this should be a no-op // and device will boot successfully. @@ -276,16 +280,16 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testFailingUserspaceReboot_doesNotRevertUpdate() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device doesn't support userspace reboot", getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); - assumeTrue("Device doesn't support fs checkpointing", supportsFsCheckpointing()); + assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); // Simulate failure in userspace reboot by triggering a full reboot in the middle of the // boot sequence. assertThat(getDevice().setProperty("test.apex_revert_test_force_reboot", "1")).isTrue(); - String error = getDevice().installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( error).isNull(); // After we reboot the device, apexd will apply the update @@ -305,17 +309,17 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testUserspaceRebootFailedShutdownSequence_doesNotRevertUpdate() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device doesn't support userspace reboot", getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); - assumeTrue("Device doesn't support fs checkpointing", supportsFsCheckpointing()); + assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); // Simulate failure in userspace reboot by triggering a full reboot in the middle of the // boot sequence. assertThat(getDevice().setProperty("test.apex_userspace_reboot_simulate_shutdown_failed", "1")).isTrue(); - String error = getDevice().installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( error).isNull(); // After the userspace reboot started, we simulate it's failure by rebooting device during @@ -338,17 +342,17 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testUserspaceRebootFailedRemount_revertsUpdate() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device doesn't support userspace reboot", getDevice().getBooleanProperty("init.userspace_reboot.is_supported", false)); - assumeTrue("Device doesn't support fs checkpointing", supportsFsCheckpointing()); + assumeTrue("Device doesn't support fs checkpointing", mHostUtils.isCheckpointSupported()); - File apexFile = mUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); // Simulate failure in userspace reboot by triggering a full reboot in the middle of the // boot sequence. assertThat(getDevice().setProperty("test.apex_userspace_reboot_simulate_remount_failed", "1")).isTrue(); - String error = getDevice().installPackage(apexFile, false, "--wait"); + String error = mHostUtils.installStagedPackage(apexFile); assertWithMessage("Failed to stage com.android.apex.cts.shim.v2.apex : %s", error).that( error).isNull(); // After we reboot the device, apexd will apply the update @@ -367,14 +371,14 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { */ @Test public void testBootCompletedCleanupHappensEvenWhenThereIsCrashingProcess() throws Exception { - assumeTrue("Device does not support updating APEX", mUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); try { // On next boot trigger setprop sys.init.updatable_crashing 1, which will trigger a // revert mechanism in apexd. Since there is nothing to revert, this should be a no-op // and device will boot successfully. getDevice().setProperty("persist.debug.trigger_updatable_crashing_for_testing", "1"); - assertThat(getDevice().pushFile(mUtils.getTestFile("apex.apexd_test_v2.apex"), + assertThat(getDevice().pushFile(mHostUtils.getTestFile("apex.apexd_test_v2.apex"), "/data/apex/active/apexd_test_v2.apex")).isTrue(); getDevice().reboot(); assertWithMessage("Timed out waiting for device to boot").that( @@ -386,18 +390,84 @@ public class ApexRollbackTests extends BaseHostJUnit4Test { ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( "com.android.apex.cts.shim", 2L); assertThat(activeApexes).doesNotContain(testApex); - mUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", + mHostUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", Duration.ofMinutes(3)); } finally { getDevice().executeShellV2Command("rm /data/apex/active/apexd_test_v2.apex"); } } + /** + * Test reason for revert is properly logged during boot loops + */ + @Test + public void testReasonForRevertIsLoggedDuringBootloop() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Fs checkpointing is enabled", mHostUtils.isCheckpointSupported()); + + ITestDevice device = getDevice(); + // Skip this test if there is already crashing process on device + final boolean hasCrashingProcess = + device.getBooleanProperty("sys.init.updatable_crashing", false); + final String crashingProcess = + device.getProperty("sys.init.updatable_crashing_process_name"); + assumeFalse( + "Device already has a crashing process: " + crashingProcess, hasCrashingProcess); + final File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + + // To simulate an apex update that causes a boot loop, we install a + // trigger_watchdog.rc file that arranges for a trigger_watchdog.sh + // script to be run at boot. The trigger_watchdog.sh script checks if + // the apex version specified in the property + // persist.debug.trigger_watchdog.apex is installed. If so, + // trigger_watchdog.sh repeatedly kills the system server causing a + // boot loop. + assertThat(device.setProperty("persist.debug.trigger_watchdog.apex", + "com.android.apex.cts.shim@2")).isTrue(); + final String error = mHostUtils.installStagedPackage(apexFile); + assertThat(error).isNull(); + + final String sessionIdToCheck = device.executeShellCommand("pm get-stagedsessions " + + "--only-ready --only-parent --only-sessionid").trim(); + assertThat(sessionIdToCheck).isNotEmpty(); + + // After we reboot the device, we expect the device to go into boot + // loop from trigger_watchdog.sh. Native watchdog should detect and + // report the boot loop, causing apexd to roll back to the previous + // version of the apex and force reboot. When the device comes up + // after the forced reboot, trigger_watchdog.sh will see the different + // version of the apex and refrain from forcing a boot loop, so the + // device will be recovered. + device.reboot(); + + final ApexInfo ctsShimV1 = new ApexInfo("com.android.apex.cts.shim", 1L); + final ApexInfo ctsShimV2 = new ApexInfo("com.android.apex.cts.shim", 2L); + final Set<ApexInfo> activatedApexes = device.getActiveApexes(); + assertThat(activatedApexes).contains(ctsShimV1); + assertThat(activatedApexes).doesNotContain(ctsShimV2); + + // Assert that a session has failed with the expected reason + final String stagedSessionString = getStagedSession(sessionIdToCheck); + assertThat(stagedSessionString).contains("Session reverted due to crashing native process"); + } - private boolean supportsFsCheckpointing() throws Exception { - CommandResult result = getDevice().executeShellV2Command("sm supports-checkpoint"); - assertWithMessage("Failed to check if fs checkpointing is supported : %s", - result.getStderr()).that(result.getStatus()).isEqualTo(CommandStatus.SUCCESS); - return "true".equals(result.getStdout().trim()); + String getStagedSession(String sessionId) throws DeviceNotAvailableException { + final String[] lines = getDevice().executeShellCommand( + "pm get-stagedsessions").split("\n"); + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("sessionId = " + sessionId + ";")) { + // Join all lines realted to this session + final StringBuilder result = new StringBuilder(lines[i]); + for (int j = i + 1; j < lines.length; j++) { + if (lines[j].startsWith("sessionId = ")) { + // A new session block has started + break; + } + result.append(lines[j]); + } + return result.toString(); + } + } + return ""; } } diff --git a/tests/src/com/android/tests/apex/ApexdHostTest.java b/tests/src/com/android/tests/apex/ApexdHostTest.java index fc1fa4be..2d72e9fd 100644 --- a/tests/src/com/android/tests/apex/ApexdHostTest.java +++ b/tests/src/com/android/tests/apex/ApexdHostTest.java @@ -21,17 +21,27 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assume.assumeTrue; -import com.android.tests.util.ModuleTestUtils; +import android.cts.install.lib.host.InstallUtilsHost; + +import com.android.apex.ApexInfo; +import com.android.apex.XmlParser; +import com.android.tests.rollback.host.AbandonSessionsRule; import com.android.tradefed.device.ITestDevice; import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; +import java.io.FileInputStream; import java.time.Duration; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * Host side integration tests for apexd. @@ -41,14 +51,36 @@ public class ApexdHostTest extends BaseHostJUnit4Test { private static final String SHIM_APEX_PATH = "/system/apex/com.android.apex.cts.shim.apex"; - private final ModuleTestUtils mTestUtils = new ModuleTestUtils(this); + private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); + + @Rule + public AbandonSessionsRule mHostTestRule = new AbandonSessionsRule(this); + + private boolean mWasAdbRoot = false; + + @Before + public void setUp() throws Exception { + mHostUtils.uninstallShimApexIfNecessary(); + mWasAdbRoot = getDevice().isAdbRoot(); + if (!mWasAdbRoot) { + assumeTrue("Device requires root", getDevice().enableAdbRoot()); + } + } + + @After + public void tearDown() throws Exception { + mHostUtils.uninstallShimApexIfNecessary(); + if (!mWasAdbRoot) { + getDevice().disableAdbRoot(); + } + } @Test public void testOrphanedApexIsNotActivated() throws Exception { - assumeTrue("Device does not support updating APEX", mTestUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); try { - assertThat(getDevice().pushFile(mTestUtils.getTestFile("apex.apexd_test_v2.apex"), + assertThat(getDevice().pushFile(mHostUtils.getTestFile("apex.apexd_test_v2.apex"), "/data/apex/active/apexd_test_v2.apex")).isTrue(); getDevice().reboot(); assertWithMessage("Timed out waiting for device to boot").that( @@ -57,7 +89,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( "com.android.apex.test_package", 2L); assertThat(activeApexes).doesNotContain(testApex); - mTestUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", + mHostUtils.waitForFileDeleted("/data/apex/active/apexd_test_v2.apex", Duration.ofMinutes(3)); } finally { getDevice().executeShellV2Command("rm /data/apex/active/apexd_test_v2.apex"); @@ -65,11 +97,11 @@ public class ApexdHostTest extends BaseHostJUnit4Test { } @Test public void testApexWithoutPbIsNotActivated() throws Exception { - assumeTrue("Device does not support updating APEX", mTestUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); final String testApexFile = "com.android.apex.cts.shim.v2_no_pb.apex"; try { - assertThat(getDevice().pushFile(mTestUtils.getTestFile(testApexFile), + assertThat(getDevice().pushFile(mHostUtils.getTestFile(testApexFile), "/data/apex/active/" + testApexFile)).isTrue(); getDevice().reboot(); assertWithMessage("Timed out waiting for device to boot").that( @@ -78,7 +110,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( "com.android.apex.cts.shim", 2L); assertThat(activeApexes).doesNotContain(testApex); - mTestUtils.waitForFileDeleted("/data/apex/active/" + testApexFile, + mHostUtils.waitForFileDeleted("/data/apex/active/" + testApexFile, Duration.ofMinutes(3)); } finally { getDevice().executeShellV2Command("rm /data/apex/active/" + testApexFile); @@ -87,14 +119,14 @@ public class ApexdHostTest extends BaseHostJUnit4Test { @Test public void testRemountApex() throws Exception { - assumeTrue("Device does not support updating APEX", mTestUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); final File oldFile = getDevice().pullFile(SHIM_APEX_PATH); try { getDevice().remountSystemWritable(); // In case remount requires a reboot, wait for boot to complete. getDevice().waitForBootComplete(Duration.ofMinutes(3).toMillis()); - final File newFile = mTestUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + final File newFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); // Stop framework getDevice().executeShellV2Command("stop"); // Push new shim APEX. This simulates adb sync. @@ -118,7 +150,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { @Test public void testApexWithoutPbIsNotActivated_ProductPartitionHasOlderVersion() throws Exception { - assumeTrue("Device does not support updating APEX", mTestUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); try { @@ -127,10 +159,10 @@ public class ApexdHostTest extends BaseHostJUnit4Test { assertWithMessage("Timed out waiting for device to boot").that( getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); - final File v1 = mTestUtils.getTestFile("apex.apexd_test.apex"); + final File v1 = mHostUtils.getTestFile("apex.apexd_test.apex"); getDevice().pushFile(v1, "/product/apex/apex.apexd_test.apex"); - final File v2_no_pb = mTestUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); + final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex"); getDevice().reboot(); @@ -144,7 +176,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { "com.android.apex.test_package", 2L)); // v2_no_pb should be deleted - mTestUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", + mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", Duration.ofMinutes(3)); } finally { getDevice().remountSystemWritable(); @@ -159,7 +191,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { @Test public void testApexWithoutPbIsNotActivated_ProductPartitionHasNewerVersion() throws Exception { - assumeTrue("Device does not support updating APEX", mTestUtils.isApexUpdateSupported()); + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); assumeTrue("Device requires root", getDevice().isAdbRoot()); try { @@ -168,10 +200,10 @@ public class ApexdHostTest extends BaseHostJUnit4Test { assertWithMessage("Timed out waiting for device to boot").that( getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); - final File v3 = mTestUtils.getTestFile("apex.apexd_test_v3.apex"); + final File v3 = mHostUtils.getTestFile("apex.apexd_test_v3.apex"); getDevice().pushFile(v3, "/product/apex/apex.apexd_test_v3.apex"); - final File v2_no_pb = mTestUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); + final File v2_no_pb = mHostUtils.getTestFile("apex.apexd_test_v2_no_pb.apex"); getDevice().pushFile(v2_no_pb, "/data/apex/active/apex.apexd_test_v2_no_pb.apex"); getDevice().reboot(); @@ -185,7 +217,7 @@ public class ApexdHostTest extends BaseHostJUnit4Test { "com.android.apex.test_package", 2L)); // v2_no_pb should be deleted - mTestUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", + mHostUtils.waitForFileDeleted("/data/apex/active/apex.apexd_test_v2_no_pb.apex", Duration.ofMinutes(3)); } finally { getDevice().remountSystemWritable(); @@ -196,4 +228,115 @@ public class ApexdHostTest extends BaseHostJUnit4Test { getDevice().executeShellV2Command("rm /data/apex/active/apex.apexd_test_v2_no_pb.apex"); } } + + @Test + public void testApexInfoListIsValid() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + try (FileInputStream fis = new FileInputStream( + getDevice().pullFile("/apex/apex-info-list.xml"))) { + // #1 Data got from apexd via binder + Set<ITestDevice.ApexInfo> fromApexd = getDevice().getActiveApexes(); + // #2 Data got from the xml file (key is the path) + Map<String, ApexInfo> fromXml = XmlParser.readApexInfoList(fis).getApexInfo().stream() + .collect(Collectors.toMap(ApexInfo::getModulePath, ai -> ai)); + + // Make sure that all items in #1 are also in #2 and they are identical + for (ITestDevice.ApexInfo ai : fromApexd) { + ApexInfo apexFromXml = fromXml.get(ai.sourceDir); + assertWithMessage("APEX (" + ai.toString() + ") is not found in the list") + .that(apexFromXml).isNotNull(); + assertWithMessage("Version mismatch for APEX (" + ai.toString() + ")") + .that(ai.versionCode).isEqualTo(apexFromXml.getVersionCode()); + assertWithMessage("APEX (" + ai.toString() + ") is not active") + .that(apexFromXml.getIsActive()).isTrue(); + } + } + } + + /** + * Test to verify that the state of a staged session does not change if apexd is stopped and + * restarted while a session is staged. + */ + @Test + public void testApexSessionStateUnchangedBeforeReboot() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + File apexFile = mHostUtils.getTestFile("com.android.apex.cts.shim.v2.apex"); + String error = mHostUtils.installStagedPackage(apexFile); + assertThat(error).isNull(); + String sessionId = getDevice().executeShellCommand( + "pm get-stagedsessions --only-ready --only-parent --only-sessionid").trim(); + assertThat(sessionId).isNotEmpty(); + String sessionStateCmd = "cmd -w apexservice getStagedSessionInfo " + sessionId; + String initialState = getDevice().executeShellV2Command(sessionStateCmd).getStdout(); + assertThat(initialState).isNotEmpty(); + + // Kill apexd. This means apexd will perform its start logic when the second install + // is staged. + getDevice().executeShellV2Command("kill `pidof apexd`"); + + // Verify that the session state remains consistent after apexd has restarted. + String updatedState = getDevice().executeShellV2Command(sessionStateCmd).getStdout(); + assertThat(updatedState).isEqualTo(initialState); + } + + /** + * Verifies that content of {@code /data/apex/sessions/} is migrated to the {@code + * /metadata/apex/sessions}. + */ + @Test + public void testSessionsDirMigrationToMetadata() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + try { + getDevice().executeShellV2Command("mkdir -p /data/apex/sessions/1543"); + File file = File.createTempFile("foo", "bar"); + getDevice().pushFile(file, "/data/apex/sessions/1543/file"); + + // During boot sequence apexd will move /data/apex/sessions/1543/file to + // /metadata/apex/sessions/1543/file. + getDevice().reboot(); + assertWithMessage("Timed out waiting for device to boot").that( + getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); + + assertThat(getDevice().doesFileExist("/metadata/apex/sessions/1543/file")).isTrue(); + assertThat(getDevice().doesFileExist("/data/apex/sessions/1543/file")).isFalse(); + } finally { + getDevice().executeShellV2Command("rm -R /data/apex/sessions/1543"); + getDevice().executeShellV2Command("rm -R /metadata/apex/sessions/1543"); + } + } + + @Test + public void testFailsToActivateApexOnDataFallbacksToPreInstalled() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + try { + final File file = + mHostUtils.getTestFile("com.android.apex.cts.shim.v2_additional_file.apex"); + getDevice().pushFile(file, "/data/apex/active/com.android.apex.cts.shim@2.apex"); + + getDevice().reboot(); + assertWithMessage("Timed out waiting for device to boot").that( + getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); + + // After reboot pre-installed version of shim apex should be activated, and corrupted + // version on /data should be deleted. + final Set<ITestDevice.ApexInfo> activeApexes = getDevice().getActiveApexes(); + ITestDevice.ApexInfo testApex = new ITestDevice.ApexInfo( + "com.android.apex.cts.shim", 1L); + assertThat(activeApexes).contains(testApex); + assertThat( + getDevice() + .doesFileExist("/data/apex/active/com.android.apex.cts.shim@2.apex")) + .isFalse(); + } finally { + getDevice().deleteFile("/data/apex/active/com.android.apex.cts.shim@2.apex"); + } + } } diff --git a/tests/src/com/android/tests/apex/MediaProviderHostTest.java b/tests/src/com/android/tests/apex/MediaProviderHostTest.java deleted file mode 100644 index 4fc72251..00000000 --- a/tests/src/com/android/tests/apex/MediaProviderHostTest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class MediaProviderHostTest extends ApexE2EBaseHostTest { -} diff --git a/tests/src/com/android/tests/apex/NeuralNetworksHostTest.java b/tests/src/com/android/tests/apex/NeuralNetworksHostTest.java deleted file mode 100644 index 1effbe2a..00000000 --- a/tests/src/com/android/tests/apex/NeuralNetworksHostTest.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class NeuralNetworksHostTest extends ApexE2EBaseHostTest { -} diff --git a/tests/src/com/android/tests/apex/PermissionHostTest.java b/tests/src/com/android/tests/apex/PermissionHostTest.java deleted file mode 100644 index c673ade9..00000000 --- a/tests/src/com/android/tests/apex/PermissionHostTest.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.Ignore; -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@Ignore("b/152717947") -@RunWith(DeviceJUnit4ClassRunner.class) -public class PermissionHostTest extends ApexE2EBaseHostTest { -} diff --git a/tests/src/com/android/tests/apex/SharedLibsApexTest.java b/tests/src/com/android/tests/apex/SharedLibsApexTest.java new file mode 100644 index 00000000..9097e3a2 --- /dev/null +++ b/tests/src/com/android/tests/apex/SharedLibsApexTest.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.tests.apex; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; + +import android.cts.install.lib.host.InstallUtilsHost; + +import com.android.compatibility.common.util.CpuFeatures; +import com.android.internal.util.test.SystemPreparer; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + + +@RunWith(DeviceJUnit4ClassRunner.class) +public class SharedLibsApexTest extends BaseHostJUnit4Test { + + private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); + private final TemporaryFolder mTemporaryFolder = new TemporaryFolder(); + private final SystemPreparer mPreparer = new SystemPreparer(mTemporaryFolder, + this::getDevice); + + @Rule + public final RuleChain ruleChain = RuleChain.outerRule(mTemporaryFolder).around(mPreparer); + + enum ApexName { + FOO, + BAR, + BAZ, + PONY, + SHAREDLIBS, + SHAREDLIBS_SECONDARY + } + + enum ApexVersion { + ONE, + TWO + } + + enum ApexType { + DEFAULT, + STRIPPED + } + + enum SharedLibsVersion { + X, + Y, + Z + } + + /** + * Utility function to generate test apex names in the form e.g.: + * "com.android.apex.test.bar.v1.libvX.apex" + */ + private String getTestApex(ApexName apexName, ApexType apexType, ApexVersion apexVersion, + SharedLibsVersion sharedLibsVersion) { + StringBuilder ret = new StringBuilder(); + ret.append("com.android.apex.test."); + switch(apexName) { + case FOO: + ret.append("foo"); + break; + case BAR: + ret.append("bar"); + break; + case BAZ: + ret.append("baz"); + break; + case PONY: + ret.append("pony"); + break; + case SHAREDLIBS: + ret.append("sharedlibs_generated"); + break; + case SHAREDLIBS_SECONDARY: + ret.append("sharedlibs_secondary_generated"); + break; + } + + switch(apexType) { + case STRIPPED: + ret.append("_stripped"); + break; + case DEFAULT: + break; + } + + switch(apexVersion) { + case ONE: + ret.append(".v1"); + break; + case TWO: + ret.append(".v2"); + break; + } + + switch(sharedLibsVersion) { + case X: + ret.append(".libvX.apex"); + break; + case Y: + ret.append(".libvY.apex"); + break; + case Z: + ret.append(".libvZ.apex"); + break; + } + + return ret.toString(); + } + + /** + * Tests basic functionality of two apex packages being force-installed and the C++ binaries + * contained in them being executed correctly. + */ + @Test + public void testInstallAndRunDefaultApexs() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + for (String apex : new String[]{ + getTestApex(ApexName.BAR, ApexType.DEFAULT, ApexVersion.ONE, SharedLibsVersion.X), + getTestApex(ApexName.FOO, ApexType.DEFAULT, ApexVersion.ONE, SharedLibsVersion.X), + getTestApex(ApexName.PONY, ApexType.DEFAULT, ApexVersion.ONE, SharedLibsVersion.Z), + }) { + mPreparer.pushResourceFile(apex, + "/system/apex/" + apex); + } + mPreparer.reboot(); + + getDevice().disableAdbRoot(); + String runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.foo/bin/foo_test"); + assertThat(runAsResult).isEqualTo("FOO_VERSION_1 SHARED_LIB_VERSION_X"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test32"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_1 SHARED_LIB_VERSION_X"); + if (CpuFeatures.isX86_64(getDevice()) || CpuFeatures.isArm64(getDevice())) { + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test64"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_1 SHARED_LIB_VERSION_X"); + } + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.pony/bin/pony_test"); + assertThat(runAsResult).isEqualTo("PONY_VERSION_1 SHARED_LIB_VERSION_Z"); + + mPreparer.stageMultiplePackages( + new String[]{ + getTestApex(ApexName.BAR, ApexType.DEFAULT, ApexVersion.TWO, SharedLibsVersion.Y), + getTestApex(ApexName.FOO, ApexType.DEFAULT, ApexVersion.TWO, SharedLibsVersion.Y), + }, + new String[] { + "com.android.apex.test.bar", + "com.android.apex.test.foo", + }).reboot(); + + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.foo/bin/foo_test"); + assertThat(runAsResult).isEqualTo("FOO_VERSION_2 SHARED_LIB_VERSION_Y"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test32"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + if (CpuFeatures.isX86_64(getDevice()) || CpuFeatures.isArm64(getDevice())) { + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test64"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + } + } + + /** + * Tests functionality of shared libraries apex: installs two apexs "stripped" of libc++.so and + * one apex containing it and verifies that C++ binaries can run. + */ + @Test + public void testInstallAndRunOptimizedApexs() throws Exception { + assumeTrue("Device does not support updating APEX", mHostUtils.isApexUpdateSupported()); + assumeTrue("Device requires root", getDevice().isAdbRoot()); + + // Pre-installed on /system: + // package bar version 1 using library version X + // package foo version 1 using library version X + // package sharedlibs version 1 exporting library version X + // + // package pony version 1 using library version Z + // package sharedlibs_secondary version 1 exporting library version Z + + for (String apex : new String[]{ + getTestApex(ApexName.BAR, ApexType.STRIPPED, ApexVersion.ONE, SharedLibsVersion.X), + getTestApex(ApexName.FOO, ApexType.STRIPPED, ApexVersion.ONE, SharedLibsVersion.X), + getTestApex(ApexName.PONY, ApexType.STRIPPED, ApexVersion.ONE, SharedLibsVersion.Z), + getTestApex(ApexName.SHAREDLIBS, ApexType.DEFAULT, ApexVersion.ONE, + SharedLibsVersion.X), + getTestApex(ApexName.SHAREDLIBS_SECONDARY, ApexType.DEFAULT, ApexVersion.ONE, + SharedLibsVersion.Z), + }) { + mPreparer.pushResourceFile(apex, + "/system/apex/" + apex); + } + mPreparer.reboot(); + + getDevice().disableAdbRoot(); + String runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.foo/bin/foo_test"); + assertThat(runAsResult).isEqualTo("FOO_VERSION_1 SHARED_LIB_VERSION_X"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test32"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_1 SHARED_LIB_VERSION_X"); + if (CpuFeatures.isX86_64(getDevice()) || CpuFeatures.isArm64(getDevice())) { + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test64"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_1 SHARED_LIB_VERSION_X"); + } + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.pony/bin/pony_test"); + assertThat(runAsResult).isEqualTo("PONY_VERSION_1 SHARED_LIB_VERSION_Z"); + + // Updated packages (installed on /data/apex/active): + // package bar version 2 using library version Y <-- new + // package foo version 2 using library version Y <-- new + // package sharedlibs version 2 exporting library version Y <-- new + // + // Pre-installed: + // (inactive) package bar version 1 using library version X + // (inactive) package foo version 1 using library version X + // package sharedlibs version 1 exporting library version X + // + // package pony version 1 using library version Z + // package sharedlibs_secondary version 1 exporting library version Z + + mPreparer.stageMultiplePackages( + new String[]{ + getTestApex(ApexName.BAR, ApexType.STRIPPED, ApexVersion.TWO, SharedLibsVersion.Y), + getTestApex(ApexName.FOO, ApexType.STRIPPED, ApexVersion.TWO, SharedLibsVersion.Y), + getTestApex(ApexName.SHAREDLIBS, ApexType.DEFAULT, ApexVersion.TWO, + SharedLibsVersion.Y), + }, + new String[]{ + "com.android.apex.test.bar", + "com.android.apex.test.foo", + "com.android.apex.test.sharedlibs", + }).reboot(); + + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.foo/bin/foo_test"); + assertThat(runAsResult).isEqualTo("FOO_VERSION_2 SHARED_LIB_VERSION_Y"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test32"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + if (CpuFeatures.isX86_64(getDevice()) || CpuFeatures.isArm64(getDevice())) { + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test64"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + } + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.pony/bin/pony_test"); + assertThat(runAsResult).isEqualTo("PONY_VERSION_1 SHARED_LIB_VERSION_Z"); + + // Assume that an OTA now adds a package baz on /system needing libraries installed on + // /system: + // + // Updated packages (installed on /data/apex/active): + // package bar version 2 using library version Y + // package foo version 2 using library version Y + // package sharedlibs version 2 exporting library version Y + // + // Pre-installed: + // (inactive) package bar version 1 using library version X + // package baz version 1 using library version X <-- new + // (inactive) package foo version 1 using library version X + // package sharedlibs version 1 exporting library version X + // package pony version 1 using library version Z + // package sharedlibs_secondary version 1 exporting library version Z + + String baz_apex = + getTestApex(ApexName.BAZ, ApexType.STRIPPED, ApexVersion.ONE, SharedLibsVersion.X); + mPreparer.pushResourceFile(baz_apex, "/system/apex/" + baz_apex); + mPreparer.reboot(); + + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.foo/bin/foo_test"); + assertThat(runAsResult).isEqualTo("FOO_VERSION_2 SHARED_LIB_VERSION_Y"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test32"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + if (CpuFeatures.isX86_64(getDevice()) || CpuFeatures.isArm64(getDevice())) { + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.bar/bin/bar_test64"); + assertThat(runAsResult).isEqualTo("BAR_VERSION_2 SHARED_LIB_VERSION_Y"); + } + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.baz/bin/baz_test"); + assertThat(runAsResult).isEqualTo("BAZ_VERSION_1 SHARED_LIB_VERSION_X"); + runAsResult = getDevice().executeShellCommand( + "/apex/com.android.apex.test.pony/bin/pony_test"); + assertThat(runAsResult).isEqualTo("PONY_VERSION_1 SHARED_LIB_VERSION_Z"); + } +} diff --git a/tests/src/com/android/tests/apex/StatsdHostTest.java b/tests/src/com/android/tests/apex/StatsdHostTest.java deleted file mode 100644 index 4ad3bb8d..00000000 --- a/tests/src/com/android/tests/apex/StatsdHostTest.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.Ignore; -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -@Ignore // TODO(b/150452752): unignore when test is fixed. -public class StatsdHostTest extends ApexE2EBaseHostTest { -} diff --git a/tests/src/com/android/tests/apex/WifiHostTest.java b/tests/src/com/android/tests/apex/WifiHostTest.java deleted file mode 100644 index 7fd85289..00000000 --- a/tests/src/com/android/tests/apex/WifiHostTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.apex; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; - -import org.junit.runner.RunWith; - -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class WifiHostTest extends ApexE2EBaseHostTest { - // TODO(b/146163587): Add more checks to verify apex installation. -} diff --git a/tests/src/com/android/tests/apex/host/ApexCompressionTests.java b/tests/src/com/android/tests/apex/host/ApexCompressionTests.java new file mode 100644 index 00000000..a09ca60d --- /dev/null +++ b/tests/src/com/android/tests/apex/host/ApexCompressionTests.java @@ -0,0 +1,438 @@ +/* + * Copyright (C) 2020 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. + */ + +package com.android.tests.apex.host; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import android.cts.install.lib.host.InstallUtilsHost; +import android.platform.test.annotations.LargeTest; + +import com.android.tradefed.device.DeviceNotAvailableException; +import com.android.tradefed.device.ITestDevice; +import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; +import com.android.tradefed.util.CommandResult; +import com.android.tradefed.util.CommandStatus; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Test for platform support for Apex Compression feature + */ +@RunWith(DeviceJUnit4ClassRunner.class) +public class ApexCompressionTests extends BaseHostJUnit4Test { + private static final String COMPRESSED_APEX_PACKAGE_NAME = "com.android.apex.compressed"; + private static final String ORIGINAL_APEX_FILE_NAME = + COMPRESSED_APEX_PACKAGE_NAME + ".v1_original.apex"; + private static final String DECOMPRESSED_DIR_PATH = "/data/apex/decompressed/"; + private static final String APEX_ACTIVE_DIR = "/data/apex/active/"; + private static final String OTA_RESERVED_DIR = "/data/apex/ota_reserved/"; + private static final String DECOMPRESSED_APEX_SUFFIX = ".decompressed.apex"; + + private final InstallUtilsHost mHostUtils = new InstallUtilsHost(this); + private boolean mWasAdbRoot = false; + + @Before + public void setUp() throws Exception { + mWasAdbRoot = getDevice().isAdbRoot(); + if (!mWasAdbRoot) { + assumeTrue("Requires root", getDevice().enableAdbRoot()); + } + deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + OTA_RESERVED_DIR + "*"); + } + + @After + public void tearDown() throws Exception { + if (!mWasAdbRoot) { + getDevice().disableAdbRoot(); + } + deleteFiles("/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "*apex", + OTA_RESERVED_DIR + "*"); + } + + /** + * Runs the given phase of a test by calling into the device. + * Throws an exception if the test phase fails. + * <p> + * For example, <code>runPhase("testApkOnlyEnableRollback");</code> + */ + private void runPhase(String phase) throws Exception { + assertTrue(runDeviceTests("com.android.tests.apex.app", + "com.android.tests.apex.app.ApexCompressionTests", + phase)); + } + + /** + * Deletes files and reboots the device if necessary. + * @param files the paths of files which might contain wildcards + */ + private void deleteFiles(String... files) throws Exception { + boolean found = false; + for (String file : files) { + CommandResult result = getDevice().executeShellV2Command("ls " + file); + if (result.getStatus() == CommandStatus.SUCCESS) { + found = true; + break; + } + } + + if (found) { + getDevice().remountSystemWritable(); + for (String file : files) { + getDevice().executeShellCommand("rm -rf " + file); + } + getDevice().reboot(); + } + } + + private void pushTestApex(final String fileName) throws Exception { + final File apex = mHostUtils.getTestFile(fileName); + getDevice().remountSystemWritable(); + assertTrue(getDevice().pushFile(apex, "/system/apex/" + fileName)); + getDevice().reboot(); + } + + private List<String> getFilesInDir(String baseDir) throws DeviceNotAvailableException { + return getDevice().getFileEntry(baseDir).getChildren(false) + .stream().map(entry -> entry.getName()) + .collect(Collectors.toList()); + } + + /** + * Returns the active apex info as optional. + */ + private Optional<ITestDevice.ApexInfo> getActiveApexInfo(String packageName) + throws DeviceNotAvailableException { + return getDevice().getActiveApexes().stream().filter( + apex -> apex.name.equals(packageName)).findAny(); + } + + @Test + @LargeTest + public void testDecompressedApexIsConsideredFactory() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + runPhase("testDecompressedApexIsConsideredFactory"); + } + + @Test + @LargeTest + public void testCompressedApexIsDecompressedAndActivated() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + + // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + + // Match the decompressed apex with original byte for byte + final File originalApex = mHostUtils.getTestFile(ORIGINAL_APEX_FILE_NAME); + final byte[] originalApexFileBytes = Files.readAllBytes(Paths.get(originalApex.toURI())); + final File decompressedFile = getDevice().pullFile( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" + + DECOMPRESSED_APEX_SUFFIX); + final byte[] decompressedFileBytes = + Files.readAllBytes(Paths.get(decompressedFile.toURI())); + assertThat(decompressedFileBytes).isEqualTo(originalApexFileBytes); + + // The decompressed APEX should note be hard linked to APEX_ACTIVE_DIR + files = getFilesInDir(APEX_ACTIVE_DIR); + assertThat(files).doesNotContain( + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + } + + @Test + @LargeTest + public void testDecompressedApexSurvivesReboot() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + + // Ensure that compressed APEX was activated from DECOMPRESSED_DIR_PATH + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + final File decompressedFile = getDevice().pullFile( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" + + DECOMPRESSED_APEX_SUFFIX); + final byte[] decompressedFileBytes = + Files.readAllBytes(Paths.get(decompressedFile.toURI())); + + getDevice().reboot(); + + // Ensure it gets activated again on reboot + files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + final File decompressedFileAfterReboot = getDevice().pullFile( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" + + DECOMPRESSED_APEX_SUFFIX); + final byte[] decompressedFileBytesAfterReboot = + Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); + assertThat(decompressedFileBytes).isEqualTo(decompressedFileBytesAfterReboot); + } + + @Test + @LargeTest + public void testDecompressionDoesNotHappenOnEveryReboot() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + + final String decompressedApexFilePath = DECOMPRESSED_DIR_PATH + + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX; + String lastModifiedTime1 = + getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); + + getDevice().reboot(); + getDevice().waitForDeviceAvailable(); + + String lastModifiedTime2 = + getDevice().executeShellCommand("stat -c %Y " + decompressedApexFilePath); + assertThat(lastModifiedTime1).isEqualTo(lastModifiedTime2); + } + + @Test + @LargeTest + public void testHigherVersionOnSystemTriggerDecompression() throws Exception { + // Install v1 on /system partition + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + // On boot, /data partition will have decompressed v1 APEX in it + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + + // Now replace /system APEX with v2 + getDevice().remountSystemWritable(); + getDevice().executeShellCommand("rm -rf /system/apex/" + + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); + + // Ensure that v2 was decompressed + files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@2" + DECOMPRESSED_APEX_SUFFIX); + } + + + @Test + @LargeTest + public void testDifferentRootDigestTriggersDecompression() throws Exception { + // Install v1 on /system partition + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + // On boot, /data partition will have decompressed v1 APEX in it + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + final File decompressedFile = getDevice().pullFile( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" + + DECOMPRESSED_APEX_SUFFIX); + final byte[] decompressedFileBytes = + Files.readAllBytes(Paths.get(decompressedFile.toURI())); + + // Now replace /system APEX with same version but different root digest + getDevice().remountSystemWritable(); + getDevice().executeShellCommand("rm -rf /system/apex/" + + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1_different_digest.capex"); + + // Ensure that decompressed APEX is different than before + files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + final File decompressedFileAfterReboot = getDevice().pullFile( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@1" + + DECOMPRESSED_APEX_SUFFIX); + final byte[] decompressedFileBytesAfterReboot = + Files.readAllBytes(Paths.get(decompressedFileAfterReboot.toURI())); + assertThat(decompressedFileBytes).isNotEqualTo(decompressedFileBytesAfterReboot); + } + + @Test + @LargeTest + public void testUnusedDecompressedApexIsCleanedUp_HigherVersion() throws Exception { + // Install v1 on /system partition + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + + // Now install an update for that APEX so that decompressed APEX becomes redundant + runPhase("testUnusedDecompressedApexIsCleanedUp_HigherVersion"); + getDevice().reboot(); + + // Verify that DECOMPRESSED_DIR_PATH does not contain the decompressed APEX + files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).doesNotContain( + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + } + + @Test + @LargeTest + public void testUnusedDecompressedApexIsCleanedUp_SameVersion() throws Exception { + // Install v1 on /system partition + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + // Ensure that compressed APEX was decompressed in DECOMPRESSED_DIR_PATH + List<String> files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + + // Now install an update for that APEX so that decompressed APEX becomes redundant + runPhase("testUnusedDecompressedApexIsCleanedUp_SameVersion"); + getDevice().reboot(); + + // Verify that DECOMPRESSED_DIR_PATH does not contain the decompressed APEX + files = getFilesInDir(DECOMPRESSED_DIR_PATH); + assertThat(files).doesNotContain( + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + } + + @Test + @LargeTest + public void testReservedSpaceIsNotCleanedOnReboot() throws Exception { + getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random"); + + getDevice().reboot(); + + List<String> files = getFilesInDir(OTA_RESERVED_DIR); + assertThat(files).hasSize(1); + assertThat(files).contains("random"); + } + + @Test + @LargeTest + public void testReservedSpaceIsCleanedUpOnDecompression() throws Exception { + getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random1"); + getDevice().executeShellCommand("touch " + OTA_RESERVED_DIR + "random2"); + + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + + assertThat(getFilesInDir(OTA_RESERVED_DIR)).isEmpty(); + } + + @Test + @LargeTest + public void testFailsToActivateApexOnDataFallbacksToPreInstalled() throws Exception { + // Push a data apex that will fail to activate + final File file = + mHostUtils.getTestFile("com.android.apex.compressed.v2_manifest_mismatch.apex"); + getDevice().pushFile(file, APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex"); + // Push a CAPEX which should act as the fallback + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); + assertWithMessage("Timed out waiting for device to boot").that( + getDevice().waitForBootComplete(Duration.ofMinutes(2).toMillis())).isTrue(); + + // After reboot pre-installed version of shim apex should be activated, and corrupted + // version on /data should be deleted. + final ITestDevice.ApexInfo activeApex = + getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME).get(); + assertThat(activeApex.versionCode).isEqualTo(2); + assertThat(getDevice().doesFileExist( + DECOMPRESSED_DIR_PATH + COMPRESSED_APEX_PACKAGE_NAME + "@2" + + DECOMPRESSED_APEX_SUFFIX)).isTrue(); + assertThat(getDevice().doesFileExist( + APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2" + + DECOMPRESSED_APEX_SUFFIX)).isFalse(); + assertThat(getDevice().doesFileExist( + APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@2.apex")).isFalse(); + } + + @Test + @LargeTest + public void testCapexToApexSwitch() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) + .contains(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + + // Now replace the CAPEX with an uncompressed APEX + getDevice().remountSystemWritable(); + getDevice().executeShellCommand("rm -rf /system/apex/" + + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); + pushTestApex(ORIGINAL_APEX_FILE_NAME); + runPhase("testCapexToApexSwitch"); + + // Ensure active apex is running from /system + final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) + .orElseThrow(() -> new AssertionError( + "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); + assertThat(activeApex.sourceDir).startsWith("/system"); + // Ensure previous decompressed APEX has been cleaned up + assertThat(getFilesInDir(DECOMPRESSED_DIR_PATH)) + .doesNotContain(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + } + + @Test + @LargeTest + public void testDecompressedApexVersionAlwaysHasSameVersionAsCapex() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v2.capex"); + // Now replace /system APEX with v1 + getDevice().remountSystemWritable(); + getDevice().executeShellCommand("rm -rf /system/apex/" + + COMPRESSED_APEX_PACKAGE_NAME + "*apex"); + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + runPhase("testDecompressedApexVersionAlwaysHasSameVersionAsCapex"); + } + + @Test + @LargeTest + public void testCompressedApexCanBeRolledBack() throws Exception { + pushTestApex(COMPRESSED_APEX_PACKAGE_NAME + ".v1.capex"); + + // Now install update with rollback + runPhase("testCompressedApexCanBeRolledBack_Commit"); + getDevice().reboot(); + + // Rollback the apex + runPhase("testCompressedApexCanBeRolledBack_Rollback"); + getDevice().reboot(); + + runPhase("testCompressedApexCanBeRolledBack_Verify"); + } + + @Test + @LargeTest + public void testOrphanedDecompressedApexInActiveDirIsIgnored() throws Exception { + final File apex = mHostUtils.getTestFile( + COMPRESSED_APEX_PACKAGE_NAME + ".v1_original.apex"); + // Prepare an APEX in active directory with .decompressed.apex suffix. + // Place the same apex in system too. When booting, system APEX should + // be mounted while the decomrpessed APEX in active direcotyr should + // be ignored. + getDevice().remountSystemWritable(); + assertTrue(getDevice().pushFile(apex, + APEX_ACTIVE_DIR + COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX)); + assertTrue(getDevice().pushFile(apex, + "/system/apex/" + COMPRESSED_APEX_PACKAGE_NAME + ".v1.apex")); + getDevice().reboot(); + // Ensure active apex is running from /system + final ITestDevice.ApexInfo activeApex = getActiveApexInfo(COMPRESSED_APEX_PACKAGE_NAME) + .orElseThrow(() -> new AssertionError( + "Can't find " + COMPRESSED_APEX_PACKAGE_NAME)); + assertThat(activeApex.sourceDir).startsWith("/system"); + // Ensure orphaned decompressed APEX has been cleaned up + assertThat(getFilesInDir(APEX_ACTIVE_DIR)) + .doesNotContain(COMPRESSED_APEX_PACKAGE_NAME + "@1" + DECOMPRESSED_APEX_SUFFIX); + } +} + diff --git a/tests/src/com/android/tests/util/ModuleTestUtilsTest.java b/tests/src/com/android/tests/util/ModuleTestUtilsTest.java deleted file mode 100644 index 833974f8..00000000 --- a/tests/src/com/android/tests/util/ModuleTestUtilsTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.util; - -import static com.google.common.truth.Truth.assertWithMessage; - -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; -import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.io.File; - -@RunWith(DeviceJUnit4ClassRunner.class) -public class ModuleTestUtilsTest extends BaseHostJUnit4Test { - private static final String SHIM_V2 = "com.android.apex.cts.shim.v2.apex"; - private final ModuleTestUtils mUtils = new ModuleTestUtils(this); - - @Before - public void setUp() throws Exception { - mUtils.abandonActiveStagedSession(); - mUtils.uninstallShimApexIfNecessary(); - } - /** - * Uninstalls any version greater than 1 of shim apex and reboots the device if necessary - * to complete the uninstall. - */ - @After - public void tearDown() throws Exception { - mUtils.abandonActiveStagedSession(); - mUtils.uninstallShimApexIfNecessary(); - } - - /** - * Unit test for {@link ModuleTestUtils#abandonActiveStagedSession()} - */ - @Test - public void testAbandonActiveStagedSession() throws Exception { - File apexFile = mUtils.getTestFile(SHIM_V2); - - // Install apex package - String installResult = getDevice().installPackage(apexFile, false, "--wait"); - assertWithMessage(String.format("failed to install apex first time %s. Reason: %s", - SHIM_V2, installResult)).that(installResult).isNull(); - - // Abandon ready session - mUtils.abandonActiveStagedSession(); - - // Install apex again - installResult = getDevice().installPackage(apexFile, false, "--wait"); - assertWithMessage(String.format("failed to install apex again %s. Reason: %s", - SHIM_V2, installResult)).that(installResult).isNull(); - } -} diff --git a/tests/statsd-e2e-tests.xml b/tests/statsd-e2e-tests.xml deleted file mode 100644 index b3a270ce..00000000 --- a/tests/statsd-e2e-tests.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Configuration for statsd module e2e test cases"> - <option name="test-suite-tag" value="statsd_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="statsd_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.os.statsd.apex" /> - </test> -</configuration> diff --git a/tests/testdata/init.rc b/tests/testdata/init.rc index 1c90c830..4e89f444 100644 --- a/tests/testdata/init.rc +++ b/tests/testdata/init.rc @@ -6,7 +6,7 @@ service surfaceflinger /apex/com.android.apex.test/bin/surfaceflinger user system group graphics drmrpc readproc onrestart restart zygote - writepid /dev/stune/foreground/tasks + task_profiles HighPerformance socket pdx/system/vr/display/client stream 0666 system graphics u:object_r:pdx_display_client_endpoint_socket:s0 socket pdx/system/vr/display/manager stream 0666 system graphics u:object_r:pdx_display_manager_endpoint_socket:s0 socket pdx/system/vr/display/vsync stream 0666 system graphics u:object_r:pdx_display_vsync_endpoint_socket:s0 diff --git a/tests/testdata/sharedlibs/README.md b/tests/testdata/sharedlibs/README.md new file mode 100644 index 00000000..a59686da --- /dev/null +++ b/tests/testdata/sharedlibs/README.md @@ -0,0 +1,13 @@ +### Test artifacts for shared libraries APEX support + +This directory contains APEX packages used for testing the platform support for +moving shared libraries used by binaries within an APEX package into another +APEX package. + +Due to the peculiarity of the build needs, this directory contains prebuilt +artifacts used by tests. In order to regenerate these artifacts, run from the +root of the tree: + +```shell script +./system/apex/tests/testdata/sharedlibs/build/build_artifacts.sh +```
\ No newline at end of file diff --git a/tests/testdata/sharedlibs/build/Android.bp b/tests/testdata/sharedlibs/build/Android.bp new file mode 100644 index 00000000..74b08a25 --- /dev/null +++ b/tests/testdata/sharedlibs/build/Android.bp @@ -0,0 +1,86 @@ +// Copyright (C) 2020 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. + + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_binary { + name: "noop", + srcs: ["noop.cc"], + shared_libs: [ + "libsharedlibtest", + ], + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + + compile_multilib: "both", + + apex_available: [ + "com.android.apex.test.sharedlibs_stub", + "com.android.apex.test.sharedlibs_secondary_stub", + ], +} + +python_binary_host { + name: "shared_libs_repack", + srcs: [ + "shared_libs_repack.py", + ], + version: { + py2: { + enabled: false, + }, + py3: { + enabled: true, + embedded_launcher: true, + }, + }, + libs: [ + "apex_build_info_proto", + "apex_manifest_proto", + ], + required: [ + "apexer", + "signapk", + ], +} + +cc_library_shared { + name: "libsharedlibtest", + srcs: [ "sharedlibstest.cpp", ], + + local_include_dirs: [ + "include", + ], + + export_include_dirs: [ + "include", + ], + apex_available: [ + "com.android.apex.test.bar", + "com.android.apex.test.baz", + "com.android.apex.test.foo", + "com.android.apex.test.pony", + "com.android.apex.test.sharedlibs_stub", + "com.android.apex.test.sharedlibs_secondary_stub", + ], +} diff --git a/tests/testdata/sharedlibs/build/build_artifacts.sh b/tests/testdata/sharedlibs/build/build_artifacts.sh new file mode 100755 index 00000000..8dbc64f5 --- /dev/null +++ b/tests/testdata/sharedlibs/build/build_artifacts.sh @@ -0,0 +1,206 @@ +#!/bin/bash -e + +# List of files required in output. Every other file generated will be skipped. +OUTFILES=( + com.android.apex.test.bar_stripped.v1.libvX.apex + com.android.apex.test.bar_stripped.v2.libvY.apex + com.android.apex.test.bar.v1.libvX.apex + com.android.apex.test.bar.v2.libvY.apex + com.android.apex.test.baz_stripped.v1.libvX.apex + com.android.apex.test.foo_stripped.v1.libvX.apex + com.android.apex.test.foo_stripped.v2.libvY.apex + com.android.apex.test.foo.v1.libvX.apex + com.android.apex.test.foo.v2.libvY.apex + com.android.apex.test.pony_stripped.v1.libvZ.apex + com.android.apex.test.pony.v1.libvZ.apex + com.android.apex.test.sharedlibs_generated.v1.libvX.apex + com.android.apex.test.sharedlibs_generated.v2.libvY.apex + com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex +) + +# "apex" type build targets to build. +APEX_TARGETS=( + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.bar:com.android.apex.test.bar + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.foo:com.android.apex.test.foo + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.pony:com.android.apex.test.pony +) + +# "genrule" type build targets to build, and directory they are built from. +GENRULE_TARGETS=( + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.bar:com.android.apex.test.bar_stripped + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.baz:com.android.apex.test.baz_stripped + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.foo:com.android.apex.test.foo_stripped + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.pony:com.android.apex.test.pony_stripped + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs:com.android.apex.test.sharedlibs_generated + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary:com.android.apex.test.sharedlibs_secondary_generated +) + +if [ ! -e "build/make/core/Makefile" ]; then + echo "$0 must be run from the top of the tree" + exit 1 +fi + +OUT_DIR=$(source build/envsetup.sh > /dev/null; TARGET_PRODUCT= get_build_var OUT_DIR) +DIST_DIR=$(source build/envsetup.sh > /dev/null; TARGET_PRODUCT= get_build_var DIST_DIR) +TMPDIR=$(source build/envsetup.sh > /dev/null; TARGET_PRODUCT= get_build_var TMPDIR) + +manifestdirs=() + +for t in "${APEX_TARGETS[@]}" "${GENRULE_TARGETS[@]}"; do + IFS=: read -a ar <<< "${t}" + manifestdirs+=( ${ar[0]}) +done + +manifestdirs=($(printf "%s\n" "${manifestdirs[@]}" | sort -u)) + +generated_artifacts=() + +archs=( + arm + arm64 + x86 + x86_64 +) + +apexversions=( + 1 + 2 +) + +libversions=( + X + Y + Z +) + +for arch in "${archs[@]}"; do + for apexversion in "${apexversions[@]}"; do + apexfingerprint="VERSION_${apexversion}" + sed -i "s/#define FINGERPRINT .*/#define FINGERPRINT \"${apexfingerprint}\"/g" \ + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.bar/bar_test.cc \ + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.baz/baz_test.cc \ + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.foo/foo_test.cc \ + system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.pony/pony_test.cc + + for d in "${manifestdirs[@]}"; do + sed -i "s/ \"version\": .*/ \"version\": ${apexversion}/g" \ + ${d}/manifest.json + done + for libversion in "${libversions[@]}"; do + # Check if we need to build this combination of versions. + found=n + for t in "${APEX_TARGETS[@]}" "${GENRULE_TARGETS[@]}"; do + IFS=: read -a ar <<< "${t}" + outfile=${ar[1]}.v${apexversion}.libv${libversion}.apex + if printf '%s\n' "${OUTFILES[@]}" | grep -q -F "${outfile}"; then + found=y + break + fi + done + if [ "${found}" != "y" ]; then + # Skipping this combination. + continue + fi + + echo "Building combination arch: ${arch}, apexversion: ${apexversion}, libversion: ${libversion}" + libfingerprint="VERSION_${libversion}" + sed -i "s/#define FINGERPRINT .*/#define FINGERPRINT \"${libfingerprint}\"/g" \ + system/apex/tests/testdata/sharedlibs/build/sharedlibstest.cpp + + build/soong/soong_ui.bash \ + --make-mode \ + TARGET_PRODUCT=aosp_${arch} \ + dist sharedlibs_test + + for t in "${APEX_TARGETS[@]}" "${GENRULE_TARGETS[@]}"; do + IFS=: read -a ar <<< "${t}" + outfile=${ar[1]}.v${apexversion}.libv${libversion}.apex + if printf '%s\n' "${OUTFILES[@]}" | grep -q -P "^${outfile}\$"; then + cp -v \ + "${DIST_DIR}"/"${ar[1]}".apex \ + system/apex/tests/testdata/sharedlibs/prebuilts/${arch}/${outfile} + generated_artifacts+=(system/apex/tests/testdata/sharedlibs/prebuilts/${arch}/${outfile}) + fi + done + done + done +done + +# Generate the Android.bp file for the prebuilts. +tmpfile=$(mktemp) + +cat > "${tmpfile}" << EOF +// Copyright (C) 2020 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. + +// This file is auto-generated by +// ./system/apex/tests/testdata/sharedlibs/build/build_artifacts.sh +// Do NOT edit manually. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} +EOF + +artifacts_filenames=() +for artifact in "${generated_artifacts[@]}"; do + artifacts_filenames+=($(basename ${artifact})) +done + +artifacts_filenames=($(printf '%s\n' "${artifacts_filenames[@]}" | sort -u)) + +for artifact in "${artifacts_filenames[@]}"; do + outfile=$(basename "${artifact}") + # remove .apex suffix + rulename=${outfile%.apex} + + cat >> "${tmpfile}" << EOF + +prebuilt_apex { + name: "${rulename}_prebuilt", + arch: { +EOF + + for arch in "${archs[@]}"; do + cat >> "${tmpfile}" << EOF + ${arch}: { + src: "${arch}/${outfile}", + }, +EOF + done + + cat >> "${tmpfile}" << EOF + }, + filename: "${outfile}", + installable: false, +} +EOF +done + +mv "${tmpfile}" system/apex/tests/testdata/sharedlibs/prebuilts/Android.bp + +# Restore the default version string to avoid bogus diffs. +sed -i "s/#define FINGERPRINT .*/#define FINGERPRINT \"VERSION_XXX\"/g" \ +system/apex/tests/testdata/sharedlibs/build/sharedlibstest.cpp \ +system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.bar/bar_test.cc \ +system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.baz/baz_test.cc \ +system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.foo/foo_test.cc \ +system/apex/tests/testdata/sharedlibs/build/com.android.apex.test.pony/pony_test.cc + +for d in "${manifestdirs[@]}"; do + sed -i "s/ \"version\": .*/ \"version\": 1/g" \ + ${d}/manifest.json +done + +ls -l "${generated_artifacts[@]}" diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/Android.bp new file mode 100644 index 00000000..0c62e645 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/Android.bp @@ -0,0 +1,96 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.bar.key", + public_key: "com.android.apex.test.bar.avbpubkey", + private_key: "com.android.apex.test.bar.pem", +} + +android_app_certificate { + name: "com.android.apex.test.bar.certificate", + certificate: "com.android.apex.test.bar", +} + +apex { + name: "com.android.apex.test.bar", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.bar.key", + installable: false, + binaries: [ "bar_test" ], + dist: { + targets: ["sharedlibs_test"], + }, + updatable: false, + compile_multilib: "both", + multilib: { + both: { + binaries: [ + "bar_test", + ], + }, + }, +} + +cc_binary { + name: "bar_test", + srcs: ["bar_test.cc"], + shared_libs: [ + "libsharedlibtest", + ], + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, + + compile_multilib: "both", + + apex_available: [ "com.android.apex.test.bar" ], +} + +genrule { + name: "com.android.apex.test.bar_stripped", + out: ["com.android.apex.test.bar_stripped.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.bar", + "com.android.apex.test.bar.avbpubkey", + "com.android.apex.test.bar.pem", + "com.android.apex.test.bar.pk8", + "com.android.apex.test.bar.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode strip" + + " --key $(location com.android.apex.test.bar.pem)" + + " --input $(location :com.android.apex.test.bar)" + + " --output $(genDir)/com.android.apex.test.bar_stripped.apex" + + " --pk8key $(location com.android.apex.test.bar.pk8)" + + " --pubkey $(location com.android.apex.test.bar.avbpubkey)" + + " --x509key $(location com.android.apex.test.bar.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/bar_test.cc b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/bar_test.cc new file mode 100644 index 00000000..9b397de2 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/bar_test.cc @@ -0,0 +1,13 @@ +#include <iostream> +#include <string> + +#include "sharedlibstest.h" + +// This parameter gets modified by the build_artifacts.sh script. +#define FINGERPRINT "VERSION_XXX" + +int main() { + std::cout << "BAR_" << FINGERPRINT << " " + << sharedlibstest::getSharedLibsTestFingerprint(); + return 0; +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.avbpubkey Binary files differnew file mode 100644 index 00000000..931a477a --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pem new file mode 100644 index 00000000..9363cda9 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAp/bgVatvywj6wEKD0ldJ+RxfFOv/67norL43z55gd/K/u1W4 +GoFHm0e5T1PV0j5zMi7EraIWy5g9eTmtU9DTwJoIKHw/agrVimxBsd5Los/zHAGJ +JoRy5Wo7cenpp2QpNzfY3HdN88PMZ3FiRooa/GepoOI7IBs3IEmGJTC3N5CZYsh7 +b3yzZn4cuWuQZ+oD+G/z1AjK9Q1po3auz5lqSu1VD8GoswwfrC7G/MvH/5Jytu/o +XKESeGIri8YDMqsVX7hHTGbQ8MBpy5RtFfIGND+XulZvTl87bXAhJVbJjD3GlZyM +i/eZL4pUdxKjv/ZOT5bI1/NTJ6Qtr6yMgJuVbL7VHYFPISxdwXYY/fkr0AvZw+hL +KopDrb3Jn+79elZqOuZ6l1muCpkXRs4QS4fRSh4Lnk2782p+y070WhdA+zgzDLhX +4ytyhAGNi9wWNCjGDT3C8AOoRzn8PhG8sHsAHL6sFYpJx3LRyv/nO1jFGHDd3cgj +Vql0NTyVPlj8JiSQ1aQvXMtf1M7pwp6igjKPf37tVrZ4gDpjQm26VSy8lcgKgR8w +Ej4Ti4NbkcZuoJNjIfJZJAUDtr+aXJA2bWRO0ggTnuvC3sHCMH4o6i2FdNzsahYn +z2QY7h7hiuCdXJdoFWBhSOQ9cUcBroymuSNWhliSxj7XObql2hK+u3Bzp3MCAwEA +AQKCAgAdQrPYGNKT40+TmMLQLOa1IA0sXuSpkyyGk2izoZqaqs5d+1PkQitQUNFm +kWtJghmdX2ph+T/RXgcvjC220UViYzMSonqFpbeHss5LBzfT+DgY4+eZry846iXK +9X3/7EIF3ZPI7HvHAJAmYSlGsp565DA319GHCVa0KDrXVcJFSsp93AEs7eNu8n9c +ifGROMJSUGaAxLter2R81psjjU1oGipcYVdbQbxuyYNe3L1Nt5yGZArtwB2wnSGK +6wb5l7ZUg4zgMXUqy8pibcwHK6+LAJ0VGCOx2oNG0Gbl01WvOb/Tpn8RjyO/lXCb +gcLHGUiRMupwPHJ7EG3pEb00VmZUUCd5bBv76A8WyS9hrGZbcHy26nS/9HkadT5Q +IZVjWSbjZA13Tm0OmSKgwV3UC5yRZnRj4vNoku9bhWhZ+n7GCAYbSBiAVBrlIgM6 +yxw66Sbxq7jLEZEUDWLTGl00W07hvF44FHYIATsOMzHyLPbKAeozimP/sZVw+dFN +KcNVKJtvxf/xoZIvM32BevrwqlfgQRr6G+sILzP60NlLMqQTsXlqsqZlyZ6ROrWp +0q1T1NQBqiyNjk5Iq+nfQl0wVK1F8Xj/IrZ4udddKKunWtDIADcdx3HshkmiVVwp +3lTadEEEA64Jwu9GyoYK78+xINkIm8/GK4LPgo4a5eRO5JaAqQKCAQEA1/DzERsj +FqmrF3SVKt15ZqAV71YkVmlVTcE62Gr93kE0YEhkK9HrxcddkiPgkaKP/VUXpzwL +zVsvdIlPaCdLQVoZt/c2weDvNO+8DKtb3v4AYgc8fltSCvPMzdc3VezL1Y0mZmMV +sZndsw2ZpZnRrtHllqPQR8mC3mLgvhUyQw2yIcWPuI1L3VBTjd/9z6MDgDTZ3gKq +h5odK5gu+KIT5hUsnxCb1Z7D3WAw/uprwpW4IFIFWbMeXPwVGuwKQ0/ueqSWQPoC +mO5sVLd43auO9pAtp0BT8shdPkxuJiI7FNckdh8QJJOu2JkvN9O9/GZEDK2NuPZN +ONo0hAkczxUkjQKCAQEAxx+BeFsY7H2ai55QR8TkEaK4jVuqiJZFjMipQWkM8vyM +kYXUG/sV61gex5TZEdagxGdGyGsm58kq9cySY75CGPjCm9bPJ2qsKnKEkw4PK6BM +eqHres2x7XjE64tF4AIYLPNVmMQKc/5Ke7uTsA2W4YYHSwDKODRFGKhgc4oI5G+w +j5TtnqeOQ+WxseFEQEp9RgUIssO6m0K4lE9lrwTFsu95USrpJQXs6J/nZct/xXAO +fRuv/nB7GMSj+Ay7TEfFbAw0D8NKEYzpF1/oUjPW8aihXh3Eq6HySBjRbAH8YCjC +WZYSm7f7TLunIonRL+dqCHB3WFDsvta2nwdPdUb7/wKCAQAZquxZhi4/jV9m5Fau +x7CcgD7bOhQLqW2YVnWWL/GJL5r4LuKpSsSJt87phhY1eWtAI5MyL7L/b+1OHtwv +dyw80mboNRxvIzuLwUtK/jtnYC3PeSi5pEU2RBB+Dyzmq8T211ZPKUv01mNB20X+ +JzCDZTOzGjmxrsQ9hudL8N0Ol1wrI36X40O3RMsJvCxBOBE8dgvHle2LPMhm3CoJ +J8rRuIabSbAcTkjd0YdBZb/1WzKNtPIp3V6oktY3YwM9SQ0ByvqJMq6IWx7JWx2k +y7WsnSqwDLdtzl82/oLBSaRYL9KHr92NW3iXCm5QZnzYuZcxIpgL+krnjRhc8XBZ +NRwpAoIBAQCntCU61J6dLvwmcuNyTqU3JTEB/R4Xg1h4RdgnOu6pB4LsXSZTmpjP +aZwiw34+w+ELCWBYE8bkmE0ST4VLdEX++iQNVFGMBQ+TgHef0st8FrnS3uSQvQUJ +2BkhuF7VV249DYQd8Z5MKvNYWpb8Q7W7o0IpLTUjOQKozcbOCIeMvXSauPeYE86B +6MZL5kmxTAtOGZdF2AsmEH+ciXI+gWpwVbh7YASUJfVtxp8A4O9vvfy16cfEJ7/F +EHh4xWBJ0ni3k1+Vlwie12rJQQFNmlOBnGCr/65QT0ja5+wZZ2LDKhDlmrt5Yu7H +pZQSRrhj/CcVjIM3YpDB+dw8+880GuDJAoIBAD/3oFK1uG++uU/Mo+3YtZj5YWuE +Z5FNE7tJGeyKUvKXTLUO1NTG+HEmSGkOyeiXrQNJssFOjgVzNx1kke5Pi4qv4+QL +087gFnhDnrMHBSwcWIpkEe5zQEYrS/yRJlD63WM72ivLDNTXe/BQfuCIdX9qBHbG +qQQzjaLdj0xtKQsidL2Cy/PlqfqGzp90I6uXbRiOrC4rsNjr9mX82atNswua0tcD +mMGi0eJXtKOS2aaJPU9yofd4sBW+n94Ff5ue1cwfjdW/lKaAPDBGNRCgvvcyM39Q +DWK6NSHzz7pVO5+tb54vMZK4TySPrS1qlin4AALo4tHIBuIDkaIHTmDsF8c= +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pk8 Binary files differnew file mode 100644 index 00000000..6119c7f2 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.x509.pem new file mode 100644 index 00000000..e687fc56 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/com.android.apex.test.bar.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF1zCCA78CFEnLOBRbjwndwo8AjZVTyorFhju2MA0GCSqGSIb3DQEBCwUAMIGm +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEiMCAGA1UEAwwZY29t +LmFuZHJvaWQuYXBleC50ZXN0LmJhcjAgFw0yMDEwMDUxNjMzMDVaGA80NzU4MDkw +MTE2MzMwNVowgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw +FAYDVQQHDA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQL +DAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMSIw +IAYDVQQDDBljb20uYW5kcm9pZC5hcGV4LnRlc3QuYmFyMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA42Gi4Y3vNA1zQh+IDY/mQbRfvy5ikmWFTP4PwKcO +XSg21BiTx7o7k+cKbuHjCWp3eh2O1fRXB9WXLQ3//8BNLZLUuaGWM7+fcsN5V+aK +yPxzt/6fYBOzb4BkYnMgJjmOHBo9TL1g35IPM6O5Y3oSvCBhQuMdvZYbBDBAZ4Zn ++0PoP7fWPPUne4PGL5KF48ttpKbXeFhUjWcVnh440Amfs4+dHrblf4/f1iKUIrT8 +hNWgOBhUrHAKyKAyE/FYydVm6smLZN39m/5ac7LDwYzRBP5IDVKbLxfmwSOfj0n9 +1YvEyl/BSwY0V29AbCbsl9eVQkE2Jy1UqS7we8rK+V+Z3b/dTOh7skdAwMHGDi6v +qIxjNr9tAX1Pxhj1PbqYM7nIa7hLmNfJk6N/k72VjJdiWyp2WcQFIJQduSbtgMS3 +CA+LzCJqEbqz5SyaFHh89KsqHPGOxcsVHI5mIRWhBioCKlCbsqvcZg+ChzdY7Hbk +ViBhqmZr7cAXyZEqUXApSmX3E1yKCXry1WF9hl8homsG0rO1C+seS+R3OiWRNdrK +o9Aik1gtwpoxbJH+Hxc0usS4yW3b3YyFN2tPUN2NoTFrkKNSRfDwqlWCJH8wd1Og +4dTxyYesVLCKW0/YfPKBCPFlAqx+yDBOe7lxP/1kuWdKeIHxdhlGpjqUoaeqi+46 ++xMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAlGQyBIzKvdstBN/6uASKOHcWuqfD +COyBXnUmMHEWawtS0C4aL0rJmvILQaVrFp3dPkjI3RpN8TZqVnQfawOewOBe/2Vk +cpzGxsIJQpjmFmMJr730LX/RDELnTnz3VKwIbCoZBZ3qktiAI5+rGmmP4FrU8MVl +m+VnShyxFSmLakhcyezOos3+ibJDgFsoNFDQ9b8aTUFKy4Xsa5OMSYeAJc4L2IWY +dowDHu5wnRTfy0uCXn095GdgSiYAAvSp58M/bXuVaHXm/Qg0upWdEaouSMecAYwz +PTF2sEpmoAUVIHOusm1Chqopa4kQtQCcVB3b/1YDZTeodjKHI7w9WLqjEeq7+fYl +msvxlYtyL3r6JpQXyYhy1643zmI8/P1TYTi1AbsVNwz/87xmHyrldLKH+jmWMspx +hiynKOTZahtO48WhkRGLa+iJk14ztUdD3MxAJwZiMUMFhLth/zYLGSyBwqR1smrs +J5k7BL67b07JApjoPL1OHn9ypBrV5L2CKwSfEGEzp65BtfGIGDYyoNxEv8zky8KY +XHxZaSWor6RQRp1QX6VRcXYk7XQHWuIevlW5W2APRXCAC05+rlVvi98swJQUcR3/ +oZCgxdr9mSV01fRZMkXv5exkhu/KR8c1ZstqRcSswRv4vrFFTFRahrTWGk0DQXU3 +EH5MwrfZ9sUq7Iw= +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.bar/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/manifest.json new file mode 100644 index 00000000..da13d6d8 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.bar/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.bar", + "version": 1 +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/Android.bp new file mode 100644 index 00000000..6076d71d --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/Android.bp @@ -0,0 +1,77 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.baz.key", + public_key: "com.android.apex.test.baz.avbpubkey", + private_key: "com.android.apex.test.baz.pem", +} + +android_app_certificate { + name: "com.android.apex.test.baz.certificate", + certificate: "com.android.apex.test.baz", +} + +apex { + name: "com.android.apex.test.baz", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.baz.key", + installable: false, + binaries: [ "baz_test" ], + dist: { + targets: ["sharedlibs_test"], + }, + updatable: false, +} + +cc_binary { + name: "baz_test", + srcs: ["baz_test.cc"], + shared_libs: [ + "libsharedlibtest", + ], + apex_available: [ "com.android.apex.test.baz" ], +} + +genrule { + name: "com.android.apex.test.baz_stripped", + out: ["com.android.apex.test.baz_stripped.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.baz", + "com.android.apex.test.baz.avbpubkey", + "com.android.apex.test.baz.pem", + "com.android.apex.test.baz.pk8", + "com.android.apex.test.baz.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode strip" + + " --key $(location com.android.apex.test.baz.pem)" + + " --input $(location :com.android.apex.test.baz)" + + " --output $(genDir)/com.android.apex.test.baz_stripped.apex" + + " --pk8key $(location com.android.apex.test.baz.pk8)" + + " --pubkey $(location com.android.apex.test.baz.avbpubkey)" + + " --x509key $(location com.android.apex.test.baz.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/baz_test.cc b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/baz_test.cc new file mode 100644 index 00000000..ea5b341a --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/baz_test.cc @@ -0,0 +1,13 @@ +#include <iostream> +#include <string> + +#include "sharedlibstest.h" + +// This parameter gets modified by the build_artifacts.sh script. +#define FINGERPRINT "VERSION_XXX" + +int main() { + std::cout << "BAZ_" << FINGERPRINT << " " + << sharedlibstest::getSharedLibsTestFingerprint(); + return 0; +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.avbpubkey Binary files differnew file mode 100644 index 00000000..ef865d7a --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pem new file mode 100644 index 00000000..ee8b8a66 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAu48tcVfIiZ9syLm4CqkRE5/MOhfzro4epTGV0VrkOc8JYQic +YjRAN02/7QfGk/aei3xRuVKqAqDxnZ5IE5ii6/HT9TOInMK08/vr7jc6N7dfQCSW +nyvG3WBC2HWEmuVCGJrY1j/lmIzKuUcMgpF0ZaqB1Qm/viI/ppXZiEm234SFAg0y +RZbiiGRy5K388xkTA0nbIAi3iRq8RSfDN4kFzIH/Y7AsNF/tzI2K+4PziGmReH/e +ekgPhC7XUUySNrjctl7naDgvuihu18QwsC+WNCop7aHLe/ByOrOFq5R/mGsPCP6q +JyUJ0P9YotI28y+h7rd88W6IsTQMz9jw1JENx2YGJHkjRYIy97vDDbeeyBm5qI3I +Suy8IgzG/JW0w4pvUB6pWzfDuw+Hr4sqU7Bjb/CFeFUfzSGTg7efDN8W3L/O/wtH ++hR1t0B0BZBtW9TOFCo4z30C1u2OltT5Vr8GgsODMdR6tQCNpu4WmWfImriETuKQ +OzPYo2eKCbsoLEQxda+EUu4h5FDH703JYWvrxCCmHeW624iXy1LavEc9YYOe0oRT +DslIvYSHimH9jvgXRU7HLz0obljwwYRM+7aHWmTClrIC+aSEq1x0COHt8fWtbJ3f +gBCdNSBcd0GEX55V6Ez1lytGWLzwPqedYbQnoKdIJPu5Ge7GMm75de0lgR8CAwEA +AQKCAgBoV2e1dVt3zHwtUrxjGdkJLM3lx6tmAWRlDCfHlyP+UQJru+mb7GuJGLTb +/YZojDt5Z8jjK2yvF7AyunpohHKmhhsffvLSGrOmRBDlrk2x706LFY/Brw3r3AB0 +ATSrIz1ZCNP2pQdqjXC+EBuSi67QXEHsLYdBFDaKyzSAUFnvEP8ZvBOqiR0vOYp9 +U5mz99AO9Uh1EsRf/sKcSlmdDJpwQiW85KZC4NcfA+M8txSFYA1wltpC9tHC/HgG +n218Ce2nezaLUS6kBphbaqaXbXHHRWmb7HWSVpqFs5d6c5tkRLLRkzM/oahLX7KE +qiOtuGMCtYtJmO9sfYNfIdYguy2JOylBQU1HLT2eTp/be29aUfblGpvG+4WM49+e +da33R65viP6uvwcTuiQIZdfouJLgADutfmLAX4XGk110k/AOzSFCeIvX0oeoc/Nv +IZDgGQjELBuyKdCEnheo3UkngXZg2hbVSoI+EQE900wm8s3vi8GoKq32VuZAiNGi +mW6ebs1A3fOLCPrK9GdbLu6A2uXyeeQH7NMiyK9o5jWPIU7NJ3KUgGQmbNz0/aqi +jN93DdRjeoYXUdD2oYC8S9XMqtdiUubVEMqDeGg2jhLxYDk9EfKD9KZnmFSBM7a0 +WlZlixTXi77ktJS7+1YlRWluPIhD0MXdEkHeRmgGZoc0w9ayoQKCAQEA6IrCf8U6 +SUG2QaLrIjAVcR76UETGR4gE5s5E1qvZB3/yGzou9QeBOGFz5lRQbrGD9nrNFuqq +Z/VJmsL2aia4dPDjjJLPW3budcq8GjR/ZdGCkE+VWc3D/CUf8ja4Lx3MxDjym0cw +i+pMysym/6QNo/sAltzQYehtQlGNUisII+p1FbHKQAZwLgczvglZObgynegSUyKT +xrXcUyqOozU933YPkPE1X3cKyF2JFoK/YzxMCfDlYIs4SfwlFnFELbz5ug4Zx2WU +Ouc9JWhAELYzHnbHP0OkP6mUTjSJC52XCdHg8b2Qtrof/PEuJ3Xoyz8PFBQIeRjD +HzTJ4i0Pzwd/rwKCAQEAznrE11R6IbsAznmE5d2nmrph8n0qnpXi9Ryd5c4W6VNm ++hnelTnhAkGtNHJCloL24zPlAlb9CAGKJM7U1I21PqgZpIJ7hHTU1FLiwc83dkal +IQ5yVbAPoa4O/qn0Omo+MQ+tYCygDHiAJFQFah+pX1Jj+6djn7kKNCzpVeN6aVFu +hblHAOgwhDlt+l461wjYsI39osPfgInO/z/oY0WuIFEYCA5kWXEuNImqZi7GTcs3 +EQVR+OxTxwt9IUJVeTAusv8V5DFqlojF9jZVBB60VcfMAEyjsAF0/ZsweuBU3jnD +mAln3pIS4AON0zGM/eZFKKkHZT+ZVqIUOVCFoyGBkQKCAQEAwmX10SCM6F7hwR80 +WCFAW4/dDCtiYrwn9NctHxUMWsOwHujWBosekIaPgFat4svNmMjyGJ1WlY+t143y +t6zk+QXEBGlapYjYMmqoM3P9qJ2r+348SZXFqE1U1oS+Fs1fuA4vanXp9J2LUuIh +HYcEzDfyNywjnCXU6OMKNE27AWNoPBmkDUAUmbX1oIFqMOF2lyFB6HP4e97ecDwc +f/3rWpr0ymOLDeKThgsDpmjpHEl0+76B0uKvzNHYI1nO+DmJvus4y8N0VoWnTVVI +cXAPbgE38gBXF81pKLOseaRldpUY6p5hkxAn26m3vs9ILFjr/wn8R1fXDohv2P94 +vsbzCwKCAQAkXsu9gkvhFSeXNyCJvPmA78PBCvsu5AgOVPQbPqoaf25sL5Jdhsxz +sU3pJxdDm94RN1rnhpsbhenngedLaYq7drDNoY5QTqQOomr+6JlEZD1CDWFmZpTa +TeamRRmYEI7T5YcMoc+vYqpvu70YbGtRNxoVge6ye82oUyDm2CL/2jA1reUr67pg +EB2nNGH47r38m4ZJ3WbJJX0oyQEOO3/ogWBSSvayKpWQ+47gYOzdVyZkASPnTPmU +3hk0epLDvhD7xqL8hxfXXFBChl+DUkVBtufgRZ+vqRIKegOYIVvRqSsi5MU/F0vr +2bRptxi2wJD+EIgU9Zb1A6e8UMq5aXWBAoIBAQDS8w0TG4R8SLHXOs9JagC53zAx +qVV8mrL9BEYzeuM6vNpyZIO75O94q2ifGvzsISxLf2xom99BxpcWo9UAyHFgubL3 ++P0spdJSeP0OWJgldEqwWGMrzQbYZomzi/QUlFpXNZfHgHZLaCK7qKu9RDQLVtug +5i+yVjSKl6RcaCCG9E2u68yxKlI246RNK1HZXQgxsnz6BnP/cqAn9G+yzT9p6nmK +tt2d2s35MS1zV9YzACi2idsZBeio7bghC1maj/TJvq9gRwIRN4UpbPRsflpdejFY +nEEW+tEbNzFrepkw08+9dRGnPU1G1NYvyQqMhc98lVBiMoDiH8X18gVWafhR +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pk8 Binary files differnew file mode 100644 index 00000000..37948c1a --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.x509.pem new file mode 100644 index 00000000..d41d86c7 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/com.android.apex.test.baz.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF1zCCA78CFAgqiNJewYXPYz+41LOIDLe9lXnzMA0GCSqGSIb3DQEBCwUAMIGm +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEiMCAGA1UEAwwZY29t +LmFuZHJvaWQuYXBleC50ZXN0LmJhejAgFw0yMDExMTAwOTIzNTJaGA80NzU4MTAw +NzA5MjM1MlowgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw +FAYDVQQHDA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQL +DAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMSIw +IAYDVQQDDBljb20uYW5kcm9pZC5hcGV4LnRlc3QuYmF6MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAtm4cczWBEzR+kzRPHsBh61ihAQNtWN2mKLhZZKRS +YrNZ59+M53menjsc1Bs5fYvKTMGj2u2a/yvq0QiSAlgVMY+1aZ40FN22tBTmy4yr +6iTMi9Q+uUbQJlHLWSy8n3oFuxEHAP96ZPDJyqPHSJpKeSPKIgwyw3I+nXb/fmB4 +HvBmjHbFnwvKqHFyoEm5p/wGZn2vMtxZqlignNtUZr2zYTJW9YCuL80L/o1jdAFk +5sHH1tBEcp1ZTXRgKm2XFfp7fqfOWkS9XL3VUF0LVWMgyv0u2fr7n8TPZb8iz5PA +naCUN9hCoPrj4vg29eVYy2IoeSrLOAUiK986mZDToKbhxuLSHVV+IRNnqIQV0+mu +rR8OCqDlWbD+fU29cacAjGFt72unLAIEE49GHZFS2pTZ0cDXX1bBUrBVTMlcnCjn +YiU7XtPaJGelBkJEu/ErjHE9TrsQGKkzMwd2ySTOsk3K4OtWl+E9i50TW8Vu7gR+ +Qr4lZvY90OmXw+k02pkCo5g1GU8uXoTPjd0JQsmiEk01dMIPyWxXBYiuiRUxu3mc +vYJJGKZSMX3VKKYmtFN9lTuf/OyjztHjRQPit9sWVcYgKuy0kw1LQSJxtyNQIeFm +ipZVzh8Wo2TMm8argWMPHxyIfjEiVbNhXirC4My0Z02agyyB4Edxg2jkiKQBss3y +/IECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEArOMz9Hn03yy2ano628v0wXMFixVA +/XzSpb8GWi4GzJxV96c9t6QQPVdo/XS0uBuEa2Uc0/W5icU9+iKzBvQHM3MI1jI6 +/oj8/mGAzvyvIA0pdKP1XOvugsgi2UDNr5QNgZ1UPIpVkzeBQpLgTiWL70Pl/znE +b7Q1nKEFeNWxBaMk6u6n6gNh6sMLv1doSCi0FM1cnWHv/0qxPizGjTHKmGs4TbMZ +zWnBoH8XSMxruAEbucl1E7vYXgYthOW0I+SCFpyP53e9VdoNNYWMeNKCm3EoIY86 +XWLArBBQcCCrsLas45670ouQ/H9Yn498MwFinuWcsfROghXQwhn7fPaDQ2oVlftj +4LQS2vD20mJV15wa/1n/VDzRAxIZwPdtbJX6JZO8E7Oc0+WSehwhaU+x7k9GYyBv +tiEGS297qZC0/WvoE3VtRHZjOzphxt6PLelCEoqhdZy+q0uGxj8TmPRXo9xtmBLH +6GPgZe3dR2SJ4uMqBjt6/6/Rki3du3btAn2O4b0Zf6h3wftqK6INbsJ7fRpIgLaU +GvCZiCUZaxkNcCO94RnZJrTTVO1rJ1ZBkcqyMHvrSRhPCAyH2zVtHcNDCiR06GNA ++7P5SuyntvmnZKteKc2HdTnm42ewIXyDIwTs1crc8/luevw81s+SJqWumSGTxpfU +IAM423xlNZfBXZA= +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.baz/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/manifest.json new file mode 100644 index 00000000..1c57f3af --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.baz/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.baz", + "version": 1 +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/Android.bp new file mode 100644 index 00000000..451f8a01 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/Android.bp @@ -0,0 +1,77 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.foo.key", + public_key: "com.android.apex.test.foo.avbpubkey", + private_key: "com.android.apex.test.foo.pem", +} + +android_app_certificate { + name: "com.android.apex.test.foo.certificate", + certificate: "com.android.apex.test.foo", +} + +apex { + name: "com.android.apex.test.foo", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.foo.key", + installable: false, + binaries: [ "foo_test" ], + dist: { + targets: ["sharedlibs_test"], + }, + updatable: false, +} + +cc_binary { + name: "foo_test", + srcs: ["foo_test.cc"], + shared_libs: [ + "libsharedlibtest", + ], + apex_available: [ "com.android.apex.test.foo" ], +} + +genrule { + name: "com.android.apex.test.foo_stripped", + out: ["com.android.apex.test.foo_stripped.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.foo", + "com.android.apex.test.foo.avbpubkey", + "com.android.apex.test.foo.pem", + "com.android.apex.test.foo.pk8", + "com.android.apex.test.foo.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode strip" + + " --key $(location com.android.apex.test.foo.pem)" + + " --input $(location :com.android.apex.test.foo)" + + " --output $(genDir)/com.android.apex.test.foo_stripped.apex" + + " --pk8key $(location com.android.apex.test.foo.pk8)" + + " --pubkey $(location com.android.apex.test.foo.avbpubkey)" + + " --x509key $(location com.android.apex.test.foo.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.avbpubkey Binary files differnew file mode 100644 index 00000000..575ba511 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pem new file mode 100644 index 00000000..1d651b58 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKAIBAAKCAgEAz2ykMX5GHx1z+1BpFcNwnZv/8AladKxjG962kBuZrxSpu3pb +8JKTqajY1TNmM7Hs0P8RjDY0M2ryNWodNu7GZKSOz7L+LztHeJFuZByCHjh4em79 +sYivhJKqlX0EVCnxW+IPQPQqxJKDoFBGWo/HfbhXiZJR5OT/XrNhl9KfpFL1NHfU +dmZHIqFJHCrcQ4jQNxAh9XIvw0bwEwWe/q3Hdxs7UC4mH4m0Hz3cmKe73H0al3FW +2L3xgy5vmc93SgzO0Csrmcrsp84B3qtNd6B0Xna52AEmBMuhWMRiFlIpV/Pf9utS +PYoxU3IRjUJ0FacMbhgQOZdtXZGW+G3FLnH2XHX9rUNnrlMmgTmoCGH/JD4ZwXvF +KoEUZ8wlGVZksNz8i79yPMxR/QyEppf56YSD72FrfCBMVdBDi0vYk7/nhqJU97ws +VFwhFDTxp0//rca8UOnn0pP9HKylxT6sff/w2U+zzzn1MLsu/Tx6JEbDj+464RnK +zxBL0wyzUUkwOodgctW8UFPSrttbQD5+7G+kQUl3q1854bX1EdW5jGF3l/OxE9P6 +mbsZlHPOjd4nFOtju7cgKpDbFtVwyDZRBtoPBIbBqS9bf7Y3oaesVeFkDfeC5SkC +bUIDlri8Sysuse9uoNtaGzvYu5Wnuv3FE1V4X9Ak92S8VyUT2a2miW6Hj9sCAwEA +AQKCAgAefqdpC1p9ypO5l+nLJE+TLFMlVAqzaoCroUOPzi76+Xu2r1eC99mzsLoo +JgVZhkf9tfI7feCQyqFPTwl6gQIz26mPSY5rHTj1tdPX7gUHMmAsB9NOXX0IbZOc +pKOVSBFO495AO2VqPuwRDpw5Rjga+JYOCK/3id8tagvoCTQlMXkRPKjEu2ar5bBc +7sQxPZT28206q43wFKbI9SOZ56ySizNeJ1q9ej479ZlP7CEHWnElYKlW9h3inloT +79dm0Jk7K42eb6H5TaUiumaKNtHE7YmHAyw2ukU/SqftBilD3/vGTnRpzb5QuU1x +ShrM8CE4slr4TJXskrHyVhkOKf0A+RNKcZ6YICrEK1GGOTItNMcAGbHSTQBqVr5I +MApXfiEQfZetgSv0e9s+DGecB016QG/uJis6OyABV0hYBcnrNPNgpUhmDfy+IN6A +dpdjeSD/7iOXTEnCe9jrY5+0ot4rKZbl00GoZpb+3pYoBcpDiIVJvPxrWUZeC6lc +aZjgmjkg2IDXE7swevsStLqSRY3cusMTnRS1Ay37/gobE+f2rJZArY4GAobckT3R +im6q8cfzTH46wAZBuO6wHlmNfbDVZlvXx6zFhkqpefxRQ1V6Fa9LvkICcGmuhn1O +9p3DBphflcbHtMGwJpmmr0A3T/aakw2aMT9Qzda91SItlqxy+QKCAQEA8pE4vt3R +a9PEABO3rF5oWT8UaHfGBTDhoZnG1KDofDvoKrw2R09mz5nhROIDnVaQMtkWChhi +YhChOHZANp1K2xaCXqDRzbFVq1e/HB9Ag6mTZkT5R9Z088/vOWRIA9H3rk74GB+y +9iaCusk8NiIqlubvc7XaoTO4ChQ9X+p9QDAmvZVaDfsJGSXYvqfVfMy179mIlzCN +DSTePQsn1qEiPKltRNBysuluDJqa/PhcsGd7Oeo8SJuLFVOHk9rTuemvIbkxI8v8 +PZ6LD4ORCDsEKst5giqvVBl3yZ2ntuLlouxQGAg0F+VGxSvDYz81zwUHUmsSXDFo +gw22tWGCQpeFvQKCAQEA2uk2jzEnMvhklfh+3mAUNpPRUyAC7KHIB0vSgvkRTK1w +aqWF5jCx0onz9sW7lGYcM6RRXBJNi7z8NxouY2Bdw7ijGd7ZuL2ybpbsIetfMV16 +xGZCEL1JEHOnglEochGzspwKmADqWpV/suXKcOlrAZeH1y28TNOgaprTG8zr02DJ +CQrV8uJnE3HArEHtQdtjsPv/OhDEjEA3Gl2k3hRuhsf0rfeMby0QvvqyVb0Y4SEM +iIq8MP/dZPgNrDi4r3s0aps10ciAHyHtyuLv2PRkc5ddsBh5V2uEc+S0TTFRRW5N +uuM6E7uiuCbNrdd/1dfl55ojIYgI4TgR6lH5kmPJdwKCAQBPAK9rstE/fkRLBiD/ +WexAjQP3lnL/Q9FpEa2pmRK/S7+tE4nWJe1FVkgBaF9nAkeK2BuOhCye5e2sdw8o ++ofj3WvuqBBNHyHY4YZUAXXArB1e5L4QALAsrJ+soJW38M3rjrrNGJ3v/9D6Rwp+ +Uxht958rn6IqeK7LUZY/xB6xJj2n55niDc4Dy8jRJ9anhAEJsl8DZwO5sTVUympa +RDbjbQcyr3V8Af0ey8gI9lcx+TIwRbMGrupYstDofhARcCPjJu7zSr/HzfhawC4f +cSFFUuorU/2wtW7HUrrKHRJPwwm/GgTld35aP4uuqmq7F1cwJ8FeF5WDgZbtcmm7 +iKA9AoIBAHaOWSsBns4e8jK6atM6S5gnQ/V137+R+ofhC3g9NZ5GTByl2jeJZbS1 +W7fo7Kb5Cgr50cpAa1jjl+CrwDW3yfAmvcZUB6viqJD2EZppI5vTmZpmGx9/s+NC +D5UnKPVmGuD/W0lpLYKzdn5HrvSppXcuPrZNoa4l6rnxcaWbvJg00YuhH6+z58kD +ESr5ZWoGTB5cy6QB0sB2QqF318MiY52BC0VwTNElIe2cThrbF29Ne8EzCaqr15ZI +NPdxnKwE2KVnu6UKpkC2GleHwgfIi+KCNo4ZIxYyN4CgevlXXUFx9IzjZN+s/fon +obqlfCkvDOb6dk5BozV+LU2u6a/bdQ8CggEBAK/n4ELmvMRbkB4lr9BEN5GyWwvq +Zkd3e1EcefMpR/dXv+2YD95BlUa2kOhvGVNb2TtnTGcodtYgmWgkSCNwYGO15YX+ +f1OdqWJdJs/FEgMWyAElLHcrwFUJIDuBBLCZrYYKSgNoUM/Ddk/CaAGWT4lsq1U7 +X+GC8SVGkxyqV4GzI4uQ9/vxgbxP9zIbHQqa4BUUM4kzaWxTtJuaF+fHsafgTsDb +BDmOR/vVAhbM02Bw6xCqWNB337dIC2LJNmvmbTz/4lxe9mZIvN56UzcXAHSB8Xkj +fbn4ipVY7l/NC4LBLzRvEItTdNr7zAC4QpsU2/upw5PPhv5R4Q2FKV96zM8= +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pk8 Binary files differnew file mode 100644 index 00000000..fa38e322 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.x509.pem new file mode 100644 index 00000000..145a6560 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/com.android.apex.test.foo.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF1zCCA78CFGuWfjBtDvTQFqx0afFbqDDoozbTMA0GCSqGSIb3DQEBCwUAMIGm +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEiMCAGA1UEAwwZY29t +LmFuZHJvaWQuYXBleC50ZXN0LmZvbzAgFw0yMDEwMDUxNDU2NTJaGA80NzU4MDkw +MTE0NTY1MlowgaYxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYw +FAYDVQQHDA1Nb3VudGFpbiBWaWV3MRAwDgYDVQQKDAdBbmRyb2lkMRAwDgYDVQQL +DAdBbmRyb2lkMSIwIAYJKoZIhvcNAQkBFhNhbmRyb2lkQGFuZHJvaWQuY29tMSIw +IAYDVQQDDBljb20uYW5kcm9pZC5hcGV4LnRlc3QuZm9vMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAuB+RzoO8K96g3xYQng3cToQGYfCFVA47S3qUBlpj +Qr4IIw/R/Mo/0/LoK3OMBbjrpwYm0mBRrBttoX0jqlANDOrsk0T6jrWsIiz0iDar +5PEJjZpnfQILN76shJ/YgW4GQdLvjrZMtksBxKXLbID1uxBBD7KZzIpb7euiV06w +gaNJZyZg+2J0Isj8qI4H34x2GtjQxd2rN8KTLYOgatekkQCauHH/LLCxVA0K62v5 ++NPmrhBTQNO8TSMLmqci13jYcvs9Oj2qkwQKD9i4SMn1VoHrswjulXMibWEyu5xB +72vjwd9+xpLDcLxHDCQW8uFS7z/omhIE/DTC4QnmGmfz3gyi8O3sekLAdrP5YMz6 ++GrrNN8dwKr470g02oMtdpvTIC+4CcIMUuNBPvzsLUnCAxYTWo2QM8hcHx7Bs3XA +gRaeC2pEIcz9oWoTr/G/5ipdmLSDUVtBQAiNa3KouY9OAO7RtqmweLiXaBfPQSBM +lBWnyNhfEnhp++4Ef3LFDRpFfyo12XUysdmdreVjlZJ8MykJD5AU0EZ3gQbjcmWX +vnIWJTc3045dMCDn/1pOss8//q8doIhwVoTCRK0UAofZEajbtfDQTtggWyjMTPtR +pDtAJA1kmBLnCpjcrnj995pkz6rbMU76zDE7SzoVHE0zRnjDIiLCvNNScJ6oxY/R +/wcCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAp5kVSkIcuFWMl2X7yTyC3AuOfsOS +nqnEGs5ns1ij1SjSsogwt4apnjfgXdc3sye2eX3s1SWnxkIBwBQVOIrvSWgN1Wda +9UQ5uKcrbiz2yT1QMBD1VYsv/zzRPWNP7rzcR7szfaNQOje3BoCaQOkWutgDKi3O +kN8mz7VtfQniKvw/bbrSQMyVkQpy40XQTyJckfizomTVXlI79AoOVayER/Osgjp9 +8qwjtKVp/o3f3Nd5g2yS9GwsGBbXST0KhSB7bmsLHxPGyF1Zw8i6kKMuS9PX0E8r +lBGGMZceKDp0eGDXeEUdVIn7labsS2UpMKRuDurpsvol8s3lVBFhAD8yT3yLRNhM +c573H1ttrE/tlDOj6pdE/uz24WG+M11iKNuqW+/XnUiUjZpK+2Bt6ev4Yg9cDknn +ih9dr+/YEvCopBCgwaURhTOxDKNNDQCQbu10NnT+apnomWVgWIAmu9SJRE0dFHj/ +46TIUnmmnjg1tTq90yJYoZyXLib6r9PUIXtUEGtBbbjg0axhUqP1jr6FKYnnK3TP +NkxrkUVDUuI76qDhPIfqECymJz8fx97AJcPGT4qwOJKcCgft86RHOJIFr1AGVGSw +b+Vw2IWKNwyAqixjN/hhmNH2nlUdhuPPDk1GuTwlODuAvC/GTfOms7yacSeEO0wk +Sw6cwZWCm3nMiiE= +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/foo_test.cc b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/foo_test.cc new file mode 100644 index 00000000..34d44d4f --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/foo_test.cc @@ -0,0 +1,13 @@ +#include <iostream> +#include <string> + +#include "sharedlibstest.h" + +// This parameter gets modified by the build_artifacts.sh script. +#define FINGERPRINT "VERSION_XXX" + +int main() { + std::cout << "FOO_" << FINGERPRINT << " " + << sharedlibstest::getSharedLibsTestFingerprint(); + return 0; +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.foo/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/manifest.json new file mode 100644 index 00000000..c986f711 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.foo/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.foo", + "version": 1 +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/Android.bp new file mode 100644 index 00000000..3dd0349e --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/Android.bp @@ -0,0 +1,77 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.pony.key", + public_key: "com.android.apex.test.pony.avbpubkey", + private_key: "com.android.apex.test.pony.pem", +} + +android_app_certificate { + name: "com.android.apex.test.pony.certificate", + certificate: "com.android.apex.test.pony", +} + +apex { + name: "com.android.apex.test.pony", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.pony.key", + installable: false, + binaries: [ "pony_test" ], + dist: { + targets: ["sharedlibs_test"], + }, + updatable: false, +} + +cc_binary { + name: "pony_test", + srcs: ["pony_test.cc"], + shared_libs: [ + "libsharedlibtest", + ], + apex_available: [ "com.android.apex.test.pony" ], +} + +genrule { + name: "com.android.apex.test.pony_stripped", + out: ["com.android.apex.test.pony_stripped.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.pony", + "com.android.apex.test.pony.avbpubkey", + "com.android.apex.test.pony.pem", + "com.android.apex.test.pony.pk8", + "com.android.apex.test.pony.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode strip" + + " --key $(location com.android.apex.test.pony.pem)" + + " --input $(location :com.android.apex.test.pony)" + + " --output $(genDir)/com.android.apex.test.pony_stripped.apex" + + " --pk8key $(location com.android.apex.test.pony.pk8)" + + " --pubkey $(location com.android.apex.test.pony.avbpubkey)" + + " --x509key $(location com.android.apex.test.pony.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.avbpubkey Binary files differnew file mode 100644 index 00000000..f7af6e90 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pem new file mode 100644 index 00000000..c0e2965d --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEAu5za6xUAaREb/RXJGh0QOi+tPLjtmDkYL3COBcZ6dTsBYZuE +TkBwNgsvfk713Ddk6iOirjcakRLdj1K9lJdyBHMocNz8TjizO/3uRhLA34Fgog6Y +h0xwpDfb5gPyb18OvCVFuRLIremGa68zPnXl+p0kxzqafKICGjAp3x3ugNcSMXlN +0nKm4MSDiqZbTlAYutW+jTFr+e2VABbbCY6ijgQJMFDHNkA4QgW2rwEK63OaTImT +OKT44JnQ5VJpl8VN7HRaAyg5MUjVfuo/HSIkFpYZ5rxgWOZioqvJw+lFV6/TSrxk +P2JWQuu9mBDmuAoaEHE/yyLPLDKtQUquc6IwGBFG2/dn1WKoTB1X0TsCaIwTBGVV +n23foz/+uE2FJuh0mNaGWMHN4cOekbYhti1xIvOnHFlOIWdpmRRkIvUwjZYVkCu9 +UnPDHmhbA+3cr8scqc66JTESc1A/uoIGBtK10aY5jUyg4Tvdjc+PlwapeIAseYzK +x4eW1ujaTP+DEMBaamQLOwhvewxXubTyhjIN4epQma11pVfLwfuM7GAGqlyzTMXE +BQLkm17TIPEkb+wpU9zP4Xn5FshE8mb6k71fVXwWeBaHtF+tx4Ml5Jv2XUe23EDJ +5pnIB0sXyJcEjDlqbNCzfZn/LMJQHSd5h+wfwLoeL3U5TigpFSQp9MypSX0CAwEA +AQKCAgB7LdhaYrabRT2AJI6eE5j06xqt9KkiudHUS+0jg5YhZDVa9bWffxVtllh/ +cK5iAQjD5dPI2Ksbtyw7DtMkPW8B1u4ldCI/5WBgsi+AWI3D8XkVzcl9g8WtPHOn +iM3jK6FMDJjDk76o2NuF1kkp6FSv//8Gw8ZssB37PcYwFMHkW9E5JHDhDJ/ekYfg +P6tRNquV+AKdR2aieMfMgDUeCEVYQvQZgd/aEb4eMwwnyOJ3hrY3LFi55y70oGkU +N9DWchfgeOAklINAhZaPNpNruF/DaJfm86W6mMEIFwxpEb6SfQGYXyrept0GISuh +LO+exBsq0oBVCizF0xwH81Wo3EMAWnjAGMNxzKOdtJhP0VCL5asgMb6/QbKecko3 +2kZNAeW0aC8wFajCOarQQ5JgMcPdC2gGqjUakqHj+1QlbvG/KI1Xav0ET/OIFOuY +RAneqIaWVkU76tldm3mnp6b4vS6lsT/v/56sHtCUWGDmVF8zNkZ94LF6XcbEt6ZJ +p0Ssq0e35Z60ikAPuxNXNA0kgJBpq/V6j0Iqf9otGuEFLCy9AEtCKSYHVQvwRmMC +wLGr0L47p7RSKyMqWwJFZmMPfbAUz+IlYtxqzQ316L2NSCRnTHPV+thh9W/HIXvu +pciYkbjk2m4cpjiYEZRS6wLadfPGqMosNJX42G1INM2wEzfeQQKCAQEA35iBH9P+ +hWGYl1rmZBQfv2uYJPo6TDyM7kOrIctmPek8XPWNI9jnayE0Mmpf/eA1sTZpOzg2 +hVVc2QK8Baqxaq6JwNz/WVIvvD8kuCnsBLVYfAxEXgS5aF6NYbSg2oZf0CaMAFtM +xf0SIyhol9Xl9d48CrmumNZnkvpa7AzOvb9PRn8TXFJFkmkyDmDWvQVaBZbbkKMC +Ak3UA2nB8ypXfujLc2aBAeoSSw2d/EdoZW4TI/v5sFI+FM9Eg8KG3MP7rhaemptl +6DNh5PpFYl6smqE87Cb+0jmR3ek9jkjxXJxE4VrkBrDT4BYogb4TnNP7wdux5GJ7 +NqBAGQ8Mjj4ezQKCAQEA1s1dt/VxbOBPCRWlmIlWnmiY4pf3dkl1HOClNOwtviG7 +gCRmeIwtt4O2uwZOMd/JyYLBg4yieT8cCDbXSPXVop765bftBz/Ce5xU9sadLzI5 +sUdHRJkBtHsC12vXZrlrcnVauKAoArDcz5AgndqZ6i7lw5wsB2fQhITGJ1fYMW0L +RRugKU+wuAgur5GOoTPmryrf8WAbKYh9eeyoBHvK2u8JxsiUXUuJIPvOPjArmHLe +fxsoN8G3o4Rud9ugPEjsfT3RU8oA+9+LUMVbNl6q8RF9qherxKqmXwukh4C4iuVn +MyJ3FBAq4Z4BSbyirl4APC4Q41z//ZTYrAuFx9J1cQKCAQBY99ihLnw+3GeYCe5U +cgFz7D78r6hUv18gS0Kjzsge6FhBcN85HUxvvyWCzfrmDLmwisLyclqXUTEBlGn2 +I0Y2+b4MRKNCCka+M63Lrbqg4PuVWFg3xM91bPH6p6G9cexb6YqZdbqlqR33aVO8 +3rqCy2u+pMWJQP6zZ/SXqjz1GVNU7Klqeb3/FOZ6/CNV0PRR9wXklkftXMR4mzM2 +K2nnMIALqgS5G0cuH/v17v/mJBdvoQpoE0FqjFJpzxRUcZMKYSu4vw6chx1zu/Wx +v5QUbwXLvXR1d7zHvM/mdrW7MN7jgIPs+Z1Es+xoO5aYN20cZOtywZDfWoJGtks7 +qhIdAoIBAQCqntwXmH2tRwtgovIzlLvZ/imaq61kJvtAoex4ejXndfHy2ncOwAI8 +aAJI0rxf/2vQhe1iqd4QwyFoIO+mw6cbkn6m5A8CGBJKj6YpkyAd8h5Dg+PHSGZD +Twa1yLKDpTsE4tTaHFVLteLfeJN/77kcfH4Df9S1WTAXY0Pm0m8m63/tOAFjbypn +NBCpYsxRneFaOItDttw8hG9u3p2jWhWLDB7O6Fp5NNvK+FkdqrOmV3AGtLKgf154 +I2SADlNcL2yyGt1gWe+oIiwOT4WhTVcpP4R7DGxjPk4C50OcYpGzun7b7j96D1GQ +fyp0wMLUEFTNeKXvg9rPOWFWX5y3WaPxAoIBAQCiWoixjIhHK0f2yMvcYAzqb3A3 +AZs8IjU7phUxAa8LB4R2V5NOYsUl8xIZqZgoPjyJvlI0S82IjEuLqnsmh8GgtJxa +vOL8JLUX27kr/eOTkz6EnzYVtViFM+D2re7QvakGSh+D43sC29S+dUSMrB6XvFdP +pBbD6paLmefvgRyP8kxMTujsqVS9TSAqZLirxGSC28wgrRx9CRTy93MxUfw9spCO +V4/TO384iLZBlj6bDJc4g7YL1KYvZ6ZCCLWoq2TXyBHAOjfThttOoGFj1B4KqdZP +rO7xAyJCS3t++Nm1AWqmnA7wmNEhMruEZchqGOP7A5f/xcxdsjY0m5LTth50 +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pk8 Binary files differnew file mode 100644 index 00000000..104a5830 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.x509.pem new file mode 100644 index 00000000..2f96a293 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/com.android.apex.test.pony.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF2TCCA8ECFBuIrFu5kQxwNJM09/WyY4JaskQLMA0GCSqGSIb3DQEBCwUAMIGn +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEjMCEGA1UEAwwaY29t +LmFuZHJvaWQuYXBleC50ZXN0LnBvbnkwIBcNMjAxMTI3MTA1MTI1WhgPNDc1ODEw +MjQxMDUxMjVaMIGnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEW +MBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UE +CwwHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEj +MCEGA1UEAwwaY29tLmFuZHJvaWQuYXBleC50ZXN0LnBvbnkwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCrNP9VtH73dqoDIKN9GQ/XTizM2y79KgiUah8J +4w2FLn3Q/TKxH/NRJVhunAbNLbu8bqfOhETFFWWr8FD2CNq7j5ZSN/xZHHgo40F+ +U1+SisPErTVa9oma4fn62qrzwOZ/uDqwCwzv2Qx7SAYE52M4JAwMI16sV7KZGXBI +3MZApfRWcuk9FcH0SPCQReY+P4nRqpNqSdsDw0u6BFnPmc2oymkUy7VV7KLjpNo1 +si5M43U3QyLhx6abVdeeIdcC1ycNYEyYKWigBqWOWHIxj2FWUA/vF1OLJm6AR5Z/ +VYh9/7EPvPW+QsMwaM/YRgoCXAnbWA1T7XDJx6aksy6CHt53FjufNwqKC9npthYl +rdy7rv8XBfrp6/ZXzRhsEWXMHliGrDQF0vyHPgs9CsfdPQtR3H+7tV5nXfa4vj2S +WbkmZyOyDLTQku7luh2dDcn4Jw9yOoAc2cD7ql6H/Hd4MKrPX/SKahpK7ypZebgY +RRovxgwxrgFkP9J8/xW1GCY767a9E/sjVaFdQb1rAUWkNG2NBUDtFKdu7mg1KFyE +c1dTY4cpLiahWLyD845bLnunWaYqutRm3ufu10lBstqF1RkBHcqvoxfoa2IeNB23 +MPqKGaovQuFj+cRHsetC5W3VmROcvAvIJzhcvy3CK5JhvXdvGTkFaoA+6hifY2WU +Chli5wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCKEncgnGCwsHkC9UGg/HDwU97Q +ZH2kqNXdFRYURp8BmQbtNYuYSs9CYDpGugOjW7RILmr954KO+KNCo1OX6c4RWwQ0 +5LAh5+/FmBHiTJlWHRFGhL9WR8Mn1iwEsrANMyhoeoLwlKDVORM2/rS7APBM1pQl +gQfQHLt0cWoFpoL+pHElyuoVVKGB8sf06atcB/U8HND/xSY8fa7YjBOPYIMoUEvQ +aeN4JbxYNStqGDEz7+FoguXudQAvq8JFECsFwjWZd0/I5AjqcHiAG9ZaOrUiVRl2 +MdZSf2fjeSHXV3TH7i9f5vmcPUNERZxo/dutOTTVxlS42qrUaVwuSJzDG/MfXZWB +A6IB3P2qe5lzGl8mmvGKbX3ZXakf0OJaGb/Wn7GpsTynS3P6Zqa02oz89kquERey +PoHrR3tzNbUbDnjc/px2h2esG7E7E4bRc1PE6ndpvhs2vKypTpYO2v0cXWSk+xiR +eUZGPIXS7yMHSu4yHxoWUnWoEfzhKJ87gpk87wkSieL73zC/uMkjjyksmngN59VU +ODkUALrflWdHHynYiaA09zsDCjDuWDBXl17DNPV9lb4L0wLjJSklhuGsHo+wtW9k +HJx+H972Qk79C4/ZuNiRx83xt8hruqlky2wkZKB9cxYTiizoD9CXwpBl+KGo5oFX +ZjsDT4tnAHMa69yUoA== +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/manifest.json new file mode 100644 index 00000000..d1b5a60d --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.pony", + "version": 1 +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.pony/pony_test.cc b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/pony_test.cc new file mode 100644 index 00000000..761d2410 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.pony/pony_test.cc @@ -0,0 +1,13 @@ +#include <iostream> +#include <string> + +#include "sharedlibstest.h" + +// This parameter gets modified by the build_artifacts.sh script. +#define FINGERPRINT "VERSION_XXX" + +int main() { + std::cout << "PONY_" << FINGERPRINT << " " + << sharedlibstest::getSharedLibsTestFingerprint(); + return 0; +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/Android.bp new file mode 100644 index 00000000..78816364 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/Android.bp @@ -0,0 +1,74 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.sharedlibs.key", + public_key: "com.android.apex.test.sharedlibs.avbpubkey", + private_key: "com.android.apex.test.sharedlibs.pem", +} + +android_app_certificate { + name: "com.android.apex.test.sharedlibs.certificate", + certificate: "com.android.apex.test.sharedlibs", +} + +apex { + name: "com.android.apex.test.sharedlibs_stub", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.sharedlibs.key", + installable: false, + // We want to force libc++.so to be available in this stub APEX, so put an empty binary. + binaries: [ "noop" ], + updatable: false, + compile_multilib: "both", + multilib: { + both: { + binaries: [ + "noop", + ], + }, + }, +} + +genrule { + name: "com.android.apex.test.sharedlibs_generated", + out: ["com.android.apex.test.sharedlibs_generated.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.sharedlibs_stub", + "com.android.apex.test.sharedlibs.avbpubkey", + "com.android.apex.test.sharedlibs.pem", + "com.android.apex.test.sharedlibs.pk8", + "com.android.apex.test.sharedlibs.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode sharedlibs" + + " --key $(location com.android.apex.test.sharedlibs.pem)" + + " --input $(location :com.android.apex.test.sharedlibs_stub)" + + " --output $(genDir)/com.android.apex.test.sharedlibs_generated.apex" + + " --pk8key $(location com.android.apex.test.sharedlibs.pk8)" + + " --pubkey $(location com.android.apex.test.sharedlibs.avbpubkey)" + + " --x509key $(location com.android.apex.test.sharedlibs.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.avbpubkey Binary files differnew file mode 100644 index 00000000..b9a268d9 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pem new file mode 100644 index 00000000..ce939dcc --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA2EG+ZcurX/nUUNhpbARwgQGgLTQkXU3yNYRk6t5WoXI6t3D2 +/bePE27RRzUhplvs5SZthRl3pgkzhZtajObm31VaUX5cXGveux1bQdtfTeMmEfPl +TZ5bjM2aWV8QOpPQxUbL3tjCr2NB6sRX864nxl6RvFSgbVACBDVDBbdiHfq4UTGM +oL0APyO9PCfPueFXwOJyW+gtMc50m+s46E/7Je0hmCfx5xlTjnue0CzF9oDPvat6 +72hX5JFOxefHPIVW2/IFnjzMsyCwjF4WyB2eZjmH3Mtx44Xn5hH/a+1wMvvcJmjx +W4rexJVdDSr+RZTQC0Wr2eObk/6ib4gjXcjCr8cAdTB5g9IbKmGdOlHM3VavlkL9 +CMzNy+/w5UnqWrLJanC9nJba6PmwxG7n5doFODwB1ZypX3crrxjyT5saKN0KnSF3 +Ux3yt8SPnAr5QZ01ta4H26I65YwQi+Bqf/MOy+DgEDv3GEUTGLvK5YK2sDSWdIr4 +s3NRpkkUpAo/RZsqlxYCkYXtrdngP5r1ej0+uuzXt8cqXytcTqMpJzwDtNvlJiRG +iqYzQZE+p3JrLgUTnT+clezOU4SbbaK17z+gLFoy9rs15ChF+VM0GFeuWh0pTOv8 +UfpsOZrI4Cwgr1q8oUGhcXwj6cgqdxzlrRFqb7no6L0rajk2dQLBoCFIV4ECAwEA +AQKCAgAgHQMtEqWMRwkkSD6/b5lVTux+SfPsdxq0n8hsqD+tEc1uWDQVUSDJ/fbN +4DHzBkuTa7VvwmxmF4+zE3LK4a7/EymqWF1WzB3zI1Td3rm0UzrgB5vRfuaRbiax +htBeIn0qDm1P1lhyuwaa2jVFVmNJrdluYhLAqNTj0xT00FqdoRGl3PnJFMfomGIN +gMv0CmaBmh7pTv0HHGVske2NcfMVmrUWZzgg3T3vNqRKvZtYE6DFxaUn0BLdOka8 +VMLdVd+kIbh72wN6xivxbDdt2BghjgGC5CMxaj0ZiSqo2EWFDKmQepz8vw59msCK +qAvCQWrzgZEXdhkwTOvKLCk0UA+4zJoZ1hF3tZWStftqHB+zl1nsz3H8vBHzUaGJ +1ufJqBZrmKqJvMEMlxoV+A8ftV/SZTmdXrEb/CNLGAfNdf8mhjc5hU58HLQPHQxA +3IAj1Jyllc5hZHYXKAvu89ift0ZxZel8Nim+STmzpXiTvhia8+NEJlbOIp7JJf9L +OOhj33PUiIR9e2iPOrhPOkV4o3HVW/Dpwu2P1DSqOIoGxB0zFE+eZGGf9HVNOyy9 +xUUGRpzdD8M09gnI3Yszabo2HjKdKYQdmoSc7jzMMwvQGd9Zt08KvpsWj8mry7l9 +VVy8h4rWlxmJtC8aXOHI5thV/4jmwfRYbDEzh4Qt2XxVQCyvUQKCAQEA9uviNj0Q +FWtpeYULYMksz63JryaPeI3wAJtz6efYTcJ025qobTiHhoIER3TInsBBUQ3/YRYy +ZBtCeKpLxYA81IabIX9lO757RMslEb5KeD3Da2HqNnrh3hyRXgXXcUMbed8JunlC +5FRCRafikkj5ABvLHB9AYgAhS/vnTGQ7+XgpFsYi1kUGbJw+t4ly/ydWLjs5XoY7 +JdAuwnI/T8z31yYGG8T3oMSQV+BXT1doGNalKGupGMrUyvvqJQvScvxsVSWS8cRS +A1Xu91R0oMYhwnUkMZc7qYEVdGqcXFagSbnXufT4J7Bw+H7wbY17fYOC3cGkh3AK +FCa1d74Zd/3aswKCAQEA4DU7vukSOSDAf1hChkYXPzUJdrk31AVfeISYZmKqYah6 +65I2kzOnaszDQRg11wHXHSRhU0oVQxA77VTQBNQzOy+F7oB3o0cuQ+n6X5U8QQy4 +2dmgiYp3rkrqoH9KVn5VsYdwyYQHmE1IVMazo2YByv2N7aq4KN0MEgYUzizF38jX +hf7ubQA7mGr9I0Wm1keQJ1m9VZ06mPKLfG70LViJAAvCYzSLY/31FBGVNq4JlreP +EI4hgrYv7tzU6BqzrIdAmFOYOOsdRnUUW9+OhVzreU2EBAAmjK1jWa6FFAXGJEhi +/qyO2suvNdWsIGxfqt5yhPMevMqjyEPmvwpLF+5u+wKCAQEAj9ShZVS2bLOvsdB0 +60DkMGkcBUGh6uhK+B+VKpgZYFo4Nb9mApEeKJTNp034mriEk5FixAvo+HUEiEMy +de4YAPgTnzSVJHL1XQI0Kpy8xkO79G4JvwhfT0E20Bz4/QnJFHl+Mjf2ZghKvkZn +7SxClvSZoFz35N4MhzVJ6y6r3MpIrPJnUobMkjGFOuX+rXAdfDqVVWE9TO5yfmOM +S5CqgZGtlzlpwSUeq4GLejUA9w75D41+52knAMIzBrdXNBGjjQmhCeGAoF7LHxj8 +ArbG7X3MwnJEl50QgUqkoAj5v1hYuAJhFsVpWOagaEA0wcz8Su5ER3xU8p4FsKV0 +MngVjwKCAQEAqVuMpcioWz7CKW8h0Qtgw/3sCCIgaaclVoPSGoSs7te1AfyP/OEn +tSS22JTRFnftZbX1TlTHesDog31tJDil+i8Lm/yuYkeCSwqSdWDlAr35Y5VgDoTp +ol40nMeJ/4uub0s/hviURBcca+0sBGEpOYwNiVlLgpJ2a6bsUFDBpyiupCjNMMjc +O2WVkO8r9vBXk2HWArWhbabIdlXZW+dklQRM8WLfZ8iNN3uQmp0b4R0GlBrIdVPp +ISTuLeT9k3UW9fkvIs92baJCnqNfpJ1rwVUsQ1lZxSmzwipxm45A/WcwX+84eU0i +LCgavOMf4JHnL0X2EeV/kea4hdXgo1MXwQKCAQEApKpBnJ2DPxN/I0cHbOFLH1rS +A3lZvx0iOz7AFVK9laJ/794s5RdugXQ0MO4D0u+QrYN/Q3GnMpuNmHM8FRRNRIte +jdpPmFLyPOrqOCaeISmMnwZl1GZtOjZwOWRWP/7pK8cv6bI4H8tDyYq9T3b0K+TK +mqhqoHyN4aNrLCxggmhFS8wlK5UmuwJRfbAx4KGbfYk7fy4THkGjn5ZL3q9PO9Fa +1jWsiBFdnmRQvD4svnTswPEts/rJ9o1P5+AnWtVKK7Npq9eOFhPRJ3R3hruHJath +Cw999aol+hd093kcd7RzRmRjxUZ6oKxs1yRF7o9QQxnsmHRUT79Yy9LBOprvVA== +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pk8 Binary files differnew file mode 100644 index 00000000..933eb474 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.x509.pem new file mode 100644 index 00000000..95f96af8 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/com.android.apex.test.sharedlibs.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF5TCCA80CFBhOpuxTEQRD+MtXkeI8yP5rnPrwMA0GCSqGSIb3DQEBCwUAMIGt +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEpMCcGA1UEAwwgY29t +LmFuZHJvaWQuYXBleC50ZXN0LnNoYXJlZGxpYnMwIBcNMjAxMDA1MTYzMzIwWhgP +NDc1ODA5MDExNjMzMjBaMIGtMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZv +cm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQ +MA4GA1UECwwHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lk +LmNvbTEpMCcGA1UEAwwgY29tLmFuZHJvaWQuYXBleC50ZXN0LnNoYXJlZGxpYnMw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDjKxHP9SI0nMgAqYsSueNU +042arnj7pjdzhqHhRWr8Ub0mwMXx0ulkgZcnhCbdg52670T2wPhl/xi4/HL7PybU +sjt1QsjOaYcDdSrdxnsPe4RXZG4aE/+z06pm7TJ2jMGjJzS9x4AXbFNBxLsLp+gk +fuxhpGQklgymRUojtUFMWhnXURQz5wyYJGUrV32FpOo5JKtO4A42pNhcHNBzeQp0 +LVAwcQalmxra1maf0PXTg6L4yukyNzfLYcukjkQilzrkWGPqGexCzLF+WXiuznNj +BqPguKXvdsmSUIqAKGw6QHnuD6cAm3d2CuMzd4fjdOwzxFtfDEXS8f8ai/K+UEvx +eqf8eXV0cpKROrbf0CafcUli/3CKIc3UXoQr5kx4LSp0eJHOyaoGEF3Dex/k0dgJ +O80nNGTydhwvfMoZ4MOm+4yzDTqKu/Hw2ebM9po3vsfF3oy/hjEX0CaS4DnKDugg +WUwAbTHG5k/lbzy6mkjRDxwSzn6sDrhHM1rs45thiQF9hfZuDwUbaGmxFAq8+Gtu +xcr6bhryxaiO10MdMHI5KP1ZfZT49c+K8oVlTOsHxxxSK3eiQOs4k8A0jef7gfTP +rnDKbdq7JHr73bHXv495UtgZMCKtS3p4kvHs5PYblWvJmYImMrLjqdtuckYOeEIO +2N+79Rm8YAItX+SigohnZwIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAiJdApc590 +zoYl2w6NOj1xMljzta5Ld92lbJ3O4r1IoFQ6p+bSuV5RlPEC/kzR1G2WrZf66IrW +UOSxtDjY6Bt1GKO949BKTp4/gdrnanst7ai5VnPeGWstUIEJ5SmF7C9QBhWnGnFT +9zGdtbKnkcreZ81yabbNwAAwZWgX5hfkLSuMu7SzgLnzVDQOvbg96esWCbbBNPAl +KhZb5Bzc42TDlUxWfqIC0Of3GjcLu1Ukn4fwFphMD4wGoHIgpGD6975BVWESpmnV +tPDwiI+02Nha1aySZr/TiTId0AUucb6fqySqjCbOowv3DimKt+anwZjk1k/12TLb +Uro5nOPbWwQkQws7tnfNb8VBWoGNc+SJbh260rhv7gpwsvXOdbKbyR7mSqXylreh +DBUd/UL2eR1IrMuixK4bvmfVd7y+lxYKutEk+ifwSEuAubcbZ/dKH4PzjRnu1evv +4M/1sLH4LRd+qHoR4ylopX8dWn3xh2Xq9KFrnTXRk1nd1YeGqcyTz+H7bBhYZKLO +vPYTUb8ZYc/h0VGaABVywnb8wWZH4Op7ytKDybV2PGjhdLWaA3Y758ySkEab80ye +XSuSPevgTDc7ZV3/Ijs7cW6+XVKnWWl4H7DQxG4B9vWrKl0YejNJBUNaXoE5t1hN +TG2GM5D9sA/8HiW1JygwH0CBXMT5drYYjA== +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/manifest.json new file mode 100644 index 00000000..abdb7c42 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.sharedlibs", + "version": 1 +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/Android.bp b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/Android.bp new file mode 100644 index 00000000..9e803e32 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/Android.bp @@ -0,0 +1,66 @@ +// Copyright (C) 2020 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. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +apex_key { + name: "com.android.apex.test.sharedlibs_secondary.key", + public_key: "com.android.apex.test.sharedlibs_secondary.avbpubkey", + private_key: "com.android.apex.test.sharedlibs_secondary.pem", +} + +android_app_certificate { + name: "com.android.apex.test.sharedlibs_secondary.certificate", + certificate: "com.android.apex.test.sharedlibs_secondary", +} + +apex { + name: "com.android.apex.test.sharedlibs_secondary_stub", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", + key: "com.android.apex.test.sharedlibs_secondary.key", + installable: false, + // We want to force libc++.so to be available in this stub APEX, so put an empty binary. + binaries: [ "noop" ], + updatable: false, +} + +genrule { + name: "com.android.apex.test.sharedlibs_secondary_generated", + out: ["com.android.apex.test.sharedlibs_secondary_generated.apex"], + defaults: ["apexer_test_host_tools_list"], + dist: { + targets: ["sharedlibs_test"], + }, + srcs: [ + ":com.android.apex.test.sharedlibs_secondary_stub", + "com.android.apex.test.sharedlibs_secondary.avbpubkey", + "com.android.apex.test.sharedlibs_secondary.pem", + "com.android.apex.test.sharedlibs_secondary.pk8", + "com.android.apex.test.sharedlibs_secondary.x509.pem", + ], + tools: [ + "shared_libs_repack", + ], + cmd: "$(location shared_libs_repack) " + + " --mode sharedlibs" + + " --key $(location com.android.apex.test.sharedlibs_secondary.pem)" + + " --input $(location :com.android.apex.test.sharedlibs_secondary_stub)" + + " --output $(genDir)/com.android.apex.test.sharedlibs_secondary_generated.apex" + + " --pk8key $(location com.android.apex.test.sharedlibs_secondary.pk8)" + + " --pubkey $(location com.android.apex.test.sharedlibs_secondary.avbpubkey)" + + " --x509key $(location com.android.apex.test.sharedlibs_secondary.x509.pem)" + + " --tmpdir $(genDir)", +} diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.avbpubkey b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.avbpubkey Binary files differnew file mode 100644 index 00000000..3ccad0fa --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.avbpubkey diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pem new file mode 100644 index 00000000..5d4e1e1d --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKgIBAAKCAgEA2U7XN6RmrgLci4ZK2Osai9MqNIfx0gBX43DeIt6/1mQ5T590 +016wrGN6Oux0+LcdPU4PmpcyHw8+JVgdZjxG5c4icfWEuuc3Y+Pb6/FhvNwNrN5U +wx7/DFGyEWaRZEe3pk6oDETSYvY5CzQjrFz+mDDwNnho0zxsRD74GWumQOkdAg0W +7ihUq4/gCnjx+SeijTJAZcN0+oSldRa8+yN5zYdtO9gt9PGZLr87pvIFbcKp8QIT +sReN9eFoLHYYktq5y4YXIhhV7W0imsPzAq6gtzHC6+qgDPCvwD6/GTvGHgF+DEUl +/NuQgqgeVrFOEnHZ5qsawRwVeVq6sRd2ZQUdj+ix5KTS9/iKpg/cyLHCaXzEWWcZ +DtKnZKxdFTyXa4w+UshQkKDHPYOKfOXLaTTywz9oAZNDBQsLjUUIDZhyxRq7a5Zk +gUi5zG+Wx6r46pdSmMb76Qa2vasMQUYFoRINRfrsU6EJp55fjWCDzFXMIdm8nSkz +4DY6T+XLu76zAMrZZPHrQle2D5zi/mu2yvbHi0sbujUCFBFrPpLkaD8TE1gqxqfh +p5+xXD8tal6Mj7exLvRCadNaQ1R8CtcZhVxtUPmGVwlUZ6aF1QpARQtfZuVBEiut +kyfTb9ns8r3xmxsUOSwJA6SgRuN0gvXIfDnNumDaP9dkuGE98uQpTu8kZbsCAwEA +AQKCAgEAg0oR1yk5bAqIirdxAwtP94h16FT18eWJM/2eB71Cc9oLkiKJp6Z+4Tgc +wfrYVOf0/3PpE4IjowZHirJo2Lq0LuVShD1MmstU+MHSvgMRBNSCYp3U4ioY961o +AwFP+CEoQI8nEnqGDYorPqyanOl2XCa9CnvHAVBxLO5KYLlcMb1lbDbSUsMFHL4J +IuqdbuXWXK4uoAzt4OlBObOqK6TsUxNuGIjsgx9waADbnmp8gyroF5ckpIrRlus/ +UBVtlVQWinMSCORhDdgw3wZiDI3KxcOHu5b+abMEzAZc2Hb3pGtMZ0djwxg4f+fo +pIHs5FHqz2Uy/dbk1nPNdW7ydegYShuQmsvbOT6R8Qi25c9SiirXKOtdCM50JTW7 +SRaReqhiy828m9DOrwSXXiHrmyNevpwGQtRELGi14rgPqnpuUNaOmMEGAstQ+xcU +Hl0DVuxWXN37iEKa4zv/LKwRzBVxlrSC1M3ufiOi2tVl5btMrdz5jZwDLYQxI7TX +Nu0l/tEWD98/sJyOk4bmGUFrD5+oBpPGw/MrK6k7DmJjp8W1NRiiIACI/flzRWcA +la7lPk23kG0/lmrWY87NQhEaPR9RMqmizlnF3VzWyDxXgZcn4ucjRmH7qhQLsZCC +VIEI3oPoQSd7w+pHghLTA4R0KqW2ur/d05mkOmLfGnL27wPqaJECggEBAPX2tgHr +DLIapja0HtxYNx0I5H/lHi3bzBI426h7317oIQpASKsqaIu/jOSDt5gLC0g+qSJi +gT/l454bkdN4lTAMvY+3vJg3cN4U8J9GAejZi2X23Hed1QVuBSckz3CrHanqVBMN +Nj9FsfQpqLJpewVqqdKR9rK+1ZNyMWfEO9M3pxlpZyWhPHg7KtbqBo0hS5Stlqpy +xOo4nF4WC+OGNUo02R12ETJM4AmMs28Z5oNYZZ6K4cqKReNcDqt891ryXrow6aq9 +JBYz8sCeBQSTnPNnhbLJH/YOVMlhdAJhBcxEDbNKM6vmFQOuBEp12tjQoXsiv54C +uBou7aRft8uGeHMCggEBAOIszCN1ZvD+xbaLDtKgPrAuDVRaZuZ9vb2TVB7Gcpto +hCXU5GoapqQgjGplsXOQ1FMRozeRL4lQWfPfiueLAV3AeIHm0qjD2aanhJPvPrnb +UCOdWRID99q9/s6bP98XiDseZiTHNlWitwpbWPAvmW2/08otmf4TgeLrA8p75CtK +qJXU7o7fWTBFHF238rusK3N9t0Vikb0Cx1xDui6b5PeWtqAREV1/swP0J0av1fwK +OuvZ8VdUjb5KMUMtGgMEWA277kYuryalu+oQH+bfe3XXUJS7fAI7GlokVqhJ8CLZ +eSSZDtdqHib968cA4wr55nt1rEiDWzG7f2GeZra6s5kCggEAN53QrABdP4ydFvOF +oudjlvIi0PSa7V2s+FXY/XD9IjW0+t9sTx/owejPUACkrAGbTHu2vOqvNSajYGX1 +hG7YtSO8XVn7kCPBJsZvXmRzHBbM2YKHeZi7yV2GVsKREXXv4DL3TdOH96inw4ED +/0uwoJnsyots0CAspQmGOGN775e+9hUKWMzrongmiLAkSRdFQto5nlMTSa8BVJkB +mTIIrL3kdi/zVX9ijWY+UJn3sK11VPMseSLpCK8RNh+swujZGJrky1G3bjnS41EX +62ABdlxrM/EchAPbkimyFLOhnv2oZ2kY4/7Ds7BOkhOyJ6KNUQ2bbHxK6si/vZJT +OfcvFwKCAQEA0nnVzvmmPocY/vMRbDjrnZB9nw4xzDUfqZe9JJaQeMcekwY3OfZr +NTmE8k6IgH8618MGHOPjVOmNjEFvRmI5d0Fx45EmYR9BILGr0u9FdDf/r+Txyq4e +rVU6FpKrMbT4deuoKnmourCdnem8LmhdY6CsOu2M7MDCkqUZ9gitIQxtLmHlTtfS +a/UknKJeJP/nv6YyM0OzVC2N0PLGBDHXNgDvGq5HdrcrpHZFRqbDf7UVd/5tdVOe +RINOrLEAD+au+rj02CMBo/l/kiZHSdaXUeZ5eq+ui3Ts5Q4EBsAn1IaFEeXNxfFe +9fI+xAazQrekISg0l5aF+xX9SJ7b/xhnoQKCAQEA7vvi5zUalTNRreeExX0wiKRj +sCvUhQoykimKpWzSUg6DpdF+DhK59/ERVSkA4C2WwHEYqZdrgEKnmqOZQkqgtq1B +O5xxioRtn6VsZRYjaRw5Cq/Ej1AeDgQr+SH29kBtym81mMBbkPnFoBxKCjuuVn9u +4Kjg910wxErzipjDB2alfkuFCqNz9IZd2XSmpmGhpE/hBlZP4slJXTX77nIxRTHB +Vq5gehJoEhLNsFAu34BgrXRIhzSBALdalbepNrHh2br87rJpjxeRASbuyyNdgZyl +wk+aJxHo6RpMD7xHxsxv5kmRcLITPICk+ysLZ5ZHMaEuGIVFxDdOoFGylRV4wQ== +-----END RSA PRIVATE KEY----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pk8 b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pk8 Binary files differnew file mode 100644 index 00000000..2eee60f0 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.pk8 diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.x509.pem b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.x509.pem new file mode 100644 index 00000000..74dd144f --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/com.android.apex.test.sharedlibs_secondary.x509.pem @@ -0,0 +1,34 @@ +-----BEGIN CERTIFICATE----- +MIIF+TCCA+ECFGnrAltwoToH4tPw7aN36oFBb8zxMA0GCSqGSIb3DQEBCwUAMIG3 +MQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91 +bnRhaW4gVmlldzEQMA4GA1UECgwHQW5kcm9pZDEQMA4GA1UECwwHQW5kcm9pZDEi +MCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTEzMDEGA1UEAwwqY29t +LmFuZHJvaWQuYXBleC50ZXN0LnNoYXJlZGxpYnNfc2Vjb25kYXJ5MCAXDTIwMTEy +NzEwNTE0N1oYDzQ3NTgxMDI0MTA1MTQ3WjCBtzELMAkGA1UEBhMCVVMxEzARBgNV +BAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxEDAOBgNVBAoM +B0FuZHJvaWQxEDAOBgNVBAsMB0FuZHJvaWQxIjAgBgkqhkiG9w0BCQEWE2FuZHJv +aWRAYW5kcm9pZC5jb20xMzAxBgNVBAMMKmNvbS5hbmRyb2lkLmFwZXgudGVzdC5z +aGFyZWRsaWJzX3NlY29uZGFyeTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBANLZWZWEtzYdHL+yio1j16r4aDCtRMdTFT8dWhXi/khEodZ9PZHV73eAWl7o +5AHO2h/4To6hqxKlzl5lbNWT/Xu7OFvhaqy1gD2XiOyZxqNITba3SV4qDnWYAMHc +JdcYKv/F5UMeiXQueBJ2HaWcWoZPOmHwfJbMxSvr+LOwtI1vvR38G68uIFyR0Rlb +jQggv/WsudLlB++0c+jOtPLmD65h7dXXXlqGvu+7Rt5GWYxtN/sIZ0KPqtLk1JQD +nIa5E05UDcqfGnyo5mz7t9Tj+4dyH/B+fY9JMXJMNMcy47CfdhR4NfnsUTmLqXRc +fOwSRfuodMbCd4WAgA6e6KHjlBO89NOA3QamQDyhT3XURrMCMeRQvb155dEoHCs5 +FDBu138hku5aylLfiHkEHfwEDu9wsWAd3CWsBfqOMAEQG7AB+4lCaGVhYKMxKWjM +Pdqzfy47ODHrMEprfrYlOkdJ+9Fu9HJZvEMQXh7tleoOSIuvqmL1ksRSUXsE2JHJ +KC/hoBCcVsMsujxn7tQilB0e+TLDml08dIrrGLV1UiqU/80q0MC2Bwinne21KAtY +UMJd2pImZr3H4z7J6wQmuevvcZksaEwWXtQzIoXenmaSW9bwqVy6k+DushOghGy/ +CAeeJAc35sUFsa8ysp0aPARZIqCYiesGdiO/wW+UVibo2JyLAgMBAAEwDQYJKoZI +hvcNAQELBQADggIBAFE7g7ZTGqSDpJ/PyLKLp+oItJ+JNJVd5UbLvLMiA4t7QTtE +0aPCxMGybJGeYbs16OB3ZqWZlVyjirEXAmSH2HZxO1uIeDcCjndSCfz+oxmBs27j +C8t5BzExaelpP/J9nyKObzaZ1EJ+KdTqPVmrhgN8uUU82mRt5oJZuxGbHNdbzxGh +zXHi+q5oecWaiBFDSF9pvKFp7nvbW4MIaGcm9Flx9JdYhgzRfXeJV7EsKE2Kxflj +nojgyUzjvkNTa/wRASgwr/hgpPL0mpD9gSejxOA5wRGLknDlIfsfEh5cJTFiBO5b +MZiSHe7Ds+8GWyKk8y8G2YtB0Z5HxD91rzPA2W/JEDHhoI8jT47I3JuOyJMjC9DV +kAu4nhReBilOmU+oS1Iv+TheslPGLEZ7JOHsmyQh5X5H0D+YH7H9L8BOWEBlNBhy +zPlMKNWsvMZmem/fhuXK1xUUWZuqoj2tF2fMmiYf6fSNRb7dQI10uBslxX7myNyj +pqZNSk/E9V650UC5JixpOtPxrSsPXifOLwB7OEkT5v7v/MoBqlSfqve9/62Ktnuc +NC9Rt3fqU+VOs6tVUPGnSJRDc2uhvU8rJZN86e1xX8d7lhHZwO8sJtaxZ2KYduM1 +ncEODTAxK7wQ041CS7H/kULG6CEPnIg0a1ZjASWhQRbQ1Rj/Y40CmHxThnyx +-----END CERTIFICATE----- diff --git a/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/manifest.json b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/manifest.json new file mode 100644 index 00000000..80da6f14 --- /dev/null +++ b/tests/testdata/sharedlibs/build/com.android.apex.test.sharedlibs_secondary/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "com.android.apex.test.sharedlibs_secondary", + "version": 1 +} diff --git a/tests/src/com/android/tests/apex/AdbdHostTest.java b/tests/testdata/sharedlibs/build/include/sharedlibstest.h index 445ca88b..a2f0cdc9 100644 --- a/tests/src/com/android/tests/apex/AdbdHostTest.java +++ b/tests/testdata/sharedlibs/build/include/sharedlibstest.h @@ -14,17 +14,15 @@ * limitations under the License. */ -package com.android.tests.apex; +#ifndef SHAREDLIBSTEST_H_ +#define SHAREDLIBSTEST_H_ -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +#include <string> -import org.junit.Ignore; -import org.junit.runner.RunWith; +namespace sharedlibstest { -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -@Ignore // TODO(b/148759193): remove when test is fixed -public class AdbdHostTest extends ApexE2EBaseHostTest { -} +std::string getSharedLibsTestFingerprint(); + +} // namespace sharedlibstest + +#endif // SHAREDLIBSTEST_H_ diff --git a/tests/testdata/sharedlibs/build/noop.cc b/tests/testdata/sharedlibs/build/noop.cc new file mode 100644 index 00000000..cfd2ef57 --- /dev/null +++ b/tests/testdata/sharedlibs/build/noop.cc @@ -0,0 +1,9 @@ +#include <iostream> + +#include "sharedlibstest.h" + +int main() { + std::cout << "This binary should never be executed"; + std::cout << sharedlibstest::getSharedLibsTestFingerprint(); + return 1; +}
\ No newline at end of file diff --git a/tests/testdata/sharedlibs/build/shared_libs_repack.py b/tests/testdata/sharedlibs/build/shared_libs_repack.py new file mode 100644 index 00000000..31b9a6bc --- /dev/null +++ b/tests/testdata/sharedlibs/build/shared_libs_repack.py @@ -0,0 +1,421 @@ +# Copyright (C) 2020 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. +# +# 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. +"""Repacking tool for Shared Libs APEX testing.""" + +import argparse +import hashlib +import logging +import os +import shutil +import subprocess +import sys +import tempfile +from zipfile import ZipFile + +import apex_build_info_pb2 +import apex_manifest_pb2 + +logger = logging.getLogger(__name__) + +def comma_separated_list(arg): + return arg.split(',') + + +def parse_args(argv): + parser = argparse.ArgumentParser( + description='Repacking tool for Shared Libs APEX testing') + + parser.add_argument('--input', required=True, help='Input file') + parser.add_argument('--output', required=True, help='Output file') + parser.add_argument( + '--key', required=True, help='Path to the private avb key file') + parser.add_argument( + '--pk8key', + required=True, + help='Path to the private apk key file in pk8 format') + parser.add_argument( + '--pubkey', required=True, help='Path to the public avb key file') + parser.add_argument( + '--tmpdir', required=True, help='Temporary directory to use') + parser.add_argument( + '--x509key', + required=True, + help='Path to the public apk key file in x509 format') + parser.add_argument( + '--mode', default='strip', choices=['strip', 'sharedlibs']) + parser.add_argument( + '--libs', + default='libc++.so,libsharedlibtest.so', + type=comma_separated_list, + help='Libraries to strip/repack. Expects comma separated values.') + return parser.parse_args(argv) + + +def run(args, verbose=None, **kwargs): + """Creates and returns a subprocess.Popen object. + + Args: + args: The command represented as a list of strings. + verbose: Whether the commands should be shown. Default to the global + verbosity if unspecified. + kwargs: Any additional args to be passed to subprocess.Popen(), such as env, + stdin, etc. stdout and stderr will default to subprocess.PIPE and + subprocess.STDOUT respectively unless caller specifies any of them. + universal_newlines will default to True, as most of the users in + releasetools expect string output. + + Returns: + A subprocess.Popen object. + """ + if 'stdout' not in kwargs and 'stderr' not in kwargs: + kwargs['stdout'] = subprocess.PIPE + kwargs['stderr'] = subprocess.STDOUT + if 'universal_newlines' not in kwargs: + kwargs['universal_newlines'] = True + if verbose: + logger.info(' Running: \"%s\"', ' '.join(args)) + return subprocess.Popen(args, **kwargs) + + +def run_and_check_output(args, verbose=None, **kwargs): + """Runs the given command and returns the output. + + Args: + args: The command represented as a list of strings. + verbose: Whether the commands should be shown. Default to the global + verbosity if unspecified. + kwargs: Any additional args to be passed to subprocess.Popen(), such as env, + stdin, etc. stdout and stderr will default to subprocess.PIPE and + subprocess.STDOUT respectively unless caller specifies any of them. + + Returns: + The output string. + + Raises: + ExternalError: On non-zero exit from the command. + """ + proc = run(args, verbose=verbose, **kwargs) + output, _ = proc.communicate() + if output is None: + output = '' + # Don't log any if caller explicitly says so. + if verbose: + logger.info('%s', output.rstrip()) + if proc.returncode != 0: + raise RuntimeError( + 'Failed to run command \'{}\' (exit code {}):\n{}'.format( + args, proc.returncode, output)) + return output + + +def get_container_files(apex_file_path, tmpdir): + dir_name = tempfile.mkdtemp(prefix='container_files_', dir=tmpdir) + with ZipFile(apex_file_path, 'r') as zip_obj: + zip_obj.extractall(path=dir_name) + files = {} + for i in [ + 'apex_manifest.json', 'apex_manifest.pb', 'apex_build_info.pb', 'assets', + 'apex_payload.img', 'apex_payload.zip' + ]: + file_path = os.path.join(dir_name, i) + if os.path.exists(file_path): + files[i] = file_path + + image_file = files.get('apex_payload.img') + if image_file is None: + image_file = files.get('apex_payload.zip') + + files['apex_payload'] = image_file + + return files + + +def extract_payload_from_img(img_file_path, tmpdir): + dir_name = tempfile.mkdtemp(prefix='extracted_payload_', dir=tmpdir) + cmd = [ + _get_host_tools_path('debugfs_static'), '-R', + 'rdump ./ %s' % dir_name, img_file_path + ] + run_and_check_output(cmd) + + # Remove payload files added by apexer and e2fs tools. + for i in ['apex_manifest.json', 'apex_manifest.pb']: + if os.path.exists(os.path.join(dir_name, i)): + os.remove(os.path.join(dir_name, i)) + if os.path.isdir(os.path.join(dir_name, 'lost+found')): + shutil.rmtree(os.path.join(dir_name, 'lost+found')) + return dir_name + + +def run_apexer(container_files, payload_dir, key_path, pubkey_path, tmpdir): + apexer_cmd = _get_host_tools_path('apexer') + cmd = [ + apexer_cmd, '--force', '--include_build_info', '--do_not_check_keyname' + ] + cmd.extend([ + '--apexer_tool_path', + os.path.dirname(apexer_cmd) + ':prebuilts/sdk/tools/linux/bin' + ]) + cmd.extend(['--manifest', container_files['apex_manifest.pb']]) + if 'apex_manifest.json' in container_files: + cmd.extend(['--manifest_json', container_files['apex_manifest.json']]) + cmd.extend(['--build_info', container_files['apex_build_info.pb']]) + if 'assets' in container_files: + cmd.extend(['--assets_dir', container_files['assets']]) + cmd.extend(['--key', key_path]) + cmd.extend(['--pubkey', pubkey_path]) + + # Decide on output file name + apex_suffix = '.apex.unsigned' + fd, fn = tempfile.mkstemp(prefix='repacked_', suffix=apex_suffix, dir=tmpdir) + os.close(fd) + cmd.extend([payload_dir, fn]) + + run_and_check_output(cmd) + return fn + + +def _get_java_toolchain(): + java_toolchain = 'java' + if os.path.isfile('prebuilts/jdk/jdk11/linux-x86/bin/java'): + java_toolchain = 'prebuilts/jdk/jdk11/linux-x86/bin/java' + + java_dep_lib = ( + os.path.join(os.path.dirname(_get_host_tools_path()), 'lib64') + ':' + + os.path.join(os.path.dirname(_get_host_tools_path()), 'lib')) + + return [java_toolchain, java_dep_lib] + + +def _get_host_tools_path(tool_name=None): + # This script is located at e.g. + # out/soong/host/linux-x86/bin/shared_libs_repack/shared_libs_repack.py. + # Find the host tools dir by going up two directories. + dirname = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + if tool_name: + return os.path.join(dirname, tool_name) + return dirname + + +def sign_apk_container(unsigned_apex, x509key_path, pk8key_path, tmpdir): + fd, fn = tempfile.mkstemp(prefix='repacked_', suffix='.apex', dir=tmpdir) + os.close(fd) + java_toolchain, java_dep_lib = _get_java_toolchain() + + cmd = [ + java_toolchain, '-Djava.library.path=' + java_dep_lib, '-jar', + os.path.join( + os.path.dirname(_get_host_tools_path()), 'framework', 'signapk.jar'), + '-a', '4096', x509key_path, pk8key_path, unsigned_apex, fn + ] + run_and_check_output(cmd) + return fn + + +def compute_sha512(file_path): + block_size = 65536 + hashbuf = hashlib.sha512() + with open(file_path, 'rb') as f: + fb = f.read(block_size) + while len(fb) > 0: + hashbuf.update(fb) + fb = f.read(block_size) + return hashbuf.hexdigest() + + +def parse_fs_config(fs_config): + configs = fs_config.splitlines() + # Result is set of configurations. + # Each configuration is set of items as [file path, uid, gid, mode]. + # All items are stored as string. + result = [] + for config in configs: + result.append(config.split()) + return result + + +def config_to_str(configs): + result = '' + for config in configs: + result += ' '.join(config) + '\n' + return result + + +def _extract_lib_or_lib64(payload_dir, lib_full_path): + # Figure out if this is lib or lib64: + # Strip out the payload_dir and split by / + libpath = lib_full_path[len(payload_dir):].lstrip('/').split('/') + return libpath[0] + + +def main(argv): + args = parse_args(argv) + apex_file_path = args.input + + container_files = get_container_files(apex_file_path, args.tmpdir) + payload_dir = extract_payload_from_img(container_files['apex_payload.img'], + args.tmpdir) + libs = args.libs + assert len(libs)> 0 + + lib_paths = [os.path.join(payload_dir, lib_dir, lib) + for lib_dir in ['lib', 'lib64'] + for lib in libs + if os.path.exists(os.path.join(payload_dir, lib_dir, lib))] + + assert len(lib_paths) > 0 + + lib_paths_hashes = [(lib, compute_sha512(lib)) for lib in lib_paths] + + if args.mode == 'strip': + # Stripping mode. Add a reference to the version of libc++.so to the + # requireSharedApexLibs entry in the manifest, and remove lib64/libc++.so + # from the payload. + pb = apex_manifest_pb2.ApexManifest() + with open(container_files['apex_manifest.pb'], 'rb') as f: + pb.ParseFromString(f.read()) + for lib_path_hash in lib_paths_hashes: + basename = os.path.basename(lib_path_hash[0]) + libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0]) + assert libpath in ('lib', 'lib64') + pb.requireSharedApexLibs.append(os.path.join(libpath, basename) + ':' + + lib_path_hash[1]) + # Replace existing library with symlink + symlink_dst = os.path.join('/', 'apex', 'sharedlibs', + libpath, basename, lib_path_hash[1], + basename) + os.remove(lib_path_hash[0]) + os.system('ln -s {0} {1}'.format(symlink_dst, lib_path_hash[0])) + # + # Example of resulting manifest: + # --- + # name: "com.android.apex.test.foo" + # version: 1 + # requireNativeLibs: "libc.so" + # requireNativeLibs: "libdl.so" + # requireNativeLibs: "libm.so" + # requireSharedApexLibs: "lib/libc++.so:23c5dd..." + # requireSharedApexLibs: "lib/libsharedlibtest.so:870f38..." + # requireSharedApexLibs: "lib64/libc++.so:72a584..." + # requireSharedApexLibs: "lib64/libsharedlibtest.so:109015..." + # -- + # To print uncomment the following: + # from google.protobuf import text_format + # print(text_format.MessageToString(pb)) + with open(container_files['apex_manifest.pb'], 'wb') as f: + f.write(pb.SerializeToString()) + + if args.mode == 'sharedlibs': + # Sharedlibs mode. Mark in the APEX manifest that this package contains + # shared libraries. + pb = apex_manifest_pb2.ApexManifest() + with open(container_files['apex_manifest.pb'], 'rb') as f: + pb.ParseFromString(f.read()) + del pb.requireNativeLibs[:] + pb.provideSharedApexLibs = True + with open(container_files['apex_manifest.pb'], 'wb') as f: + f.write(pb.SerializeToString()) + + pb = apex_build_info_pb2.ApexBuildInfo() + with open(container_files['apex_build_info.pb'], 'rb') as f: + pb.ParseFromString(f.read()) + + canned_fs_config = parse_fs_config(pb.canned_fs_config.decode('utf-8')) + + # Remove the bin directory from payload dir and from the canned_fs_config. + shutil.rmtree(os.path.join(payload_dir, 'bin')) + canned_fs_config = [config for config in canned_fs_config + if not config[0].startswith('/bin')] + + # Remove from the canned_fs_config the entries we are about to relocate in + # different dirs. + source_lib_paths = [os.path.join('/', libpath, lib) + for libpath in ['lib', 'lib64'] + for lib in libs] + # We backup the fs config lines for the libraries we are going to relocate, + # so we can set the same permissions later. + canned_fs_config_original_lib = {config[0] : config + for config in canned_fs_config + if config[0] in source_lib_paths} + + canned_fs_config = [config for config in canned_fs_config + if config[0] not in source_lib_paths] + + # We move any targeted library in lib64/ or lib/ to a directory named + # /lib64/libNAME.so/${SHA512_OF_LIBCPP}/ or + # /lib/libNAME.so/${SHA512_OF_LIBCPP}/ + # + for lib_path_hash in lib_paths_hashes: + basename = os.path.basename(lib_path_hash[0]) + libpath = _extract_lib_or_lib64(payload_dir, lib_path_hash[0]) + tmp_lib = os.path.join(payload_dir, libpath, basename + '.bak') + shutil.move(lib_path_hash[0], tmp_lib) + destdir = os.path.join(payload_dir, libpath, basename, lib_path_hash[1]) + os.makedirs(destdir) + shutil.move(tmp_lib, os.path.join(destdir, basename)) + + canned_fs_config.append( + ['/' + libpath + '/' + basename, '0', '2000', '0755']) + canned_fs_config.append( + ['/' + libpath + '/' + basename + '/' + lib_path_hash[1], + '0', '2000', '0755']) + + if os.path.join('/', libpath, basename) in canned_fs_config_original_lib: + config = canned_fs_config_original_lib[os.path.join( + '/', + libpath, + basename)] + canned_fs_config.append([os.path.join('/', libpath, basename, + lib_path_hash[1], basename), + config[1], config[2], config[3]]) + else: + canned_fs_config.append([os.path.join('/', libpath, basename, + lib_path_hash[1], basename), + '1000', '1000', '0644']) + + pb.canned_fs_config = config_to_str(canned_fs_config).encode('utf-8') + with open(container_files['apex_build_info.pb'], 'wb') as f: + f.write(pb.SerializeToString()) + + try: + for lib in lib_paths: + os.rmdir(os.path.dirname(lib)) + except OSError: + # Directory not empty, that's OK. + pass + + repack_apex_file_path = run_apexer(container_files, payload_dir, args.key, + args.pubkey, args.tmpdir) + + resigned_apex_file_path = sign_apk_container(repack_apex_file_path, + args.x509key, args.pk8key, + args.tmpdir) + + shutil.copyfile(resigned_apex_file_path, args.output) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/tests/src/com/android/tests/apex/ExtServicesHostTest.java b/tests/testdata/sharedlibs/build/sharedlibstest.cpp index ff5d6942..bac580ef 100644 --- a/tests/src/com/android/tests/apex/ExtServicesHostTest.java +++ b/tests/testdata/sharedlibs/build/sharedlibstest.cpp @@ -14,15 +14,17 @@ * limitations under the License. */ -package com.android.tests.apex; +#include "sharedlibstest.h" -import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; +#include <string> -import org.junit.runner.RunWith; +namespace sharedlibstest { -/** - * Test to check if Apex can be staged, activated and uninstalled successfully. - */ -@RunWith(DeviceJUnit4ClassRunner.class) -public class ExtServicesHostTest extends ApexE2EBaseHostTest { +// This parameter gets modified by the build_artifacts.sh script. +#define FINGERPRINT "VERSION_XXX" + +std::string getSharedLibsTestFingerprint() { + return std::string("SHARED_LIB_") + FINGERPRINT; } + +} // namespace sharedlibstest diff --git a/tests/testdata/sharedlibs/prebuilts/Android.bp b/tests/testdata/sharedlibs/prebuilts/Android.bp new file mode 100644 index 00000000..1ef2977e --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/Android.bp @@ -0,0 +1,301 @@ +// Copyright (C) 2020 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. + +// This file is auto-generated by +// ./system/apex/tests/testdata/sharedlibs/build/build_artifacts.sh +// Do NOT edit manually. + +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +prebuilt_apex { + name: "com.android.apex.test.bar_stripped.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.bar_stripped.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.bar_stripped.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.bar_stripped.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.bar_stripped.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.bar_stripped.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.bar_stripped.v2.libvY_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.bar_stripped.v2.libvY.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.bar_stripped.v2.libvY.apex", + }, + x86: { + src: "x86/com.android.apex.test.bar_stripped.v2.libvY.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.bar_stripped.v2.libvY.apex", + }, + }, + filename: "com.android.apex.test.bar_stripped.v2.libvY.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.bar.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.bar.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.bar.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.bar.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.bar.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.bar.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.bar.v2.libvY_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.bar.v2.libvY.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.bar.v2.libvY.apex", + }, + x86: { + src: "x86/com.android.apex.test.bar.v2.libvY.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.bar.v2.libvY.apex", + }, + }, + filename: "com.android.apex.test.bar.v2.libvY.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.baz_stripped.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.baz_stripped.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.baz_stripped.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.baz_stripped.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.baz_stripped.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.baz_stripped.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.foo_stripped.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.foo_stripped.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.foo_stripped.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.foo_stripped.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.foo_stripped.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.foo_stripped.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.foo_stripped.v2.libvY_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.foo_stripped.v2.libvY.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.foo_stripped.v2.libvY.apex", + }, + x86: { + src: "x86/com.android.apex.test.foo_stripped.v2.libvY.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.foo_stripped.v2.libvY.apex", + }, + }, + filename: "com.android.apex.test.foo_stripped.v2.libvY.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.foo.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.foo.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.foo.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.foo.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.foo.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.foo.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.foo.v2.libvY_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.foo.v2.libvY.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.foo.v2.libvY.apex", + }, + x86: { + src: "x86/com.android.apex.test.foo.v2.libvY.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.foo.v2.libvY.apex", + }, + }, + filename: "com.android.apex.test.foo.v2.libvY.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.pony_stripped.v1.libvZ_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.pony_stripped.v1.libvZ.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.pony_stripped.v1.libvZ.apex", + }, + x86: { + src: "x86/com.android.apex.test.pony_stripped.v1.libvZ.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.pony_stripped.v1.libvZ.apex", + }, + }, + filename: "com.android.apex.test.pony_stripped.v1.libvZ.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.pony.v1.libvZ_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.pony.v1.libvZ.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.pony.v1.libvZ.apex", + }, + x86: { + src: "x86/com.android.apex.test.pony.v1.libvZ.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.pony.v1.libvZ.apex", + }, + }, + filename: "com.android.apex.test.pony.v1.libvZ.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.sharedlibs_generated.v1.libvX_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.sharedlibs_generated.v1.libvX.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex", + }, + x86: { + src: "x86/com.android.apex.test.sharedlibs_generated.v1.libvX.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex", + }, + }, + filename: "com.android.apex.test.sharedlibs_generated.v1.libvX.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.sharedlibs_generated.v2.libvY_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.sharedlibs_generated.v2.libvY.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex", + }, + x86: { + src: "x86/com.android.apex.test.sharedlibs_generated.v2.libvY.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex", + }, + }, + filename: "com.android.apex.test.sharedlibs_generated.v2.libvY.apex", + installable: false, +} + +prebuilt_apex { + name: "com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ_prebuilt", + arch: { + arm: { + src: "arm/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex", + }, + arm64: { + src: "arm64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex", + }, + x86: { + src: "x86/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex", + }, + x86_64: { + src: "x86_64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex", + }, + }, + filename: "com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex", + installable: false, +} diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..07fcff94 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..2361fd60 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..20bdd345 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..67915351 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.bar_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.baz_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.baz_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..e6405b42 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.baz_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..799890ab --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..906ec226 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..5ee73835 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..c2f9f687 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.foo_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..323b81ff --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony_stripped.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony_stripped.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..8a578ba0 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.pony_stripped.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..58e7f956 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..b0385526 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_generated.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..3ab73afa --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..c04a1691 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..9bedfa25 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..a8459790 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..c8f89611 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.bar_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.baz_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.baz_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..e2e5b708 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.baz_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..d6795a59 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..65dcd649 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..894afa22 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..c1ce6fad --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.foo_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..80c64239 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony_stripped.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony_stripped.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..4d380da7 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.pony_stripped.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..e0cf9e7c --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..93f24f25 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..21a6dc3c --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/arm64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..4767f4a3 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..cff076ac --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..bca5c496 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..01f822e9 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.bar_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.baz_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.baz_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..96248526 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.baz_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..8b352bd0 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..71cdc202 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..eccb3d4e --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..0265ac95 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.foo_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..ef1d1d90 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony_stripped.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony_stripped.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..8439fe3c --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.pony_stripped.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..8ba0e5b4 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..2928193e --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_generated.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..e849bcd0 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..e733e1d2 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..522b9c8e --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..eec0184b --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..cbd96978 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.bar_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.baz_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.baz_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..b2ca4130 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.baz_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..2f5a2c27 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..1bc4774e --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..77db6dff --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..042d4bc4 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.foo_stripped.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..5a7c5fb3 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony_stripped.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony_stripped.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..bb99f7ef --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.pony_stripped.v1.libvZ.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex Binary files differnew file mode 100644 index 00000000..2ff31ca4 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v1.libvX.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex Binary files differnew file mode 100644 index 00000000..013f2754 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_generated.v2.libvY.apex diff --git a/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex Binary files differnew file mode 100644 index 00000000..3c6cf339 --- /dev/null +++ b/tests/testdata/sharedlibs/prebuilts/x86_64/com.android.apex.test.sharedlibs_secondary_generated.v1.libvZ.apex diff --git a/tests/util/com/android/tests/util/ModuleTestUtils.java b/tests/util/com/android/tests/util/ModuleTestUtils.java deleted file mode 100644 index 8f62bc33..00000000 --- a/tests/util/com/android/tests/util/ModuleTestUtils.java +++ /dev/null @@ -1,247 +0,0 @@ -/* - * Copyright (C) 2019 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. - */ - -package com.android.tests.util; - -import static com.google.common.truth.Truth.assertThat; - -import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey; -import com.android.tradefed.build.IBuildInfo; -import com.android.tradefed.device.DeviceNotAvailableException; -import com.android.tradefed.device.ITestDevice; -import com.android.tradefed.device.ITestDevice.ApexInfo; -import com.android.tradefed.log.LogUtil.CLog; -import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test; -import com.android.tradefed.util.CommandResult; -import com.android.tradefed.util.CommandStatus; -import com.android.tradefed.util.FileUtil; -import com.android.tradefed.util.IRunUtil; -import com.android.tradefed.util.RunUtil; -import com.android.tradefed.util.SystemUtil.EnvVariable; - -import com.google.common.base.Stopwatch; - -import org.junit.Assert; - -import java.io.File; -import java.io.IOException; -import java.time.Duration; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; - -public class ModuleTestUtils { - private static final String SHIM = "com.android.apex.cts.shim"; - private static final String APEX_INFO_EXTRACT_REGEX = - ".*package:\\sname='(\\S+)\\'\\sversionCode='(\\d+)'\\s.*"; - - private static final Duration WAIT_FOR_SESSION_READY_TTL = Duration.ofSeconds(10); - private static final Duration SLEEP_FOR = Duration.ofMillis(200); - - protected final Pattern mIsSessionReadyPattern = - Pattern.compile("(isReady = true)|(isStagedSessionReady = true)"); - protected final Pattern mIsSessionAppliedPattern = - Pattern.compile("(isApplied = true)|(isStagedSessionApplied = true)"); - - private IRunUtil mRunUtil = new RunUtil(); - private BaseHostJUnit4Test mTest; - - private IBuildInfo getBuild() { - return mTest.getBuild(); - } - - public ModuleTestUtils(BaseHostJUnit4Test test) { - mTest = test; - } - - /** - * Retrieve package name and version code from test apex file. - * - * @param apex input apex file to retrieve the info from - */ - public ApexInfo getApexInfo(File apex) { - String aaptOutput = runCmd(String.format( - "aapt dump badging %s", apex.getAbsolutePath())); - String[] lines = aaptOutput.split("\n"); - Pattern p = Pattern.compile(APEX_INFO_EXTRACT_REGEX); - for (String l : lines) { - Matcher m = p.matcher(l); - if (m.matches()) { - ApexInfo apexInfo = new ApexInfo(m.group(1), Long.parseLong(m.group(2))); - return apexInfo; - } - } - return null; - } - - /** - * Get the test file. - * - * @param testFileName name of the file - */ - public File getTestFile(String testFileName) throws IOException { - File testFile = null; - - String testcasesPath = System.getenv(EnvVariable.ANDROID_HOST_OUT_TESTCASES.toString()); - if (testcasesPath != null) { - testFile = searchTestFile(new File(testcasesPath), testFileName); - } - if (testFile != null) { - return testFile; - } - - File hostLinkedDir = getBuild().getFile(BuildInfoFileKey.HOST_LINKED_DIR); - if (hostLinkedDir != null) { - testFile = searchTestFile(hostLinkedDir, testFileName); - } - if (testFile != null) { - return testFile; - } - - // Find the file in the buildinfo. - File buildInfoFile = getBuild().getFile(testFileName); - if (buildInfoFile != null) { - return buildInfoFile; - } - - throw new IOException("Cannot find " + testFileName); - } - - private String runCmd(String cmd) { - CLog.d("About to run command: %s", cmd); - CommandResult result = mRunUtil.runTimedCmd(1000 * 60 * 5, cmd.split("\\s+")); - Assert.assertNotNull(result); - Assert.assertTrue( - String.format("Command %s failed", cmd), - result.getStatus().equals(CommandStatus.SUCCESS)); - CLog.v("output:\n%s", result.getStdout()); - return result.getStdout(); - } - - /** - * Searches the file with the given name under the given directory, returns null if not found. - */ - private File searchTestFile(File baseSearchFile, String testFileName) { - if (baseSearchFile != null && baseSearchFile.isDirectory()) { - File testFile = FileUtil.findFile(baseSearchFile, testFileName); - if (testFile != null && testFile.isFile()) { - return testFile; - } - } - return null; - } - - public void waitForStagedSessionReady() throws DeviceNotAvailableException { - // TODO: implement wait for session ready logic inside PackageManagerShellCommand instead. - boolean sessionReady = false; - Duration spentWaiting = Duration.ZERO; - while (spentWaiting.compareTo(WAIT_FOR_SESSION_READY_TTL) < 0) { - CommandResult res = mTest.getDevice().executeShellV2Command("pm get-stagedsessions"); - Assert.assertEquals("", res.getStderr()); - sessionReady = Stream.of(res.getStdout().split("\n")).anyMatch(this::isReadyNotApplied); - if (sessionReady) { - CLog.i("Done waiting after " + spentWaiting); - break; - } - try { - Thread.sleep(SLEEP_FOR.toMillis()); - spentWaiting = spentWaiting.plus(SLEEP_FOR); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - Assert.assertTrue("Staged session wasn't ready in " + WAIT_FOR_SESSION_READY_TTL, - sessionReady); - } - - /** - * Abandons any staged session that is marked {@code ready} - */ - public void abandonActiveStagedSession() throws DeviceNotAvailableException { - CommandResult res = mTest.getDevice().executeShellV2Command("pm list staged-sessions " - + "--only-ready --only-parent --only-sessionid"); - assertThat(res.getStderr()).isEqualTo(""); - String activeSessionId = res.getStdout(); - if (activeSessionId != null && !activeSessionId.equals("")) { - res = mTest.getDevice().executeShellV2Command("pm install-abandon " - + activeSessionId); - if (!res.getStderr().equals("") || res.getStatus() != CommandStatus.SUCCESS) { - CLog.d("Failed to abandon session " + activeSessionId - + " Error: " + res.getStderr()); - } - } - } - - /** - * Uninstalls a shim apex only if its latest version is installed on /data partition - * - * <p>This is purely to optimize tests run time, since uninstalling an apex requires a reboot. - */ - public void uninstallShimApexIfNecessary() throws Exception { - if (!isApexUpdateSupported()) { - return; - } - - final String errorMessage = mTest.getDevice().uninstallPackage(SHIM); - if (errorMessage == null) { - CLog.i("Uninstalling shim apex"); - mTest.getDevice().reboot(); - } else { - // Most likely we tried to uninstall system version and failed. It should be fine to - // continue tests. - // TODO(b/140813980): use ApexInfo.sourceDir to decide whenever to issue an uninstall. - CLog.w("Failed to uninstall shim APEX: " + errorMessage); - } - assertThat(getShimApex().versionCode).isEqualTo(1L); - } - - private ITestDevice.ApexInfo getShimApex() throws DeviceNotAvailableException { - return mTest.getDevice().getActiveApexes().stream().filter( - apex -> apex.name.equals(SHIM)).findAny().orElseThrow( - () -> new AssertionError("Can't find " + SHIM)); - } - - /** - * Return {@code true} if and only if device supports updating apex. - */ - public boolean isApexUpdateSupported() throws Exception { - return mTest.getDevice().getBooleanProperty("ro.apex.updatable", false); - } - - private boolean isReadyNotApplied(String sessionInfo) { - boolean isReady = mIsSessionReadyPattern.matcher(sessionInfo).find(); - boolean isApplied = mIsSessionAppliedPattern.matcher(sessionInfo).find(); - return isReady && !isApplied; - } - - /** - * Waits for given {@code timeout} for {@code filePath} to be deleted. - */ - public void waitForFileDeleted(String filePath, Duration timeout) throws Exception { - Stopwatch stopwatch = Stopwatch.createStarted(); - while (true) { - if (!mTest.getDevice().doesFileExist(filePath)) { - return; - } - if (stopwatch.elapsed().compareTo(timeout) > 0) { - break; - } - Thread.sleep(500); - } - throw new AssertionError("Timed out waiting for " + filePath + " to be deleted"); - } -} diff --git a/tests/wifi-e2e-tests.xml b/tests/wifi-e2e-tests.xml deleted file mode 100644 index 1a130962..00000000 --- a/tests/wifi-e2e-tests.xml +++ /dev/null @@ -1,25 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2019 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. ---> -<configuration description="Config for wifi apex e2e testing"> - <option name="test-suite-tag" value="wifi_e2e_tests" /> - <option name="test-suite-tag" value="apct" /> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <test class="com.android.tradefed.testtype.HostTest" > - <option name="jar" value="wifi_e2e_tests.jar" /> - <option name="set-option" value="apex_file_name:test_com.android.wifi.apex" /> - </test> -</configuration> - diff --git a/tools/Android.bp b/tools/Android.bp index 1f119e52..6b4427b1 100644 --- a/tools/Android.bp +++ b/tools/Android.bp @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + python_binary_host { name: "deapexer", srcs: [ @@ -34,3 +38,56 @@ python_binary_host { ], } +python_binary_host { + name: "apex_compression_tool", + srcs: [ + "apex_compression_tool.py", + ], + version: { + py2: { + enabled: false, + }, + py3: { + enabled: true, + embedded_launcher: true, + }, + }, + libs: [ + "apex_manifest_proto", + ], + required: [ + "avbtool", + "conv_apex_manifest", + ], +} + +python_test_host { + name: "apex_compression_test", + main: "apex_compression_test.py", + srcs: [ + "apex_compression_test.py", + ], + data: [ + ":avbtool", + ":com.android.example.apex", + ":conv_apex_manifest", + ":apex_compression_tool", + ":deapexer", + ":soong_zip", + ], + version: { + py2: { + enabled: false, + }, + py3: { + enabled: true, + }, + }, + libs: [ + "apex_manifest_proto", + ], + test_suites: ["general-tests"], + test_options: { + unit_test: true, + }, +} diff --git a/tools/apex_compression_test.py b/tools/apex_compression_test.py new file mode 100644 index 00000000..018fc563 --- /dev/null +++ b/tools/apex_compression_test.py @@ -0,0 +1,323 @@ +#!/usr/bin/env python +# +# Copyright (C) 2020 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. +# +"""Unit tests for apex_compression_tool.""" +import hashlib +import logging +import os +import shutil +import subprocess +import tempfile +import unittest +from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED + +import apex_manifest_pb2 + +logger = logging.getLogger(__name__) + +TEST_APEX = 'com.android.example.apex' + +# In order to debug test failures, set DEBUG_TEST to True and run the test from +# local workstation bypassing atest, e.g.: +# $ m apex_compression_tool_test && \ +# out/host/linux-x86/nativetest64/apex_compression_tool_test/\ +# apex_compression_tool_test +# +# the test will print out the command used, and the temporary files used by the +# test. +DEBUG_TEST = False + + +def run(args, verbose=None, **kwargs): + """Creates and returns a subprocess.Popen object. + + Args: + args: The command represented as a list of strings. + verbose: Whether the commands should be shown. Default to the global + verbosity if unspecified. + kwargs: Any additional args to be passed to subprocess.Popen(), such as env, + stdin, etc. stdout and stderr will default to subprocess.PIPE and + subprocess.STDOUT respectively unless caller specifies any of them. + universal_newlines will default to True, as most of the users in + releasetools expect string output. + + Returns: + A subprocess.Popen object. + """ + if 'stdout' not in kwargs and 'stderr' not in kwargs: + kwargs['stdout'] = subprocess.PIPE + kwargs['stderr'] = subprocess.STDOUT + if 'universal_newlines' not in kwargs: + kwargs['universal_newlines'] = True + if DEBUG_TEST: + print('\nRunning: \n%s\n' % ' '.join(args)) + # Don't log any if caller explicitly says so. + if verbose: + logger.info(' Running: \'%s\'', ' '.join(args)) + return subprocess.Popen(args, **kwargs) + + +def run_host_command(args, verbose=None, **kwargs): + host_build_top = os.environ.get('ANDROID_BUILD_TOP') + if host_build_top: + host_command_dir = os.path.join(host_build_top, + 'out/soong/host/linux-x86/bin') + args[0] = os.path.join(host_command_dir, args[0]) + return run_and_check_output(args, verbose, **kwargs) + + +def run_and_check_output(args, verbose=None, **kwargs): + """Runs the given command and returns the output. + + Args: + args: The command represented as a list of strings. + verbose: Whether the commands should be shown. Default to the global + verbosity if unspecified. + kwargs: Any additional args to be passed to subprocess.Popen(), such as env, + stdin, etc. stdout and stderr will default to subprocess.PIPE and + subprocess.STDOUT respectively unless caller specifies any of them. + + Returns: + The output string. + + Raises: + ExternalError: On non-zero exit from the command. + """ + + proc = run(args, verbose=verbose, **kwargs) + output, _ = proc.communicate() + if output is None: + output = '' + # Don't log any if caller explicitly says so. + if verbose: + logger.info('%s', output.rstrip()) + if proc.returncode != 0: + raise RuntimeError( + "Failed to run command '{}' (exit code {}):\n{}".format( + args, proc.returncode, output)) + return output + + +def get_current_dir(): + """Returns the current dir, relative to the script dir.""" + # The script dir is the one we want, which could be different from pwd. + current_dir = os.path.dirname(os.path.realpath(__file__)) + return current_dir + + +def get_sha1sum(file_path): + h = hashlib.sha256() + + with open(file_path, 'rb') as file: + while True: + # Reading is buffered, so we can read smaller chunks. + chunk = file.read(h.block_size) + if not chunk: + break + h.update(chunk) + + return h.hexdigest() + + +class ApexCompressionTest(unittest.TestCase): + def setUp(self): + self._to_cleanup = [] + + def tearDown(self): + if not DEBUG_TEST: + for i in self._to_cleanup: + if os.path.isdir(i): + shutil.rmtree(i, ignore_errors=True) + else: + os.remove(i) + del self._to_cleanup[:] + else: + print('Cleanup: ' + str(self._to_cleanup)) + + def _run_apex_compression_tool(self, args): + cmd = ['apex_compression_tool'] + host_build_top = os.environ.get('ANDROID_BUILD_TOP') + if host_build_top: + os.environ['APEX_COMPRESSION_TOOL_PATH'] = ( + os.path.join(host_build_top, 'out/soong/host/linux-x86/bin') + + ':' + os.path.join(host_build_top, 'prebuilts/sdk/tools/linux/bin')) + else: + os.environ['APEX_COMPRESSION_TOOL_PATH'] = os.path.dirname( + shutil.which('apex_compression_tool')) + cmd.extend(args) + run_host_command(cmd, True) + + def _get_container_files(self, apex_file_path): + dir_name = tempfile.mkdtemp( + prefix=self._testMethodName + '_container_files_') + self._to_cleanup.append(dir_name) + with ZipFile(apex_file_path, 'r') as zip_obj: + zip_obj.extractall(path=dir_name) + files = {} + for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey', + 'apex_build_info.pb', 'apex_payload.img', 'apex_payload.zip', + 'AndroidManifest.xml', 'original_apex']: + file_path = os.path.join(dir_name, i) + if os.path.exists(file_path): + files[i] = file_path + + image_file = files.get('apex_payload.img', None) + if image_file is None: + image_file = files.get('apex_payload.zip', None) + else: + files['apex_payload'] = image_file + # Also retrieve the root digest of the image + avbtool_cmd = ['avbtool', + 'print_partition_digests', '--image', files['apex_payload']] + # avbtool_cmd output has format "<name>: <value>" + files['digest'] = run_host_command( + avbtool_cmd, True).split(': ')[1].strip() + + return files + + def _get_manifest_string(self, manifest_path): + cmd = ['conv_apex_manifest'] + cmd.extend([ + 'print', + manifest_path + ]) + return run_host_command(cmd, 'True') + + # Mutates the manifest located at |manifest_path| + def _unset_original_apex_digest(self, manifest_path): + # Open the protobuf + with open(manifest_path, 'rb') as f: + pb = apex_manifest_pb2.ApexManifest() + pb.ParseFromString(f.read()) + pb.ClearField('capexMetadata') + with open(manifest_path, 'wb') as f: + f.write(pb.SerializeToString()) + + def _compress_apex(self, uncompressed_apex_fp): + """Returns file path to compressed APEX""" + fd, compressed_apex_fp = tempfile.mkstemp( + prefix=self._testMethodName + '_compressed_', + suffix='.capex') + os.close(fd) + self._to_cleanup.append(compressed_apex_fp) + self._run_apex_compression_tool([ + 'compress', + '--input', uncompressed_apex_fp, + '--output', compressed_apex_fp + ]) + return compressed_apex_fp + + def _decompress_apex(self, compressed_apex_fp): + """Returns file path to decompressed APEX""" + decompressed_apex_fp = tempfile. \ + NamedTemporaryFile(prefix=self._testMethodName + '_decompressed_', + suffix='.apex').name + # Use deapexer to decompress + cmd = ['deapexer'] + cmd.extend([ + 'decompress', + '--input', compressed_apex_fp, + '--output', decompressed_apex_fp + ]) + run_host_command(cmd, True) + + self.assertTrue(os.path.exists(decompressed_apex_fp), + 'Decompressed APEX does not exist') + self._to_cleanup.append(decompressed_apex_fp) + return decompressed_apex_fp + + def _get_type(self, apex_file_path): + cmd = ['deapexer', 'info', '--print-type', apex_file_path] + return run_host_command(cmd, True).strip() + + def test_compression(self): + uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex') + # TODO(samiul): try compressing a compressed APEX + compressed_apex_fp = self._compress_apex(uncompressed_apex_fp) + + # Verify output file has been created and is smaller than input file + uncompressed_file_size = os.path.getsize(uncompressed_apex_fp) + compressed_file_size = os.path.getsize(compressed_apex_fp) + self.assertGreater(compressed_file_size, 0, 'Compressed APEX is empty') + self.assertLess(compressed_file_size, uncompressed_file_size, + 'Compressed APEX is not smaller than uncompressed APEX') + + # Verify type of the apex is 'COMPRESSED' + self.assertEqual(self._get_type(compressed_apex_fp), 'COMPRESSED') + + # Verify the contents of the compressed apex files + content_in_compressed_apex = self._get_container_files(compressed_apex_fp) + self.assertIsNotNone(content_in_compressed_apex['original_apex']) + content_in_uncompressed_apex = self._get_container_files( + uncompressed_apex_fp) + self.assertIsNotNone(content_in_uncompressed_apex['apex_payload']) + self.assertIsNotNone(content_in_uncompressed_apex['digest']) + + # Verify that CAPEX manifest contains digest of original_apex + manifest_string = self._get_manifest_string( + content_in_compressed_apex['apex_manifest.pb']) + self.assertIn('originalApexDigest: "' + + content_in_uncompressed_apex['digest'] + '"', manifest_string) + + for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey', + 'apex_build_info.pb', 'AndroidManifest.xml']: + if i in content_in_uncompressed_apex: + if i == 'apex_manifest.pb': + # Get rid of originalApexDigest field, which should be the + # only difference + self._unset_original_apex_digest(content_in_compressed_apex[i]) + self.assertEqual(get_sha1sum(content_in_compressed_apex[i]), + get_sha1sum(content_in_uncompressed_apex[i])) + + def test_decompression(self): + # setup: create compressed APEX + uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex') + compressed_apex_fp = self._compress_apex(uncompressed_apex_fp) + + # Decompress it + decompressed_apex_fp = self._decompress_apex(compressed_apex_fp) + + # Verify type of the apex is 'UNCOMPRESSED' + self.assertEqual(self._get_type(decompressed_apex_fp), 'UNCOMPRESSED') + + # Verify decompressed APEX is same as uncompressed APEX + self.assertEqual(get_sha1sum(uncompressed_apex_fp), + get_sha1sum(decompressed_apex_fp), + 'Decompressed APEX is not same as uncompressed APEX') + + # Try decompressing uncompressed APEX. It should not work. + with self.assertRaises(RuntimeError) as error: + self._decompress_apex(uncompressed_apex_fp) + + self.assertIn(uncompressed_apex_fp + + ' is not a compressed APEX', str(error.exception)) + + def test_only_original_apex_is_compressed(self): + uncompressed_apex_fp = os.path.join(get_current_dir(), TEST_APEX + '.apex') + compressed_apex_fp = self._compress_apex(uncompressed_apex_fp) + + with ZipFile(compressed_apex_fp, 'r') as zip_obj: + self.assertEqual(zip_obj.getinfo('original_apex').compress_type, + ZIP_DEFLATED) + content_in_uncompressed_apex = self._get_container_files( + uncompressed_apex_fp) + for i in ['apex_manifest.json', 'apex_manifest.pb', 'apex_pubkey', + 'apex_build_info.pb', 'AndroidManifest.xml']: + if i in content_in_uncompressed_apex: + self.assertEqual(zip_obj.getinfo(i).compress_type, ZIP_STORED) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tools/apex_compression_tool.py b/tools/apex_compression_tool.py new file mode 100644 index 00000000..3c0f16d8 --- /dev/null +++ b/tools/apex_compression_tool.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# +# Copyright (C) 2020 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. + +"""apex_compression_tool is a tool that can compress/decompress APEX. + +Example: + apex_compression_tool compress --input /apex/to/compress --output output/path + apex_compression_tool decompress --input /apex/to/decompress --output dir/ + apex_compression_tool verify-compressed --input /file/to/check +""" +from __future__ import print_function + +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +from zipfile import ZipFile + +import apex_manifest_pb2 + +tool_path_list = None + + +def FindBinaryPath(binary): + for path in tool_path_list: + binary_path = os.path.join(path, binary) + if os.path.exists(binary_path): + return binary_path + raise Exception('Failed to find binary ' + binary + ' in path ' + + ':'.join(tool_path_list)) + + +def RunCommand(cmd, verbose=False, env=None, expected_return_values=None): + expected_return_values = expected_return_values or {0} + env = env or {} + env.update(os.environ.copy()) + + cmd[0] = FindBinaryPath(cmd[0]) + + if verbose: + print('Running: ' + ' '.join(cmd)) + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) + output, _ = p.communicate() + + if verbose or p.returncode not in expected_return_values: + print(output.rstrip()) + + assert p.returncode in expected_return_values, 'Failed to execute: ' \ + + ' '.join(cmd) + + return output, p.returncode + + +def RunCompress(args, work_dir): + """RunCompress takes an uncompressed APEX and compresses into compressed APEX + + Compressed apex will contain the following items: + - original_apex: The original uncompressed APEX + - Duplicates of various meta files inside the input APEX, e.g + AndroidManifest.xml, public_key + + Args: + args.input: file path to uncompressed APEX + args.output: file path to where compressed APEX will be placed + work_dir: file path to a temporary folder + Returns: + True if compression was executed successfully, otherwise False + """ + global tool_path_list + tool_path_list = args.apex_compression_tool_path + + cmd = ['soong_zip'] + cmd.extend(['-o', args.output]) + + # We want to put the input apex inside the compressed APEX with name + # "original_apex". So we create a hard link and put the renamed file inside + # the zip + original_apex = os.path.join(work_dir, 'original_apex') + os.link(args.input, original_apex) + cmd.extend(['-C', work_dir]) + cmd.extend(['-f', original_apex]) + + # We also need to extract some files from inside of original_apex and zip + # together with compressed apex + with ZipFile(original_apex, 'r') as zip_obj: + extract_dir = os.path.join(work_dir, 'extract') + for meta_file in ['apex_manifest.json', 'apex_manifest.pb', + 'apex_pubkey', 'apex_build_info.pb', + 'AndroidManifest.xml']: + if meta_file in zip_obj.namelist(): + zip_obj.extract(meta_file, path=extract_dir) + file_path = os.path.join(extract_dir, meta_file) + cmd.extend(['-C', extract_dir]) + cmd.extend(['-f', file_path]) + cmd.extend(['-s', meta_file]) + # Extract the image for retrieving root digest + zip_obj.extract('apex_payload.img', path= work_dir) + image_path = os.path.join(work_dir, 'apex_payload.img') + + # Set digest of original_apex to apex_manifest.pb + apex_manifest_path = os.path.join(extract_dir, 'apex_manifest.pb') + assert AddOriginalApexDigestToManifest(apex_manifest_path, image_path) + + # Don't forget to compress + cmd.extend(['-L', '9']) + + RunCommand(cmd, verbose=True) + + return True + + +def AddOriginalApexDigestToManifest(capex_manifest_path, apex_image_path): + # Retrieve the root digest of the image + avbtool_cmd = [ + 'avbtool', + 'print_partition_digests', '--image', + apex_image_path] + # avbtool_cmd output has format "<name>: <value>" + root_digest = RunCommand(avbtool_cmd, True)[0].decode().split(': ')[1].strip() + # Update the manifest proto file + with open(capex_manifest_path, 'rb') as f: + pb = apex_manifest_pb2.ApexManifest() + pb.ParseFromString(f.read()) + # Populate CompressedApexMetadata + capex_metadata = apex_manifest_pb2.ApexManifest().CompressedApexMetadata() + capex_metadata.originalApexDigest = root_digest + # Set updated value to protobuf + pb.capexMetadata.CopyFrom(capex_metadata) + with open(capex_manifest_path, 'wb') as f: + f.write(pb.SerializeToString()) + return True + + +def ParseArgs(argv): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(required=True, dest='cmd') + + # Handle sub-command "compress" + parser_compress = subparsers.add_parser('compress', + help='compresses an APEX') + parser_compress.add_argument('--input', type=str, required=True, + help='path to input APEX file that will be ' + 'compressed') + parser_compress.add_argument('--output', type=str, required=True, + help='output path to compressed APEX file') + apex_compression_tool_path_in_environ = \ + 'APEX_COMPRESSION_TOOL_PATH' in os.environ + parser_compress.add_argument( + '--apex_compression_tool_path', + required=not apex_compression_tool_path_in_environ, + default=os.environ['APEX_COMPRESSION_TOOL_PATH'].split(':') + if apex_compression_tool_path_in_environ else None, + type=lambda s: s.split(':'), + help="""A list of directories containing all the tools used by + apex_compression_tool (e.g. soong_zip etc.) separated by ':'. Can also + be set using the APEX_COMPRESSION_TOOL_PATH environment variable""") + parser_compress.set_defaults(func=RunCompress) + + return parser.parse_args(argv) + + +class TempDirectory(object): + + def __enter__(self): + self.name = tempfile.mkdtemp() + return self.name + + def __exit__(self, *unused): + shutil.rmtree(self.name) + + +def main(argv): + args = ParseArgs(argv) + + with TempDirectory() as work_dir: + success = args.func(args, work_dir) + + if not success: + sys.exit(1) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/tools/create_apex_skeleton.sh b/tools/create_apex_skeleton.sh new file mode 100644 index 00000000..818e119a --- /dev/null +++ b/tools/create_apex_skeleton.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +# Creates an apex stub in a subdirectory named after the package name. Edit the APEX_NAME variable +# before running. + +APEX_NAME=com.android.yourpackagenamehere + +mkdir ${APEX_NAME} +cd ${APEX_NAME} + +cat > Android.bp <<EOF +// Copyright (C) 2020 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. +apex_key { + name: "${APEX_NAME}.key", + public_key: "${APEX_NAME}.avbpubkey", + private_key: "${APEX_NAME}.pem", +} + +android_app_certificate { + name: "${APEX_NAME}.certificate", + certificate: "${APEX_NAME}", +} + +apex { + name: "${APEX_NAME}", + manifest: "manifest.json", + file_contexts: ":apex.test-file_contexts", // Default, please edit, see go/android-apex-howto + key: "${APEX_NAME}.key", +} +EOF + +openssl genrsa -out ${APEX_NAME}.pem 4096 +avbtool extract_public_key --key ${APEX_NAME}.pem --output ${APEX_NAME}.avbpubkey + +cat > csr.conf <<EOF +[req] +default_bits = 4096 +distinguished_name = dn +prompt = no + +[dn] +C="US" +ST="California" +L="Mountain View" +O="Android" +OU="Android" +emailAddress="android@android.com" +CN="${APEX_NAME}" +EOF + +openssl req -x509 -config csr.conf -newkey rsa:4096 -nodes -days 999999 -keyout key.pem -out ${APEX_NAME}.x509.pem +rm csr.conf +openssl pkcs8 -topk8 -inform PEM -outform DER -in key.pem -out ${APEX_NAME}.pk8 -nocrypt +rm key.pem + +cat > manifest.json << EOF +{ + "name": "${APEX_NAME}", + "version": 1 +} +EOF diff --git a/tools/deapexer.py b/tools/deapexer.py index b170d90f..92001c9d 100644 --- a/tools/deapexer.py +++ b/tools/deapexer.py @@ -24,23 +24,29 @@ To extract content of an APEX to the given directory: from __future__ import print_function import argparse +import apex_manifest +import enum import os import shutil import sys import subprocess import tempfile import zipfile -import apex_manifest + +BLOCK_SIZE = 4096 class ApexImageEntry(object): - def __init__(self, name, base_dir, permissions, size, is_directory=False, is_symlink=False): + def __init__(self, name, base_dir, permissions, size, ino, extents, is_directory=False, + is_symlink=False): self._name = name self._base_dir = base_dir self._permissions = permissions self._size = size self._is_directory = is_directory self._is_symlink = is_symlink + self._ino = ino + self._extents = extents @property def name(self): @@ -70,6 +76,14 @@ class ApexImageEntry(object): def size(self): return self._size + @property + def ino(self): + return self._ino + + @property + def extents(self): + return self._extents + def __str__(self): ret = '' if self._is_directory: @@ -152,10 +166,43 @@ class Apex(object): name = parts[5] if not name: continue + ino = parts[1] bits = parts[2] size = parts[6] + extents = [] + is_symlink = bits[1]=='2' + is_directory=bits[1]=='4' + + if not is_symlink and not is_directory: + process = subprocess.Popen([self._debugfs, '-R', 'dump_extents <%s>' % ino, + self._payload], stdout=subprocess.PIPE, stderr=subprocess.PIPE, + universal_newlines=True) + stdout, _ = process.communicate() + # Output of dump_extents for an inode fragmented in 3 blocks (length and addresses represent + # block-sized sections): + # Level Entries Logical Physical Length Flags + # 0/ 0 1/ 3 0 - 0 18 - 18 1 + # 0/ 0 2/ 3 1 - 15 20 - 34 15 + # 0/ 0 3/ 3 16 - 1863 37 - 1884 1848 + res = str(stdout).splitlines() + res.pop(0) # the first line contains only columns names + left_length = int(size) + try: # dump_extents sometimes has an unexpected output + for line in res: + tokens = line.split() + offset = int(tokens[7]) * BLOCK_SIZE + length = min(int(tokens[-1]) * BLOCK_SIZE, left_length) + left_length -= length + extents.append((offset, length)) + if (left_length != 0): # dump_extents sometimes fails to display "hole" blocks + raise ValueError + except: + extents = [] # [] means that we failed to retrieve the file location successfully + entries.append(ApexImageEntry(name, base_dir=path, permissions=int(bits[3:], 8), size=size, - is_directory=bits[1]=='4', is_symlink=bits[1]=='2')) + is_directory=is_directory, is_symlink=is_symlink, ino=ino, + extents=extents)) + return ApexImageDirectory(path, entries, self) def _extract(self, path, dest): @@ -168,17 +215,38 @@ class Apex(object): def RunList(args): + if GetType(args.apex) == ApexType.COMPRESSED: + with tempfile.TemporaryDirectory() as temp: + decompressed_apex = os.path.join(temp, 'temp.apex') + decompress(args.apex, decompressed_apex) + args.apex = decompressed_apex + + RunList(args) + return + with Apex(args) as apex: for e in apex.list(is_recursive=True): if e.is_directory: continue + res = '' if args.size: - print(e.size, e.full_path) - else: - print(e.full_path) + res += e.size + ' ' + res += e.full_path + if args.extents: + res += ' [' + '-'.join(str(x) for x in e.extents) + ']' + print(res) def RunExtract(args): + if GetType(args.apex) == ApexType.COMPRESSED: + with tempfile.TemporaryDirectory() as temp: + decompressed_apex = os.path.join(temp, "temp.apex") + decompress(args.apex, decompressed_apex) + args.apex = decompressed_apex + + RunExtract(args) + return + with Apex(args) as apex: if not os.path.exists(args.dest): os.makedirs(args.dest, mode=0o755) @@ -186,15 +254,75 @@ def RunExtract(args): shutil.rmtree(os.path.join(args.dest, "lost+found")) +class ApexType(enum.Enum): + INVALID = 0 + UNCOMPRESSED = 1 + COMPRESSED = 2 + + +def GetType(apex_path): + with zipfile.ZipFile(apex_path, 'r') as zip_file: + names = zip_file.namelist() + has_payload = 'apex_payload.img' in names + has_original_apex = 'original_apex' in names + if has_payload and has_original_apex: + return ApexType.INVALID + if has_payload: + return ApexType.UNCOMPRESSED + if has_original_apex: + return ApexType.COMPRESSED + return ApexType.INVALID + + def RunInfo(args): - manifest = apex_manifest.fromApex(args.apex) - print(apex_manifest.toJsonString(manifest)) + if args.print_type: + res = GetType(args.apex) + if res == ApexType.INVALID: + print(args.apex + ' is not a valid apex') + sys.exit(1) + print(res.name) + else: + manifest = apex_manifest.fromApex(args.apex) + print(apex_manifest.toJsonString(manifest)) + + +def RunDecompress(args): + """RunDecompress takes path to compressed APEX and decompresses it to + produce the original uncompressed APEX at give output path + + See apex_compression_tool.py#RunCompress for details on compressed APEX + structure. + + Args: + args.input: file path to compressed APEX + args.output: file path to where decompressed APEX will be placed + """ + compressed_apex_fp = args.input + decompressed_apex_fp = args.output + return decompress(compressed_apex_fp, decompressed_apex_fp) + +def decompress(compressed_apex_fp, decompressed_apex_fp): + if os.path.exists(decompressed_apex_fp): + print("Output path '" + decompressed_apex_fp + "' already exists") + sys.exit(1) + + with zipfile.ZipFile(compressed_apex_fp, 'r') as zip_obj: + if 'original_apex' not in zip_obj.namelist(): + print(compressed_apex_fp + ' is not a compressed APEX. Missing ' + "'original_apex' file inside it.") + sys.exit(1) + # Rename original_apex file to what user provided as output filename + original_apex_info = zip_obj.getinfo('original_apex') + original_apex_info.filename = os.path.basename(decompressed_apex_fp) + # Extract the original_apex as desired name + zip_obj.extract(original_apex_info, + path=os.path.dirname(decompressed_apex_fp)) def main(argv): parser = argparse.ArgumentParser() - debugfs_default = 'debugfs' # assume in PATH by default + debugfs_default = None if 'ANDROID_HOST_OUT' in os.environ: debugfs_default = '%s/bin/debugfs_static' % os.environ['ANDROID_HOST_OUT'] parser.add_argument('--debugfs_path', help='The path to debugfs binary', default=debugfs_default) @@ -204,6 +332,7 @@ def main(argv): parser_list = subparsers.add_parser('list', help='prints content of an APEX to stdout') parser_list.add_argument('apex', type=str, help='APEX file') parser_list.add_argument('--size', help='also show the size of the files', action="store_true") + parser_list.add_argument('--extents', help='also show the location of the files', action="store_true") parser_list.set_defaults(func=RunList) parser_extract = subparsers.add_parser('extract', help='extracts content of an APEX to the given ' @@ -214,10 +343,31 @@ def main(argv): parser_info = subparsers.add_parser('info', help='prints APEX manifest') parser_info.add_argument('apex', type=str, help='APEX file') + parser_info.add_argument('--print-type', + help='Prints type of the apex (COMPRESSED or UNCOMPRESSED)', + action='store_true') parser_info.set_defaults(func=RunInfo) + # Handle sub-command "decompress" + parser_decompress = subparsers.add_parser('decompress', + help='decompresses a compressed ' + 'APEX') + parser_decompress.add_argument('--input', type=str, required=True, + help='path to compressed APEX file that ' + 'will be decompressed') + parser_decompress.add_argument('--output', type=str, required=True, + help='output directory path where ' + 'decompressed APEX will be extracted') + parser_decompress.set_defaults(func=RunDecompress) + args = parser.parse_args(argv) + debugfs_required_for_cmd = ['list', 'extract'] + if args.cmd in debugfs_required_for_cmd and not args.debugfs_path: + print('ANDROID_HOST_OUT environment variable is not defined, --debugfs_path must be set', + file=sys.stderr) + sys.exit(1) + args.func(args) |