Compare commits

..

37 Commits

Author SHA1 Message Date
Andrew Gunnerson da124e4e05 Version 3.15.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-06 00:09:21 -04:00
Andrew Gunnerson 7b778515d1 CHANGELOG.md: Add entry for PR #439
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-06 00:02:48 -04:00
Andrew Gunnerson 98745afe33 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:54:20 -04:00
Andrew Gunnerson a47c501211 CHANGELOG.md: Add entry for PR #438
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:53:50 -04:00
Andrew Gunnerson f2e47a65d4 Switch back to ring
This reverts commit e929ecbe44.

Ring is back to being maintained again, so let's switch back to it since
it has fewer build dependencies and is much faster to compile.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:52:53 -04:00
Andrew Gunnerson e11ddd2ba7 CHANGELOG.md: Add entry for PR #437
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:38:51 -04:00
Andrew Gunnerson 5d66774d13 Add support for changing VABC CoW compression algorithm
Devices that launch with Android <14 generally use gzip as the CoW
compression algorithm. This never changes because future full OTAs
always need to be installable from the version of Android the device
launched with.

However, for users that don't care about the upgrade path from old
versions of Android, a new --vabc-algo option can be used to switch from
gz to lz4 compression. This can cut down the OTA installation time by
more than 2/3rds when installing via a custom OTA updater app. On my
Pixel Tablet, the installation time for the update_engine DOWNLOADING
phase decreased from 32:05 to 9:41. Note that this has absolutely no
effect on the performance when sideloading from recovery mode because
that does not use CoW.

When this new option is used, all dynamic partitions need to be
extracted from the OTA during patching so that the CoW estimates can be
recomputed. This will slow down the patching process and use up more
temporary disk space.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-05 23:06:13 -04:00
Andrew Gunnerson 3f09a506a0 Version 3.14.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:27:05 -04:00
Andrew Gunnerson 4664f8ea37 CHANGELOG.md: Add entry for PR #435
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:26:29 -04:00
Andrew Gunnerson 796e2a4fa2 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:17:15 -04:00
Andrew Gunnerson e6b60d5d0f CHANGELOG.md: Add entry for PR #434
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 19:02:39 -04:00
Andrew Gunnerson e8cb4a8d53 Fix incorrect compression input for gzip CoW size estimation
Instead of compressing the 64 MiB input in 2 MiB chunks, each loop
iteration was compressing the full 64 MiB. This massively slowed down
the patching process from seconds to potentially hours and would
temporarily waste a bunch of space during OTA installation.

Fixes: #433

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-23 18:50:44 -04:00
Andrew Gunnerson b2d280eb20 CHANGELOG.md: Add entry for PR #430
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 20:23:48 -04:00
Andrew Gunnerson e2dc5174b4 format/ota: Decouple OTA signature parsing from verification
This way, we can fail hard for parsing errors, but not for verification
errors in `avbroot ota verify`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 20:22:16 -04:00
Andrew Gunnerson 80c47e9a02 CHANGELOG.md: Add entry for PR #429
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:49:54 -04:00
Andrew Gunnerson 65ba3ad5cc Fix clippy 1.85 warnings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:48:32 -04:00
Andrew Gunnerson a7438876ce CHANGELOG.md: Add entry for PR #428
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:38:16 -04:00
Andrew Gunnerson c039901a85 cli/ota: Improve ota verify otacerts.zip handling
* Try to avoid fail-fast behavior to show as many errors as possible.
  Parsing errors always fail immediately, but verification errors don't.
* Move the recovery otacerts.zip check to the end to let more important
  checks run first.
* Improve error message when otacerts.zip does not contain the signing
  certificate for the OTA to make it clear the issue is not that the zip
  contains no certificates at all.
* Always run the recovery otacerts.zip check, but just log the error as
  a warning when running with --skip-recovery-ota-cert.
* Fix unformatted error context string when parsing a boot image's
  otacerts.zip file fails.

Discussion: #426

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-13 19:34:42 -04:00
Andrew Gunnerson 0cdc7172dd Version 3.13.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:39:59 -04:00
Andrew Gunnerson 1296275418 CHANGELOG.md: Add entry for PR #427
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:38:43 -04:00
Andrew Gunnerson a62d0a5c91 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:25:35 -04:00
Andrew Gunnerson de433a2724 README.md: Clarify what --skip-{system,recovery}-ota-cert affects
Discussion: #426

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-03-12 18:07:17 -04:00
Andrew Gunnerson c08af33343 CHANGELOG.md: Add entry for PR #425
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-24 21:17:44 -05:00
Andrew Gunnerson e224884591 format/lp: Relax validation checks to parse on-device super partition
There are two documented behaviors in liblp that are violated with the
on-disk super partition layout after a virtual A/B CoW merge:

1. The partition name includes a `-` due to the `-cow` suffix. This is
   not meant to be a valid character.
2. The extent list is likely to have many gaps and not be sorted. The
   format documentation says that gaps are not allowed.

This commit updates avbroot's LP parser to be less strict so that it can
load real on-device super partitions. The extent allocator for the
`pack` subcommand remains unchanged though, so avbroot will always
produce LP images with sorted, gapless extents.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-24 21:05:28 -05:00
Andrew Gunnerson a525fc4550 CHANGELOG.md: Add entry for PR #424
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:34:30 -05:00
Andrew Gunnerson 8c6b0fdfc5 cli/ota: Discard unmodified system image temp file earlier
This is the same optimization as is currently done for boot images.
There's no reason to keep the temp file around for the entire patching
process if it's unmodified and we're not going to be copying its data
into the payload.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:23:10 -05:00
Andrew Gunnerson 36d4ed19ad CHANGELOG.md: Add entry for PR #423
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:18:12 -05:00
Andrew Gunnerson f9b06b33e6 cli/ota: Fix incorrect unprotected partition warning with --skip-system-ota-cert
The filtering out of partitions was done at the wrong scope, causing
avbroot to warn that extracted-but-unmodified partitions were not
protected by AVB. We never encountered this before because the system
image was always patched and unmodified boot images got filtered out at
an earlier phase during patching.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:00:59 -05:00
Andrew Gunnerson 620c873be5 CHANGELOG.md: Add entry for PR #422
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 21:00:20 -05:00
Andrew Gunnerson e929ecbe44 Replace ring with aws-lc
The author of ring recently announced that the library is no longer
being maintained and fixes for security issues may be significantly
delayed. Big thanks to Brian Smith for creating and maintaining the
library for so long!

This commit replaces ring with aws-lc, a cryptography library maintained
by Amazon AWS. It seems to be well-regarded and is used by high-profile
projects like rustls. It is also API-compatible with ring, so it is
effectively a drop-in replacement.

Unfortunately, we still cannot switch back to the RustCrypto SHA1 and
SHA2 implementations because they are still significantly slower than
ring and aws-lc on systems that do not support the SHA-NI extensions.

https://rustsec.org/advisories/RUSTSEC-2025-0007

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 20:44:19 -05:00
Andrew Gunnerson 31685713ef CHANGELOG.md: Add entry for PR #421
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 19:33:58 -05:00
Andrew Gunnerson 0dab7aa32c Switch to stable bzip2-rs release and use zlib-rs flate2 backend
* There is now a stable release of bzip2-rs with the fix for both the C
  and Rust versions of bzip2 being compiled.

