Android Pony EXpress (APEX) 容器格式在 Android 10 中引入,用于较低级别系统模块的安装流程。此格式有助于更新不适合标准 Android 应用模型的系统组件。一些示例组件是原生服务和库、硬件抽象层 (HAL)、运行时 (ART) 和类库。
术语“APEX”也可以指 APEX 文件。
背景
虽然 Android 支持通过软件包安装程序应用(例如 Google Play 商店应用)更新标准应用模型(例如服务、Activity)中的模块,但对较低级别的操作系统组件使用类似模型存在以下缺点:
- 基于 APK 的模块无法在启动序列的早期使用。软件包管理器是关于应用的集中信息库,只能从 Activity 管理器启动,而 Activity 管理器在启动过程的后期才会准备就绪。
- APK 格式(尤其是清单)专为 Android 应用而设计,系统模块并不总是很适合。
设计
本部分介绍 APEX 文件格式和 APEX 管理器(一种管理 APEX 文件的服务)的高级设计。
如需详细了解为何选择此 APEX 设计,请参阅开发 APEX 时考虑的替代方案。
APEX 格式
这是 APEX 文件的格式。
图 1. APEX 文件格式
在顶层,APEX 文件是一个 zip 文件,其中文件以未压缩形式存储,并位于 4 KB 边界。
APEX 文件中的四个文件是:
apex_manifest.json
AndroidManifest.xml
apex_payload.img
apex_pubkey
apex_manifest.json
文件包含软件包名称和版本,用于标识 APEX 文件。这是一个 JSON 格式的 ApexManifest
协议缓冲区。
AndroidManifest.xml
文件允许 APEX 文件使用与 APK 相关的工具和基础架构,例如 ADB、PackageManager 和软件包安装程序应用(例如 Play 商店)。例如,APEX 文件可以使用现有工具(例如 aapt
)来检查文件中的基本元数据。该文件包含软件包名称和版本信息。此信息通常也可在 apex_manifest.json
中找到。
apex_manifest.json
建议用于处理 APEX 的新代码和系统,而不是 AndroidManifest.xml
。AndroidManifest.xml
可能包含额外的目标定位信息,现有应用发布工具可以使用这些信息。
apex_payload.img
是一个由 dm-verity 支持的 ext4 文件系统镜像。该镜像在运行时通过环回设备进行挂载。具体来说,哈希树和元数据块是使用 libavb
库创建的。文件系统有效负载不会被解析(因为镜像应该可以就地挂载)。常规文件包含在 apex_payload.img
文件中。
apex_pubkey
是用于签署文件系统镜像的公钥。在运行时,此密钥确保下载的 APEX 与内置分区中签署相同 APEX 的实体签署一致。
APEX 命名指南
为了帮助防止平台发展过程中新的 APEX 之间出现命名冲突,请遵循以下命名指南
com.android.*
- 为 AOSP APEX 保留。并非任何公司或设备独有。
com.<companyname>.*
- 为公司保留。可能由该公司多台设备使用。
com.<companyname>.<devicename>.*
- 为特定设备(或设备子集)独有的 APEX 保留。
APEX 管理器
APEX 管理器(或 apexd
)是一个独立的本地进程,负责验证、安装和卸载 APEX 文件。此进程在启动序列的早期启动并准备就绪。APEX 文件通常预安装在设备上的 /system/apex
下。如果没有可用的更新,APEX 管理器默认使用这些软件包。
APEX 的更新序列使用 PackageManager 类,如下所示。
- APEX 文件通过软件包安装程序应用、ADB 或其他来源下载。
- 软件包管理器启动安装过程。当识别到该文件是 APEX 时,软件包管理器将控制权转移给 APEX 管理器。
- APEX 管理器验证 APEX 文件。
- 如果 APEX 文件通过验证,则更新 APEX 管理器的内部数据库,以反映 APEX 文件将在下次启动时激活。
- 安装请求者在软件包验证成功后收到广播。
- 要继续安装,必须重启系统。
在下次启动时,APEX 管理器启动,读取内部数据库,并对列出的每个 APEX 文件执行以下操作
- 验证 APEX 文件。
- 从 APEX 文件创建环回设备。
- 在环回设备之上创建设备映射器块设备。
- 将设备映射器块设备挂载到唯一路径(例如,
/apex/name@ver
)。
当内部数据库中列出的所有 APEX 文件都挂载后,APEX 管理器会提供 binder 服务,供其他系统组件查询有关已安装 APEX 文件的信息。例如,其他系统组件可以查询设备中安装的 APEX 文件列表,或查询特定 APEX 的挂载确切路径,以便可以访问这些文件。
APEX 文件是 APK 文件
APEX 文件是有效的 APK 文件,因为它们是已签名的 zip 归档文件(使用 APK 签名方案),其中包含 AndroidManifest.xml
文件。这允许 APEX 文件使用 APK 文件的基础设施,例如软件包安装程序应用、签名实用程序和软件包管理器。
APEX 文件内的 AndroidManifest.xml
文件非常简单,仅包含软件包name
、versionCode
以及可选的 targetSdkVersion
、minSdkVersion
和 maxSdkVersion
,用于精细的目标定位。此信息允许 APEX 文件通过现有渠道(例如软件包安装程序应用和 ADB)进行交付。
支持的文件类型
APEX 格式支持以下文件类型
- 原生共享库
- 原生可执行文件
- JAR 文件
- 数据文件
- 配置文件
这并不意味着 APEX 可以更新所有这些文件类型。文件类型是否可以更新取决于平台以及文件类型接口定义的稳定性。
签名选项
APEX 文件通过两种方式签名。首先,apex_payload.img
文件(特别是附加到 apex_payload.img
的 vbmeta 描述符)使用密钥签名。然后,整个 APEX 使用 APK 签名方案 v3 进行签名。此过程中使用两个不同的密钥。
在设备端,安装与用于签署 vbmeta 描述符的私钥对应的公钥。APEX 管理器使用公钥来验证请求安装的 APEX。每个 APEX 都必须使用不同的密钥进行签名,这在构建时和运行时都会强制执行。
内置分区中的 APEX
APEX 文件可以位于内置分区中,例如 /system
。分区已经通过 dm-verity 保护,因此 APEX 文件直接通过环回设备挂载。
如果内置分区中存在 APEX,则可以通过提供具有相同软件包名称和大于或等于版本代码的 APEX 软件包来更新 APEX。新的 APEX 存储在 /data
中,与 APK 类似,新安装的版本会遮盖已存在于内置分区中的版本。但与 APK 不同,新安装的 APEX 版本仅在重启后激活。
内核要求
要在 Android 设备上支持 APEX 主线模块,需要以下 Linux 内核功能:环回驱动程序和 dm-verity。环回驱动程序挂载 APEX 模块中的文件系统镜像,dm-verity 验证 APEX 模块。
环回驱动程序和 dm-verity 的性能对于在使用 APEX 模块时实现良好的系统性能非常重要。
支持的内核版本
使用内核版本 4.4 或更高版本的设备支持 APEX 主线模块。搭载 Android 10 或更高版本的新设备必须使用内核版本 4.9 或更高版本才能支持 APEX 模块。
所需的内核补丁
支持 APEX 模块所需的内核补丁包含在 Android 通用树中。要获取支持 APEX 的补丁,请使用最新版本的 Android 通用树。
内核版本 4.4
此版本仅支持从 Android 9 升级到 Android 10 且想要支持 APEX 模块的设备。要获取所需的补丁,强烈建议从 android-4.4
分支向下合并。以下是内核版本 4.4 所需的单个补丁列表。
- UPSTREAM: loop: add ioctl for changing logical block size (4.4)
- BACKPORT: block/loop: set hw_sectors (4.4)
- UPSTREAM: loop: Add LOOP_SET_BLOCK_SIZE in compat ioctl (4.4)
- ANDROID: mnt: Fix next_descendent (4.4)
- ANDROID: mnt: remount should propagate to slaves of slaves (4.4)
- ANDROID: mnt: Propagate remount correctly (4.4)
- Revert "ANDROID: dm verity: add minimum prefetch size" (4.4)
- UPSTREAM: loop: drop caches if offset or block_size are changed (4.4)
内核版本 4.9/4.14/4.19
要获取内核版本 4.9/4.14/4.19 所需的补丁,请从 android-common
分支向下合并。
所需的内核配置选项
以下列表显示了支持 Android 10 中引入的 APEX 模块的基本配置要求。带有星号 (*) 的项目是 Android 9 及更低版本的现有要求。
(*) CONFIG_AIO=Y # AIO support (for direct I/O on loop devices)
CONFIG_BLK_DEV_LOOP=Y # for loop device support
CONFIG_BLK_DEV_LOOP_MIN_COUNT=16 # pre-create 16 loop devices
(*) CONFIG_CRYPTO_SHA1=Y # SHA1 hash for DM-verity
(*) CONFIG_CRYPTO_SHA256=Y # SHA256 hash for DM-verity
CONFIG_DM_VERITY=Y # DM-verity support
内核命令行参数要求
要支持 APEX,请确保内核命令行参数满足以下要求
loop.max_loop
必须未设置loop.max_part
必须 <= 8
构建 APEX
本节介绍如何使用 Android 构建系统构建 APEX。以下是名为 apex.test
的 APEX 的 Android.bp
示例。
apex {
name: "apex.test",
manifest: "apex_manifest.json",
file_contexts: "file_contexts",
// libc.so and libcutils.so are included in the apex
native_shared_libs: ["libc", "libcutils"],
binaries: ["vold"],
java_libs: ["core-all"],
prebuilts: ["my_prebuilt"],
compile_multilib: "both",
key: "apex.test.key",
certificate: "platform",
}
apex_manifest.json
示例
{
"name": "com.android.example.apex",
"version": 1
}
file_contexts
示例
(/.*)? u:object_r:system_file:s0
/sub(/.*)? u:object_r:sub_file:s0
/sub/file3 u:object_r:file3_file:s0
APEX 中的文件类型和位置
文件类型 | APEX 中的位置 |
---|---|
共享库 | /lib 和 /lib64 (/lib/arm 用于 x86 中的已转换 arm) |
可执行文件 | /bin |
Java 库 | /javalib |
预编译文件 | /etc |
传递依赖项
APEX 文件自动包含原生共享库或可执行文件的传递依赖项。例如,如果 libFoo
依赖于 libBar
,则当仅在 native_shared_libs
属性中列出 libFoo
时,这两个库都会被包含在内。
处理多个 ABI
为设备的主 ABI 和辅助应用程序二进制接口 (ABI) 安装 native_shared_libs
属性。如果 APEX 针对仅具有单个 ABI 的设备(即仅 32 位或仅 64 位),则仅安装具有相应 ABI 的库。
仅为设备的主 ABI 安装 binaries
属性,如下所述
- 如果设备仅为 32 位,则仅安装 32 位版本的二进制文件。
- 如果设备仅为 64 位,则仅安装 64 位版本的二进制文件。
要对原生库和二进制文件的 ABI 添加精细控制,请使用 multilib.[first|lib32|lib64|prefer32|both].[native_shared_libs|binaries]
属性。
first
:与设备的主 ABI 匹配。这是二进制文件的默认值。lib32
:与设备的 32 位 ABI 匹配(如果支持)。lib64
:与设备的 64 位 ABI 匹配(如果支持)。prefer32
:与设备的 32 位 ABI 匹配(如果支持)。如果不支持 32 位 ABI,则与 64 位 ABI 匹配。both
:与两个 ABI 都匹配。这是native_shared_libraries
的默认值。
java
、libraries
和 prebuilts
属性与 ABI 无关。
此示例适用于支持 32/64 位且不首选 32 位的设备
apex {
// other properties are omitted
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
},
},
}
vbmeta 签名
使用不同的密钥对每个 APEX 进行签名。当需要新密钥时,创建公钥-私钥对并创建一个 apex_key
模块。使用 key
属性使用该密钥对 APEX 进行签名。公钥会自动包含在 APEX 中,名称为 avb_pubkey
。
# create an rsa key pairopenssl genrsa -out foo.pem 4096
# extract the public key from the key pairavbtool extract_public_key --key foo.pem --output foo.avbpubkey
# in Android.bpapex_key { name: "apex.test.key", public_key: "foo.avbpubkey", private_key: "foo.pem", }
在上述示例中,公钥的名称 (foo
) 成为密钥的 ID。用于签署 APEX 的密钥 ID 写入 APEX 中。在运行时,apexd
使用设备中具有相同 ID 的公钥来验证 APEX。
APEX 签名
以与签名 APK 相同的方式签名 APEX。对 APEX 进行两次签名;一次用于迷你文件系统(apex_payload.img
文件),一次用于整个文件。
要在文件级别签名 APEX,请通过以下三种方式之一设置 certificate
属性
- 未设置:如果未设置任何值,则 APEX 将使用位于
PRODUCT_DEFAULT_DEV_CERTIFICATE
的证书进行签名。如果未设置任何标志,则路径默认为build/target/product/security/testkey
。 <name>
:APEX 使用与PRODUCT_DEFAULT_DEV_CERTIFICATE
位于同一目录中的<name>
证书进行签名。:<name>
:APEX 使用由名为<name>
的 Soong 模块定义的证书进行签名。证书模块可以定义如下。
android_app_certificate {
name: "my_key_name",
certificate: "dir/cert",
// this will use dir/cert.x509.pem (the cert) and dir/cert.pk8 (the private key)
}
安装 APEX
要安装 APEX,请使用 ADB。
adb install apex_file_name
adb reboot
如果在 apex_manifest.json
中将 supportsRebootlessUpdate
设置为 true
,并且当前安装的 APEX 未使用(例如,它包含的任何服务都已停止),则可以使用 --force-non-staged
标志在不重启的情况下安装新的 APEX。
adb install --force-non-staged apex_file_name
使用 APEX
重启后,APEX 将挂载在 /apex/<apex_name>@<version>
目录中。同一 APEX 的多个版本可以同时挂载。在挂载路径中,与最新版本对应的路径会绑定挂载到 /apex/<apex_name>
。
客户端可以使用绑定挂载的路径来读取或执行 APEX 中的文件。
APEX 通常按如下方式使用
- OEM 或 ODM 在设备出厂时预加载
/system/apex
下的 APEX。 - APEX 中的文件通过
/apex/<apex_name>/
路径访问。 - 当更新版本的 APEX 安装在
/data/apex
中时,该路径在重启后指向新的 APEX。
使用 APEX 更新服务
要使用 APEX 更新服务
将系统分区中的服务标记为可更新。将选项
updatable
添加到服务定义中。/system/etc/init/myservice.rc: service myservice /system/bin/myservice class core user system ... updatable
为更新后的服务创建新的
.rc
文件。使用override
选项重新定义现有服务。/apex/my.apex/etc/init.rc: service myservice /apex/my.apex/bin/myservice class core user system ... override
服务定义只能在 APEX 的 .rc
文件中定义。APEX 中不支持操作触发器。
如果标记为可更新的服务在 APEX 激活之前启动,则启动将延迟到 APEX 激活完成。
配置系统以支持 APEX 更新
将以下系统属性设置为 true
以支持 APEX 文件更新。
<device.mk>:
PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=true
BoardConfig.mk:
TARGET_FLATTEN_APEX := false
或仅
<device.mk>:
$(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk)
扁平化 APEX
对于旧设备,有时无法或不可行更新旧内核以完全支持 APEX。例如,内核可能是在未启用 CONFIG_BLK_DEV_LOOP=Y
的情况下构建的,这对于挂载 APEX 内的文件系统镜像至关重要。
扁平化 APEX 是一种特殊构建的 APEX,可以在具有旧内核的设备上激活。扁平化 APEX 中的文件直接安装到内置分区下的目录中。例如,扁平化 APEX my.apex
中的 lib/libFoo.so
将安装到 /system/apex/my.apex/lib/libFoo.so
。
激活扁平化 APEX 不涉及环回设备。整个目录 /system/apex/my.apex
直接绑定挂载到 /apex/name@ver
。
扁平化 APEX 无法通过从网络下载更新版本的 APEX 进行更新,因为下载的 APEX 无法扁平化。扁平化 APEX 只能通过常规 OTA 进行更新。
扁平化 APEX 是默认配置。这意味着除非您显式配置设备构建非扁平化 APEX 以支持 APEX 更新(如上所述),否则所有 APEX 默认都是扁平化的。
不支持在设备中混合使用扁平化和非扁平化 APEX。设备中的 APEX 必须全部是非扁平化或全部是扁平化。这在为 Mainline 等项目交付预签名 APEX 预编译文件时尤为重要。未预签名的 APEX(即从源代码构建的 APEX)也应是非扁平化的,并使用正确的密钥进行签名。设备应从 updatable_apex.mk
继承,如使用 APEX 更新服务中所述。
压缩 APEX
Android 12 及更高版本具有 APEX 压缩功能,可减少可更新 APEX 软件包的存储影响。在安装 APEX 的更新后,尽管不再使用其预安装版本,但它仍然占用相同的空间。该占用的空间仍然不可用。
APEX 压缩通过在只读分区(例如 /system
分区)上使用高度压缩的 APEX 文件集来最大限度地减少这种存储影响。Android 12 及更高版本使用 DEFLATE zip 压缩算法。
压缩不为以下项提供优化
需要在启动序列早期挂载的引导 APEX。
不可更新的 APEX。仅当 APEX 的更新版本安装在
/data
分区上时,压缩才有利。模块化系统组件页面上提供了可更新 APEX 的完整列表。动态共享库 APEX。由于
apexd
始终激活此类 APEX 的两个版本(预安装和升级),因此压缩它们不会增加价值。
压缩 APEX 文件格式
这是压缩 APEX 文件的格式。
图 2. 压缩 APEX 文件格式
在顶层,压缩 APEX 文件是一个 zip 文件,其中包含原始 apex 文件(采用 deflate 形式,压缩级别为 9)以及其他未压缩存储的文件。
APEX 文件包含四个文件
original_apex
:使用压缩级别 9 进行 deflate 压缩。这是原始的未压缩 APEX 文件。apex_manifest.pb
:仅存储AndroidManifest.xml
:仅存储apex_pubkey
:仅存储
apex_manifest.pb
、AndroidManifest.xml
和 apex_pubkey
文件是 original_apex
中相应文件的副本。
构建压缩 APEX
可以使用位于 system/apex/tools
的 apex_compression_tool.py
工具构建压缩 APEX。
构建系统中提供了几个与 APEX 压缩相关的参数。
在 Android.bp
中,APEX 文件是否可压缩由 compressible
属性控制
apex {
name: "apex.test",
manifest: "apex_manifest.json",
file_contexts: "file_contexts",
compressible: true,
}
PRODUCT_COMPRESSED_APEX
产品标志控制从源代码构建的系统镜像是否必须包含压缩的 APEX 文件。
对于本地实验,您可以通过将 OVERRIDE_PRODUCT_COMPRESSED_APEX=
设置为 true
来强制构建压缩 APEX。
构建系统生成的压缩 APEX 文件具有 .capex
扩展名。该扩展名使区分 APEX 文件的压缩版本和未压缩版本变得更容易。
支持的压缩算法
Android 12 仅支持 deflate-zip 压缩。
在启动期间激活压缩 APEX 文件
在激活压缩 APEX 之前,它内部的 original_apex
文件将被解压缩到 /data/apex/decompressed
目录中。生成的解压缩 APEX 文件硬链接到 /data/apex/active
目录。
请考虑以下示例,以说明上述过程。
假设 /system/apex/com.android.foo.capex
是正在激活的压缩 APEX,版本代码为 37。
/system/apex/com.android.foo.capex
中的original_apex
文件被解压缩到/data/apex/decompressed/com.android.foo@37.apex
。- 执行
restorecon /data/apex/decompressed/com.android.foo@37.apex
以验证它是否具有正确的 SELinux 标签。 - 对
/data/apex/decompressed/com.android.foo@37.apex
执行验证检查以确保其有效性:apexd
检查捆绑在/data/apex/decompressed/com.android.foo@37.apex
中的公钥,以验证它是否与捆绑在/system/apex/com.android.foo.capex
中的公钥相同。 /data/apex/decompressed/com.android.foo@37.apex
文件硬链接到/data/apex/active/com.android.foo@37.apex
目录。- 在
/data/apex/active/com.android.foo@37.apex
上执行未压缩 APEX 文件的常规激活逻辑。
与 OTA 的交互
压缩 APEX 文件对 OTA 交付和应用具有影响。由于 OTA 更新可能包含版本级别高于设备上激活版本的压缩 APEX 文件,因此在设备重启以应用 OTA 更新之前,必须预留一定量的可用空间。
为了支持 OTA 系统,apexd
公开了以下两个 binder API
calculateSizeForCompressedApex
- 计算 OTA 软件包中解压缩 APEX 文件所需的大小。这可用于在下载 OTA 之前验证设备是否具有足够的空间。reserveSpaceForCompressedApex
- 在磁盘上预留空间,供apexd
将来使用,以解压缩 OTA 软件包内的压缩 APEX 文件。
在 A/B OTA 更新的情况下,apexd
尝试在后台解压缩,作为 postinstall OTA 例程的一部分。如果解压缩失败,apexd
将在应用 OTA 更新的启动期间执行解压缩。
开发 APEX 时考虑的替代方案
以下是 AOSP 在设计 APEX 文件格式时考虑的一些选项,以及它们被包含或排除的原因。
常规软件包管理系统
Linux 发行版具有软件包管理系统,如 dpkg
和 rpm
,它们功能强大、成熟且可靠。但是,它们未被 APEX 采用,因为它们无法在安装后保护软件包。仅在安装软件包时执行验证。攻击者可能会破坏已安装软件包的完整性,而不会被注意到。这对于 Android 来说是一种倒退,因为所有系统组件都存储在只读文件系统中,这些文件系统的完整性受到 dm-verity 的保护,以进行每次 I/O。必须禁止或检测对系统组件的任何篡改,以便设备可以在受损时拒绝启动。
用于完整性的 dm-crypt
APEX 容器中的文件来自受 dm-verity 保护的内置分区(例如,/system
分区),即使在分区挂载后也禁止对文件进行任何修改。为了为文件提供相同的安全级别,APEX 中的所有文件都存储在文件系统镜像中,该镜像与哈希树和 vbmeta 描述符配对。如果没有 dm-verity,/data
分区中的 APEX 很容易受到验证和安装后进行的意外修改的影响。
实际上,/data
分区也受到加密层(如 dm-crypt)的保护。虽然这提供了一定程度的防篡改保护,但其主要目的是隐私,而不是完整性。当攻击者获得对 /data
分区的访问权限时,可能不再有进一步的保护,这再次是一种倒退,与每个系统组件都在 /system
分区中相比。APEX 文件内部的哈希树与 dm-verity 一起提供相同级别的内容保护。
将路径从 /system 重定向到 /apex
APEX 中打包的系统组件文件可以通过新路径访问,例如 /apex/<name>/lib/libfoo.so
。当文件是 /system
分区的一部分时,它们可以通过诸如 /system/lib/libfoo.so
之类的路径访问。APEX 文件的客户端(其他 APEX 文件或平台)必须使用新路径。您可能需要更新现有代码以适应路径更改。
虽然避免路径更改的一种方法是将 APEX 文件中的文件内容覆盖到 /system
分区上,但 Android 团队决定不将文件覆盖到 /system
分区上,因为这可能会影响性能,因为被覆盖的文件数量(甚至可能彼此堆叠)会增加。
另一种选择是劫持文件访问函数(如 open
、stat
和 readlink
),以便将以 /system
开头的路径重定向到其在 /apex
下的对应路径。Android 团队放弃了此选项,因为更改所有接受路径的函数是不可行的。例如,某些应用静态链接了 Bionic,后者实现了这些函数。在这种情况下,这些应用不会被重定向。