Compare commits

...

55 Commits

Author SHA1 Message Date
Andrew Gunnerson c019ccad1c Version 3.20.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 22:35:40 -04:00
Andrew Gunnerson 192e737dd3 CHANGELOG.md: Add entry for PR #487
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 22:24:42 -04:00
Andrew Gunnerson 339267149f Update dependencies and fix clippy lints
if-let chains!

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 22:21:24 -04:00
Andrew Gunnerson 21c2536759 CHANGELOG.md: Add entry for PR #486
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 22:10:31 -04:00
Andrew Gunnerson 185f02c209 cli/avb: verify-device: Use bootloader's reported key digest by default
This removes the need for the user to explicitly specify a public key to
verify against.

Issue: #482

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 22:03:09 -04:00
Andrew Gunnerson a8908d6d06 CHANGELOG.md: Add entry for PR #485
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 21:57:42 -04:00
Andrew Gunnerson 2683781737 cli/avb: Add new subcommand to verify device partitions
This adds a new `avbroot avb verify-device` subcommand, which is just
like the normal `verify` subcommand, except it reads the actual
partitions on the device. This is only available with the Android build
of avbroot since it needs to run on the actual device.

Fixes: #482

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 21:24:28 -04:00
Andrew Gunnerson 44b90936bf CHANGELOG.md: Add entry for PR #484
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 19:59:41 -04:00
Andrew Gunnerson d7369e73e9 Remove cap-std and cap-tempfile dependencies
There is not much benefit for our use case to have kernel-level
openat-style sandboxing of paths. We already check all untrusted paths
for safety and the sandboxing prevented the use of symlinks that point
outside of the parent directory of specified paths.

This commit also moves the path safety checks to the util module to
avoid having multiple implementations spread out everywhere.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 19:50:43 -04:00
Andrew Gunnerson bb5c97ea1b CHANGELOG.md: Add entry for PR #483
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 16:35:40 -04:00
Andrew Gunnerson ac95660e23 Switch to rust-lzma2 for XZ compression/decompression
The performance, both in CPU time and compression ratios, is very
comparable to liblzma. This lets us drop the last remaining
compression-related dependency written in C.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-10 16:20:41 -04:00
Andrew Gunnerson 2bac85f080 Version 3.19.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:23:18 -04:00
Andrew Gunnerson fa99a3bb98 CHANGELOG.md: Add entry for PR #479
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:20:12 -04:00
Andrew Gunnerson 0d5bc574b2 cli/ota: Re-sign signed image when header verification fails
Previously, unless forced, `avbroot avb pack` would only re-sign an
image if the packing process changed the header (eg. root digest).
However, this isn't sufficient when packing an image after the user
modifies avb.toml manually. This is especially the case when packing a
vbmeta image, which never triggered the old check because it does not
contain a raw image.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:14:06 -04:00
Andrew Gunnerson e34c48c92b CHANGELOG.md: Add entry for PR #478
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:13:47 -04:00
Andrew Gunnerson 8ea08ef98c cli/avb: Warn when verifying image with insecure flags
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:10:24 -04:00
Andrew Gunnerson 779b1116e1 CHANGELOG.md: Add entry for PR #477
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:09:24 -04:00
Andrew Gunnerson 821c5fe088 format/avb: Fix verifying unsigned AVB images
Previously, Header::verify() tried to always decode the public_key
field, even if the header was unsigned. This prevented verifying
unsigned images with `avbroot avb verify`. Verifying unsigned images
referenced by signed images was unaffected.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 23:04:01 -04:00
Andrew Gunnerson 6a1da333eb Version 3.18.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 01:53:44 -04:00
Andrew Gunnerson 256483d248 CHANGELOG.md: Add entry for PR #476
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 01:53:25 -04:00
Andrew Gunnerson 2314c371a8 sparse: Fix u32 overflow when unpacking files with large holes
CHUNK_TYPE_RAW is the only chunk type that's guaranteed to not overflow
a u32 when its number of blocks is multiplied by the block size.
CHUNK_TYPE_FILL and CHUNK_TYPE_DONT_CARE require a u64.