* The zlib-rs deflate implementation is faster than the default
  miniz_oxide. Changing this requires updating the checksums in the e2e
  tests due to slight differences in compression levels between the two
  implementations.

* Temporarily silence RUSTSEC-2025-0007 to avoid blocking CI. The ring
  library is no longer maintained.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-21 19:27:18 -05:00
Ivan ba6c1b1400 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/c198646c7c9b3eb8013dca43df6df8502e619e3c
* https://github.com/chenxiaolong/avbroot/commit/d4eb231dd49e7e6c7320135785cde7cd57634a50
* https://github.com/chenxiaolong/avbroot/commit/1484cd47c354197e1179a554f9f001020297aac8
* https://github.com/chenxiaolong/avbroot/commit/84fa6c6bc63b62d1b3b96dcb50b18b13dea0076f

Signed-off-by: Ivan <reddxae@proton.me>
2025-02-11 14:20:11 +03:00
Andrew Gunnerson 1ecbf1144d CHANGELOG.md: Add entry for PR #418
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-09 21:59:13 -05:00
Andrew Gunnerson 84fa6c6bc6 Add option to skip replacing OTA cert in system image
This is analogous to the existing --skip-recovery-ota-cert option,
except for the system image.

Discussion: #417

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-09 21:42:18 -05:00
Andrew Gunnerson 15b7db4631 CHANGELOG.md: Add entry for PR #415
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-04 00:16:38 -05:00
Andrew Gunnerson 1d9c1574da lp: Stop checking for zeroed first block
AOSP says that for non-empty images, the first 4 KiB block is supposed
to be filled with zeros to prevent it from being interpreted as an old
BIOS boot sector. The previous implementation relied on that to
distinguish between empty and non-empty images. However, Samsung decided
to use this region for their own SignerVer02 structure, so the heuristic
doesn't work.

