mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a1da333eb | |||
| 256483d248 | |||
| 2314c371a8 | |||
| 59cf37faaf | |||
| 36269acd7b | |||
| 6aacc5a76c | |||
| bd4ebde403 | |||
| ffdae0bf88 | |||
| 8282f8087c | |||
| 6b11cd8af2 | |||
| f610f3b794 | |||
| b8d022d52c | |||
| cd38217111 | |||
| c8d548d224 | |||
| 9ad430ac7f | |||
| 8ca4eb5ad9 | |||
| e2b1ccb7a1 | |||
| 71a31ae01b | |||
| 83d7ffbc5e | |||
| e397998d9e | |||
| 8ef22508f5 | |||
| bf42a6a75c | |||
| a2fe6fc9d8 | |||
| faeb1fe988 | |||
| f1b2c6f468 | |||
| 2f964bf113 | |||
| f393d7adc4 | |||
| 72a1c3f216 | |||
| 2db8d3826e | |||
| 1448e55205 | |||
| b3862a9c4a | |||
| 088db04673 | |||
| b1410c869d | |||
| dd6eaf8e78 | |||
| 0eac8e6614 | |||
| 8a4f90176e | |||
| bd166594e2 | |||
| 19129ae927 | |||
| fb2aaed042 | |||
| 182d937d34 | |||
| 113bdec6dc | |||
| 1f0d012ac0 | |||
| e4fbe00ea2 | |||
| ad0b3d5aa8 | |||
| f4394b0c21 | |||
| 9dd98fd715 | |||
| b6e3c68241 | |||
| 6fee5346bb | |||
| 7d786150cf | |||
| b922f0d23c | |||
| 1f2b2170b3 | |||
| da124e4e05 | |||
| 7b778515d1 | |||
| 98745afe33 | |||
| a47c501211 | |||
| f2e47a65d4 | |||
| e11ddd2ba7 | |||
| 5d66774d13 |
@@ -20,8 +20,6 @@ jobs:
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
# https://aws.github.io/aws-lc-rs/requirements/windows.html#use-of-prebuilt-nasm-objects
|
||||
AWS_LC_SYS_PREBUILT_NASM: 1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -85,7 +83,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
key: ${{ matrix.artifact.name }}
|
||||
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Run cargo-deny
|
||||
uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
|
||||
uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.version }}
|
||||
name: Version ${{ steps.get_version.outputs.version }}
|
||||
|
||||
@@ -7,6 +7,59 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.18.1
|
||||
|
||||
* Fix output file corruption in `avbroot sparse unpack` when unpacking a sparse file with holes larger than 2^32 ([Issue #472], [PR #476])
|
||||
|
||||
### Version 3.18.0
|
||||
|
||||
* Make OTA metadata property file field validation more lenient ([Issue #469], [PR #470])
|
||||
* Fixes `avbroot ota verify` for stock OTAs that include extra zip file entries in the metadata
|
||||
* Remove automatic promotion of insecure SHA-1 AVB hash algorithm to SHA-256 ([Issue #366], [Issue #469], [PR #473])
|
||||
* There are insecure devices that don't support SHA-256 and won't boot with it.
|
||||
* The original feature was a bandaid for OnePlus devices to make them a tiny bit more secure. They used SHA-256 for every partition except `system`. However, OnePlus no longer supports custom AVB keys anyway, so this feature is going away.
|
||||
* Add support for Magisk 30200 ([PR #474])
|
||||
* Update dependencies ([PR #475])
|
||||
|
||||
### Version 3.17.2
|
||||
|
||||
* Add support for Magisk 30100 ([PR #468])
|
||||
|
||||
### Version 3.17.1
|
||||
|
||||
* Update end-to-end tests to place streaming and seekable OTAs in separate directories for easier troubleshooting ([PR #463])
|
||||
* Update dependencies ([PR #464])
|
||||
* Add support for Magisk 30000 ([PR #467])
|
||||
|
||||
### Version 3.17.0
|
||||
|
||||
* Fix reserved space error when patching OTA zips larger than ~10 GB ([Issue #451], [PR #452])
|
||||
* Update dependencies ([PR #453])
|
||||
|
||||
### Version 3.16.1
|
||||
|
||||
* Add support for Magisk 29000 ([PR #448])
|
||||
* Update dependencies ([PR #449])
|
||||
|
||||
### Version 3.16.0
|
||||
|
||||
* Add support for CoW version 3 for virtual A/B ([Issue #441], [PR #442], [PR #445])
|
||||
* This was recently introduced with the Pixel 9a. Previous devices all used CoW version 2.
|
||||
* Add support for uncompressed CoW for virtual A/B ([PR #443])
|
||||
* This is not used on actual devices, but is very useful for testing the CoW estimation logic.
|
||||
* All differences between avbroot's and AOSP delta_generator's estimation logic are now fixed.
|
||||
* Add support for custom CoW compression levels for virtual A/B ([PR #444])
|
||||
* This is also not used on actual devices, but is supported by AOSP, so avbroot should support it too.
|
||||
* Update dependencies ([PR #446])
|
||||
|
||||
### Version 3.15.0
|
||||
|
||||
* Add support for changing the virtual A/B compression algorithm ([PR #437])
|
||||
* For devices that launched with Android <14, `--vabc-algo lz4` can significantly increase OTA installation speed when using a custom OTA updater app (with caveats). There is no difference when sideloading from recovery mode.
|
||||
* See [the documentation](./README.md#changing-virtual-ab-cow-compression-algorithm) for more details.
|
||||
* Switch back to the ring library now that it is maintained again ([PR #438])
|
||||
* Update dependencies ([PR #439])
|
||||
|
||||
### Version 3.14.0
|
||||
|
||||
* Report as many errors as possible before failing in `avbroot ota verify` and improve error messages ([Discussion #426], [PR #428], [PR #430])
|
||||
@@ -329,6 +382,10 @@ Behind-the-scenes changes:
|
||||
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
|
||||
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
|
||||
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
|
||||
[Issue #441]: https://github.com/chenxiaolong/avbroot/issues/441
|
||||
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
|
||||
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
|
||||
[Issue #472]: https://github.com/chenxiaolong/avbroot/issues/472
|
||||
[PR #130]: https://github.com/chenxiaolong/avbroot/pull/130
|
||||
[PR #132]: https://github.com/chenxiaolong/avbroot/pull/132
|
||||
[PR #133]: https://github.com/chenxiaolong/avbroot/pull/133
|
||||
@@ -480,3 +537,24 @@ Behind-the-scenes changes:
|
||||
[PR #430]: https://github.com/chenxiaolong/avbroot/pull/430
|
||||
[PR #434]: https://github.com/chenxiaolong/avbroot/pull/434
|
||||
[PR #435]: https://github.com/chenxiaolong/avbroot/pull/435
|
||||
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
|
||||
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
|
||||
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
|
||||
[PR #442]: https://github.com/chenxiaolong/avbroot/pull/442
|
||||
[PR #443]: https://github.com/chenxiaolong/avbroot/pull/443
|
||||
[PR #444]: https://github.com/chenxiaolong/avbroot/pull/444
|
||||
[PR #445]: https://github.com/chenxiaolong/avbroot/pull/445
|
||||
[PR #446]: https://github.com/chenxiaolong/avbroot/pull/446
|
||||
[PR #448]: https://github.com/chenxiaolong/avbroot/pull/448
|
||||
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
|
||||
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
|
||||
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
|
||||
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
|
||||
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
|
||||
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
|
||||
[PR #468]: https://github.com/chenxiaolong/avbroot/pull/468
|
||||
[PR #470]: https://github.com/chenxiaolong/avbroot/pull/470
|
||||
[PR #473]: https://github.com/chenxiaolong/avbroot/pull/473
|
||||
[PR #474]: https://github.com/chenxiaolong/avbroot/pull/474
|
||||
[PR #475]: https://github.com/chenxiaolong/avbroot/pull/475
|
||||
[PR #476]: https://github.com/chenxiaolong/avbroot/pull/476
|
||||
|
||||
Generated
+490
-459
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -4,9 +4,9 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.14.0"
|
||||
version = "3.18.1"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
|
||||
@@ -30,8 +30,6 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
|
||||
* To force an image to be signed, use `--key <path> --force`.
|
||||
* To force an image to be unsigned, use `--force` without specifying `--key`.
|
||||
|
||||
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
|
||||
|
||||
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
|
||||
|
||||
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
(This page is also available in: [Russian (Русский)](./README.ru.md).)
|
||||
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
|
||||
avbroot is a tool for modifying Android A/B OTA images reproducibly and re-signing them with custom keys. It also includes a [collection of subcommands](./README.extra.md) for packing and unpacking numerous Android image formats.
|
||||
|
||||
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
|
||||
|
||||
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
|
||||
@@ -23,7 +21,7 @@ Having a good understanding of how AVB and A/B OTAs work is recommended prior to
|
||||
|
||||
avbroot applies the following patches to the partition images:
|
||||
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
|
||||
* The `boot` or `init_boot` image, depending on device, is patched to enable root access if requested.
|
||||
|
||||
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
|
||||
|
||||
@@ -31,7 +29,7 @@ avbroot applies the following patches to the partition images:
|
||||
|
||||
## Warnings and Caveats
|
||||
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader while rooted.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
|
||||
|
||||
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
|
||||
|
||||
@@ -53,6 +51,8 @@ avbroot applies the following patches to the partition images:
|
||||
|
||||
3. Follow the steps to [generate signing keys](#generating-keys).
|
||||
|
||||
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
|
||||
|
||||
4. Patch the OTA zip. The base command is:
|
||||
|
||||
```bash
|
||||
@@ -225,21 +225,25 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
|
||||
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
|
||||
|
||||
10. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
|
||||
10. That's it! To update the OS, Magisk, or KernelSU see the [next section](#updates).
|
||||
|
||||
## Updates
|
||||
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
|
||||
Updates to Android, Magisk, and KernelSU are all done the same way: by patching (or repatching) the OTA.
|
||||
|
||||
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
|
||||
1. Generate a new patched OTA by following the steps in the [usage section](#usage).
|
||||
|
||||
2. Follow the step in the [usage section](#usage) to patch the new OTA.
|
||||
2. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
|
||||
|
||||
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
|
||||
|
||||
4. Sideload the patched OTA with `adb sideload`.
|
||||
|
||||
5. That's it!
|
||||
5. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
|
||||
|
||||
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
|
||||
|
||||
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
|
||||
|
||||
## Reverting to stock firmware
|
||||
|
||||
@@ -431,6 +435,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
|
||||
|
||||
### Changing virtual A/B CoW compression algorithm
|
||||
|
||||
The virtual A/B CoW compression algorithm can be changed by passing in `--vabc-algo <algo>` with `gz` or `lz4`. OTAs normally use an algorithm that is compatible with the initial version of Android shipped on the device.
|
||||
|
||||
* Devices launching with Android 12 support `gz` and `brotli` (unsupported by avbroot)
|
||||
* Devices launching with Android 14 support `lz4`
|
||||
* Devices launching with Android 15 support `zstd` (unsupported by avbroot)
|
||||
|
||||
Picking a fast algorithm, like lz4, can speed up OTA installation significantly when installing via a custom OTA updater app. However, there is no performance difference when sideloading an OTA from recovery mode.
|
||||
|
||||
Note that the currently running version of Android must support the specified compression algorithm or else the OTA will fail to install. For example, trying to install an Android 14 OTA that uses lz4 CoW compression will fail if the running system is Android 13.
|
||||
|
||||
### Non-interactive use
|
||||
|
||||
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
|
||||
|
||||
+39
-17
@@ -1,11 +1,9 @@
|
||||
# avbroot
|
||||
|
||||
avbroot – это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
|
||||
avbroot – это утилита для воспроизводимой модификации OTA-образов Android A/B-формата и их переподписания пользовательскими ключами. Она также включает в себя [набор подкоманд](./README.extra.md) для упаковки и распаковки образов Android различных форматов.
|
||||
|
||||
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
|
||||
|
||||
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
|
||||
|
||||
## Требования
|
||||
|
||||
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
|
||||
@@ -21,7 +19,7 @@ avbroot – это программа для модификации OTA-обра
|
||||
|
||||
avbroot модифицирует следующие образы:
|
||||
|
||||
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
|
||||
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа, если это запрашивается.
|
||||
|
||||
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
|
||||
|
||||
@@ -122,7 +120,7 @@ avbroot модифицирует следующие образы:
|
||||
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
|
||||
```
|
||||
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
|
||||
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при установке обновления.
|
||||
|
||||
```bash
|
||||
avbroot key generate-cert -k ota.key -o ota.crt
|
||||
@@ -185,7 +183,7 @@ avbroot совместим с любым стандартным 4096-битны
|
||||
fastboot flashall --skip-reboot
|
||||
```
|
||||
|
||||
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
|
||||
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
|
||||
|
||||
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
|
||||
|
||||
@@ -255,11 +253,11 @@ avbroot совместим с любым стандартным 4096-битны
|
||||
|
||||
## OTA-обновления
|
||||
|
||||
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как при загрузке в Android, так и при сайдлоадинге через Recovery.
|
||||
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как из-под загруженной системы, так и при прошивке через Recovery.
|
||||
|
||||
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
|
||||
Рекомендуется отключить системное приложение для обновлений, чтобы оно не пыталось установить непропатченные OTA:
|
||||
|
||||
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
|
||||
* Стоковая (заводская) прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
|
||||
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
|
||||
|
||||
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
|
||||
@@ -384,20 +382,32 @@ avbroot можно использовать для простого перепо
|
||||
|
||||
### Пропуск патчинга сертификата OTA
|
||||
|
||||
avbroot может пропускать изменение `otacerts.zip` с помощью аргументов `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный (boot) образ и передаете его программе через опции `--prepatched` или `--replace`.)
|
||||
В противном случае, на устройстве может не остаться возможности устанавливать дальнейшие обновления.
|
||||
Вы можете пропустить изменение otacerts.zip, используя аргументы `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.**
|
||||
|
||||
При использовании `--skip-system-ota-cert`, никаких изменений в образ `system` не вносится.
|
||||
При использовании `--skip-system-ota-cert`, сертификаты OTA в образе `system` изменены не будут. Это не позволит сторонним приложениям для OTA-обновлений устанавливать будущие пропатченные OTA из-под загруженной системы.
|
||||
|
||||
При использовании `--skip-recovery-ota-cert` совместно с `--rootless` и без указания `--dsu`, не вносится никаких изменений в загрузочные образы, кроме обеспечения их корректной подписи.
|
||||
При использовании `--skip-recovery-ota-cert`, сертификаты OTA в образах `vendor_boot` или `recovery` изменены не будут. **Это не позволит устанавливать будущие пропатченные OTA в режиме Recovery.**
|
||||
|
||||
Если вы вручную добавили сертификат OTA в загрузочный (boot) образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
|
||||
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
|
||||
|
||||
### Подмена разделов
|
||||
### Пропуск всех патчей
|
||||
|
||||
Чтобы внести самый минимум изменений, укажите аргументы:
|
||||
|
||||
* `--skip-system-ota-cert`
|
||||
* `--skip-recovery-ota-cert`
|
||||
* `--rootless`
|
||||
* не используйте аргумент `--dsu`.
|
||||
|
||||
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
|
||||
|
||||
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
|
||||
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
### Подмена образов
|
||||
|
||||
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
|
||||
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, используя аргумент `--replace <имя раздела> /путь/к/образу.img`.
|
||||
|
||||
Единственное, что меняется – это то, откуда считывается файл. При использовании `--replace` вместо образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
|
||||
|
||||
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
|
||||
|
||||
@@ -411,6 +421,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
|
||||
|
||||
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
|
||||
|
||||
### Изменение алгоритма CoW сжатия для вирутального A/B
|
||||
|
||||
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
|
||||
|
||||
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
|
||||
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
|
||||
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
|
||||
|
||||
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
|
||||
|
||||
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
|
||||
|
||||
### Использование в неинтерактивном режиме
|
||||
|
||||
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
|
||||
|
||||
+17
-15
@@ -10,15 +10,10 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
# We use aws-lc-rs instead of sha2 for sha256 digest computation of large files
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
aws-lc-rs = { version = "1.0.0", default-features = false, features = ["aws-lc-sys"] }
|
||||
base64 = "0.22.1"
|
||||
bitflags = { version = "2.4.1", features = ["serde"] }
|
||||
bstr = "1.6.2"
|
||||
bzip2 = { version = "0.5.1", default-features = false, features = ["libbz2-rs-sys"] }
|
||||
bzip2 = "0.6.0"
|
||||
cap-std = "3.0.0"
|
||||
cap-tempfile = "3.0.0"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
@@ -33,26 +28,31 @@ dlv-list = "0.6.0"
|
||||
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
|
||||
gf256 = { version = "0.3.0", features = ["rs"] }
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
liblzma = "0.3.0"
|
||||
liblzma = "0.4.1"
|
||||
lz4_flex = "0.11.1"
|
||||
memchr = "2.6.0"
|
||||
num-bigint-dig = "0.8.4"
|
||||
num-traits = "0.2.16"
|
||||
passterm = "2.0.3"
|
||||
phf = { version = "0.11.2", features = ["macros"] }
|
||||
phf = { version = "0.12.1", features = ["macros"] }
|
||||
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
|
||||
prost = "0.13.1"
|
||||
prost = "0.14.1"
|
||||
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
|
||||
rand = "0.8.5"
|
||||
rayon = "1.7.0"
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
# We use ring instead of sha2 for sha256 digest computation of large files
|
||||
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
|
||||
# instructions. sha2 is still used for signing purposes.
|
||||
# https://github.com/RustCrypto/hashes/issues/327
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
sha1 = "0.10.5"
|
||||
sha2 = "0.10.7"
|
||||
tempfile = "3.8.0"
|
||||
thiserror = "2.0.3"
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
toml_edit = { version = "0.23.3", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
@@ -60,10 +60,12 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
|
||||
zerocopy = { version = "0.8.10", features = ["std"] }
|
||||
zerocopy-derive = "0.8.5"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
features = ["deflate"]
|
||||
|
||||
@@ -73,8 +75,8 @@ rustix = { version = "1.0.3", default-features = false, features = ["process"] }
|
||||
|
||||
[build-dependencies]
|
||||
constcat = "0.6.0"
|
||||
prost-build = "0.13.1"
|
||||
protox = "0.7.0"
|
||||
prost-build = "0.14.1"
|
||||
protox = "0.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5.0"
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing::{Level, debug};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
|
||||
|
||||
+5
-20
@@ -10,7 +10,7 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{
|
||||
ambient_authority,
|
||||
fs::{Dir, OpenOptions},
|
||||
@@ -19,7 +19,7 @@ use clap::{Args, Parser, Subcommand};
|
||||
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
||||
use rsa::RsaPublicKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug_span, info, warn, Span};
|
||||
use tracing::{Span, debug_span, info, warn};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
@@ -27,7 +27,7 @@ use crate::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
|
||||
stream::{self, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter, check_cancel},
|
||||
util,
|
||||
};
|
||||
|
||||
@@ -95,18 +95,6 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Packing with insecure algorithms is intentionally not supported, so promote
|
||||
/// to a secure algorithm if needed.
|
||||
fn promote_insecure_hash_algorithm(algorithm: &mut String) {
|
||||
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
|
||||
const NEW_ALGORITHM: &str = "sha256";
|
||||
|
||||
if INSECURE_ALGORITHMS.contains(&algorithm.as_str()) {
|
||||
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
|
||||
NEW_ALGORITHM.clone_into(algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
|
||||
/// both readable and writable.
|
||||
fn write_raw(
|
||||
@@ -194,13 +182,11 @@ fn write_raw_and_update(
|
||||
|
||||
match info.header.appended_descriptor_mut()? {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.image_size = image_size;
|
||||
d.update(&raw_file, &raw_file, None, cancel_signal)
|
||||
.context("Failed to update hash tree descriptor")?;
|
||||
}
|
||||
AppendedDescriptorMut::Hash(d) => {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.image_size = image_size;
|
||||
raw_file.rewind()?;
|
||||
d.update(&mut raw_file, cancel_signal)
|
||||
@@ -599,7 +585,7 @@ pub fn verify_descriptors(
|
||||
fn compute_digest_recursive(
|
||||
directory: &Dir,
|
||||
name: &str,
|
||||
context: &mut aws_lc_rs::digest::Context,
|
||||
context: &mut ring::digest::Context,
|
||||
max_depth: u8,
|
||||
seen: &mut HashSet<String>,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -670,7 +656,7 @@ fn compute_digest_recursive(
|
||||
/// chained partitions more than one level deep are ignored.
|
||||
pub fn compute_digest(directory: &Dir, name: &str, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let mut seen = HashSet::<String>::new();
|
||||
let mut context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
|
||||
compute_digest_recursive(directory, name, &mut context, 2, &mut seen, cancel_signal)?;
|
||||
|
||||
@@ -745,7 +731,6 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
// Write new hash tree and FEC data instead of copying the original.
|
||||
// There could have been errors in the original FEC data itself.
|
||||
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
|
||||
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
|
||||
d.update(&file, &file, None, cancel_signal)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use bstr::ByteSlice;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use rayon::iter::{
|
||||
|
||||
+355
-168
@@ -5,24 +5,25 @@ use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeSet, HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::Display,
|
||||
fs::{self, File},
|
||||
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
sync::{atomic::AtomicBool, Mutex},
|
||||
str::FromStr,
|
||||
sync::{Mutex, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use bitflags::bitflags;
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempDir;
|
||||
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
|
||||
use clap::{ArgAction, Args, Parser, Subcommand, value_parser};
|
||||
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
|
||||
use tempfile::NamedTempFile;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{debug_span, error, info, warn};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
use zip::{CompressionMethod, DateTime, ZipArchive, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
cli,
|
||||
@@ -31,7 +32,8 @@ use crate::{
|
||||
avb::{self, Descriptor, Header},
|
||||
ota::{self, SigningWriter, ZipEntry, ZipMode},
|
||||
padding,
|
||||
payload::{self, PayloadHeader, PayloadWriter, VabcAlgo},
|
||||
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcAlgo, VabcParams},
|
||||
zip::ZipWriterWrapper,
|
||||
},
|
||||
patch::{
|
||||
boot::{
|
||||
@@ -50,72 +52,67 @@ use crate::{
|
||||
util,
|
||||
};
|
||||
|
||||
fn joined(into_iter: impl IntoIterator<Item = impl Display>) -> String {
|
||||
use std::fmt::Write;
|
||||
bitflags! {
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PartitionFlags: u8 {
|
||||
const BOOT = 1 << 0;
|
||||
const SYSTEM = 1 << 1;
|
||||
const VBMETA = 1 << 2;
|
||||
const COW = 1 << 3;
|
||||
|
||||
let mut result = String::new();
|
||||
const KNOWN = Self::BOOT.bits() | Self::SYSTEM.bits() | Self::VBMETA.bits();
|
||||
}
|
||||
|
||||
for (i, item) in into_iter.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push_str(", ");
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct RequiredFlags: u8 {
|
||||
const SYSTEM = 1 << 0;
|
||||
const ALL_COW = 1 << 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the images required for patching. If [`RequiredFlags::SYSTEM`] is
|
||||
/// specified, then the system image is included. If [`RequiredFlags::ALL_COW`]
|
||||
/// is specified, then all images with CoW size estimates are included.
|
||||
pub fn get_required_images(
|
||||
manifest: &DeltaArchiveManifest,
|
||||
required_flags: RequiredFlags,
|
||||
) -> HashMap<String, PartitionFlags> {
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for partition in &manifest.partitions {
|
||||
let name = &partition.partition_name;
|
||||
let mut flags = PartitionFlags::empty();
|
||||
|
||||
if name == "boot" || name == "init_boot" || name == "recovery" || name == "vendor_boot" {
|
||||
flags |= PartitionFlags::BOOT;
|
||||
} else if required_flags.contains(RequiredFlags::SYSTEM) && name == "system" {
|
||||
flags |= PartitionFlags::SYSTEM;
|
||||
} else if name.starts_with("vbmeta") {
|
||||
flags |= PartitionFlags::VBMETA;
|
||||
}
|
||||
|
||||
write!(result, "{item}").expect("Failed to allocate");
|
||||
if partition.estimate_cow_size.is_some() {
|
||||
flags |= PartitionFlags::COW;
|
||||
}
|
||||
|
||||
// Skip completely unrecognized partitions.
|
||||
if flags.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip unrecognized CoW partitions unless we ask for them.
|
||||
if flags == PartitionFlags::COW && !required_flags.contains(RequiredFlags::ALL_COW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.insert(name.clone(), flags);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn sorted<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
|
||||
let mut items = iter.collect::<Vec<_>>();
|
||||
items.sort();
|
||||
items
|
||||
}
|
||||
|
||||
pub struct RequiredImages(HashSet<String>);
|
||||
|
||||
impl RequiredImages {
|
||||
pub fn new(manifest: &DeltaArchiveManifest) -> Self {
|
||||
let partitions = manifest
|
||||
.partitions
|
||||
.iter()
|
||||
.map(|p| &p.partition_name)
|
||||
.filter(|n| Self::is_boot(n) || Self::is_system(n) || Self::is_vbmeta(n))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
Self(partitions)
|
||||
}
|
||||
|
||||
pub fn is_boot(name: &str) -> bool {
|
||||
name == "boot" || name == "init_boot" || name == "recovery" || name == "vendor_boot"
|
||||
}
|
||||
|
||||
pub fn is_system(name: &str) -> bool {
|
||||
name == "system"
|
||||
}
|
||||
|
||||
pub fn is_vbmeta(name: &str) -> bool {
|
||||
name.starts_with("vbmeta")
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &str> {
|
||||
self.0.iter().map(|n| n.as_str())
|
||||
}
|
||||
|
||||
pub fn iter_boot(&self) -> impl Iterator<Item = &str> {
|
||||
self.iter().filter(|n| Self::is_boot(n))
|
||||
}
|
||||
|
||||
pub fn iter_system(&self) -> impl Iterator<Item = &str> {
|
||||
self.iter().filter(|n| Self::is_system(n))
|
||||
}
|
||||
|
||||
pub fn iter_vbmeta(&self) -> impl Iterator<Item = &str> {
|
||||
self.iter().filter(|n| Self::is_vbmeta(n))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum InputFileState {
|
||||
External,
|
||||
@@ -134,7 +131,7 @@ struct InputFile {
|
||||
/// operating system).
|
||||
fn open_input_files(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
required_images: &RequiredImages,
|
||||
required_images: &HashMap<String, PartitionFlags>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
header: &PayloadHeader,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -144,8 +141,8 @@ fn open_input_files(
|
||||
// We always include replacement images that the user specifies, even if
|
||||
// they don't need to be patched.
|
||||
let all_images = required_images
|
||||
.iter()
|
||||
.chain(external_images.keys().map(|k| k.as_str()))
|
||||
.keys()
|
||||
.chain(external_images.keys())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for name in all_images {
|
||||
@@ -158,7 +155,7 @@ fn open_input_files(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to open external image: {path:?}"))?;
|
||||
input_files.insert(
|
||||
name.to_owned(),
|
||||
name.clone(),
|
||||
InputFile {
|
||||
file,
|
||||
state: InputFileState::External,
|
||||
@@ -174,7 +171,7 @@ fn open_input_files(
|
||||
payload::extract_image(payload, &file, header, name, cancel_signal)
|
||||
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
|
||||
input_files.insert(
|
||||
name.to_owned(),
|
||||
name.clone(),
|
||||
InputFile {
|
||||
file,
|
||||
state: InputFileState::Extracted,
|
||||
@@ -190,19 +187,23 @@ fn open_input_files(
|
||||
/// necessarily patched. Each patcher will determine which image it should
|
||||
/// target. If the original image is signed, then it will be re-signed with
|
||||
/// `key_avb`.
|
||||
fn patch_boot_images<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
fn patch_boot_images(
|
||||
required_images: &HashMap<String, PartitionFlags>,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let input_files = Mutex::new(input_files);
|
||||
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
|
||||
let boot_partitions = required_images
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.contains(PartitionFlags::BOOT))
|
||||
.map(|(name, _)| name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
info!(
|
||||
"Candidate boot images: {}",
|
||||
joined(sorted(boot_partitions.iter())),
|
||||
util::join(util::sort(boot_partitions.iter()), ", "),
|
||||
);
|
||||
|
||||
boot::patch_boot_images(
|
||||
@@ -225,7 +226,7 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to patch boot images: {}",
|
||||
joined(sorted(boot_partitions.iter())),
|
||||
util::join(util::sort(boot_partitions.iter()), ", "),
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -234,16 +235,23 @@ fn patch_boot_images<'a, 'b: 'a>(
|
||||
|
||||
/// Patch the single system image listed in `required_images` to replace the
|
||||
/// `otacerts.zip` contents.
|
||||
fn patch_system_image<'a, 'b: 'a>(
|
||||
required_images: &'b RequiredImages,
|
||||
fn patch_system_image<'a>(
|
||||
required_images: &'a HashMap<String, PartitionFlags>,
|
||||
input_files: &mut HashMap<String, InputFile>,
|
||||
cert_ota: &Certificate,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(&'b str, Vec<Range<u64>>)> {
|
||||
let Some(target) = required_images.iter_system().next() else {
|
||||
) -> Result<(&'a str, Vec<Range<u64>>)> {
|
||||
let mut system_iter = required_images
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.contains(PartitionFlags::SYSTEM))
|
||||
.map(|(name, _)| name);
|
||||
let Some(target) = system_iter.next() else {
|
||||
bail!("No system partition found");
|
||||
};
|
||||
if system_iter.next().is_some() {
|
||||
bail!("Multiple system partitions found");
|
||||
}
|
||||
|
||||
let _span = debug_span!("image", name = target).entered();
|
||||
|
||||
@@ -309,12 +317,13 @@ fn load_vbmeta_images(
|
||||
/// Check that all critical partitions within the payload are protected by a
|
||||
/// vbmeta image in `vbmeta_headers`.
|
||||
fn ensure_partitions_protected(
|
||||
required_images: &RequiredImages,
|
||||
required_images: &HashMap<String, PartitionFlags>,
|
||||
vbmeta_headers: &HashMap<String, Header>,
|
||||
) -> Result<()> {
|
||||
let critical_partitions = required_images
|
||||
.iter_boot()
|
||||
.chain(required_images.iter_vbmeta())
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.intersects(PartitionFlags::BOOT | PartitionFlags::VBMETA))
|
||||
.map(|(name, _)| name.as_str())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// vbmeta partitions first.
|
||||
@@ -337,7 +346,7 @@ fn ensure_partitions_protected(
|
||||
if !missing.is_empty() {
|
||||
bail!(
|
||||
"Found critical partitions that are not protected by AVB: {}",
|
||||
joined(missing),
|
||||
util::join(missing, ", "),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -380,7 +389,10 @@ fn get_vbmeta_patch_order(
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
warn!("Partitions aren't protected by AVB: {}", joined(missing));
|
||||
warn!(
|
||||
"Partitions aren't protected by AVB: {}",
|
||||
util::join(missing, ", "),
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure that there's only a single root of trust. Otherwise, there could
|
||||
@@ -396,7 +408,10 @@ fn get_vbmeta_patch_order(
|
||||
|
||||
// For zero roots, let TopologicalSort report the cycle.
|
||||
if roots.len() > 1 {
|
||||
bail!("Found multiple root vbmeta images: {}", joined(roots));
|
||||
bail!(
|
||||
"Found multiple root vbmeta images: {}",
|
||||
util::join(roots, ", "),
|
||||
);
|
||||
}
|
||||
|
||||
// Compute the patching order. This only includes vbmeta images. All vbmeta
|
||||
@@ -464,7 +479,9 @@ fn update_security_descriptors(
|
||||
*pd = cd.clone();
|
||||
}
|
||||
_ => {
|
||||
bail!("{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})");
|
||||
bail!(
|
||||
"{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})"
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -474,7 +491,9 @@ fn update_security_descriptors(
|
||||
child_header.public_key.clone_into(&mut pd.public_key);
|
||||
}
|
||||
_ => {
|
||||
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
|
||||
bail!(
|
||||
"{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,6 +565,74 @@ fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the VABC parameters from the payload header. This will fail if an
|
||||
/// unsupported VABC algorithm or CoW version is specified, but not if VABC is
|
||||
/// disabled.
|
||||
fn get_vabc_params(header: &PayloadHeader) -> Result<Option<VabcParams>> {
|
||||
// Only CoW v2 seems to exist in the wild currently, so that is all we
|
||||
// support.
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if !dpm.vabc_enabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let cow_version = match dpm.cow_version() {
|
||||
2 => CowVersion::V2,
|
||||
3 => CowVersion::V3,
|
||||
v => bail!("Unsupported CoW version: {v}"),
|
||||
};
|
||||
|
||||
let compression = dpm.vabc_compression_param();
|
||||
let Ok(vabc_algo) = VabcAlgo::from_str(compression) else {
|
||||
bail!("Unsupported VABC compression: {compression}");
|
||||
};
|
||||
|
||||
// This is unused by v2, but delta_generator sets it anyway.
|
||||
let Some(compression_factor) = dpm.compression_factor else {
|
||||
bail!("No CoW compression factor specified");
|
||||
};
|
||||
let Ok(compression_factor) = u32::try_from(compression_factor) else {
|
||||
bail!("CoW compression factor is too large: {compression_factor}");
|
||||
};
|
||||
|
||||
let vabc_params = VabcParams {
|
||||
version: cow_version,
|
||||
algo: vabc_algo,
|
||||
compression_factor,
|
||||
};
|
||||
|
||||
Ok(Some(vabc_params))
|
||||
}
|
||||
|
||||
/// Set the VABC algorithm in the payload header and return whether it was
|
||||
/// changed. This will fail if VABC was originally disabled. Returns whether the
|
||||
/// new algorithm is different from the old algorithm.
|
||||
fn set_vabc_algo(header: &mut PayloadHeader, vabc_algo: VabcAlgo) -> Result<bool> {
|
||||
let Some(dpm) = &mut header.manifest.dynamic_partition_metadata else {
|
||||
bail!("Dynamic partition metadata is missing");
|
||||
};
|
||||
|
||||
if !dpm.vabc_enabled() {
|
||||
bail!("Cannot change VABC algorithm when VABC is disabled");
|
||||
}
|
||||
|
||||
let compression = dpm.vabc_compression_param();
|
||||
let Ok(old_vabc_algo) = VabcAlgo::from_str(compression) else {
|
||||
bail!("Unsupported VABC compression: {compression}");
|
||||
};
|
||||
|
||||
if vabc_algo == old_vabc_algo {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
dpm.vabc_compression_param = Some(vabc_algo.to_string());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Update vbmeta headers.
|
||||
///
|
||||
/// * If [`Header::flags`] is non-zero, then an error is returned because the
|
||||
@@ -566,6 +653,11 @@ fn update_vbmeta_headers(
|
||||
key: &RsaSigningKey,
|
||||
block_size: u64,
|
||||
) -> Result<()> {
|
||||
info!(
|
||||
"Patching vbmeta images: {}",
|
||||
util::join(order.iter().map(|(n, _)| n), ", "),
|
||||
);
|
||||
|
||||
for (name, deps) in order {
|
||||
let parent_header = headers.get_mut(name).unwrap();
|
||||
let orig_parent_header = parent_header.clone();
|
||||
@@ -644,6 +736,7 @@ pub fn compress_image(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let vabc_params = get_vabc_params(header)?;
|
||||
let block_size = header.manifest.block_size();
|
||||
let partition = header
|
||||
.manifest
|
||||
@@ -652,6 +745,23 @@ pub fn compress_image(
|
||||
.find(|p| p.partition_name == name)
|
||||
.unwrap();
|
||||
|
||||
// If VABC is enabled, we need to update the CoW size estimate or else the
|
||||
// CoW block device may run out of space during flashing.
|
||||
let vabc_params = if partition.estimate_cow_size.is_some() {
|
||||
let Some(vabc_params) = vabc_params else {
|
||||
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
|
||||
};
|
||||
|
||||
info!(
|
||||
"Needs updated {} CoW size estimate: {name}",
|
||||
vabc_params.algo,
|
||||
);
|
||||
|
||||
Some(vabc_params)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(r) = ranges {
|
||||
info!("Compressing partial image: {name}: {r:?}");
|
||||
|
||||
@@ -665,7 +775,26 @@ pub fn compress_image(
|
||||
cancel_signal,
|
||||
) {
|
||||
Ok(indices) => {
|
||||
// The changes we make usually aren't any less compressible, but
|
||||
// we'll still recompute the CoW size estimate to handle the
|
||||
// case where the user requested a different algorithm.
|
||||
if let Some(vabc_params) = vabc_params {
|
||||
let cow_estimate = payload::compute_cow_estimate(
|
||||
&*file,
|
||||
partition.operations.len() as u64,
|
||||
name,
|
||||
block_size,
|
||||
vabc_params,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
partition.estimate_cow_size = Some(cow_estimate.size);
|
||||
partition.estimate_op_count_max =
|
||||
(vabc_params.version == CowVersion::V3).then_some(cow_estimate.num_ops);
|
||||
}
|
||||
|
||||
*file = writer;
|
||||
|
||||
return Ok(indices);
|
||||
}
|
||||
// If we can't take advantage of the optimization, we can still
|
||||
@@ -679,43 +808,20 @@ pub fn compress_image(
|
||||
|
||||
info!("Compressing full image: {name}");
|
||||
|
||||
// Otherwise, compress the entire image. If VABC is enabled, we need to
|
||||
// update the CoW size estimate or else the CoW block device may run out of
|
||||
// space during flashing.
|
||||
let vabc_algo = if partition.estimate_cow_size.is_some() {
|
||||
// Only CoW v2 seems to exist in the wild currently, so that is all we
|
||||
// support.
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
bail!("Dynamic partition metadata is missing");
|
||||
};
|
||||
|
||||
if !dpm.vabc_enabled() {
|
||||
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
|
||||
}
|
||||
|
||||
let cow_version = dpm.cow_version();
|
||||
if dpm.cow_version() != 2 {
|
||||
bail!("Unsupported CoW version: {cow_version}");
|
||||
}
|
||||
|
||||
let compression = dpm.vabc_compression_param();
|
||||
let Some(vabc_algo) = VabcAlgo::new(compression) else {
|
||||
bail!("Unsupported VABC compression: {compression}");
|
||||
};
|
||||
|
||||
info!("Needs updated {vabc_algo} CoW size estimate: {name}");
|
||||
|
||||
Some(vabc_algo)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (partition_info, operations, cow_estimate) =
|
||||
payload::compress_image(&*file, &writer, name, block_size, vabc_algo, cancel_signal)?;
|
||||
let (partition_info, operations, cow_estimate) = payload::compress_image(
|
||||
&*file,
|
||||
&writer,
|
||||
name,
|
||||
block_size,
|
||||
vabc_params,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
partition.new_partition_info = Some(partition_info);
|
||||
partition.operations = operations;
|
||||
partition.estimate_cow_size = cow_estimate;
|
||||
partition.estimate_cow_size = cow_estimate.map(|e| e.size);
|
||||
let is_v3 = vabc_params.is_some_and(|p| p.version == CowVersion::V3);
|
||||
partition.estimate_op_count_max = cow_estimate.and_then(|e| is_v3.then_some(e.num_ops));
|
||||
|
||||
*file = writer;
|
||||
|
||||
@@ -723,6 +829,53 @@ pub fn compress_image(
|
||||
Ok(vec![0..partition.operations.len()])
|
||||
}
|
||||
|
||||
/// Recompute the CoW estimate for an image and update the OTA manifest
|
||||
/// partition entry appropriately. The input file is not modified.
|
||||
fn recow_image(
|
||||
name: &str,
|
||||
file: &mut PSeekFile,
|
||||
header: &mut PayloadHeader,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let _span = debug_span!("image", name).entered();
|
||||
|
||||
file.rewind()?;
|
||||
|
||||
let vabc_params = get_vabc_params(header)?;
|
||||
let block_size = header.manifest.block_size();
|
||||
let partition = header
|
||||
.manifest
|
||||
.partitions
|
||||
.iter_mut()
|
||||
.find(|p| p.partition_name == name)
|
||||
.unwrap();
|
||||
|
||||
if partition.estimate_cow_size.is_none() {
|
||||
bail!("Partition has no original CoW estimate: {name}");
|
||||
}
|
||||
|
||||
let Some(vabc_params) = vabc_params else {
|
||||
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
|
||||
};
|
||||
|
||||
info!("Recomputing {} CoW size estimate: {name}", vabc_params.algo);
|
||||
|
||||
let cow_estimate = payload::compute_cow_estimate(
|
||||
&*file,
|
||||
partition.operations.len() as u64,
|
||||
name,
|
||||
block_size,
|
||||
vabc_params,
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
partition.estimate_cow_size = Some(cow_estimate.size);
|
||||
partition.estimate_op_count_max =
|
||||
(vabc_params.version == CowVersion::V3).then_some(cow_estimate.num_ops);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn patch_ota_payload(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
@@ -731,6 +884,7 @@ fn patch_ota_payload(
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
skip_system_ota_cert: bool,
|
||||
clear_vbmeta_flags: bool,
|
||||
vabc_algo_override: Option<VabcAlgo>,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
@@ -742,6 +896,16 @@ fn patch_ota_payload(
|
||||
bail!("Payload is a delta OTA, not a full OTA");
|
||||
}
|
||||
|
||||
let mut required_flags = RequiredFlags::empty();
|
||||
if !skip_system_ota_cert {
|
||||
required_flags |= RequiredFlags::SYSTEM;
|
||||
}
|
||||
if let Some(vabc_algo) = vabc_algo_override {
|
||||
if set_vabc_algo(&mut header, vabc_algo)? {
|
||||
required_flags |= RequiredFlags::ALL_COW;
|
||||
}
|
||||
}
|
||||
|
||||
let all_partitions = header
|
||||
.manifest
|
||||
.partitions
|
||||
@@ -757,11 +921,17 @@ fn patch_ota_payload(
|
||||
}
|
||||
}
|
||||
|
||||
// Determine what images need to be patched. For simplicity, we pre-read all
|
||||
// vbmeta images since they're tiny. They're discarded later if the they
|
||||
// don't need to be modified.
|
||||
let required_images = RequiredImages::new(&header.manifest);
|
||||
let vbmeta_images = required_images.iter_vbmeta().collect::<HashSet<_>>();
|
||||
let required_images = get_required_images(&header.manifest, required_flags);
|
||||
let vbmeta_images = required_images
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.contains(PartitionFlags::VBMETA))
|
||||
.map(|(name, _)| name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let cow_images = required_images
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.contains(PartitionFlags::COW))
|
||||
.map(|(name, _)| name.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
// The set of source images to be inserted into the new payload, replacing
|
||||
// what was in the original payload. Initially, this refers to either user
|
||||
@@ -784,9 +954,6 @@ fn patch_ota_payload(
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
input_files
|
||||
.retain(|n, f| !(f.state == InputFileState::Extracted && RequiredImages::is_boot(n)));
|
||||
|
||||
let system_result = if skip_system_ota_cert {
|
||||
None
|
||||
} else {
|
||||
@@ -799,20 +966,12 @@ fn patch_ota_payload(
|
||||
)?)
|
||||
};
|
||||
|
||||
input_files
|
||||
.retain(|n, f| !(f.state == InputFileState::Extracted && RequiredImages::is_system(n)));
|
||||
|
||||
let mut vbmeta_headers = load_vbmeta_images(&mut input_files, &vbmeta_images)?;
|
||||
|
||||
ensure_partitions_protected(&required_images, &vbmeta_headers)?;
|
||||
|
||||
let mut vbmeta_order = get_vbmeta_patch_order(&input_files, &vbmeta_headers)?;
|
||||
|
||||
info!(
|
||||
"Patching vbmeta images: {}",
|
||||
joined(vbmeta_order.iter().map(|(n, _)| n)),
|
||||
);
|
||||
|
||||
update_vbmeta_headers(
|
||||
&mut input_files,
|
||||
&mut vbmeta_headers,
|
||||
@@ -822,7 +981,19 @@ fn patch_ota_payload(
|
||||
header.manifest.block_size().into(),
|
||||
)?;
|
||||
|
||||
// Unmodified vbmeta images no longer need to be kept around either.
|
||||
// Recompute CoW estimates for partitions we don't modify.
|
||||
input_files
|
||||
.iter_mut()
|
||||
.filter(|(name, f)| {
|
||||
f.state == InputFileState::Extracted && cow_images.contains(name.as_str())
|
||||
})
|
||||
.try_for_each(|(name, input_file)| {
|
||||
recow_image(name, &mut input_file.file, &mut header, cancel_signal)
|
||||
})?;
|
||||
|
||||
// Drop all unmodified images. We only want to compress modified images.
|
||||
// For recowed images, the payload header was already updated with the new
|
||||
// estimate. The actual data can be copied from the original payload.
|
||||
input_files.retain(|_, f| f.state != InputFileState::Extracted);
|
||||
|
||||
let mut compressed_files = input_files
|
||||
@@ -928,11 +1099,12 @@ fn patch_ota_payload(
|
||||
fn patch_ota_zip(
|
||||
raw_reader: &PSeekFile,
|
||||
zip_reader: &mut ZipArchive<impl Read + Seek>,
|
||||
mut zip_writer: &mut ZipWriter<impl Write>,
|
||||
mut zip_writer: &mut ZipWriterWrapper<impl Write>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
|
||||
skip_system_ota_cert: bool,
|
||||
clear_vbmeta_flags: bool,
|
||||
vabc_algo_override: Option<VabcAlgo>,
|
||||
zip_mode: ZipMode,
|
||||
key_avb: &RsaSigningKey,
|
||||
key_ota: &RsaSigningKey,
|
||||
@@ -953,7 +1125,7 @@ fn patch_ota_zip(
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
bail!("Missing entries in OTA zip: {}", joined(missing));
|
||||
bail!("Missing entries in OTA zip: {}", util::join(missing, ", "));
|
||||
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
|
||||
bail!(
|
||||
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
|
||||
@@ -981,7 +1153,8 @@ fn patch_ota_zip(
|
||||
// threshold. This should be sufficient since the output file is likely
|
||||
// to be larger.
|
||||
let use_zip64 = reader.size() >= 0xffffffff;
|
||||
let options = FileOptions::default()
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.large_file(use_zip64);
|
||||
|
||||
@@ -1018,12 +1191,9 @@ fn patch_ota_zip(
|
||||
}
|
||||
|
||||
// All remaining entries are written immediately.
|
||||
zip_writer
|
||||
.start_file_with_extra_data(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let offset = zip_writer
|
||||
.end_extra_data()
|
||||
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
|
||||
.start_file(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let mut writer = CountingWriter::new(&mut zip_writer);
|
||||
|
||||
match path.as_str() {
|
||||
@@ -1056,6 +1226,7 @@ fn patch_ota_zip(
|
||||
boot_patchers,
|
||||
skip_system_ota_cert,
|
||||
clear_vbmeta_flags,
|
||||
vabc_algo_override,
|
||||
key_avb,
|
||||
key_ota,
|
||||
cert_ota,
|
||||
@@ -1086,7 +1257,7 @@ fn patch_ota_zip(
|
||||
let size = writer.stream_position()?;
|
||||
|
||||
entries.push(ZipEntry {
|
||||
name: path.clone(),
|
||||
path: path.clone(),
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
@@ -1135,7 +1306,7 @@ pub fn extract_payload(
|
||||
}
|
||||
}
|
||||
|
||||
info!("Extracting from the payload: {}", joined(images));
|
||||
info!("Extracting from the payload: {}", util::join(images, ", "));
|
||||
|
||||
// Pre-open all output files.
|
||||
let output_files = images
|
||||
@@ -1201,7 +1372,7 @@ fn verify_partition_hashes(
|
||||
|
||||
let mut writer = HashingWriter::new(
|
||||
io::sink(),
|
||||
aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256),
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
);
|
||||
|
||||
stream::copy(file, &mut writer, cancel_signal)?;
|
||||
@@ -1321,7 +1492,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
)));
|
||||
} else {
|
||||
assert!(cli.root.rootless);
|
||||
};
|
||||
}
|
||||
|
||||
if cli.skip_system_ota_cert {
|
||||
warn!("Not inserting OTA cert into system image; sideloading further updates may fail");
|
||||
@@ -1355,11 +1526,11 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
let mut zip_writer = match cli.zip_mode {
|
||||
ZipMode::Streaming => {
|
||||
let signing_writer = SigningWriter::new_streaming(temp_writer);
|
||||
ZipWriter::new_streaming(signing_writer)
|
||||
ZipWriterWrapper::new_streaming(signing_writer)
|
||||
}
|
||||
ZipMode::Seekable => {
|
||||
let signing_writer = SigningWriter::new_seekable(temp_writer);
|
||||
ZipWriter::new(signing_writer)
|
||||
ZipWriterWrapper::new_seekable(signing_writer)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1371,6 +1542,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
&boot_patchers,
|
||||
cli.skip_system_ota_cert,
|
||||
cli.clear_vbmeta_flags,
|
||||
cli.vabc_algo,
|
||||
cli.zip_mode,
|
||||
&key_avb,
|
||||
&key_ota,
|
||||
@@ -1487,18 +1659,17 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !missing_images.is_empty() {
|
||||
bail!("Invalid partitions: {}", joined(missing_images));
|
||||
bail!("Invalid partitions: {}", util::join(missing_images, ", "));
|
||||
}
|
||||
|
||||
unique_images.extend(cli.extract.partition.iter().cloned());
|
||||
} else if !cli.extract.none {
|
||||
let images = RequiredImages::new(&header.manifest);
|
||||
let images = get_required_images(&header.manifest, RequiredFlags::SYSTEM)
|
||||
.into_iter()
|
||||
.filter(|(_, flags)| !cli.extract.boot_only || flags.contains(PartitionFlags::BOOT))
|
||||
.map(|(name, _)| name);
|
||||
|
||||
if cli.extract.boot_only {
|
||||
unique_images.extend(images.iter_boot().map(|n| n.to_owned()));
|
||||
} else {
|
||||
unique_images.extend(images.iter().map(|n| n.to_owned()));
|
||||
}
|
||||
unique_images.extend(images);
|
||||
}
|
||||
|
||||
if let Some(path) = &cli.cert_ota {
|
||||
@@ -1748,7 +1919,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
.with_context(|| format!("Failed to parse property files: {}", ota::PF_NAME))?;
|
||||
let pf_payload = pfs
|
||||
.iter()
|
||||
.find(|pf| pf.name == ota::PATH_PAYLOAD)
|
||||
.find(|pf| pf.name() == ota::PATH_PAYLOAD)
|
||||
.ok_or_else(|| anyhow!("Missing property files entry: {}", ota::PATH_PAYLOAD))?;
|
||||
|
||||
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)
|
||||
@@ -1825,16 +1996,20 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
|
||||
info!("Checking recovery ramdisk's otacerts.zip");
|
||||
|
||||
let required_images = RequiredImages::new(&header.manifest);
|
||||
let boot_images =
|
||||
boot::load_boot_images(&required_images.iter_boot().collect::<Vec<_>>(), |name| {
|
||||
Ok(Box::new(
|
||||
temp_dir
|
||||
.open(format!("{name}.img"))
|
||||
.map(|f| PSeekFile::new(f.into_std()))?,
|
||||
))
|
||||
})
|
||||
.context("Failed to load all boot images")?;
|
||||
let required_images = get_required_images(&header.manifest, RequiredFlags::empty());
|
||||
let boot_image_names = required_images
|
||||
.iter()
|
||||
.filter(|(_, flags)| flags.contains(PartitionFlags::BOOT))
|
||||
.map(|(name, _)| name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let boot_images = boot::load_boot_images(&boot_image_names, |name| {
|
||||
Ok(Box::new(
|
||||
temp_dir
|
||||
.open(format!("{name}.img"))
|
||||
.map(|f| PSeekFile::new(f.into_std()))?,
|
||||
))
|
||||
})
|
||||
.context("Failed to load all boot images")?;
|
||||
let targets = OtaCertPatcher::new(ota_cert.clone())
|
||||
.find_targets(&boot_images, cancel_signal)
|
||||
.context("Failed to find boot image containing otacerts.zip")?;
|
||||
@@ -2080,6 +2255,18 @@ pub struct PatchCli {
|
||||
#[arg(long, help_heading = HEADING_OTHER)]
|
||||
pub clear_vbmeta_flags: bool,
|
||||
|
||||
/// Override the virtual A/B CoW compression algorithm.
|
||||
///
|
||||
/// This will slow down the patching process because every dynamic partition
|
||||
/// needs to be extracted to recompute the CoW size estimate. However, if a
|
||||
/// faster algorithm is chosen, then OTA installation using an OTA updater
|
||||
/// app will be faster. This does not affect sideloading from recovery mode.
|
||||
///
|
||||
/// Note that selecting a newer algorithm will prevent upgrading from older
|
||||
/// Android versions before support for the algorithm was introduced.
|
||||
#[arg(long, value_name = "ALGO", help_heading = HEADING_OTHER)]
|
||||
pub vabc_algo: Option<VabcAlgo>,
|
||||
|
||||
/// Zip creation mode for the output OTA zip.
|
||||
///
|
||||
/// The streaming mode produces zip files that contain data descriptors.
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use cap_std::{ambient_authority, fs::Dir};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use tracing::info;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -10,10 +10,10 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use crc32fast::Hasher;
|
||||
use zerocopy::{little_endian, IntoBytes};
|
||||
use zerocopy::{IntoBytes, little_endian};
|
||||
|
||||
use crate::{
|
||||
format::{
|
||||
@@ -317,11 +317,11 @@ fn unpack_subcommand(
|
||||
})?;
|
||||
}
|
||||
ChunkData::Hole => {
|
||||
// This cannot overflow.
|
||||
let to_skip = chunk.bounds.len() * metadata.header.block_size;
|
||||
// Unlike ChunkData::Data, this can overflow a u32.
|
||||
let to_skip = i64::from(chunk.bounds.len()) * i64::from(metadata.header.block_size);
|
||||
|
||||
writer
|
||||
.seek(SeekFrom::Current(to_skip.into()))
|
||||
.seek(SeekFrom::Current(to_skip))
|
||||
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
|
||||
}
|
||||
ChunkData::Crc32(_) => {}
|
||||
|
||||
@@ -21,25 +21,25 @@ use cms::{
|
||||
};
|
||||
use passterm::PromptError;
|
||||
use pkcs8::{
|
||||
pkcs5::{pbes2, scrypt},
|
||||
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
|
||||
LineEnding, PrivateKeyInfo,
|
||||
pkcs5::{pbes2, scrypt},
|
||||
};
|
||||
use rand::RngCore;
|
||||
use rsa::{
|
||||
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
|
||||
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, traits::PublicKeyParts,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256, Sha512};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{
|
||||
Certificate,
|
||||
builder::{Builder, CertificateBuilder, Profile},
|
||||
der::{pem::PemLabel, referenced::OwnedToRef, Any, Decode, DecodePem, EncodePem},
|
||||
der::{Any, Decode, DecodePem, EncodePem, pem::PemLabel, referenced::OwnedToRef},
|
||||
serial_number::SerialNumber,
|
||||
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
|
||||
time::Validity,
|
||||
Certificate,
|
||||
};
|
||||
|
||||
use crate::util::DebugString;
|
||||
@@ -144,6 +144,7 @@ pub enum PassphraseSource {
|
||||
|
||||
impl PassphraseSource {
|
||||
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
if let Some(v) = env_var {
|
||||
Self::EnvVar(v.to_owned())
|
||||
} else if let Some(p) = pass_file {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use std::{fmt, marker::PhantomData};
|
||||
|
||||
use bstr::{ByteSlice, ByteVec};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use serde::{Deserializer, Serializer, de::Visitor};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
|
||||
+14
-15
@@ -10,14 +10,14 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Algorithm, Context};
|
||||
use bstr::ByteSlice;
|
||||
use num_bigint_dig::{ModInverse, ToBigInt};
|
||||
use num_traits::{Pow, ToPrimitive};
|
||||
use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use rsa::{BigUint, RsaPublicKey, traits::PublicKeyParts};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{big_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, big_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
@@ -55,7 +55,7 @@ pub const HEADER_MAX_SIZE: u64 = 64 * 1024;
|
||||
///
|
||||
/// ```rust
|
||||
/// use avbroot::format::hashtree::HashTree;
|
||||
/// let size = HashTree::new(4096, &aws_lc_rs::digest::SHA256, b"")
|
||||
/// let size = HashTree::new(4096, &ring::digest::SHA256, b"")
|
||||
/// .compute_level_offsets(8 * 1024 * 1024 * 1024)
|
||||
/// .unwrap()
|
||||
/// .first()
|
||||
@@ -163,11 +163,11 @@ pub enum Error {
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub(crate) fn digest_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
|
||||
pub(crate) fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
match name {
|
||||
"sha1" if for_verify => Ok(&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY),
|
||||
"sha256" => Ok(&aws_lc_rs::digest::SHA256),
|
||||
"sha512" => Ok(&aws_lc_rs::digest::SHA512),
|
||||
"sha1" => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
|
||||
"sha256" => Ok(&ring::digest::SHA256),
|
||||
"sha512" => Ok(&ring::digest::SHA512),
|
||||
a => Err(Error::UnsupportedHashAlgorithm(a.to_owned())),
|
||||
}
|
||||
}
|
||||
@@ -534,7 +534,7 @@ impl HashTreeDescriptor {
|
||||
ranges: Option<&[Range<u64>]>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, false)?;
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm)?;
|
||||
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
|
||||
let (root_digest, hash_tree_data) = match ranges {
|
||||
Some(r) => {
|
||||
@@ -642,7 +642,7 @@ impl HashTreeDescriptor {
|
||||
) -> Result<()> {
|
||||
self.check_offsets()?;
|
||||
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, true)?;
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm)?;
|
||||
|
||||
util::check_bounds(self.tree_size, ..=HASH_TREE_MAX_SIZE)
|
||||
.map_err(|e| Error::IntOutOfBounds("HashTree::tree_size", e))?;
|
||||
@@ -903,10 +903,9 @@ impl HashDescriptor {
|
||||
fn calculate(
|
||||
&self,
|
||||
reader: impl Read,
|
||||
for_verify: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<aws_lc_rs::digest::Digest> {
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm, for_verify)?;
|
||||
) -> Result<ring::digest::Digest> {
|
||||
let algorithm = digest_algorithm(&self.hash_algorithm)?;
|
||||
let mut context = Context::new(algorithm);
|
||||
context.update(&self.salt);
|
||||
|
||||
@@ -924,14 +923,14 @@ impl HashDescriptor {
|
||||
|
||||
/// Update the root hash from the input reader's contents.
|
||||
pub fn update(&mut self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest = self.calculate(reader, false, cancel_signal)?;
|
||||
let digest = self.calculate(reader, cancel_signal)?;
|
||||
self.root_digest = digest.as_ref().to_vec();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Verify the root hash against the input reader.
|
||||
pub fn verify(&self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let digest = self.calculate(reader, true, cancel_signal)?;
|
||||
let digest = self.calculate(reader, cancel_signal)?;
|
||||
|
||||
if self.root_digest != digest.as_ref() {
|
||||
return Err(Error::InvalidRootDigest {
|
||||
|
||||
@@ -8,12 +8,12 @@ use std::{
|
||||
str::{self, Utf8Error},
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use bstr::ByteSlice;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
@@ -269,6 +269,7 @@ impl fmt::Display for BootImageV0Through2 {
|
||||
|
||||
impl BootImageExt for BootImageV0Through2 {
|
||||
fn header_version(&self) -> u32 {
|
||||
#[allow(clippy::bool_to_int_with_if)]
|
||||
if self.v2_extra.is_some() {
|
||||
2
|
||||
} else if self.v1_extra.is_some() {
|
||||
@@ -668,11 +669,7 @@ impl fmt::Display for BootImageV3Through4 {
|
||||
|
||||
impl BootImageExt for BootImageV3Through4 {
|
||||
fn header_version(&self) -> u32 {
|
||||
if self.v4_extra.is_some() {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
}
|
||||
if self.v4_extra.is_some() { 4 } else { 3 }
|
||||
}
|
||||
|
||||
fn header_size(&self) -> u32 {
|
||||
@@ -890,7 +887,7 @@ impl BootImageV3Through4 {
|
||||
/// image was successfully signed. Returns false if there's no vbmeta
|
||||
/// structure to sign in [`V4Extra::signature`].
|
||||
pub fn sign(&mut self, key: &RsaSigningKey) -> Result<bool> {
|
||||
let mut context = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
let image_size;
|
||||
|
||||
if let Some(v4) = &self.v4_extra {
|
||||
@@ -1093,11 +1090,7 @@ impl fmt::Display for VendorBootImageV3Through4 {
|
||||
|
||||
impl BootImageExt for VendorBootImageV3Through4 {
|
||||
fn header_version(&self) -> u32 {
|
||||
if self.v4_extra.is_some() {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
}
|
||||
if self.v4_extra.is_some() { 4 } else { 3 }
|
||||
}
|
||||
|
||||
fn header_size(&self) -> u32 {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use std::io::{self, Read, Seek, Write};
|
||||
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
|
||||
use liblzma::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
|
||||
@@ -771,11 +771,7 @@ pub fn sort(entries: &mut [CpioEntry]) {
|
||||
/// 300000.
|
||||
pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()> {
|
||||
fn next_non_zero(i: u32) -> u32 {
|
||||
if i == u32::MAX {
|
||||
1
|
||||
} else {
|
||||
i.wrapping_add(1)
|
||||
}
|
||||
if i == u32::MAX { 1 } else { i.wrapping_add(1) }
|
||||
}
|
||||
|
||||
// (dev maj, dev min) -> (inode set, last assigned inode)
|
||||
|
||||
@@ -16,7 +16,7 @@ use rayon::{
|
||||
slice::{ParallelSlice, ParallelSliceMut},
|
||||
};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
@@ -707,7 +707,7 @@ impl FecImage {
|
||||
let fec_size: u32 =
|
||||
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
|
||||
|
||||
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &self.fec);
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
|
||||
|
||||
let header = RawHeader {
|
||||
magic: FEC_MAGIC.into(),
|
||||
@@ -781,7 +781,7 @@ impl<R: Read> FromReader<R> for FecImage {
|
||||
|
||||
let data_size = header.data_size.get();
|
||||
|
||||
let actual_digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &fec[..fec_size]);
|
||||
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
|
||||
if header.digest != actual_digest.as_ref() {
|
||||
return Err(Error::InvalidFecDigest {
|
||||
expected: hex::encode(header.digest),
|
||||
@@ -827,7 +827,7 @@ impl<W: Write> ToWriter<W> for FecImage {
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{Cursor, Seek},
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
@@ -886,7 +886,7 @@ mod tests {
|
||||
let mut buf = vec![0u8; size];
|
||||
rand::thread_rng().fill_bytes(&mut buf);
|
||||
file.write_all(&buf).unwrap();
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &buf)
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
|
||||
let fec = Fec::new(size as u64, block_size, parity).unwrap();
|
||||
@@ -921,7 +921,7 @@ mod tests {
|
||||
let mut buf = Vec::new();
|
||||
file.rewind().unwrap();
|
||||
file.read_to_end(&mut buf).unwrap();
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &buf)
|
||||
ring::digest::digest(&ring::digest::SHA256, &buf)
|
||||
};
|
||||
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Algorithm, Context};
|
||||
use bstr::ByteSlice;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, ParallelIterator},
|
||||
slice::ParallelSliceMut,
|
||||
};
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
@@ -416,8 +416,8 @@ impl HashTree {
|
||||
if hash_tree_data != actual_hash_tree_data {
|
||||
// These are multiple megabytes, so only report the hashes.
|
||||
let algorithm = self.salted_context.algorithm();
|
||||
let expected = aws_lc_rs::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = aws_lc_rs::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
let expected = ring::digest::digest(algorithm, hash_tree_data);
|
||||
let actual = ring::digest::digest(algorithm, &actual_hash_tree_data);
|
||||
|
||||
return Err(Error::InvalidHashTree {
|
||||
expected: hex::encode(expected),
|
||||
@@ -493,7 +493,7 @@ impl HashTreeImage {
|
||||
const VERSION: u16 = 1;
|
||||
|
||||
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
|
||||
avb::digest_algorithm(name, false)
|
||||
avb::digest_algorithm(name)
|
||||
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
|
||||
}
|
||||
|
||||
@@ -662,7 +662,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn calculate_level_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &aws_lc_rs::digest::SHA256, &[]);
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
|
||||
assert_eq!(
|
||||
hash_tree.compute_level_offsets(0).unwrap(),
|
||||
&[] as &[Range<usize>],
|
||||
@@ -675,7 +675,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn blocks_for_ranges() {
|
||||
let hash_tree = HashTree::new(4096, &aws_lc_rs::digest::SHA256, b"Salt");
|
||||
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
|
||||
assert_eq!(
|
||||
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
|
||||
&[0..4],
|
||||
@@ -696,7 +696,7 @@ mod tests {
|
||||
#[test]
|
||||
fn generate_update_verify() {
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
let hash_tree = HashTree::new(64, &aws_lc_rs::digest::SHA256, b"Salt");
|
||||
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
|
||||
let mut input = SharedCursor::new();
|
||||
|
||||
// Try input smaller than one block.
|
||||
|
||||
+13
-13
@@ -13,7 +13,7 @@ use bitflags::bitflags;
|
||||
use bstr::ByteSlice;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{byteorder::little_endian, FromBytes, FromZeros, Immutable, IntoBytes};
|
||||
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, byteorder::little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
stream::{
|
||||
CountingReader, FromReader, ReadDiscardExt, ReadFixedSizeExt, ToWriter, WriteZerosExt,
|
||||
},
|
||||
util::{self, is_zero, DebugString},
|
||||
util::{self, DebugString, is_zero},
|
||||
};
|
||||
|
||||
/// Magic value for [`RawGeometry::magic`].
|
||||
@@ -301,7 +301,7 @@ impl RawGeometry {
|
||||
let mut copy = *self;
|
||||
copy.checksum.fill(0);
|
||||
|
||||
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, copy.as_bytes());
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, copy.as_bytes());
|
||||
if digest.as_ref() != self.checksum {
|
||||
return Err(Error::GeometryInvalidDigest {
|
||||
expected: hex::encode(self.checksum),
|
||||
@@ -517,7 +517,7 @@ impl RawHeader {
|
||||
|
||||
let portion = ©.as_bytes()[..expected_size];
|
||||
|
||||
let digest = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, portion);
|
||||
let digest = ring::digest::digest(&ring::digest::SHA256, portion);
|
||||
if digest.as_ref() != self.header_checksum {
|
||||
return Err(Error::HeaderInvalidDigest {
|
||||
expected: hex::encode(self.header_checksum),
|
||||
@@ -812,7 +812,7 @@ impl RawExtent {
|
||||
return Err(Error::ExtentInvalidType {
|
||||
index,
|
||||
extent_type: n,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,7 +1179,7 @@ impl RawMetadata {
|
||||
for slot in &self.slots {
|
||||
#[cfg(not(fuzzing))]
|
||||
{
|
||||
let mut context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
context.update(slot.partitions.as_bytes());
|
||||
context.update(slot.extents.as_bytes());
|
||||
context.update(slot.groups.as_bytes());
|
||||
@@ -1640,7 +1640,7 @@ impl TryFrom<&RawMetadataSlot> for MetadataSlot {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(raw_slot: &RawMetadataSlot) -> Result<Self> {
|
||||
let mut slot = MetadataSlot {
|
||||
let mut slot = Self {
|
||||
major_version: raw_slot.header.major_version.get(),
|
||||
minor_version: raw_slot.header.minor_version.get(),
|
||||
groups: Vec::with_capacity(raw_slot.groups.len()),
|
||||
@@ -1720,7 +1720,7 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
|
||||
fn try_from(slot: &MetadataSlot) -> Result<Self> {
|
||||
let header_size = RawHeader::size_for_version(slot.major_version, slot.minor_version);
|
||||
|
||||
let mut raw_slot = RawMetadataSlot {
|
||||
let mut raw_slot = Self {
|
||||
header: RawHeader {
|
||||
magic: HEADER_MAGIC.into(),
|
||||
major_version: slot.major_version.into(),
|
||||
@@ -1849,7 +1849,7 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
|
||||
raw_slot.header.tables_size = offset.into();
|
||||
|
||||
let tables_digest = {
|
||||
let mut context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
context.update(raw_slot.partitions.as_bytes());
|
||||
context.update(raw_slot.extents.as_bytes());
|
||||
context.update(raw_slot.groups.as_bytes());
|
||||
@@ -1861,8 +1861,8 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
|
||||
.tables_checksum
|
||||
.copy_from_slice(tables_digest.as_ref());
|
||||
|
||||
let header_digest = aws_lc_rs::digest::digest(
|
||||
&aws_lc_rs::digest::SHA256,
|
||||
let header_digest = ring::digest::digest(
|
||||
&ring::digest::SHA256,
|
||||
&raw_slot.header.as_bytes()[..header_size],
|
||||
);
|
||||
raw_slot
|
||||
@@ -1937,7 +1937,7 @@ impl TryFrom<&Metadata> for RawMetadata {
|
||||
// We only do the bare minimum calculations needed here to fill out the
|
||||
// raw fields. There is no semantic validation.
|
||||
|
||||
let mut raw_metadata = RawMetadata {
|
||||
let mut raw_metadata = Self {
|
||||
image_type: metadata.image_type,
|
||||
geometry: RawGeometry {
|
||||
magic: GEOMETRY_MAGIC.into(),
|
||||
@@ -1951,7 +1951,7 @@ impl TryFrom<&Metadata> for RawMetadata {
|
||||
};
|
||||
|
||||
let geometry_digest =
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, raw_metadata.geometry.as_bytes());
|
||||
ring::digest::digest(&ring::digest::SHA256, raw_metadata.geometry.as_bytes());
|
||||
raw_metadata
|
||||
.geometry
|
||||
.checksum
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod avb;
|
||||
@@ -13,3 +13,4 @@ pub mod padding;
|
||||
pub mod payload;
|
||||
pub mod sparse;
|
||||
pub mod verityrs;
|
||||
pub mod zip;
|
||||
|
||||
+206
-96
@@ -2,29 +2,35 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
fmt::{self, Write as _},
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
iter,
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Algorithm, Context};
|
||||
use clap::ValueEnum;
|
||||
use cms::signed_data::SignedData;
|
||||
use const_oid::{db::rfc5912, ObjectIdentifier};
|
||||
use const_oid::{ObjectIdentifier, db::rfc5912};
|
||||
use memchr::memmem;
|
||||
use prost::Message;
|
||||
use ring::digest::{Algorithm, Context};
|
||||
use thiserror::Error;
|
||||
use x509_cert::{der::Encode, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
use x509_cert::{Certificate, der::Encode};
|
||||
use zip::{CompressionMethod, DateTime, ZipArchive, result::ZipError, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
format::payload::{self, PayloadHeader},
|
||||
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
|
||||
format::{
|
||||
payload::{self, PayloadHeader},
|
||||
zip::ZipWriterWrapper,
|
||||
},
|
||||
protobuf::build::tools::releasetools::{OtaMetadata, ota_metadata::OtaType},
|
||||
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
|
||||
util,
|
||||
};
|
||||
|
||||
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
|
||||
@@ -70,8 +76,12 @@ pub enum Error {
|
||||
InvalidLegacyMetadataLine(String),
|
||||
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
|
||||
UnsupportedLegacyMetadataField { key: String, value: String },
|
||||
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
|
||||
MismatchedPropertyFiles { expected: String, actual: String },
|
||||
#[error("Mismatched {key:?} entry offsets: zip only: {zip_only:?}, prop only: {prop_only:?}")]
|
||||
MismatchedPropertyFiles {
|
||||
key: String,
|
||||
zip_only: String,
|
||||
prop_only: String,
|
||||
},
|
||||
#[error("Property files {value:?} exceed {reserved} byte reserved space")]
|
||||
InsufficientReservedSpace { value: String, reserved: usize },
|
||||
#[error("Invalid property file entry: {0:?}")]
|
||||
@@ -280,16 +290,48 @@ fn serialize_metadata(metadata: &OtaMetadata) -> (String, Vec<u8>) {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ZipEntry {
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
/// Parse OTA property files string.
|
||||
pub fn parse_property_files(data: &str) -> Result<Vec<ZipEntry>> {
|
||||
let mut result = vec![];
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PropEntry {
|
||||
name: String,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
for entry in data.trim_end().split(',') {
|
||||
impl PropEntry {
|
||||
pub fn new(path: &str, offset: u64, size: u64) -> Self {
|
||||
Self {
|
||||
name: property_file_name(path).to_owned(),
|
||||
offset,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ZipEntry> for PropEntry {
|
||||
fn from(entry: &ZipEntry) -> Self {
|
||||
Self::new(&entry.path, entry.offset, entry.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PropEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}:{}:{}", self.name, self.offset, self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for PropEntry {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(entry: &str) -> Result<Self> {
|
||||
let mut pieces = entry.split(':');
|
||||
|
||||
let name = pieces
|
||||
@@ -309,38 +351,63 @@ pub fn parse_property_files(data: &str) -> Result<Vec<ZipEntry>> {
|
||||
return Err(Error::InvalidPropertyFileEntry(entry.to_owned()));
|
||||
}
|
||||
|
||||
result.push(ZipEntry { name, offset, size });
|
||||
Ok(Self { name, offset, size })
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse OTA property files string.
|
||||
pub fn parse_property_files(data: &str) -> Result<Vec<PropEntry>> {
|
||||
let mut result = vec![];
|
||||
|
||||
for entry in data.trim_end().split(',') {
|
||||
result.push(entry.parse()?);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get the filename for use in property files entries.
|
||||
fn property_file_name(path: &str) -> &str {
|
||||
path.rsplit_once('/').map_or(path, |p| p.1)
|
||||
}
|
||||
|
||||
/// Compute the property files entries listing the offsets and sizes to every
|
||||
/// zip entry.
|
||||
fn compute_property_files(
|
||||
pf_name: &str,
|
||||
entries: &[ZipEntry],
|
||||
entries: &[PropEntry],
|
||||
max_length: Option<usize>,
|
||||
want_pb: bool,
|
||||
) -> Result<String> {
|
||||
let compute = |path: &'static str| -> Result<String> {
|
||||
// AOSP's ota_utils.py reserves 15 bytes for the `<offset>:<size>`
|
||||
// placeholder. Since the size of `metadata.pb` is almost always 4 digits,
|
||||
// this prevents the offset from exceeding 10 digits. In the wild, there are
|
||||
// OTA files larger than 10 GB. With ota_utils.py, this limit is never
|
||||
// reached because it puts the OTA metadata files at the beginning of the
|
||||
// zip. However, avbroot needs to put them at the end due to streaming
|
||||
// writes, so we reserve an additional byte to allow offsets <100 GB.
|
||||
const RESERVATION_SIZE: usize = 16;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let mut append = |path: &'static str| -> Result<()> {
|
||||
let name = property_file_name(path);
|
||||
let entry = entries
|
||||
.iter()
|
||||
.find(|e| e.name == path)
|
||||
.find(|e| e.name == name)
|
||||
.ok_or(Error::MissingZipEntry(path))?;
|
||||
let name = path.rsplit_once('/').map_or(path, |p| p.1);
|
||||
|
||||
Ok(format!("{name}:{}:{}", entry.offset, entry.size))
|
||||
let _ = write!(&mut buf, "{entry},");
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut tokens = vec![];
|
||||
|
||||
if pf_name == PF_NAME {
|
||||
tokens.push(compute(NAME_PAYLOAD_METADATA)?);
|
||||
append(NAME_PAYLOAD_METADATA)?;
|
||||
}
|
||||
|
||||
for path in [PATH_PAYLOAD, PATH_PROPERTIES] {
|
||||
tokens.push(compute(path)?);
|
||||
append(path)?;
|
||||
}
|
||||
|
||||
for path in [
|
||||
@@ -349,44 +416,51 @@ fn compute_property_files(
|
||||
"care_map.txt",
|
||||
"compatibility.zip",
|
||||
] {
|
||||
if let Ok(token) = compute(path) {
|
||||
tokens.push(token);
|
||||
}
|
||||
// These are optional.
|
||||
let _ = append(path);
|
||||
}
|
||||
|
||||
if max_length.is_none() {
|
||||
tokens.push(format!("metadata:{}", " ".repeat(15)));
|
||||
buf.push_str(property_file_name(PATH_METADATA));
|
||||
buf.push(':');
|
||||
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
|
||||
buf.push(',');
|
||||
|
||||
if want_pb {
|
||||
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
|
||||
buf.push_str(property_file_name(PATH_METADATA_PB));
|
||||
buf.push(':');
|
||||
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
|
||||
buf.push(',');
|
||||
}
|
||||
} else {
|
||||
tokens.push(compute(PATH_METADATA)?);
|
||||
append(PATH_METADATA)?;
|
||||
if want_pb {
|
||||
tokens.push(compute(PATH_METADATA_PB)?);
|
||||
append(PATH_METADATA_PB)?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut joined = tokens.join(",");
|
||||
// Strip final trailing comma.
|
||||
buf.pop();
|
||||
|
||||
if let Some(l) = max_length {
|
||||
if joined.len() > l {
|
||||
if buf.len() > l {
|
||||
return Err(Error::InsufficientReservedSpace {
|
||||
value: joined,
|
||||
value: buf,
|
||||
reserved: l,
|
||||
});
|
||||
}
|
||||
|
||||
let remain = l - joined.len();
|
||||
joined.extend(iter::repeat(' ').take(remain));
|
||||
let remain = l - buf.len();
|
||||
buf.extend(iter::repeat_n(' ', remain));
|
||||
}
|
||||
|
||||
Ok(joined)
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
// Add fake payload_metadata.bin entry, covering the header + header signature
|
||||
// regions of the payload.
|
||||
fn add_payload_metadata_entry(
|
||||
entries: &mut Vec<ZipEntry>,
|
||||
entries: &mut Vec<PropEntry>,
|
||||
payload_metadata_size: u64,
|
||||
) -> Result<()> {
|
||||
let payload_offset = entries
|
||||
@@ -394,11 +468,11 @@ fn add_payload_metadata_entry(
|
||||
.find(|e| e.name == PATH_PAYLOAD)
|
||||
.ok_or(Error::MissingZipEntry(PATH_PAYLOAD))?
|
||||
.offset;
|
||||
entries.push(ZipEntry {
|
||||
name: NAME_PAYLOAD_METADATA.to_owned(),
|
||||
offset: payload_offset,
|
||||
size: payload_metadata_size,
|
||||
});
|
||||
entries.push(PropEntry::new(
|
||||
NAME_PAYLOAD_METADATA,
|
||||
payload_offset,
|
||||
payload_metadata_size,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -426,17 +500,19 @@ impl fmt::Display for ZipMode {
|
||||
/// directory would start.
|
||||
pub fn add_metadata(
|
||||
zip_entries: &[ZipEntry],
|
||||
zip_writer: &mut ZipWriter<impl Write>,
|
||||
zip_writer: &mut ZipWriterWrapper<impl Write>,
|
||||
next_offset: u64,
|
||||
metadata: &OtaMetadata,
|
||||
payload_metadata_size: u64,
|
||||
zip_mode: ZipMode,
|
||||
) -> Result<OtaMetadata> {
|
||||
let mut metadata = metadata.clone();
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored);
|
||||
|
||||
let mut zip_entries = zip_entries.to_owned();
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
let mut prop_entries = zip_entries.iter().map(PropEntry::from).collect();
|
||||
add_payload_metadata_entry(&mut prop_entries, payload_metadata_size)?;
|
||||
|
||||
// Compute initial property files with reserved space as placeholders to
|
||||
// store the self-referential metadata entries later.
|
||||
@@ -444,7 +520,7 @@ pub fn add_metadata(
|
||||
for pf in [PF_NAME, PF_STREAMING_NAME] {
|
||||
metadata.property_files.insert(
|
||||
pf.to_owned(),
|
||||
compute_property_files(pf, &zip_entries, None, true)?,
|
||||
compute_property_files(pf, &prop_entries, None, true)?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -453,68 +529,56 @@ pub fn add_metadata(
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
let raw_writer = Cursor::new(Vec::new());
|
||||
let mut writer = match zip_mode {
|
||||
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriter::new(raw_writer),
|
||||
ZipMode::Streaming => ZipWriterWrapper::new_streaming(raw_writer),
|
||||
ZipMode::Seekable => ZipWriterWrapper::new_seekable(raw_writer),
|
||||
};
|
||||
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = writer
|
||||
.end_extra_data()
|
||||
.start_file(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = writer
|
||||
.end_extra_data()
|
||||
.start_file(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
writer
|
||||
.write_all(&modern_raw)
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
|
||||
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA.to_owned(),
|
||||
offset: next_offset + legacy_offset,
|
||||
size: legacy_raw.len() as u64,
|
||||
});
|
||||
zip_entries.push(ZipEntry {
|
||||
name: PATH_METADATA_PB.to_owned(),
|
||||
offset: next_offset + modern_offset,
|
||||
size: modern_raw.len() as u64,
|
||||
});
|
||||
prop_entries.push(PropEntry::new(
|
||||
PATH_METADATA,
|
||||
next_offset + legacy_offset,
|
||||
legacy_raw.len() as u64,
|
||||
));
|
||||
prop_entries.push(PropEntry::new(
|
||||
PATH_METADATA_PB,
|
||||
next_offset + modern_offset,
|
||||
modern_raw.len() as u64,
|
||||
));
|
||||
|
||||
(next_offset + legacy_offset, next_offset + modern_offset)
|
||||
};
|
||||
|
||||
// Compute the final property files using the offsets of the fake entries.
|
||||
for (key, value) in &mut metadata.property_files {
|
||||
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
|
||||
*value = compute_property_files(key, &prop_entries, Some(value.len()), true)?;
|
||||
}
|
||||
|
||||
// Add the final metadata files to the real zip.
|
||||
{
|
||||
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
|
||||
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
let legacy_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.start_file(PATH_METADATA, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
|
||||
zip_writer
|
||||
.write_all(legacy_raw.as_bytes())
|
||||
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
|
||||
|
||||
zip_writer
|
||||
.start_file_with_extra_data(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
let modern_offset = zip_writer
|
||||
.end_extra_data()
|
||||
.start_file(PATH_METADATA_PB, options)
|
||||
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
|
||||
zip_writer
|
||||
.write_all(&modern_raw)
|
||||
@@ -540,24 +604,73 @@ pub fn verify_metadata(
|
||||
let entry = zip_reader
|
||||
.by_index(i)
|
||||
.map_err(|e| Error::ZipIndexOpen(i, e))?;
|
||||
zip_entries.push(ZipEntry {
|
||||
name: entry.name().to_owned(),
|
||||
offset: entry.data_start(),
|
||||
size: entry.size(),
|
||||
});
|
||||
|
||||
if entry.compression() != CompressionMethod::Stored {
|
||||
continue;
|
||||
}
|
||||
|
||||
zip_entries.push(PropEntry::new(
|
||||
entry.name(),
|
||||
entry.data_start(),
|
||||
entry.size(),
|
||||
));
|
||||
}
|
||||
|
||||
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
|
||||
|
||||
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
|
||||
zip_entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
for (key, value) in &metadata.property_files {
|
||||
let new_value =
|
||||
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
|
||||
if *value != new_value {
|
||||
let mut prop_entries = parse_property_files(value)?;
|
||||
prop_entries.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
// Check that this is a subset of the actual entries.
|
||||
let mut zip_iter = zip_entries.iter().peekable();
|
||||
let mut prop_iter = prop_entries.iter().peekable();
|
||||
let mut zip_only = vec![];
|
||||
let mut prop_only = vec![];
|
||||
|
||||
loop {
|
||||
match (zip_iter.peek(), prop_iter.peek()) {
|
||||
(Some(&zip), Some(&prop)) => match zip.name.cmp(&prop.name) {
|
||||
Ordering::Less => {
|
||||
// Exists in zip, but not in property files.
|
||||
zip_iter.next();
|
||||
}
|
||||
Ordering::Equal => {
|
||||
// If the zip had multiple files with the same filename,
|
||||
// but in different directories, this will fail.
|
||||
if zip != prop {
|
||||
zip_only.push(zip);
|
||||
prop_only.push(prop);
|
||||
}
|
||||
zip_iter.next();
|
||||
prop_iter.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
// Exists in property files, but not in zip.
|
||||
prop_only.push(prop);
|
||||
prop_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(_), None) => {
|
||||
// Exists in zip, but not in property files.
|
||||
zip_iter.next();
|
||||
}
|
||||
(None, Some(prop)) => {
|
||||
// Exists in property files, but not in zip.
|
||||
prop_only.push(prop);
|
||||
prop_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if !zip_only.is_empty() || !prop_only.is_empty() {
|
||||
return Err(Error::MismatchedPropertyFiles {
|
||||
expected: value.clone(),
|
||||
actual: new_value,
|
||||
key: key.clone(),
|
||||
zip_only: util::join(zip_only.into_iter().map(|e| e.to_string()), ","),
|
||||
prop_only: util::join(prop_only.into_iter().map(|e| e.to_string()), ","),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -627,13 +740,10 @@ impl TryFrom<RawOtaSignature> for OtaSignature {
|
||||
|
||||
// We support SHA1 for verification only.
|
||||
let (digest_algo, sig_algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
|
||||
(
|
||||
&aws_lc_rs::digest::SHA256,
|
||||
SignatureAlgorithm::Sha256WithRsa,
|
||||
)
|
||||
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
|
||||
} else {
|
||||
(
|
||||
&aws_lc_rs::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
|
||||
SignatureAlgorithm::Sha1WithRsa,
|
||||
)
|
||||
};
|
||||
@@ -831,7 +941,7 @@ fn validate_eocd(eocd: &[u8]) -> Result<()> {
|
||||
fn compute_signature_comment(
|
||||
key: &RsaSigningKey,
|
||||
cert: &Certificate,
|
||||
digest: aws_lc_rs::digest::Digest,
|
||||
digest: ring::digest::Digest,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cms_signature =
|
||||
crypto::cms_sign_external(key, cert, digest.as_ref()).map_err(Error::CmsSign)?;
|
||||
@@ -883,7 +993,7 @@ pub struct StreamingSigningWriter<W> {
|
||||
impl<W: Write> StreamingSigningWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner: HashingWriter::new(inner, Context::new(&aws_lc_rs::digest::SHA256)),
|
||||
inner: HashingWriter::new(inner, Context::new(&ring::digest::SHA256)),
|
||||
queue: Default::default(),
|
||||
used: 0,
|
||||
}
|
||||
@@ -987,7 +1097,7 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
|
||||
// Compute the digest of everything up until the comment size field.
|
||||
let mut hashing_writer = HashingWriter::new(
|
||||
io::sink(),
|
||||
aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256),
|
||||
ring::digest::Context::new(&ring::digest::SHA256),
|
||||
);
|
||||
|
||||
self.rewind().map_err(|e| Error::DataRead("raw_data", e))?;
|
||||
|
||||
+431
-87
@@ -1,40 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
num::NonZeroU32,
|
||||
ops::{Add, Range},
|
||||
str::FromStr,
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::{Context, Digest};
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use bzip2::write::BzDecoder;
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use flate2::{Compression, write::GzEncoder};
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
write::XzDecoder,
|
||||
write::XzEncoder,
|
||||
};
|
||||
use num_traits::CheckedAdd;
|
||||
use prost::Message;
|
||||
use rayon::{
|
||||
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
|
||||
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
|
||||
};
|
||||
use ring::digest::{Context, Digest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use x509_cert::Certificate;
|
||||
use zerocopy::{big_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, big_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
|
||||
protobuf::chromeos_update_engine::{
|
||||
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
|
||||
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
DeltaArchiveManifest, Extent, InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
|
||||
install_operation::Type, signatures::Signature,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
|
||||
@@ -48,6 +51,11 @@ const PAYLOAD_VERSION: u64 = 2;
|
||||
|
||||
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
|
||||
|
||||
/// Size of each extent. This matches what AOSP's delta_generator does. We also
|
||||
/// require this to be a multiple of the block size and a multiple of the
|
||||
/// maximum CoW compression chunk size.
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown magic: {0:?}")]
|
||||
@@ -77,6 +85,10 @@ pub enum Error {
|
||||
expected: Option<String>,
|
||||
actual: String,
|
||||
},
|
||||
#[error("Invalid block size: {0}")]
|
||||
InvalidBlockSize(u32),
|
||||
#[error("Invalid maximum CoW compression chunk size: {0}")]
|
||||
InvalidMaxCompressionChunkSize(u32),
|
||||
#[error("Size of {name} ({size}) is not aligned to the block size ({block_size})")]
|
||||
InvalidPartitionSize {
|
||||
name: String,
|
||||
@@ -109,7 +121,9 @@ pub enum Error {
|
||||
DataWrite(&'static str, #[source] io::Error),
|
||||
#[error("Expected {expected} bytes, but only wrote {actual} bytes")]
|
||||
UnwrittenData { actual: u64, expected: u64 },
|
||||
#[error("I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}")]
|
||||
#[error(
|
||||
"I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}"
|
||||
)]
|
||||
OperationApply {
|
||||
op_type: Type,
|
||||
start_block: u64,
|
||||
@@ -374,7 +388,7 @@ impl<W: Write> PayloadWriter<W> {
|
||||
// Get the length of an dummy signature struct since the length fields
|
||||
// are part of the data to be signed.
|
||||
let dummy_sig = sign_digest(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"").as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA256, b"").as_ref(),
|
||||
&key,
|
||||
)?;
|
||||
let dummy_sig_size = dummy_sig.encoded_len();
|
||||
@@ -387,9 +401,9 @@ impl<W: Write> PayloadWriter<W> {
|
||||
let manifest_raw_new = header.manifest.encode_to_vec();
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
// Includes signatures (hashes are for properties file).
|
||||
let mut h_full = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut h_full = Context::new(&ring::digest::SHA256);
|
||||
|
||||
// Write header to output file.
|
||||
let raw_header = RawHeader {
|
||||
@@ -600,9 +614,9 @@ pub fn verify_payload(
|
||||
.ok_or(Error::MissingField("signatures_size"))?;
|
||||
|
||||
// Excludes signatures (hashes are for signing).
|
||||
let mut h_partial = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut h_partial = Context::new(&ring::digest::SHA256);
|
||||
// Includes signatures (hashes are for properties file).
|
||||
let mut h_full = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut h_full = Context::new(&ring::digest::SHA256);
|
||||
|
||||
// Read from the beginning to the metadata signature.
|
||||
let metadata_size = header.blob_offset - u64::from(header.metadata_signature_size);
|
||||
@@ -756,7 +770,7 @@ pub fn apply_operation(
|
||||
|
||||
writer.seek(SeekFrom::Start(out_offset)).map_err(error_fn)?;
|
||||
|
||||
let mut hasher = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut hasher = Context::new(&ring::digest::SHA256);
|
||||
|
||||
match op.r#type() {
|
||||
// Handle ZERO/DISCARD specially since they don't require access to
|
||||
@@ -922,10 +936,12 @@ pub fn extract_images<'a>(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Compress raw data into a chunk to be used with a [`Type::ReplaceXz`]
|
||||
/// [`InstallOperation`].
|
||||
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
|
||||
let reader = Cursor::new(raw_data);
|
||||
let writer = Cursor::new(Vec::new());
|
||||
let hashing_writer = HashingWriter::new(writer, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
let hashing_writer = HashingWriter::new(writer, Context::new(&ring::digest::SHA256));
|
||||
|
||||
// AOSP's payload_consumer does not support checking CRC during
|
||||
// decompression. Also, we intentionally pick the lowest compression level
|
||||
@@ -946,56 +962,390 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum VabcAlgo {
|
||||
pub enum CowVersion {
|
||||
V2,
|
||||
V3,
|
||||
}
|
||||
|
||||
impl CowVersion {
|
||||
/// Compute the size overhead required to store the headers and footers
|
||||
/// needed for this version of the on-disk CoW format.
|
||||
fn size_overhead(self, cow_replace_ops: u64, payload_install_ops: u64) -> u64 {
|
||||
const BUFFER_REGION_DEFAULT_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CLUSTER_OPS: u64 = 200;
|
||||
const NUM_RESUME_POINTS: u64 = 4;
|
||||
const SIZEOF_COW_FOOTER_V2: u64 = 84;
|
||||
const SIZEOF_COW_HEADER_V2: u64 = 38;
|
||||
const SIZEOF_COW_HEADER_V3: u64 = SIZEOF_COW_HEADER_V2 + 40;
|
||||
const SIZEOF_COW_OPERATION_V2: u64 = 20;
|
||||
const SIZEOF_COW_OPERATION_V3: u64 = 16;
|
||||
const SIZEOF_RESUME_POINT_V3: u64 = 16;
|
||||
|
||||
let mut overhead = 0;
|
||||
|
||||
match self {
|
||||
Self::V2 => {
|
||||
// sizeof(CowHeader).
|
||||
// AOSP: CowWriterV2::InitPos()
|
||||
overhead += SIZEOF_COW_HEADER_V2;
|
||||
|
||||
// header_.buffer_size. update_engine uses the default value.
|
||||
// AOSP: CowWriterV2::InitPos()
|
||||
overhead += BUFFER_REGION_DEFAULT_SIZE;
|
||||
|
||||
// Add all the CoW operation headers:
|
||||
//
|
||||
// - There is a kCowReplaceOp for each compressed chunk.
|
||||
// - There is a kCowLabelOp for each InstallOperation in the
|
||||
// payload. This is added by delta_generator in CowDryRun().
|
||||
// - There is a kCowClusterOp at the end of each cluster of
|
||||
// operations (which includes the kCowClusterOp itself). The
|
||||
// cluster size used to be 200, but was changed to 1024 in
|
||||
// 5e8e488c13cbff9e0a305ce7c22fd6a13aabb886. We'll use the
|
||||
// smaller value because it's better to overestimate.
|
||||
//
|
||||
// AOSP: CowWriterV2::EmitClusterIfNeeded()
|
||||
let cow_label_ops = payload_install_ops;
|
||||
let cow_cluster_ops = (cow_replace_ops + cow_label_ops).div_ceil(CLUSTER_OPS - 1);
|
||||
|
||||
// A cluster cannot be truncated, so round up to the nearest
|
||||
// cluster boundary.
|
||||
// AOSP: CowWriterV2::AddOperation()
|
||||
overhead += cow_cluster_ops * CLUSTER_OPS * SIZEOF_COW_OPERATION_V2;
|
||||
|
||||
// sizeof(CowFooter).
|
||||
// AOSP: CowWriterV2::GetCowSizeInfo()
|
||||
overhead += SIZEOF_COW_FOOTER_V2;
|
||||
}
|
||||
Self::V3 => {
|
||||
// AOSP: CowWriterV3::OpenForWrite() -> GetDataOffset()
|
||||
overhead += SIZEOF_COW_HEADER_V3;
|
||||
overhead += BUFFER_REGION_DEFAULT_SIZE;
|
||||
overhead += NUM_RESUME_POINTS * SIZEOF_RESUME_POINT_V3;
|
||||
|
||||
// Add an operation header (sizeof(CowOperationV3)) for each
|
||||
// chunk of compressed data.
|
||||
// AOSP: CowWriterV3::WriteOperation()
|
||||
overhead += cow_replace_ops * SIZEOF_COW_OPERATION_V3;
|
||||
}
|
||||
}
|
||||
|
||||
overhead
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ChunkingMethod {
|
||||
/// Compress data in block sized chunks each iteration.
|
||||
Exact,
|
||||
/// Compress data in chunks where each chunk is sized at the largest power
|
||||
/// of 2 that's `<=` the specified size and the remaining input size.
|
||||
MaxPowerOf2(NonZeroU32),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
struct ChunkingParams {
|
||||
block_size: u32,
|
||||
method: ChunkingMethod,
|
||||
}
|
||||
|
||||
impl ChunkingParams {
|
||||
fn chunk_size(self, num_blocks: u64) -> u32 {
|
||||
match self.method {
|
||||
ChunkingMethod::Exact => self.block_size,
|
||||
ChunkingMethod::MaxPowerOf2(max_chunk_size) => {
|
||||
assert!(
|
||||
max_chunk_size.is_power_of_two() && max_chunk_size.get() % self.block_size == 0
|
||||
);
|
||||
|
||||
let mut chunk_size = max_chunk_size.get();
|
||||
while chunk_size > self.block_size {
|
||||
let min_blocks = chunk_size / self.block_size;
|
||||
if num_blocks >= u64::from(min_blocks) {
|
||||
return chunk_size;
|
||||
}
|
||||
|
||||
chunk_size >>= 1;
|
||||
}
|
||||
|
||||
self.block_size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CowEstimate {
|
||||
/// Size of estimate in bytes.
|
||||
pub size: u64,
|
||||
/// Number of CoW operations (v3 only).
|
||||
pub num_ops: u64,
|
||||
}
|
||||
|
||||
impl CowEstimate {
|
||||
/// Add fudge factor to account for overhead.
|
||||
fn fudged(&self, payload_install_ops: u64, cow_version: CowVersion) -> Option<Self> {
|
||||
let version_overhead = cow_version.size_overhead(self.num_ops, payload_install_ops);
|
||||
|
||||
let mut size = self.size.checked_add(version_overhead)?;
|
||||
|
||||
// delta_generator adds 1% overhead to the original CoW size estimate,
|
||||
// even if compression is disabled. We'll do the same too. For the
|
||||
// compressed scenario, we rely on this more because lz4_flex and
|
||||
// zlib-rs usually compress better than the lz4 and zlib implementations
|
||||
// used by libsnapshot_cow.
|
||||
size += size / 100;
|
||||
|
||||
// AOSP: PartitionProcessor::Run()
|
||||
let num_ops = self.num_ops.max(25);
|
||||
|
||||
Some(Self { size, num_ops })
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for CowEstimate {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self {
|
||||
size: self.size + rhs.size,
|
||||
num_ops: self.num_ops + rhs.num_ops,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckedAdd for CowEstimate {
|
||||
fn checked_add(&self, rhs: &Self) -> Option<Self> {
|
||||
Some(Self {
|
||||
size: self.size.checked_add(rhs.size)?,
|
||||
num_ops: self.num_ops.checked_add(rhs.num_ops)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum VabcAlgoKind {
|
||||
None,
|
||||
Lz4,
|
||||
Gzip,
|
||||
Gz,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct VabcAlgo {
|
||||
/// Compression algorithm.
|
||||
pub kind: VabcAlgoKind,
|
||||
/// Compression level. AOSP allows this to be present even if the algorithm
|
||||
/// can't use it.
|
||||
pub level: Option<u32>,
|
||||
}
|
||||
|
||||
impl VabcAlgo {
|
||||
pub fn new(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"lz4" => Some(Self::Lz4),
|
||||
"gz" => Some(Self::Gzip),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Compute the compressed size of the raw data when split into chunks based
|
||||
/// on the specified [`ChunkingParams`]. The length of `raw_data` must be a
|
||||
/// multiple of the block size or else this will panic. The compressed data
|
||||
/// for each chunk is temporarily stored in memory, but discarded after each
|
||||
/// loop iteration.
|
||||
fn compressed_size(self, mut raw_data: &[u8], chunking: ChunkingParams) -> Result<CowEstimate> {
|
||||
assert!(raw_data.len() as u64 % u64::from(chunking.block_size) == 0);
|
||||
|
||||
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> Result<u64> {
|
||||
let mut total = 0;
|
||||
let mut size = 0;
|
||||
let mut num_ops = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
let n = raw_data.len().min(block_size as usize);
|
||||
let (chunk, remaining) = raw_data.split_at(n);
|
||||
let num_blocks = raw_data.len() as u64 / u64::from(chunking.block_size);
|
||||
let chunk_size = chunking.chunk_size(num_blocks) as usize;
|
||||
let (chunk, remaining) = raw_data.split_at(chunk_size);
|
||||
|
||||
// This should match CompressWorker::GetDefaultCompressionLevel() in
|
||||
// AOSP's libsnapshot.
|
||||
let compressed = match self {
|
||||
Self::Lz4 => lz4_flex::block::compress(chunk),
|
||||
Self::Gzip => {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
|
||||
//
|
||||
// CoW v3 uses the raw data instead of the compressed data if the
|
||||
// raw data is smaller. Because we use a different implementation of
|
||||
// the compression algorithms, we don't implement this. It's safer
|
||||
// to just overestimate and use the (larger) compressed size.
|
||||
size += match self.kind {
|
||||
VabcAlgoKind::None => chunk_size as u64,
|
||||
VabcAlgoKind::Lz4 => lz4_flex::block::compress(chunk).len() as u64,
|
||||
VabcAlgoKind::Gz => {
|
||||
let level = self.level.map_or(Compression::best(), Compression::new);
|
||||
let mut encoder = GzEncoder::new(Vec::new(), level);
|
||||
encoder.write_all(chunk).map_err(Error::GzCompress)?;
|
||||
encoder.finish().map_err(Error::GzCompress)?
|
||||
encoder.finish().map_err(Error::GzCompress)?.len() as u64
|
||||
}
|
||||
};
|
||||
|
||||
total += compressed.len().min(n) as u64;
|
||||
|
||||
num_ops += 1;
|
||||
raw_data = remaining;
|
||||
}
|
||||
|
||||
Ok(total)
|
||||
Ok(CowEstimate { size, num_ops })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VabcAlgo {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Lz4 => f.write_str("lz4"),
|
||||
Self::Gzip => f.write_str("gz"),
|
||||
let name = match self.kind {
|
||||
VabcAlgoKind::None => "none",
|
||||
VabcAlgoKind::Lz4 => "lz4",
|
||||
VabcAlgoKind::Gz => "gz",
|
||||
};
|
||||
|
||||
f.write_str(name)?;
|
||||
|
||||
if let Some(level) = self.level {
|
||||
write!(f, ",{level}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
#[error("Invalid VABC algorithm: {0:?} (must be {{none|lz4|gz}}[,<level>])")]
|
||||
pub struct InvalidVabcAlgo(String);
|
||||
|
||||
impl FromStr for VabcAlgo {
|
||||
type Err = InvalidVabcAlgo;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let (prefix, suffix) = s.split_once(',').unwrap_or((s, ""));
|
||||
|
||||
// AOSP allows any algorithm to accept a level, even if it's unused.
|
||||
let level = if !suffix.is_empty() {
|
||||
Some(suffix.parse().map_err(|_| InvalidVabcAlgo(s.to_owned()))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let kind = match prefix {
|
||||
"" | "none" => VabcAlgoKind::None,
|
||||
"lz4" => VabcAlgoKind::Lz4,
|
||||
"gz" => VabcAlgoKind::Gz,
|
||||
_ => return Err(InvalidVabcAlgo(s.to_owned())),
|
||||
};
|
||||
|
||||
Ok(Self { kind, level })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct VabcParams {
|
||||
/// CoW on-disk format version.
|
||||
pub version: CowVersion,
|
||||
/// CoW compression algorithm.
|
||||
pub algo: VabcAlgo,
|
||||
/// The maximum number of bytes to compress at a time.
|
||||
pub compression_factor: u32,
|
||||
}
|
||||
|
||||
/// Ensure that the partition size is aligned to the block size and that the
|
||||
/// block size and compression factor are factors of our [`CHUNK_SIZE`].
|
||||
fn validate_partition_size(
|
||||
partition_name: &str,
|
||||
file_size: u64,
|
||||
block_size: u32,
|
||||
compression_factor: u32,
|
||||
) -> Result<()> {
|
||||
if block_size == 0 || !block_size.is_power_of_two() || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidBlockSize(block_size));
|
||||
}
|
||||
|
||||
if compression_factor == 0
|
||||
|| !compression_factor.is_power_of_two()
|
||||
|| CHUNK_SIZE % u64::from(compression_factor) != 0
|
||||
{
|
||||
return Err(Error::InvalidMaxCompressionChunkSize(compression_factor));
|
||||
}
|
||||
|
||||
if file_size % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition_name.to_owned(),
|
||||
size: file_size,
|
||||
block_size,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compute the VABC CoW size estimate. For a more accurate size estimate with
|
||||
/// CoW version 2, `payload_install_ops` must be equal to the number of
|
||||
/// [`InstallOperation`]s in the payload. The caller must update
|
||||
/// [`PartitionUpdate::estimate_cow_size`] and
|
||||
/// [`PartitionUpdate::estimate_op_count_max`] or else update_engine may fail to
|
||||
/// flash the partition due to running out of space on the CoW block device.
|
||||
pub fn compute_cow_estimate(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
payload_install_ops: u64,
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
vabc_params: VabcParams,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<CowEstimate> {
|
||||
let file_size = input
|
||||
.reopen_boxed()
|
||||
.and_then(|mut r| r.seek(SeekFrom::End(0)))
|
||||
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
validate_partition_size(
|
||||
partition_name,
|
||||
file_size,
|
||||
block_size,
|
||||
vabc_params.compression_factor,
|
||||
)?;
|
||||
|
||||
let chunking = ChunkingParams {
|
||||
block_size,
|
||||
method: match vabc_params.version {
|
||||
CowVersion::V2 => ChunkingMethod::Exact,
|
||||
CowVersion::V3 => {
|
||||
ChunkingMethod::MaxPowerOf2(vabc_params.compression_factor.try_into().unwrap())
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
|
||||
|
||||
let initial_estimate = (0..chunks_total)
|
||||
.into_par_iter()
|
||||
.map(|chunk| -> Result<CowEstimate> {
|
||||
let data = (|| {
|
||||
let mut reader = input.reopen_boxed()?;
|
||||
reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
|
||||
|
||||
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
|
||||
file_size % CHUNK_SIZE
|
||||
} else {
|
||||
CHUNK_SIZE
|
||||
};
|
||||
|
||||
stream::check_cancel(cancel_signal)?;
|
||||
reader.read_vec_exact(chunk_size as usize)
|
||||
})()
|
||||
.map_err(Error::ChunkRead)?;
|
||||
|
||||
vabc_params.algo.compressed_size(&data, chunking)
|
||||
})
|
||||
.try_fold(
|
||||
CowEstimate::default,
|
||||
|total, chunk_estimate| -> Result<CowEstimate> {
|
||||
total
|
||||
.checked_add(&chunk_estimate?)
|
||||
.ok_or(Error::IntOverflow("initial_estimate"))
|
||||
},
|
||||
)
|
||||
.try_reduce(CowEstimate::default, |total, partial| {
|
||||
total
|
||||
.checked_add(&partial)
|
||||
.ok_or(Error::IntOverflow("initial_estimate"))
|
||||
})?;
|
||||
|
||||
initial_estimate
|
||||
.fudged(payload_install_ops, vabc_params.version)
|
||||
.ok_or(Error::IntOverflow("fudged_estimate"))
|
||||
}
|
||||
|
||||
/// Compress the image and return the corresponding information to insert into
|
||||
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
|
||||
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
|
||||
@@ -1004,21 +1354,17 @@ impl fmt::Display for VabcAlgo {
|
||||
/// update [`InstallOperation::data_offset`] in each operation manually because
|
||||
/// the initial values are relative to 0.
|
||||
///
|
||||
/// If `vabc_algo` is set, the VABC CoW v2 size estimate will be computed. The
|
||||
/// caller must update [`PartitionUpdate::estimate_cow_size`] with this value or
|
||||
/// else update_engine may fail to flash the partition due to running out of
|
||||
/// space on the CoW block device. CoW v2 + other algorithms and also CoW v3 are
|
||||
/// currently unsupported because there currently are no known OTAs that use
|
||||
/// those configurations.
|
||||
/// If `vabc_algo` is set, the VABC CoW size estimate will also be computed.
|
||||
/// This is more efficient than separately calling [`compute_cow_estimate`]
|
||||
/// since the input does not need to be read twice.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
vabc_algo: Option<VabcAlgo>,
|
||||
vabc_params: Option<VabcParams>,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<CowEstimate>)> {
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
let file_size = input
|
||||
@@ -1027,18 +1373,23 @@ pub fn compress_image(
|
||||
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
|
||||
let final_chunk_different = file_size % CHUNK_SIZE != 0;
|
||||
|
||||
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
|
||||
return Err(Error::InvalidPartitionSize {
|
||||
name: partition_name.to_owned(),
|
||||
size: file_size,
|
||||
block_size,
|
||||
});
|
||||
}
|
||||
let compression_factor = vabc_params.map_or(block_size, |p| p.compression_factor);
|
||||
validate_partition_size(partition_name, file_size, block_size, compression_factor)?;
|
||||
|
||||
let chunking = ChunkingParams {
|
||||
block_size,
|
||||
method: match vabc_params.map(|p| p.version) {
|
||||
Some(CowVersion::V3) => {
|
||||
ChunkingMethod::MaxPowerOf2(compression_factor.try_into().unwrap())
|
||||
}
|
||||
_ => ChunkingMethod::Exact,
|
||||
},
|
||||
};
|
||||
|
||||
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut context_uncompressed = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut cow_estimate = 0;
|
||||
let mut bytes_compressed = 0u64;
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut initial_estimate = CowEstimate::default();
|
||||
let mut operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -1075,12 +1426,12 @@ pub fn compress_image(
|
||||
let mut compressed_data_group = uncompressed_data_group
|
||||
.into_par_iter()
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, CowEstimate)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
let cow_size = vabc_algo
|
||||
.map(|a| a.compressed_size(&raw_data, block_size))
|
||||
let cow_estimate = vabc_params
|
||||
.map(|p| p.algo.compressed_size(&raw_data, chunking))
|
||||
.transpose()?
|
||||
.unwrap_or(0);
|
||||
.unwrap_or_default();
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -1093,15 +1444,19 @@ pub fn compress_image(
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
Ok((data, operation, cow_size))
|
||||
Ok((data, operation, cow_estimate))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, operation, cow_size) in &mut compressed_data_group {
|
||||
for (data, operation, cow_estimate) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
cow_estimate += *cow_size;
|
||||
bytes_compressed = bytes_compressed
|
||||
.checked_add(data.len() as u64)
|
||||
.ok_or(Error::IntOverflow("bytes_compressed"))?;
|
||||
initial_estimate = initial_estimate
|
||||
.checked_add(cow_estimate)
|
||||
.ok_or(Error::IntOverflow("initial_estimate"))?;
|
||||
}
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
@@ -1125,25 +1480,12 @@ pub fn compress_image(
|
||||
hash: Some(digest_uncompressed.as_ref().to_vec()),
|
||||
};
|
||||
|
||||
let cow_estimate = if vabc_algo.is_some() {
|
||||
// lz4_flex and miniz_oxide usually compress better than the lz4 and
|
||||
// zlib implementations used by libsnapshot_cow. Make up for this by
|
||||
// adding percentage-based overhead.
|
||||
cow_estimate += cow_estimate / 100;
|
||||
|
||||
// We also need to account for constant overhead, especially with
|
||||
// smaller partitions. We can match what delta_generator normally adds
|
||||
// in CowWriterV2::InitPos() exactly. Since we only ever create full
|
||||
// OTAs, we can assume that all CoW operations are kCowReplaceOp.
|
||||
|
||||
// sizeof(CowHeader).
|
||||
cow_estimate += 38;
|
||||
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
|
||||
cow_estimate += 2 * 1024 * 1024;
|
||||
// CowOptions::cluster_ops * sizeof(CowOperationV2).
|
||||
cow_estimate += 200 * 20;
|
||||
|
||||
Some(cow_estimate)
|
||||
let cow_estimate = if let Some(p) = vabc_params {
|
||||
Some(
|
||||
initial_estimate
|
||||
.fudged(operations.len() as u64, p.version)
|
||||
.ok_or(Error::IntOverflow("fudged_estimate"))?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1208,7 +1550,7 @@ pub fn compress_modified_image(
|
||||
|
||||
let groups_total = operations.len().div_ceil(OPERATION_GROUP);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut context_uncompressed = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut modified_operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -1290,6 +1632,8 @@ pub fn compress_modified_image(
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
|
||||
// Clippy doesn't know we're returning a Range.
|
||||
#[allow(clippy::range_plus_one)]
|
||||
Ok(i..i + 1)
|
||||
})
|
||||
.collect::<io::Result<Vec<_>>>()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
use crc32fast::Hasher;
|
||||
use dlv_list::{Index, VecList};
|
||||
use thiserror::Error;
|
||||
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
|
||||
use zerocopy::{FromBytes, IntoBytes, byteorder::little_endian};
|
||||
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
|
||||
|
||||
use crate::stream::ReadDiscardExt;
|
||||
@@ -159,7 +159,7 @@ impl RawHeader {
|
||||
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
|
||||
}
|
||||
|
||||
if self.file_hdr_sz.get() < mem::size_of::<RawHeader>() as u16 {
|
||||
if self.file_hdr_sz.get() < mem::size_of::<Self>() as u16 {
|
||||
return Err(Error::InvalidFileHeaderSize(self.file_hdr_sz.get()));
|
||||
} else if self.chunk_hdr_sz.get() < mem::size_of::<RawChunk>() as u16 {
|
||||
return Err(Error::InvalidChunkHeaderSize(self.chunk_hdr_sz.get()));
|
||||
@@ -173,7 +173,7 @@ impl RawHeader {
|
||||
}
|
||||
|
||||
fn excess_raw_header_bytes(&self) -> u16 {
|
||||
self.file_hdr_sz.get() - mem::size_of::<RawHeader>() as u16
|
||||
self.file_hdr_sz.get() - mem::size_of::<Self>() as u16
|
||||
}
|
||||
|
||||
fn excess_raw_chunk_bytes(&self) -> u16 {
|
||||
@@ -225,7 +225,7 @@ impl RawChunk {
|
||||
return Err(Error::InvalidChunkType {
|
||||
index,
|
||||
chunk_type: t,
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -374,6 +374,9 @@ impl fmt::Debug for ChunkData {
|
||||
/// metadata they contain.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Chunk {
|
||||
/// When [`Self::data`] is [`ChunkData::Data`], this is guaranteed to not
|
||||
/// exceed the bounds of [`u32`] when multiplied by [`Header::block_size`].
|
||||
/// For other types of data, a 64-bit signed or unsigned integer is needed.
|
||||
pub bounds: ChunkBounds,
|
||||
pub data: ChunkData,
|
||||
}
|
||||
@@ -811,7 +814,7 @@ impl<R: Read> SparseReader<R> {
|
||||
data = ChunkData::Crc32(expected.get());
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
}
|
||||
|
||||
let chunk = Chunk {
|
||||
bounds: ChunkBounds {
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::io::{self, Seek, SeekFrom, Write};
|
||||
|
||||
use zip::{
|
||||
ZipWriter,
|
||||
result::ZipResult,
|
||||
write::{FileOptionExtension, FileOptions, StreamWriter},
|
||||
};
|
||||
|
||||
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
|
||||
/// during the creation of a new instance. The resulting type can be stored in a
|
||||
/// parent container where the generic type does not implement [`Seek`].
|
||||
pub struct SeekWriter<W: Write> {
|
||||
inner: W,
|
||||
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
|
||||
}
|
||||
|
||||
impl<W: Write> SeekWriter<W> {
|
||||
pub fn into_inner(self) -> W {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> SeekWriter<W> {
|
||||
pub fn new(inner: W) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
seek_fn: W::seek,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for SeekWriter<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.inner.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Seek for SeekWriter<W> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
(self.seek_fn)(&mut self.inner, pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// This is an ugly hack to have a single type represent both seekable and
|
||||
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
|
||||
/// a seekable instance via [`Self::new_seekable`].
|
||||
pub enum ZipWriterWrapper<W: Write> {
|
||||
Streaming(ZipWriter<StreamWriter<W>>),
|
||||
Seekable(ZipWriter<SeekWriter<W>>),
|
||||
}
|
||||
|
||||
impl<W: Write + Seek> ZipWriterWrapper<W> {
|
||||
pub fn new_seekable(inner: W) -> Self {
|
||||
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> ZipWriterWrapper<W> {
|
||||
pub fn new_streaming(inner: W) -> Self {
|
||||
Self::Streaming(ZipWriter::new_stream(inner))
|
||||
}
|
||||
|
||||
pub fn start_file(
|
||||
&mut self,
|
||||
name: impl ToString,
|
||||
options: FileOptions<impl FileOptionExtension>,
|
||||
) -> ZipResult<u64> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.start_file(name, options),
|
||||
Self::Seekable(z) => z.start_file(name, options),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(self) -> ZipResult<W> {
|
||||
match self {
|
||||
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
|
||||
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Write for ZipWriterWrapper<W> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.write(buf),
|
||||
Self::Seekable(z) => z.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Streaming(z) => z.flush(),
|
||||
Self::Seekable(z) => z.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,8 +4,8 @@
|
||||
use std::{
|
||||
process::ExitCode,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
};
|
||||
|
||||
use num_traits::{Num, PrimInt};
|
||||
use serde::{de::Visitor, Deserializer, Serializer};
|
||||
use serde::{Deserializer, Serializer, de::Visitor};
|
||||
|
||||
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
||||
@@ -14,7 +14,6 @@ use std::{
|
||||
sync::atomic::AtomicBool,
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use bstr::ByteSlice;
|
||||
use liblzma::{
|
||||
stream::{Check, Stream},
|
||||
@@ -22,11 +21,12 @@ use liblzma::{
|
||||
};
|
||||
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
|
||||
use regex::bytes::Regex;
|
||||
use ring::digest::Context;
|
||||
use rsa::RsaPublicKey;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, warn, Span};
|
||||
use tracing::{Span, debug, debug_span, trace, warn};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{result::ZipError, ZipArchive};
|
||||
use zip::{ZipArchive, result::ZipError};
|
||||
|
||||
use crate::{
|
||||
crypto::{self, RsaSigningKey},
|
||||
@@ -194,7 +194,7 @@ impl MagiskRootPatcher {
|
||||
// replaced by PREINITDEVICE
|
||||
// - Versions newer than the latest supported version are assumed to support
|
||||
// the same features as the latest version
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28200];
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30300];
|
||||
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
|
||||
@@ -432,7 +432,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
@@ -781,7 +781,7 @@ impl DsuPubKeyPatcher {
|
||||
e.data = data;
|
||||
} else {
|
||||
entries.push(CpioEntry::new_file(Self::AVBROOT_KEY_PATH, 0o644, data));
|
||||
};
|
||||
}
|
||||
|
||||
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
@@ -971,7 +971,7 @@ impl BootImagePatch for PrepatchedImagePatcher {
|
||||
targets.push("init_boot");
|
||||
} else if boot_images.contains_key("boot") {
|
||||
targets.push("boot");
|
||||
};
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
@@ -1180,7 +1180,7 @@ fn save_boot_image(
|
||||
};
|
||||
|
||||
// Write new boot image. We reuse the existing salt for the digest.
|
||||
let mut context = Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let mut context = Context::new(&ring::digest::SHA256);
|
||||
context.update(&descriptor.salt);
|
||||
let mut hashing_writer = HashingWriter::new(writer, context);
|
||||
info.boot_image
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
|
||||
use bitflags::bitflags;
|
||||
use thiserror::Error;
|
||||
use tracing::trace;
|
||||
use x509_cert::{der::asn1::BitString, Certificate};
|
||||
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
|
||||
use x509_cert::{Certificate, der::asn1::BitString};
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
|
||||
|
||||
use crate::{crypto, format::ota};
|
||||
|
||||
@@ -79,7 +79,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
|
||||
CompressionMethod::Stored
|
||||
};
|
||||
|
||||
let options = FileOptions::default().compression_method(compression_method);
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(compression_method);
|
||||
let name = "ota.x509.pem";
|
||||
writer.start_file(name, options).map_err(Error::ZipWrite)?;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{
|
||||
use memchr::memmem;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, debug_span, trace, Span};
|
||||
use tracing::{Span, debug, debug_span, trace};
|
||||
use x509_cert::Certificate;
|
||||
use zip::ZipArchive;
|
||||
|
||||
@@ -190,23 +190,9 @@ pub fn patch_system_image(
|
||||
return Err(Error::OldZipNotFound);
|
||||
}
|
||||
|
||||
let update_ranges = if descriptor.hash_algorithm == "sha1" {
|
||||
// Promote to a secure algorithm. SHA1 is allowed for verification only.
|
||||
// The entire hash tree and FEC data will need to be recomputed.
|
||||
let new_algorithm = "sha256".to_owned();
|
||||
|
||||
debug!(
|
||||
"Changing insecure hash algorithm {} to {new_algorithm}",
|
||||
descriptor.hash_algorithm,
|
||||
);
|
||||
|
||||
descriptor.hash_algorithm = new_algorithm;
|
||||
None
|
||||
} else {
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
Some(modified_ranges.as_slice())
|
||||
};
|
||||
// Only need to update the hash tree and FEC data corresponding to the
|
||||
// modified regions.
|
||||
let update_ranges = Some(modified_ranges.as_slice());
|
||||
|
||||
descriptor
|
||||
.update(input, output, update_ranges, cancel_signal)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::all)]
|
||||
#![allow(clippy::nursery)]
|
||||
#![allow(clippy::pedantic)]
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, RwLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use num_traits::ToPrimitive;
|
||||
use ring::digest::Context;
|
||||
|
||||
use crate::util;
|
||||
|
||||
@@ -637,7 +637,7 @@ mod tests {
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use aws_lc_rs::digest::Context;
|
||||
use ring::digest::Context;
|
||||
|
||||
use super::{
|
||||
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
|
||||
@@ -713,7 +713,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hashing_reader() {
|
||||
let raw_reader = Cursor::new(b"foobar");
|
||||
let mut reader = HashingReader::new(raw_reader, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
let mut reader = HashingReader::new(raw_reader, Context::new(&ring::digest::SHA256));
|
||||
|
||||
let mut buf = [0u8; 6];
|
||||
reader.read_exact(&mut buf[..0]).unwrap();
|
||||
@@ -730,7 +730,7 @@ mod tests {
|
||||
#[test]
|
||||
fn hashing_writer() {
|
||||
let raw_writer = Cursor::new([0u8; 6]);
|
||||
let mut writer = HashingWriter::new(raw_writer, Context::new(&aws_lc_rs::digest::SHA256));
|
||||
let mut writer = HashingWriter::new(raw_writer, Context::new(&ring::digest::SHA256));
|
||||
|
||||
writer.write_all(b"").unwrap();
|
||||
writer.write_all(b"foo").unwrap();
|
||||
|
||||
+25
-2
@@ -1,9 +1,10 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt, mem,
|
||||
fmt::{self, Display},
|
||||
mem,
|
||||
ops::{
|
||||
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
|
||||
},
|
||||
@@ -378,6 +379,28 @@ where
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut result = String::new();
|
||||
|
||||
for (i, item) in into_iter.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
result.push_str(sep);
|
||||
}
|
||||
|
||||
write!(result, "{item}").expect("Failed to allocate");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn sort<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
|
||||
let mut items = iter.collect::<Vec<_>>();
|
||||
items.sort();
|
||||
items
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -134,7 +134,7 @@ fn round_trip_root_image() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
|
||||
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
|
||||
@@ -233,7 +233,7 @@ fn round_trip_appended_hash_image() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
|
||||
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
|
||||
@@ -341,7 +341,7 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
|
||||
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
|
||||
@@ -448,7 +448,7 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
|
||||
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
|
||||
|
||||
@@ -49,7 +49,7 @@ fn round_trip(image: &BootImage, sha512: &[u8; 64], expected_version: u32) {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ fn generate_archive() -> Vec<u8> {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xb0, 0x51, 0xac, 0x28, 0x6f, 0x78, 0xe2, 0xe7, 0x45, 0xa0, 0x52, 0x7c, 0xff, 0x42,
|
||||
0x30, 0x55, 0xbd, 0x64, 0x7d, 0x4e, 0xb8, 0xe6, 0x95, 0xe5, 0x9b, 0xd1, 0x13, 0xd6,
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) {
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[
|
||||
let data = writer.into_inner();
|
||||
|
||||
assert_eq!(
|
||||
aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA512, &data).as_ref(),
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
sha512,
|
||||
);
|
||||
|
||||
|
||||
@@ -37,11 +37,17 @@ allow = [
|
||||
"GPL-3.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 },
|
||||
]
|
||||
|
||||
[bans]
|
||||
multiple-versions = "warn"
|
||||
multiple-versions-include-dev = true
|
||||
@@ -57,13 +63,11 @@ include-workspace = true
|
||||
bypass = [
|
||||
# Copies of unmodified crashwrangler objects for old macOS versions.
|
||||
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
|
||||
# Only used in tests.
|
||||
{ name = "libloading", allow-globs = ["tests/*.dll"] }
|
||||
]
|
||||
|
||||
[sources]
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "deny"
|
||||
allow-git = [
|
||||
"https://github.com/chenxiaolong/zip",
|
||||
"https://github.com/chenxiaolong/zip2",
|
||||
]
|
||||
|
||||
+7
-5
@@ -9,25 +9,27 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
aws-lc-rs = { version = "1.0.0", default-features = false, features = ["aws-lc-sys"] }
|
||||
anyhow = "1.0.75"
|
||||
avbroot = { path = "../avbroot" }
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
ctrlc = "3.4.0"
|
||||
hex = { version = "0.4.3", features = ["serde"] }
|
||||
ring = "0.17.14"
|
||||
rsa = { version = "0.9.6", features = ["hazmat"] }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
tempfile = "3.8.0"
|
||||
toml_edit = { version = "0.22.9", features = ["serde"] }
|
||||
toml_edit = { version = "0.23.3", features = ["serde"] }
|
||||
topological-sort = "0.2.2"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
x509-cert = "0.2.5"
|
||||
|
||||
# https://github.com/zip-rs/zip/pull/383
|
||||
# https://github.com/zip-rs/zip2/pull/367
|
||||
# https://github.com/zip-rs/zip2/pull/368
|
||||
# For getting the data offset when writing new zip entries.
|
||||
[dependencies.zip]
|
||||
git = "https://github.com/chenxiaolong/zip"
|
||||
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
|
||||
git = "https://github.com/chenxiaolong/zip2"
|
||||
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
|
||||
default-features = false
|
||||
|
||||
[features]
|
||||
|
||||
+26
-25
@@ -12,8 +12,10 @@ security_patch_level = "2024-01-01"
|
||||
# Google Pixel 7 Pro
|
||||
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
|
||||
|
||||
[profile.pixel_v4_gki]
|
||||
vabc_algo = "Lz4"
|
||||
[profile.pixel_v4_gki.vabc]
|
||||
# CoW v3 is used starting with the Google Pixel 9a.
|
||||
version = "V3"
|
||||
algo = { kind = "Lz4" }
|
||||
|
||||
[profile.pixel_v4_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -49,18 +51,19 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes_streaming]
|
||||
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
|
||||
patched = "6c27ffb07f4497af8539f8283e506066af9417580230c3209c9875fc15d5069d"
|
||||
original = "ef6261cd9ebea90f036e52a46160a400c5b8f6ef24ed2469c4a1e9689987aa06"
|
||||
patched = "37fd353a766a7b9a339fbf51fa79c703e94640dc6a2c6310d79357aaefcc7ca1"
|
||||
|
||||
[profile.pixel_v4_gki.hashes_seekable]
|
||||
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
|
||||
patched = "e4fc12523ffc312796b92210bc1e3bbb70dd60797a47daae76c8b5852e48b382"
|
||||
original = "8a2c717607c10dfa5483d6f9a9f37b3d978acaf8d2ea18e36544af267943e750"
|
||||
patched = "2c4734c9e1d028ee6aaf02bb416e5e173857faffd2ca067366790655147b3afa"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
|
||||
[profile.pixel_v4_non_gki]
|
||||
vabc_algo = "Lz4"
|
||||
[profile.pixel_v4_non_gki.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Lz4" }
|
||||
|
||||
[profile.pixel_v4_non_gki.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -90,18 +93,19 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_streaming]
|
||||
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
|
||||
patched = "6832ded3e98a14edc8c5ea7284fcea0b958fa710ebf222c27116faec8dfefe2e"
|
||||
original = "630220ef813a2b4743d1941179cc9705da86ad4805f1c52341dcb38fbce3d29e"
|
||||
patched = "b725e91751fe58aed20495aecbf9b4bdc14d2799cd88dcbd58f3a3b02b3af15b"
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes_seekable]
|
||||
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
|
||||
patched = "114f8936e917d7e4a71bc1521adb3c8e676de3a738f8c7c505b86464d20bd95c"
|
||||
original = "1afbe6867ded345d941098ee7c7fcf94a3df52c50ff96ab8f3a67b2ab957259a"
|
||||
patched = "4357b977249006b101002c961916f962787315a80b8608494c6a1f0cf09cecd1"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
|
||||
[profile.pixel_v3]
|
||||
vabc_algo = "Lz4"
|
||||
[profile.pixel_v3.vabc]
|
||||
version = "V2"
|
||||
algo = { kind = "Gz" }
|
||||
|
||||
[profile.pixel_v3.partitions.boot]
|
||||
avb.signed = true
|
||||
@@ -132,19 +136,16 @@ data.version = "vendor_v3"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes_streaming]
|
||||
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
|
||||
patched = "1f28d9210a17e233cd5da4af55b07db764b19eeab991394514170b405240464f"
|
||||
original = "9b65037343d45211e0f9706929cba34643a9c54274d1b39740c43f45974984e0"
|
||||
patched = "fb23ab9616968b38b96d1e5e6a503154f89aebc1741e89a9e9dfd2c4d9946b05"
|
||||
|
||||
[profile.pixel_v3.hashes_seekable]
|
||||
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
|
||||
patched = "27b80c7be9c1e527ea26abe3dabde245c580e6f26ec084204278fbfd81a39f83"
|
||||
original = "e581934887dd93b8a9d9c3aa5dec1d48aa7e01bf01ac507e8c5fb256b59cbe7d"
|
||||
patched = "669a826abc6d67e7e0b1def724aa7b470461087255c65663d195df1426a355f0"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
|
||||
[profile.pixel_v2]
|
||||
vabc_algo = "Gzip"
|
||||
|
||||
[profile.pixel_v2.partitions.boot]
|
||||
avb.signed = false
|
||||
data.type = "boot"
|
||||
@@ -168,9 +169,9 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes_streaming]
|
||||
original = "bd2f19cf3d2285e35e8b36d44f75ed910e8e0be44c3ebd29f17a812521ba754b"
|
||||
patched = "cf65d5b90500af54cd1204a646379bb852825061bc7c3f973b7a042f353f75ad"
|
||||
original = "f10ee15c900a474cc6bbefa705f272cef42636ea096e75563d2d78f6c4327fd1"
|
||||
patched = "6929f65909037f5550a53982b71e96bdf69ab876bc5e86702c469ed601be8a9a"
|
||||
|
||||
[profile.pixel_v2.hashes_seekable]
|
||||
original = "7f96ebf7366e0b60c91ac1e5f196a2189ffdb0bbc73f77804a736466fcab7315"
|
||||
patched = "c2d9d60d73c038da39f82073ffadb459c96b901d66db7af11f59da58e0dd53e4"
|
||||
original = "4e863d251b9ff6eaa1511f9c03e9bdb8919650b2e0eaf23e33892a639edafcaf"
|
||||
patched = "9a103222e73df70a097281525546d25c850df2ae7a2ba715aa5dfbbba3f7972b"
|
||||
|
||||
+21
-14
@@ -1,14 +1,14 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{collections::BTreeMap, fs, path::Path};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use avbroot::format::payload::VabcAlgo;
|
||||
use avbroot::format::payload::{CowVersion, VabcAlgo};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Sha256Hash(
|
||||
#[serde(
|
||||
serialize_with = "hex::serialize",
|
||||
@@ -17,7 +17,7 @@ pub struct Sha256Hash(
|
||||
pub [u8; 32],
|
||||
);
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct OtaInfo {
|
||||
pub device: String,
|
||||
@@ -29,7 +29,7 @@ pub struct OtaInfo {
|
||||
pub security_patch_level: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Avb {
|
||||
pub signed: bool,
|
||||
@@ -55,7 +55,7 @@ pub enum BootVersion {
|
||||
VendorV4,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BootData {
|
||||
pub version: BootVersion,
|
||||
@@ -71,19 +71,19 @@ pub enum DmVerityContent {
|
||||
SystemOtacerts,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DmVerityData {
|
||||
pub content: DmVerityContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VbmetaData {
|
||||
pub deps: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Data {
|
||||
Boot(BootData),
|
||||
@@ -91,30 +91,37 @@ pub enum Data {
|
||||
Vbmeta(VbmetaData),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Hashes {
|
||||
pub original: Sha256Hash,
|
||||
pub patched: Sha256Hash,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Partition {
|
||||
pub avb: Avb,
|
||||
pub data: Data,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct VabcSettings {
|
||||
pub version: CowVersion,
|
||||
pub algo: VabcAlgo,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Profile {
|
||||
pub vabc_algo: Option<VabcAlgo>,
|
||||
pub vabc: Option<VabcSettings>,
|
||||
pub partitions: BTreeMap<String, Partition>,
|
||||
pub hashes_streaming: Hashes,
|
||||
pub hashes_seekable: Hashes,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub ota_info: OtaInfo,
|
||||
|
||||
+62
-44
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
@@ -14,12 +14,12 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
slice,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
use avbroot::{
|
||||
cli::ota::{ExtractCli, PatchCli, VerifyCli},
|
||||
crypto::{self, PassphraseSource, RsaSigningKey},
|
||||
@@ -36,11 +36,12 @@ use avbroot::{
|
||||
cpio::{self, CpioEntry, CpioEntryData},
|
||||
ota::{self, SigningWriter, ZipEntry, ZipMode},
|
||||
padding,
|
||||
payload::{self, PayloadHeader, PayloadWriter},
|
||||
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
|
||||
zip::ZipWriterWrapper,
|
||||
},
|
||||
patch::otacert::{self, OtaCertBuildFlags},
|
||||
protobuf::{
|
||||
build::tools::releasetools::{ota_metadata::OtaType, DeviceState, OtaMetadata},
|
||||
build::tools::releasetools::{DeviceState, OtaMetadata, ota_metadata::OtaType},
|
||||
chromeos_update_engine::{
|
||||
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
|
||||
},
|
||||
@@ -48,12 +49,12 @@ use avbroot::{
|
||||
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
|
||||
};
|
||||
use clap::Parser;
|
||||
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
|
||||
use rsa::{BigUint, rand_core::OsRng, traits::PublicKeyParts};
|
||||
use tempfile::TempDir;
|
||||
use topological_sort::TopologicalSort;
|
||||
use tracing::{info, info_span};
|
||||
use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
|
||||
use zip::{CompressionMethod, DateTime, ZipWriter, write::SimpleFileOptions};
|
||||
|
||||
use crate::{
|
||||
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
|
||||
@@ -69,7 +70,7 @@ fn hash_file(path: &Path, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
|
||||
let raw_reader =
|
||||
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
|
||||
let buf_reader = BufReader::new(raw_reader);
|
||||
let context = aws_lc_rs::digest::Context::new(&aws_lc_rs::digest::SHA256);
|
||||
let context = ring::digest::Context::new(&ring::digest::SHA256);
|
||||
let mut hashing_reader = HashingReader::new(buf_reader, context);
|
||||
|
||||
stream::copy(&mut hashing_reader, io::sink(), cancel_signal)?;
|
||||
@@ -97,14 +98,14 @@ fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &AtomicBool) -> Re
|
||||
fn append_avb(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
hash_tree: bool,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<()> {
|
||||
let image_size = file.seek(SeekFrom::End(0))?;
|
||||
let salt = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, b"avbroot");
|
||||
let salt = ring::digest::digest(&ring::digest::SHA256, b"avbroot");
|
||||
let descriptors = vec![
|
||||
if hash_tree {
|
||||
let mut descriptor = HashTreeDescriptor {
|
||||
@@ -295,7 +296,7 @@ fn create_ramdisk(
|
||||
fn create_boot_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
boot_data: &BootData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaSigningKey,
|
||||
@@ -374,7 +375,7 @@ fn create_boot_image(
|
||||
.ramdisks
|
||||
.iter()
|
||||
.map(|c_list| {
|
||||
if c_list.iter().any(|c| *c == RamdiskContent::Dlkm) {
|
||||
if c_list.contains(&RamdiskContent::Dlkm) {
|
||||
RamdiskMeta {
|
||||
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
|
||||
ramdisk_name: "dlkm".to_owned(),
|
||||
@@ -422,8 +423,8 @@ fn create_boot_image(
|
||||
fn create_dm_verity_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
dm_verity_data: &DmVerityData,
|
||||
avb: Avb,
|
||||
dm_verity_data: DmVerityData,
|
||||
ota_info: &OtaInfo,
|
||||
key_avb: &RsaSigningKey,
|
||||
cert_ota: &Certificate,
|
||||
@@ -451,7 +452,7 @@ fn create_dm_verity_image(
|
||||
fn create_vbmeta_image(
|
||||
file: &mut PSeekFile,
|
||||
name: &str,
|
||||
avb: &Avb,
|
||||
avb: Avb,
|
||||
vbmeta_data: &VbmetaData,
|
||||
inputs: &BTreeMap<String, PSeekFile>,
|
||||
key: &RsaSigningKey,
|
||||
@@ -537,7 +538,7 @@ fn create_partition_images(
|
||||
create_boot_image(
|
||||
&mut file,
|
||||
name,
|
||||
&partition.avb,
|
||||
partition.avb,
|
||||
data,
|
||||
ota_info,
|
||||
key_avb,
|
||||
@@ -550,8 +551,8 @@ fn create_partition_images(
|
||||
create_dm_verity_image(
|
||||
&mut file,
|
||||
name,
|
||||
&partition.avb,
|
||||
data,
|
||||
partition.avb,
|
||||
*data,
|
||||
ota_info,
|
||||
key_avb,
|
||||
cert_ota,
|
||||
@@ -560,7 +561,7 @@ fn create_partition_images(
|
||||
.with_context(|| format!("Failed to create dm-verity image: {name}"))?;
|
||||
}
|
||||
Data::Vbmeta(data) => {
|
||||
create_vbmeta_image(&mut file, name, &partition.avb, data, &files, key_avb)
|
||||
create_vbmeta_image(&mut file, name, partition.avb, data, &files, key_avb)
|
||||
.with_context(|| format!("Failed to create vbmeta image: {name}"))?;
|
||||
}
|
||||
}
|
||||
@@ -580,6 +581,8 @@ fn create_payload(
|
||||
key_ota: &RsaSigningKey,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(String, u64)> {
|
||||
const COMPRESSION_FACTOR: u32 = 64 * 1024;
|
||||
|
||||
let dynamic_partitions_names = partitions
|
||||
.iter()
|
||||
.filter(|(_, p)| matches!(&p.data, Data::DmVerity(_)))
|
||||
@@ -594,17 +597,23 @@ fn create_payload(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let vabc_algo = if dynamic_partitions_names.contains(name) {
|
||||
profile.vabc_algo
|
||||
let vabc_params = if dynamic_partitions_names.contains(name) {
|
||||
profile.vabc.map(|v| VabcParams {
|
||||
version: v.version,
|
||||
algo: v.algo,
|
||||
compression_factor: COMPRESSION_FACTOR,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (partition_info, operations, cow_estimate) =
|
||||
payload::compress_image(file, &writer, name, 4096, vabc_algo, cancel_signal)?;
|
||||
payload::compress_image(file, &writer, name, 4096, vabc_params, cancel_signal)?;
|
||||
|
||||
compressed.insert(name, writer);
|
||||
|
||||
let is_v3 = profile.vabc.is_some_and(|e| e.version == CowVersion::V3);
|
||||
|
||||
payload_partitions.push(PartitionUpdate {
|
||||
partition_name: name.clone(),
|
||||
run_postinstall: None,
|
||||
@@ -624,8 +633,8 @@ fn create_payload(
|
||||
fec_roots: None,
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: cow_estimate,
|
||||
estimate_op_count_max: None,
|
||||
estimate_cow_size: cow_estimate.map(|e| e.size),
|
||||
estimate_op_count_max: cow_estimate.and_then(|e| is_v3.then_some(e.num_ops)),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -645,11 +654,16 @@ fn create_payload(
|
||||
partition_names: dynamic_partitions_names,
|
||||
}],
|
||||
snapshot_enabled: Some(true),
|
||||
vabc_enabled: Some(true),
|
||||
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
|
||||
cow_version: Some(2),
|
||||
// Everything below is meant to be unset if VABC is not
|
||||
// supported.
|
||||
vabc_enabled: profile.vabc.map(|_| true),
|
||||
vabc_compression_param: profile.vabc.map(|v| v.algo.to_string()),
|
||||
cow_version: profile.vabc.map(|v| match v.version {
|
||||
CowVersion::V2 => 2,
|
||||
CowVersion::V3 => 3,
|
||||
}),
|
||||
vabc_feature_set: None,
|
||||
compression_factor: None,
|
||||
compression_factor: profile.vabc.map(|_| COMPRESSION_FACTOR.into()),
|
||||
}),
|
||||
partial_update: None,
|
||||
apex_info: vec![],
|
||||
@@ -728,14 +742,15 @@ fn create_ota(
|
||||
let mut zip_writer = match zip_mode {
|
||||
ZipMode::Streaming => {
|
||||
let signing_writer = SigningWriter::new_streaming(raw_writer);
|
||||
ZipWriter::new_streaming(signing_writer)
|
||||
ZipWriterWrapper::new_streaming(signing_writer)
|
||||
}
|
||||
ZipMode::Seekable => {
|
||||
let signing_writer = SigningWriter::new_seekable(raw_writer);
|
||||
ZipWriter::new(signing_writer)
|
||||
ZipWriterWrapper::new_seekable(signing_writer)
|
||||
}
|
||||
};
|
||||
let options = FileOptions::default()
|
||||
let options = SimpleFileOptions::default()
|
||||
.last_modified_time(DateTime::default())
|
||||
.compression_method(CompressionMethod::Stored)
|
||||
.large_file(false);
|
||||
|
||||
@@ -745,12 +760,9 @@ fn create_ota(
|
||||
|
||||
for path in [ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES] {
|
||||
// All remaining entries are written immediately.
|
||||
zip_writer
|
||||
.start_file_with_extra_data(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let offset = zip_writer
|
||||
.end_extra_data()
|
||||
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
|
||||
.start_file(path, options)
|
||||
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
|
||||
let mut writer = CountingWriter::new(&mut zip_writer);
|
||||
|
||||
match path {
|
||||
@@ -785,7 +797,7 @@ fn create_ota(
|
||||
let size = writer.stream_position()?;
|
||||
|
||||
entries.push(ZipEntry {
|
||||
name: path.to_owned(),
|
||||
path: path.to_owned(),
|
||||
offset,
|
||||
size,
|
||||
});
|
||||
@@ -851,6 +863,7 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
let raw_writer =
|
||||
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
|
||||
let mut zip_writer = ZipWriter::new(raw_writer);
|
||||
let options = SimpleFileOptions::default().last_modified_time(DateTime::default());
|
||||
|
||||
for path in [
|
||||
"assets/stub.apk",
|
||||
@@ -867,12 +880,12 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
|
||||
"lib/x86_64/libmagisk64.so",
|
||||
"lib/x86_64/libmagiskinit.so",
|
||||
] {
|
||||
zip_writer.start_file(path, FileOptions::default())?;
|
||||
zip_writer.start_file(path, options)?;
|
||||
write!(zip_writer, "dummy contents for {path}")?;
|
||||
}
|
||||
|
||||
// avbroot looks for the version number in this file.
|
||||
zip_writer.start_file("assets/util_functions.sh", FileOptions::default())?;
|
||||
zip_writer.start_file("assets/util_functions.sh", options)?;
|
||||
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
|
||||
|
||||
Ok(())
|
||||
@@ -1127,7 +1140,7 @@ fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()>
|
||||
.iter_mut()
|
||||
.find(|e| e.path == b"system/etc/security/otacerts.zip")
|
||||
{
|
||||
let mut zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let empty_zip = zip_writer.finish()?.into_inner();
|
||||
|
||||
entry.data = CpioEntryData::Data(empty_zip);
|
||||
@@ -1189,6 +1202,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
Some(_) => None,
|
||||
None => Some(TempDir::new().context("Failed to create temp directory")?),
|
||||
};
|
||||
#[allow(clippy::option_if_let_else)]
|
||||
let work_dir = match &cli.config.work_dir {
|
||||
Some(w) => w.as_path(),
|
||||
None => work_temp_dir.as_ref().unwrap().path(),
|
||||
@@ -1220,8 +1234,9 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
] {
|
||||
let _span = info_span!("profile", name, %zip_mode).entered();
|
||||
|
||||
// Can't used NamedTempFile because avbroot does atomic replaces.
|
||||
let profile_dir = work_dir.join(name);
|
||||
// Can't use NamedTempFile because avbroot does atomic replaces.
|
||||
let mut profile_dir = work_dir.join(name);
|
||||
profile_dir.push(zip_mode.to_string());
|
||||
let out_original = profile_dir.join("ota.zip");
|
||||
let out_magisk = profile_dir.join("ota_magisk.zip");
|
||||
let out_prepatched = profile_dir.join("ota_prepatched.zip");
|
||||
@@ -1324,7 +1339,7 @@ fn helper_mode() -> Result<()> {
|
||||
let cli = HelperCli::parse();
|
||||
|
||||
let private_key_path = {
|
||||
let parent = cli.public_key.parent().unwrap_or(Path::new("."));
|
||||
let parent = cli.public_key.parent().unwrap_or_else(|| Path::new("."));
|
||||
let name = cli
|
||||
.public_key
|
||||
.file_name()
|
||||
@@ -1398,7 +1413,10 @@ fn main() -> Result<()> {
|
||||
if env::var_os(ENV_HELPER_MODE).is_some() {
|
||||
return helper_mode();
|
||||
}
|
||||
env::set_var(ENV_HELPER_MODE, "true");
|
||||
// SAFETY: No multithreading at this point.
|
||||
unsafe {
|
||||
env::set_var(ENV_HELPER_MODE, "true");
|
||||
}
|
||||
|
||||
// Set up a cancel signal so we can properly clean up any temporary files.
|
||||
let cancel_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ publish = false
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.1", features = ["derive"] }
|
||||
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
|
||||
toml_edit = "0.22.9"
|
||||
toml_edit = "0.23.3"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fmt,
|
||||
fmt::{self, Write as _},
|
||||
fs::{self, File},
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use anyhow::{Result, anyhow, bail};
|
||||
use regex::Regex;
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
@@ -108,7 +108,7 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
for (link_ref, link) in links {
|
||||
result.push_str(&format!("{link_ref}: {link}\n"));
|
||||
let _ = writeln!(result, "{link_ref}: {link}");
|
||||
}
|
||||
|
||||
fs::write(path, result)?;
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::{
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use anyhow::{Result, bail};
|
||||
use clap::Parser;
|
||||
use toml_edit::{value, DocumentMut};
|
||||
use toml_edit::{DocumentMut, value};
|
||||
|
||||
use crate::WORKSPACE_DIR;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user