頂點文件格式

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 文件的格式。

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.xmlAndroidManifest.xml可能包含可供現有應用發布工具使用的其他目標信息。

apex_payload.img是由 dm-verity 支持的 ext4 文件系統映像。映像在運行時通過回送設備安裝。具體來說,哈希樹和元數據塊是使用libavb庫創建的。不解析文件系統有效負載(因為圖像應該可以就地安裝)。常規文件包含在apex_payload.img文件中。

apex_pubkey是用於簽署文件系統映像的公鑰。在運行時,此密鑰可確保下載的 APEX 使用與內置分區中相同 APEX 簽名的同一實體進行簽名。

頂點經理

APEX 管理器(或apexd )是一個獨立的本機進程,負責驗證、安裝和卸載 APEX 文件。此過程已啟動並在引導序列的早期準備就緒。 APEX 文件通常預安裝在設備上的/system/apex下。如果沒有可用的更新,APEX 管理器默認使用這些包。

APEX 的更新順序使用PackageManager 類,如下所示。

  1. APEX 文件通過包安裝程序應用程序、ADB 或其他來源下載。
  2. 包管理器啟動安裝過程。在識別出文件是 APEX 後,包管理器將控制權轉移給 APEX 管理器。
  3. APEX 管理器驗證 APEX 文件。
  4. 如果驗證了 APEX 文件,則更新 APEX 管理器的內部數據庫以反映 APEX 文件在下次啟動時被激活。
  5. 安裝請求者在成功的包驗證後接收廣播。
  6. 要繼續安裝,必須重新啟動系統。
  7. 下次啟動時,APEX 管理器啟動,讀取內部數據庫,並對列出的每個 APEX 文件執行以下操作:

    1. 驗證 APEX 文件。
    2. 從 APEX 文件創建環回設備。
    3. 在環回設備之上創建一個設備映射器塊設備。
    4. 將設備映射器塊設備安裝到唯一路徑(例如/apex/ name @ ver )。

當內部數據庫中列出的所有 APEX 文件都已掛載時,APEX 管理器為其他系統組件提供了一個 binder 服務,以查詢有關已安裝 APEX 文件的信息。例如,其他系統組件可以查詢設備中安裝的APEX文件列表,或者查詢特定APEX掛載的確切路徑,從而可以訪問這些文件。

APEX 文件是 APK 文件

APEX 文件是有效的 APK 文件,因為它們是包含AndroidManifest.xml文件的簽名 zip 存檔(使用 APK 簽名方案)。這允許 APEX 文件使用 APK 文件的基礎架構,例如包安裝程序應用程序、簽名實用程序和包管理器。

APEX 文件中的AndroidManifest.xml文件非常小,由包nameversionCode和可選targetSdkVersionminSdkVersionmaxSdkVersion ,用於細粒度定位。此信息允許通過現有渠道(例如軟件包安裝程序應用程序和 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的默認值。

javalibrariesprebuilts屬性與 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 pair
openssl genrsa -out foo.pem 4096

# extract the public key from the key pair
avbtool extract_public_key --key foo.pem --output foo.avbpubkey

# in Android.bp
apex_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 通常按如下方式使用:

  1. 設備出廠時,OEM 或 ODM 會在/system/apex下預加載 APEX。
  2. APEX 中的文件通過/apex/<apex_name>/路徑訪問。
  3. 當更新版本的 APEX 安裝在/data/apex中時,重新啟動後路徑將指向新的 APEX。

使用 APEX 更新服務

要使用 APEX 更新服務:

  1. 將系統分區中的服務標記為可更新。將updatable選項添加到服務定義。

    /system/etc/init/myservice.rc:
    
    service myservice /system/bin/myservice
        class core
        user system
        ...
        updatable
    
  2. 為更新的服務創建一個新的.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 文件的格式。

Diagram shows the format of a compressed APEX file

圖 2.壓縮的 APEX 文件格式

在頂層,壓縮的 APEX 文件是一個 zip 文件,其中包含壓縮級別為 9 的壓縮形式的原始 apex 文件,以及未壓縮存儲的其他文件。

四個文件組成一個 APEX 文件:

  • original_apex :壓縮級別為 9 的壓縮 這是原始的、未壓縮的APEX 文件
  • apex_manifest.pb :僅存儲
  • AndroidManifest.xml :僅存儲
  • apex_pubkey :僅存儲

apex_manifest.pbAndroidManifest.xmlapex_pubkey文件是它們在original_apex中對應文件的副本。

構建壓縮的 APEX

壓縮的 APEX 可以使用位於system/apex/toolsapex_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。

  1. /system/apex/com.android.foo.capex裡面的original_apex文件被解壓成/data/apex/decompressed/com.android.foo@37.apex
  2. 執行restorecon /data/apex/decompressed/com.android.foo@37.apex以驗證它是否具有正確的 SELinux 標籤。
  3. /data/apex/decompressed/com.android.foo@37.apex執行驗證檢查以確保其有效性: apexd檢查捆綁在/data/apex/decompressed/com.android.foo@37.apex中的公鑰以驗證它是否等於捆綁在/system/apex/com.android.foo.capex中的那個。
  4. /data/apex/decompressed/com.android.foo@37.apex文件硬鏈接到/data/apex/active/com.android.foo@37.apex目錄。
  5. 未壓縮 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 發行版具有像dpkgrpm這樣的包管理系統,它們功能強大、成熟且健壯。但是,它們沒有被 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分區上覆蓋文件,因為這可能會影響性能,因為被覆蓋的文件數量(甚至可能一個接一個地堆疊)增加。

另一種選擇是劫持文件訪問函數,例如openstatreadlink ,以便將/system開頭的路徑重定向到/apex下的相應路徑。 Android 團隊放棄了這個選項,因為改變所有接受路徑的函數是不可行的。例如,一些應用程序靜態鏈接仿生,它實現了這些功能。在這種情況下,這些應用程序不會被重定向。