AOSP's liblp tries to parse the input file as an empty image before
falling back to parsing as a normal image. We'll do the same.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-04 00:13:27 -05:00
22 changed files with 1170 additions and 631 deletions
+3 -4
View File
@@ -34,8 +34,7 @@ jobs:
- aarch64-apple-darwin
- x86_64-apple-darwin
combine: lipo
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
- os: ubuntu-24.04
- os: ubuntu-latest
name: aarch64-linux-android31
targets:
- aarch64-linux-android
@@ -84,7 +83,7 @@ jobs:
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@f0deed1e0edfc6a9be95417288c0e1099b1eeec3 # v2.7.7
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:
key: ${{ matrix.artifact.name }}
@@ -155,7 +154,7 @@ jobs:
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
+1 -1
View File
@@ -13,4 +13,4 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@e2f4ede4a4e60ea15ff31bc0647485d80c66cfba # v2.0.4
uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
+47
View File
@@ -7,6 +7,34 @@
to update the actual links at the bottom of the file.
-->
### 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](#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])
* Fix new clippy warnings introduced in Rust 1.85 ([PR #429])
* Fix massive performance regression introduced in 3.13.0 for OTAs that use gzip for virtual A/B CoW compression ([Issue #433], [PR #434])
* Update dependencies ([PR #435])
### Version 3.13.0
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
* The ring library is no longer maintained
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
* Update dependencies ([PR #427])
### Version 3.12.0
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
@@ -280,6 +308,8 @@ Behind-the-scenes changes:
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
@@ -306,6 +336,7 @@ Behind-the-scenes changes:
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
[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
[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
@@ -444,3 +475,19 @@ Behind-the-scenes changes:
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
[PR #428]: https://github.com/chenxiaolong/avbroot/pull/428
[PR #429]: https://github.com/chenxiaolong/avbroot/pull/429
[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
Generated
+258 -197
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.12.0"
version = "3.15.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+32 -3
View File
@@ -384,11 +384,28 @@ Note that avbroot will validate that the prepatched image is compatible with the
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
### Skipping recovery OTA certificate patches
### Skipping OTA certificate patches
avbroot can skip modifying `otacerts.zip` in the recovery image with the `--skip-recovery-ota-cert` option. **Do not do this unless you have a good reason to do so.** (For example, if you've already manually inserted the OTA certificate into a boot image specified with `--prepatched` or `--replace`.) When this option is used with `--rootless` (and `--dsu` is not specified), then no modifications are performed on any boot image besides ensuring they are properly signed.
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
When manually adding the OTA certificate to a boot image, [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done.
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
### Skipping all patches
To have avbroot make the absolute minimal changes:
* Specify `--skip-system-ota-cert`
* Specify `--skip-recovery-ota-cert`
* Specify `--rootless`
* Omit `--dsu`
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
### Replacing partitions
@@ -414,6 +431,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:
+63 -7
View File
@@ -295,6 +295,7 @@ Magisk версии 25211 и новее требует наличие разде
--input /path/to/ota.zip \
--directory . \
--boot-only
--partition <название раздела> # init_boot или boot, в зависимости от устройства
```
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
@@ -381,11 +382,16 @@ avbroot может подменить используемый загрузоч
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
### Пропуск патчинга сертификата OTA в разделе Recovery
### Пропуск патчинга сертификата OTA
avbroot может пропустить изменение файла `otacerts.zip` в разделе Recovery с помощью опции `--skip-recovery-ota-cert`. **Не используйте эту функцию, если на то нет веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный образ (`boot.img`) и передаете его программе через опции `--prepatched` или `--replace`.) Если эта опция применяется совместно с `--rootless` (и без указания параметра `--dsu`), то в загрузочный образ не будут внесены никакие изменения, кроме обеспечения его корректной подписи.
avbroot может пропускать изменение `otacerts.zip` с помощью аргументов `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный (boot) образ и передаете его программе через опции `--prepatched` или `--replace`.)
В противном случае, на устройстве может не остаться возможности устанавливать дальнейшие обновления.
Если вы вручную добавили сертификат OTA в загрузочный образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
При использовании `--skip-system-ota-cert`, никаких изменений в образ `system` не вносится.
При использовании `--skip-recovery-ota-cert` совместно с `--rootless` и без указания `--dsu`, не вносится никаких изменений в загрузочные образы, кроме обеспечения их корректной подписи.
Если вы вручную добавили сертификат OTA в загрузочный (boot) образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
### Подмена разделов
@@ -441,17 +447,20 @@ Verified boot is disabled by vbmeta's header flags: 0x3
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
### Извлечение всей OTA
### Извлечение образов из OTA
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
Чтобы извлечь образы разделов, содержащихся в `payload.bin`, используйте команду:
```bash
avbroot ota extract \
--input /путь/к/ota.zip \
--directory extracted \
--all
--directory extracted
```
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
### Режим записи ZIP
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
@@ -480,6 +489,53 @@ avbroot поддерживает делегирование всех опера
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
### Размер страницы 16 КБ в настройках для разработчиков
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
3. Переподпишите `payload.bin` вашим OTA-ключом:
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
```bash
avbroot ota patch \
--replace vendor <модифицированный vendor.img> \
<дальше указываются аргументы, как при обычном патчинге>
```
## Сборка из исходного кода
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
+8 -12
View File
@@ -13,28 +13,31 @@ anyhow = "1.0.75"
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"] }
cap-std = "3.0.0"
cap-tempfile = "3.0.0"
clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
# We can't upgrade to 0.10.0 until x509-cert updates it too, since it's part of
# the public API.
const-oid = "0.9.5"
crc32fast = "1.4.2"
ctrlc = "3.4.0"
dlv-list = "0.6.0"
flate2 = "1.0.27"
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"
lz4_flex = "0.11.1"
memchr = "2.6.0"
miniz_oxide = "0.8.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
passterm = "2.0.3"
phf = { version = "0.11.2", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.13.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"] }
@@ -42,7 +45,7 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
# 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.0"
ring = "0.17.14"
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
serde = { version = "1.0.188", features = ["derive"] }
sha1 = "0.10.5"
@@ -57,13 +60,6 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
# Waiting for next stable release.
[dependencies.bzip2]
git = "https://github.com/trifectatechfoundation/bzip2-rs"
rev = "09a87db73c0517a9715ab3fd96fbe4961d545aee"
default-features = false
features = ["libbz2-rs-sys"]
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
@@ -73,10 +69,10 @@ features = ["deflate"]
[target.'cfg(unix)'.dependencies]
libc = "0.2.158"
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
[build-dependencies]
constcat = "0.5.0"
constcat = "0.6.0"
prost-build = "0.13.1"
protox = "0.7.0"
+412 -170
View File
@@ -14,13 +14,14 @@ use std::{
};
use anyhow::{anyhow, bail, Context, Result};
use bitflags::bitflags;
use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempDir;
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
use clap::{value_parser, ArgAction, Args, Parser, Subcommand, ValueEnum};
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
use tempfile::NamedTempFile;
use topological_sort::TopologicalSort;
use tracing::{debug_span, info, warn};
use tracing::{debug_span, error, info, warn};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
@@ -72,48 +73,65 @@ fn sorted<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
items
}
pub struct RequiredImages(HashSet<String>);
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;
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)
const KNOWN = Self::BOOT.bits() | Self::SYSTEM.bits() | Self::VBMETA.bits();
}
pub fn is_boot(name: &str) -> bool {
name == "boot" || name == "init_boot" || name == "recovery" || name == "vendor_boot"
#[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;
}
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);
}
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))
}
result
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -134,7 +152,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 +162,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 +176,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 +192,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,15 +208,19 @@ 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: {}",
@@ -234,16 +256,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 +338,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.
@@ -373,8 +403,9 @@ fn get_vbmeta_patch_order(
.get_mut(vbmeta_name.as_str())
.unwrap()
.insert(partition_name.to_owned());
missing.remove(partition_name);
}
missing.remove(partition_name);
}
}
@@ -545,6 +576,58 @@ fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header
}
}
/// Get the VABC algorithm from the payload header. This will fail if an
/// unsupported VABC algorithm is specified, but not if VABC is disabled.
fn get_vabc_algo(header: &PayloadHeader) -> Result<Option<VabcAlgo>> {
// 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 = dpm.cow_version();
if dpm.cow_version() != 2 {
bail!("Unsupported CoW version: {cow_version}");
}
let compression = dpm.vabc_compression_param();
let Ok(vabc_algo) = VabcAlgo::from_str(compression, false) else {
bail!("Unsupported VABC compression: {compression}");
};
Ok(Some(vabc_algo))
}
/// 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, false) 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
@@ -565,6 +648,11 @@ fn update_vbmeta_headers(
key: &RsaSigningKey,
block_size: u64,
) -> Result<()> {
info!(
"Patching vbmeta images: {}",
joined(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();
@@ -592,7 +680,12 @@ fn update_vbmeta_headers(
// Only sign and rewrite the image if we need to. Some vbmeta images may
// have no dependencies and are only being processed to ensure that the
// flags are set to a sane value.
if parent_header != &orig_parent_header {
//
// The root vbmeta image is always signed because it is possible to
// invoke avbroot is a way that no modifications are made (rootless +
// skipping recovery otacerts.zip patch). We still want the result to be
// bootable.
if parent_header != &orig_parent_header || name == "vbmeta" {
parent_header
.set_algo_for_key(key)
.with_context(|| format!("Failed to set signature algorithm: {name}"))?;
@@ -638,6 +731,7 @@ pub fn compress_image(
.map(PSeekFile::new)
.with_context(|| format!("Failed to create temp file for: {name}"))?;
let vabc_algo = get_vabc_algo(header)?;
let block_size = header.manifest.block_size();
let partition = header
.manifest
@@ -646,6 +740,20 @@ 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_algo = if partition.estimate_cow_size.is_some() {
let Some(vabc_algo) = vabc_algo else {
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
};
info!("Needs updated {vabc_algo} CoW size estimate: {name}");
Some(vabc_algo)
} else {
None
};
if let Some(r) = ranges {
info!("Compressing partial image: {name}: {r:?}");
@@ -659,7 +767,23 @@ 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_algo) = vabc_algo {
let cow_estimate = payload::compute_cow_estimate(
&*file,
name,
block_size,
vabc_algo,
cancel_signal,
)?;
partition.estimate_cow_size = Some(cow_estimate);
}
*file = writer;
return Ok(indices);
}
// If we can't take advantage of the optimization, we can still
@@ -673,37 +797,6 @@ 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() {
info!("Needs updated CoW size estimate: {name}");
// 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}");
};
Some(vabc_algo)
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(&*file, &writer, name, block_size, vabc_algo, cancel_signal)?;
@@ -717,13 +810,54 @@ 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_algo = get_vabc_algo(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_algo) = vabc_algo else {
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
};
info!("Recomputing {vabc_algo} CoW size estimate: {name}");
let cow_estimate =
payload::compute_cow_estimate(&*file, name, block_size, vabc_algo, cancel_signal)?;
partition.estimate_cow_size = Some(cow_estimate);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn patch_ota_payload(
payload: &(dyn ReadSeekReopen + Sync),
writer: 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>,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
@@ -735,6 +869,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
@@ -750,11 +894,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
@@ -777,18 +927,17 @@ fn patch_ota_payload(
cancel_signal,
)?;
// Main patching operation is done. Unmodified boot images no longer need to
// be kept around.
input_files
.retain(|n, f| !(f.state == InputFileState::Extracted && RequiredImages::is_boot(n)));
let (system_target, system_ranges) = patch_system_image(
&required_images,
&mut input_files,
cert_ota,
key_avb,
cancel_signal,
)?;
let system_result = if skip_system_ota_cert {
None
} else {
Some(patch_system_image(
&required_images,
&mut input_files,
cert_ota,
key_avb,
cancel_signal,
)?)
};
let mut vbmeta_headers = load_vbmeta_images(&mut input_files, &vbmeta_images)?;
@@ -796,11 +945,6 @@ fn patch_ota_payload(
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,
@@ -810,7 +954,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
@@ -822,8 +978,12 @@ fn patch_ota_payload(
&mut header,
// We can only perform the optimization of avoiding
// recompression if the image came from the original payload.
if name == system_target && !external_images.contains_key(&name) {
Some(&system_ranges)
if let Some((system_target, system_ranges)) = &system_result {
if name == *system_target && !external_images.contains_key(&name) {
Some(system_ranges)
} else {
None
}
} else {
None
},
@@ -915,7 +1075,9 @@ fn patch_ota_zip(
mut zip_writer: &mut ZipWriter<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,
@@ -1037,7 +1199,9 @@ fn patch_ota_zip(
&mut writer,
external_images,
boot_patchers,
skip_system_ota_cert,
clear_vbmeta_flags,
vabc_algo_override,
key_avb,
key_ota,
cert_ota,
@@ -1305,6 +1469,10 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
assert!(cli.root.rootless);
};
if cli.skip_system_ota_cert {
warn!("Not inserting OTA cert into system image; sideloading further updates may fail");
}
if cli.skip_recovery_ota_cert {
warn!("Not inserting OTA cert into recovery image; sideloading further updates may fail");
} else {
@@ -1347,7 +1515,9 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
&mut zip_writer,
&external_images,
&boot_patchers,
cli.skip_system_ota_cert,
cli.clear_vbmeta_flags,
cli.vabc_algo,
cli.zip_mode,
&key_avb,
&key_ota,
@@ -1469,22 +1639,20 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
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 {
info!("Extracting embedded OTA certificate from zip signature");
let ota_sig = ota::parse_ota_sig(&mut raw_reader)?;
let embedded_cert = ota_sig.embedded_cert()?;
crypto::write_pem_cert_file(path, embedded_cert)
crypto::write_pem_cert_file(path, &ota_sig.cert)
.with_context(|| format!("Failed to write OTA certificate: {path:?}"))?;
}
@@ -1667,6 +1835,15 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
}
pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut errors = 0;
macro_rules! fail_later {
($($arg:tt)+) => {
error!($($arg)+);
errors += 1;
};
}
let raw_reader = File::open(&cli.input)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
@@ -1674,27 +1851,38 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
info!("Verifying whole-file signature");
let embedded_cert = ota::verify_ota(&mut reader, cancel_signal)?;
let ota_sig = ota::parse_ota_sig(&mut reader).context("Failed to parse OTA signature")?;
let (metadata, ota_cert, header, properties) = ota::parse_zip_ota_info(&mut reader)?;
if embedded_cert != ota_cert {
bail!(
"CMS embedded certificate does not match {}",
if let Err(e) = ota_sig
.verify_ota(&mut reader, cancel_signal)
.context("Failed to verify OTA against embedded certificate")
{
fail_later!("{e:?}");
}
let (metadata, ota_cert, header, properties) =
ota::parse_zip_ota_info(&mut reader).context("Failed to parse OTA metadata")?;
if ota_cert != ota_sig.cert {
fail_later!(
"{} does not match CMS embedded certificate",
ota::PATH_OTACERT,
);
} else if let Some(p) = &cli.cert_ota {
let verify_cert = crypto::read_pem_cert_file(p)
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
if embedded_cert != verify_cert {
bail!("OTA has a valid signature, but was not signed with: {p:?}");
if ota_sig.cert != verify_cert {
fail_later!("OTA has a valid signature, but was not signed with: {p:?}");
}
} else {
warn!("Whole-file signature is valid, but its trust is unknown");
}
ota::verify_metadata(&mut reader, &metadata, header.blob_offset)
.context("Failed to verify OTA metadata offsets")?;
if let Err(e) = ota::verify_metadata(&mut reader, &metadata, header.blob_offset)
.context("Failed to verify OTA metadata offsets")
{
fail_later!("{e:?}");
}
info!("Verifying payload");
@@ -1712,7 +1900,12 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)
.context("Failed to directly open payload section")?;
payload::verify_payload(section_reader, &ota_cert, &properties, cancel_signal)?;
if let Err(e) =
payload::verify_payload(section_reader, &ota_sig.cert, &properties, cancel_signal)
.context("Failed to verify payload signatures and digests")
{
fail_later!("{e:?}");
}
info!("Extracting partition images to temporary directory");
@@ -1739,40 +1932,8 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
info!("Verifying partition hashes");
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
if cli.skip_recovery_ota_cert {
warn!("Not verifying recovery ramdisk's otacerts.zip");
} else {
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 targets = OtaCertPatcher::new(ota_cert.clone())
.find_targets(&boot_images, cancel_signal)
.context("Failed to find boot image containing otacerts.zip")?;
if targets.is_empty() {
bail!("No boot image contains otacerts.zip");
}
for target in targets {
let boot_image = &boot_images[target].boot_image;
let ramdisk_certs = OtaCertPatcher::get_certificates(boot_image, cancel_signal)
.context("Failed to read {target}'s otacerts.zip")?;
if !ramdisk_certs.contains(&ota_cert) {
bail!("{target}'s otacerts.zip does not contain OTA certificate");
}
}
if let Err(e) = verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal) {
fail_later!("{e:?}");
}
info!("Verifying AVB signatures");
@@ -1790,16 +1951,77 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
cli::avb::verify_headers(
if let Err(e) = cli::avb::verify_headers(
&temp_dir,
"vbmeta",
public_key.as_ref(),
&mut seen,
&mut descriptors,
)?;
cli::avb::verify_descriptors(&temp_dir, &descriptors, false, cancel_signal)?;
)
.context("Failed to verify AVB signatures")
{
fail_later!("{e:?}");
}
info!("Signatures are all valid!");
if let Err(e) = cli::avb::verify_descriptors(&temp_dir, &descriptors, false, cancel_signal)
.context("Failed to verify images against AVB descriptors")
{
fail_later!("{e:?}");
}
info!("Checking recovery ramdisk's otacerts.zip");
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")?;
if targets.is_empty() {
let msg = "No boot image contains otacerts.zip";
if cli.skip_recovery_ota_cert {
warn!("{msg}");
} else {
fail_later!("{msg}");
}
}
for target in targets {
let boot_image = &boot_images[target].boot_image;
let ramdisk_certs = OtaCertPatcher::get_certificates(boot_image, cancel_signal)
.with_context(|| format!("Failed to read {target}'s otacerts.zip"))?;
if !ramdisk_certs.contains(&ota_cert) {
let msg = format!(
"{target}'s otacerts.zip does not contain the certificate that signed the OTA"
);
if cli.skip_recovery_ota_cert {
warn!("{msg}");
} else {
fail_later!("{msg}");
}
}
}
if errors == 0 {
info!("OK!");
} else {
bail!("Encountered {errors} error(s) during verification");
}
Ok(())
}
@@ -1981,11 +2203,19 @@ pub struct PatchCli {
)]
pub ignore_prepatched_compat: u8,
/// Skip adding OTA certificate to system image.
///
/// DO NOT USE THIS unless you've manually added the certificate to the
/// system image already. Otherwise, installing further updates via a custom
/// OTA updater app while booted into Android will not be possible.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_system_ota_cert: bool,
/// Skip adding OTA certificate to recovery image.
///
/// DO NOT USE THIS unless you've manually added the certificate to the
/// recovery image already. Otherwise, sideloading further updates will not
/// be possible.
/// recovery image already. Otherwise, sideloading further updates while
/// booted into recovery mode will not be possible.
///
/// When this option is used with --rootless, the boot images in the OTA
/// will not be modified.
@@ -2000,6 +2230,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.
+2 -2
View File
@@ -122,13 +122,13 @@ fn find_allocated_regions(
loop {
stream::check_cancel(cancel_signal)?;
start = match rustix::fs::seek(reader, SeekFrom::Data(end as i64)) {
start = match rustix::fs::seek(reader, SeekFrom::Data(end)) {
Ok(offset) => offset,
Err(e) if e == Errno::NXIO => break,
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
};
end = rustix::fs::seek(reader, SeekFrom::Hole(start as i64))
end = rustix::fs::seek(reader, SeekFrom::Hole(start))
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
result.push(start..end);
+13 -14
View File
@@ -163,7 +163,7 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
pub(crate) fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
pub(crate) fn digest_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
match name {
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
"sha256" => Ok(&ring::digest::SHA256),
@@ -292,7 +292,7 @@ trait DescriptorTag {
/// Raw on-disk layout for the AVB property descriptor after the prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawPropertyDescriptor {
key_size: big_endian::U64,
value_size: big_endian::U64,
@@ -395,7 +395,7 @@ impl<W: Write> ToWriter<W> for PropertyDescriptor {
/// Raw on-disk layout for the AVB hash tree descriptor after the prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHashTreeDescriptor {
dm_verity_version: big_endian::U32,
image_size: big_endian::U64,
@@ -534,7 +534,7 @@ impl HashTreeDescriptor {
ranges: Option<&[Range<u64>]>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
let algorithm = digest_algorithm(&self.hash_algorithm, false)?;
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 = ring_algorithm(&self.hash_algorithm, true)?;
let algorithm = digest_algorithm(&self.hash_algorithm, true)?;
util::check_bounds(self.tree_size, ..=HASH_TREE_MAX_SIZE)
.map_err(|e| Error::IntOutOfBounds("HashTree::tree_size", e))?;
@@ -860,7 +860,7 @@ impl<W: Write> ToWriter<W> for HashTreeDescriptor {
/// Raw on-disk layout for the AVB hash descriptor after the prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHashDescriptor {
image_size: big_endian::U64,
hash_algorithm: [u8; 32],
@@ -906,7 +906,7 @@ impl HashDescriptor {
for_verify: bool,
cancel_signal: &AtomicBool,
) -> Result<ring::digest::Digest> {
let algorithm = ring_algorithm(&self.hash_algorithm, for_verify)?;
let algorithm = digest_algorithm(&self.hash_algorithm, for_verify)?;
let mut context = Context::new(algorithm);
context.update(&self.salt);
@@ -1065,7 +1065,7 @@ impl<W: Write> ToWriter<W> for HashDescriptor {
/// Raw on-disk layout for the AVB kernel command line descriptor after the
/// prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawKernelCmdlineDescriptor {
flags: big_endian::U32,
cmdline_len: big_endian::U32,
@@ -1139,7 +1139,7 @@ impl<W: Write> ToWriter<W> for KernelCmdlineDescriptor {
/// Raw on-disk layout for the AVB chain partition descriptor after the prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawChainPartitionDescriptor {
rollback_index_location: big_endian::U32,
partition_name_len: big_endian::U32,
@@ -1258,7 +1258,7 @@ impl<W: Write> ToWriter<W> for ChainPartitionDescriptor {
/// Raw on-disk layout for the AVB descriptor prefix.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawDescriptor {
tag: big_endian::U64,
num_bytes_following: big_endian::U64,
@@ -1458,7 +1458,7 @@ impl<'a> TryFrom<&'a mut Descriptor> for AppendedDescriptorMut<'a> {
/// Raw on-disk layout for the AVB header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HEADER_MAGIC`].
magic: [u8; 4],
@@ -1927,7 +1927,7 @@ impl<W: Write> ToWriter<W> for Header {
/// Raw on-disk layout for the AVB footer.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawFooter {
/// Magic value. This should be equal to [`FOOTER_MAGIC`].
magic: [u8; 4],
@@ -2015,10 +2015,9 @@ impl<W: Write> ToWriter<W> for Footer {
/// Raw on-disk layout for the AVB binary public key header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawPublicKey {
key_num_bits: big_endian::U32,
#[expect(unused)]
n0inv: big_endian::U32,
}
+8 -8
View File
@@ -117,7 +117,7 @@ pub trait BootImageExt {
/// Raw on-disk layout for the v0 image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawV0 {
/// Magic value. This should be equal to [`BOOT_MAGIC`].
magic: [u8; 8],
@@ -139,7 +139,7 @@ struct RawV0 {
/// Raw on-disk layout for the extra v1 image header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawV1Extra {
recovery_dtbo_size: little_endian::U32,
recovery_dtbo_offset: little_endian::U64,
@@ -148,7 +148,7 @@ struct RawV1Extra {
/// Raw on-disk layout for the extra v2 image header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawV2Extra {
dtb_size: little_endian::U32,
dtb_addr: little_endian::U64,
@@ -595,7 +595,7 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
/// Raw on-disk layout for the v3 image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawV3 {
/// Magic value. This should be equal to [`BOOT_MAGIC`].
magic: [u8; 8],
@@ -610,7 +610,7 @@ struct RawV3 {
/// Raw on-disk layout for the extra v4 image header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawV4Extra {
signature_size: little_endian::U32,
}
@@ -961,7 +961,7 @@ impl<W: Write> ToWriter<W> for BootImageV3Through4 {
/// Raw on-disk layout for the vendor v3 image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawVendorV3 {
/// Magic value. This should be equal to [`VENDOR_BOOT_MAGIC`].
magic: [u8; 8],
@@ -980,7 +980,7 @@ struct RawVendorV3 {
/// Raw on-disk layout for the extra vendor v4 image header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawVendorV4Extra {
vendor_ramdisk_table_size: little_endian::U32,
vendor_ramdisk_table_entry_num: little_endian::U32,
@@ -990,7 +990,7 @@ struct RawVendorV4Extra {
/// Raw on-disk layout for the vendor v4 ramdisk table entry.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawVendorV4RamdiskTableEntry {
ramdisk_size: little_endian::U32,
ramdisk_offset: little_endian::U32,
+2 -2
View File
@@ -76,7 +76,7 @@ pub struct InvalidHexCharError(RawHexU32, char);
/// ASCII-encoded hex integer value used in cpio header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHexU32([u8; 8]);
impl fmt::Debug for RawHexU32 {
@@ -121,7 +121,7 @@ impl TryFrom<RawHexU32> for u32 {
/// Raw on-disk layout for the cpio header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
magic: [u8; 6],
+1 -1
View File
@@ -592,7 +592,7 @@ impl Fec {
/// Raw on-disk layout for the FEC image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`FEC_MAGIC`].
magic: little_endian::U32,
+9 -9
View File
@@ -431,7 +431,7 @@ impl HashTree {
/// Raw on-disk layout for our custom hash tree image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
magic: [u8; 16],
@@ -492,8 +492,8 @@ impl HashTreeImage {
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
const VERSION: u16 = 1;
fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::ring_algorithm(name, false)
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::digest_algorithm(name, false)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
}
@@ -509,8 +509,8 @@ impl HashTreeImage {
.reopen_boxed()
.and_then(|mut f| f.seek(SeekFrom::End(0)))
.map_err(Error::InputReopen)?;
let ring_algorithm = Self::ring_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
let digest_algorithm = Self::digest_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
Ok(Self {
@@ -530,8 +530,8 @@ impl HashTreeImage {
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
self.root_digest = hash_tree.update(
input,
@@ -550,8 +550,8 @@ impl HashTreeImage {
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
hash_tree.verify(
input,
+55 -57
View File
@@ -1,8 +1,7 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
fmt,
io::{self, Read, Seek, Write},
mem,
@@ -152,10 +151,8 @@ pub enum Error {
ExtentTypeZeroNotEmpty { index: usize },
#[error("Extent #{index}: Invalid type: {extent_type}")]
ExtentInvalidType { index: usize, extent_type: u32 },
#[error("Extent #{index}: Overlaps previous extent")]
ExtentOverlapsPrevious { index: usize },
#[error("Extent #{index}: Earlier block device index than previous extent")]
ExtentDeviceNotConsecutive { index: usize },
#[error("Extent #{index}: Overlaps another extent: #{other}")]
ExtentOverlapsAnother { index: usize, other: usize },
#[error("Extent #{index}: Block device index too large")]
ExtentDeviceIndexTooLarge { index: usize },
// Partition group errors.
@@ -253,7 +250,7 @@ impl PartitionAttributes {
/// Raw on-disk layout for the metadata geometry.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawGeometry {
/// Magic value. This should be equal to [`GEOMETRY_MAGIC`].
magic: little_endian::U32,
@@ -335,7 +332,7 @@ impl RawGeometry {
/// Raw on-disk layout for a table descriptor within a [`RawHeader`].
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawTableDescriptor {
/// Offset relative to the end of the [`RawHeader`].
offset: little_endian::U32,
@@ -394,7 +391,7 @@ impl RawTableDescriptor {
/// Raw on-disk layout for the metadata header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HEADER_MAGIC`].
magic: little_endian::U32,
@@ -572,7 +569,7 @@ impl RawHeader {
/// A potentially invalid raw partition name string.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct PartitionName([u8; 36]);
impl fmt::Debug for PartitionName {
@@ -598,17 +595,15 @@ impl PartitionName {
fn validate(&self) -> Result<()> {
let (prefix, suffix) = self.split();
let mut has_alnum = false;
for b in prefix {
match b {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => has_alnum = true,
b'_' => {}
_ => return Err(Error::PartitionNameInvalid(DebugString::new(self))),
}
}
// AOSP liblp's metadata_format.h says "Characters may only be
// alphanumeric or _", but AOSP creates partitions named like
// "system_b-cow".
let prefix_valid = prefix
.iter()
.all(|b| matches!(*b, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-'));
if has_alnum && is_zero(suffix) {
if prefix_valid && is_zero(suffix) {
Ok(())
} else {
Err(Error::PartitionNameInvalid(DebugString::new(self)))
@@ -644,7 +639,7 @@ impl FromStr for PartitionName {
/// Raw on-disk layout for an entry in the logical partitions table.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawPartition {
/// Partition name in ASCII. This must be unique across all partitions.
name: PartitionName,
@@ -706,7 +701,7 @@ impl RawPartition {
.first_extent_index
.get()
.checked_add(self.num_extents.get())
.map_or(true, |n| n as usize > extents.len())
.is_none_or(|n| n as usize > extents.len())
{
return Err(Error::PartitionExtentIndicesTooLarge {
name: DebugString::new(self.name),
@@ -735,7 +730,7 @@ impl RawPartition {
/// Raw on-disk layout for an entry in the extent table.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawExtent {
/// Number of [`SECTOR_SIZE`]-byte sectors in this extent.
num_sectors: little_endian::U64,
@@ -827,7 +822,7 @@ impl RawExtent {
/// Raw on-disk layout for an entry in the partition groups table.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawPartitionGroup {
/// Partition group name in ASCII. This must be unique across all groups.
name: PartitionName,
@@ -894,7 +889,7 @@ impl RawPartitionGroup {
/// Raw on-disk layout for an entry in the block devices table.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawBlockDevice {
/// The first [`SECTOR_SIZE`]-byte sector where actual data for the logical
/// partitions can be allocated.
@@ -1010,23 +1005,29 @@ impl RawMetadataSlot {
extent.validate(i, &self.block_devices)?;
}
// Ensure that all extents are in increasing order and not overlapping.
let mut iter = self
// Ensure that all extents are not overlapping. We have to sort here
// because the extents may not be in order when loading the super
// partition on an actual device. Also, AOSP liblp's `metadata_format.h`
// says "Gaps between extents are not allowed", but AOSP frequently
// creates this situation after a virtual A/B CoW merge.
let mut sorted_extents = self
.extents
.iter()
.filter(|e| e.target_type.get() == RawExtent::TARGET_TYPE_LINEAR)
.enumerate();
while let (Some((_, a)), Some((i, b))) = (iter.next(), iter.next()) {
match a.target_source.get().cmp(&b.target_source.get()) {
Ordering::Equal => {
if a.target_data.get() + a.num_sectors.get() > b.target_data.get() {
return Err(Error::ExtentOverlapsPrevious { index: i });
}
}
Ordering::Greater => {
return Err(Error::ExtentDeviceNotConsecutive { index: i });
}
Ordering::Less => {}
.enumerate()
.filter(|(_, e)| e.target_type.get() == RawExtent::TARGET_TYPE_LINEAR)
.collect::<Vec<_>>();
sorted_extents.sort_by_key(|(_, e)| (e.target_source, e.target_data));
for window in sorted_extents.windows(2) {
let ((a_i, a), (b_i, b)) = (window[0], window[1]);
if a.target_source == b.target_source
&& a.target_data.get() + a.num_sectors.get() > b.target_data.get()
{
return Err(Error::ExtentOverlapsAnother {
index: b_i,
other: a_i,
});
}
}
@@ -1060,14 +1061,19 @@ impl RawMetadata {
.read_exact(&mut buf)
.map_err(|e| Error::DataRead("geometry", e))?;
let image_type = if util::is_zero(&buf) {
ImageType::Normal
} else {
ImageType::Empty
};
// For non-empty images, AOSP says the first block is supposed to be
// filled with zeros, but Samsung puts their own SignerVer02 structure
// in there, so we can't rely on that.
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
let geometry = match image_type {
ImageType::Normal => {
let image_type = match geometry.validate() {
Ok(()) => {
// This is an empty image for use with fastboot. These have no
// extra padding at the beginning of the file nor backup copies
// of the geometry and metadata structs.
ImageType::Empty
}
Err(Error::GeometryInvalidMagic(_)) => {
// This is an normal non-empty image, which has extra padding at
// the beginning to avoid having the geometry struct interpreted
// as a boot sector.
@@ -1077,7 +1083,7 @@ impl RawMetadata {
.read_exact(&mut buf)
.map_err(|e| Error::DataRead("geometry_primary", e))?;
let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
if geometry.validate().is_ok() {
// Skip the backup copy.
@@ -1094,17 +1100,9 @@ impl RawMetadata {
geometry.validate()?;
}
geometry
}
ImageType::Empty => {
// This is an empty image for use with fastboot. These have no
// extra padding at the beginning of the file nor backup copies
// of the geometry and metadata structs.
let geometry = RawGeometry::ref_from_prefix(&buf).unwrap().0;
geometry.validate()?;
geometry
ImageType::Normal
}
Err(e) => return Err(e),
};
Ok((image_type, geometry.to_owned()))
+109 -72
View File
@@ -15,7 +15,7 @@ use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
use memchr::memmem;
use prost::Message;
use ring::digest::Context;
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use x509_cert::{der::Encode, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
@@ -566,14 +566,14 @@ pub fn verify_metadata(
}
#[derive(Clone, Debug)]
pub struct OtaSignature {
struct RawOtaSignature {
/// Decoded CMS structure.
pub signed_data: SignedData,
signed_data: SignedData,
/// Length of the file (from the beginning) that's covered by the signature.
pub hashed_size: u64,
hashed_size: u64,
}
impl OtaSignature {
impl RawOtaSignature {
pub fn embedded_cert(&self) -> Result<&Certificate> {
let mut iter = crypto::iter_cms_certs(&self.signed_data);
@@ -589,9 +589,107 @@ impl OtaSignature {
}
}
#[derive(Clone, Debug)]
pub struct OtaSignature {
pub cert: Certificate,
pub digest_algo: &'static Algorithm,
pub sig_algo: SignatureAlgorithm,
pub sig: Vec<u8>,
pub data_size: u64,
}
impl TryFrom<RawOtaSignature> for OtaSignature {
type Error = Error;
fn try_from(raw_ota_sig: RawOtaSignature) -> Result<Self> {
let cert = raw_ota_sig.embedded_cert()?;
// Make sure this is a signature scheme we can handle. There's currently
// no Rust library to verify arbitrary CMS signatures for large files
// without fully reading them into memory.
let signers_len = raw_ota_sig.signed_data.signer_infos.0.len();
if signers_len != 1 {
return Err(Error::NotOneCmsSignerInfo(signers_len));
}
let signer = raw_ota_sig.signed_data.signer_infos.0.get(0).unwrap();
if signer.digest_alg.oid != rfc5912::ID_SHA_256
&& signer.digest_alg.oid != rfc5912::ID_SHA_1
{
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
{
return Err(Error::UnsupportedSignatureAlgorithm(
signer.signature_algorithm.oid,
));
}
// We support SHA1 for verification only.
let (digest_algo, sig_algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
} else {
(
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
SignatureAlgorithm::Sha1WithRsa,
)
};
Ok(Self {
cert: cert.clone(),
digest_algo,
sig_algo,
sig: signer.signature.as_bytes().to_vec(),
data_size: raw_ota_sig.hashed_size,
})
}
}
impl OtaSignature {
/// Verify an OTA zip against its embedded certificate. This function makes
/// no assertion about whether the certificate is actually trusted.
///
/// CMS signed attributes are intentionally not supported because AOSP
/// recovery does not support them either. It expects the CMS [`SignedData`]
/// structure to be used for nothing more than a raw signature transport
/// mechanism.
pub fn verify_ota(
&self,
mut reader: impl Read + Seek,
cancel_signal: &AtomicBool,
) -> Result<()> {
let public_key = crypto::get_public_key(&self.cert).map_err(Error::OtaCertExtractPubKey)?;
// Manually hash the parts of the file covered by the signature.
reader
.seek(SeekFrom::Start(0))
.map_err(|e| Error::DataRead("raw_data", e))?;
let mut hashing_reader = HashingReader::new(reader, Context::new(self.digest_algo));
stream::copy_n(
&mut hashing_reader,
io::sink(),
self.data_size,
cancel_signal,
)
.map_err(|e| Error::DataRead("raw_data", e))?;
let (_, context) = hashing_reader.finish();
let digest = context.finish();
// Verify the signature against the public key.
public_key
.verify_sig(self.sig_algo, digest.as_ref(), &self.sig)
.map_err(Error::CmsVerify)?;
Ok(())
}
}
/// Parse the CMS signature from the OTA zip comment. This does not perform any
/// parsing of zip data structures.
pub fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<OtaSignature> {
fn parse_raw_ota_sig(mut reader: impl Read + Seek) -> Result<RawOtaSignature> {
let file_size = reader
.seek(SeekFrom::End(0))
.map_err(|e| Error::DataRead("file_size", e))?;
@@ -642,77 +740,16 @@ pub fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<OtaSignature> {
// length field.
let hashed_size = file_size - 2 - u64::from(comment_size);
Ok(OtaSignature {
Ok(RawOtaSignature {
signed_data,
hashed_size,
})
}
/// Verify an OTA zip against its embedded certificates. This function makes no
/// assertion about whether the certificate is actually trusted. Returns the
/// embedded certificate.
///
/// CMS signed attributes are intentionally not supported because AOSP recovery
/// does not support them either. It expects the CMS [`SignedData`] structure to
/// be used for nothing more than a raw signature transport mechanism.
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
let ota_sig = parse_ota_sig(&mut reader)?;
let cert = ota_sig.embedded_cert()?;
let public_key = crypto::get_public_key(cert).map_err(Error::OtaCertExtractPubKey)?;
// Make sure this is a signature scheme we can handle. There's currently no
// Rust library to verify arbitrary CMS signatures for large files without
// fully reading them into memory.
let signers_len = ota_sig.signed_data.signer_infos.0.len();
if signers_len != 1 {
return Err(Error::NotOneCmsSignerInfo(signers_len));
}
let signer = ota_sig.signed_data.signer_infos.0.get(0).unwrap();
if signer.digest_alg.oid != rfc5912::ID_SHA_256 && signer.digest_alg.oid != rfc5912::ID_SHA_1 {
return Err(Error::UnsupportedDigestAlgorithm(signer.digest_alg.oid));
} else if signer.signature_algorithm.oid != rfc5912::RSA_ENCRYPTION
&& signer.signature_algorithm.oid != rfc5912::SHA_256_WITH_RSA_ENCRYPTION
{
return Err(Error::UnsupportedSignatureAlgorithm(
signer.signature_algorithm.oid,
));
}
// Manually hash the parts of the file covered by the signature.
reader
.seek(SeekFrom::Start(0))
.map_err(|e| Error::DataRead("raw_data", e))?;
// We support SHA1 for verification only.
let (algorithm, algo) = if signer.digest_alg.oid == rfc5912::ID_SHA_256 {
(&ring::digest::SHA256, SignatureAlgorithm::Sha256WithRsa)
} else {
(
&ring::digest::SHA1_FOR_LEGACY_USE_ONLY,
SignatureAlgorithm::Sha1WithRsa,
)
};
let mut hashing_reader = HashingReader::new(reader, Context::new(algorithm));
stream::copy_n(
&mut hashing_reader,
io::sink(),
ota_sig.hashed_size,
cancel_signal,
)
.map_err(|e| Error::DataRead("raw_data", e))?;
let (_, context) = hashing_reader.finish();
let digest = context.finish();
// Verify the signature against the public key.
public_key
.verify_sig(algo, digest.as_ref(), signer.signature.as_bytes())
.map_err(Error::CmsVerify)?;
Ok(cert.clone())
/// Parse the signature information from the CMS signature embedded in the OTA
/// zip archive comment.
pub fn parse_ota_sig(reader: impl Read + Seek) -> Result<OtaSignature> {
parse_raw_ota_sig(reader)?.try_into()
}
/// Get and parse the protobuf-encoded OTA metadata, the PEM-encoded otacert,
+131 -55
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -12,6 +12,8 @@ use std::{
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use bzip2::write::BzDecoder;
use clap::ValueEnum;
use flate2::{write::GzEncoder, Compression};
use liblzma::{
stream::{Check, Stream},
write::XzDecoder,
@@ -47,6 +49,9 @@ 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.
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown magic: {0:?}")]
@@ -121,6 +126,8 @@ pub enum Error {
InputOpen(String, #[source] io::Error),
#[error("Failed to open output file for partition: {0}")]
OutputOpen(String, #[source] io::Error),
#[error("Failed to GZ compress partition image chunk")]
GzCompress(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
#[error("Failed to XZ compress partition image chunk")]
@@ -135,7 +142,7 @@ type Result<T> = std::result::Result<T, Error>;
/// Raw on-disk layout for the payload header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`PAYLOAD_MAGIC`].
magic: [u8; 4],
@@ -942,52 +949,136 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
Ok((data, digest_compressed))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
pub enum VabcAlgo {
Lz4,
Gzip,
Gz,
}
impl VabcAlgo {
pub fn new(name: &str) -> Option<Self> {
match name {
"lz4" => Some(Self::Lz4),
"gz" => Some(Self::Gzip),
_ => None,
}
}
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> u64 {
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> Result<u64> {
let mut total = 0;
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let (chunk, remaining) = raw_data.split_at(n);
// This should match CompressWorker::GetDefaultCompressionLevel() in
// AOSP's libsnapshot.
let compressed = match self {
Self::Lz4 => lz4_flex::block::compress(&raw_data[..n]),
// We use the miniz_oxide backend for flate2, but flate2 doesn't
// expose a nice function for compressing to a vec, so just use
// miniz_oxide directly.
Self::Gzip => miniz_oxide::deflate::compress_to_vec_zlib(&raw_data[..n], 9),
Self::Lz4 => lz4_flex::block::compress(chunk),
Self::Gz => {
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
encoder.write_all(chunk).map_err(Error::GzCompress)?;
encoder.finish().map_err(Error::GzCompress)?
}
};
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
raw_data = remaining;
}
total
Ok(total)
}
}
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"),
}
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
/// Add fudge factor to CoW estimate to account for overhead.
fn fudge_cow_estimate(mut estimate: u64) -> Option<u64> {
// lz4_flex and zlib-rs usually compress better than the lz4 and zlib
// implementations used by libsnapshot_cow. Make up for this by adding
// percentage-based overhead.
estimate = estimate.checked_add(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).
estimate = estimate.checked_add(38)?;
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
estimate = estimate.checked_add(2 * 1024 * 1024)?;
// CowOptions::cluster_ops * sizeof(CowOperationV2).
estimate = estimate.checked_add(200 * 20)?;
Some(estimate)
}
/// Compute the VABC CoW v2 size estimate. 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.
pub fn compute_cow_estimate(
input: &(dyn ReadSeekReopen + Sync),
partition_name: &str,
block_size: u32,
vabc_algo: VabcAlgo,
cancel_signal: &AtomicBool,
) -> Result<u64> {
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;
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 chunks_total = file_size.div_ceil(CHUNK_SIZE);
let cow_estimate = (0..chunks_total)
.into_par_iter()
.map(|chunk| -> Result<u64> {
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_algo.compressed_size(&data, block_size)
})
.try_fold(
|| 0u64,
|total, chunk_estimate| -> Result<u64> {
total
.checked_add(chunk_estimate?)
.ok_or(Error::IntOverflow("cow_estimate"))
},
)
.try_reduce(
|| 0u64,
|total, partial| {
total
.checked_add(partial)
.ok_or(Error::IntOverflow("cow_estimate"))
},
)?;
fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))
}
/// 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
@@ -996,12 +1087,9 @@ 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),
@@ -1010,7 +1098,6 @@ pub fn compress_image(
vabc_algo: Option<VabcAlgo>,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
const CHUNK_GROUP: u64 = 32;
let file_size = input
@@ -1028,9 +1115,9 @@ pub fn compress_image(
}
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
let mut bytes_compressed = 0;
let mut bytes_compressed = 0u64;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut cow_estimate = 0;
let mut cow_estimate = 0u64;
let mut operations = vec![];
// Read the file one group at a time. This allows for some parallelization
@@ -1069,8 +1156,10 @@ pub fn compress_image(
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size =
vabc_algo.map_or(0, |a| a.compressed_size(&raw_data, block_size));
let cow_size = vabc_algo
.map(|a| a.compressed_size(&raw_data, block_size))
.transpose()?
.unwrap_or(0);
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
@@ -1090,8 +1179,12 @@ pub fn compress_image(
for (data, operation, cow_size) 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"))?;
cow_estimate = cow_estimate
.checked_add(*cow_size)
.ok_or(Error::IntOverflow("cow_estimate"))?;
}
let group_operations = compressed_data_group
@@ -1116,24 +1209,7 @@ pub fn compress_image(
};
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)
Some(fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))?)
} else {
None
};
+2 -2
View File
@@ -106,7 +106,7 @@ type Result<T> = std::result::Result<T, Error>;
/// Raw on-disk layout for the header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HEADER_MAGIC`].
magic: little_endian::U32,
@@ -183,7 +183,7 @@ impl RawHeader {
/// Raw on-disk layout for the chunk header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(packed)]
#[repr(C, packed)]
struct RawChunk {
/// Chunk type. Must be [`CHUNK_TYPE_RAW`], [`CHUNK_TYPE_FILL`],
/// [`CHUNK_TYPE_DONT_CARE`], or [`CHUNK_TYPE_CRC32`].
+1 -2
View File
@@ -37,8 +37,8 @@ allow = [
"GPL-3.0",
"ISC",
"MIT",
"OpenSSL",
"Unicode-3.0",
"Zlib",
]
[[licenses.clarify]]
@@ -70,5 +70,4 @@ unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/zip",
"https://github.com/trifectatechfoundation/bzip2-rs",
]
+1 -1
View File
@@ -14,7 +14,7 @@ avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
ring = "0.17.0"
ring = "0.17.14"
rsa = { version = "0.9.6", features = ["hazmat"] }
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
+11 -11
View File
@@ -50,11 +50,11 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes_streaming]
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
patched = "6c27ffb07f4497af8539f8283e506066af9417580230c3209c9875fc15d5069d"
[profile.pixel_v4_gki.hashes_seekable]
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
patched = "e4fc12523ffc312796b92210bc1e3bbb70dd60797a47daae76c8b5852e48b382"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
@@ -91,11 +91,11 @@ data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes_streaming]
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
patched = "6832ded3e98a14edc8c5ea7284fcea0b958fa710ebf222c27116faec8dfefe2e"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
patched = "114f8936e917d7e4a71bc1521adb3c8e676de3a738f8c7c505b86464d20bd95c"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
@@ -133,17 +133,17 @@ data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes_streaming]
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
patched = "1f28d9210a17e233cd5da4af55b07db764b19eeab991394514170b405240464f"
[profile.pixel_v3.hashes_seekable]
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
patched = "27b80c7be9c1e527ea26abe3dabde245c580e6f26ec084204278fbfd81a39f83"
# Google Pixel 4a
# What's unique: boot (boot v2)
[profile.pixel_v2]
vabc_algo = "Gzip"
vabc_algo = "Gz"
[profile.pixel_v2.partitions.boot]
avb.signed = false
@@ -168,9 +168,9 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes_streaming]
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
original = "bd2f19cf3d2285e35e8b36d44f75ed910e8e0be44c3ebd29f17a812521ba754b"
patched = "cf65d5b90500af54cd1204a646379bb852825061bc7c3f973b7a042f353f75ad"
[profile.pixel_v2.hashes_seekable]
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
original = "7f96ebf7366e0b60c91ac1e5f196a2189ffdb0bbc73f77804a736466fcab7315"
patched = "c2d9d60d73c038da39f82073ffadb459c96b901d66db7af11f59da58e0dd53e4"