Android Pony EXpress (APEX) 容器格式是在 Android 10 中引入的,它用於較低級別系統模塊的安裝流程。這種格式有助於更新不適合標準 Android 應用程序模型的系統組件。一些示例組件是本機服務和庫、硬件抽象層 ( HAL )、運行時 ( ART ) 和類庫。
術語“APEX”也可以指 APEX 文件。
背景
儘管 Android 支持通過包安裝程序應用程序(例如 Google Play Store 應用程序)更新適合標準應用程序模型(例如服務、活動)的模塊,但對較低級別的操作系統組件使用類似模型具有以下缺點:
- 基於 APK 的模塊不能在啟動序列的早期使用。包管理器是有關應用程序信息的中央存儲庫,只能從活動管理器啟動,該活動管理器在啟動過程的後期準備就緒。
- APK 格式(尤其是清單)是為 Android 應用程序設計的,系統模塊並不總是很合適。
設計
本節介紹 APEX 文件格式和 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 文件。
AndroidManifest.xml
文件允許 APEX 文件使用 APK 相關的工具和基礎設施,例如 ADB、PackageManager 和包安裝程序應用程序(例如 Play 商店)。例如,APEX 文件可以使用現有工具(例如aapt
來檢查文件中的基本元數據。該文件包含包名稱和版本信息。此信息通常也可在apex_manifest.json
中找到。
對於處理 APEX 的新代碼和系統,建議使用apex_manifest.json
而不是AndroidManifest.xml
。 AndroidManifest.xml
可能包含可供現有應用發布工具使用的其他目標信息。
apex_payload.img
是由 dm-verity 支持的 ext4 文件系統映像。映像在運行時通過回送設備安裝。具體來說,哈希樹和元數據塊是使用libavb
庫創建的。不解析文件系統有效負載(因為圖像應該可以就地安裝)。常規文件包含在apex_payload.img
文件中。
apex_pubkey
是用於簽署文件系統映像的公鑰。在運行時,此密鑰可確保下載的 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 文件,因為它們是包含AndroidManifest.xml
文件的簽名 zip 存檔(使用 APK 簽名方案)。這允許 APEX 文件使用 APK 文件的基礎架構,例如包安裝程序應用程序、簽名實用程序和包管理器。
APEX 文件中的AndroidManifest.xml
文件非常小,由包name
、 versionCode
和可選targetSdkVersion
、 minSdkVersion
和maxSdkVersion
,用於細粒度定位。此信息允許通過現有渠道(例如軟件包安裝程序應用程序和 ADB)交付 APEX 文件。
支持的文件類型
APEX 格式支持以下文件類型:
- 本機共享庫
- 本機可執行文件
- JAR 文件
- 數據文件
- 配置文件
這並不意味著 APEX 可以更新所有這些文件類型。文件類型是否可以更新取決於平台以及文件類型接口定義的穩定性。
簽約
APEX 文件以兩種方式簽名。首先,使用密鑰對apex_payload.img
(特別是附加到apex_payload.img
的 vbmeta 描述符)文件進行簽名。然後,使用APK 簽名方案 v3對整個 APEX 進行簽名。在此過程中使用了兩個不同的密鑰。
在設備端,安裝了對應於用於簽署 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 模塊。
在使用 APEX 模塊時,環回驅動程序和 dm-verity 的性能對於實現良好的系統性能非常重要。
支持的內核版本
使用內核版本 4.4 或更高版本的設備支持 APEX 主線模塊。搭載 Android 10 或更高版本的新設備必須使用內核版本 4.9 或更高版本才能支持 APEX 模塊。
所需的內核補丁
支持 APEX 模塊所需的內核補丁包含在 Android 通用樹中。要獲取支持 APEX 的補丁程序,請使用最新版本的 Android 公共樹。
內核版本 4.4
此版本僅支持從 Android 9 升級到 Android 10 並希望支持 APEX 模塊的設備。要獲得所需的補丁,強烈建議從android-4.4
分支進行向下合併。以下是內核版本 4.4 所需的各個補丁的列表。
- 上游:循環:添加 ioctl 以更改邏輯塊大小( 4.4 )
- BACKPORT:塊/循環:設置 hw_sectors ( 4.4 )
- 上游:循環:在兼容 ioctl ( 4.4 ) 中添加 LOOP_SET_BLOCK_SIZE
- ANDROID:mnt:修復 next_descendent( 4.4 )
- ANDROID: mnt: remount 應該傳播到奴隸的奴隸( 4.4 )
- ANDROID:mnt:正確傳播重新安裝( 4.4 )
- 還原“ANDROID:dm verity:添加最小預取大小”( 4.4 )
- 上游:循環:如果偏移量或塊大小發生更改,則丟棄緩存( 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) 安裝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。
郵編簽名
以與簽署 APK 相同的方式簽署 APEX。兩次簽署 APEX;一次用於迷你文件系統( apex_payload.img
文件),一次用於整個文件。
要在文件級別簽署 APEX,請通過以下三種方式之一設置certificate
屬性:
- 未設置:如果未設置任何值,則使用位於
PRODUCT_DEFAULT_DEV_CERTIFICATE
的證書對 APEX 進行簽名。如果未設置標誌,則路徑默認為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
重新啟動後,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 中安裝文件系統映像至關重要。
Flattened 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 也應該是非扁平化的,並使用正確的密鑰進行簽名。設備應從updatable_apex.mk
繼承,如使用 APEX 更新服務中所述。
壓縮的 APEX
Android 12 及更高版本具有 APEX 壓縮功能,可減少可更新 APEX 包的存儲影響。安裝 APEX 更新後,雖然不再使用其預安裝版本,但仍佔用相同數量的空間。佔用的空間仍然不可用。
APEX 壓縮通過在只讀分區(例如/system
分區)上使用一組高度壓縮的 APEX 文件來最大限度地減少這種存儲影響。 Android 12 及更高版本使用 DEFLATE zip 壓縮算法。
壓縮不提供以下優化:
需要在引導序列的早期安裝的引導 APEX。
不可更新的 APEX。只有在
/data
分區上安裝了更新版本的 APEX 時,壓縮才有用。模塊化系統組件頁面上提供了可更新 APEX 的完整列表。動態共享庫 APEX。由於
apexd
始終激活此類 APEX 的兩個版本(預安裝和升級),因此壓縮它們不會增加價值。
壓縮的 APEX 文件格式
這是壓縮 APEX 文件的格式。
圖 2.壓縮的 APEX 文件格式
在頂層,壓縮的 APEX 文件是一個 zip 文件,其中包含壓縮級別為 9 的壓縮形式的原始 apex 文件,以及未壓縮存儲的其他文件。
四個文件組成一個 APEX 文件:
-
original_apex
:壓縮級別為 9 的壓縮 這是原始的、未壓縮的APEX 文件。 -
apex_manifest.pb
:僅存儲 AndroidManifest.xml
:僅存儲apex_pubkey
:僅存儲
apex_manifest.pb
、 AndroidManifest.xml
和apex_pubkey
文件是它們在original_apex
中對應文件的副本。
構建壓縮的 APEX
壓縮的 APEX 可以使用位於system/apex/tools
的apex_compression_tool.py
工具構建。
構建系統中提供了幾個與 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
目錄。 - 未壓縮 APEX 文件的常規激活邏輯在
/data/apex/active/com.android.foo@37.apex
上執行。
與OTA互動
壓縮的 APEX 文件對 OTA 交付和應用程序有影響。由於 OTA 更新可能包含一個壓縮的 APEX 文件,其版本級別高於設備上的活動版本,因此在重新啟動設備以應用 OTA 更新之前,必須保留一定數量的可用空間。
為了支持 OTA 系統, apexd
公開了這兩個 binder API:
-
calculateSizeForCompressedApex
- 計算解壓縮 OTA 包中的 APEX 文件所需的大小。這可用於在下載 OTA 之前驗證設備是否有足夠的空間。 -
reserveSpaceForCompressedApex
- 在磁盤上保留空間供apexd
用於解壓縮 OTA 包內的壓縮 APEX 文件。
在 A/B OTA 更新的情況下, apexd
嘗試在後台解壓縮,作為安裝後 OTA 例程的一部分。如果解壓失敗, apexd
在應用 OTA 更新的引導期間執行解壓。
開發 APEX 時考慮的替代方案
以下是 AOSP 在設計 APEX 文件格式時考慮的一些選項,以及包含或排除它們的原因。
常規包裹管理系統
Linux 發行版具有像dpkg
和rpm
這樣的包管理系統,它們功能強大、成熟且健壯。但是,它們沒有被 APEX 採用,因為它們在安裝後無法保護包。僅在安裝軟件包時執行驗證。攻擊者可以在不被注意的情況下破壞已安裝軟件包的完整性。這是 Android 的回歸,其中所有系統組件都存儲在只讀文件系統中,其完整性由每個 I/O 的 dm-verity 保護。對系統組件的任何篡改都必須被禁止或可檢測到,以便設備在受到損害時可以拒絕啟動。
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 團隊放棄了這個選項,因為改變所有接受路徑的函數是不可行的。例如,一些應用程序靜態鏈接仿生,它實現了這些功能。在這種情況下,這些應用程序不會被重定向。