Issue: #472

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-07 01:36:22 -04:00
Andrew Gunnerson 59cf37faaf Version 3.18.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:41:13 -04:00
Andrew Gunnerson 36269acd7b CHANGELOG.md: Add entry for PR #475
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:37:45 -04:00
Andrew Gunnerson 6aacc5a76c Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:33:54 -04:00
Andrew Gunnerson bd4ebde403 CHANGELOG.md: Add entry for PR #474
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:30:23 -04:00
Andrew Gunnerson ffdae0bf88 Bump Magisk version upper bound to 30300
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:28:33 -04:00
Andrew Gunnerson 8282f8087c CHANGELOG.md: Add entry for PR #473
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:27:46 -04:00
Andrew Gunnerson 6b11cd8af2 Allow using SHA-1 as AVB hash algorithm
Previously, we automatically promoted SHA-1 to SHA-256 because SHA-1 is
insecure, but there are devices that don't support SHA-256. It's safer
to just keep using the original hash algorithm.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:17:21 -04:00
Andrew Gunnerson f610f3b794 CHANGELOG.md: Add entry for PR #470
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-06 21:14:26 -04:00
Andrew Gunnerson b8d022d52c format/ota: Make property files verification more lenient
Previously, we computed the expected property files string (based on
AOSP's rules) and checked if the string in the OTA metadata was a
byte-for-byte match. This would fail for OTAs with strings that differ
from how AOSP generates them. This could be additional files or just
different ordering of the entries.

This commit changes the approach to just verify that the property file
entries is a valid subset of the zip file entries. We no longer try to
compute the expected value.

This does not change what avbroot generates when patching an OTA. Newly
generated property files strings always follow AOSP's rules.

Fixes: #469

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-22 20:49:05 -04:00
Andrew Gunnerson cd38217111 Version 3.17.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:48:16 -04:00
Andrew Gunnerson c8d548d224 CHANGELOG.md: Add entry for PR #468
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:31:59 -04:00
Andrew Gunnerson 9ad430ac7f Bump Magisk version upper bound to 30200
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-03 14:30:50 -04:00
Andrew Gunnerson 8ca4eb5ad9 Version 3.17.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:07:13 -04:00
Andrew Gunnerson e2b1ccb7a1 CHANGELOG.md: Add entry for PR #467
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 19:06:37 -04:00
Andrew Gunnerson 71a31ae01b Bump Magisk version upper bound to 30100
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-07-01 18:55:14 -04:00
Andrew Gunnerson 83d7ffbc5e CHANGELOG.md: Add entry for PR #464
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 20:20:54 -04:00
Andrew Gunnerson e397998d9e Update dependencies
The zip crate gained support for streaming writes in its master branch,
so we can finally upgrade from our ancient fork of it. The new
implementation is done a bit differently, with seekable writers having
the ZipWriter<W> type and streaming writers having the
ZipWriter<StreamWriter<W>> type. This forces us to add a new wrapper
type since we have to switch between them at runtime.

We still need to maintain a (hopefully temporary) fork of the crate due
to a few issues:

1. There's no way to get the original underlying writer instance back
   after finalizing a streaming zip. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/367

2. The streaming writes implementation does not include the magic
   signature for data descriptors. While the zip spec says the magic
   value is optional and parsers should not require it, older versions
   of Android's libziparchive do. A fix for this has been submitted
   upstream:
   https://github.com/zip-rs/zip2/pull/368

3. There is currently no way to get the data offset of zip entries.
   avbroot requires this to fill in the OTA metadata's "property files"
   entries, which Android uses to read file data without parsing the zip
   file structures.

This new zip update produces files that are slightly different to
before. The "version made by" and "version needed to extract" fields are
now set to their minimum possible values. Previously, the zip crate was
hardcoded to use versions 4.6 and 2.0, respectively.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:55:08 -04:00
Andrew Gunnerson 8ef22508f5 CHANGELOG.md: Add entry for PR #463
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 18:49:32 -04:00
Andrew Gunnerson bf42a6a75c e2e: Split streaming and seekable work directories
Makes troubleshooting easier when files aren't being overwritten.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-19 17:49:00 -04:00
lefuglyduck a2fe6fc9d8 Update README.md
Clarify the updates section so it doesn't "loop back" to the usage section. 

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 16:12:13 -07:00
lefuglyduck faeb1fe988 Update README.md
Minor grammatical change.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-12 17:53:52 -04:00
Andrew Gunnerson f1b2c6f468 Merge PR #458
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-12 17:51:40 -04:00
lefuglyduck 2f964bf113 Update README.md
Clarify step 3 of usage section.

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-06-11 22:36:24 -07:00
Andrew Gunnerson f393d7adc4 README.md: Add warning about post-installation snapshot merge operation
Issue: #454

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-06-08 17:00:21 -04:00
Andrew Gunnerson 72a1c3f216 Version 3.17.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 12:00:31 -04:00
Andrew Gunnerson 2db8d3826e CHANGELOG.md: Add entry for PR #453
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:26:46 -04:00
Andrew Gunnerson 1448e55205 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:21:14 -04:00
Andrew Gunnerson b3862a9c4a CHANGELOG.md: Add entry for PR #452
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:18:11 -04:00
Andrew Gunnerson 088db04673 ota: Reserve 16 bytes for OTA metadata property files
Our previous limit was 15 bytes for the <offset>:<size> placeholder,
matching AOSP's ota_utils.py. Since the size of metadata.pb is almost
always 4 digits, this leaves 10 digits for the offset, which isn't
enough for large OTAs. AOSP never actually hits the limit because it
puts metadata and metadata.pb at the beginning of the output zip file.
We put the files at the end of the zip since we do streaming writes.

Fixes: #451

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-31 10:16:45 -04:00
Andrew Gunnerson b1410c869d Version 3.16.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:54:52 -04:00
Andrew Gunnerson dd6eaf8e78 CHANGELOG.md: Add entry for PR #449
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:50:59 -04:00
Andrew Gunnerson 0eac8e6614 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:37:32 -04:00
Andrew Gunnerson 8a4f90176e CHANGELOG.md: Add entry for PR #448
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:33:01 -04:00
Andrew Gunnerson bd166594e2 Bump Magisk version upper bound to 29100
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-19 22:31:34 -04:00
46 changed files with 1609 additions and 1061 deletions
+5 -5
View File
@@ -83,7 +83,7 @@ jobs:
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
key: ${{ matrix.artifact.name }}
@@ -92,7 +92,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
clippy --release --workspace \
--target "${target}"
done
@@ -101,7 +101,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
build --release --workspace \
--target "${target}"
done
@@ -110,7 +110,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
test --release --workspace \
--target "${target}"
done
@@ -119,7 +119,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
run --release -p e2e --features static \
run --release -p e2e \
--target "${target}" \
-- test -a -c e2e/e2e.toml
done
+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@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
uses: EmbarkStudios/cargo-deny-action@30f817c6f72275c6d54dc744fbca09ebc958599f # v2.0.12
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Create release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
+73
View File
@@ -7,6 +7,54 @@
to update the actual links at the bottom of the file.
-->
### Version 3.20.0
* Switch to using lzma-rust2 for XZ compression and decompression ([PR #483])
* Remove cap-std and cap-tempfile dependencies ([PR #484])
* Add new `avbroot avb verify-device` command to verify the signatures on the actual device ([Issue #482], [PR #485], [PR #486])
* This is only available in the Android build of avbroot.
* Update dependencies and fix new Rust 1.89 clippy lints ([PR #487])
### Version 3.19.0
* Allow verifying hashes of unsigned images with `avbroot avb verify` ([PR #477])
* Warn when verifying image containing insecure flags field with `avbroot avb verify` ([PR #478])
* Force re-signing of signed images in `avbroot avb pack` when fields were changed externally ([PR #479])
### Version 3.18.1
* Fix output file corruption in `avbroot sparse unpack` when unpacking a sparse file with holes larger than 2^32 ([Issue #472], [PR #476])
### Version 3.18.0
* Make OTA metadata property file field validation more lenient ([Issue #469], [PR #470])
* Fixes `avbroot ota verify` for stock OTAs that include extra zip file entries in the metadata
* Remove automatic promotion of insecure SHA-1 AVB hash algorithm to SHA-256 ([Issue #366], [Issue #469], [PR #473])
* There are insecure devices that don't support SHA-256 and won't boot with it.
* The original feature was a bandaid for OnePlus devices to make them a tiny bit more secure. They used SHA-256 for every partition except `system`. However, OnePlus no longer supports custom AVB keys anyway, so this feature is going away.
* Add support for Magisk 30200 ([PR #474])
* Update dependencies ([PR #475])
### Version 3.17.2
* Add support for Magisk 30100 ([PR #468])
### Version 3.17.1
* Update end-to-end tests to place streaming and seekable OTAs in separate directories for easier troubleshooting ([PR #463])
* Update dependencies ([PR #464])
* Add support for Magisk 30000 ([PR #467])
### Version 3.17.0
* Fix reserved space error when patching OTA zips larger than ~10 GB ([Issue #451], [PR #452])
* Update dependencies ([PR #453])
### Version 3.16.1
* Add support for Magisk 29000 ([PR #448])
* Update dependencies ([PR #449])
### Version 3.16.0
* Add support for CoW version 3 for virtual A/B ([Issue #441], [PR #442], [PR #445])
@@ -349,6 +397,10 @@ Behind-the-scenes changes:
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
[Issue #441]: https://github.com/chenxiaolong/avbroot/issues/441
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
[Issue #472]: https://github.com/chenxiaolong/avbroot/issues/472
[Issue #482]: https://github.com/chenxiaolong/avbroot/issues/482
[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
@@ -508,3 +560,24 @@ Behind-the-scenes changes:
[PR #444]: https://github.com/chenxiaolong/avbroot/pull/444
[PR #445]: https://github.com/chenxiaolong/avbroot/pull/445
[PR #446]: https://github.com/chenxiaolong/avbroot/pull/446
[PR #448]: https://github.com/chenxiaolong/avbroot/pull/448
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
[PR #468]: https://github.com/chenxiaolong/avbroot/pull/468
[PR #470]: https://github.com/chenxiaolong/avbroot/pull/470
[PR #473]: https://github.com/chenxiaolong/avbroot/pull/473
[PR #474]: https://github.com/chenxiaolong/avbroot/pull/474
[PR #475]: https://github.com/chenxiaolong/avbroot/pull/475
[PR #476]: https://github.com/chenxiaolong/avbroot/pull/476
[PR #477]: https://github.com/chenxiaolong/avbroot/pull/477
[PR #478]: https://github.com/chenxiaolong/avbroot/pull/478
[PR #479]: https://github.com/chenxiaolong/avbroot/pull/479
[PR #483]: https://github.com/chenxiaolong/avbroot/pull/483
[PR #484]: https://github.com/chenxiaolong/avbroot/pull/484
[PR #485]: https://github.com/chenxiaolong/avbroot/pull/485
[PR #486]: https://github.com/chenxiaolong/avbroot/pull/486
[PR #487]: https://github.com/chenxiaolong/avbroot/pull/487
Generated
+454 -363
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.16.0"
version = "3.20.0"
license = "GPL-3.0-only"
edition = "2024"
repository = "https://github.com/chenxiaolong/avbroot"
+12 -3
View File
@@ -30,8 +30,6 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
* To force an image to be signed, use `--key <path> --force`.
* To force an image to be unsigned, use `--force` without specifying `--key`.
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
@@ -60,12 +58,23 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
avbroot avb verify -i <root vbmeta image> -p <public key>
```
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All images are expected to be in the same directory as the vbmeta image being verified. Missing images are ignored by default because the vbmeta images in some OTAs reference partitions that only exist on a real device. `--fail-if-missing` can be used to override this.
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
### Verifying AVB hashes and signatures on device
```bash
# Run from a root adb shell:
avbroot avb verify-device [-p <public key>]
```
This subcommand is like `avbroot avb verify`, except that it verifies the actual partitions on the device instead of a directory of image files. This is only available in the Android build of avbroot.
If `-p` is omitted, the signatures are verified against the public key SHA-256 digest reported by the bootloader. This is the same digest shown on screen every time the device boots.
### Computing vbmeta digest
```bash
+13 -7
View File
@@ -51,6 +51,8 @@ avbroot applies the following patches to the partition images:
3. Follow the steps to [generate signing keys](#generating-keys).
Skip this step if you're updating Android, Magisk, or KernelSU after you've performed an [initial setup](#initial-setup). [Updates](#updates) do not require signing keys since you have already generated them in the initial setup.
4. Patch the OTA zip. The base command is:
```bash
@@ -211,6 +213,8 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
init: [libfs_avb]Returning avb_handle with status: Success
```
Alternatively, the Android build of avbroot can also be used to [verify the partitions on the device](./README.extra.md#verifying-avb-hashes-and-signatures-on-device).
9. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
```bash
@@ -223,21 +227,25 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
**WARNING**: If you are flashing CalyxOS, the setup wizard will [automatically turn off the `OEM unlocking` switch](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140). Make sure to manually reenable it again from Android's developer settings. Consider using the [`OEMUnlockOnBoot` module](https://github.com/chenxiaolong/OEMUnlockOnBoot) to automatically ensure OEM unlocking is enabled on every boot.
10. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
10. That's it! To update the OS, Magisk, or KernelSU see the [next section](#updates).
## Updates
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
Updates to Android, Magisk, and KernelSU are all done the same way: by patching (or repatching) the OTA.
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
1. Generate a new patched OTA by following the steps in the [usage section](#usage).
2. Follow the step in the [usage section](#usage) to patch the new OTA.
2. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
4. Sideload the patched OTA with `adb sideload`.
5. That's it!
5. Restart your phone. Note: the phone will likely take a long time to startup after an OS update (a few minutes in some cases).
**Warning**: Due to how virtual A/B works, there is a snapshot merge operation that Android runs invisibly in the background after installing an OTA and rebooting. During the snapshot merge process, it's not possible to sideload another OTA from recovery mode. Avoid doing anything that could result in a boot loop (eg. installing modules) until this process is complete because there is no way to recover, aside from unlocking the bootloader (and wiping) again.
The status can be found by running `adb logcat -v color -s update_engine`. Alternatively, if [Custota](https://github.com/chenxiaolong/Custota) is installed (even if it's not configured to point to a custom OTA server), it will show a notification until the snapshot merge operation completes.
## Reverting to stock firmware
@@ -582,8 +590,6 @@ The output binary is written to `target/release/avbroot`.
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
### Android cross-compilation
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
+10 -12
View File
@@ -391,17 +391,17 @@ avbroot можно использовать для простого перепо
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
### Пропуск всех патчей
Чтобы внести самый минимум изменений, укажите аргументы:
* `--skip-system-ota-cert`
* `--skip-recovery-ota-cert`
* `--rootless`
* не используйте аргумент `--dsu`.
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
### Подмена образов
@@ -422,15 +422,15 @@ Verified boot is disabled by vbmeta's header flags: 0x3
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
### Изменение алгоритма CoW сжатия для вирутального A/B
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
### Использование в неинтерактивном режиме
@@ -515,7 +515,7 @@ avbroot поддерживает делегирование всех опера
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
* `/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`.
@@ -570,8 +570,6 @@ cargo build --release
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
### Кросс-компиляция на Android
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
+15 -15
View File
@@ -13,9 +13,7 @@ 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"
bzip2 = "0.6.0"
clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
@@ -28,15 +26,15 @@ dlv-list = "0.6.0"
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.4.1"
lz4_flex = "0.11.1"
lzma-rust2 = "0.8.0"
memchr = "2.6.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
passterm = "2.0.3"
phf = { version = "0.11.2", features = ["macros"] }
phf = { version = "0.12.1", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.13.1"
prost = "0.14.1"
# We can't upgrade to 0.9.0 until rsa updates its rand_core dependency.
rand = "0.8.5"
rayon = "1.7.0"
@@ -52,7 +50,7 @@ sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "2.0.3"
toml_edit = { version = "0.22.9", features = ["serde"] }
toml_edit = { version = "0.23.3", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
@@ -60,10 +58,12 @@ x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
# https://github.com/zip-rs/zip/pull/383
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
features = ["deflate"]
@@ -71,16 +71,16 @@ features = ["deflate"]
libc = "0.2.158"
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
[target.'cfg(target_os = "android")'.dependencies]
system-properties = { git = "https://github.com/chenxiaolong/system-properties", tag = "v0.2.1" }
[build-dependencies]
constcat = "0.6.0"
prost-build = "0.13.1"
protox = "0.8.0"
prost-build = "0.14.1"
protox = "0.9.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["liblzma/static"]
[lints]
workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, Level};
use tracing::{Level, debug};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
+242 -103
View File
@@ -1,25 +1,23 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fs::{self, File},
ffi::OsString,
fmt,
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use cap_std::{
ambient_authority,
fs::{Dir, OpenOptions},
};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, Parser, Subcommand};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use rsa::RsaPublicKey;
use serde::{Deserialize, Serialize};
use tracing::{debug_span, info, warn, Span};
use sha2::{Digest, Sha256};
use tracing::{Span, debug_span, info, warn};
use crate::{
crypto::{self, PassphraseSource, RsaSigningKey},
@@ -27,7 +25,7 @@ use crate::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
stream::{self, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter, check_cancel},
util,
};
@@ -95,18 +93,6 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
Ok(())
}
/// Packing with insecure algorithms is intentionally not supported, so promote
/// to a secure algorithm if needed.
fn promote_insecure_hash_algorithm(algorithm: &mut String) {
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
const NEW_ALGORITHM: &str = "sha256";
if INSECURE_ALGORITHMS.contains(&algorithm.as_str()) {
warn!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
NEW_ALGORITHM.clone_into(algorithm);
}
}
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
/// both readable and writable.
fn write_raw(
@@ -194,13 +180,11 @@ fn write_raw_and_update(
match info.header.appended_descriptor_mut()? {
AppendedDescriptorMut::HashTree(d) => {
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
d.update(&raw_file, &raw_file, None, cancel_signal)
.context("Failed to update hash tree descriptor")?;
}
AppendedDescriptorMut::Hash(d) => {
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.image_size = image_size;
raw_file.rewind()?;
d.update(&mut raw_file, cancel_signal)
@@ -312,14 +296,13 @@ fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
};
for d in &mut info.header.descriptors {
if let Descriptor::KernelCmdline(d) = d {
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
&& d.cmdline.starts_with("dm=")
&& d.cmdline != new_cmdline
{
d.cmdline = new_cmdline;
return Ok(true);
}
if let Descriptor::KernelCmdline(d) = d
&& d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
&& d.cmdline.starts_with("dm=")
&& d.cmdline != new_cmdline
{
d.cmdline = new_cmdline;
return Ok(true);
}
}
@@ -345,7 +328,7 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
} else {
SignAction::Clear
}
} else if originally_signed && &info.header != orig_header {
} else if originally_signed && (&info.header != orig_header || info.header.verify().is_err()) {
SignAction::Sign
} else {
// If the original image was signed, we can preserve the existing
@@ -424,22 +407,82 @@ fn display_info(display: &DisplayGroup, info: &AvbInfo) {
}
}
/// Ensure that the partition name won't cause directory traversals.
fn ensure_name_is_safe(name: &str) -> Result<()> {
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
#[derive(Debug, Clone)]
struct SearchPath {
dir: PathBuf,
suffix: String,
}
impl fmt::Display for SearchPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?} (suffix: {:?})", self.dir, self.suffix)
}
}
#[derive(Debug, Clone, Default)]
pub struct ImageOpener {
search: Vec<SearchPath>,
}
impl ImageOpener {
pub fn new() -> Self {
Self::default()
}
Ok(())
pub fn with_dir(dir: impl Into<PathBuf>) -> Self {
let mut result = Self::new();
result.add_dir(dir, ".img");
result
}
pub fn add_dir(&mut self, dir: impl Into<PathBuf>, suffix: impl Into<String>) {
self.search.push(SearchPath {
dir: dir.into(),
suffix: suffix.into(),
});
}
fn open(&self, name: &str, options: &OpenOptions) -> io::Result<(PathBuf, File)> {
for search in &self.search {
let path = util::path_join_single(&search.dir, format!("{name}{}", search.suffix))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
match options.open(&path) {
Ok(f) => return Ok((path, f)),
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => {
return Err(io::Error::new(
e.kind(),
format!("Failed to open for reading: {path:?}: {e}"),
));
}
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!(
"Failed to find {name:?} image in: {}",
util::join(&self.search, ", "),
),
))
}
}
#[derive(Debug, Clone)]
pub enum TrustMethod {
Key(RsaPublicKey),
KeyDigest([u8; 32]),
Anything,
}
/// Recursively verify an image's vbmeta header and all of the chained images.
/// `seen` is used to prevent cycles. `descriptors` will contain all of the hash
/// and hash tree descriptors that need to be verified.
pub fn verify_headers(
directory: &Dir,
opener: &ImageOpener,
name: &str,
expected_key: Option<&RsaPublicKey>,
trust_method: &TrustMethod,
seen: &mut HashSet<String>,
descriptors: &mut HashMap<String, Descriptor>,
) -> Result<()> {
@@ -447,12 +490,7 @@ pub fn verify_headers(
return Ok(());
}
ensure_name_is_safe(name)?;
let path = format!("{name}.img");
let raw_reader = directory
.open(&path)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let (path, raw_reader) = opener.open(name, OpenOptions::new().read(true))?;
let (header, _, _) = avb::load_image(BufReader::new(raw_reader))
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
@@ -464,19 +502,36 @@ pub fn verify_headers(
if let Some(k) = &public_key {
let prefix = format!("{name} has a signed vbmeta header");
if let Some(e) = expected_key {
if k == e {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
match trust_method {
TrustMethod::Key(expected) => {
if k == expected {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
}
}
TrustMethod::KeyDigest(expected_sha256) => {
let encoded = avb::encode_public_key(k)?;
let digest = Sha256::digest(&encoded);
if digest.as_slice() == expected_sha256 {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
}
}
TrustMethod::Anything => {
warn!("{prefix}, but parent does not list a trusted key");
}
} else {
warn!("{prefix}, but parent does not list a trusted key");
}
} else {
info!("{name} has an unsigned vbmeta header");
}
if header.flags != 0 {
warn!("{name} has insecure flags: {:#x}", header.flags);
}
for descriptor in &header.descriptors {
let Some(target_name) = descriptor.partition_name() else {
continue;
@@ -496,8 +551,9 @@ pub fn verify_headers(
let target_key = avb::decode_public_key(&d.public_key).with_context(|| {
format!("Failed to decode chained public key for: {target_name}")
})?;
let target_trust = TrustMethod::Key(target_key);
verify_headers(directory, target_name, Some(&target_key), seen, descriptors)?;
verify_headers(opener, target_name, &target_trust, seen, descriptors)?;
}
_ => {}
}
@@ -555,34 +611,33 @@ fn verify_and_repair(
/// Verify hash and hash tree descriptor digests and FEC data against their
/// corresponding input files.
pub fn verify_descriptors(
directory: &Dir,
opener: &ImageOpener,
descriptors: &HashMap<String, Descriptor>,
repair: bool,
allow_missing: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let parent_span = Span::current();
let mut options = OpenOptions::new();
options.read(true);
options.write(repair);
descriptors
.par_iter()
.map(|(name, descriptor)| {
let _span = parent_span.enter();
let path = format!("{name}.img");
let file = match directory
.open_with(&path, OpenOptions::new().read(true).write(repair))
.map(|f| PSeekFile::new(f.into_std()))
{
Ok(f) => f,
let file = match opener.open(name, &options) {
Ok((_, f)) => PSeekFile::new(f),
// Some devices, like bluejay, have vbmeta descriptors that
// refer to partitions that exist on the device, but not in the
// OTA.
Err(e) if e.kind() == io::ErrorKind::NotFound => {
warn!("Partition image does not exist: {path:?}");
Err(e) if e.kind() == io::ErrorKind::NotFound && allow_missing => {
warn!("{e}");
return Ok(());
}
Err(e) => {
Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?
}
Err(e) => return Err(e.into()),
};
verify_and_repair(
@@ -597,7 +652,7 @@ pub fn verify_descriptors(
}
fn compute_digest_recursive(
directory: &Dir,
directory: &Path,
name: &str,
context: &mut ring::digest::Context,
max_depth: u8,
@@ -612,11 +667,8 @@ fn compute_digest_recursive(
seen.insert(name.to_owned());
ensure_name_is_safe(name)?;
let path = format!("{name}.img");
let mut raw_reader = directory
.open(&path)
let path = util::path_join_single(directory, format!("{name}.img"))?;
let mut raw_reader = File::open(&path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let (header, footer, _) = avb::load_image(&mut raw_reader)
@@ -668,7 +720,11 @@ fn compute_digest_recursive(
/// the root vbmeta image, followed by the headers in the immediate chained
/// partitions. This digest is not defined to be recursive, so headers of
/// chained partitions more than one level deep are ignored.
pub fn compute_digest(directory: &Dir, name: &str, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
pub fn compute_digest(
directory: &Path,
name: &str,
cancel_signal: &AtomicBool,
) -> Result<[u8; 32]> {
let mut seen = HashSet::<String>::new();
let mut context = ring::digest::Context::new(&ring::digest::SHA256);
@@ -745,7 +801,6 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
// Write new hash tree and FEC data instead of copying the original.
// There could have been errors in the original FEC data itself.
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
promote_insecure_hash_algorithm(&mut d.hash_algorithm);
d.update(&file, &file, None, cancel_signal)?;
}
@@ -776,50 +831,40 @@ fn info_subcommand(cli: &InfoCli) -> Result<()> {
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let public_key = if let Some(p) = &cli.public_key {
fn verify_internal(
public_key_path: Option<&Path>,
public_key_digest: Option<[u8; 32]>,
opener: &ImageOpener,
name: &str,
repair: bool,
allow_missing: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let trust_method = if let Some(p) = public_key_path {
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
let key = avb::decode_public_key(&data)
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
Some(key)
TrustMethod::Key(key)
} else if let Some(d) = public_key_digest {
TrustMethod::KeyDigest(d)
} else {
None
TrustMethod::Anything
};
let authority = ambient_authority();
let parent_path = util::parent_path(&cli.input);
let directory = Dir::open_ambient_dir(parent_path, authority)
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
let name = cli
.input
.file_stem()
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
verify_headers(
&directory,
name,
public_key.as_ref(),
&mut seen,
&mut descriptors,
)?;
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
verify_headers(opener, name, &trust_method, &mut seen, &mut descriptors)?;
verify_descriptors(opener, &descriptors, repair, allow_missing, cancel_signal)?;
info!("Successfully verified all vbmeta signatures and hashes");
Ok(())
}
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
let authority = ambient_authority();
let parent_path = util::parent_path(&cli.input);
let directory = Dir::open_ambient_dir(parent_path, authority)
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let directory = util::parent_path(&cli.input);
let name = cli
.input
.file_stem()
@@ -827,7 +872,71 @@ fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()>
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
let digest = compute_digest(&directory, name, cancel_signal)?;
let opener = ImageOpener::with_dir(directory);
verify_internal(
cli.public_key.as_deref(),
None,
&opener,
name,
cli.repair,
!cli.fail_if_missing,
cancel_signal,
)
}
#[cfg(target_os = "android")]
fn get_required_property(name: &str) -> Result<String> {
system_properties::read(name)
.with_context(|| format!("Failed to query property: {name}"))?
.ok_or_else(|| anyhow!("Property is not set: {name}"))
}
#[cfg(target_os = "android")]
fn verify_device_subcommand(cli: &VerifyDeviceCli, cancel_signal: &AtomicBool) -> Result<()> {
let slot_suffix = get_required_property("ro.boot.slot_suffix")?;
// Use the bootloader's public key digest if no key is specified. This is
// what the user flashed for avb_custom_key.
let public_key_digest = if cli.public_key.is_none() {
let hex_digest = get_required_property("ro.boot.vbmeta.public_key_digest")?;
let mut digest = [0u8; 32];
hex::decode_to_slice(&hex_digest, &mut digest)
.with_context(|| format!("Invalid public key digest: {hex_digest}"))?;
info!("Verifying against bootloader public key digest: {hex_digest}");
Some(digest)
} else {
None
};
let mut opener = ImageOpener::new();
opener.add_dir("/dev/block/by-name", &slot_suffix);
opener.add_dir("/dev/block/mapper", &slot_suffix);
verify_internal(
cli.public_key.as_deref(),
public_key_digest,
&opener,
&cli.partition,
false,
false,
cancel_signal,
)
}
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
let directory = util::parent_path(&cli.input);
let name = cli
.input
.file_stem()
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
let digest = compute_digest(directory, name, cancel_signal)?;
println!("{}", hex::encode(digest));
@@ -841,6 +950,8 @@ pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
AvbCommand::Info(c) => info_subcommand(c),
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
#[cfg(target_os = "android")]
AvbCommand::VerifyDevice(c) => verify_device_subcommand(c, cancel_signal),
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
}
}
@@ -1044,6 +1155,32 @@ struct VerifyCli {
/// Only images with hash tree descriptors can contain FEC data.
#[arg(short, long)]
repair: bool,
/// Fail if a referenced image is missing.
///
/// Missing images are ignored by default because some OTAs contain vbmeta
/// images referencing partitions that only exist on the real device.
#[arg(long)]
fail_if_missing: bool,
}
/// Verify vbmeta signatures for the currently booted system.
///
/// This behaves like the `verify` subcommand, except that it checks the actual
/// partitions that this device is currently booted from.
#[cfg(target_os = "android")]
#[derive(Debug, Parser)]
struct VerifyDeviceCli {
/// Path to public key in AVB binary format.
///
/// If this is not specified, the signatures can only be checked for
/// validity, not whether they are trusted.
#[arg(short, long, value_name = "FILE", value_parser)]
public_key: Option<PathBuf>,
/// Partition to recursively verify.
#[arg(short = 'P', long, value_name = "NAME", default_value = "vbmeta")]
partition: String,
}
/// Compute the vbmeta digest.
@@ -1065,6 +1202,8 @@ enum AvbCommand {
#[command(alias = "dump")]
Info(InfoCli),
Verify(VerifyCli),
#[cfg(target_os = "android")]
VerifyDevice(VerifyDeviceCli),
Digest(DigestCli),
}
+1 -1
View File
@@ -7,7 +7,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use crate::{
+19 -27
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -9,9 +9,8 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use bstr::ByteSlice;
use cap_std::{ambient_authority, fs::Dir};
use clap::{Parser, Subcommand};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
@@ -34,7 +33,7 @@ fn open_reader(
path: &Path,
include_trailer: bool,
) -> Result<(
CpioReader<CompressedReader<BufReader<File>>>,
CpioReader<CompressedReader<'_, BufReader<File>>>,
CompressedFormat,
)> {
let file =
@@ -50,7 +49,7 @@ fn open_reader(
fn open_writer(
path: &Path,
format: CompressedFormat,
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
) -> Result<CpioWriter<CompressedWriter<'_, BufWriter<File>>>> {
let file =
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
let writer = CompressedWriter::new(BufWriter::new(file), format)
@@ -91,17 +90,17 @@ fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
/// Open reader to the corresponding file inside the tree if the entry is a
/// regular file. Unsafe paths will result in an error.
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
fn open_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
let sub_path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let path = util::path_join(tree, sub_path)?;
let mut reader = tree
.open(path)
.map(|f| BufReader::new(f.into_std()))
let mut reader = File::open(&path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let file_size = reader
@@ -122,21 +121,21 @@ fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<Fil
/// Open writer to the corresponding file inside the tree if the entry is a
/// regular file. Intermediate directories are automatically created as needed.
/// Unsafe paths will result in an error.
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
fn create_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
let sub_path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let parent = util::parent_path(path);
let path = util::path_join(tree, sub_path)?;
let parent = util::parent_path(&path);
tree.create_dir_all(parent)
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
let writer = tree
.create(path)
.map(|f| BufWriter::new(f.into_std()))
let writer = File::create(&path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok(Some(writer))
@@ -171,16 +170,13 @@ fn unpack_subcommand(
display_format(cpio_cli, format);
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_tree, authority)
fs::create_dir_all(&cli.output_tree)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
if let Some(mut writer) = create_tree_file(&cli.output_tree, &entry)? {
let file_size = entry.data.size()?;
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
@@ -209,12 +205,8 @@ fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool
cpio::assign_inodes(&mut info.entries, true)?;
let authority = ambient_authority();
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
for entry in &mut info.entries {
let out = open_tree_file(&tree, entry)?;
let out = open_tree_file(&cli.input_tree, entry)?;
if let Some((_, file_size)) = &out {
entry.data = CpioEntryData::Size(*file_size);
+12 -30
View File
@@ -1,16 +1,14 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
ffi::OsStr,
fs::{self, File},
io::{Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{bail, Context, Result};
use cap_std::{ambient_authority, fs::Dir};
use anyhow::{Context, Result, bail};
use clap::{CommandFactory, Parser, Subcommand};
use rayon::iter::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator,
@@ -19,6 +17,7 @@ use rayon::iter::{
use crate::{
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
util,
};
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
@@ -190,11 +189,8 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
return Ok(());
}
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
fs::create_dir_all(&cli.output_images)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
let slot = &metadata.slots[0];
@@ -216,11 +212,11 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
for partition in &group.partitions {
// A partition name with unsafe characters fails during parsing.
let path = format!("{}.img", partition.name);
let path =
util::path_join_single(&cli.output_images, format!("{}.img", partition.name))?;
let file = directory
.create(&path)
.map(|f| PSeekFile::new(f.into_std()))
let file = File::create(&path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
file.set_len(partition.size()?)
@@ -303,35 +299,21 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
}
}
for group in &slot.groups {
for partition in &group.partitions {
let name = &partition.name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
}
// Preopen all image input files.
let mut paths = vec![];
let mut files = vec![];
if metadata.image_type == ImageType::Normal {
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for group in &mut slot.groups {
let mut group_paths = vec![];
let mut group_files = vec![];
for partition in &mut group.partitions {
let path = format!("{}.img", partition.name);
let path =
util::path_join_single(&cli.input_images, format!("{}.img", partition.name))?;
let mut file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
let mut file = File::open(&path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let size = file
+107 -118
View File
@@ -5,35 +5,36 @@ use std::{
borrow::Cow,
collections::{BTreeSet, HashMap, HashSet},
ffi::{OsStr, OsString},
fmt::Display,
fs::{self, File},
io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write},
ops::Range,
path::{Path, PathBuf},
str::FromStr,
sync::{atomic::AtomicBool, Mutex},
sync::{Mutex, atomic::AtomicBool},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use bitflags::bitflags;
use cap_std::{ambient_authority, fs::Dir};
use cap_tempfile::TempDir;
use clap::{value_parser, ArgAction, Args, Parser, Subcommand};
use clap::{ArgAction, Args, Parser, Subcommand, value_parser};
use rayon::{iter::IntoParallelRefIterator, prelude::ParallelIterator};
use tempfile::NamedTempFile;
use tempfile::{NamedTempFile, TempDir};
use topological_sort::TopologicalSort;
use tracing::{debug_span, error, info, warn};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use zip::{CompressionMethod, DateTime, ZipArchive, write::SimpleFileOptions};
use crate::{
cli,
cli::{
self,
avb::{ImageOpener, TrustMethod},
},
crypto::{self, PassphraseSource, RsaSigningKey},
format::{
avb::{self, Descriptor, Header},
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcAlgo, VabcParams},
zip::ZipWriterWrapper,
},
patch::{
boot::{
@@ -52,28 +53,6 @@ use crate::{
util,
};
fn joined(into_iter: impl IntoIterator<Item = impl Display>) -> String {
use std::fmt::Write;
let mut result = String::new();
for (i, item) in into_iter.into_iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
write!(result, "{item}").expect("Failed to allocate");
}
result
}
fn sorted<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
let mut items = iter.collect::<Vec<_>>();
items.sort();
items
}
bitflags! {
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -225,7 +204,7 @@ fn patch_boot_images(
info!(
"Candidate boot images: {}",
joined(sorted(boot_partitions.iter())),
util::join(util::sort(boot_partitions.iter()), ", "),
);
boot::patch_boot_images(
@@ -248,7 +227,7 @@ fn patch_boot_images(
.with_context(|| {
format!(
"Failed to patch boot images: {}",
joined(sorted(boot_partitions.iter())),
util::join(util::sort(boot_partitions.iter()), ", "),
)
})?;
@@ -368,7 +347,7 @@ fn ensure_partitions_protected(
if !missing.is_empty() {
bail!(
"Found critical partitions that are not protected by AVB: {}",
joined(missing),
util::join(missing, ", "),
);
}
@@ -411,7 +390,10 @@ fn get_vbmeta_patch_order(
}
if !missing.is_empty() {
warn!("Partitions aren't protected by AVB: {}", joined(missing));
warn!(
"Partitions aren't protected by AVB: {}",
util::join(missing, ", "),
);
}
// Ensure that there's only a single root of trust. Otherwise, there could
@@ -427,7 +409,10 @@ fn get_vbmeta_patch_order(
// For zero roots, let TopologicalSort report the cycle.
if roots.len() > 1 {
bail!("Found multiple root vbmeta images: {}", joined(roots));
bail!(
"Found multiple root vbmeta images: {}",
util::join(roots, ", "),
);
}
// Compute the patching order. This only includes vbmeta images. All vbmeta
@@ -495,7 +480,9 @@ fn update_security_descriptors(
*pd = cd.clone();
}
_ => {
bail!("{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})");
bail!(
"{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})"
);
}
}
} else {
@@ -505,7 +492,9 @@ fn update_security_descriptors(
child_header.public_key.clone_into(&mut pd.public_key);
}
_ => {
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
bail!(
"{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor"
);
}
}
}
@@ -667,7 +656,7 @@ fn update_vbmeta_headers(
) -> Result<()> {
info!(
"Patching vbmeta images: {}",
joined(order.iter().map(|(n, _)| n)),
util::join(order.iter().map(|(n, _)| n), ", "),
);
for (name, deps) in order {
@@ -912,10 +901,10 @@ fn patch_ota_payload(
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;
}
if let Some(vabc_algo) = vabc_algo_override
&& set_vabc_algo(&mut header, vabc_algo)?
{
required_flags |= RequiredFlags::ALL_COW;
}
let all_partitions = header
@@ -1063,23 +1052,23 @@ fn patch_ota_payload(
// Try to copy from our replacement image. The compressed chunks are
// laid out sequentially and data_offset is set to the offset within
// that file.
if let Some((input_file, modified_operations)) = compressed_files.get_mut(&name) {
if util::ranges_contains(modified_operations, &oi) {
input_file
.file
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek image: {name}"))?;
if let Some((input_file, modified_operations)) = compressed_files.get_mut(&name)
&& util::ranges_contains(modified_operations, &oi)
{
input_file
.file
.seek(SeekFrom::Start(data_offset))
.with_context(|| format!("Failed to seek image: {name}"))?;
stream::copy_n(
&mut input_file.file,
&mut payload_writer,
data_length,
cancel_signal,
)
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
stream::copy_n(
&mut input_file.file,
&mut payload_writer,
data_length,
cancel_signal,
)
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
continue;
}
continue;
}
// Otherwise, copy from the original payload.
@@ -1111,7 +1100,7 @@ fn patch_ota_payload(
fn patch_ota_zip(
raw_reader: &PSeekFile,
zip_reader: &mut ZipArchive<impl Read + Seek>,
mut zip_writer: &mut ZipWriter<impl Write>,
mut zip_writer: &mut ZipWriterWrapper<impl Write>,
external_images: &HashMap<String, PathBuf>,
boot_patchers: &[Box<dyn BootImagePatch + Sync>],
skip_system_ota_cert: bool,
@@ -1137,7 +1126,7 @@ fn patch_ota_zip(
}
if !missing.is_empty() {
bail!("Missing entries in OTA zip: {}", joined(missing));
bail!("Missing entries in OTA zip: {}", util::join(missing, ", "));
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
bail!(
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
@@ -1165,7 +1154,8 @@ fn patch_ota_zip(
// threshold. This should be sufficient since the output file is likely
// to be larger.
let use_zip64 = reader.size() >= 0xffffffff;
let options = FileOptions::default()
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored)
.large_file(use_zip64);
@@ -1202,12 +1192,9 @@ fn patch_ota_zip(
}
// All remaining entries are written immediately.
zip_writer
.start_file_with_extra_data(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let offset = zip_writer
.end_extra_data()
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
.start_file(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let mut writer = CountingWriter::new(&mut zip_writer);
match path.as_str() {
@@ -1271,7 +1258,7 @@ fn patch_ota_zip(
let size = writer.stream_position()?;
entries.push(ZipEntry {
name: path.clone(),
path: path.clone(),
offset,
size,
});
@@ -1307,29 +1294,22 @@ fn patch_ota_zip(
pub fn extract_payload(
raw_reader: &PSeekFile,
directory: &Dir,
directory: &Path,
payload_offset: u64,
payload_size: u64,
header: &PayloadHeader,
images: &BTreeSet<String>,
cancel_signal: &AtomicBool,
) -> Result<()> {
for name in images {
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
info!("Extracting from the payload: {}", joined(images));
info!("Extracting from the payload: {}", util::join(images, ", "));
// Pre-open all output files.
let output_files = images
.iter()
.map(|name| {
let path = format!("{name}.img");
let file = directory
.create(&path)
.map(|f| PSeekFile::new(f.into_std()))
let path = util::path_join_single(directory, format!("{name}.img"))?;
let file = File::create(&path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok((name.as_str(), file))
})
@@ -1359,7 +1339,7 @@ pub fn extract_payload(
}
fn verify_partition_hashes(
directory: &Dir,
directory: &Path,
header: &PayloadHeader,
images: &BTreeSet<String>,
cancel_signal: &AtomicBool,
@@ -1379,9 +1359,8 @@ fn verify_partition_hashes(
.and_then(|info| info.hash.as_ref())
.ok_or_else(|| anyhow!("Hash not found for partition: {name}"))?;
let path = format!("{name}.img");
let file = directory
.open(&path)
let path = util::path_join_single(directory, format!("{name}.img"))?;
let file = File::open(&path)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let mut writer = HashingWriter::new(
@@ -1540,11 +1519,11 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
let mut zip_writer = match cli.zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(temp_writer);
ZipWriter::new_streaming(signing_writer)
ZipWriterWrapper::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(temp_writer);
ZipWriter::new(signing_writer)
ZipWriterWrapper::new_seekable(signing_writer)
}
};
@@ -1673,7 +1652,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
.collect::<Vec<_>>();
if !missing_images.is_empty() {
bail!("Invalid partitions: {}", joined(missing_images));
bail!("Invalid partitions: {}", util::join(missing_images, ", "));
}
unique_images.extend(cli.extract.partition.iter().cloned());
@@ -1716,15 +1695,12 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
return Ok(());
}
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.directory, authority)
fs::create_dir_all(&cli.directory)
.with_context(|| format!("Failed to create directory: {:?}", cli.directory))?;
let directory = Dir::open_ambient_dir(&cli.directory, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.directory))?;
extract_payload(
&raw_reader,
&directory,
&cli.directory,
payload_offset,
payload_size,
&header,
@@ -1756,9 +1732,9 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
.and_then(|p| p.device.first())
.ok_or_else(|| anyhow!("Device codename not found in OTA metadata"))?;
directory
.write(ANDROID_INFO, format!("require board={device}\n"))
.with_context(|| format!("Failed to write file: {ANDROID_INFO}"))?;
let android_info_path = util::path_join_single(&cli.directory, ANDROID_INFO)?;
fs::write(&android_info_path, format!("require board={device}\n"))
.with_context(|| format!("Failed to write file: {android_info_path:?}"))?;
// Find out which images can be flashed with fastboot. The bootloader
// (and potentially modem) partitions need to be flashed as a whole and
@@ -1767,9 +1743,9 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
let mut flashable_images = BTreeSet::new();
for name in &unique_images {
let file = directory
.open(format!("{name}.img"))
.with_context(|| format!("Failed to open image for reading: {name}"))?;
let path = util::path_join_single(&cli.directory, format!("{name}.img"))?;
let file = File::open(&path)
.with_context(|| format!("Failed to open image for reading: {path:?}"))?;
match avb::load_image(file) {
Ok(_) => {
@@ -1777,7 +1753,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
}
// Treat images without AVB metadata as bootloader partitions.
Err(avb::Error::InvalidHeaderMagic(_)) => continue,
Err(e) => return Err(e).with_context(|| format!("Failed to load image: {name}")),
Err(e) => return Err(e).with_context(|| format!("Failed to load image: {path:?}")),
}
}
@@ -1865,9 +1841,9 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
fastboot_info.push_str("if-wipe erase userdata\n");
fastboot_info.push_str("if-wipe erase metadata\n");
directory
.write(FASTBOOT_INFO, fastboot_info)
.with_context(|| format!("Failed to write file: {FASTBOOT_INFO}"))?;
let fastboot_info_path = util::path_join_single(&cli.directory, FASTBOOT_INFO)?;
fs::write(&fastboot_info_path, fastboot_info)
.with_context(|| format!("Failed to write file: {fastboot_info_path:?}"))?;
}
Ok(())
@@ -1933,7 +1909,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
.with_context(|| format!("Failed to parse property files: {}", ota::PF_NAME))?;
let pf_payload = pfs
.iter()
.find(|pf| pf.name == ota::PATH_PAYLOAD)
.find(|pf| pf.name() == ota::PATH_PAYLOAD)
.ok_or_else(|| anyhow!("Missing property files entry: {}", ota::PATH_PAYLOAD))?;
let section_reader = SectionReader::new(&mut reader, pf_payload.offset, pf_payload.size)
@@ -1948,8 +1924,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
info!("Extracting partition images to temporary directory");
let authority = ambient_authority();
let temp_dir = TempDir::new(authority).context("Failed to create temporary directory")?;
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
let raw_reader = reader.into_inner();
let unique_images = header
.manifest
@@ -1961,7 +1936,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
extract_payload(
&raw_reader,
&temp_dir,
temp_dir.path(),
pf_payload.offset,
pf_payload.size,
&header,
@@ -1971,29 +1946,31 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
info!("Verifying partition hashes");
if let Err(e) = verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal) {
if let Err(e) = verify_partition_hashes(temp_dir.path(), &header, &unique_images, cancel_signal)
{
fail_later!("{e:?}");
}
info!("Verifying AVB signatures");
let public_key = if let Some(p) = &cli.public_key_avb {
let trust_method = if let Some(p) = &cli.public_key_avb {
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
let key = avb::decode_public_key(&data)
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
Some(key)
TrustMethod::Key(key)
} else {
None
TrustMethod::Anything
};
let opener = ImageOpener::with_dir(temp_dir.path());
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
if let Err(e) = cli::avb::verify_headers(
&temp_dir,
&opener,
"vbmeta",
public_key.as_ref(),
&trust_method,
&mut seen,
&mut descriptors,
)
@@ -2002,8 +1979,14 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
fail_later!("{e:?}");
}
if let Err(e) = cli::avb::verify_descriptors(&temp_dir, &descriptors, false, cancel_signal)
.context("Failed to verify images against AVB descriptors")
if let Err(e) = cli::avb::verify_descriptors(
&opener,
&descriptors,
false,
!cli.fail_if_missing,
cancel_signal,
)
.context("Failed to verify images against AVB descriptors")
{
fail_later!("{e:?}");
}
@@ -2017,11 +2000,10 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
.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()))?,
))
let path = util::path_join_single(temp_dir.path(), format!("{name}.img"))
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
Ok(Box::new(File::open(path).map(PSeekFile::new)?))
})
.context("Failed to load all boot images")?;
let targets = OtaCertPatcher::new(ota_cert.clone())
@@ -2401,6 +2383,13 @@ pub struct VerifyCli {
/// same key.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_recovery_ota_cert: bool,
/// Fail if a referenced image is missing.
///
/// Missing images are ignored by default because some OTAs contain vbmeta
/// images referencing partitions that only exist on the real device.
#[arg(long)]
fail_if_missing: bool,
}
#[allow(clippy::large_enum_variant)]
+10 -25
View File
@@ -1,17 +1,16 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
ffi::OsString,
fs::{self, File},
io::{BufReader, BufWriter, Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use cap_std::{ambient_authority, fs::Dir};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Args, Parser, Subcommand};
use tracing::info;
@@ -20,6 +19,7 @@ use crate::{
crypto::{self, PassphraseSource, RsaSigningKey},
format::payload::{PayloadHeader, PayloadWriter},
stream::{self, FromReader, PSeekFile},
util,
};
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
@@ -113,15 +113,12 @@ fn unpack_subcommand(
write_info(&cli.output_info, &header)?;
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
fs::create_dir_all(&cli.output_images)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
let directory = Dir::open_ambient_dir(&cli.output_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?;
ota::extract_payload(
&PSeekFile::new(reader.into_inner()),
&directory,
&cli.output_images,
0,
payload_size,
&header,
@@ -147,28 +144,16 @@ fn pack_subcommand(
let mut header = read_info(&cli.input_info)?;
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for p in &header.manifest.partitions {
let name = &p.partition_name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
// Pre-open all of the image files.
let input_files = header
.manifest
.partitions
.iter()
.map(|p| {
let path = format!("{}.img", p.partition_name);
let file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
let path =
util::path_join_single(&cli.input_images, format!("{}.img", p.partition_name))?;
let file = File::open(&path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))?;
Ok((p.partition_name.clone(), file))
+6 -6
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -10,10 +10,10 @@ use std::{
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use clap::{Parser, Subcommand};
use crc32fast::Hasher;
use zerocopy::{little_endian, IntoBytes};
use zerocopy::{IntoBytes, little_endian};
use crate::{
format::{
@@ -317,11 +317,11 @@ fn unpack_subcommand(
})?;
}
ChunkData::Hole => {
// This cannot overflow.
let to_skip = chunk.bounds.len() * metadata.header.block_size;
// Unlike ChunkData::Data, this can overflow a u32.
let to_skip = i64::from(chunk.bounds.len()) * i64::from(metadata.header.block_size);
writer
.seek(SeekFrom::Current(to_skip.into()))
.seek(SeekFrom::Current(to_skip))
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
ChunkData::Crc32(_) => {}
+8 -8
View File
@@ -21,25 +21,25 @@ use cms::{
};
use passterm::PromptError;
use pkcs8::{
pkcs5::{pbes2, scrypt},
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
LineEnding, PrivateKeyInfo,
pkcs5::{pbes2, scrypt},
};
use rand::RngCore;
use rsa::{
pkcs1v15::SigningKey, traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey,
Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, pkcs1v15::SigningKey, traits::PublicKeyParts,
};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Digest, Sha256, Sha512};
use thiserror::Error;
use x509_cert::{
Certificate,
builder::{Builder, CertificateBuilder, Profile},
der::{pem::PemLabel, referenced::OwnedToRef, Any, Decode, DecodePem, EncodePem},
der::{Any, Decode, DecodePem, EncodePem, pem::PemLabel, referenced::OwnedToRef},
serial_number::SerialNumber,
spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned},
time::Validity,
Certificate,
};
use crate::util::DebugString;
@@ -160,10 +160,10 @@ impl PassphraseSource {
Err(e) => {
#[cfg(unix)]
if let PromptError::IOError(io_e) = e {
if let Some(errno) = io_e.raw_os_error() {
if errno == libc::ENXIO || errno == libc::ENOTTY {
return Err(Error::NotInteractive(io_e));
}
if let Some(errno) = io_e.raw_os_error()
&& (errno == libc::ENXIO || errno == libc::ENOTTY)
{
return Err(Error::NotInteractive(io_e));
}
return Err(Error::PassphrasePrompt(PromptError::IOError(io_e)));
+1 -1
View File
@@ -4,7 +4,7 @@
use std::{fmt, marker::PhantomData};
use bstr::{ByteSlice, ByteVec};
use serde::{de::Visitor, Deserializer, Serializer};
use serde::{Deserializer, Serializer, de::Visitor};
use thiserror::Error;
#[derive(Clone, Debug, Error)]
+23 -20
View File
@@ -14,10 +14,10 @@ use bstr::ByteSlice;
use num_bigint_dig::{ModInverse, ToBigInt};
use num_traits::{Pow, ToPrimitive};
use ring::digest::{Algorithm, Context};
use rsa::{traits::PublicKeyParts, BigUint, RsaPublicKey};
use rsa::{BigUint, RsaPublicKey, traits::PublicKeyParts};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{big_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, big_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -163,9 +163,9 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
pub(crate) fn digest_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
pub(crate) fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
match name {
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
"sha1" => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
"sha256" => Ok(&ring::digest::SHA256),
"sha512" => Ok(&ring::digest::SHA512),
a => Err(Error::UnsupportedHashAlgorithm(a.to_owned())),
@@ -534,7 +534,7 @@ impl HashTreeDescriptor {
ranges: Option<&[Range<u64>]>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let algorithm = digest_algorithm(&self.hash_algorithm, false)?;
let algorithm = digest_algorithm(&self.hash_algorithm)?;
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
let (root_digest, hash_tree_data) = match ranges {
Some(r) => {
@@ -642,7 +642,7 @@ impl HashTreeDescriptor {
) -> Result<()> {
self.check_offsets()?;
let algorithm = digest_algorithm(&self.hash_algorithm, true)?;
let algorithm = digest_algorithm(&self.hash_algorithm)?;
util::check_bounds(self.tree_size, ..=HASH_TREE_MAX_SIZE)
.map_err(|e| Error::IntOutOfBounds("HashTree::tree_size", e))?;
@@ -903,10 +903,9 @@ impl HashDescriptor {
fn calculate(
&self,
reader: impl Read,
for_verify: bool,
cancel_signal: &AtomicBool,
) -> Result<ring::digest::Digest> {
let algorithm = digest_algorithm(&self.hash_algorithm, for_verify)?;
let algorithm = digest_algorithm(&self.hash_algorithm)?;
let mut context = Context::new(algorithm);
context.update(&self.salt);
@@ -924,14 +923,14 @@ impl HashDescriptor {
/// Update the root hash from the input reader's contents.
pub fn update(&mut self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
let digest = self.calculate(reader, false, cancel_signal)?;
let digest = self.calculate(reader, cancel_signal)?;
self.root_digest = digest.as_ref().to_vec();
Ok(())
}
/// Verify the root hash against the input reader.
pub fn verify(&self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
let digest = self.calculate(reader, true, cancel_signal)?;
let digest = self.calculate(reader, cancel_signal)?;
if self.root_digest != digest.as_ref() {
return Err(Error::InvalidRootDigest {
@@ -1658,7 +1657,7 @@ impl Header {
/// Get the first hash or hash tree descriptor if there is only one. This is
/// the case for appended AVB images.
pub fn appended_descriptor(&self) -> Result<AppendedDescriptorRef> {
pub fn appended_descriptor(&self) -> Result<AppendedDescriptorRef<'_>> {
let mut result = None;
for descriptor in &self.descriptors {
@@ -1684,7 +1683,7 @@ impl Header {
/// Get the first hash or hash tree descriptor if there is only one. This is
/// the case for appended AVB images.
pub fn appended_descriptor_mut(&mut self) -> Result<AppendedDescriptorMut> {
pub fn appended_descriptor_mut(&mut self) -> Result<AppendedDescriptorMut<'_>> {
let mut result = None;
for descriptor in &mut self.descriptors {
@@ -1759,16 +1758,20 @@ impl Header {
/// and return the public key. If the header is not signed, then `None` is
/// returned.
pub fn verify(&self) -> Result<Option<RsaPublicKey>> {
// Reconstruct the public key.
let public_key = decode_public_key(&self.public_key)?;
if self.public_key.len() != self.algorithm_type.public_key_len() {
return Err(Error::IncorrectKeySize(
public_key.size(),
self.public_key.len(),
self.algorithm_type,
));
}
if self.algorithm_type == AlgorithmType::None {
return Ok(None);
}
// Reconstruct the public key.
let public_key = decode_public_key(&self.public_key)?;
let mut without_auth_writer = Cursor::new(Vec::new());
self.to_writer_internal(&mut without_auth_writer, true)?;
let without_auth = without_auth_writer.into_inner();
@@ -2175,10 +2178,10 @@ pub fn write_appended_image(
.and_then(|s| s.checked_add(header_padding))
.ok_or(Error::IntOverflow("Appended::header_end_padded"))?;
if let Some(s) = image_size {
if header_end_padded > s {
return Err(Error::TooSmallForHeader(s));
}
if let Some(s) = image_size
&& header_end_padded > s
{
return Err(Error::TooSmallForHeader(s));
}
writer
+13 -24
View File
@@ -13,7 +13,7 @@ use num_traits::ToPrimitive;
use ring::digest::Context;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -387,14 +387,13 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
None
};
if let Some(v1) = &v1_data {
if reader
if let Some(v1) = &v1_data
&& reader
.stream_position()
.map_err(|e| Error::DataRead("Boot::V1::header_size", e))?
!= u64::from(v1.header_size)
{
return Err(Error::InvalidHeaderSize(v1.header_size));
}
{
return Err(Error::InvalidHeaderSize(v1.header_size));
}
padding::read_discard(&mut reader, page_size.into())
@@ -669,11 +668,7 @@ impl fmt::Display for BootImageV3Through4 {
impl BootImageExt for BootImageV3Through4 {
fn header_version(&self) -> u32 {
if self.v4_extra.is_some() {
4
} else {
3
}
if self.v4_extra.is_some() { 4 } else { 3 }
}
fn header_size(&self) -> u32 {
@@ -874,14 +869,12 @@ impl BootImageV3Through4 {
padding::write_zeros(&mut writer, PAGE_SIZE.into())
.map_err(|e| Error::DataWrite("Boot::V3::ramdisk_padding", e))?;
if !skip_v4_sig {
if let Some(sig) = v4_signature {
writer
.write_all(&sig)
.map_err(|e| Error::DataWrite("Boot::V4::signature", e))?;
padding::write_zeros(&mut writer, PAGE_SIZE.into())
.map_err(|e| Error::DataWrite("Boot::V4::signature_padding", e))?;
}
if !skip_v4_sig && let Some(sig) = v4_signature {
writer
.write_all(&sig)
.map_err(|e| Error::DataWrite("Boot::V4::signature", e))?;
padding::write_zeros(&mut writer, PAGE_SIZE.into())
.map_err(|e| Error::DataWrite("Boot::V4::signature_padding", e))?;
}
Ok(())
@@ -1094,11 +1087,7 @@ impl fmt::Display for VendorBootImageV3Through4 {
impl BootImageExt for VendorBootImageV3Through4 {
fn header_version(&self) -> u32 {
if self.v4_extra.is_some() {
4
} else {
3
}
if self.v4_extra.is_some() { 4 } else { 3 }
}
fn header_size(&self) -> u32 {
+19 -19
View File
@@ -1,15 +1,11 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use liblzma::{
read::XzDecoder,
stream::{Check, Stream},
write::XzEncoder,
};
use flate2::{Compression, read::GzDecoder, write::GzEncoder};
use lz4_flex::frame::FrameDecoder;
use lzma_rust2::{CheckType, XZOptions, XZReader, XZWriter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -28,7 +24,7 @@ pub enum Error {
#[error("Failed to initialize legacy LZ4 encoder")]
Lz4Init(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
XzInit(#[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -114,14 +110,14 @@ pub enum CompressedFormat {
Xz,
}
pub enum CompressedReader<R: Read> {
pub enum CompressedReader<'reader, R: Read> {
None(R),
Gzip(GzDecoder<R>),
Lz4(FrameDecoder<R>),
Xz(XzDecoder<R>),
Xz(XZReader<'reader, R>),
}
impl<R: Read + Seek> CompressedReader<R> {
impl<'reader, R: Read + Seek + 'reader> CompressedReader<'reader, R> {
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
let magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
@@ -132,7 +128,7 @@ impl<R: Read + Seek> CompressedReader<R> {
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(XzDecoder::new(reader)))
Ok(Self::Xz(XZReader::new(reader, false)))
} else if raw_if_unknown {
Ok(Self::None(reader))
} else {
@@ -159,7 +155,7 @@ impl<R: Read + Seek> CompressedReader<R> {
}
}
impl<R: Read> Read for CompressedReader<R> {
impl<'reader, R: Read + 'reader> Read for CompressedReader<'reader, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::None(r) => r.read(buf),
@@ -170,14 +166,15 @@ impl<R: Read> Read for CompressedReader<R> {
}
}
pub enum CompressedWriter<W: Write> {
#[allow(clippy::large_enum_variant)]
pub enum CompressedWriter<'writer, W: Write> {
None(W),
Gzip(GzEncoder<W>),
Lz4Legacy(Lz4LegacyEncoder<W>),
Xz(XzEncoder<W>),
Xz(XZWriter<'writer, W>),
}
impl<W: Write> CompressedWriter<W> {
impl<'writer, W: Write + 'writer> CompressedWriter<'writer, W> {
pub fn new(writer: W, format: CompressedFormat) -> Result<Self> {
match format {
CompressedFormat::None => Ok(Self::None(writer)),
@@ -190,8 +187,11 @@ impl<W: Write> CompressedWriter<W> {
}
CompressedFormat::Xz => {
// Some kernels are compiled without support for the default CRC64.
let stream = Stream::new_easy_encoder(6, Check::Crc32).map_err(Error::XzInit)?;
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
let mut options = XZOptions::with_preset(6);
options.set_check_sum_type(CheckType::Crc32);
let xz_writer = XZWriter::new(writer, options).map_err(Error::XzInit)?;
Ok(Self::Xz(xz_writer))
}
}
}
@@ -215,7 +215,7 @@ impl<W: Write> CompressedWriter<W> {
}
}
impl<W: Write> Write for CompressedWriter<W> {
impl<'writer, W: Write + 'writer> Write for CompressedWriter<'writer, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::None(w) => w.write(buf),
+1 -5
View File
@@ -771,11 +771,7 @@ pub fn sort(entries: &mut [CpioEntry]) {
/// 300000.
pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()> {
fn next_non_zero(i: u32) -> u32 {
if i == u32::MAX {
1
} else {
i.wrapping_add(1)
}
if i == u32::MAX { 1 } else { i.wrapping_add(1) }
}
// (dev maj, dev min) -> (inode set, last assigned inode)
+2 -2
View File
@@ -16,7 +16,7 @@ use rayon::{
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -827,7 +827,7 @@ impl<W: Write> ToWriter<W> for FecImage {
mod tests {
use std::{
io::{Cursor, Seek},
sync::{atomic::AtomicBool, Arc},
sync::{Arc, atomic::AtomicBool},
};
use assert_matches::assert_matches;
+2 -2
View File
@@ -16,7 +16,7 @@ use rayon::{
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use zerocopy::{little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -493,7 +493,7 @@ impl HashTreeImage {
const VERSION: u16 = 1;
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::digest_algorithm(name, false)
avb::digest_algorithm(name)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned().into_bytes()))
}
+3 -3
View File
@@ -13,7 +13,7 @@ use bitflags::bitflags;
use bstr::ByteSlice;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromBytes, FromZeros, Immutable, IntoBytes};
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
@@ -21,7 +21,7 @@ use crate::{
stream::{
CountingReader, FromReader, ReadDiscardExt, ReadFixedSizeExt, ToWriter, WriteZerosExt,
},
util::{self, is_zero, DebugString},
util::{self, DebugString, is_zero},
};
/// Magic value for [`RawGeometry::magic`].
@@ -812,7 +812,7 @@ impl RawExtent {
return Err(Error::ExtentInvalidType {
index,
extent_type: n,
})
});
}
}
+2 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod avb;
@@ -13,3 +13,4 @@ pub mod padding;
pub mod payload;
pub mod sparse;
pub mod verityrs;
pub mod zip;
+200 -87
View File
@@ -2,29 +2,35 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
collections::BTreeMap,
fmt,
fmt::{self, Write as _},
io::{self, Cursor, Read, Seek, SeekFrom, Write},
iter,
path::Path,
str::FromStr,
sync::atomic::AtomicBool,
};
use clap::ValueEnum;
use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
use const_oid::{ObjectIdentifier, db::rfc5912};
use memchr::memmem;
use prost::Message;
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use x509_cert::{der::Encode, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use x509_cert::{Certificate, der::Encode};
use zip::{CompressionMethod, DateTime, ZipArchive, result::ZipError, write::SimpleFileOptions};
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
format::payload::{self, PayloadHeader},
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
format::{
payload::{self, PayloadHeader},
zip::ZipWriterWrapper,
},
protobuf::build::tools::releasetools::{OtaMetadata, ota_metadata::OtaType},
stream::{self, FromReader, HashingReader, HashingWriter, ReadFixedSizeExt},
util,
};
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
@@ -70,8 +76,12 @@ pub enum Error {
InvalidLegacyMetadataLine(String),
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
UnsupportedLegacyMetadataField { key: String, value: String },
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
MismatchedPropertyFiles { expected: String, actual: String },
#[error("Mismatched {key:?} entry offsets: zip only: {zip_only:?}, prop only: {prop_only:?}")]
MismatchedPropertyFiles {
key: String,
zip_only: String,
prop_only: String,
},
#[error("Property files {value:?} exceed {reserved} byte reserved space")]
InsufficientReservedSpace { value: String, reserved: usize },
#[error("Invalid property file entry: {0:?}")]
@@ -280,16 +290,48 @@ fn serialize_metadata(metadata: &OtaMetadata) -> (String, Vec<u8>) {
#[derive(Clone, Debug)]
pub struct ZipEntry {
pub name: String,
pub path: String,
pub offset: u64,
pub size: u64,
}
/// Parse OTA property files string.
pub fn parse_property_files(data: &str) -> Result<Vec<ZipEntry>> {
let mut result = vec![];
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PropEntry {
name: String,
pub offset: u64,
pub size: u64,
}
for entry in data.trim_end().split(',') {
impl PropEntry {
pub fn new(path: &str, offset: u64, size: u64) -> Self {
Self {
name: property_file_name(path).to_owned(),
offset,
size,
}
}
pub fn name(&self) -> &str {
&self.name
}
}
impl From<&ZipEntry> for PropEntry {
fn from(entry: &ZipEntry) -> Self {
Self::new(&entry.path, entry.offset, entry.size)
}
}
impl fmt::Display for PropEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.name, self.offset, self.size)
}
}
impl FromStr for PropEntry {
type Err = Error;
fn from_str(entry: &str) -> Result<Self> {
let mut pieces = entry.split(':');
let name = pieces
@@ -309,38 +351,63 @@ pub fn parse_property_files(data: &str) -> Result<Vec<ZipEntry>> {
return Err(Error::InvalidPropertyFileEntry(entry.to_owned()));
}
result.push(ZipEntry { name, offset, size });
Ok(Self { name, offset, size })
}
}
/// Parse OTA property files string.
pub fn parse_property_files(data: &str) -> Result<Vec<PropEntry>> {
let mut result = vec![];
for entry in data.trim_end().split(',') {
result.push(entry.parse()?);
}
Ok(result)
}
/// Get the filename for use in property files entries.
fn property_file_name(path: &str) -> &str {
path.rsplit_once('/').map_or(path, |p| p.1)
}
/// Compute the property files entries listing the offsets and sizes to every
/// zip entry.
fn compute_property_files(
pf_name: &str,
entries: &[ZipEntry],
entries: &[PropEntry],
max_length: Option<usize>,
want_pb: bool,
) -> Result<String> {
let compute = |path: &'static str| -> Result<String> {
// AOSP's ota_utils.py reserves 15 bytes for the `<offset>:<size>`
// placeholder. Since the size of `metadata.pb` is almost always 4 digits,
// this prevents the offset from exceeding 10 digits. In the wild, there are
// OTA files larger than 10 GB. With ota_utils.py, this limit is never
// reached because it puts the OTA metadata files at the beginning of the
// zip. However, avbroot needs to put them at the end due to streaming
// writes, so we reserve an additional byte to allow offsets <100 GB.
const RESERVATION_SIZE: usize = 16;
let mut buf = String::new();
let mut append = |path: &'static str| -> Result<()> {
let name = property_file_name(path);
let entry = entries
.iter()
.find(|e| e.name == path)
.find(|e| e.name == name)
.ok_or(Error::MissingZipEntry(path))?;
let name = path.rsplit_once('/').map_or(path, |p| p.1);
Ok(format!("{name}:{}:{}", entry.offset, entry.size))
let _ = write!(&mut buf, "{entry},");
Ok(())
};
let mut tokens = vec![];
if pf_name == PF_NAME {
tokens.push(compute(NAME_PAYLOAD_METADATA)?);
append(NAME_PAYLOAD_METADATA)?;
}
for path in [PATH_PAYLOAD, PATH_PROPERTIES] {
tokens.push(compute(path)?);
append(path)?;
}
for path in [
@@ -349,44 +416,51 @@ fn compute_property_files(
"care_map.txt",
"compatibility.zip",
] {
if let Ok(token) = compute(path) {
tokens.push(token);
}
// These are optional.
let _ = append(path);
}
if max_length.is_none() {
tokens.push(format!("metadata:{}", " ".repeat(15)));
buf.push_str(property_file_name(PATH_METADATA));
buf.push(':');
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
buf.push(',');
if want_pb {
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
buf.push_str(property_file_name(PATH_METADATA_PB));
buf.push(':');
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
buf.push(',');
}
} else {
tokens.push(compute(PATH_METADATA)?);
append(PATH_METADATA)?;
if want_pb {
tokens.push(compute(PATH_METADATA_PB)?);
append(PATH_METADATA_PB)?;
}
}
let mut joined = tokens.join(",");
// Strip final trailing comma.
buf.pop();
if let Some(l) = max_length {
if joined.len() > l {
if buf.len() > l {
return Err(Error::InsufficientReservedSpace {
value: joined,
value: buf,
reserved: l,
});
}
let remain = l - joined.len();
joined.extend(iter::repeat_n(' ', remain));
let remain = l - buf.len();
buf.extend(iter::repeat_n(' ', remain));
}
Ok(joined)
Ok(buf)
}
// Add fake payload_metadata.bin entry, covering the header + header signature
// regions of the payload.
fn add_payload_metadata_entry(
entries: &mut Vec<ZipEntry>,
entries: &mut Vec<PropEntry>,
payload_metadata_size: u64,
) -> Result<()> {
let payload_offset = entries
@@ -394,11 +468,11 @@ fn add_payload_metadata_entry(
.find(|e| e.name == PATH_PAYLOAD)
.ok_or(Error::MissingZipEntry(PATH_PAYLOAD))?
.offset;
entries.push(ZipEntry {
name: NAME_PAYLOAD_METADATA.to_owned(),
offset: payload_offset,
size: payload_metadata_size,
});
entries.push(PropEntry::new(
NAME_PAYLOAD_METADATA,
payload_offset,
payload_metadata_size,
));
Ok(())
}
@@ -426,17 +500,19 @@ impl fmt::Display for ZipMode {
/// directory would start.
pub fn add_metadata(
zip_entries: &[ZipEntry],
zip_writer: &mut ZipWriter<impl Write>,
zip_writer: &mut ZipWriterWrapper<impl Write>,
next_offset: u64,
metadata: &OtaMetadata,
payload_metadata_size: u64,
zip_mode: ZipMode,
) -> Result<OtaMetadata> {
let mut metadata = metadata.clone();
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored);
let mut zip_entries = zip_entries.to_owned();
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
let mut prop_entries = zip_entries.iter().map(PropEntry::from).collect();
add_payload_metadata_entry(&mut prop_entries, payload_metadata_size)?;
// Compute initial property files with reserved space as placeholders to
// store the self-referential metadata entries later.
@@ -444,7 +520,7 @@ pub fn add_metadata(
for pf in [PF_NAME, PF_STREAMING_NAME] {
metadata.property_files.insert(
pf.to_owned(),
compute_property_files(pf, &zip_entries, None, true)?,
compute_property_files(pf, &prop_entries, None, true)?,
);
}
@@ -453,68 +529,56 @@ pub fn add_metadata(
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
let raw_writer = Cursor::new(Vec::new());
let mut writer = match zip_mode {
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
ZipMode::Seekable => ZipWriter::new(raw_writer),
ZipMode::Streaming => ZipWriterWrapper::new_streaming(raw_writer),
ZipMode::Seekable => ZipWriterWrapper::new_seekable(raw_writer),
};
writer
.start_file_with_extra_data(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
let legacy_offset = writer
.end_extra_data()
.start_file(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
writer
.write_all(legacy_raw.as_bytes())
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
writer
.start_file_with_extra_data(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
let modern_offset = writer
.end_extra_data()
.start_file(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
writer
.write_all(&modern_raw)
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
zip_entries.push(ZipEntry {
name: PATH_METADATA.to_owned(),
offset: next_offset + legacy_offset,
size: legacy_raw.len() as u64,
});
zip_entries.push(ZipEntry {
name: PATH_METADATA_PB.to_owned(),
offset: next_offset + modern_offset,
size: modern_raw.len() as u64,
});
prop_entries.push(PropEntry::new(
PATH_METADATA,
next_offset + legacy_offset,
legacy_raw.len() as u64,
));
prop_entries.push(PropEntry::new(
PATH_METADATA_PB,
next_offset + modern_offset,
modern_raw.len() as u64,
));
(next_offset + legacy_offset, next_offset + modern_offset)
};
// Compute the final property files using the offsets of the fake entries.
for (key, value) in &mut metadata.property_files {
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
*value = compute_property_files(key, &prop_entries, Some(value.len()), true)?;
}
// Add the final metadata files to the real zip.
{
let (legacy_raw, modern_raw) = serialize_metadata(&metadata);
zip_writer
.start_file_with_extra_data(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
let legacy_offset = zip_writer
.end_extra_data()
.start_file(PATH_METADATA, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
zip_writer
.write_all(legacy_raw.as_bytes())
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
zip_writer
.start_file_with_extra_data(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
let modern_offset = zip_writer
.end_extra_data()
.start_file(PATH_METADATA_PB, options)
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
zip_writer
.write_all(&modern_raw)
@@ -540,24 +604,73 @@ pub fn verify_metadata(
let entry = zip_reader
.by_index(i)
.map_err(|e| Error::ZipIndexOpen(i, e))?;
zip_entries.push(ZipEntry {
name: entry.name().to_owned(),
offset: entry.data_start(),
size: entry.size(),
});
if entry.compression() != CompressionMethod::Stored {
continue;
}
zip_entries.push(PropEntry::new(
entry.name(),
entry.data_start(),
entry.size(),
));
}
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
zip_entries.sort_by(|a, b| a.name.cmp(&b.name));
for (key, value) in &metadata.property_files {
let new_value =
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
if *value != new_value {
let mut prop_entries = parse_property_files(value)?;
prop_entries.sort_by(|a, b| a.name.cmp(&b.name));
// Check that this is a subset of the actual entries.
let mut zip_iter = zip_entries.iter().peekable();
let mut prop_iter = prop_entries.iter().peekable();
let mut zip_only = vec![];
let mut prop_only = vec![];
loop {
match (zip_iter.peek(), prop_iter.peek()) {
(Some(&zip), Some(&prop)) => match zip.name.cmp(&prop.name) {
Ordering::Less => {
// Exists in zip, but not in property files.
zip_iter.next();
}
Ordering::Equal => {
// If the zip had multiple files with the same filename,
// but in different directories, this will fail.
if zip != prop {
zip_only.push(zip);
prop_only.push(prop);
}
zip_iter.next();
prop_iter.next();
}
Ordering::Greater => {
// Exists in property files, but not in zip.
prop_only.push(prop);
prop_iter.next();
}
},
(Some(_), None) => {
// Exists in zip, but not in property files.
zip_iter.next();
}
(None, Some(prop)) => {
// Exists in property files, but not in zip.
prop_only.push(prop);
prop_iter.next();
}
(None, None) => break,
}
}
if !zip_only.is_empty() || !prop_only.is_empty() {
return Err(Error::MismatchedPropertyFiles {
expected: value.clone(),
actual: new_value,
key: key.clone(),
zip_only: util::join(zip_only.into_iter().map(|e| e.to_string()), ","),
prop_only: util::join(prop_only.into_iter().map(|e| e.to_string()), ","),
});
}
}
+23 -26
View File
@@ -11,15 +11,11 @@ use std::{
sync::atomic::AtomicBool,
};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use bzip2::write::BzDecoder;
use flate2::{write::GzEncoder, Compression};
use liblzma::{
stream::{Check, Stream},
write::XzDecoder,
write::XzEncoder,
};
use flate2::{Compression, write::GzEncoder};
use lzma_rust2::{CheckType, XZOptions, XZReader, XZWriter};
use num_traits::CheckedAdd;
use prost::Message;
use rayon::{
@@ -30,18 +26,18 @@ use ring::digest::{Context, Digest};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use x509_cert::Certificate;
use zerocopy::{big_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, big_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
protobuf::chromeos_update_engine::{
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
DeltaArchiveManifest, Extent, InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
install_operation::Type, signatures::Signature,
},
stream::{
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadFixedSizeExt,
ReadSeekReopen, WriteSeek, WriteSeekReopen,
self, CountingReader, FromReader, HashingReader, HashingWriter, ReadDiscardExt,
ReadFixedSizeExt, ReadSeekReopen, WriteSeek, WriteSeekReopen,
},
util::{self, OutOfBoundsError},
};
@@ -121,7 +117,9 @@ pub enum Error {
DataWrite(&'static str, #[source] io::Error),
#[error("Expected {expected} bytes, but only wrote {actual} bytes")]
UnwrittenData { actual: u64, expected: u64 },
#[error("I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}")]
#[error(
"I/O error when applying {op_type:?} operation for {num_blocks} blocks starting at {start_block}"
)]
OperationApply {
op_type: Type,
start_block: u64,
@@ -137,7 +135,7 @@ pub enum Error {
#[error("Failed to GZ compress partition image chunk")]
GzCompress(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
XzInit(#[source] io::Error),
#[error("Failed to XZ compress partition image chunk")]
XzCompress(#[source] io::Error),
#[error("Failed to read uncompressed input partition image chunk")]
@@ -816,16 +814,14 @@ pub fn apply_operation(
.map_err(error_fn)?;
}
Type::ReplaceXz => {
let mut decoder = XzDecoder::new(&mut writer);
stream::copy_n_inspect(
&mut reader,
&mut decoder,
data_length,
|data| hasher.update(data),
cancel_signal,
)
.and_then(|()| decoder.finish())
.map_err(error_fn)?;
// lzma_rust2 does not have a Write API, so we limit the
// reader and read till EOF.
let limited_reader = (&mut reader).take(data_length);
let hashing_reader = HashingReader::new(limited_reader, hasher);
let mut decoder = XZReader::new(hashing_reader, false);
stream::copy(&mut decoder, &mut writer, cancel_signal).map_err(error_fn)?;
(_, hasher) = decoder.into_inner().finish();
}
_ => return Err(Error::UnsupportedOperation(op.r#type())),
}
@@ -945,8 +941,9 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
// decompression. Also, we intentionally pick the lowest compression level
// since we primarily care about squishing zeros. The non-zero portions of
// boot images are usually already-compressed kernels and ramdisks.
let stream = Stream::new_easy_encoder(0, Check::None).map_err(Error::XzInit)?;
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
let mut options = XZOptions::with_preset(0);
options.set_check_sum_type(CheckType::None);
let mut xz_writer = XZWriter::new(hashing_writer, options).map_err(Error::XzInit)?;
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)
.map_err(Error::XzCompress)?;
+11 -8
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -11,7 +11,7 @@ use std::{
use crc32fast::Hasher;
use dlv_list::{Index, VecList};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::stream::ReadDiscardExt;
@@ -225,7 +225,7 @@ impl RawChunk {
return Err(Error::InvalidChunkType {
index,
chunk_type: t,
})
});
}
};
@@ -374,6 +374,9 @@ impl fmt::Debug for ChunkData {
/// metadata they contain.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Chunk {
/// When [`Self::data`] is [`ChunkData::Data`], this is guaranteed to not
/// exceed the bounds of [`u32`] when multiplied by [`Header::block_size`].
/// For other types of data, a 64-bit signed or unsigned integer is needed.
pub bounds: ChunkBounds,
pub data: ChunkData,
}
@@ -519,11 +522,11 @@ impl ChunkList {
// entire list every time.
let mut insert_before = self.chunks.front_index();
if let Some(last_used) = self.last_used {
if chunk.bounds.start >= self.chunks.get(last_used).unwrap().bounds.start {
// The new chunk starts after the last used chunk.
insert_before = Some(last_used);
}
if let Some(last_used) = self.last_used
&& chunk.bounds.start >= self.chunks.get(last_used).unwrap().bounds.start
{
// The new chunk starts after the last used chunk.
insert_before = Some(last_used);
}
while let Some(index) = insert_before {
+103
View File
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: 2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Seek, SeekFrom, Write};
use zip::{
ZipWriter,
result::ZipResult,
write::{FileOptionExtension, FileOptions, StreamWriter},
};
/// A wrapper around a seekable writer. `W` must implement [`Seek`], but only
/// during the creation of a new instance. The resulting type can be stored in a
/// parent container where the generic type does not implement [`Seek`].
pub struct SeekWriter<W: Write> {
inner: W,
seek_fn: fn(&mut W, SeekFrom) -> io::Result<u64>,
}
impl<W: Write> SeekWriter<W> {
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write + Seek> SeekWriter<W> {
pub fn new(inner: W) -> Self {
Self {
inner,
seek_fn: W::seek,
}
}
}
impl<W: Write> Write for SeekWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<W: Write> Seek for SeekWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
(self.seek_fn)(&mut self.inner, pos)
}
}
/// This is an ugly hack to have a single type represent both seekable and
/// streaming [`ZipWriter`]s. `W` only needs to implement [`Seek`] when creating
/// a seekable instance via [`Self::new_seekable`].
pub enum ZipWriterWrapper<W: Write> {
Streaming(ZipWriter<StreamWriter<W>>),
Seekable(ZipWriter<SeekWriter<W>>),
}
impl<W: Write + Seek> ZipWriterWrapper<W> {
pub fn new_seekable(inner: W) -> Self {
Self::Seekable(ZipWriter::new(SeekWriter::new(inner)))
}
}
impl<W: Write> ZipWriterWrapper<W> {
pub fn new_streaming(inner: W) -> Self {
Self::Streaming(ZipWriter::new_stream(inner))
}
pub fn start_file(
&mut self,
name: impl ToString,
options: FileOptions<impl FileOptionExtension>,
) -> ZipResult<u64> {
match self {
Self::Streaming(z) => z.start_file(name, options),
Self::Seekable(z) => z.start_file(name, options),
}
}
pub fn finish(self) -> ZipResult<W> {
match self {
Self::Streaming(z) => Ok(z.finish()?.into_inner()),
Self::Seekable(z) => Ok(z.finish()?.into_inner()),
}
}
}
impl<W: Write> Write for ZipWriterWrapper<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::Streaming(z) => z.write(buf),
Self::Seekable(z) => z.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::Streaming(z) => z.flush(),
Self::Seekable(z) => z.flush(),
}
}
}
+1 -1
View File
@@ -4,8 +4,8 @@
use std::{
process::ExitCode,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
+1 -1
View File
@@ -10,7 +10,7 @@ use std::{
};
use num_traits::{Num, PrimInt};
use serde::{de::Visitor, Deserializer, Serializer};
use serde::{Deserializer, Serializer, de::Visitor};
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
+31 -32
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::{
@@ -15,18 +15,15 @@ use std::{
};
use bstr::ByteSlice;
use liblzma::{
stream::{Check, Stream},
write::XzEncoder,
};
use lzma_rust2::{CheckType, XZOptions, XZWriter};
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
use regex::bytes::Regex;
use ring::digest::Context;
use rsa::RsaPublicKey;
use thiserror::Error;
use tracing::{debug, debug_span, trace, warn, Span};
use tracing::{Span, debug, debug_span, trace, warn};
use x509_cert::Certificate;
use zip::{result::ZipError, ZipArchive};
use zip::{ZipArchive, result::ZipError};
use crate::{
crypto::{self, RsaSigningKey},
@@ -38,6 +35,7 @@ use crate::{
},
patch::otacert::{self, OtaCertBuildFlags},
stream::{self, FromReader, HashingWriter, ReadSeek, SectionReader, ToWriter, WriteSeek},
util,
};
#[derive(Debug, Error)]
@@ -83,7 +81,7 @@ pub enum Error {
#[error("Failed to generate replacement otacerts zip")]
OtaCertZip(#[source] otacert::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
XzInit(#[source] io::Error),
#[error("Failed to XZ compress entry: {:?}", .0.as_bstr())]
XzCompress(Vec<u8>, #[source] io::Error),
#[error("Failed to open zip file: {0:?}")]
@@ -194,7 +192,7 @@ impl MagiskRootPatcher {
// replaced by PREINITDEVICE
// - Versions newer than the latest supported version are assumed to support
// the same features as the latest version
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28200];
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..30300];
const VER_PREINIT_DEVICE: RangeFrom<u32> = 25211..;
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
const VER_PATCH_VBMETA: Range<u32> = Self::VERS_SUPPORTED[0].start..26202;
@@ -288,9 +286,11 @@ impl MagiskRootPatcher {
}
fn xz_compress(name: &[u8], reader: impl Read, cancel_signal: &AtomicBool) -> Result<Vec<u8>> {
let stream = Stream::new_easy_encoder(9, Check::Crc32).map_err(Error::XzInit)?;
let mut options = XZOptions::with_preset(9);
options.set_check_sum_type(CheckType::None);
let raw_writer = Cursor::new(Vec::new());
let mut writer = XzEncoder::new_stream(raw_writer, stream);
let mut writer = XZWriter::new(raw_writer, options).map_err(Error::XzInit)?;
let raw_writer = stream::copy(reader, &mut writer, cancel_signal)
.and_then(|_| writer.finish())
@@ -378,14 +378,12 @@ impl MagiskRootPatcher {
let mut new_data = None;
if xz_compress {
if let CpioEntryData::Data(data) = &old_entry.data {
new_path.extend(b".xz");
if xz_compress && let CpioEntryData::Data(data) = &old_entry.data {
new_path.extend(b".xz");
let reader = Cursor::new(data);
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
new_data = Some(CpioEntryData::Data(buf));
}
let reader = Cursor::new(data);
let buf = Self::xz_compress(&new_path, reader, cancel_signal)?;
new_data = Some(CpioEntryData::Data(buf));
}
new_entries.push(CpioEntry {
@@ -544,10 +542,10 @@ impl BootImagePatch for MagiskRootPatcher {
magisk_config.push_str("RECOVERYMODE=false\n");
if Self::VER_PREINIT_DEVICE.contains(&self.version) {
if let Some(device) = &self.preinit_device {
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
}
if Self::VER_PREINIT_DEVICE.contains(&self.version)
&& let Some(device) = &self.preinit_device
{
writeln!(&mut magisk_config, "PREINITDEVICE={device}").unwrap();
}
// Magisk normally saves the original SHA1 digest in its config file. It
@@ -930,16 +928,17 @@ impl PrepatchedImagePatcher {
return Ok(None);
};
let kmi_version = captures
.iter()
// Capture #0 is the entire match.
.skip(1)
.flatten()
.map(|c| c.as_bytes())
// Our regex only matches ASCII bytes.
.map(|c| std::str::from_utf8(c).unwrap())
.collect::<Vec<_>>()
.join("-");
let kmi_version = util::join(
captures
.iter()
// Capture #0 is the entire match.
.skip(1)
.flatten()
.map(|c| c.as_bytes())
// Our regex only matches ASCII bytes.
.map(|c| std::str::from_utf8(c).unwrap()),
"-",
);
Ok(Some(kmi_version))
}
+9 -7
View File
@@ -6,8 +6,8 @@ use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
use bitflags::bitflags;
use thiserror::Error;
use tracing::trace;
use x509_cert::{der::asn1::BitString, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
use x509_cert::{Certificate, der::asn1::BitString};
use zip::{CompressionMethod, DateTime, ZipWriter, result::ZipError, write::SimpleFileOptions};
use crate::{crypto, format::ota};
@@ -79,7 +79,9 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
CompressionMethod::Stored
};
let options = FileOptions::default().compression_method(compression_method);
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(compression_method);
let name = "ota.x509.pem";
writer.start_file(name, options).map_err(Error::ZipWrite)?;
@@ -93,10 +95,10 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
modified.signature =
BitString::from_bytes(&[]).expect("Empty ASN.1 bit string was invalid");
}
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
extensions.clear();
}
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS)
&& let Some(extensions) = &mut modified.tbs_certificate.extensions
{
extensions.clear();
}
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
modified.tbs_certificate.issuer.0.clear();
+4 -18
View File
@@ -10,7 +10,7 @@ use std::{
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use thiserror::Error;
use tracing::{debug, debug_span, trace, Span};
use tracing::{Span, debug, debug_span, trace};
use x509_cert::Certificate;
use zip::ZipArchive;
@@ -190,23 +190,9 @@ pub fn patch_system_image(
return Err(Error::OldZipNotFound);
}
let update_ranges = if descriptor.hash_algorithm == "sha1" {
// Promote to a secure algorithm. SHA1 is allowed for verification only.
// The entire hash tree and FEC data will need to be recomputed.
let new_algorithm = "sha256".to_owned();
debug!(
"Changing insecure hash algorithm {} to {new_algorithm}",
descriptor.hash_algorithm,
);
descriptor.hash_algorithm = new_algorithm;
None
} else {
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
Some(modified_ranges.as_slice())
};
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
let update_ranges = Some(modified_ranges.as_slice());
descriptor
.update(input, output, update_ranges, cancel_signal)
+19 -13
View File
@@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::File,
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
atomic::{AtomicBool, Ordering},
},
};
@@ -603,12 +603,14 @@ pub fn copy_n(
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
}
/// Copy data from `reader` to `writer` until `reader` reaches EOF. If `writer`
/// reaches EOF before `reader` does, an error is returned. The operation is
/// cancelled on the next loop iteration if `cancel_signal` is set to `true`.
pub fn copy(
/// Copy data from `reader` to `writer` until `reader` reaches EOF, invoking
/// `inspect` after every buffer read iteration. If `writer` reaches EOF before
/// `reader` does, an error is returned. The operation is cancelled on the next
/// loop iteration if `cancel_signal` is set to `true`.
pub fn copy_inspect(
mut reader: impl Read,
mut writer: impl Write,
mut inspect: impl FnMut(&[u8]),
cancel_signal: &AtomicBool,
) -> io::Result<u64> {
let mut buf = [0u8; 16384];
@@ -622,6 +624,8 @@ pub fn copy(
break;
}
inspect(&buf[..n]);
writer.write_all(&buf[..n])?;
copied += n as u64;
@@ -630,6 +634,11 @@ pub fn copy(
Ok(copied)
}
/// Copy data from `reader` to `writer` until `reader` reaches EOF.
pub fn copy(reader: impl Read, writer: impl Write, cancel_signal: &AtomicBool) -> io::Result<u64> {
copy_inspect(reader, writer, |_| {}, cancel_signal)
}
#[cfg(test)]
mod tests {
use std::{
@@ -822,21 +831,19 @@ mod tests {
let mut reader = Cursor::new(b"foobar");
let mut writer = Cursor::new([0u8; 6]);
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap();
super::copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap();
assert_eq!(writer.get_ref(), b"foobar");
// Reader early EOF.
reader.seek(SeekFrom::Start(3)).unwrap();
writer.rewind().unwrap();
let err =
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
let err = super::copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
// Writer early EOF.
reader.rewind().unwrap();
writer.seek(SeekFrom::Start(3)).unwrap();
let err =
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
let err = super::copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
reader.rewind().unwrap();
@@ -860,8 +867,7 @@ mod tests {
reader.rewind().unwrap();
writer.rewind().unwrap();
cancel_signal.store(true, Ordering::SeqCst);
let err =
super::copy_n_inspect(&mut reader, &mut writer, 6, |_| {}, &cancel_signal).unwrap_err();
let err = super::copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Interrupted);
+98 -12
View File
@@ -1,13 +1,14 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
fmt, mem,
fmt::{self, Display},
mem,
ops::{
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
},
path::Path,
path::{Component, Path, PathBuf},
};
use num_traits::{NumCast, PrimInt};
@@ -303,15 +304,76 @@ pub fn is_zero(mut buf: &[u8]) -> bool {
/// Get the non-empty parent of a path. If the path has no parent in the string,
/// then `.` is returned. This does not perform any filesystem operations.
pub fn parent_path(path: &Path) -> &Path {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
return parent;
}
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
return parent;
}
Path::new(".")
}
/// Path safety-related errors.
#[derive(Clone, Debug, Error)]
pub enum PathSafetyError {
#[error("Path contains multiple components: {0:?}")]
NotSingle(PathBuf),
#[error("Path contains '..': {0:?}")]
HasDotDot(PathBuf),
}
/// Join `parent` with `child`, disallowing unsafe child paths. Absolute child
/// paths are converted into relative paths and `..` components result in an
/// error.
pub fn path_join(
parent: impl AsRef<Path>,
child: impl AsRef<Path>,
) -> Result<PathBuf, PathSafetyError> {
fn inner(parent: &Path, child: &Path) -> Result<PathBuf, PathSafetyError> {
let mut result = parent.to_owned();
for component in child.components() {
match component {
// Make absolute paths relative.
Component::Prefix(_) | Component::RootDir | Component::CurDir => continue,
// Unsafe path. We block this even if it wouldn't escape the parent.
Component::ParentDir => {
return Err(PathSafetyError::HasDotDot(child.to_path_buf()));
}
Component::Normal(os_str) => result.push(os_str),
}
}
Ok(result)
}
inner(parent.as_ref(), child.as_ref())
}
/// Ensure that the path has no directory components.
pub fn path_ensure_single(name: impl AsRef<Path>) -> Result<(), PathSafetyError> {
fn inner(name: &Path) -> Result<(), PathSafetyError> {
// Path::file_name() already checks for `.` and `..`.
if name.file_name() != Some(name.as_os_str()) {
return Err(PathSafetyError::NotSingle(name.to_path_buf()));
}
Ok(())
}
inner(name.as_ref())
}
/// Like [`path_join`], but ensures that the child path contains no directory
/// components with [`path_ensure_single`].
pub fn path_join_single(
parent: impl AsRef<Path>,
child: impl AsRef<Path>,
) -> Result<PathBuf, PathSafetyError> {
path_ensure_single(child.as_ref())?;
path_join(parent.as_ref(), child.as_ref())
}
/// Sort and merge overlapping intervals.
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
where
@@ -325,11 +387,11 @@ where
for section in sections {
if section.start >= section.end {
continue;
} else if let Some(last) = result.last_mut() {
if section.start <= last.end {
last.end = last.end.max(section.end);
continue;
}
} else if let Some(last) = result.last_mut()
&& section.start <= last.end
{
last.end = last.end.max(section.end);
continue;
}
result.push(section);
@@ -378,6 +440,30 @@ where
.is_ok()
}
/// Join arbitrary displayable items with a separator.
pub fn join(into_iter: impl IntoIterator<Item = impl Display>, sep: &str) -> String {
use std::fmt::Write;
let mut result = String::new();
for (i, item) in into_iter.into_iter().enumerate() {
if i > 0 {
result.push_str(sep);
}
write!(result, "{item}").expect("Failed to allocate");
}
result
}
/// Sort arbitrary sequence of sortable items.
pub fn sort<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
let mut items = iter.collect::<Vec<_>>();
items.sort();
items
}
#[cfg(test)]
mod tests {
use super::*;
+4 -1
View File
@@ -63,11 +63,14 @@ include-workspace = true
bypass = [
# Copies of unmodified crashwrangler objects for old macOS versions.
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
# Only used in tests.
{ name = "libloading", allow-globs = ["tests/nagisa*.dll"] },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/zip",
"https://github.com/chenxiaolong/system-properties",
"https://github.com/chenxiaolong/zip2",
]
+6 -7
View File
@@ -18,20 +18,19 @@ ring = "0.17.14"
rsa = { version = "0.9.6", features = ["hazmat"] }
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.22.9", features = ["serde"] }
toml_edit = { version = "0.23.3", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = "0.2.5"
# https://github.com/zip-rs/zip/pull/383
# https://github.com/zip-rs/zip2/pull/367
# https://github.com/zip-rs/zip2/pull/368
# For getting the data offset when writing new zip entries.
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
git = "https://github.com/chenxiaolong/zip2"
rev = "59685f4dadbfee8cb3ea74c8fbb402b60d8137e8"
default-features = false
[features]
static = ["avbroot/static"]
[lints]
workspace = true
+16 -16
View File
@@ -51,12 +51,12 @@ data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes_streaming]
original = "c83bff30dbd30ed8baf5a954a158a2c3a75b25175089809e52ea9052020b0627"
patched = "20bd1835c6ea90104eab7ec51be48ee8aae76508f8a6501621d7a1f384103c2f"
original = "4e283ad1e3450795a32f46445bf626f6af983f52bd0a3484b49f8740c1029653"
patched = "837861bf64e9387380e02d740b21c94e60c41bba4161d85ab44fc5ce86f19631"
[profile.pixel_v4_gki.hashes_seekable]
original = "ffc5c7839dfa68d5ff7d888287f808259be34bd63c5df0a385e2e78e6fdcc647"
patched = "90a51e890560486d4bd73685176c833c94f31f9d05d524186bb4c544c8eca32c"
original = "0ab2403a2634f00063c44f9a477a672205922ede189442e18850dd8efceb5d6f"
patched = "2d94841c3be6cc1739f4c7ad5d9db048301b3f93f8ccd510e2c825bf13ecff90"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
@@ -93,12 +93,12 @@ data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes_streaming]
original = "8d76f17b33949c8ca3d4d5872858bb725d755e315f67d61db0e0b7b58cf69685"
patched = "7c7cb69087d4c8c54a7f4bac6525354b1e9c1550545bc954194aaf6882f7982d"
original = "c3a978b7225632875d3b1e87e852494b1a69019f5ba4099cf3981503623facca"
patched = "cf0ef7429c4657875018d10d7302aaca371b21c5968af234aabd941304f0035b"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "6825bc02115e0c64f05651571138fbcc5cc04459e5c7e225dcc5e3096051c341"
patched = "373309f28234866625462253d6f41f5a3b016b3676c7cb87a88bf55099ad6b13"
original = "cc11ee5a5be66bf34dcb6834a9a635016fa9f82dd0d9a2fb1029aa4f218d1d2e"
patched = "e2fb7d3ce2c697372fb342ae5cd7bb606cd764f06a2aa8a2d6d7bd2c88456780"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
@@ -136,12 +136,12 @@ data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes_streaming]
original = "678f5ba8cff01802ce964d31abf7a2c77b63e9c06e911857b588a16872fb6860"
patched = "3691c2cf89728307e143b7084b40bbbfb181e15be6f65914a6c40408e0c6eab3"
original = "d12d92c051bcc832cc9ceef1d46c415af80a09808a984db4c42a88d65d644a8b"
patched = "408496eb2cca51c0563eceaa3a9def95a5c42acff938850a8577aaa047331284"
[profile.pixel_v3.hashes_seekable]
original = "202d447215f80919f464bd59c7b38f20cbe3a9625d38bc81a0b71e5a3da334eb"
patched = "ec94739fdef7efc4930e96828761c2ebd4ece79014b735eab303c5cbdda2e529"
original = "63505cfd7c2c9d948a5ee150cf53f448f11c83cdce3919fd43231640bf002812"
patched = "b246497de1588d41b919ba002b37dff13cb565682aa3e086e543b25ead7d7aaf"
# Google Pixel 4a
# What's unique: boot (boot v2)
@@ -169,9 +169,9 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes_streaming]
original = "79bb20bd57f51a8a7b52d0ff59b7d096a1e4ada425a6230818ca25916a81e021"
patched = "7f199cc4a6fd244034b597cb916651e1c3e0d6f59450617a5cadbd274f02ec7a"
original = "c303728f4ee9c42ef990bb576bd0688635b263a67127d2a986caf574c9eedd63"
patched = "e907db408767f81223e3ae53dd8a44c6f59f4702a016093cd6983499c9391ff0"
[profile.pixel_v2.hashes_seekable]
original = "c9b32282f247555fed92cc6d599b9005c81ffdc88d5f55d2248e8bf6fb66f1b5"
patched = "c856f7f7245c02d32b3cec93c2d4365a2b3823fed9a9b27f94508637d79b1492"
original = "951b1f70bef7736b9e06f3215357fcebf8a4925872cd31691198fdda9d7c04ce"
patched = "2b1bfe46f41942be88e09b40000745c68ed133900d7970f57d15019683a1ba87"
+22 -24
View File
@@ -14,12 +14,12 @@ use std::{
path::{Path, PathBuf},
slice,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use avbroot::{
cli::ota::{ExtractCli, PatchCli, VerifyCli},
crypto::{self, PassphraseSource, RsaSigningKey},
@@ -37,23 +37,25 @@ use avbroot::{
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
zip::ZipWriterWrapper,
},
patch::otacert::{self, OtaCertBuildFlags},
protobuf::{
build::tools::releasetools::{ota_metadata::OtaType, DeviceState, OtaMetadata},
build::tools::releasetools::{DeviceState, OtaMetadata, ota_metadata::OtaType},
chromeos_update_engine::{
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
},
},
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
util,
};
use clap::Parser;
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
use rsa::{BigUint, rand_core::OsRng, traits::PublicKeyParts};
use tempfile::TempDir;
use topological_sort::TopologicalSort;
use tracing::{info, info_span};
use x509_cert::Certificate;
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
use zip::{CompressionMethod, DateTime, ZipWriter, write::SimpleFileOptions};
use crate::{
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
@@ -374,7 +376,7 @@ fn create_boot_image(
.ramdisks
.iter()
.map(|c_list| {
if c_list.iter().any(|c| *c == RamdiskContent::Dlkm) {
if c_list.contains(&RamdiskContent::Dlkm) {
RamdiskMeta {
ramdisk_type: bootimage::VENDOR_RAMDISK_TYPE_DLKM,
ramdisk_name: "dlkm".to_owned(),
@@ -741,14 +743,15 @@ fn create_ota(
let mut zip_writer = match zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(raw_writer);
ZipWriter::new_streaming(signing_writer)
ZipWriterWrapper::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(raw_writer);
ZipWriter::new(signing_writer)
ZipWriterWrapper::new_seekable(signing_writer)
}
};
let options = FileOptions::default()
let options = SimpleFileOptions::default()
.last_modified_time(DateTime::default())
.compression_method(CompressionMethod::Stored)
.large_file(false);
@@ -758,12 +761,9 @@ fn create_ota(
for path in [ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES] {
// All remaining entries are written immediately.
zip_writer
.start_file_with_extra_data(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let offset = zip_writer
.end_extra_data()
.with_context(|| format!("Failed to end new zip entry: {path}"))?;
.start_file(path, options)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let mut writer = CountingWriter::new(&mut zip_writer);
match path {
@@ -798,7 +798,7 @@ fn create_ota(
let size = writer.stream_position()?;
entries.push(ZipEntry {
name: path.to_owned(),
path: path.to_owned(),
offset,
size,
});
@@ -864,6 +864,7 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
let raw_writer =
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
let mut zip_writer = ZipWriter::new(raw_writer);
let options = SimpleFileOptions::default().last_modified_time(DateTime::default());
for path in [
"assets/stub.apk",
@@ -880,12 +881,12 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
"lib/x86_64/libmagisk64.so",
"lib/x86_64/libmagiskinit.so",
] {
zip_writer.start_file(path, FileOptions::default())?;
zip_writer.start_file(path, options)?;
write!(zip_writer, "dummy contents for {path}")?;
}
// avbroot looks for the version number in this file.
zip_writer.start_file("assets/util_functions.sh", FileOptions::default())?;
zip_writer.start_file("assets/util_functions.sh", options)?;
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
Ok(())
@@ -1140,7 +1141,7 @@ fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()>
.iter_mut()
.find(|e| e.path == b"system/etc/security/otacerts.zip")
{
let mut zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
let zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
let empty_zip = zip_writer.finish()?.into_inner();
entry.data = CpioEntryData::Data(empty_zip);
@@ -1222,10 +1223,6 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
];
for name in profiles {
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe profile name: {name}");
}
let profile = &config.profile[name];
for (zip_mode, hashes) in [
@@ -1234,8 +1231,9 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
] {
let _span = info_span!("profile", name, %zip_mode).entered();
// Can't used NamedTempFile because avbroot does atomic replaces.
let profile_dir = work_dir.join(name);
// Can't use NamedTempFile because avbroot does atomic replaces.
let mut profile_dir = util::path_join_single(work_dir, name)?;
profile_dir.push(zip_mode.to_string());
let out_original = profile_dir.join("ota.zip");
let out_magisk = profile_dir.join("ota_magisk.zip");
let out_prepatched = profile_dir.join("ota_prepatched.zip");
+1 -1
View File
@@ -12,7 +12,7 @@ publish = false
anyhow = "1.0.75"
clap = { version = "4.4.1", features = ["derive"] }
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
toml_edit = "0.22.9"
toml_edit = "0.23.3"
[lints]
workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ use std::{
path::Path,
};
use anyhow::{anyhow, bail, Result};
use anyhow::{Result, anyhow, bail};
use regex::Regex;
use crate::WORKSPACE_DIR;
+2 -2
View File
@@ -7,9 +7,9 @@ use std::{
path::Path,
};
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use clap::Parser;
use toml_edit::{value, DocumentMut};
use toml_edit::{DocumentMut, value};
use crate::WORKSPACE_DIR;