Compare commits

..

195 Commits

Author SHA1 Message Date
Andrew Gunnerson 07b6901d8e Version 3.29.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-04-23 11:36:35 -04:00
Andrew Gunnerson b2b3d98005 CHANGELOG.md: Add entry for PR #594
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-04-23 11:26:51 -04:00
Andrew Gunnerson 21c1dc86ce format/payload: Allow manifests up to 16 MiB
The memory limit prevented some larger incremental OTAs from being
loaded.

Fixes: #593

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-04-23 11:24:57 -04:00
Andrew Gunnerson 462084ac18 Version 3.29.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-28 01:15:32 -04:00
Andrew Gunnerson 4c3e1efd99 CHANGELOG.md: Add entry for PR #587
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-28 01:14:41 -04:00
Andrew Gunnerson 9b7f27e384 cli/payload: Add support for unpacking skeleton images
This is useful for getting properly sized (but empty) image files when
unpacking delta payloads.

Discussion: #582

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-28 01:03:37 -04:00
Andrew Gunnerson 2120827755 CHANGELOG.md: Add entry for PR #586
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-28 00:56:58 -04:00
Andrew Gunnerson 8b33645026 cli/zip: Add support for packing/unpacking the payload at the same time
This is more efficient since there's no need to waste space on an extra
copy of payload.bin when we can directly read it from the zip file or
directly write to the zip file.

Discussion: #582

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-28 00:22:02 -04:00
Andrew Gunnerson eafc6ad3b4 Version 3.28.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-25 18:48:32 -04:00
Andrew Gunnerson 188d256d03 CHANGELOG.md: Add entry for PR #584
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-25 18:41:08 -04:00
Andrew Gunnerson c1836d01a4 cli/payload: Allow unpacking only metadata from delta payloads
The images are the only parts that can't be extracted. Having the
unpacked metadata is useful because it allows for manually building a
full payload using the metadata from the delta payload.

Discussion: #582

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-25 18:40:07 -04:00
Andrew Gunnerson e8532870b1 Version 3.27.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-20 00:01:47 -04:00
Andrew Gunnerson b854654cab CHANGELOG.md: Add entry for PR #580
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-20 00:00:40 -04:00
Andrew Gunnerson f68a492131 format/compression: Add support for LZMA
Fixes: #578

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-19 21:04:09 -04:00
Andrew Gunnerson c22a5d6eca CHANGELOG.md: Add entry for PR #579
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-19 20:06:42 -04:00
Andrew Gunnerson 2631228e8d Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-03-19 19:33:15 -04:00
Andrew Gunnerson f2b5d9646e Version 3.26.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-23 22:27:43 -05:00
Andrew Gunnerson 4991c5192e CHANGELOG.md: Add entry for PR #573
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-23 22:14:38 -05:00
Andrew Gunnerson 8ef9f89cdd Bump Magisk version upper bound to 30800
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-23 22:12:09 -05:00
Andrew Gunnerson 43cd7eac78 Version 3.26.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-21 16:44:08 -05:00
Andrew Gunnerson faf5a908ec CHANGELOG.md: Add entry for PR #571
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-21 16:39:19 -05:00
Andrew Gunnerson 97cad8f508 cli/avb: Warn when AVB header is signed with AOSP test keys
This will log a warning message, but will not return a fatal error.

Issue: #569

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-21 16:33:56 -05:00
Andrew Gunnerson dd73dcbd56 CHANGELOG.md: Add entry for PR #570
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-21 16:32:15 -05:00
Andrew Gunnerson 6f8de8b2c4 Add option to re-sign unmodified partitions during patching
This is useful when the OTA contains partition images that are signed
with the AOSP test keys.

Issue: #569

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-21 15:32:44 -05:00
Andrew Gunnerson 1be81d87b9 README.extra.md: Fix avbroot ota -> avbroot zip
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-20 23:14:17 -05:00
Andrew Gunnerson 5df2ffa435 Version 3.25.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-16 00:17:39 -05:00
Andrew Gunnerson 16c1c2a4d3 CHANGELOG.md: Add entry for PR #565
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-16 00:15:12 -05:00
Andrew Gunnerson 1155a14b00 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-16 00:06:42 -05:00
Andrew Gunnerson 806870664a CHANGELOG.md: Add entry for PR #564
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-16 00:01:09 -05:00
Andrew Gunnerson 4717407a43 Add missing flush() and remove no-op flush()
* flush() or into_inner() is now called for all BufWriters
* flush() is no longer called for Files, which is a no-op operation

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 23:55:53 -05:00
Andrew Gunnerson ff4a2a6db8 CHANGELOG.md: Add entry for PR #563
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 23:37:50 -05:00
Andrew Gunnerson 49bd389bf3 cli/payload: Fix short option conflict for output and output_properties
These were both requesting -o. --output-properties will now be -O
instead.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 23:37:02 -05:00
Andrew Gunnerson 5afe76bf2b CHANGELOG.md: Add entry for PR #562
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 23:28:32 -05:00
Andrew Gunnerson 0c8357924c Add support for unpacking and packing raw OTA zips
This provides the last bit of machinery needed to build an OTA from
scratch, outside of the usual `avbroot ota patch` mechanism. The new
subcommands exist under `avbroot zip` instead of `avbroot ota` to reduce
the chance of someone accidentally using them.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 22:37:18 -05:00
Andrew Gunnerson ef9e563010 CHANGELOG.md: Add entry for PR #561
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 15:31:16 -05:00
Andrew Gunnerson 2e53147f87 cli/{cpio,lp,payload}: Remove outdated mention of openat-style sandboxing
This was previously removed in d7369e73e9
because it was preventing other legitimate use cases.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-15 15:27:33 -05:00
Andrew Gunnerson 69903c4382 Version 3.24.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-03 18:19:05 -05:00
Andrew Gunnerson 9987d21cef CHANGELOG.md: Add entry for PR #559
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-03 18:16:25 -05:00
Andrew Gunnerson 9802bba5f7 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-02-03 18:09:44 -05:00
Andrew Gunnerson 1b30328b01 CHANGELOG.md: Add entry for PR #555
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-24 18:55:42 -05:00
Andrew Gunnerson efb25e15dd cli/avb: Fix help text for avbroot avb verify-device -p
Issue: #554

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-24 18:52:32 -05:00
Ivan 2e5754e16c README.ru.md: Update translation
Sync with https://github.com/chenxiaolong/avbroot/commit/2033e481632fb7b81df296f00d1d75ab1759adae

Signed-off-by: Ivan <reddxae@proton.me>
2026-01-10 15:43:36 +03:00
Andrew Gunnerson cd4da8d147 Version 3.24.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 23:32:00 -05:00
Andrew Gunnerson 9c48d0ddc5 CHANGELOG.md: Add entry for PR #550
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 23:31:25 -05:00
Andrew Gunnerson ee70fdbcae deny.toml: Ignore compiled honggfuzz executable and object files
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 23:10:50 -05:00
Andrew Gunnerson f8f1afd6b3 CHANGELOG.md: Add entry for PR #549
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 23:07:24 -05:00
Andrew Gunnerson f2b98623cb Update dependencies
Also, switch e2e from toml_edit to toml since we don't need any editing
capabilities.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 21:10:38 -05:00
Andrew Gunnerson 63e21c5aa9 Merge pull request #534 from lefuglyduck/patch-1
Update README.md
2026-01-09 20:27:00 -05:00
Andrew Gunnerson a78a122763 CHANGELOG.md: Add entry for PR #548
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 20:25:41 -05:00
Andrew Gunnerson cd7316b429 Move fuzzing corpus generation to xtask
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 20:15:33 -05:00
Andrew Gunnerson 68d5bd6990 CHANGELOG.md: Add entry for PR #547
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 19:52:31 -05:00
Andrew Gunnerson 2a4c422943 Update toml to support serialization of large u64 values
Upstream bug report: https://github.com/toml-rs/toml/issues/1085
Upstream fix: https://github.com/toml-rs/toml/pull/1086

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 19:49:44 -05:00
Andrew Gunnerson 94b12bf727 CHANGELOG.md: Add entry for PR #539
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 19:36:30 -05:00
Andrew Gunnerson f966714310 cli/ota: Ignore vbmeta partitions filled with zeros
For whatever reason, some OTAs seem to include empty unused vbmeta
partitions. To avoid needing to extract the partitions to check the
contents, we'll just filter out small vbmeta partitions (>=4 KiB and
<=64 KiB) where the partition checksum specified in the payload matches
the SHA-256 digest of an array of zeros of the same size.

Fixes: #537

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 19:35:15 -05:00
Andrew Gunnerson 3eb08f48d1 CHANGELOG.md: Add entry for PR #545
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 01:22:06 -05:00
Andrew Gunnerson dbb5bc4767 format/lp: Fix potential panic caused by empty image extent indices
7017bc66dd relaxed the parser logic to
allow empty images to contain extents. However, the validation check for
extent indices didn't run because it still had a conditional for normal
images. This could cause a panic during parsing due to an out of bounds
read of the extents array.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 01:20:30 -05:00
Andrew Gunnerson 903d6dcd13 CHANGELOG.md: Add entry for PR #544
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 00:55:52 -05:00
Andrew Gunnerson ddcef31b96 fuzz: Fix initial corpus
e22f9f5676 removed all the old binary test
files, leaving broken symlinks in hfuzz_workspace. This commit adds the
source files for generating them from scratch.

This commit also replaces toml_edit with toml since the former cannot
deserialize u64 values.

Fixes: #532

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-09 00:53:18 -05:00
Andrew Gunnerson cbd419f8e9 CHANGELOG.md: Add entry for PR #543
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-08 19:54:20 -05:00
Andrew Gunnerson 9214a832cd patch/boot: Add support for DSU on dedicated recovery devices
On devices with dedicated recovery partitions, first_stage_ramdisk
exists in the ramdisks, but is unused. The first stage switch root
operation is just skipped. To properly add a public key for DSU on these
devices, the top level /avb directory needs to be used.

Fixes: #536

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-08 19:21:55 -05:00
Andrew Gunnerson dc9244f014 CHANGELOG.md: Add entry for PR #541
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-07 20:28:21 -05:00
Andrew Gunnerson 7017bc66dd format/lp: Allow empty images to list extents
Android's DSU mechanism uses this in /metadata/gsi/dsu/dsu/lp_metadata.
The file is an empty image that references the userdata block device and
the extents point to preallocated space for the partition images in
/data/gsi/dsu/dsu.

This commit also relaxes the metadata_max_size limit to 512 KiB since
these DSU LP images set the field to 256 KiB:

https://android.googlesource.com/platform/system/core/+/refs/tags/android-16.0.0_r4/fs_mgr/libfiemap/metadata.cpp#35

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-07 20:26:14 -05:00
Andrew Gunnerson bff45fc53c CHANGELOG.md: Add entry for PR #540
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-07 20:13:44 -05:00
Andrew Gunnerson 0e5ba0f0e2 patch/boot: Always normalize ramdisk archives
Previously, some of the boot image patches did not sort the cpio entries
and (re)assign inode numbers. In practice, this makes no difference, but
it is still not the intended behavior.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2026-01-07 20:10:37 -05:00
lefuglyduck 2033e48163 Update README.md
Added adb command to reboot into recovery. 

Signed-off-by: lefuglyduck <31975903+lefuglyduck@users.noreply.github.com>
2025-12-11 07:02:04 -08:00
Andrew Gunnerson 237f59fd9b Version 3.23.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-12-01 21:02:12 -05:00
Andrew Gunnerson b0b00a4c54 CHANGELOG.md: Add entry for PR #530
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-12-01 20:58:15 -05:00
Andrew Gunnerson b9a70efb15 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-12-01 20:54:47 -05:00
Andrew Gunnerson aeef2936d8 CHANGELOG.md: Add entry for PR #529
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-12-01 20:49:24 -05:00
Andrew Gunnerson 3adab80893 Bump Magisk version upper bound to 30700
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-12-01 20:48:16 -05:00
Andrew Gunnerson 4069429361 Version 3.23.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-11-04 20:55:17 -05:00
Andrew Gunnerson 74193c1c9f CHANGELOG.md: Add entry for PR #524
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-11-04 20:54:42 -05:00
Andrew Gunnerson 0f8cef2daa ci.yml: Add aarch64-pc-windows-msvc target
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-11-04 18:10:05 -05:00
Andrew Gunnerson 8731df4504 CHANGELOG.md: Add entry for PR #523
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-11-04 18:08:35 -05:00
Andreas Papon fda11b4262 Add aarch64-unknown-linux-gnu target. 2025-11-04 20:08:05 +01:00
Andrew Gunnerson f0970aa80c Version 3.23.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-02 19:37:41 -04:00
Andrew Gunnerson 4f0db5a7c4 CHANGELOG.md: Add entry for PR #521
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-02 19:28:22 -04:00
Andrew Gunnerson 57374e2e41 Bump Magisk version upper bound to 30500
There are no breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-10-02 19:27:19 -04:00
Andrew Gunnerson f4e5c55fe9 Version 3.23.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-30 20:39:13 -04:00
Andrew Gunnerson 850ce86388 CHANGELOG.md: Add entry for PR #520
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-30 20:16:31 -04:00
Andrew Gunnerson c16636a68c Update dependencies
The only breaking changes are a few types from lzma-rust2 being renamed.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-30 20:15:20 -04:00
Andrew Gunnerson 26812cd43a CHANGELOG.md: Add entry for PR #519
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-30 20:08:55 -04:00
Andrew Gunnerson 135cd320ce Bump Magisk version upper bound to 30400
30300 introduced a new RECOVERYMODE config option, which we
unconditionally disable because we never patch init_boot.cpio within
the vendor_boot image.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-30 20:07:34 -04:00
Andrew Gunnerson 919f621833 CHANGELOG.md: Add entry for PR #517
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-21 19:03:34 -04:00
Andrew Gunnerson 2c1d86cd96 deny.toml: Remove outdated override for ring license
Issue: #516

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-21 18:52:15 -04:00
Andrew Gunnerson 91074ae4c8 Update dependencies and fix clippy 1.90 lints
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-09-21 18:51:25 -04:00
Andrew Gunnerson a5c628b66f Version 3.22.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 23:47:22 -04:00
Andrew Gunnerson 156c96d3d4 CHANGELOG.md: Add entry for PR #512
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 23:46:31 -04:00
Andrew Gunnerson 2460d1807d Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 23:24:42 -04:00
Andrew Gunnerson 728d333951 CHANGELOG.md: Add entry for PR #511
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 23:11:10 -04:00
Andrew Gunnerson 39661a994d Cargo.toml: Specify minimum supported toolchain version
Fixes: #506

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 23:08:58 -04:00
Andrew Gunnerson d6b19b69f7 CHANGELOG.md: Add entry for PR #510
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 22:58:44 -04:00
Andrew Gunnerson 96806cba36 cli: Allow skipping unpacking of images
This is useful if the file format metadata is the only thing that the
user needs.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 22:54:44 -04:00
Andrew Gunnerson 5c474a1e98 CHANGELOG.md: Add entry for PR #509
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 22:40:38 -04:00
Andrew Gunnerson 4cf546e5ca cli/payload: Compute CoW size estimates during packing
This was overlooked during the initial implementation. Payload images
using VABC, but without CoW size estimates, are unflashable outside of
recovery.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-27 22:06:44 -04:00
Andrew Gunnerson 8f71b61b21 Version 3.21.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 21:18:59 -04:00
Andrew Gunnerson fa8d9eb58c CHANGELOG.md: Add entry for PR #505
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 20:15:19 -04:00
Andrew Gunnerson 39afcf485f Fix clippy lints
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 20:14:52 -04:00
Andrew Gunnerson a3eb8284c8 CHANGELOG.md: Add entry for PR #504
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 20:08:01 -04:00
Andrew Gunnerson e2708d39dd format/payload: Make compression factor required only for CoW v3
It turns out that even though newer versions of delta_generator set this
field for CoW v2 (unused), older versions did not.

Fixes: #493

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 20:03:33 -04:00
Andrew Gunnerson a5f13826e7 CHANGELOG.md: Add entry for PR #503
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 19:21:42 -04:00
Andrew Gunnerson 963456c194 patch/boot: Replace function pointers with opener trait
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 19:19:55 -04:00
Andrew Gunnerson 842a2feb88 CHANGELOG.md: Add entry for PR #502
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 19:02:00 -04:00
Andrew Gunnerson 8bb1f771af format/payload: Remove unnecessary function pointer in extract_images()
The list of output files that are needed are known beforehand. There's
no need to dynamically open them.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:47:05 -04:00
Andrew Gunnerson ab51654dad CHANGELOG.md: Add entry for PR #498
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:43:26 -04:00
Andrew Gunnerson d75b87d3df Update dependencies
The e2e checksums were updated because the new lzma-rust2 version has
slight differences in compression ratio.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:18:39 -04:00
Andrew Gunnerson 73e0404662 CHANGELOG.md: Add entry for PR #501
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:18:27 -04:00
Andrew Gunnerson ebb4f18add Use try_for_each where possible (round 2)
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:17:46 -04:00
Andrew Gunnerson bee8d80df0 CHANGELOG.md: Add entry for PR #500
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 18:12:08 -04:00
Andrew Gunnerson fdb19e3b97 format/payload: Use dynamic dispatch for large functions
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 17:54:05 -04:00
Andrew Gunnerson cef29fd280 CHANGELOG.md: Add entry for PR #499
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 17:18:00 -04:00
Andrew Gunnerson 58155513af format/payload: Fix out-of-order multithreaded writes
This was a regression from d874921a69.
extract_images()'s open_output parameter was requesting a WriteSeek
instead of a WriteAt, so it received multiple instances of Arc<File>
all with the same underlying File (and file offset).

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-23 17:08:23 -04:00
Andrew Gunnerson 01cdd0b0d3 CHANGELOG.md: Add entry for PR #497
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:37:07 -04:00
Andrew Gunnerson e39d023855 format/cpio: Fix integer underflow panic in debug builds
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:35:48 -04:00
Andrew Gunnerson 05c86af298 CHANGELOG.md: Add entry for PR #496
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:21:45 -04:00
Andrew Gunnerson 759ed7fae5 Use try_for_each where possible
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:11:32 -04:00
Andrew Gunnerson f6aac1c12f deny.toml: Remove unused git repo URL
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:07:31 -04:00
Andrew Gunnerson e5b754c786 CHANGELOG.md: Add entry for PR #495
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:07:31 -04:00
Andrew Gunnerson d874921a69 Replace file reopen concept with ReadAt/WriteAt traits
File reopening was conflating ownership of file-like types with the fact
that they support parallel reads and writes at arbitrary offsets.

The Reopen trait has now been replaced with ReadAt and WriteAt traits,
which are implemented for types that support parallel I/O. If a type
compatible with the standard Read/Write/Seek traits is needed, a new
UserPosFile type can act as the bridge by storing its own userspace file
offset. For the opposite bridge, there's MutexFile, which implements
ReadAt/WriteAt by using locks to make the operations sequential. This is
only really used in the tests though.

This eliminates the need for the PSeekFile and SharedCursor types. The
standard File and Cursor types can be used instead, and if shared
ownership is needed, Arc can be used.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-22 22:07:11 -04:00
Andrew Gunnerson 390dce5f0c CHANGELOG.md: Add entry for PR #492
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-21 18:57:08 -04:00
Andrew Gunnerson 6d939bda25 Use seek_relative where possible
This will not make a meaningful performance difference for our use case,
but does not make things any more complex either.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-21 18:55:21 -04:00
Andrew Gunnerson a40d6ea379 CHANGELOG.md: Add entry for PR #489
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-21 00:50:00 -04:00
Andrew Gunnerson 7d0bb378a6 Switch to rawzip crate for zip file handling
rawzip is a lower-level zip file library that is much more suited for
avbroot's use case. Its speed improvements aren't too important since
OTAs only have a handful of files, but it is a simpler layer of
abstraction and exposes more about zip file internals. We also no longer
need to maintain a perpetual fork of the zip library.

The only caveat is that rawzip (much like avbroot) is built around
writing zip files in a streaming fashion. To support `--zip-mode
seekable`, the output file is post-processed to copy the relevant data
descriptor fields to the local header. The unused data descriptors
remain in the file to avoid needing to shift file data, but this does
not violate the spec and Android's libziparchive accepts it just fine.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-20 21:12:27 -04:00
Andrew Gunnerson 3ed38b8d29 ci.yml: Set RUSTDOCFLAGS to statically link doctests
Prior to Rust 1.89, these tests were just skipped when cross-compiling.
Now, they are actually compiled and ran. Unfortunately, doctests don't
use the normal RUSTFLAGS environment variable, so we also need to set
RUSTDOCFLAGS or else the resulting dynamically linked executable will
fail to run on a non-Android host.

Upstream change: https://github.com/rust-lang/cargo/pull/15462

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-08-19 20:13:56 -04:00
Ivan 0717981d62 README.ru.md: Update translation
* https://github.com/chenxiaolong/avbroot/commit/f393d7adc42e43b2857a60c3b5fd404b14f042c4
* https://github.com/chenxiaolong/avbroot/commit/2f964bf113512bd7ff33eb3e47116305a262e9fc
* https://github.com/chenxiaolong/avbroot/commit/a2fe6fc9d882d4fd955d03b3a2046c5a41d5840c
* https://github.com/chenxiaolong/avbroot/commit/2683781737230b98172de335a6597363c6d71ff2

Signed-off-by: Ivan <reddxae@proton.me>
2025-08-14 12:20:38 +03:00
Ivan 556f86e4df README.md: Use "device" as a generic reference, clarifying & style improvements
* Use "device" as a generic reference instead of "phone" throughout the project description as more than just phones can support custom AVB functionality.
* Finalize the clarification on signing key generation in the initial setup section introduced by commit https://github.com/chenxiaolong/avbroot/commit/2f964bf113512bd7ff33eb3e47116305a262e9fc.
* The "warning" in the merging snapshots section is marked in bold and uppercase, aligning with the rest of the text, while less important "notes" don't stand out in the same way.

Signed-off-by: Ivan <reddxae@proton.me>
2025-08-14 12:19:38 +03:00
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
Andrew Gunnerson 19129ae927 Version 3.16.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:15:02 -04:00
Andrew Gunnerson fb2aaed042 CHANGELOG.md: Add entry for PR #446
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:14:25 -04:00
Andrew Gunnerson 182d937d34 Update all dependencies
This also fixes a number of disabled-by-default clippy warnings and
updates the Rust edition to 2024.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 18:06:59 -04:00
Andrew Gunnerson 113bdec6dc CHANGELOG.md: Add entry for PR #445
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:57:03 -04:00
Andrew Gunnerson 1f0d012ac0 e2e: Fix incorrect metadata when generating a non-CoW-V2 payload
It was hardcoded to set the CoW version to v2 in the payload manifest.
This commit also updates the pixel_v2 profile to disable VABC so that
scenario gets tested. The pixel_v3 profile now uses CoW v2 with gz.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:55:38 -04:00
Andrew Gunnerson e4fbe00ea2 CHANGELOG.md: Add entry for PR #444
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-04 16:29:40 -04:00
Andrew Gunnerson ad0b3d5aa8 payload: Add support for custom CoW compression levels
No known device uses this functionality, but AOSP supports it, so we
should too.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 23:34:25 -04:00
Andrew Gunnerson f4394b0c21 CHANGELOG.md: Add entry for PR #443
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 23:03:44 -04:00
Andrew Gunnerson 9dd98fd715 payload: Add support for uncompressed CoW and fix more estimation bugs
No known device uses this, but it's very useful for testing that our
overhead calculation is identical to AOSP's. A few more bugs were found
in our overhead calculation logic:

* The CowHeaderV3 size was missing the original CowHeader (v2) fields
  that are supposed to be included due to inheritance in the C++ class.
* The additional 1% overhead was incorrectly calculated against the
  initial CoW estimate before static overhead for CoW headers was added.
* The V3 num_ops estimation did not set a minimum of 25 to match
  delta_generator.
* The V2 size estimation did not take into account that a cluster of CoW
  operations cannot be truncated. It must be a multiple of cluster_ops
  (200 for avbroot).

With these fixes, the CoW estimation when compression is disabled
matches AOSP exactly. This means all the overhead calculation is now
correct and the only difference when compression is enabled is in the
compression ratios of the various lz4/gz implementations.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 22:59:15 -04:00
Andrew Gunnerson b6e3c68241 CHANGELOG.md: Add entry for PR #442
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 20:39:57 -04:00
Andrew Gunnerson 6fee5346bb payload: Add support for CoW version 3
AOSP has long supported CoW version 3, but it wasn't used by the stock
OS on any Pixel devices until the new Pixel 9a.

CoW version 3 is fundamentally similar to version 2, though with
differences in the main header and how the operation headers are stored.
The compression is no longer done in fixed-size chunks equal to the
block size. Instead, the payload specifies a "compression factor", which
is the maximum chunk size to pass to the compressor. The actual chunk
size is the largest power of 2 <= the compression factor and the
remaining input size. Additionally, for version 3, the payload stores an
additional estimate_op_count_max field containing the number of CoW
operations.

While working on support for version 3, a few bugs in the version 2
estimation logic were found and fixed:

* The cluster_ops * sizeof(CowOperationV2) overhead incorrectly
  assumed that cluster_ops was a constant 200 instead of the actual
  number of CoW operations.
* The overhead did not account for kCowLabelOp headers, which
  delta_generator emits once for every InstallOperation in the payload.
* The overhead did not account for kCowClusterOp headers, which batch
  CoW operations into groups of 200.
* The overhead did not account for the CowFooter.

The version 3 overhead is much simpler and easier to calculate compared
to version 2.

Fixes: #441

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-05-03 20:28:50 -04:00
Ivan 7d786150cf README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/de433a27245adb26e578bdeb34a404552c79ea85
* https://github.com/chenxiaolong/avbroot/commit/5d66774d131505928b31e1968da0498732ac28ab
* https://github.com/chenxiaolong/avbroot/commit/b922f0d23ce841ac0bd14bfd33da46273f183f68
2025-04-21 19:51:43 +03:00
Andrew Gunnerson b922f0d23c README.md: Deemphasize rooting in project description
While avbroot initially started as a way to allow a rooted boot image to
be used with a locked bootloader, it has evolved much since then.
Nowadays, many folks use it to make modifications to their OTAs that
don't involve enabling root access. avbroot also has many subcommands
for packing and unpacking various Android image formats that people use
without ever using avbroot's main OTA patching functionality.

This commit updates the project description to reflect this and
simplifies the wording a bit.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-18 20:51:54 -04:00
Andrew Gunnerson 1f2b2170b3 CHANGELOG.md: Fix incorrect link to VABC compression algorithm documentation
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-04-11 21:13:17 -04:00
119 changed files with 6776 additions and 2796 deletions
+13 -7
View File
@@ -18,6 +18,8 @@ jobs:
CARGO_TERM_COLOR: always
# https://github.com/rust-lang/rust/issues/78210
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
# https://github.com/rust-lang/cargo/pull/15462
RUSTDOCFLAGS: -C target-feature=+crt-static
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
ANDROID_API: ${{ matrix.artifact.android_api }}
strategy:
@@ -26,8 +28,12 @@ jobs:
artifact:
- os: ubuntu-latest
name: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
name: aarch64-unknown-linux-gnu
- os: windows-latest
name: x86_64-pc-windows-msvc
- os: windows-11-arm
name: aarch64-pc-windows-msvc
- os: macos-latest
name: universal-apple-darwin
targets:
@@ -41,7 +47,7 @@ jobs:
android_api: '31'
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# For git describe
fetch-depth: 0
@@ -83,7 +89,7 @@ jobs:
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: ${{ matrix.artifact.name }}
@@ -92,7 +98,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
clippy --release --workspace \
--target "${target}"
done
@@ -101,7 +107,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
build --release --workspace \
--target "${target}"
done
@@ -110,7 +116,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
test --release --workspace \
--target "${target}"
done
@@ -119,7 +125,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
@@ -154,7 +160,7 @@ jobs:
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # v2.0.11
uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
+2 -2
View File
@@ -24,10 +24,10 @@ jobs:
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Check out repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Create release
uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
+236 -1
View File
@@ -7,11 +7,155 @@
to update the actual links at the bottom of the file.
-->
### Version 3.29.1
* Increase memory limit for payload manifest size to 16 MiB ([Issue #593], [PR #594])
### Version 3.29.0
* Add support for unpacking/packing payloads at the same time as the OTA in `avbroot zip {unpack,pack}` ([Discussion #582], [PR #586])
* Add new `--skeleton` option to `avbroot payload unpack` ([Discussion #582], [PR #587])
### Version 3.28.0
* Add support for unpacking metadata from delta payloads in `avbroot payload unpack` ([Discussion #582], [PR #584])
### Version 3.27.0
* Add support for LZMA compression for ramdisks ([Issue #578], [PR #580])
* Update dependencies ([PR #579])
### Version 3.26.1
* Add support for Magisk 30700 ([PR #573])
### Version 3.26.0
* Add new `--re-sign` option to `avbroot ota patch` for re-signing partitions that would otherwise be unmodified ([Issue #569], [PR #570])
* Warn when a partition is signed with an AOSP test key in `avbroot ota verify` ([Issue #569], [PR #571])
### Version 3.25.0
* Remove outdated mentions of guarantees provided by cap-std in `--help` text, which was removed in version 3.20.0 ([PR #561])
* Add new commands for packing and unpacking raw OTA zips ([PR #562])
* For folks who are familiar with how OTAs are built, this means it's now possible create an OTA from scratch without using `avbroot ota patch`.
* Fix `-o` being used as the short option for both `--output` and `--output-properties` in `avbroot payload pack` and `avbroot payload repack` ([PR #563])
* The short option for `--output-properties` is now uppercase `-O`.
* Add missing file flush operations and remove them where they are no-ops ([PR #564])
* Update dependencies ([PR #565])
### Version 3.24.1
* Update `avbroot avb verify-device --help` to mention that `ro.boot.vbmeta.public_key_digest` is used when `-p` is not specified ([Issue #554], [PR #555])
* Update dependencies ([PR #559])
### Version 3.24.0
* Always sort ramdisk cpio entries and ensure they have inode numbers assigned ([PR #540])
* Relax LP image parser's validation to allow parsing the `/metadata/gsi/dsu/dsu/lp_metadata` image used by DSU ([PR #541], [PR #545])
* Add support for devices with separate recovery partitions when using `--dsu` ([Issue #536], [PR #543])
* Fix broken symlinks in fuzzing corpus ([Issue #532], [PR #544], [PR #548])
* Ignore unused vbmeta partitions that are filled with zeros ([Issue #537], [PR #539])
* Update dependencies ([PR #547], [PR #549])
* Fix `cargo deny` failing after running fuzzers with `cargo hfuzz` ([PR #550])
### Version 3.23.3
* Add support for Magisk 30600 ([PR #529])
* Update dependencies ([PR #530])
### Version 3.23.2
* Add prebuilt binaries for aarch64 GNU/Linux ([PR #523])
* Add prebuilt binaries for aarch64 Windows ([PR #524])
### Version 3.23.1
* Add support for Magisk 30400 ([PR #521])
### Version 3.23.0
* Update dependencies and fix Rust 1.90 clippy lints ([PR #517], [PR #520])
* Add support for Magisk 30300 ([PR #519])
### Version 3.22.0
* Fix CoW size estimates not being set when running `avbroot payload pack` ([PR #509])
* Add support for only unpacking metadata in `avbroot {avb,boot,cpio,lp,payload} unpack` ([PR #510])
* Specify minimum supported compiler version for building from source ([PR #511])
* Update dependencies ([PR #512])
### Version 3.21.0
* Switch to using rawzip for zip parsing and writing ([PR #489])
* Various code improvements ([PR #492], [PR #495], [PR #496], [PR #497], [PR #499], [PR #500], [PR #501], [PR #502], [PR #503], [PR #505])
* Update dependencies ([PR #498])
* Fix patching older OTAs with payloads using CoW v2 without the compression factor field ([Issue #493], [PR #504])
### 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])
* This was recently introduced with the Pixel 9a. Previous devices all used CoW version 2.
* Add support for uncompressed CoW for virtual A/B ([PR #443])
* This is not used on actual devices, but is very useful for testing the CoW estimation logic.
* All differences between avbroot's and AOSP delta_generator's estimation logic are now fixed.
* Add support for custom CoW compression levels for virtual A/B ([PR #444])
* This is also not used on actual devices, but is supported by AOSP, so avbroot should support it too.
* Update dependencies ([PR #446])
### Version 3.15.0
* Add support for changing the virtual A/B compression algorithm ([PR #437])
* For devices that launched with Android <14, `--vabc-algo lz4` can significantly increase OTA installation speed when using a custom OTA updater app (with caveats). There is no difference when sideloading from recovery mode.
* See [the documentation](#changing-virtual-ab-cow-compression-algorithm) for more details.
* See [the documentation](./README.md#changing-virtual-ab-cow-compression-algorithm) for more details.
* Switch back to the ring library now that it is maintained again ([PR #438])
* Update dependencies ([PR #439])
@@ -310,6 +454,7 @@ Behind-the-scenes changes:
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
[Discussion #582]: https://github.com/chenxiaolong/avbroot/discussions/582
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
@@ -337,6 +482,19 @@ Behind-the-scenes changes:
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
[Issue #441]: https://github.com/chenxiaolong/avbroot/issues/441
[Issue #451]: https://github.com/chenxiaolong/avbroot/issues/451
[Issue #469]: https://github.com/chenxiaolong/avbroot/issues/469
[Issue #472]: https://github.com/chenxiaolong/avbroot/issues/472
[Issue #482]: https://github.com/chenxiaolong/avbroot/issues/482
[Issue #493]: https://github.com/chenxiaolong/avbroot/issues/493
[Issue #532]: https://github.com/chenxiaolong/avbroot/issues/532
[Issue #536]: https://github.com/chenxiaolong/avbroot/issues/536
[Issue #537]: https://github.com/chenxiaolong/avbroot/issues/537
[Issue #554]: https://github.com/chenxiaolong/avbroot/issues/554
[Issue #569]: https://github.com/chenxiaolong/avbroot/issues/569
[Issue #578]: https://github.com/chenxiaolong/avbroot/issues/578
[Issue #593]: https://github.com/chenxiaolong/avbroot/issues/593
[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
@@ -491,3 +649,80 @@ Behind-the-scenes changes:
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
[PR #442]: https://github.com/chenxiaolong/avbroot/pull/442
[PR #443]: https://github.com/chenxiaolong/avbroot/pull/443
[PR #444]: https://github.com/chenxiaolong/avbroot/pull/444
[PR #445]: https://github.com/chenxiaolong/avbroot/pull/445
[PR #446]: https://github.com/chenxiaolong/avbroot/pull/446
[PR #448]: https://github.com/chenxiaolong/avbroot/pull/448
[PR #449]: https://github.com/chenxiaolong/avbroot/pull/449
[PR #452]: https://github.com/chenxiaolong/avbroot/pull/452
[PR #453]: https://github.com/chenxiaolong/avbroot/pull/453
[PR #463]: https://github.com/chenxiaolong/avbroot/pull/463
[PR #464]: https://github.com/chenxiaolong/avbroot/pull/464
[PR #467]: https://github.com/chenxiaolong/avbroot/pull/467
[PR #468]: https://github.com/chenxiaolong/avbroot/pull/468
[PR #470]: https://github.com/chenxiaolong/avbroot/pull/470
[PR #473]: https://github.com/chenxiaolong/avbroot/pull/473
[PR #474]: https://github.com/chenxiaolong/avbroot/pull/474
[PR #475]: https://github.com/chenxiaolong/avbroot/pull/475
[PR #476]: https://github.com/chenxiaolong/avbroot/pull/476
[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
[PR #489]: https://github.com/chenxiaolong/avbroot/pull/489
[PR #492]: https://github.com/chenxiaolong/avbroot/pull/492
[PR #495]: https://github.com/chenxiaolong/avbroot/pull/495
[PR #496]: https://github.com/chenxiaolong/avbroot/pull/496
[PR #497]: https://github.com/chenxiaolong/avbroot/pull/497
[PR #498]: https://github.com/chenxiaolong/avbroot/pull/498
[PR #499]: https://github.com/chenxiaolong/avbroot/pull/499
[PR #500]: https://github.com/chenxiaolong/avbroot/pull/500
[PR #501]: https://github.com/chenxiaolong/avbroot/pull/501
[PR #502]: https://github.com/chenxiaolong/avbroot/pull/502
[PR #503]: https://github.com/chenxiaolong/avbroot/pull/503
[PR #504]: https://github.com/chenxiaolong/avbroot/pull/504
[PR #505]: https://github.com/chenxiaolong/avbroot/pull/505
[PR #509]: https://github.com/chenxiaolong/avbroot/pull/509
[PR #510]: https://github.com/chenxiaolong/avbroot/pull/510
[PR #511]: https://github.com/chenxiaolong/avbroot/pull/511
[PR #512]: https://github.com/chenxiaolong/avbroot/pull/512
[PR #517]: https://github.com/chenxiaolong/avbroot/pull/517
[PR #519]: https://github.com/chenxiaolong/avbroot/pull/519
[PR #520]: https://github.com/chenxiaolong/avbroot/pull/520
[PR #521]: https://github.com/chenxiaolong/avbroot/pull/521
[PR #523]: https://github.com/chenxiaolong/avbroot/pull/523
[PR #524]: https://github.com/chenxiaolong/avbroot/pull/524
[PR #529]: https://github.com/chenxiaolong/avbroot/pull/529
[PR #530]: https://github.com/chenxiaolong/avbroot/pull/530
[PR #539]: https://github.com/chenxiaolong/avbroot/pull/539
[PR #540]: https://github.com/chenxiaolong/avbroot/pull/540
[PR #541]: https://github.com/chenxiaolong/avbroot/pull/541
[PR #543]: https://github.com/chenxiaolong/avbroot/pull/543
[PR #544]: https://github.com/chenxiaolong/avbroot/pull/544
[PR #545]: https://github.com/chenxiaolong/avbroot/pull/545
[PR #547]: https://github.com/chenxiaolong/avbroot/pull/547
[PR #548]: https://github.com/chenxiaolong/avbroot/pull/548
[PR #549]: https://github.com/chenxiaolong/avbroot/pull/549
[PR #550]: https://github.com/chenxiaolong/avbroot/pull/550
[PR #555]: https://github.com/chenxiaolong/avbroot/pull/555
[PR #559]: https://github.com/chenxiaolong/avbroot/pull/559
[PR #561]: https://github.com/chenxiaolong/avbroot/pull/561
[PR #562]: https://github.com/chenxiaolong/avbroot/pull/562
[PR #563]: https://github.com/chenxiaolong/avbroot/pull/563
[PR #564]: https://github.com/chenxiaolong/avbroot/pull/564
[PR #565]: https://github.com/chenxiaolong/avbroot/pull/565
[PR #570]: https://github.com/chenxiaolong/avbroot/pull/570
[PR #571]: https://github.com/chenxiaolong/avbroot/pull/571
[PR #573]: https://github.com/chenxiaolong/avbroot/pull/573
[PR #579]: https://github.com/chenxiaolong/avbroot/pull/579
[PR #580]: https://github.com/chenxiaolong/avbroot/pull/580
[PR #584]: https://github.com/chenxiaolong/avbroot/pull/584
[PR #586]: https://github.com/chenxiaolong/avbroot/pull/586
[PR #587]: https://github.com/chenxiaolong/avbroot/pull/587
[PR #594]: https://github.com/chenxiaolong/avbroot/pull/594
Generated
+671 -531
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -4,10 +4,12 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.15.0"
version = "3.29.1"
license = "GPL-3.0-only"
edition = "2021"
edition = "2024"
repository = "https://github.com/chenxiaolong/avbroot"
publish = false
rust-version = "1.88.0"
[workspace.lints.clippy]
cast_lossless = "deny"
+80 -9
View File
@@ -19,7 +19,7 @@ The vbmeta descriptor digests are validated during unpacking. For dm-verity imag
### Packing an AVB image
```bash
avbroot avb pack -o <output AVB image> [--key <AVB private key>]
avbroot avb pack -o <output AVB image> [-k <AVB private key>]
```
This subcommand packs a new AVB image from the `avb.toml` file and, for appended vbmeta images, the `raw.img` 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.
@@ -54,18 +52,31 @@ avbroot avb info -i <image>
This subcommand shows all of the vbmeta header and footer fields. `vbmeta` partition images will only have a header, while partitions with actual data (eg. boot images) will have both a header and a footer.
(The `unpack`, `pack`, and `repack` subcommands also show this information. This specific subcommand just does so without performing any other operation.)
### Verifying AVB hashes and signatures
```bash
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
@@ -108,7 +119,9 @@ This subcommand repacks a boot image without writing the individual components t
avbroot boot info -i <input boot image>
```
All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`.
This subcommand shows the information contained in the boot image's header. To show avbroot's internal representation of the information, pass in `-d`.
(All of the `boot` subcommands show this information. This specific subcommand just does so without performing any other operation.)
## `avbroot cpio`
@@ -150,7 +163,9 @@ This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot
avbroot cpio info -i <input cpio archive>
```
All of the `cpio` subcommands show details about all the entries in the archive. This specific subcommand just does it without performing any other operation.
This subcommand shows details about every entry in the archive.
(All of the `cpio` subcommands show this information. This specific subcommand just does so without performing any other operation.)
## `avbroot fec`
@@ -311,6 +326,8 @@ avbroot lp info -i <first LP image>
This subcommand shows the LP image metadata, including all metadata slots. If there are multiple images, only the first one is needed because it is the only one that stores the metadata.
(All of the `lp` subcommands show this information. This specific subcommand just does so without performing any other operation.)
## `avbroot payload`
This set of commands is for working with payload binary files (`payload.bin`). The `unpack` and `pack` commands can only work with full payloads because they require the complete data to be available, but the `repack` and `info` commands also work with delta payloads.
@@ -323,22 +340,24 @@ avbroot payload unpack -i <input payload>
This subcommand unpacks the payload header information to `payload.toml` and the partition images to the `payload_images` directory.
Only full payload binaries can be unpacked. Delta payload binaries from incremental OTAs are not supported.
Data can only be unpacked from full payload binaries. For delta payloads, either use `--no-output-images` to skip image extraction or use `--skeleton` to create properly sized sparse files with no data.
### Packing a payload binary
```bash
avbroot payload pack -o <output payload> --key <OTA private key>
avbroot payload pack -o <output payload> -k <OTA private key> [-O <output properties>]
```
This subcommand packs a new payload binary from the `payload.toml` file and `payload_images` directory. Any `.img` files in the `payload_images` directory that don't have a corresponding entry in `payload.toml` are silently ignored.
When replacing the payload binary in an OTA, it is not sufficient to only update `payload.bin`. The checksums in `payload_properties.txt` need to be updated as well. Use `--output-properties` to generate a new properties file.
Packing a payload binary requires compressing all of the partition images, which is very CPU intensive. If re-signing an existing payload binary without making any other modifications is all that's needed, use the `repack` subcommand instead.
### Repacking a payload binary
```bash
avbroot payload repack -i <input payload> -o <output payload> --key <OTA private key>
avbroot payload repack -i <input payload> -o <output payload> -k <OTA private key> [-O <output properties>]
```
This subcommand is logically equivalent to `avbroot payload unpack` followed by `avbroot payload pack`, except significantly more efficient. Instead of decompressing and recompressing all partition images, the raw data is directly copied from the input payload binary.
@@ -353,6 +372,8 @@ avbroot payload info -i <payload>
This subcommand shows all of the payload header fields (which will likely be extremely long).
(All of the `payload` subcommands show this information. This specific subcommand just does so without performing any other operation.)
## `avbroot sparse`
This set of commands is for working with Android sparse images. All features of the file format are supported, including hole chunks and CRC32 checksums.
@@ -396,3 +417,53 @@ avbroot sparse info -i <input sparse image>
```
This subcommand shows the sparse image metadata, including the header and all chunks.
(All of the `sparse` subcommands show this information. This specific subcommand just does so without performing any other operation.)
## `avbroot zip`
This set of commands is for working with raw OTA zip files. They are intentionally placed outside of `avbroot ota` to make them less discoverable by accident. These commands are useful for creating OTA zip files without going through the normal `avbroot ota patch` mechanism.
**WARNING**: Make sure to run `avbroot ota verify` on OTA files before installing them. These pack commands are low level operations that only check that the file structure of the OTA itself is valid, not the contents contained within.
### Unpacking an OTA zip
```bash
avbroot zip unpack -i <input OTA> [--payload]
```
This subcommand unpacks the OTA metadata to `ota.toml` and the OTA files to the `ota_files` directory.
When `--payload` is specified, the payload and OTA zip will be unpacked at the same time, which is significantly more efficient that unpack the payload and OTA zip separately. The payload unpacking process writes to the same `payload.toml` and `payload_images` outputs as `avbroot payload unpack`.
### Packing an OTA zip
```bash
avbroot zip pack -o <output OTA> -k <OTA private key> [--payload]
```
This subcommand packs a new OTA zip from the `ota.toml` file and `ota_files` directory. Any files in the `ota_files` directory that don't have a corresponding entry in `ota.toml` are silently ignored.
When `--payload` is specified, the payload and OTA zip will be packed at the same time, which is significantly more efficient than packing the payload and OTA zip separately. The payload packing process reads the same `payload.toml` and `payload_images` inputs as `avbroot payload pack`.
When packing an OTA zip, the `metadata.property_files` field in `ota.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 `ota.toml` if desired.
### Repacking an OTA zip
```bash
avbroot zip repack -i <input OTA> -o <output OTA> -k <OTA private key>
```
This subcommand is logically equivalent to `avbroot zip unpack` followed by `avbroot zip pack`, except more efficient.
**WARNING**: This is generally not a useful command. Resigning the OTA zip without also resigning the payload binary inside results in an invalid OTA.
### Showing OTA metadata
```bash
avbroot zip info -i <input OTA>
```
This subcommand shows all of the OTA metadata fields. If both the modern protobuf metadata and the legacy plain text metadata exist, the protobuf metadata takes precedence.
(All of the `zip` subcommands show this information. This specific subcommand just does so without performing any other operation.)
+21 -13
View File
@@ -2,12 +2,10 @@
(This page is also available in: [Russian (Русский)](./README.ru.md).)
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
avbroot is a tool for modifying Android A/B OTA images reproducibly and re-signing them with custom keys. It also includes a [collection of subcommands](./README.extra.md) for packing and unpacking numerous Android image formats.
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
## Requirements
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
@@ -23,7 +21,7 @@ Having a good understanding of how AVB and A/B OTAs work is recommended prior to
avbroot applies the following patches to the partition images:
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
* The `boot` or `init_boot` image, depending on device, is patched to enable root access if requested.
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
@@ -31,7 +29,7 @@ avbroot applies the following patches to the partition images:
## Warnings and Caveats
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader while rooted.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
@@ -53,6 +51,8 @@ avbroot applies the following patches to the partition images:
3. Follow the steps to [generate signing keys](#generating-keys).
Skip this step if you're updating Android, Magisk, or KernelSU after you've already performed an [initial setup](#initial-setup). There's no need to generate new signing keys for [updates](#updates): any further updates must use the keys that were created during the initial setup.
4. Patch the OTA zip. The base command is:
```bash
@@ -213,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
@@ -225,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.
3. Reboot to recovery mode: `adb reboot recovery`. 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 device. Note that the device will likely take longer than usual to start on the first boot 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
@@ -415,6 +421,10 @@ The only behavior this changes is where the partition is read from. When using `
This has no impact on what patches are applied. For example, when using Magisk, the root patch is applied to the boot partition, no matter if the partition came from the original `payload.bin` or from `--replace`.
### Re-signing partitions
avbroot will automatically re-sign any partitions in the OTA that it modifies. However, partitions that are otherwise unmodified can also be re-signed with `--re-sign <partition name>`. This is useful, for example, when the OTA contains partitions signed with the public AOSP test key.
### Booting signed GSIs
Android's [Dynamic System Updates (DSU)](https://developer.android.com/topic/dsu) feature uses a different root of trust than the regular system. Instead of using the bootloader's `avb_custom_key`, it obtains the trusted keys from the `first_stage_ramdisk/avb/*.avbpubkey` files inside the `init_boot` or `vendor_boot` ramdisk. These files are encoded in the same binary format as `avb_pkmd.bin`.
@@ -584,8 +594,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:
+55 -27
View File
@@ -1,14 +1,12 @@
# avbroot
avbroot это программа для модификации OTA-образов Android A/B-формата с целью получения root-прав при сохранении прохождения AVB (Android Verified Boot) с использованием кастомных (пользовательских) ключей подписи. Она совместима как с Magisk, так и с KernelSU. При необходимости можно просто переподписать OTA, без получения root-доступа.
avbroot это утилита для воспроизводимой модификации OTA-образов Android A/B-формата и их переподписания пользовательскими ключами. Она также включает в себя [набор подкоманд](./README.extra.md) для упаковки и распаковки образов Android различных форматов.
Прежде чем использовать avbroot, рекомендуется иметь хорошее понимание того, как работают AVB и OTA в формате A/B. Как минимум, следует ознакомиться с [разделом предостережений,](#предостережения) чтобы избежать хардбрика устройства.
**ПРИМЕЧАНИЕ:** avbroot 2.0 была переписана на Rust и больше не имеет в основе никакого кода AOSP, а CLI полностью обратно совместим. Тем не менее, старую реализацию на Python можно найти в одноименной ветке `python`.
## Требования
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваш телефон необходимую схему разметки, откройте zip-архив OTA и проверьте:
* Поддерживаются только устройства, использующие современную A/B-разметку. Это большинство девайсов, выпускаемых с Android 10 и новее (за исключением устройств от Samsung). Чтобы проверить, использует ли ваше устройство необходимую схему разметки, откройте zip-архив OTA и проверьте:
* наличие файла `payload.bin` (обычно находится в корне архива)
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
@@ -21,7 +19,7 @@ avbroot – это программа для модификации OTA-обра
avbroot модифицирует следующие образы:
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа. В случае с Magisk, патч будет эквивалентен тому, что производится в самом приложении Magisk.
* `boot` или `init_boot`, в зависимости от устройства, модифицируется для получения root-доступа, если это запрашивается.
* `boot`, `recovery` или `vendor_boot`, в зависимости от устройства, модифицируется для замены сертификата проверки подписи OTA на пользовательский. Это позволяет устанавливать будущие пропатченные OTA через режим Recovery уже после блокировки загрузчика, то есть в качестве обновления. Также это предотвращает случайную установку оригинального непропатченного OTA.
@@ -51,6 +49,8 @@ avbroot модифицирует следующие образы:
3. [Сгенерируйте ключи подписи.](#генерация-ключей)
Пропустите этот шаг, если вы обновляете Android, Magisk или KernelSU уже после выполнения [первоначальной настройки](#первоначальная-настройка). Повторная генерация ключей подписи для [обновлений](#обновления) не требуется: для всех последующих обновлений должны использоваться те ключи, что были созданы при первоначальной настройке.
4. Пропатчите ОТА-архив с помощью команды:
```bash
@@ -107,7 +107,7 @@ avbroot модифицирует следующие образы:
Первые два компонента подписываются ключом AVB, а последние два – ключом OTA. Можно использовать один и тот же ключ, однако в следующих шагах описано, как сгенерировать два отдельных.
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA для другого телефона.
Если вы патчите OTA сразу для нескольких устройств, настоятельно рекомендуется генерировать уникальные ключи для каждого девайса – так вы защитите себя от случайной прошивки неподходящего OTA.
1. Сгенерируйте ключи подписи для AVB и OTA.
@@ -122,7 +122,7 @@ avbroot модифицирует следующие образы:
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
```
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при сайдлоадинге обновления.
3. Сгенерируйте самоподписанный сертификат для ключа подписи OTA. Он используется режимом Recovery для проверки подписи OTA при установке обновления.
```bash
avbroot key generate-cert -k ota.key -o ota.crt
@@ -185,7 +185,7 @@ avbroot совместим с любым стандартным 4096-битны
fastboot flashall --skip-reboot
```
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
@@ -211,6 +211,8 @@ avbroot совместим с любым стандартным 4096-битны
init: [libfs_avb]Returning avb_handle with status: Success
```
Как ещё один вариант, Android-версию avbroot также можно использовать для [проверки разделов на устройстве](./README.extra.md#verifying-avb-hashes-and-signatures-on-device).
9. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
```bash
@@ -229,15 +231,19 @@ avbroot совместим с любым стандартным 4096-битны
Обновления Android, Magisk и KernelSU выполняются одинаково – исключительно путем обновления или репатчинга того же самого OTA.
1. Если Magisk или KernelSU обновились, сначала установите их новый `.apk`. Если вы случайно открыли приложение после обновления, убедитесь, что оно не начало прошивать загрузочный образ. Если появится предложение обновить сам загрузочный образ – отклоните его.
1. Сгенерируйте новый пропатченный OTA согласно инструкции в разделе [использования.](#использование)
2. Следуйте инструкции в разделе [использования,](#использование) чтобы пропатчить OTA уже с новым .apk Magisk'а/предварительно пропатченным образом с Magisk или KernelSU.
2. Если обновляется Magisk или KernelSU, сначала установите их новый `.apk`. Если вы случайно открыли приложение, убедитесь, что оно **не начало** прошивать загрузочный образ. Если в самом приложении появится предложение обновить загрузочный образ, отклоните его.
3. Перезагрузитесь в режим Recovery. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем нажмите кнопку увеличения громкости один раз.
3. Перезагрузитесь в режим Recovery: `adb reboot recovery`. Если устройство повисло на сплеше с сообщением "No command", удерживайте кнопку питания, а затем один раз нажмите кнопку увеличения громкости.
4. Обновитесь (Apply update from adb → `adb sideload <ota.zip.patched>`).
5. Готово!
5. Перезагрузите устройство. Обратите внимание, что при первом запуске после обновления ОС устройство может загружаться дольше обычного (иногда до нескольких минут).
**ПРЕДУПРЕЖДЕНИЕ**: В силу специфики работы виртуального A/B в Android, сразу после установки OTA и перезагрузки, в фоновом режиме незаметно запускается операция слияния снапшотов. Во время этого процесса невозможно установить другой OTA через режим Recovery. Пока продолжается слияние снапшотов, избегайте любых действий, которые могут привести к бутлупу (например, установка модулей), поскольку в случае сбоя восстановить устройство получится только повторно разблокировав загрузчик, стирая все данные.
Узнать текущий статус процесса можно, выполнив команду: `adb logcat -v color -s update_engine`. Дополнительно, если установлено [Custota](https://github.com/chenxiaolong/Custota) (даже если оно не настроено на использование пользовательского OTA-сервера), приложение будет отображать соответствующее уведомление до завершения операции слияния снапшота.
## Возврат на заводскую прошивку
@@ -255,11 +261,11 @@ avbroot совместим с любым стандартным 4096-битны
## OTA-обновления
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как при загрузке в Android, так и при сайдлоадинге через Recovery.
avbroot заменяет `/system/etc/security/otacerts.zip` в разделах системы и Recovery на новый архив, содержащий пользовательский сертификат подписи OTA. Это предотвращает случайную установку непропатченных OTA как из-под загруженной системы, так и при прошивке через Recovery.
Рекомендуется отключить приложение обновлений системы, чтобы оно не пыталось установить непропатченные OTA:
Рекомендуется отключить системное приложение для обновлений, чтобы оно не пыталось установить непропатченные OTA:
* Стоковая прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
* Стоковая (заводская) прошивка: Отключите `Автоматические обновления системы` (Automatic system updates в англ.) в настройках для разработчиков.
* Кастомная прошивка: Отключите приложение обновлений системы (или запретите ему доступ к Интернету) через Настройки -> Приложения -> Все приложения -> (меню/три точки) -> Показать системные -> (найдите приложение обновлений, например Обновления системы/Updater).
Это особенно важно для некоторых кастомных прошивок, поскольку их фирменное приложение для обновления системы может уйти в бесконечный цикл, загружая OTA-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
@@ -317,7 +323,7 @@ Magisk версии 25211 и новее требует наличие разде
Теперь, когда имя раздела известно, его нужно указать avbroot с помощью команды `--magisk-preinit-device <имя>`. Имя раздела стоит запомнить или сохранить где-нибудь на будущее, оно вряд ли изменится при обновлении Magisk.
Если запустить приложение Magisk на целевом устройстве невозможно (например, телефон не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
Если запустить приложение Magisk на целевом устройстве невозможно (например, оно не загружается), пропатчите OTA с аргументом `--ignore-magisk-warnings` и прошейте его. Затем выполните указанные выше шаги и повторно пропатчите OTA, но уже с указанием аргумента `--magisk-preinit-device <имя>`.
## Проверка OTA
@@ -384,20 +390,32 @@ avbroot можно использовать для простого перепо
### Пропуск патчинга сертификата OTA
avbroot может пропускать изменение `otacerts.zip` с помощью аргументов `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.** (Например, если вы уже самостоятельно встроили сертификат OTA в загрузочный (boot) образ и передаете его программе через опции `--prepatched` или `--replace`.)
В противном случае, на устройстве может не остаться возможности устанавливать дальнейшие обновления.
Вы можете пропустить изменение otacerts.zip, используя аргументы `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.**
При использовании `--skip-system-ota-cert`, никаких изменений в образ `system` не вносится.
При использовании `--skip-system-ota-cert`, сертификаты OTA в образе `system` изменены не будут. Это не позволит сторонним приложениям для OTA-обновлений устанавливать будущие пропатченные OTA из-под загруженной системы.
При использовании `--skip-recovery-ota-cert` совместно с `--rootless` и без указания `--dsu`, не вносится никаких изменений в загрузочные образы, кроме обеспечения их корректной подписи.
При использовании `--skip-recovery-ota-cert`, сертификаты OTA в образах `vendor_boot` или `recovery` изменены не будут. **Это не позволит устанавливать будущие пропатченные OTA в режиме Recovery.**
Если вы вручную добавили сертификат OTA в загрузочный (boot) образ, рекомендуем [предварительно проверить пропатченный OTA.](#проверка-ota)
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
### Подмена разделов
### Пропуск всех патчей
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
Чтобы внести самый минимум изменений, укажите аргументы:
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
* `--skip-system-ota-cert`
* `--skip-recovery-ota-cert`
* `--rootless`
* не используйте аргумент `--dsu`.
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
### Подмена образов
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, используя аргумент `--replace <имя раздела> /путь/к/образу.img`.
Единственное, что меняется – это то, откуда считывается файл. При использовании `--replace` вместо образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
@@ -411,6 +429,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
Чтобы принудительно включить AVB (очистив флаги), укажите аргумент `--clear-vbmeta-flags`.
### Изменение алгоритма CoW сжатия для вирутального A/B
Алгоритм CoW (copy-on-write) сжатия для виртуального A/B можно изменить, используя аргумент `--vabc-algo <алгоритм>`, указав `gz` или `lz4`. Как правило, по умолчанию OTA использует алгоритм, который совместим с изначальной версией Android, на которой поставлялось устройство.
* Девайсы, поставляемые с Android 12, поддерживают `gz` и `brotli` (последний не поддерживается avbroot)
* Девайсы, поставляемые с Android 14, поддерживают `lz4`
* Девайсы, поставляемые с Android 15, поддерживают `zstd` (не поддерживается avbroot)
Выбор быстрого алгоритма, такого как lz4, может значительно ускорить установку OTA из-под системы (при использованием стороннего приложения для OTA-обновлений). Однако, при установке OTA в режиме Recovery, разницы в скорости не будет.
Обратите внимание, что текущая используемая версия Android должна поддерживать выбранный алгоритм сжатия. В противном случае установка завершится ошибкой. Например, попытка установить OTA-обновление с Android 14, использующее алгоритм lz4, приведет к ошибке, если установка производится из-под Android 13.
### Использование в неинтерактивном режиме
По умолчанию avbroot интерактивно запрашивает пароли к приватным ключам. Чтобы запустить avbroot в неинтерактивном режиме, можно:
@@ -493,7 +523,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`.
@@ -548,8 +578,6 @@ cargo build --release
Дебаг-сборки тоже работают, но они будут работать значительно медленнее (в вычислениях sha256), потому что оптимизации компилятора отключены.
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
### Кросс-компиляция на Android
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
+15 -21
View File
@@ -4,7 +4,8 @@ version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
publish = false
publish.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -13,9 +14,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,17 +27,19 @@ dlv-list = "0.6.0"
flate2 = { version = "1.0.29", features = ["zlib-rs"] }
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.3.0"
lz4_flex = "0.11.1"
lz4_flex = "0.13.0"
lzma-rust2 = "0.16.1"
memchr = "2.6.0"
# We can't upgrade to 0.9.0 until rsa updates its dependency.
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.13.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"
rawzip = "0.4.0"
rayon = "1.7.0"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
# We use ring instead of sha2 for sha256 digest computation of large files
@@ -52,7 +53,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 = { version = "1.0.1", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
@@ -60,27 +61,20 @@ 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
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
features = ["deflate"]
[target.'cfg(unix)'.dependencies]
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.7.0"
prost-build = "0.14.1"
protox = "0.9.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["liblzma/static"]
[lints]
workspace = true
+28 -13
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{env, ffi::OsStr, fs, io, path::Path};
@@ -35,6 +35,10 @@ fn main() {
const CUE_PU: &str = ".chromeos_update_engine.PartitionUpdate";
const CUE_VABCFS: &str = ".chromeos_update_engine.VABCFeatureSet";
const BTR_DS: &str = ".build.tools.releasetools.DeviceState";
const BTR_OM: &str = ".build.tools.releasetools.OtaMetadata";
const BTR_PS: &str = ".build.tools.releasetools.PartitionState";
const DERIVE_SERDE: &str = "#[derive(serde::Deserialize, serde::Serialize)]";
const SERDE_DEFAULT: &str = "#[serde(default)]";
const SERDE_SKIP: &str = "#[serde(skip)]";
@@ -45,19 +49,25 @@ fn main() {
prost_build::Config::new()
.btree_map(["."])
// Allow deserializing and serializing the types we care about.
.type_attribute(CUE_AI, DERIVE_SERDE)
.type_attribute(CUE_DAM, DERIVE_SERDE)
.type_attribute(CUE_DPG, DERIVE_SERDE)
.type_attribute(CUE_DPM, DERIVE_SERDE)
.type_attribute(CUE_PU, DERIVE_SERDE)
.type_attribute(CUE_VABCFS, DERIVE_SERDE)
.message_attribute(CUE_AI, DERIVE_SERDE)
.message_attribute(CUE_DAM, DERIVE_SERDE)
.message_attribute(CUE_DPG, DERIVE_SERDE)
.message_attribute(CUE_DPM, DERIVE_SERDE)
.message_attribute(CUE_PU, DERIVE_SERDE)
.message_attribute(CUE_VABCFS, DERIVE_SERDE)
.message_attribute(BTR_DS, DERIVE_SERDE)
.message_attribute(BTR_OM, DERIVE_SERDE)
.message_attribute(BTR_PS, DERIVE_SERDE)
// Allow default-initializing all fields.
.type_attribute(CUE_AI, SERDE_DEFAULT)
.type_attribute(CUE_DAM, SERDE_DEFAULT)
.type_attribute(CUE_DPG, SERDE_DEFAULT)
.type_attribute(CUE_DPM, SERDE_DEFAULT)
.type_attribute(CUE_PU, SERDE_DEFAULT)
.type_attribute(CUE_VABCFS, SERDE_DEFAULT)
.message_attribute(CUE_AI, SERDE_DEFAULT)
.message_attribute(CUE_DAM, SERDE_DEFAULT)
.message_attribute(CUE_DPG, SERDE_DEFAULT)
.message_attribute(CUE_DPM, SERDE_DEFAULT)
.message_attribute(CUE_PU, SERDE_DEFAULT)
.message_attribute(CUE_VABCFS, SERDE_DEFAULT)
.message_attribute(BTR_DS, SERDE_DEFAULT)
.message_attribute(BTR_OM, SERDE_DEFAULT)
.message_attribute(BTR_PS, SERDE_DEFAULT)
// Don't serialize fields that define the structure of the payload
// binary and that we recompute during packing.
.field_attribute(c!(CUE_DAM, ".signatures_offset"), SERDE_SKIP)
@@ -84,6 +94,11 @@ fn main() {
.field_attribute(c!(CUE_DAM, ".partitions"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPG, ".partition_names"), SERDE_SKIP_IF_VEC_EMPTY)
.field_attribute(c!(CUE_DPM, ".groups"), SERDE_SKIP_IF_VEC_EMPTY)
// Serialize enums as their enum types, not their underlying reprs.
.field_attribute(
c!(BTR_OM, ".type"),
"#[serde(with = \"crate::protobuf::ota_type\")]",
)
.compile_fds(file_descriptors)
.unwrap();
}
+7 -10
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -10,10 +10,10 @@ 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};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse, zip};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -29,6 +29,7 @@ pub enum Command {
Ota(ota::OtaCli),
Payload(payload::PayloadCli),
Sparse(sparse::SparseCli),
Zip(zip::ZipCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
#[command(hide = true)]
Patch(ota::PatchCli),
@@ -40,19 +41,14 @@ pub enum Command {
MagiskInfo(boot::MagiskInfoCli),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
pub enum LogFormat {
#[default]
Short,
Medium,
Long,
}
impl Default for LogFormat {
fn default() -> Self {
Self::Short
}
}
impl fmt::Display for LogFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
@@ -137,6 +133,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
Command::Sparse(c) => sparse::sparse_main(&c, cancel_signal),
Command::Zip(c) => zip::zip_main(&c, cancel_signal),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
+330 -145
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,10 +25,57 @@ use crate::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, check_cancel, PSeekFile, ReadFixedSizeExt, Reopen, ToWriter},
stream::{self, ReadFixedSizeExt, ToWriter, UserPosFile, check_cancel},
util,
};
/// SHA-256 digests of the AOSP test keys encoded in the AVB binary format. This
/// excludes 8192-bit keys since RustCrypto doesn't support them anyway.
const PUBLIC_PUBKEYS: [[u8; 32]; 7] = [
// testkey_cert_pik.pem
[
0xce, 0x46, 0x4c, 0x0f, 0xef, 0xc0, 0x23, 0x76, 0x0a, 0xdc, 0x3f, 0xe0, 0x20, 0x7c, 0x28,
0x2d, 0x33, 0x7e, 0x1b, 0x91, 0xe3, 0xdf, 0x7a, 0xb4, 0xfe, 0xe4, 0x3f, 0xcd, 0x62, 0x0d,
0xb4, 0x6d,
],
// testkey_cert_prk.pem
[
0x86, 0xb0, 0x4a, 0x9d, 0x62, 0x80, 0x7f, 0xda, 0x80, 0xcb, 0xf4, 0x45, 0x11, 0x8e, 0x8a,
0x4a, 0x73, 0xaa, 0x59, 0x37, 0x28, 0x3b, 0x49, 0xe0, 0x18, 0xe4, 0x32, 0xdd, 0x8e, 0x3a,
0x76, 0xa1,
],
// testkey_cert_psk.pem
[
0x7e, 0xc0, 0x2e, 0xe1, 0xbe, 0x69, 0x63, 0x66, 0xf3, 0xfa, 0x91, 0x24, 0x0a, 0x8e, 0xc6,
0x81, 0x25, 0xc4, 0x14, 0x5d, 0x69, 0x8f, 0x59, 0x7a, 0xa2, 0xb3, 0x46, 0x4b, 0x59, 0xca,
0x7f, 0xc3,
],
// testkey_cert_puk.pem
[
0xc6, 0x08, 0xff, 0xa5, 0xe7, 0xee, 0x92, 0x98, 0x4f, 0x44, 0x86, 0xe0, 0x30, 0x88, 0x44,
0x0c, 0x36, 0x0c, 0xf3, 0x15, 0xec, 0x6a, 0x5f, 0x52, 0xae, 0xf7, 0x90, 0xcb, 0x30, 0xef,
0xce, 0xc9,
],
// testkey_rsa2048.pem
[
0x22, 0xde, 0x39, 0x94, 0x53, 0x21, 0x96, 0xf6, 0x1c, 0x03, 0x9e, 0x90, 0x26, 0x0d, 0x78,
0xa9, 0x3a, 0x4c, 0x57, 0x36, 0x2c, 0x7e, 0x78, 0x9b, 0xe9, 0x28, 0x03, 0x6e, 0x80, 0xb7,
0x7c, 0x8c,
],
// testkey_rsa2048_2.pem
[
0x2b, 0xed, 0x47, 0x45, 0x1b, 0xc6, 0x98, 0xe9, 0xe8, 0x2d, 0x92, 0xa6, 0x66, 0x8b, 0xd0,
0x3a, 0xb6, 0xcf, 0x8d, 0xd1, 0xa1, 0x44, 0x34, 0x1c, 0xb7, 0xf4, 0x26, 0xf2, 0x0b, 0x28,
0x79, 0xcf,
],
// testkey_rsa4096.pem
[
0x77, 0x28, 0xe3, 0x0f, 0x50, 0xbf, 0xa5, 0xce, 0xa1, 0x65, 0xf4, 0x73, 0x17, 0x5a, 0x08,
0x80, 0x3f, 0x6a, 0x83, 0x46, 0x64, 0x2b, 0x5a, 0xa1, 0x09, 0x13, 0xe9, 0xd9, 0xe6, 0xde,
0xfe, 0xf6,
],
];
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct AvbInfo {
header: Header,
@@ -39,9 +84,9 @@ struct AvbInfo {
}
fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
let file = File::open(path)
let mut reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open AVB image for reading: {path:?}"))?;
let mut reader = BufReader::new(file);
let (header, footer, image_size) = avb::load_image(&mut reader)
.with_context(|| format!("Failed to load AVB image: {path:?}"))?;
@@ -54,7 +99,7 @@ fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
Ok((info, reader))
}
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
fn write_avb_image(file: &File, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
let mut writer = BufWriter::new(file);
info.image_size = if let Some(f) = &mut info.footer {
@@ -80,7 +125,7 @@ fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) ->
fn read_info(path: &Path) -> Result<AvbInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read AVB info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
let info = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse AVB info TOML: {path:?}"))?;
Ok(info)
@@ -88,25 +133,13 @@ fn read_info(path: &Path) -> Result<AvbInfo> {
/// Write AVB information to TOML file.
fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
let data = toml::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize AVB info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write AVB info TOML: {path:?}"))?;
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(
@@ -114,16 +147,15 @@ fn write_raw(
reader: &mut BufReader<File>,
size: u64,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
let file = fs::OpenOptions::new()
) -> Result<File> {
let mut writer = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map(PSeekFile::new)
.map(BufWriter::new)
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
let mut writer = BufWriter::new(file);
reader
.rewind()
@@ -146,7 +178,7 @@ fn write_raw_and_verify(
info: &AvbInfo,
ignore_invalid: bool,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
) -> Result<File> {
let f = info.footer.as_ref().expect("Not an appended image");
let descriptor = info.header.appended_descriptor()?;
@@ -160,7 +192,7 @@ fn write_raw_and_verify(
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
let result = verify_and_repair(None, &raw_file, descriptor, true, cancel_signal);
// Chop off the old hash tree and FEC data.
raw_file.set_len(f.original_image_size)?;
@@ -183,7 +215,7 @@ fn write_raw_and_update(
reader: &mut BufReader<File>,
info: &mut AvbInfo,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
) -> Result<File> {
assert!(info.footer.is_some(), "Not an appended image");
let image_size = reader
@@ -194,13 +226,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)
d.update(&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 +342,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 +374,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 +453,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 +536,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:?}"))?;
@@ -463,20 +547,40 @@ pub fn verify_headers(
if let Some(k) = &public_key {
let prefix = format!("{name} has a signed vbmeta header");
let encoded = avb::encode_public_key(k)?;
let digest = Sha256::digest(&encoded);
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");
}
}
} else {
warn!("{prefix}, but parent does not list a trusted key");
TrustMethod::KeyDigest(expected_sha256) => {
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");
}
}
if PUBLIC_PUBKEYS.iter().any(|p| p == digest.as_slice()) {
warn!("{name} is signed with public AOSP test 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 +600,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)?;
}
_ => {}
}
@@ -512,7 +617,7 @@ pub fn verify_headers(
/// work.
fn verify_and_repair(
name: Option<&str>,
mut file: PSeekFile,
file: &File,
descriptor: AppendedDescriptorRef,
repair: bool,
cancel_signal: &AtomicBool,
@@ -529,7 +634,7 @@ fn verify_and_repair(
warn!("Failed to verify hash tree descriptor{suffix}: {e}");
warn!("Attempting to repair using FEC data{suffix}");
d.repair(&file, &file, cancel_signal)
d.repair(&file, cancel_signal)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.verify(&file, cancel_signal).inspect(|()| {
@@ -543,8 +648,7 @@ fn verify_and_repair(
AppendedDescriptorRef::Hash(d) => {
info!("Verifying hash descriptor{suffix}");
file.rewind()?;
d.verify(file, cancel_signal)
d.verify(UserPosFile::new(file), cancel_signal)
.with_context(|| format!("Failed to verify hash descriptor{suffix}"))?;
}
}
@@ -555,49 +659,44 @@ 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();
descriptors
.par_iter()
.map(|(name, descriptor)| {
let _span = parent_span.enter();
let mut options = OpenOptions::new();
options.read(true);
options.write(repair);
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,
// 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:?}");
return Ok(());
}
Err(e) => {
Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?
}
};
descriptors.par_iter().try_for_each(|(name, descriptor)| {
let _span = parent_span.enter();
verify_and_repair(
Some(name),
file,
descriptor.try_into()?,
repair,
cancel_signal,
)
})
.collect()
let file = match opener.open(name, &options) {
Ok((_, f)) => 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 && allow_missing => {
warn!("{e}");
return Ok(());
}
Err(e) => return Err(e.into()),
};
verify_and_repair(
Some(name),
&file,
descriptor.try_into()?,
repair,
cancel_signal,
)
})
}
fn compute_digest_recursive(
directory: &Dir,
directory: &Path,
name: &str,
context: &mut ring::digest::Context,
max_depth: u8,
@@ -612,11 +711,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 +764,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);
@@ -685,7 +785,7 @@ fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()>
write_info(&cli.output_info, &info)?;
if info.footer.is_some() {
if info.footer.is_some() && !cli.no_output_raw {
write_raw_and_verify(
&cli.output_raw,
&mut reader,
@@ -716,13 +816,12 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
file
} else {
File::create(&cli.output)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open output for writing: {:?}", cli.output))?
};
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info, cli.recompute_size)?;
write_avb_image(&file, &mut info, cli.recompute_size)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
@@ -745,8 +844,7 @@ 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)?;
d.update(&file, None, cancel_signal)?;
}
update_dm_verity_cmdline(&mut info)?;
@@ -754,13 +852,12 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
file
} else {
File::create(&cli.output)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for writing: {:?}", cli.output))?
};
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info, false)?;
write_avb_image(&file, &mut info, false)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
@@ -776,50 +873,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 +914,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 +992,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),
}
}
@@ -922,6 +1075,10 @@ struct UnpackCli {
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
output_raw: PathBuf,
/// Do not output raw image.
#[arg(long, conflicts_with = "output_raw")]
no_output_raw: bool,
/// Ignore invalid digests or FEC data.
#[arg(long)]
ignore_invalid: bool,
@@ -1044,6 +1201,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, then the signature is verified against the
/// ro.boot.vbmeta.public_key_digest value provided by the bootloader.
#[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 +1248,8 @@ enum AvbCommand {
#[command(alias = "dump")]
Info(InfoCli),
Verify(VerifyCli),
#[cfg(target_os = "android")]
VerifyDevice(VerifyDeviceCli),
Digest(DigestCli),
}
+76 -26
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::{
@@ -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::{
@@ -16,8 +16,9 @@ use crate::{
};
fn read_image(path: &Path) -> Result<BootImage> {
let file = File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
let reader = BufReader::new(file);
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let image = BootImage::from_reader(reader)
.with_context(|| format!("Failed to read boot image: {path:?}"))?;
@@ -25,9 +26,9 @@ fn read_image(path: &Path) -> Result<BootImage> {
}
fn write_image(path: &Path, image: &BootImage) -> Result<()> {
let file =
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
let mut writer = BufWriter::new(file);
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
image
.to_writer(&mut writer)
.with_context(|| format!("Failed to write boot image: {path:?}"))?;
@@ -41,14 +42,14 @@ fn write_image(path: &Path, image: &BootImage) -> Result<()> {
fn read_header(path: &Path) -> Result<BootImage> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read header TOML: {path:?}"))?;
let image = toml_edit::de::from_str(&data)
let image = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse header TOML: {path:?}"))?;
Ok(image)
}
fn write_header(path: &Path, image: &BootImage) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(image)
let data = toml::ser::to_string_pretty(image)
.with_context(|| format!("Failed to serialize header TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write header TOML: {path:?}"))?;
@@ -76,12 +77,12 @@ fn read_text_if_exists(path: &Path) -> Result<Option<String>> {
}
fn read_avb_header_if_exists(path: &Path) -> Result<Option<Header>> {
let file = match File::open(path) {
Ok(f) => f,
let reader = match File::open(path) {
Ok(f) => BufReader::new(f),
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?,
};
let header = Header::from_reader(BufReader::new(file))
let header = Header::from_reader(reader)
.with_context(|| format!("Failed to read vbmeta header: {path:?}"))?;
Ok(Some(header))
@@ -105,9 +106,15 @@ fn write_text_if_not_empty(path: &Path, text: &str) -> Result<()> {
}
fn write_avb_header(path: &Path, header: &Header) -> Result<()> {
let file =
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
header.to_writer(BufWriter::new(file))?;
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
header
.to_writer(&mut writer)
.with_context(|| format!("Failed to write AVB header: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush AVB header: {path:?}"))?;
Ok(())
}
@@ -164,30 +171,44 @@ fn unpack_subcommand(boot_cli: &BootCli, cli: &UnpackCli) -> Result<()> {
}
}
if let Some(data) = kernel {
if let Some(data) = kernel
&& !cli.no_output_kernel
{
write_data_if_not_empty(&cli.output_kernel, data)?;
}
if let Some(data) = second {
if let Some(data) = second
&& !cli.no_output_second
{
write_data_if_not_empty(&cli.output_second, data)?;
}
if let Some(data) = recovery_dtbo {
if let Some(data) = recovery_dtbo
&& !cli.no_output_recovery_dtbo
{
write_data_if_not_empty(&cli.output_recovery_dtbo, data)?;
}
if let Some(data) = dtb {
if let Some(data) = dtb
&& !cli.no_output_dtb
{
write_data_if_not_empty(&cli.output_dtb, data)?;
}
if let Some(header) = vts_signature {
if let Some(header) = vts_signature
&& !cli.no_output_vts_signature
{
write_avb_header(&cli.output_vts_signature, header)?;
}
if let Some(text) = bootconfig {
if let Some(text) = bootconfig
&& !cli.no_output_bootconfig
{
write_text_if_not_empty(&cli.output_bootconfig, text)?;
}
for (i, data) in ramdisks.iter().enumerate() {
let mut path = cli.output_ramdisk_prefix.as_os_str().to_owned();
path.push(i.to_string());
if !cli.no_output_ramdisk {
for (i, data) in ramdisks.iter().enumerate() {
let mut path = cli.output_ramdisk_prefix.as_os_str().to_owned();
path.push(i.to_string());
write_data_if_not_empty(Path::new(&path), data)?;
write_data_if_not_empty(Path::new(&path), data)?;
}
}
Ok(())
@@ -272,8 +293,9 @@ fn info_subcommand(boot_cli: &BootCli, cli: &InfoCli) -> Result<()> {
pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
let raw_reader = File::open(&cli.image)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {:?}", cli.image))?;
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))
let boot_image = BootImage::from_reader(raw_reader)
.with_context(|| format!("Failed to load boot image: {:?}", cli.image))?;
let mut ramdisks = vec![];
@@ -339,6 +361,10 @@ struct UnpackCli {
#[arg(long, value_name = "FILE", value_parser, default_value = "kernel.img")]
output_kernel: PathBuf,
/// Do not output kernel image.
#[arg(long, conflicts_with = "output_kernel")]
no_output_kernel: bool,
/// Path prefix for output ramdisk images.
#[arg(
long,
@@ -348,10 +374,18 @@ struct UnpackCli {
)]
output_ramdisk_prefix: PathBuf,
/// Do not output ramdisk images.
#[arg(long, conflicts_with = "output_ramdisk_prefix")]
no_output_ramdisk: bool,
/// Path to output second stage bootloader image.
#[arg(long, value_name = "FILE", value_parser, default_value = "second.img")]
output_second: PathBuf,
/// Do not output second stage bootloader image.
#[arg(long, conflicts_with = "output_second")]
no_output_second: bool,
/// Path to output recovery dtbo/acpio image.
#[arg(
long,
@@ -361,10 +395,18 @@ struct UnpackCli {
)]
output_recovery_dtbo: PathBuf,
/// Do not output recovery dtbo/acpio image.
#[arg(long, conflicts_with = "output_recovery_dtbo")]
no_output_recovery_dtbo: bool,
/// Path to output device tree blob image.
#[arg(long, value_name = "FILE", value_parser, default_value = "dtb.img")]
output_dtb: PathBuf,
/// Do not output device tree blob image.
#[arg(long, conflicts_with = "output_dtb")]
no_output_dtb: bool,
/// Path to output VTS signature.
#[arg(
long,
@@ -374,6 +416,10 @@ struct UnpackCli {
)]
output_vts_signature: PathBuf,
/// Do not output VTS signature.
#[arg(long, conflicts_with = "output_vts_signature")]
no_output_vts_signature: bool,
/// Path to output bootconfig text.
#[arg(
long,
@@ -382,6 +428,10 @@ struct UnpackCli {
default_value = "bootconfig.txt"
)]
output_bootconfig: PathBuf,
/// Do not output bootconfig text.
#[arg(long, conflicts_with = "output_bootconfig")]
no_output_bootconfig: bool,
}
/// Pack a boot image.
+43 -44
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};
@@ -37,9 +36,10 @@ fn open_reader(
CpioReader<CompressedReader<BufReader<File>>>,
CompressedFormat,
)> {
let file =
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
let reader = CompressedReader::new(BufReader::new(file), true)
let raw_reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
let reader = CompressedReader::new(raw_reader, true)
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
let format = reader.format();
let cpio_reader = CpioReader::new(reader, include_trailer);
@@ -51,9 +51,10 @@ fn open_writer(
path: &Path,
format: CompressedFormat,
) -> 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)
let raw_writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
let writer = CompressedWriter::new(raw_writer, format)
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
let cpio_writer = CpioWriter::new(writer, false);
@@ -74,7 +75,7 @@ fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result
fn read_info(path: &Path) -> Result<CpioInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
let info = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
Ok(info)
@@ -82,7 +83,7 @@ fn read_info(path: &Path) -> Result<CpioInfo> {
/// Write cpio information to TOML file.
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
let data = toml::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
@@ -91,17 +92,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 +123,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,25 +172,24 @@ fn unpack_subcommand(
display_format(cpio_cli, format);
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_tree, authority)
.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))?;
if !cli.no_output_tree {
fs::create_dir_all(&cli.output_tree)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
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)? {
let file_size = entry.data.size()?;
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)
.context("Failed to copy data")?;
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
writer.into_inner().context("Failed to flush data")?;
writer.into_inner().context("Failed to flush data")?;
}
info.entries.push(entry);
}
info.entries.push(entry);
}
write_info(&cli.output_info, &info)?;
@@ -209,12 +209,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);
@@ -293,8 +289,7 @@ pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
/// TOML file, like the UID/GID, permissions, and symlink targets.
///
/// If any paths inside the cpio archive are unsafe, the extraction process will
/// fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
/// fail and exit.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input cpio file.
@@ -308,6 +303,10 @@ struct UnpackCli {
/// Path to output files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
output_tree: PathBuf,
/// Do not output files.
#[arg(long, conflicts_with = "output_tree")]
no_output_tree: bool,
}
/// Pack a cpio archive.
+4 -8
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::{
@@ -13,15 +13,14 @@ use clap::{Parser, Subcommand};
use crate::{
format::fec::FecImage,
stream::{FromReader, PSeekFile, ToWriter},
stream::{FromReader, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
fn open_input(path: &Path, rw: bool) -> Result<File> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
@@ -91,10 +90,7 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
let input = open_input(&cli.input, true)?;
let fec = read_fec(&cli.fec)?;
// The separate buffered readers and writers are safe because the function
// guarantees that every thread touches disjoint offsets and every offset is
// read and written at most once.
fec.repair(&input, &input, cancel_signal)
fec.repair(&input, cancel_signal)
.context("Failed to repair file")?;
Ok(())
+3 -4
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::{
@@ -13,15 +13,14 @@ use clap::{Parser, Subcommand};
use crate::{
format::hashtree::HashTreeImage,
stream::{FromReader, PSeekFile, ToWriter},
stream::{FromReader, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
fn open_input(path: &Path, rw: bool) -> Result<File> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
+37 -72
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,
@@ -18,36 +16,33 @@ use rayon::iter::{
use crate::{
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
stream::{self, FromReader, ToWriter, UserPosFile},
util,
};
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
let mut inputs = paths
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<File>, Metadata)> {
let inputs = paths
.iter()
.map(|p| {
let p = p.as_ref();
File::open(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
File::open(p).with_context(|| format!("Failed to open LP image for reading: {p:?}"))
})
.collect::<Result<Vec<_>>>()?;
let metadata = Metadata::from_reader(&mut inputs[0])
let metadata = Metadata::from_reader(&inputs[0])
.with_context(|| format!("Failed to parse LP image metadata: {:?}", paths[0].as_ref()))?;
Ok((inputs, metadata))
}
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<PSeekFile>> {
fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<File>> {
paths
.iter()
.map(|p| {
let p = p.as_ref();
File::create(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for writing: {p:?}"))
File::create(p).with_context(|| format!("Failed to open LP image for writing: {p:?}"))
})
.collect::<Result<Vec<_>>>()
}
@@ -55,14 +50,14 @@ fn open_lp_outputs(paths: &[impl AsRef<Path>]) -> Result<Vec<PSeekFile>> {
fn read_info(path: &Path) -> Result<Metadata> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read metadata info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
let info = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse metadata info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, metadata: &Metadata) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(metadata)
let data = toml::ser::to_string_pretty(metadata)
.with_context(|| format!("Failed to serialize metadata info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write metadata info TOML: {path:?}"))?;
@@ -166,17 +161,15 @@ fn fill_slots(metadata: &mut Metadata) {
}
fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut inputs = cli
let inputs = cli
.input
.iter()
.map(|p| {
File::open(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
File::open(p).with_context(|| format!("Failed to open LP image for reading: {p:?}"))
})
.collect::<Result<Vec<_>>>()?;
let mut metadata = Metadata::from_reader(&mut inputs[0])
let mut metadata = Metadata::from_reader(&inputs[0])
.with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?;
// Display and write only the selected slot.
@@ -186,15 +179,12 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
write_info(&cli.output_info, &metadata)?;
// For empty images, there's no data to unpack.
if metadata.image_type == ImageType::Empty {
if metadata.image_type == ImageType::Empty || cli.no_output_images {
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 +206,10 @@ 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)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
file.set_len(partition.size()?)
@@ -251,10 +240,9 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
.into_par_iter()
.map(move |e| (g_index, p_index, e))
})
.map(|(g_index, p_index, extent)| {
// Never fails for PSeekFiles.
let mut reader = inputs[extent.device_index].reopen()?;
let mut writer = files[g_index][p_index].reopen()?;
.try_for_each(|(g_index, p_index, extent)| {
let mut reader = UserPosFile::new(&inputs[extent.device_index]);
let mut writer = UserPosFile::new(&files[g_index][p_index]);
let r_path = &cli.input[extent.device_index];
let w_path = &paths[g_index][p_index];
@@ -271,9 +259,6 @@ fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
@@ -303,35 +288,20 @@ 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)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let size = file
@@ -398,10 +368,9 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
.into_par_iter()
.map(move |e| (g_index, p_index, e))
})
.map(|(g_index, p_index, extent)| {
// Never fails for PSeekFiles.
let mut reader = files[g_index][p_index].reopen()?;
let mut writer = outputs[extent.device_index].reopen()?;
.try_for_each(|(g_index, p_index, extent)| {
let mut reader = UserPosFile::new(&files[g_index][p_index]);
let mut writer = UserPosFile::new(&outputs[extent.device_index]);
let r_path = &paths[g_index][p_index];
let w_path = &cli.output[extent.device_index];
@@ -418,9 +387,6 @@ fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) ->
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
@@ -503,10 +469,9 @@ fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|partition| split_extents(&partition.extents))
.map(|extent| {
// Never fails for PSeekFiles.
let mut reader = inputs[extent.device_index].reopen()?;
let mut writer = outputs[extent.device_index].reopen()?;
.try_for_each(|extent| {
let mut reader = UserPosFile::new(&inputs[extent.device_index]);
let mut writer = UserPosFile::new(&outputs[extent.device_index]);
let r_path = &cli.input[extent.device_index];
let w_path = &cli.output[extent.device_index];
@@ -523,9 +488,6 @@ fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> {
@@ -553,8 +515,7 @@ pub fn lp_main(cli: &LpCli, cancel_signal: &AtomicBool) -> Result<()> {
/// directory. For empty images, the output images directory is unused.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
/// will fail and exit.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input LP images.
@@ -573,6 +534,10 @@ struct UnpackCli {
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
output_images: PathBuf,
/// Do not output images.
#[arg(long, conflicts_with = "output_images")]
no_output_images: bool,
/// The LP metadata slot to use.
///
/// This slot is the only slot where data extents are copied from. Any data
+2 -1
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod args;
@@ -13,3 +13,4 @@ pub mod lp;
pub mod ota;
pub mod payload;
pub mod sparse;
pub mod zip;
+618 -370
View File
File diff suppressed because it is too large Load Diff
+175 -82
View File
@@ -1,17 +1,16 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2026 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},
io::{BufReader, BufWriter, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
sync::{Arc, 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;
@@ -19,30 +18,34 @@ use crate::{
cli::ota,
crypto::{self, PassphraseSource, RsaSigningKey},
format::payload::{PayloadHeader, PayloadWriter},
stream::{self, FromReader, PSeekFile},
stream::{self, FromReader, SectionReader},
util,
};
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
let mut reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
fn open_raw_reader(path: &Path) -> Result<File> {
File::open(path).with_context(|| format!("Failed to open payload for reading: {path:?}"))
}
fn open_reader(path: &Path) -> Result<(BufReader<File>, PayloadHeader)> {
let mut reader = open_raw_reader(path).map(BufReader::new)?;
let header = PayloadHeader::from_reader(&mut reader)
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
if !allow_delta && !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
Ok((reader, header))
}
fn open_raw_writer(path: &Path) -> Result<BufWriter<File>> {
File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open payload for writing: {path:?}"))
}
fn open_writer(
path: &Path,
header: PayloadHeader,
key: RsaSigningKey,
) -> Result<PayloadWriter<BufWriter<File>>> {
let writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open payload for writing: {path:?}"))?;
let writer = open_raw_writer(path)?;
let payload_writer = PayloadWriter::new(writer, header, key)
.with_context(|| format!("Failed to write payload header: {path:?}"))?;
@@ -52,14 +55,14 @@ fn open_writer(
fn read_info(path: &Path) -> Result<PayloadHeader> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read payload info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
let info = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse payload info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(manifest)
let data = toml::ser::to_string_pretty(manifest)
.with_context(|| format!("Failed to serialize payload info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write payload info TOML: {path:?}"))?;
@@ -67,8 +70,8 @@ fn write_info(path: &Path, manifest: &PayloadHeader) -> Result<()> {
Ok(())
}
fn display_header(cli: &PayloadCli, header: &PayloadHeader) {
if !cli.quiet {
fn display_header(quiet: bool, header: &PayloadHeader) {
if !quiet {
println!("{header:#?}");
}
}
@@ -99,65 +102,105 @@ fn load_key(group: &KeyGroup) -> Result<RsaSigningKey> {
Ok(signing_key)
}
#[allow(clippy::too_many_arguments)]
pub fn unpack_payload(
quiet: bool,
output_info: &Path,
output_images: &Path,
no_output_images: bool,
skeleton: bool,
reader: &File,
payload_offset: u64,
payload_size: u64,
cancel_signal: &AtomicBool,
) -> Result<()> {
let section_reader = SectionReader::new(reader, payload_offset, payload_size)
.context("Failed to directly open payload section")?;
let header =
PayloadHeader::from_reader(section_reader).context("Failed to read payload header")?;
display_header(quiet, &header);
write_info(output_info, &header)?;
if !no_output_images {
fs::create_dir_all(output_images)
.with_context(|| format!("Failed to create directory: {output_images:?}"))?;
if skeleton {
for partition in &header.manifest.partitions {
let name = &partition.partition_name;
let path = util::path_join_single(output_images, format!("{name}.img"))?;
let size = partition
.new_partition_info
.as_ref()
.and_then(|info| info.size)
.ok_or_else(|| anyhow!("Size not found for partition: {name}"))?;
let file = File::create(&path)
.with_context(|| format!("Failed to create file: {path:?}"))?;
file.set_len(size)
.with_context(|| format!("Failed to truncate file: {path:?}"))?;
}
} else {
if !header.is_full_ota() {
bail!("Cannot extract images from a delta payload");
}
ota::extract_payload(
reader,
output_images,
payload_offset,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
}
}
Ok(())
}
fn unpack_subcommand(
payload_cli: &PayloadCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, header) = open_reader(&cli.input, false)?;
let mut reader = open_raw_reader(&cli.input)?;
let payload_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
display_header(payload_cli, &header);
write_info(&cli.output_info, &header)?;
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_images, authority)
.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,
unpack_payload(
payload_cli.quiet,
&cli.output_info,
&cli.output_images,
cli.no_output_images,
cli.skeleton,
&reader,
0,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
Ok(())
)
}
fn pack_subcommand(
payload_cli: &PayloadCli,
cli: &PackCli,
pub fn pack_payload(
quiet: bool,
input_info: &Path,
input_images: &Path,
writer: &mut dyn Write,
signing_key: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
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}");
}
}
) -> Result<(String, u64)> {
let mut header = read_info(input_info)?;
// Pre-open all of the image files.
let input_files = header
@@ -165,16 +208,35 @@ fn pack_subcommand(
.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(input_images, format!("{}.img", p.partition_name))?;
let file = File::open(&path)
.map(Arc::new)
.with_context(|| format!("Failed to open file: {path:?}"))?;
Ok((p.partition_name.clone(), file))
})
.collect::<Result<HashMap<_, _>>>()?;
for (name, input_file) in &input_files {
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
continue;
};
if !dpm.groups.iter().any(|g| g.partition_names.contains(name)) {
continue;
}
header
.manifest
.partitions
.iter_mut()
.find(|p| p.partition_name == *name)
.unwrap()
.estimate_cow_size = Some(0);
ota::recow_image(name, input_file, &mut header, cancel_signal)?;
}
// Compress the images and compute the list of install operations for
// insertion into the payload header. The compressed data is stored in new
// temp files and the original input files are dropped.
@@ -192,7 +254,8 @@ fn pack_subcommand(
// Now we can write the actual payload. With everything precomputed, this is
// mostly just a simple copy.
let mut payload_writer = open_writer(&cli.output, header.clone(), signing_key)?;
let mut payload_writer = PayloadWriter::new(writer, header.clone(), signing_key.clone())
.context("Failed to write payload header")?;
while payload_writer
.begin_next_operation()
@@ -228,12 +291,35 @@ fn pack_subcommand(
.with_context(|| format!("Failed to copy from replacement image: {name}"))?;
}
let (_, header, properties, _) = payload_writer
let (_, header, properties) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
display_header(quiet, &header);
Ok((properties, header.blob_offset))
}
fn pack_subcommand(
payload_cli: &PayloadCli,
cli: &PackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let mut writer = open_raw_writer(&cli.output)?;
let (properties, _) = pack_payload(
payload_cli.quiet,
&cli.input_info,
&cli.input_images,
&mut writer,
&signing_key,
cancel_signal,
)?;
writer.into_inner().context("Failed to flush payload")?;
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
@@ -251,7 +337,7 @@ fn repack_subcommand(
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let (mut reader, header) = open_reader(&cli.input, true)?;
let (mut reader, header) = open_reader(&cli.input)?;
info!("Generating new OTA payload");
@@ -290,12 +376,12 @@ fn repack_subcommand(
.with_context(|| format!("Failed to copy from original payload: {name}"))?;
}
let (_, header, properties, _) = payload_writer
let (_, header, properties) = payload_writer
.finish()
.context("Failed to finalize payload")?;
// Display the header information now that it has been finalized.
display_header(payload_cli, &header);
display_header(payload_cli.quiet, &header);
// Optionally, write payload_properties.txt.
if let Some(path) = &cli.output_properties {
@@ -307,9 +393,9 @@ fn repack_subcommand(
}
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
let (_, header) = open_reader(&cli.input, true)?;
let (_, header) = open_reader(&cli.input)?;
display_header(payload_cli, &header);
display_header(payload_cli.quiet, &header);
Ok(())
}
@@ -356,8 +442,7 @@ struct KeyGroup {
/// directory. The payload header metadata is written to the info TOML file.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
/// will fail and exit.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input payload binary.
@@ -381,6 +466,14 @@ struct UnpackCli {
default_value = "payload_images"
)]
output_images: PathBuf,
/// Do not output images.
#[arg(long, conflicts_with_all = ["output_images", "skeleton"])]
no_output_images: bool,
/// Only create empty sparse files for output images.
#[arg(long)]
skeleton: bool,
}
/// Pack a payload binary.
@@ -396,7 +489,7 @@ struct PackCli {
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
#[arg(short = 'O', long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
/// Path to input info TOML.
@@ -436,7 +529,7 @@ struct RepackCli {
output: PathBuf,
/// Path to output payload properties file.
#[arg(short, long, value_name = "FILE", value_parser)]
#[arg(short = 'O', long, value_name = "FILE", value_parser)]
output_properties: Option<PathBuf>,
#[command(flatten)]
@@ -446,7 +539,7 @@ struct RepackCli {
/// Display payload information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input payload file.
/// Path to input payload binary.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
+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_relative(to_skip)
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
ChunkData::Crc32(_) => {}
+751
View File
@@ -0,0 +1,751 @@
// SPDX-FileCopyrightText: 2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::BTreeMap,
ffi::OsString,
fs::{self, File, OpenOptions},
io::{BufReader, Seek, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result, anyhow};
use clap::{Args, Parser, Subcommand};
use rawzip::{
CompressionMethod, RECOMMENDED_BUFFER_SIZE, ZipArchive, ZipArchiveEntryWayfinder,
ZipArchiveWriter, ZipDataWriter, ZipEntryWriter, extra_fields::ExtraFieldId,
};
use serde::{Deserialize, Serialize};
use tracing::warn;
use x509_cert::Certificate;
use crate::{
cli::payload,
crypto::{self, PassphraseSource, RsaSigningKey},
format::{
ota::{self, SigningWriter, ZipEntry, ZipMode},
payload::PayloadHeader,
zip::{
self, ReaderAtWrapper, ZipArchiveReadAtExt, ZipEntriesSafeExt, ZipFileHeaderRecordExt,
},
},
protobuf::build::tools::releasetools::OtaMetadata,
stream::{self, FromReader},
util,
};
#[derive(Clone, Debug, Deserialize, Serialize)]
struct OtaInfo {
metadata: OtaMetadata,
files: Vec<String>,
}
struct InputEntry {
compression_method: CompressionMethod,
is_zip64: bool,
wayfinder: ZipArchiveEntryWayfinder,
}
fn is_excluded_path(path: &str) -> bool {
path == ota::PATH_METADATA || path == ota::PATH_METADATA_PB || path == ota::PATH_OTACERT
}
#[allow(clippy::type_complexity)]
fn open_reader(
path: &Path,
) -> Result<(
ZipArchive<ReaderAtWrapper<File>>,
OtaMetadata,
BTreeMap<String, InputEntry>,
)> {
let mut reader =
File::open(path).with_context(|| format!("Failed to open OTA for reading: {path:?}"))?;
let (metadata, _, _, _) = ota::parse_zip_ota_info(&mut reader)
.with_context(|| format!("Failed to parse OTA metadata: {path:?}"))?;
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let zip_reader =
ZipArchive::from_read_at(reader, &mut buffer).context("Failed to read OTA zip")?;
let mut entries = zip_reader.entries_safe(&mut buffer);
let mut input_entries = BTreeMap::new();
while let Some((cd_entry, _)) = entries.next_entry().context("Failed to list zip entries")? {
if cd_entry.is_dir() {
continue;
}
let path = cd_entry
.file_path_utf8()
.context("Zip contains non-UTF-8 paths")?;
if is_excluded_path(path) {
// OTA metadata is never copied from the input file.
continue;
}
input_entries.insert(
path.to_owned(),
InputEntry {
compression_method: cd_entry.compression_method(),
// We only check for the sizes here instead of the presence of
// the ZIP64 extra field. The central header's extra fields may
// have ZIP64 only for the local header offset.
is_zip64: cd_entry.compressed_size_hint() >= 0xffffffff
|| cd_entry.uncompressed_size_hint() >= 0xffffffff,
wayfinder: cd_entry.wayfinder(),
},
);
}
Ok((zip_reader, metadata, input_entries))
}
fn open_writer(path: &Path, zip_mode: ZipMode) -> Result<ZipArchiveWriter<SigningWriter<File>>> {
let writer = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.with_context(|| format!("Failed to open OTA for writing: {path:?}"))?;
let signing_writer = match zip_mode {
ZipMode::Streaming => SigningWriter::new_streaming(writer),
ZipMode::Seekable => SigningWriter::new_seekable(writer),
};
let zip_writer = ZipArchiveWriter::new(signing_writer);
Ok(zip_writer)
}
fn read_info(path: &Path) -> Result<OtaInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read OTA info TOML: {path:?}"))?;
let info = toml::de::from_str(&data)
.with_context(|| format!("Failed to parse OTA info TOML: {path:?}"))?;
Ok(info)
}
fn write_info(path: &Path, info: &OtaInfo) -> Result<()> {
let data = toml::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize OTA info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write OTA info TOML: {path:?}"))?;
Ok(())
}
fn display_info(quiet: bool, info: &OtaInfo) {
if !quiet {
println!("{info:#?}");
}
}
fn load_key(group: &KeyGroup) -> Result<(RsaSigningKey, Certificate)> {
let source = PassphraseSource::new(
&group.key,
group.pass_file.as_deref(),
group.pass_env_var.as_deref(),
);
let signing_key = if let Some(helper) = &group.signing_helper {
let public_key = crypto::read_pem_public_key_file(&group.key)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::External {
program: helper.clone(),
public_key_file: group.key.clone(),
public_key,
passphrase_source: source,
}
} else {
let private_key = crypto::read_pem_key_file(&group.key, &source)
.with_context(|| format!("Failed to load key: {:?}", group.key))?;
RsaSigningKey::Internal(private_key)
};
let cert = crypto::read_pem_cert_file(&group.cert)
.with_context(|| format!("Failed to load certificate: {:?}", group.cert))?;
Ok((signing_key, cert))
}
fn start_entry<'a>(
zip_writer: &'a mut ZipArchiveWriter<SigningWriter<File>>,
path: &str,
zip_mode: ZipMode,
is_zip64: bool,
) -> Result<(u64, ZipDataWriter<ZipEntryWriter<'a, SigningWriter<File>>>)> {
let mut builder = zip_writer
.new_file(path)
.compression_method(CompressionMethod::Store);
if zip_mode == ZipMode::Seekable && is_zip64 {
// We need to reserve space for the ZIP64 extra field when doing the
// post-processing to convert a streaming zip to a seekable one.
builder = builder.extra_field(
ExtraFieldId::ANDROID_ZIP_ALIGNMENT,
&[0u8; 16],
rawzip::Header::LOCAL,
)?;
}
let (entry_writer, data_config) = builder
.start()
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let offset = entry_writer.stream_offset();
let data_writer = data_config.wrap(entry_writer);
Ok((offset, data_writer))
}
fn finalize_entry(
path: &str,
offset: u64,
data_writer: ZipDataWriter<ZipEntryWriter<SigningWriter<File>>>,
metadata_entries: &mut Vec<ZipEntry>,
) -> Result<()> {
let size = data_writer
.finish()
.and_then(|(w, d)| w.finish(d))
.with_context(|| format!("Failed to finalize zip entry: {path}"))?;
metadata_entries.push(ZipEntry {
path: path.to_owned(),
offset,
size,
});
Ok(())
}
fn add_otacert_entry(
zip_writer: &mut ZipArchiveWriter<SigningWriter<File>>,
zip_mode: ZipMode,
cert: &Certificate,
metadata_entries: &mut Vec<ZipEntry>,
) -> Result<()> {
let (offset, mut data_writer) = start_entry(zip_writer, ota::PATH_OTACERT, zip_mode, false)?;
crypto::write_pem_cert(Path::new(ota::PATH_OTACERT), &mut data_writer, cert)
.with_context(|| format!("Failed to write entry: {}", ota::PATH_OTACERT))?;
finalize_entry(ota::PATH_OTACERT, offset, data_writer, metadata_entries)?;
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn finalize_ota(
key: &RsaSigningKey,
cert: &Certificate,
mut zip_writer: ZipArchiveWriter<SigningWriter<File>>,
zip_mode: ZipMode,
metadata_entries: &mut Vec<ZipEntry>,
payload_metadata_size: Option<u64>,
metadata: &mut OtaMetadata,
cancel_signal: &AtomicBool,
) -> Result<File> {
add_otacert_entry(&mut zip_writer, zip_mode, cert, metadata_entries)?;
let payload_metadata_size =
payload_metadata_size.ok_or_else(|| anyhow!("Missing payload metadata size"))?;
let metadata_offset = zip_writer.stream_offset();
*metadata = ota::add_metadata(
metadata_entries,
&mut zip_writer,
// Offset where next entry would begin.
metadata_offset,
metadata,
payload_metadata_size,
)
.context("Failed to write new OTA metadata")?;
let signing_writer = zip_writer
.finish()
.context("Failed to finalize output zip")?;
let mut raw_writer = signing_writer
.finish(key, cert, cancel_signal)
.context("Failed to sign output zip")?;
raw_writer.rewind().context("Failed to seek output zip")?;
ota::verify_metadata(
BufReader::new(&mut raw_writer),
metadata,
payload_metadata_size,
)
.context("Failed to verify OTA metadata offsets")?;
Ok(raw_writer)
}
fn unpack_subcommand(zip_cli: &ZipCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
let (zip_reader, metadata, input_entries) = open_reader(&cli.input)?;
for (path, input_entry) in &input_entries {
let entry = zip_reader
.get_entry(input_entry.wayfinder)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
if cli.payload && path == ota::PATH_PAYLOAD {
let (payload_offset, payload_size) = {
let range = entry.compressed_data_range();
(range.0, range.1 - range.0)
};
payload::unpack_payload(
zip_cli.quiet,
&cli.output_payload_info,
&cli.output_payload_images,
cli.no_output_payload_images,
cli.payload_skeleton,
&zip_reader.get_ref().0,
payload_offset,
payload_size,
cancel_signal,
)?;
} else if cli.payload && path == ota::PATH_PROPERTIES {
// We intentionally ignore this since it's recomputed when
// packing with --payload.
} else if !cli.no_output_files {
let mut entry_reader = zip::verifying_reader(&entry, input_entry.compression_method)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
let output_path = util::path_join(&cli.output_files, path)?;
let output_parent = output_path.parent().expect("No parent path");
fs::create_dir_all(output_parent)
.with_context(|| format!("Failed to create directory: {output_parent:?}"))?;
let mut output_file = File::create(&output_path)
.with_context(|| format!("Failed to open for writing: {output_path:?}"))?;
stream::copy(&mut entry_reader, &mut output_file, cancel_signal)
.with_context(|| format!("Failed to extract zip entry: {path}"))?;
}
}
let info = OtaInfo {
metadata,
files: input_entries.into_keys().collect(),
};
display_info(zip_cli.quiet, &info);
write_info(&cli.output_info, &info)?;
Ok(())
}
fn pack_subcommand(zip_cli: &ZipCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let (signing_key, cert) = load_key(&cli.key)?;
let mut info = read_info(&cli.input_info)?;
let mut zip_writer = open_writer(&cli.output, cli.zip_mode.zip_mode)?;
let mut input_entries = BTreeMap::new();
for path in &info.files {
if is_excluded_path(path) {
warn!("Ignoring input file: {path}");
continue;
}
let input_size =
if cli.payload && (path == ota::PATH_PAYLOAD || path == ota::PATH_PROPERTIES) {
0
} else {
let input_path = util::path_join(&cli.input_files, path)?;
fs::metadata(&input_path)
.map(|m| m.len())
.with_context(|| format!("Failed to get file size: {input_path:?}"))?
};
input_entries.insert(path, input_size >= 0xffffffff);
}
let mut payload_metadata_size = None;
let mut metadata_entries = vec![];
// Only used when packing the zip and payload at the same time.
let mut payload_properties = None;
for (&path, &is_zip64) in &input_entries {
let (offset, mut data_writer) =
start_entry(&mut zip_writer, path, cli.zip_mode.zip_mode, is_zip64)?;
if path == ota::PATH_PAYLOAD && cli.payload {
// Pack the payload directly into the zip.
let (properties, metadata_size) = payload::pack_payload(
zip_cli.quiet,
&cli.input_payload_info,
&cli.input_payload_images,
&mut data_writer,
&signing_key,
cancel_signal,
)?;
payload_metadata_size = Some(metadata_size);
payload_properties = Some(properties);
} else if path == ota::PATH_PROPERTIES && cli.payload {
// We process files in order, so this would only happen if the file
// list did not contain the payload.
let properties = payload_properties
.as_ref()
.ok_or_else(|| anyhow!("Payload has not been processed yet"))?;
data_writer
.write_all(properties.as_bytes())
.with_context(|| format!("Failed to write zip entry: {path}"))?;
} else {
let input_path = util::path_join(&cli.input_files, path)?;
let mut input_file = File::open(&input_path)
.with_context(|| format!("Failed to open for reading: {input_path:?}"))?;
if path == ota::PATH_PAYLOAD {
let header = PayloadHeader::from_reader(&mut input_file)
.with_context(|| format!("Failed to read payload header: {input_path:?}"))?;
payload_metadata_size = Some(header.blob_offset);
input_file
.rewind()
.with_context(|| format!("Failed to seek file: {input_path:?}"))?;
}
stream::copy(&mut input_file, &mut data_writer, cancel_signal)
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
}
finalize_entry(path, offset, data_writer, &mut metadata_entries)?;
}
finalize_ota(
&signing_key,
&cert,
zip_writer,
cli.zip_mode.zip_mode,
&mut metadata_entries,
payload_metadata_size,
&mut info.metadata,
cancel_signal,
)?;
drop(input_entries);
info.files.retain(|p| !is_excluded_path(p));
info.files.sort();
display_info(zip_cli.quiet, &info);
if let Some(path) = &cli.output_info {
write_info(path, &info)?;
}
Ok(())
}
fn repack_subcommand(zip_cli: &ZipCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
let (signing_key, cert) = load_key(&cli.key)?;
let (zip_reader, mut metadata, input_entries) = open_reader(&cli.input)?;
let mut zip_writer = open_writer(&cli.output, cli.zip_mode.zip_mode)?;
let mut payload_metadata_size = None;
let mut metadata_entries = vec![];
for (path, input_entry) in &input_entries {
let (offset, mut data_writer) = start_entry(
&mut zip_writer,
path,
cli.zip_mode.zip_mode,
input_entry.is_zip64,
)?;
let entry = zip_reader
.get_entry(input_entry.wayfinder)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
if path == ota::PATH_PAYLOAD {
let entry_reader = zip::verifying_reader(&entry, input_entry.compression_method)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
let header = PayloadHeader::from_reader(entry_reader)
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
payload_metadata_size = Some(header.blob_offset);
}
let mut entry_reader = zip::verifying_reader(&entry, input_entry.compression_method)
.with_context(|| format!("Failed to open zip entry: {path}"))?;
stream::copy(&mut entry_reader, &mut data_writer, cancel_signal)
.with_context(|| format!("Failed to copy zip entry: {path}"))?;
finalize_entry(path, offset, data_writer, &mut metadata_entries)?;
}
finalize_ota(
&signing_key,
&cert,
zip_writer,
cli.zip_mode.zip_mode,
&mut metadata_entries,
payload_metadata_size,
&mut metadata,
cancel_signal,
)?;
let info = OtaInfo {
metadata,
files: input_entries.into_keys().collect(),
};
display_info(zip_cli.quiet, &info);
Ok(())
}
fn info_subcommand(zip_cli: &ZipCli, cli: &InfoCli) -> Result<()> {
let (_, metadata, input_entries) = open_reader(&cli.input)?;
let info = OtaInfo {
metadata,
files: input_entries.into_keys().collect(),
};
display_info(zip_cli.quiet, &info);
Ok(())
}
pub fn zip_main(cli: &ZipCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
ZipCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
ZipCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
ZipCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
ZipCommand::Info(c) => info_subcommand(cli, c),
}
}
#[derive(Debug, Args)]
struct KeyGroup {
/// Path to signing key.
///
/// This should normally be a private key. However, if --signing-helper is
/// used, then it should be a public key instead.
#[arg(short, long, value_name = "FILE", value_parser)]
key: PathBuf,
/// Certificate for signing key.
#[arg(short, long, value_name = "FILE", value_parser)]
cert: PathBuf,
/// Environment variable containing private key passphrase.
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
pass_env_var: Option<OsString>,
/// File containing private key passphrase.
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
pass_file: Option<PathBuf>,
/// External program for signing.
///
/// If this option is specified, then --key must refer to a public key. The
/// program will be invoked as:
///
/// <program> <algo> <public key> [file <pass file>|env <pass env>]
#[arg(long, value_name = "PROGRAM", value_parser)]
signing_helper: Option<PathBuf>,
}
#[derive(Debug, Args)]
struct ZipModeGroup {
/// Zip creation mode for the output OTA zip.
///
/// The streaming mode produces zip files that contain data descriptors.
/// The zip file is hashed as it is being written. This mode is the default
/// and works with the vast majority of devices.
///
/// The seekable mode produces zip files that do not use data descriptors.
/// The zip file is reread and hashed after it has been fully written. The
/// output file is more likely to be compatible with devices that have
/// broken zip file parsers.
#[arg(
long,
value_name = "MODE",
default_value_t = ZipMode::Streaming,
)]
zip_mode: ZipMode,
}
/// Unpack an OTA zip.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "ota.toml")]
output_info: PathBuf,
/// Path to output files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "ota_files")]
output_files: PathBuf,
/// Do not output files.
#[arg(long, conflicts_with = "output_files")]
no_output_files: bool,
/// Also unpack the payload in the OTA.
///
/// Unpacking the zip and payload at the same time is more efficient than
/// unpacking them separately.
#[arg(long)]
payload: bool,
/// Path to output payload info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml",
requires = "payload"
)]
output_payload_info: PathBuf,
/// Path to output payload images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images",
requires = "payload"
)]
output_payload_images: PathBuf,
/// Do not output payload images.
#[arg(
long,
conflicts_with_all = ["output_payload_images", "payload_skeleton"],
requires = "payload"
)]
no_output_payload_images: bool,
/// Only create empty sparse files for output payload images.
#[arg(long, requires = "payload")]
payload_skeleton: bool,
}
/// Pack an OTA zip.
///
/// The new OTA zip will *only* contain files listed in the info TOML file.
/// Extra files in the input files directory that aren't listed will be silently
/// ignored.
///
/// WARNING: This subcommand is for raw file manipulation and does not perform
/// any validation of the contents of the payload binary. For normal usage, use
/// `avbroot ota patch` instead.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "ota.toml")]
input_info: PathBuf,
/// Path to output info TOML.
///
/// If specified, the OTA info containing all recomputed fields will be
/// written to this file. This can point to the same file as --input-info.
#[arg(long, value_name = "FILE", value_parser)]
output_info: Option<PathBuf>,
/// Path to input files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "ota_files")]
input_files: PathBuf,
/// Also pack the payload in the OTA.
///
/// Packing the zip and payload at the same time is more efficient than
/// packing them separately.
#[arg(long)]
payload: bool,
/// Path to input payload info TOML.
#[arg(
long,
value_name = "FILE",
value_parser,
default_value = "payload.toml",
requires = "payload"
)]
input_payload_info: PathBuf,
/// Path to input payload images directory.
#[arg(
long,
value_name = "DIR",
value_parser,
default_value = "payload_images",
requires = "payload"
)]
input_payload_images: PathBuf,
#[command(flatten)]
key: KeyGroup,
#[command(flatten)]
zip_mode: ZipModeGroup,
}
/// Repack an OTA zip.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk first.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
#[command(flatten)]
key: KeyGroup,
#[command(flatten)]
zip_mode: ZipModeGroup,
}
/// Display OTA zip information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum ZipCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect OTA zips.
#[derive(Debug, Parser)]
pub struct ZipCli {
#[command(subcommand)]
command: ZipCommand,
/// Don't print OTA metadata information.
#[arg(short, long, global = true)]
quiet: bool,
}
+9 -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;
@@ -144,6 +144,7 @@ pub enum PassphraseSource {
impl PassphraseSource {
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
#[allow(clippy::option_if_let_else)]
if let Some(v) = env_var {
Self::EnvVar(v.to_owned())
} else if let Some(p) = pass_file {
@@ -159,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)));
+3 -3
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)]
@@ -151,11 +151,11 @@ mod tests {
contents.extend("💩".as_bytes());
let data = TestData { contents };
let serialized = toml_edit::ser::to_string(&data).unwrap();
let serialized = toml::ser::to_string(&data).unwrap();
assert_eq!(serialized, "contents = 'foo\\xFFbar💩'\n");
let new_data: TestData = toml_edit::de::from_str(&serialized).unwrap();
let new_data: TestData = toml::de::from_str(&serialized).unwrap();
assert_eq!(data.contents, new_data.contents);
}
}
+62 -79
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -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::{
@@ -29,8 +29,8 @@ use crate::{
padding::{self, ZeroPadding},
},
stream::{
self, CountingReader, CountingWriter, FromReader, ReadFixedSizeExt, ReadSeekReopen,
ToWriter, WriteSeekReopen, WriteZerosExt,
self, CountingReader, CountingWriter, FromReader, ReadAt, ReadFixedSizeExt, ReadWriteAt,
ToWriter, UserPosFile, WriteZerosExt,
},
util::{self, OutOfBoundsError},
};
@@ -149,10 +149,6 @@ pub enum Error {
FecVerify(#[source] fec::Error),
#[error("Failed to repair file with FEC data")]
FecRepair(#[source] fec::Error),
#[error("Failed to reopen input file")]
InputReopen(#[source] io::Error),
#[error("Failed to reopen output file")]
OutputReopen(#[source] io::Error),
#[error("Failed to compute hash of input file")]
InputDigest(#[source] io::Error),
#[error("Failed to read AVB data: {0}")]
@@ -163,9 +159,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())),
@@ -510,10 +506,7 @@ impl HashTreeDescriptor {
/// Update the root hash, hash tree, and FEC data. The hash tree and FEC
/// data will be written immediately following the image data at offset
/// [`Self::image_size`]. Both `open_input` and `open_output` may be called
/// from multiple threads and must return independently seekable handles to
/// the same file. It is guaranteed that every thread will read and write
/// disjoint file offsets.
/// [`Self::image_size`].
///
/// If `ranges` is [`Option::None`], then the hash tree and FEC data are
/// updated for the whole while. Due to the nature of the file access
@@ -525,42 +518,37 @@ impl HashTreeDescriptor {
/// that what is specified in order to perform the computations.
///
/// The fields in this instance are updated atomically. No fields are
/// updated if an error occurs. The input file can be restored back to its
/// updated if an error occurs. The file can be restored back to its
/// original state by truncating it to [`Self::image_size`].
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
file: &(dyn ReadWriteAt + Sync),
ranges: Option<&[Range<u64>]>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let algorithm = digest_algorithm(&self.hash_algorithm, false)?;
let mut pos_file = UserPosFile::new(file);
let algorithm = digest_algorithm(&self.hash_algorithm)?;
let hash_tree = HashTree::new(self.data_block_size, algorithm, &self.salt);
let tree_offset = self.image_size;
let (root_digest, hash_tree_data) = match ranges {
Some(r) => {
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
reader
.seek(SeekFrom::Start(self.tree_offset))
pos_file
.seek(SeekFrom::Start(tree_offset))
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
let mut hash_tree_data = reader
let mut hash_tree_data = pos_file
.read_vec_exact(self.tree_size as usize)
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
let root_digest = hash_tree
.update(
input,
self.image_size,
r,
&mut hash_tree_data,
cancel_signal,
)
.update(file, self.image_size, r, &mut hash_tree_data, cancel_signal)
.map_err(Error::HashTreeUpdate)?;
(root_digest, hash_tree_data)
}
None => hash_tree
.generate(input, self.image_size, cancel_signal)
.generate(file, self.image_size, cancel_signal)
.map_err(Error::HashTreeGenerate)?,
};
@@ -569,11 +557,10 @@ impl HashTreeDescriptor {
let tree_size = hash_tree_data.len() as u64;
let mut writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
writer
.seek(SeekFrom::Start(self.image_size))
pos_file
.seek(SeekFrom::Start(tree_offset))
.map_err(|e| Error::DataWrite("HashTree::tree_data", e))?;
writer
pos_file
.write_all(&hash_tree_data)
.map_err(|e| Error::DataWrite("HashTree::tree_data", e))?;
@@ -588,66 +575,62 @@ impl HashTreeDescriptor {
let parity: u8 = util::try_cast(self.fec_num_roots)
.map_err(|e| Error::IntOutOfBounds("HashTree::fec_num_roots", e))?;
let fec_offset = tree_offset + tree_size;
let fec_data = if let Some(r) = ranges {
let mut r_with_hash_tree = r.to_vec();
r_with_hash_tree.push(self.tree_offset..self.tree_offset + tree_size);
r_with_hash_tree.push(tree_offset..tree_offset + tree_size);
let (fec, fec_size) = self.get_fec()?;
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
reader
.seek(SeekFrom::Start(self.fec_offset))
pos_file
.seek(SeekFrom::Start(fec_offset))
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
let mut fec_data = reader
let mut fec_data = pos_file
.read_vec_exact(fec_size)
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
fec.update(input, &r_with_hash_tree, &mut fec_data, cancel_signal)
fec.update(file, &r_with_hash_tree, &mut fec_data, cancel_signal)
.map_err(Error::FecUpdate)?;
fec_data
} else {
// The FEC covers the hash tree as well.
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)
.map_err(Error::FecInit)?;
fec.generate(input, cancel_signal)
let fec =
Fec::new(fec_offset, self.data_block_size, parity).map_err(Error::FecInit)?;
fec.generate(file, cancel_signal)
.map_err(Error::FecGenerate)?
};
// Already seeked to FEC.
writer
pos_file
.seek(SeekFrom::Start(fec_offset))
.map_err(|e| Error::DataWrite("HashTree::fec_data", e))?;
pos_file
.write_all(&fec_data)
.map_err(|e| Error::DataWrite("HashTree::fec_data", e))?;
self.fec_offset = self.image_size + tree_size;
self.fec_offset = fec_offset;
self.fec_size = fec_data.len() as u64;
}
self.tree_offset = self.image_size;
self.tree_offset = tree_offset;
self.tree_size = tree_size;
self.root_digest = root_digest;
Ok(())
}
/// Verify the root hash, hash tree, and FEC data. `open_input` will be
/// called from multiple threads and must return independently seekable
/// handles to the same file.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
/// Verify the root hash, hash tree, and FEC data.
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> 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))?;
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
let mut reader = UserPosFile::new(input);
reader
.seek(SeekFrom::Start(self.tree_offset))
.map_err(|e| Error::DataRead("HashTree::tree_data", e))?;
@@ -684,9 +667,7 @@ impl HashTreeDescriptor {
Ok(())
}
/// Try to repair errors in the input file using the FEC data. Both
/// `open_input` and `open_output` may be called from multiple threads and
/// must return independently seekable handles to the same file.
/// Try to repair errors in the input file using the FEC data.
///
/// Due to the nature of FEC, when there are too many errors, it's possible
/// for the data to be miscorrected to a "valid" state. [`Self::verify()`]
@@ -694,8 +675,7 @@ impl HashTreeDescriptor {
/// actually valid.
pub fn repair(
&self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
file: &(dyn ReadWriteAt + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
self.check_offsets()?;
@@ -705,7 +685,7 @@ impl HashTreeDescriptor {
return Err(Error::FecMissing);
}
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
let mut reader = UserPosFile::new(file);
reader
.seek(SeekFrom::Start(self.fec_offset))
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
@@ -717,7 +697,7 @@ impl HashTreeDescriptor {
.read_vec_exact(fec_size)
.map_err(|e| Error::DataRead("HashTree::fec_data", e))?;
fec.repair(input, output, &fec_data, cancel_signal)
fec.repair(file, &fec_data, cancel_signal)
.map_err(Error::FecRepair)?;
Ok(())
@@ -903,10 +883,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 +903,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 +1637,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 +1663,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 +1738,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 +2158,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
+14 -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::{
@@ -269,6 +269,7 @@ impl fmt::Display for BootImageV0Through2 {
impl BootImageExt for BootImageV0Through2 {
fn header_version(&self) -> u32 {
#[allow(clippy::bool_to_int_with_if)]
if self.v2_extra.is_some() {
2
} else if self.v1_extra.is_some() {
@@ -386,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())
@@ -668,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 {
@@ -873,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(())
@@ -1093,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 {
+180 -37
View File
@@ -1,15 +1,15 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2026 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::{DeflateDecoder, GzDecoder},
write::{DeflateEncoder, GzEncoder},
};
use lz4_flex::frame::FrameDecoder;
use lzma_rust2::{CheckType, LzmaOptions, LzmaReader, LzmaWriter, XzOptions, XzReader, XzWriter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -18,6 +18,14 @@ use crate::stream::ReadFixedSizeExt;
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
// This is not actually a magic value, but is good enough. The first byte
// encodes (lc=3, lp=0, pb=2) and the next two bytes are two of the U32LE bytes
// encoding the dictionary size. If this heuristic is good enough for libmagic,
// then it's good enough for us too.
// https://nigeltao.github.io/blog/2024/xz-lzma-part-5-xz.html
static LZMA_MAGIC: &[u8; 3] = b"\x5d\x00\x00";
const LZMA_MAX_MEMORY_KIB: u32 = 16 * 1024;
#[derive(Debug, Error)]
pub enum Error {
@@ -26,9 +34,13 @@ pub enum Error {
#[error("I/O error when autodetecting compression format")]
AutoDetect(#[source] io::Error),
#[error("Failed to initialize legacy LZ4 encoder")]
Lz4Init(#[source] io::Error),
Lz4EncodeInit(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
XzEncodeInit(#[source] io::Error),
#[error("Failed to initialize LZMA decoder")]
LzmaDecodeInit(#[source] io::Error),
#[error("Failed to initialize LZMA encoder")]
LzmaEncodeInit(#[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -109,16 +121,60 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum CompressedFormat {
None,
Deflate,
Gzip,
Lz4Legacy,
Xz,
Lzma,
}
pub enum CompressedReader<R: Read> {
None(R),
/// Not autodetected.
Deflate(DeflateDecoder<R>),
Gzip(GzDecoder<R>),
Lz4(FrameDecoder<R>),
Xz(XzDecoder<R>),
/// Boxed because the [`XzReader`] is nearly 4 KiB.
Xz(Box<XzReader<R>>),
Lzma(Box<LzmaReader<R>>),
}
impl<R: Read> CompressedReader<R> {
pub fn with_format(reader: R, format: CompressedFormat) -> Result<Self> {
match format {
CompressedFormat::None => Ok(Self::None(reader)),
CompressedFormat::Deflate => Ok(Self::Deflate(DeflateDecoder::new(reader))),
CompressedFormat::Gzip => Ok(Self::Gzip(GzDecoder::new(reader))),
CompressedFormat::Lz4Legacy => Ok(Self::Lz4(FrameDecoder::new(reader))),
CompressedFormat::Xz => Ok(Self::Xz(Box::new(XzReader::new(reader, false)))),
CompressedFormat::Lzma => Ok(Self::Lzma(Box::new(
LzmaReader::new_mem_limit(reader, LZMA_MAX_MEMORY_KIB, None)
.map_err(Error::LzmaDecodeInit)?,
))),
}
}
pub fn format(&self) -> CompressedFormat {
match self {
Self::None(_) => CompressedFormat::None,
Self::Deflate(_) => CompressedFormat::Deflate,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
Self::Lzma(_) => CompressedFormat::Lzma,
}
}
pub fn into_inner(self) -> R {
match self {
Self::None(r) => r,
Self::Deflate(r) => r.into_inner(),
Self::Gzip(r) => r.into_inner(),
Self::Lz4(r) => r.into_inner(),
Self::Xz(r) => r.into_inner(),
Self::Lzma(r) => r.into_inner(),
}
}
}
impl<R: Read + Seek> CompressedReader<R> {
@@ -127,35 +183,21 @@ impl<R: Read + Seek> CompressedReader<R> {
reader.rewind().map_err(Error::AutoDetect)?;
if &magic[0..2] == GZIP_MAGIC {
Ok(Self::Gzip(GzDecoder::new(reader)))
let format = if &magic[0..2] == GZIP_MAGIC {
CompressedFormat::Gzip
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
CompressedFormat::Lz4Legacy
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(XzDecoder::new(reader)))
CompressedFormat::Xz
} else if &magic[0..3] == LZMA_MAGIC {
CompressedFormat::Lzma
} else if raw_if_unknown {
Ok(Self::None(reader))
CompressedFormat::None
} else {
Err(Error::UnknownFormat)
}
}
return Err(Error::UnknownFormat);
};
pub fn format(&self) -> CompressedFormat {
match self {
Self::None(_) => CompressedFormat::None,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
}
}
pub fn into_inner(self) -> R {
match self {
Self::None(r) => r,
Self::Gzip(r) => r.into_inner(),
Self::Lz4(r) => r.into_inner(),
Self::Xz(r) => r.into_inner(),
}
Self::with_format(reader, format)
}
}
@@ -163,35 +205,56 @@ impl<R: Read> Read for CompressedReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::None(r) => r.read(buf),
Self::Deflate(r) => r.read(buf),
Self::Gzip(r) => r.read(buf),
Self::Lz4(r) => r.read(buf),
Self::Xz(r) => r.read(buf),
Self::Lzma(r) => r.read(buf),
}
}
}
#[allow(clippy::large_enum_variant)]
pub enum CompressedWriter<W: Write> {
None(W),
Deflate(DeflateEncoder<W>),
Gzip(GzEncoder<W>),
Lz4Legacy(Lz4LegacyEncoder<W>),
Xz(XzEncoder<W>),
Xz(Box<XzWriter<W>>),
Lzma(Box<LzmaWriter<W>>),
}
impl<W: Write> CompressedWriter<W> {
pub fn new(writer: W, format: CompressedFormat) -> Result<Self> {
match format {
CompressedFormat::None => Ok(Self::None(writer)),
CompressedFormat::Deflate => Ok(Self::Deflate(DeflateEncoder::new(
writer,
Compression::default(),
))),
CompressedFormat::Gzip => {
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
}
CompressedFormat::Lz4Legacy => {
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4EncodeInit)?;
Ok(Self::Lz4Legacy(encoder))
}
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::XzEncodeInit)?;
Ok(Self::Xz(Box::new(xz_writer)))
}
CompressedFormat::Lzma => {
// Defaults are already (lc=3, lp=0, pb=2) and has preset 6.
let options = LzmaOptions::default();
// We only support streaming output.
let lzma_writer = LzmaWriter::new_use_header(writer, &options, None)
.map_err(Error::LzmaEncodeInit)?;
Ok(Self::Lzma(Box::new(lzma_writer)))
}
}
}
@@ -199,18 +262,22 @@ impl<W: Write> CompressedWriter<W> {
pub fn format(&self) -> CompressedFormat {
match self {
Self::None(_) => CompressedFormat::None,
Self::Deflate(_) => CompressedFormat::Deflate,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
Self::Lzma(_) => CompressedFormat::Lzma,
}
}
pub fn finish(self) -> io::Result<W> {
match self {
Self::None(w) => Ok(w),
Self::Deflate(w) => w.finish(),
Self::Gzip(w) => w.finish(),
Self::Lz4Legacy(w) => w.finish(),
Self::Xz(w) => w.finish(),
Self::Lzma(w) => w.finish(),
}
}
}
@@ -219,18 +286,94 @@ impl<W: Write> Write for CompressedWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match self {
Self::None(w) => w.write(buf),
Self::Deflate(w) => w.write(buf),
Self::Gzip(w) => w.write(buf),
Self::Lz4Legacy(w) => w.write(buf),
Self::Xz(w) => w.write(buf),
Self::Lzma(w) => w.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match self {
Self::None(w) => w.flush(),
Self::Deflate(w) => w.flush(),
Self::Gzip(w) => w.flush(),
Self::Lz4Legacy(w) => w.flush(),
Self::Xz(w) => w.flush(),
Self::Lzma(w) => w.flush(),
}
}
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use rand::Rng;
use super::*;
fn round_trip(format: CompressedFormat, can_autodetect: bool) {
let mut data = vec![0u8; 1024 * 1024];
rand::thread_rng().fill(data.as_mut_slice());
let mut writer = CompressedWriter::new(Cursor::new(Vec::new()), format).unwrap();
writer.write_all(&data).unwrap();
let mut compressed = writer.finish().unwrap();
compressed.rewind().unwrap();
let mut reader = if can_autodetect {
CompressedReader::new(compressed, true).unwrap()
} else {
CompressedReader::with_format(compressed, format).unwrap()
};
assert_eq!(reader.format(), format);
let mut buf = [0u8; 8192];
let mut total_read = 0;
loop {
let n = reader.read(&mut buf).unwrap();
if n == 0 {
break;
}
assert_eq!(&buf[..n], &data[total_read..][..n]);
total_read += n;
}
assert_eq!(total_read, data.len());
}
#[test]
fn round_trip_none() {
round_trip(CompressedFormat::None, true);
}
#[test]
fn round_trip_deflate() {
round_trip(CompressedFormat::Deflate, false);
}
#[test]
fn round_trip_gzip() {
round_trip(CompressedFormat::Gzip, true);
}
#[test]
fn round_trip_lz4_legacy() {
round_trip(CompressedFormat::Lz4Legacy, true);
}
#[test]
fn round_trip_xz() {
round_trip(CompressedFormat::Xz, true);
}
#[test]
fn round_trip_lzma() {
round_trip(CompressedFormat::Lzma, true);
}
}
+60 -11
View File
@@ -89,12 +89,10 @@ impl fmt::Debug for RawHexU32 {
impl From<u32> for RawHexU32 {
fn from(mut value: u32) -> Self {
let mut buf = [b'0'; 8];
let mut index = 7;
while value != 0 {
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
for c in buf.iter_mut().rev() {
*c = char::from_digit(value & 0xf, 16).unwrap() as u8;
value >>= 4;
index -= 1;
}
Self(buf)
@@ -771,18 +769,14 @@ 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)
let mut inodes: HashMap<(u32, u32), (HashSet<u32>, u32)> = HashMap::new();
if missing_only {
for entry in &mut *entries {
for entry in &mut *entries {
if missing_only {
if entry.inode != 0 {
let key = (entry.dev_maj, entry.dev_min);
let (set, last) = inodes.entry(key).or_default();
@@ -790,6 +784,8 @@ pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()
set.insert(entry.inode);
*last = (*last).max(entry.inode);
}
} else {
entry.inode = 0;
}
}
@@ -841,3 +837,56 @@ pub fn save(
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_assign_inodes_missing() {
let mut entries = [
CpioEntry::new_directory(b"a", 0o755),
CpioEntry::new_directory(b"b", 0o755),
CpioEntry::new_directory(b"c", 0o755),
CpioEntry::new_directory(b"d", 0o755),
CpioEntry::new_directory(b"e", 0o755),
];
entries[0].inode = 999;
entries[2].inode = 500;
entries[3].dev_maj = 1;
entries[3].dev_min = 1;
assign_inodes(&mut entries, true).unwrap();
assert_eq!(entries[0].inode, 999);
assert_eq!(entries[1].inode, 1000);
assert_eq!(entries[2].inode, 500);
assert_eq!(entries[3].inode, 300000);
assert_eq!(entries[4].inode, 1001);
}
#[test]
fn test_assign_inodes_reassign() {
let mut entries = [
CpioEntry::new_directory(b"a", 0o755),
CpioEntry::new_directory(b"b", 0o755),
CpioEntry::new_directory(b"c", 0o755),
CpioEntry::new_directory(b"d", 0o755),
CpioEntry::new_directory(b"e", 0o755),
];
entries[0].dev_maj = 1;
entries[0].dev_min = 1;
entries[0].inode = 1000;
entries[1].inode = 1000;
entries[2].inode = 500;
entries[3].dev_maj = 1;
entries[3].dev_min = 1;
assign_inodes(&mut entries, false).unwrap();
assert_eq!(entries[0].inode, 300000);
assert_eq!(entries[1].inode, 300000);
assert_eq!(entries[2].inode, 300001);
assert_eq!(entries[3].inode, 300001);
}
}
+63 -88
View File
@@ -1,10 +1,10 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashSet,
fmt,
io::{self, Read, Seek, SeekFrom, Write},
io::{self, Read, SeekFrom, Write},
mem,
ops::Range,
sync::atomic::AtomicBool,
@@ -16,12 +16,15 @@ 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::{
format::verityrs,
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
stream::{
self, FromReader, ReadAt, ReadSeek, ReadWriteAt, ReadWriteSeek, ToWriter, UserPosFile,
WriteSeek, WriteZerosExt,
},
util::{self, NumBytes, OutOfBoundsError},
};
@@ -68,10 +71,8 @@ pub enum Error {
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to reopen input file")]
InputReopen(#[source] io::Error),
#[error("Failed to reopen output file")]
OutputReopen(#[source] io::Error),
#[error("Failed to get input file size")]
InputSize(#[source] io::Error),
#[error("Failed to read FEC data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write FEC data: {0}")]
@@ -248,7 +249,7 @@ impl Fec {
/// slice in the file offset grid.
fn read_seq_block(
&self,
mut reader: impl Read + Seek,
reader: &mut dyn ReadSeek,
offset: u64,
buf: &mut [u8],
) -> io::Result<()> {
@@ -276,7 +277,7 @@ impl Fec {
/// slice in the file offset grid.
fn write_seq_block(
&self,
mut writer: impl Write + Seek,
writer: &mut dyn WriteSeek,
offset: u64,
buf: &[u8],
) -> io::Result<()> {
@@ -299,7 +300,7 @@ impl Fec {
/// Read the nth round from the file. The data is laid out sequentially
/// (row-by-row).
fn read_round(&self, mut reader: impl Read + Seek, round: u64) -> io::Result<Vec<u8>> {
fn read_round(&self, reader: &mut dyn ReadSeek, round: u64) -> io::Result<Vec<u8>> {
let mut grid = vec![0u8; usize::from(self.rs_k) * self.block_size as usize];
for row in 0..self.rs_k {
@@ -309,7 +310,7 @@ impl Fec {
let row_end = row_start + self.block_size as usize;
let row_slice = &mut grid[row_start..row_end];
self.read_seq_block(&mut reader, interleaved_offset, row_slice)?;
self.read_seq_block(reader, interleaved_offset, row_slice)?;
}
Ok(grid)
@@ -317,12 +318,7 @@ impl Fec {
/// Write the nth round to the file. The data is expected to be laid out
/// sequentially (row-by-row).
fn write_round(
&self,
mut writer: impl Write + Seek,
round: u64,
grid: &[u8],
) -> io::Result<()> {
fn write_round(&self, writer: &mut dyn WriteSeek, round: u64, grid: &[u8]) -> io::Result<()> {
for row in 0..self.rs_k {
let interleaved_offset =
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
@@ -330,7 +326,7 @@ impl Fec {
let row_end = row_start + self.block_size as usize;
let row_slice = &grid[row_start..row_end];
self.write_seq_block(&mut writer, interleaved_offset, row_slice)?;
self.write_seq_block(writer, interleaved_offset, row_slice)?;
}
Ok(())
@@ -360,7 +356,7 @@ impl Fec {
/// Generate FEC data for a single round.
fn generate_one_round(
&self,
reader: impl Read + Seek,
reader: &mut dyn ReadSeek,
round: u64,
fec: &mut [u8],
) -> Result<()> {
@@ -386,7 +382,7 @@ impl Fec {
}
/// Verify file data for a single round.
fn verify_one_round(&self, reader: impl Read + Seek, round: u64, fec: &[u8]) -> Result<()> {
fn verify_one_round(&self, reader: &mut dyn ReadSeek, round: u64, fec: &[u8]) -> Result<()> {
assert_eq!(
fec.len(),
usize::from(self.parity()) * self.block_size as usize,
@@ -414,8 +410,7 @@ impl Fec {
/// Repair file data for a single round.
fn repair_one_round(
&self,
reader: impl Read + Seek,
writer: impl Write + Seek,
file: &mut dyn ReadWriteSeek,
round: u64,
fec: &[u8],
) -> Result<u64> {
@@ -426,7 +421,7 @@ impl Fec {
);
let mut grid = self
.read_round(reader, round)
.read_round(file, round)
.map_err(|e| Error::DataRead("round", e))?;
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
let parity = usize::from(self.parity());
@@ -445,7 +440,7 @@ impl Fec {
}
if num_corrected > 0 {
self.write_round(writer, round, &grid)
self.write_round(file, round, &grid)
.map_err(|e| Error::DataWrite("round", e))?;
}
@@ -458,7 +453,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn generate(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let fec_size = self.fec_size();
@@ -466,13 +461,12 @@ impl Fec {
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
self.generate_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
let mut reader = UserPosFile::new(input);
self.generate_one_round(&mut reader, round as u64, buf)
})?;
Ok(fec)
}
@@ -482,7 +476,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
ranges: &[Range<u64>],
fec: &mut [u8],
cancel_signal: &AtomicBool,
@@ -501,15 +495,12 @@ impl Fec {
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
.enumerate()
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
self.generate_one_round(reader, round as u64, buf)
let mut reader = UserPosFile::new(input);
self.generate_one_round(&mut reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(())
}
/// Verify that the file contains no errors. This is significantly faster
@@ -519,7 +510,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -534,15 +525,12 @@ impl Fec {
fec.par_chunks_exact(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
self.verify_one_round(reader, round as u64, buf)
let mut reader = UserPosFile::new(input);
self.verify_one_round(&mut reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(())
}
/// Repair the file. Up to `parity / 2` bytes per codeword can be repaired.
@@ -558,8 +546,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn repair(
&self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
file: &(dyn ReadWriteAt + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<u64> {
@@ -576,15 +563,12 @@ impl Fec {
.par_chunks_exact(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<u64> {
stream::check_cancel(cancel_signal).map_err(Error::InputReopen)?;
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed().map_err(Error::InputReopen)?;
let writer = output.reopen_boxed().map_err(Error::OutputReopen)?;
self.repair_one_round(reader, writer, round as u64, buf)
let mut file = UserPosFile::new(file);
self.repair_one_round(&mut file, round as u64, buf)
})
.collect::<Result<Vec<u64>>>()?
.into_iter()
.sum();
.try_reduce(|| 0, |prev, cur| Ok(prev + cur))?;
Ok(num_corrected)
}
@@ -636,14 +620,11 @@ impl FecImage {
/// Generate FEC data for a file. `parity` is the number of parity bytes per
/// 255-byte Reed-Solomon codeword.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
parity: u8,
cancel_signal: &AtomicBool,
) -> Result<Self> {
let data_size = input
.reopen_boxed()
.and_then(|mut f| f.seek(SeekFrom::End(0)))
.map_err(Error::InputReopen)?;
let data_size = input.file_len().map_err(Error::InputSize)?;
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
let fec_data = fec.generate(input, cancel_signal)?;
@@ -657,7 +638,7 @@ impl FecImage {
/// Update FEC data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -667,11 +648,7 @@ impl FecImage {
/// Check that a file contains no errors. This is significantly faster than
/// [`Self::repair()`] if performing a repair is not necessary.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> Result<()> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.verify(input, &self.fec, cancel_signal)
}
@@ -687,18 +664,13 @@ impl FecImage {
/// possible for there to be a false positive where the corrupted codeword
/// is "corrected" into an incorrect value. FEC error detection is not a
/// replacement for cryptographically secure digests.
///
/// The inputs and outputs should point to the same underlying file because
/// only regions where errors are corrected are written. It is guaranteed
/// that multiple threads will always read and write disjoint file offsets.
pub fn repair(
&self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
file: &(dyn ReadWriteAt + Sync),
cancel_signal: &AtomicBool,
) -> Result<u64> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.repair(input, output, &self.fec, cancel_signal)
fec.repair(file, &self.fec, cancel_signal)
}
/// Build one instance of the FEC header. The caller is responsible for
@@ -827,13 +799,13 @@ 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;
use rand::RngCore;
use crate::stream::SharedCursor;
use crate::stream::MutexFile;
use super::*;
@@ -862,7 +834,7 @@ mod tests {
);
}
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
fn corrupt_byte(file: &mut UserPosFile<&MutexFile<Cursor<Vec<u8>>>>, offset: u64) {
let mut buf = [0u8; 1];
file.seek(SeekFrom::Start(offset)).unwrap();
@@ -881,11 +853,13 @@ mod tests {
// Generate data big enough to span multiple rounds, but don't fill the
// offset grid to ensure that the out-of-bounds-is-0 behavior works.
let size = usize::from(rs_k) * block_size as usize * 3 - block_size as usize;
let mut file = SharedCursor::default();
let file = MutexFile::new(Cursor::new(Vec::new()));
let mut pos_file = UserPosFile::new(&file);
let orig_digest = {
let mut buf = vec![0u8; size];
rand::thread_rng().fill_bytes(&mut buf);
file.write_all(&buf).unwrap();
pos_file.write_all(&buf).unwrap();
ring::digest::digest(&ring::digest::SHA256, &buf)
};
@@ -901,7 +875,7 @@ mod tests {
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
// Verify that errors are detected.
corrupt_byte(&mut file, 0);
corrupt_byte(&mut pos_file, 0);
assert_matches!(
fec.verify(&file, &fec_data, &cancel_signal),
Err(Error::HasErrors)
@@ -909,24 +883,24 @@ mod tests {
// Corrupt one byte in every single codeword.
for offset in 1..num_codewords {
corrupt_byte(&mut file, offset as u64);
corrupt_byte(&mut pos_file, offset as u64);
}
// Verify that all the single-byte errors can be fixed. We don't test
// for Error::TooManyErrors because of the chance of false positives due
// to the nature of RS.
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
fec.repair(&file, &fec_data, &cancel_signal).unwrap();
let repaired_digest = {
let mut buf = Vec::new();
file.rewind().unwrap();
file.read_to_end(&mut buf).unwrap();
pos_file.rewind().unwrap();
pos_file.read_to_end(&mut buf).unwrap();
ring::digest::digest(&ring::digest::SHA256, &buf)
};
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
// Intentionally update some data.
corrupt_byte(&mut file, 0);
corrupt_byte(&mut pos_file, 0);
let mut fec_data_updated = fec_data.clone();
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
@@ -948,11 +922,12 @@ mod tests {
fn round_trip_image() {
let cancel_signal = Arc::new(AtomicBool::new(false));
let mut file = SharedCursor::default();
let file = MutexFile::new(Cursor::new(Vec::new()));
{
let mut buf = [0u8; FEC_BLOCK_SIZE];
rand::thread_rng().fill_bytes(&mut buf);
file.write_all(&buf).unwrap();
UserPosFile::new(&file).write_all(&buf).unwrap();
}
let image = FecImage::generate(&file, 2, &cancel_signal).unwrap();
+29 -41
View File
@@ -3,7 +3,7 @@
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
str,
sync::atomic::AtomicBool,
@@ -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::{
@@ -24,7 +24,7 @@ use crate::{
avb,
padding::{self, ZeroPadding},
},
stream::{self, FromReader, ReadFixedSizeExt, ReadSeekReopen, ToWriter},
stream::{self, FromReader, ReadAt, ReadFixedSizeExt, ToWriter, UserPosFile},
util::{self, NumBytes, OutOfBoundsError},
};
@@ -50,8 +50,8 @@ pub enum Error {
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to reopen input file")]
InputReopen(#[source] io::Error),
#[error("Failed to get input file size")]
InputSize(#[source] io::Error),
#[error("Failed to compute hash tree of input file")]
InputDigest(#[source] io::Error),
#[error("Failed to read hash tree data: {0}")]
@@ -189,7 +189,7 @@ impl HashTree {
/// Hash one full level in parallel.
fn hash_one_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
size: u64,
level_data: &mut [u8],
cancel_signal: &AtomicBool,
@@ -207,26 +207,23 @@ impl HashTree {
level_data
.par_chunks_mut(digest_size * multiplier as usize)
.enumerate()
.map(|(chunk, out_data)| -> io::Result<()> {
.try_for_each(|(chunk, out_data)| -> io::Result<()> {
let digests = out_data.len() / digest_size;
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
let mut reader = input.reopen_boxed()?;
let mut reader = UserPosFile::new(input);
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Update parts of the hash tree level corresponding to the specified
/// blocks.
fn hash_partial_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
size: u64,
block_ranges: &[Range<u64>],
level_data: &mut [u8],
@@ -239,18 +236,15 @@ impl HashTree {
.par_chunks_exact_mut(digest_size)
.enumerate()
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
.map(|(chunk, out_data)| -> io::Result<()> {
.try_for_each(|(chunk, out_data)| -> io::Result<()> {
let in_start = (chunk as u64) * u64::from(self.block_size);
let in_size = u64::from(self.block_size).min(size - in_start);
let mut reader = input.reopen_boxed()?;
let mut reader = UserPosFile::new(input);
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Compute the hash tree and return the root digest. If `ranges` is
@@ -261,7 +255,7 @@ impl HashTree {
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
fn calculate(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
image_size: u64,
ranges: Option<&[Range<u64>]>,
level_offsets: &[Range<usize>],
@@ -270,7 +264,7 @@ impl HashTree {
) -> Result<Vec<u8>> {
// Small files are hashed directly.
if image_size <= u64::from(self.block_size) {
let mut reader = input.reopen_boxed().map_err(Error::InputReopen)?;
let mut reader = UserPosFile::new(input);
let buf = reader
.read_vec_exact(image_size as usize)
.map_err(Error::InputDigest)?;
@@ -334,7 +328,7 @@ impl HashTree {
/// hash tree data.
pub fn generate(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
image_size: u64,
cancel_signal: &AtomicBool,
) -> Result<(Vec<u8>, Vec<u8>)> {
@@ -358,7 +352,7 @@ impl HashTree {
/// Returns the new root digest.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
image_size: u64,
ranges: &[Range<u64>],
hash_tree_data: &mut [u8],
@@ -387,7 +381,7 @@ impl HashTree {
/// Verify that the file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
image_size: u64,
root_digest: &[u8],
hash_tree_data: &[u8],
@@ -493,22 +487,19 @@ 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()))
}
/// Generate hash tree data for a file.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
block_size: u32,
algorithm: &str,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> Result<Self> {
let image_size = input
.reopen_boxed()
.and_then(|mut f| f.seek(SeekFrom::End(0)))
.map_err(Error::InputReopen)?;
let image_size = input.file_len().map_err(Error::InputSize)?;
let digest_algorithm = Self::digest_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, digest_algorithm, salt);
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
@@ -526,7 +517,7 @@ impl HashTreeImage {
/// Update hash tree data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -545,11 +536,7 @@ impl HashTreeImage {
}
/// Check that a file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
pub fn verify(&self, input: &(dyn ReadAt + Sync), cancel_signal: &AtomicBool) -> Result<()> {
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
@@ -656,7 +643,7 @@ mod tests {
use assert_matches::assert_matches;
use crate::stream::SharedCursor;
use crate::stream::MutexFile;
use super::*;
@@ -697,7 +684,8 @@ mod tests {
fn generate_update_verify() {
let cancel_signal = AtomicBool::new(false);
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
let mut input = SharedCursor::new();
let input = MutexFile::new(Cursor::new(Vec::new()));
let mut pos_input = UserPosFile::new(&input);
// Try input smaller than one block.
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
@@ -714,7 +702,7 @@ mod tests {
// Try larger input that spans multiple blocks are results in an actual
// hash tree being created.
input.write_all(&b"Data".repeat(25)).unwrap();
pos_input.write_all(&b"Data".repeat(25)).unwrap();
let (root_digest, mut hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
@@ -738,8 +726,8 @@ mod tests {
);
// Change some data and update the hash tree.
input.rewind().unwrap();
input.write_all(b"Changed").unwrap();
pos_input.rewind().unwrap();
pos_input.write_all(b"Changed").unwrap();
let root_digest = hash_tree
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
@@ -775,8 +763,8 @@ mod tests {
.unwrap();
// But not if the data is corrupted.
input.rewind().unwrap();
input.write_all(b"Bad").unwrap();
pos_input.rewind().unwrap();
pos_input.write_all(b"Bad").unwrap();
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
+22 -40
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`].
@@ -54,7 +54,7 @@ const PARTITION_RESERVED_BYTES: u32 = 4096;
/// Maximum allowed size of [`RawGeometry::metadata_max_size`] to prevent the
/// memory usage from blowing up.
const METADATA_MAX_SIZE: u32 = 128 * 1024;
const METADATA_MAX_SIZE: u32 = 512 * 1024;
#[derive(Debug, Error)]
pub enum Error {
@@ -118,8 +118,6 @@ pub enum Error {
},
#[error("Partition {name:?}: Extent indices too large")]
PartitionExtentIndicesTooLarge { name: DebugString },
#[error("Partition {name:?}: Extent indices set on empty image")]
PartitionExtentIndicesEmptyImage { name: DebugString },
#[error("Partition {name:?}: Extent index too large")]
PartitionExtentIndexTooLarge { name: DebugString },
#[error("Partition {name:?}: Extent count too large")]
@@ -674,7 +672,6 @@ impl RawPartition {
/// [`RawPartitionGroup::validate`] do not.
fn validate(
&self,
image_type: ImageType,
header: &RawHeader,
extents: &[RawExtent],
groups: &[RawPartitionGroup],
@@ -695,26 +692,15 @@ impl RawPartition {
});
}
match image_type {
ImageType::Normal => {
if self
.first_extent_index
.get()
.checked_add(self.num_extents.get())
.is_none_or(|n| n as usize > extents.len())
{
return Err(Error::PartitionExtentIndicesTooLarge {
name: DebugString::new(self.name),
});
}
}
ImageType::Empty => {
if self.first_extent_index.get() != 0 || self.num_extents.get() != 0 {
return Err(Error::PartitionExtentIndicesEmptyImage {
name: DebugString::new(self.name),
});
}
}
if self
.first_extent_index
.get()
.checked_add(self.num_extents.get())
.is_none_or(|n| n as usize > extents.len())
{
return Err(Error::PartitionExtentIndicesTooLarge {
name: DebugString::new(self.name),
});
}
if self.group_index.get() as usize >= groups.len() {
@@ -812,7 +798,7 @@ impl RawExtent {
return Err(Error::ExtentInvalidType {
index,
extent_type: n,
})
});
}
}
@@ -971,7 +957,7 @@ struct RawMetadataSlot {
impl RawMetadataSlot {
/// Ensure that all fields are semantically valid and can be used without
/// further checks.
fn validate(&self, image_type: ImageType, geometry: &RawGeometry) -> Result<()> {
fn validate(&self, geometry: &RawGeometry) -> Result<()> {
self.header.validate(geometry)?;
for (len, descriptor, name) in [
@@ -1032,7 +1018,7 @@ impl RawMetadataSlot {
}
for partition in &self.partitions {
partition.validate(image_type, &self.header, &self.extents, &self.groups)?;
partition.validate(&self.header, &self.extents, &self.groups)?;
}
for (i, group) in self.groups.iter().enumerate() {
@@ -1111,11 +1097,7 @@ impl RawMetadata {
/// Read a single [`RawMetadataSlot`], including the header and all tables.
/// This does not read the extra padding between the end of the last table
/// and [`RawGeometry::metadata_max_size`].
fn read_metadata(
mut reader: impl Read,
image_type: ImageType,
geometry: &RawGeometry,
) -> Result<RawMetadataSlot> {
fn read_metadata(mut reader: impl Read, geometry: &RawGeometry) -> Result<RawMetadataSlot> {
let mut header = RawHeader::new_zeroed();
reader
@@ -1155,7 +1137,7 @@ impl RawMetadata {
block_devices: block_devices.to_vec(),
};
slot.validate(image_type, geometry)?;
slot.validate(geometry)?;
Ok(slot)
}
@@ -1194,7 +1176,7 @@ impl RawMetadata {
}
}
slot.validate(self.image_type, &self.geometry)?;
slot.validate(&self.geometry)?;
}
Ok(())
@@ -1229,7 +1211,7 @@ impl<R: Read> FromReader<R> for RawMetadata {
.stream_position()
.map_err(|e| Error::DataRead("orig_offset", e))?;
match Self::read_metadata(&mut reader, image_type, &geometry) {
match Self::read_metadata(&mut reader, &geometry) {
Ok(m) => *slot = Some(m),
Err(e @ Error::DataRead(_, _)) => return Err(e),
Err(e) => last_err = Some(e),
@@ -1640,7 +1622,7 @@ impl TryFrom<&RawMetadataSlot> for MetadataSlot {
type Error = Error;
fn try_from(raw_slot: &RawMetadataSlot) -> Result<Self> {
let mut slot = MetadataSlot {
let mut slot = Self {
major_version: raw_slot.header.major_version.get(),
minor_version: raw_slot.header.minor_version.get(),
groups: Vec::with_capacity(raw_slot.groups.len()),
@@ -1720,7 +1702,7 @@ impl TryFrom<&MetadataSlot> for RawMetadataSlot {
fn try_from(slot: &MetadataSlot) -> Result<Self> {
let header_size = RawHeader::size_for_version(slot.major_version, slot.minor_version);
let mut raw_slot = RawMetadataSlot {
let mut raw_slot = Self {
header: RawHeader {
magic: HEADER_MAGIC.into(),
major_version: slot.major_version.into(),
@@ -1937,7 +1919,7 @@ impl TryFrom<&Metadata> for RawMetadata {
// We only do the bare minimum calculations needed here to fill out the
// raw fields. There is no semantic validation.
let mut raw_metadata = RawMetadata {
let mut raw_metadata = Self {
image_type: metadata.image_type,
geometry: RawGeometry {
magic: GEOMETRY_MAGIC.into(),
+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;
+312 -167
View File
@@ -2,29 +2,36 @@
// SPDX-License-Identifier: GPL-3.0-only
use std::{
borrow::Cow,
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 rawzip::{CompressionMethod, RECOMMENDED_BUFFER_SIZE, ZipArchive, ZipArchiveWriter};
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 crate::{
crypto::{self, RsaPublicKeyExt, RsaSigningKey, SignatureAlgorithm},
format::payload::{self, PayloadHeader},
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
format::{
payload::{self, PayloadHeader},
zip::{self, ZipEntriesSafeExt, ZipFileHeaderRecordExt},
},
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 +77,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:?}")]
@@ -81,17 +92,21 @@ pub enum Error {
#[error("Failed to decode OTA metadata protobuf message")]
MetadataDecode(#[source] prost::DecodeError),
#[error("Failed to open zip file")]
ZipOpen(#[source] ZipError),
ZipOpen(#[source] rawzip::Error),
#[error("Failed to list zip entries")]
ZipEntryList(#[source] rawzip::Error),
#[error("Missing zip entry: {0:?}")]
ZipEntryMissing(Cow<'static, str>),
#[error("Failed to open zip entry: {0:?}")]
ZipEntryOpen(&'static str, #[source] ZipError),
ZipEntryOpen(Cow<'static, str>, #[source] rawzip::Error),
#[error("Failed to start new zip entry: {0:?}")]
ZipEntryStart(&'static str, #[source] ZipError),
ZipEntryStart(Cow<'static, str>, #[source] rawzip::Error),
#[error("Failed to read zip entry: {0:?}")]
ZipEntryRead(&'static str, #[source] io::Error),
ZipEntryRead(Cow<'static, str>, #[source] io::Error),
#[error("Failed to write zip entry: {0:?}")]
ZipEntryWrite(&'static str, #[source] io::Error),
#[error("Failed to open zip entry #{0}")]
ZipIndexOpen(usize, #[source] ZipError),
ZipEntryWrite(Cow<'static, str>, #[source] io::Error),
#[error("Failed to finalize zip entry: {0:?}")]
ZipEntryFinish(Cow<'static, str>, #[source] rawzip::Error),
#[error("Failed to load OTA certificate")]
OtaCertLoad(#[source] crypto::Error),
#[error("Failed to extract public key from OTA certificate")]
@@ -110,6 +125,8 @@ pub enum Error {
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write OTA data: {0}")]
DataWrite(&'static str, #[source] io::Error),
#[error("Failed to convert to non-streaming zip")]
MakeNonStreaming(#[source] rawzip::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -280,16 +297,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 +358,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 +423,51 @@ fn compute_property_files(
"care_map.txt",
"compatibility.zip",
] {
if let Ok(token) = compute(path) {
tokens.push(token);
}
// These are optional.
let _ = append(path);
}
if max_length.is_none() {
tokens.push(format!("metadata:{}", " ".repeat(15)));
buf.push_str(property_file_name(PATH_METADATA));
buf.push(':');
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
buf.push(',');
if want_pb {
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
buf.push_str(property_file_name(PATH_METADATA_PB));
buf.push(':');
buf.extend(iter::repeat_n(' ', RESERVATION_SIZE));
buf.push(',');
}
} else {
tokens.push(compute(PATH_METADATA)?);
append(PATH_METADATA)?;
if want_pb {
tokens.push(compute(PATH_METADATA_PB)?);
append(PATH_METADATA_PB)?;
}
}
let mut joined = tokens.join(",");
// Strip final trailing comma.
buf.pop();
if let Some(l) = max_length {
if joined.len() > l {
if buf.len() > l {
return Err(Error::InsufficientReservedSpace {
value: joined,
value: buf,
reserved: l,
});
}
let remain = l - joined.len();
joined.extend(iter::repeat(' ').take(remain));
let remain = l - buf.len();
buf.extend(iter::repeat_n(' ', remain));
}
Ok(joined)
Ok(buf)
}
// Add fake payload_metadata.bin entry, covering the header + header signature
// regions of the payload.
fn add_payload_metadata_entry(
entries: &mut Vec<ZipEntry>,
entries: &mut Vec<PropEntry>,
payload_metadata_size: u64,
) -> Result<()> {
let payload_offset = entries
@@ -394,11 +475,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 +507,39 @@ impl fmt::Display for ZipMode {
/// directory would start.
pub fn add_metadata(
zip_entries: &[ZipEntry],
zip_writer: &mut ZipWriter<impl Write>,
zip_writer: &mut ZipArchiveWriter<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);
fn write_entry(
archive: &mut ZipArchiveWriter<impl Write>,
path: &'static str,
data: &[u8],
) -> Result<(u64, u64)> {
let (entry_writer, data_config) = archive
.new_file(path)
.start()
.map_err(|e| Error::ZipEntryStart(path.into(), e))?;
let data_offset = entry_writer.stream_offset();
let mut data_writer = data_config.wrap(entry_writer);
let mut zip_entries = zip_entries.to_owned();
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
data_writer
.write_all(data)
.map_err(|e| Error::ZipEntryWrite(path.into(), e))?;
let data_size = data_writer
.finish()
.and_then(|(w, d)| w.finish(d))
.map_err(|e| Error::ZipEntryFinish(path.into(), e))?;
Ok((data_offset, data_size))
}
let mut metadata = metadata.clone();
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 +547,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)?,
);
}
@@ -452,73 +555,41 @@ pub fn add_metadata(
let (temp_legacy_offset, temp_modern_offset) = {
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),
};
// Note that we don't need to worry about the offsets changing based on
// the zip writing mode (streaming vs. seekable). Currently, we always
// include data descriptors and do post-processing to copy the fields
// into the local header without shifting the data.
let mut writer = ZipArchiveWriter::new(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()
.map_err(|e| Error::ZipEntryStart(PATH_METADATA, e))?;
writer
.write_all(legacy_raw.as_bytes())
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA, e))?;
let (legacy_offset, legacy_size) =
write_entry(&mut writer, PATH_METADATA, legacy_raw.as_bytes())?;
let (modern_offset, modern_size) = write_entry(&mut writer, PATH_METADATA_PB, &modern_raw)?;
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()
.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_size,
));
prop_entries.push(PropEntry::new(
PATH_METADATA_PB,
next_offset + modern_offset,
modern_size,
));
(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()
.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()
.map_err(|e| Error::ZipEntryStart(PATH_METADATA_PB, e))?;
zip_writer
.write_all(&modern_raw)
.map_err(|e| Error::ZipEntryWrite(PATH_METADATA_PB, e))?;
let (legacy_offset, _) = write_entry(zip_writer, PATH_METADATA, legacy_raw.as_bytes())?;
let (modern_offset, _) = write_entry(zip_writer, PATH_METADATA_PB, &modern_raw)?;
assert_eq!(legacy_offset, temp_legacy_offset);
assert_eq!(modern_offset, temp_modern_offset);
@@ -533,31 +604,81 @@ pub fn verify_metadata(
metadata: &OtaMetadata,
payload_metadata_size: u64,
) -> Result<()> {
let mut zip_reader = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let archive = ZipArchive::from_seekable(reader, &mut buffer).map_err(Error::ZipOpen)?;
let mut zip_entries = vec![];
for i in 0..zip_reader.len() {
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(),
});
let mut entries = archive.entries_safe(&mut buffer);
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
if cd_entry.compression_method() != CompressionMethod::Store {
continue;
}
let Ok(path) = cd_entry.file_path_utf8() else {
continue;
};
let range = entry.compressed_data_range();
zip_entries.push(PropEntry::new(path, range.0, range.1 - range.0));
}
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()), ","),
});
}
}
@@ -695,7 +816,7 @@ fn parse_raw_ota_sig(mut reader: impl Read + Seek) -> Result<RawOtaSignature> {
.map_err(|e| Error::DataRead("file_size", e))?;
reader
.seek(SeekFrom::Current(-6))
.seek_relative(-6)
.map_err(|e| Error::DataRead("footer", e))?;
let footer = reader
.read_array_exact::<6>()
@@ -757,54 +878,73 @@ pub fn parse_ota_sig(reader: impl Read + Seek) -> Result<OtaSignature> {
pub fn parse_zip_ota_info(
reader: impl Read + Seek,
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
let mut zip = ZipArchive::new(reader).map_err(Error::ZipOpen)?;
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let archive = ZipArchive::from_seekable(reader, &mut buffer).map_err(Error::ZipOpen)?;
let metadata = match zip.by_name(PATH_METADATA_PB) {
Ok(mut entry) => {
let mut metadata_modern = None;
let mut metadata_legacy = None;
let mut certificate = None;
let mut header = None;
let mut properties = None;
let mut entries = archive.entries_safe(&mut buffer);
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
if path == PATH_METADATA_PB {
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA_PB.into(), e))?;
let mut buf = Vec::new();
entry
reader
.read_to_end(&mut buf)
.map_err(|e| Error::ZipEntryRead(PATH_METADATA_PB, e))?;
parse_protobuf_metadata(&buf)?
}
e @ Err(ZipError::FileNotFound) => {
drop(e);
let mut entry = zip
.by_name(PATH_METADATA)
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA, e))?;
.map_err(|e| Error::ZipEntryRead(PATH_METADATA_PB.into(), e))?;
metadata_modern = Some(parse_protobuf_metadata(&buf)?);
} else if path == PATH_METADATA {
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(PATH_METADATA.into(), e))?;
let mut buf = String::new();
entry
reader
.read_to_string(&mut buf)
.map_err(|e| Error::ZipEntryRead(PATH_METADATA, e))?;
parse_legacy_metadata(&buf)?
.map_err(|e| Error::ZipEntryRead(PATH_METADATA.into(), e))?;
metadata_legacy = Some(parse_legacy_metadata(&buf)?);
} else if path == PATH_OTACERT {
let reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(PATH_OTACERT.into(), e))?;
certificate = Some(
crypto::read_pem_cert(Path::new(PATH_OTACERT), reader)
.map_err(Error::OtaCertLoad)?,
);
} else if path == PATH_PAYLOAD {
// No CRC validation because we only read the header.
let reader = zip::compressed_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(PATH_PAYLOAD.into(), e))?;
header = Some(PayloadHeader::from_reader(reader).map_err(Error::PayloadLoad)?);
} else if path == PATH_PROPERTIES {
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES.into(), e))?;
let mut buf = String::new();
reader
.read_to_string(&mut buf)
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES.into(), e))?;
properties = Some(buf);
}
Err(e) => return Err(Error::ZipEntryOpen(PATH_METADATA_PB, e)),
};
}
let certificate = {
let entry = zip
.by_name(PATH_OTACERT)
.map_err(|e| Error::ZipEntryOpen(PATH_OTACERT, e))?;
crypto::read_pem_cert(Path::new(PATH_OTACERT), entry).map_err(Error::OtaCertLoad)?
};
let header = {
let entry = zip
.by_name(PATH_PAYLOAD)
.map_err(|e| Error::ZipEntryOpen(PATH_PAYLOAD, e))?;
PayloadHeader::from_reader(entry).map_err(Error::PayloadLoad)?
};
let properties = {
let mut entry = zip
.by_name(PATH_PROPERTIES)
.map_err(|e| Error::ZipEntryOpen(PATH_PROPERTIES, e))?;
let mut buf = String::new();
entry
.read_to_string(&mut buf)
.map_err(|e| Error::ZipEntryRead(PATH_PROPERTIES, e))?;
buf
};
let metadata = metadata_modern
.or(metadata_legacy)
.ok_or_else(|| Error::ZipEntryMissing(PATH_METADATA_PB.into()))?;
let certificate = certificate.ok_or_else(|| Error::ZipEntryMissing(PATH_OTACERT.into()))?;
let header = header.ok_or_else(|| Error::ZipEntryMissing(PATH_PAYLOAD.into()))?;
let properties = properties.ok_or_else(|| Error::ZipEntryMissing(PATH_PROPERTIES.into()))?;
Ok((metadata, certificate, header, properties))
}
@@ -964,6 +1104,11 @@ impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
cert: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<W> {
// We always write a streaming zip because that is what rawzip supports.
// Convert it to not be streaming. This will leave the data descriptors
// behind, but that is fine.
zip::make_non_streaming(&mut self.inner).map_err(Error::MakeNonStreaming)?;
let file_size = self
.seek(SeekFrom::End(0))
.map_err(|e| Error::DataRead("file_size", e))?;
+462 -196
View File
@@ -1,24 +1,22 @@
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
// SPDX-FileCopyrightText: 2022-2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{HashMap, HashSet},
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
num::NonZeroU32,
ops::{Add, Range},
str::FromStr,
sync::atomic::AtomicBool,
};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use bzip2::write::BzDecoder;
use clap::ValueEnum;
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::{
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
@@ -28,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, ReadAt, ReadDiscardExt,
ReadFixedSizeExt, ReadSeek, UserPosFile, WriteAt, WriteSeek,
},
util::{self, OutOfBoundsError},
};
@@ -47,9 +45,11 @@ use crate::{
const PAYLOAD_MAGIC: &[u8; 4] = b"CrAU";
const PAYLOAD_VERSION: u64 = 2;
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
const MANIFEST_MAX_SIZE: usize = 16 * 1024 * 1024;
/// Size of each extent. This matches what AOSP's delta_generator does.
/// Size of each extent. This matches what AOSP's delta_generator does. We also
/// require this to be a multiple of the block size and a multiple of the
/// maximum CoW compression chunk size.
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
#[derive(Debug, Error)]
@@ -81,6 +81,10 @@ pub enum Error {
expected: Option<String>,
actual: String,
},
#[error("Invalid block size: {0}")]
InvalidBlockSize(u32),
#[error("Invalid maximum CoW compression chunk size: {0}")]
InvalidMaxCompressionChunkSize(u32),
#[error("Size of {name} ({size}) is not aligned to the block size ({block_size})")]
InvalidPartitionSize {
name: String,
@@ -113,23 +117,21 @@ 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,
num_blocks: u64,
source: io::Error,
},
#[error("Failed to reopen payload")]
PayloadReopen(#[source] io::Error),
#[error("Failed to open input file for partition: {0}")]
InputOpen(String, #[source] io::Error),
#[error("Failed to open output file for partition: {0}")]
OutputOpen(String, #[source] io::Error),
#[error("Failed to get input file size for partition: {0}")]
InputSize(String, #[source] io::Error),
#[error("Failed to GZ compress partition image chunk")]
GzCompress(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] liblzma::stream::Error),
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")]
@@ -434,11 +436,11 @@ impl<W: Write> PayloadWriter<W> {
/// Finalize the payload. If this function is not called, the payload will
/// be left in an incomplete state. Returns the original writer, the final
/// header, the contents that should be written for `payload_properties.txt`
/// and the length of the header + manifest + manifest signature sections
/// (for constructing the `payload_metadata.bin` OTA metadata property files
/// entry).
pub fn finish(mut self) -> Result<(W, PayloadHeader, String, u64)> {
/// header and the data that should be written for `payload_properties.txt`.
/// [`PayloadHeader::blob_offset`] indicates the length of the header +
/// manifest + manifest signature sections (for constructing the
/// `payload_metadata.bin` OTA metadata property files entry).
pub fn finish(mut self) -> Result<(W, PayloadHeader, String)> {
// Append payload signature.
let payload_partial_hash = self.h_partial.clone().finish();
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
@@ -463,7 +465,9 @@ impl<W: Write> PayloadWriter<W> {
self.metadata_size as u64,
);
Ok((self.inner, self.header, properties, metadata_with_sig_size))
self.header.blob_offset = metadata_with_sig_size;
Ok((self.inner, self.header, properties))
}
/// Prepare for writing the next source data blob corresponding to an
@@ -586,12 +590,12 @@ impl<W: Write> Write for PayloadWriter<W> {
/// Verify the payload signatures using the specified certificate and check that
/// the digests in `payload_properties.txt` are correct.
pub fn verify_payload(
mut reader: impl Read + Seek,
reader: &mut dyn ReadSeek,
cert: &Certificate,
properties_raw: &str,
cancel_signal: &AtomicBool,
) -> Result<()> {
let header = PayloadHeader::from_reader(&mut reader)?;
let header = PayloadHeader::from_reader(&mut *reader)?;
reader.rewind().map_err(|e| Error::DataRead("header", e))?;
let payload_signatures_offset = header
@@ -611,7 +615,7 @@ pub fn verify_payload(
// Read from the beginning to the metadata signature.
let metadata_size = header.blob_offset - u64::from(header.metadata_signature_size);
stream::copy_n_inspect(
&mut reader,
&mut *reader,
io::sink(),
metadata_size,
|data| {
@@ -628,7 +632,7 @@ pub fn verify_payload(
let mut writer = Cursor::new(Vec::new());
stream::copy_n_inspect(
&mut reader,
&mut *reader,
&mut writer,
header.metadata_signature_size.into(),
|data| h_full.update(data),
@@ -658,7 +662,7 @@ pub fn verify_payload(
// Read (and discard) all the payload blobs.
stream::copy_n_inspect(
&mut reader,
&mut *reader,
io::sink(),
payload_signatures_offset,
|data| {
@@ -686,7 +690,7 @@ pub fn verify_payload(
let mut writer = Cursor::new(Vec::new());
stream::copy_n_inspect(
&mut reader,
&mut *reader,
&mut writer,
payload_signatures_size,
|data| h_full.update(data),
@@ -731,8 +735,8 @@ pub fn verify_payload(
/// Apply a partition operation from `reader` to `writer`.
pub fn apply_operation(
mut reader: impl Read + Seek,
mut writer: impl Write + Seek,
reader: &mut dyn ReadSeek,
writer: &mut dyn WriteSeek,
block_size: u32,
blob_offset: u64,
op: &InstallOperation,
@@ -768,7 +772,7 @@ pub fn apply_operation(
Type::Zero | Type::Discard => {
stream::copy_n_inspect(
io::repeat(0),
&mut writer,
&mut *writer,
out_data_length,
|data| hasher.update(data),
cancel_signal,
@@ -787,8 +791,8 @@ pub fn apply_operation(
match other {
Type::Replace => {
stream::copy_n_inspect(
&mut reader,
&mut writer,
&mut *reader,
&mut *writer,
data_length,
|data| hasher.update(data),
cancel_signal,
@@ -796,9 +800,9 @@ pub fn apply_operation(
.map_err(error_fn)?;
}
Type::ReplaceBz => {
let mut decoder = BzDecoder::new(&mut writer);
let mut decoder = BzDecoder::new(&mut *writer);
stream::copy_n_inspect(
&mut reader,
&mut *reader,
&mut decoder,
data_length,
|data| hasher.update(data),
@@ -808,16 +812,15 @@ 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())),
}
@@ -839,11 +842,10 @@ pub fn apply_operation(
}
/// Extract the specified image from the payload. This is done multithreaded and
/// uses rayon's global thread pool. Both the `payload` and `output` streams
/// will be reopened from multiple threads.
/// uses rayon's global thread pool.
pub fn extract_image(
payload: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
payload: &(dyn ReadAt + Sync),
output: &(dyn WriteAt + Sync),
header: &PayloadHeader,
partition_name: &str,
cancel_signal: &AtomicBool,
@@ -855,40 +857,28 @@ pub fn extract_image(
.find(|p| p.partition_name == partition_name)
.ok_or_else(|| Error::MissingPartition(partition_name.to_owned()))?;
partition
.operations
.par_iter()
.map(|op| -> Result<()> {
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
let writer = output
.reopen_boxed()
.map_err(|e| Error::OutputOpen(partition_name.to_owned(), e))?;
apply_operation(
reader,
writer,
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
)?;
Ok(())
})
.collect::<Result<_>>()
partition.operations.par_iter().try_for_each(|op| {
apply_operation(
&mut UserPosFile::new(payload),
&mut UserPosFile::new(output),
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
)
})
}
/// Extract the specified partition images from the payload into writers. This
/// is done multithreaded and uses rayon's global thread pool. `open_payload`
/// and `open_output` will be called from multiple threads.
pub fn extract_images<'a>(
payload: &(dyn ReadSeekReopen + Sync),
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
/// is done multithreaded and uses rayon's global thread pool.
pub fn extract_images<'name, 'file>(
payload: &(dyn ReadAt + Sync),
outputs: impl IntoIterator<Item = (&'name str, &'file (dyn WriteAt + Sync))>,
header: &PayloadHeader,
partition_names: impl IntoIterator<Item = &'a str>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let mut remaining = partition_names.into_iter().collect::<HashSet<_>>();
let outputs = outputs.into_iter().collect::<HashMap<_, _>>();
let mut remaining = outputs.keys().copied().collect::<HashSet<_>>();
// We parallelize at the operation level or else one thread might get stuck
// processing a giant image.
let mut operations = vec![];
@@ -908,24 +898,23 @@ pub fn extract_images<'a>(
operations
.into_par_iter()
.map(|(name, op)| -> Result<()> {
let reader = payload.reopen_boxed().map_err(Error::PayloadReopen)?;
let writer = open_output(name).map_err(|e| Error::OutputOpen(name.to_owned(), e))?;
.try_for_each(|(name, op)| -> Result<()> {
let mut reader = UserPosFile::new(payload);
let mut writer = UserPosFile::new(&outputs[name]);
apply_operation(
reader,
writer,
&mut reader,
&mut writer,
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
)?;
Ok(())
)
})
.collect()
}
/// Compress raw data into a chunk to be used with a [`Type::ReplaceXz`]
/// [`InstallOperation`].
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
let reader = Cursor::new(raw_data);
let writer = Cursor::new(Vec::new());
@@ -935,8 +924,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)?;
@@ -949,87 +939,310 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
Ok((data, digest_compressed))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ValueEnum)]
pub enum VabcAlgo {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum CowVersion {
V2,
V3 {
/// The maximum number of bytes to compress at a time.
compression_factor: u32,
},
}
impl CowVersion {
fn compression_factor(self) -> Option<u32> {
match self {
Self::V2 => None,
Self::V3 { compression_factor } => Some(compression_factor),
}
}
/// Compute the size overhead required to store the headers and footers
/// needed for this version of the on-disk CoW format.
fn size_overhead(self, cow_replace_ops: u64, payload_install_ops: u64) -> u64 {
const BUFFER_REGION_DEFAULT_SIZE: u64 = 2 * 1024 * 1024;
const CLUSTER_OPS: u64 = 200;
const NUM_RESUME_POINTS: u64 = 4;
const SIZEOF_COW_FOOTER_V2: u64 = 84;
const SIZEOF_COW_HEADER_V2: u64 = 38;
const SIZEOF_COW_HEADER_V3: u64 = SIZEOF_COW_HEADER_V2 + 40;
const SIZEOF_COW_OPERATION_V2: u64 = 20;
const SIZEOF_COW_OPERATION_V3: u64 = 16;
const SIZEOF_RESUME_POINT_V3: u64 = 16;
let mut overhead = 0;
match self {
Self::V2 => {
// sizeof(CowHeader).
// AOSP: CowWriterV2::InitPos()
overhead += SIZEOF_COW_HEADER_V2;
// header_.buffer_size. update_engine uses the default value.
// AOSP: CowWriterV2::InitPos()
overhead += BUFFER_REGION_DEFAULT_SIZE;
// Add all the CoW operation headers:
//
// - There is a kCowReplaceOp for each compressed chunk.
// - There is a kCowLabelOp for each InstallOperation in the
// payload. This is added by delta_generator in CowDryRun().
// - There is a kCowClusterOp at the end of each cluster of
// operations (which includes the kCowClusterOp itself). The
// cluster size used to be 200, but was changed to 1024 in
// 5e8e488c13cbff9e0a305ce7c22fd6a13aabb886. We'll use the
// smaller value because it's better to overestimate.
//
// AOSP: CowWriterV2::EmitClusterIfNeeded()
let cow_label_ops = payload_install_ops;
let cow_cluster_ops = (cow_replace_ops + cow_label_ops).div_ceil(CLUSTER_OPS - 1);
// A cluster cannot be truncated, so round up to the nearest
// cluster boundary.
// AOSP: CowWriterV2::AddOperation()
overhead += cow_cluster_ops * CLUSTER_OPS * SIZEOF_COW_OPERATION_V2;
// sizeof(CowFooter).
// AOSP: CowWriterV2::GetCowSizeInfo()
overhead += SIZEOF_COW_FOOTER_V2;
}
Self::V3 { .. } => {
// AOSP: CowWriterV3::OpenForWrite() -> GetDataOffset()
overhead += SIZEOF_COW_HEADER_V3;
overhead += BUFFER_REGION_DEFAULT_SIZE;
overhead += NUM_RESUME_POINTS * SIZEOF_RESUME_POINT_V3;
// Add an operation header (sizeof(CowOperationV3)) for each
// chunk of compressed data.
// AOSP: CowWriterV3::WriteOperation()
overhead += cow_replace_ops * SIZEOF_COW_OPERATION_V3;
}
}
overhead
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ChunkingMethod {
/// Compress data in block sized chunks each iteration.
Exact,
/// Compress data in chunks where each chunk is sized at the largest power
/// of 2 that's `<=` the specified size and the remaining input size.
MaxPowerOf2(NonZeroU32),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ChunkingParams {
block_size: u32,
method: ChunkingMethod,
}
impl ChunkingParams {
fn chunk_size(self, num_blocks: u64) -> u32 {
match self.method {
ChunkingMethod::Exact => self.block_size,
ChunkingMethod::MaxPowerOf2(max_chunk_size) => {
assert!(
max_chunk_size.is_power_of_two() && max_chunk_size.get() % self.block_size == 0
);
let mut chunk_size = max_chunk_size.get();
while chunk_size > self.block_size {
let min_blocks = chunk_size / self.block_size;
if num_blocks >= u64::from(min_blocks) {
return chunk_size;
}
chunk_size >>= 1;
}
self.block_size
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CowEstimate {
/// Size of estimate in bytes.
pub size: u64,
/// Number of CoW operations (v3 only).
pub num_ops: u64,
}
impl CowEstimate {
/// Add fudge factor to account for overhead.
fn fudged(&self, payload_install_ops: u64, cow_version: CowVersion) -> Option<Self> {
let version_overhead = cow_version.size_overhead(self.num_ops, payload_install_ops);
let mut size = self.size.checked_add(version_overhead)?;
// delta_generator adds 1% overhead to the original CoW size estimate,
// even if compression is disabled. We'll do the same too. For the
// compressed scenario, we rely on this more because lz4_flex and
// zlib-rs usually compress better than the lz4 and zlib implementations
// used by libsnapshot_cow.
size += size / 100;
// AOSP: PartitionProcessor::Run()
let num_ops = self.num_ops.max(25);
Some(Self { size, num_ops })
}
}
impl Add for CowEstimate {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
size: self.size + rhs.size,
num_ops: self.num_ops + rhs.num_ops,
}
}
}
impl CheckedAdd for CowEstimate {
fn checked_add(&self, rhs: &Self) -> Option<Self> {
Some(Self {
size: self.size.checked_add(rhs.size)?,
num_ops: self.num_ops.checked_add(rhs.num_ops)?,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum VabcAlgoKind {
None,
Lz4,
Gz,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VabcAlgo {
/// Compression algorithm.
pub kind: VabcAlgoKind,
/// Compression level. AOSP allows this to be present even if the algorithm
/// can't use it.
pub level: Option<u32>,
}
impl VabcAlgo {
fn compressed_size(self, mut raw_data: &[u8], block_size: u32) -> Result<u64> {
let mut total = 0;
/// Compute the compressed size of the raw data when split into chunks based
/// on the specified [`ChunkingParams`]. The length of `raw_data` must be a
/// multiple of the block size or else this will panic. The compressed data
/// for each chunk is temporarily stored in memory, but discarded after each
/// loop iteration.
fn compressed_size(self, mut raw_data: &[u8], chunking: ChunkingParams) -> Result<CowEstimate> {
assert!(raw_data.len() as u64 % u64::from(chunking.block_size) == 0);
let mut size = 0;
let mut num_ops = 0;
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let (chunk, remaining) = raw_data.split_at(n);
let num_blocks = raw_data.len() as u64 / u64::from(chunking.block_size);
let chunk_size = chunking.chunk_size(num_blocks) as usize;
let (chunk, remaining) = raw_data.split_at(chunk_size);
// This should match CompressWorker::GetDefaultCompressionLevel() in
// AOSP's libsnapshot.
let compressed = match self {
Self::Lz4 => lz4_flex::block::compress(chunk),
Self::Gz => {
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
//
// CoW v3 uses the raw data instead of the compressed data if the
// raw data is smaller. Because we use a different implementation of
// the compression algorithms, we don't implement this. It's safer
// to just overestimate and use the (larger) compressed size.
size += match self.kind {
VabcAlgoKind::None => chunk_size as u64,
VabcAlgoKind::Lz4 => lz4_flex::block::compress(chunk).len() as u64,
VabcAlgoKind::Gz => {
let level = self.level.map_or(Compression::best(), Compression::new);
let mut encoder = GzEncoder::new(Vec::new(), level);
encoder.write_all(chunk).map_err(Error::GzCompress)?;
encoder.finish().map_err(Error::GzCompress)?
encoder.finish().map_err(Error::GzCompress)?.len() as u64
}
};
total += compressed.len().min(n) as u64;
num_ops += 1;
raw_data = remaining;
}
Ok(total)
Ok(CowEstimate { size, num_ops })
}
}
impl fmt::Display for VabcAlgo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
let name = match self.kind {
VabcAlgoKind::None => "none",
VabcAlgoKind::Lz4 => "lz4",
VabcAlgoKind::Gz => "gz",
};
f.write_str(name)?;
if let Some(level) = self.level {
write!(f, ",{level}")?;
}
Ok(())
}
}
/// Add fudge factor to CoW estimate to account for overhead.
fn fudge_cow_estimate(mut estimate: u64) -> Option<u64> {
// lz4_flex and zlib-rs usually compress better than the lz4 and zlib
// implementations used by libsnapshot_cow. Make up for this by adding
// percentage-based overhead.
estimate = estimate.checked_add(estimate / 100)?;
#[derive(Clone, Debug, Error)]
#[error("Invalid VABC algorithm: {0:?} (must be {{none|lz4|gz}}[,<level>])")]
pub struct InvalidVabcAlgo(String);
// We also need to account for constant overhead, especially with smaller
// partitions. We can match what delta_generator normally adds in
// CowWriterV2::InitPos() exactly. Since we only ever create full OTAs, we
// can assume that all CoW operations are kCowReplaceOp.
impl FromStr for VabcAlgo {
type Err = InvalidVabcAlgo;
// sizeof(CowHeader).
estimate = estimate.checked_add(38)?;
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
estimate = estimate.checked_add(2 * 1024 * 1024)?;
// CowOptions::cluster_ops * sizeof(CowOperationV2).
estimate = estimate.checked_add(200 * 20)?;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let (prefix, suffix) = s.split_once(',').unwrap_or((s, ""));
Some(estimate)
// AOSP allows any algorithm to accept a level, even if it's unused.
let level = if !suffix.is_empty() {
Some(suffix.parse().map_err(|_| InvalidVabcAlgo(s.to_owned()))?)
} else {
None
};
let kind = match prefix {
"" | "none" => VabcAlgoKind::None,
"lz4" => VabcAlgoKind::Lz4,
"gz" => VabcAlgoKind::Gz,
_ => return Err(InvalidVabcAlgo(s.to_owned())),
};
Ok(Self { kind, level })
}
}
/// Compute the VABC CoW v2 size estimate. The caller must update
/// [`PartitionUpdate::estimate_cow_size`] with this value or else update_engine
/// may fail to flash the partition due to running out of space on the CoW block
/// device. CoW v2 + other algorithms and also CoW v3 are currently unsupported
/// because there currently are no known OTAs that use those configurations.
pub fn compute_cow_estimate(
input: &(dyn ReadSeekReopen + Sync),
partition_name: &str,
block_size: u32,
vabc_algo: VabcAlgo,
cancel_signal: &AtomicBool,
) -> Result<u64> {
let file_size = input
.reopen_boxed()
.and_then(|mut r| r.seek(SeekFrom::End(0)))
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
let final_chunk_different = file_size % CHUNK_SIZE != 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VabcParams {
/// CoW on-disk format version.
pub version: CowVersion,
/// CoW compression algorithm.
pub algo: VabcAlgo,
}
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
/// Ensure that the partition size is aligned to the block size and that the
/// block size and compression factor are factors of our [`CHUNK_SIZE`].
fn validate_partition_size(
partition_name: &str,
file_size: u64,
block_size: u32,
compression_factor: Option<u32>,
) -> Result<()> {
if block_size == 0 || !block_size.is_power_of_two() || CHUNK_SIZE % u64::from(block_size) != 0 {
return Err(Error::InvalidBlockSize(block_size));
}
if let Some(factor) = compression_factor
&& (factor == 0 || !factor.is_power_of_two() || CHUNK_SIZE % u64::from(factor) != 0)
{
return Err(Error::InvalidMaxCompressionChunkSize(factor));
}
if file_size % u64::from(block_size) != 0 {
return Err(Error::InvalidPartitionSize {
name: partition_name.to_owned(),
size: file_size,
@@ -1037,13 +1250,53 @@ pub fn compute_cow_estimate(
});
}
Ok(())
}
/// Compute the VABC CoW size estimate. For a more accurate size estimate with
/// CoW version 2, `payload_install_ops` must be equal to the number of
/// [`InstallOperation`]s in the payload. The caller must update
/// [`PartitionUpdate::estimate_cow_size`] and
/// [`PartitionUpdate::estimate_op_count_max`] or else update_engine may fail to
/// flash the partition due to running out of space on the CoW block device.
pub fn compute_cow_estimate(
input: &(dyn ReadAt + Sync),
payload_install_ops: u64,
partition_name: &str,
block_size: u32,
vabc_params: VabcParams,
cancel_signal: &AtomicBool,
) -> Result<CowEstimate> {
let file_size = input
.file_len()
.map_err(|e| Error::InputSize(partition_name.to_owned(), e))?;
let final_chunk_different = file_size % CHUNK_SIZE != 0;
validate_partition_size(
partition_name,
file_size,
block_size,
vabc_params.version.compression_factor(),
)?;
let chunking = ChunkingParams {
block_size,
method: match vabc_params.version {
CowVersion::V2 => ChunkingMethod::Exact,
CowVersion::V3 { compression_factor } => {
// validate_partition_size() already validated that it is not 0.
ChunkingMethod::MaxPowerOf2(compression_factor.try_into().unwrap())
}
},
};
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
let cow_estimate = (0..chunks_total)
let initial_estimate = (0..chunks_total)
.into_par_iter()
.map(|chunk| -> Result<u64> {
.map(|chunk| -> Result<CowEstimate> {
let data = (|| {
let mut reader = input.reopen_boxed()?;
let mut reader = UserPosFile::new(input);
reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
@@ -1057,26 +1310,25 @@ pub fn compute_cow_estimate(
})()
.map_err(Error::ChunkRead)?;
vabc_algo.compressed_size(&data, block_size)
vabc_params.algo.compressed_size(&data, chunking)
})
.try_fold(
|| 0u64,
|total, chunk_estimate| -> Result<u64> {
CowEstimate::default,
|total, chunk_estimate| -> Result<CowEstimate> {
total
.checked_add(chunk_estimate?)
.ok_or(Error::IntOverflow("cow_estimate"))
.checked_add(&chunk_estimate?)
.ok_or(Error::IntOverflow("initial_estimate"))
},
)
.try_reduce(
|| 0u64,
|total, partial| {
total
.checked_add(partial)
.ok_or(Error::IntOverflow("cow_estimate"))
},
)?;
.try_reduce(CowEstimate::default, |total, partial| {
total
.checked_add(&partial)
.ok_or(Error::IntOverflow("initial_estimate"))
})?;
fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))
initial_estimate
.fudged(payload_install_ops, vabc_params.version)
.ok_or(Error::IntOverflow("fudged_estimate"))
}
/// Compress the image and return the corresponding information to insert into
@@ -1091,33 +1343,41 @@ pub fn compute_cow_estimate(
/// This is more efficient than separately calling [`compute_cow_estimate`]
/// since the input does not need to be read twice.
pub fn compress_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
output: &(dyn WriteAt + Sync),
partition_name: &str,
block_size: u32,
vabc_algo: Option<VabcAlgo>,
vabc_params: Option<VabcParams>,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<CowEstimate>)> {
const CHUNK_GROUP: u64 = 32;
let file_size = input
.reopen_boxed()
.and_then(|mut r| r.seek(SeekFrom::End(0)))
.map_err(|e| Error::InputOpen(partition_name.to_owned(), e))?;
.file_len()
.map_err(|e| Error::InputSize(partition_name.to_owned(), e))?;
let final_chunk_different = file_size % CHUNK_SIZE != 0;
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
return Err(Error::InvalidPartitionSize {
name: partition_name.to_owned(),
size: file_size,
block_size,
});
}
validate_partition_size(
partition_name,
file_size,
block_size,
vabc_params.and_then(|p| p.version.compression_factor()),
)?;
let chunking = ChunkingParams {
block_size,
method: match vabc_params.map(|p| p.version) {
Some(CowVersion::V3 { compression_factor }) => {
ChunkingMethod::MaxPowerOf2(compression_factor.try_into().unwrap())
}
_ => ChunkingMethod::Exact,
},
};
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
let mut bytes_compressed = 0u64;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut cow_estimate = 0u64;
let mut initial_estimate = CowEstimate::default();
let mut operations = vec![];
// Read the file one group at a time. This allows for some parallelization
@@ -1130,7 +1390,7 @@ pub fn compress_image(
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
.into_par_iter()
.map(|chunk| -> io::Result<(u64, Vec<u8>)> {
let mut reader = input.reopen_boxed()?;
let mut reader = UserPosFile::new(input);
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
@@ -1154,12 +1414,12 @@ pub fn compress_image(
let mut compressed_data_group = uncompressed_data_group
.into_par_iter()
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, CowEstimate)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = vabc_algo
.map(|a| a.compressed_size(&raw_data, block_size))
let cow_estimate = vabc_params
.map(|p| p.algo.compressed_size(&raw_data, chunking))
.transpose()?
.unwrap_or(0);
.unwrap_or_default();
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
@@ -1172,25 +1432,25 @@ pub fn compress_image(
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, operation, cow_size))
Ok((data, operation, cow_estimate))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, operation, cow_size) in &mut compressed_data_group {
for (data, operation, cow_estimate) in &mut compressed_data_group {
operation.data_offset = Some(bytes_compressed);
bytes_compressed = bytes_compressed
.checked_add(data.len() as u64)
.ok_or(Error::IntOverflow("bytes_compressed"))?;
cow_estimate = cow_estimate
.checked_add(*cow_size)
.ok_or(Error::IntOverflow("cow_estimate"))?;
initial_estimate = initial_estimate
.checked_add(cow_estimate)
.ok_or(Error::IntOverflow("initial_estimate"))?;
}
let group_operations = compressed_data_group
.into_par_iter()
.map(|(data, operation, _)| -> io::Result<InstallOperation> {
let mut writer = output.reopen_boxed()?;
let mut writer = UserPosFile::new(output);
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
@@ -1208,8 +1468,12 @@ pub fn compress_image(
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
let cow_estimate = if vabc_algo.is_some() {
Some(fudge_cow_estimate(cow_estimate).ok_or(Error::IntOverflow("cow_estimate_fudged"))?)
let cow_estimate = if let Some(p) = vabc_params {
Some(
initial_estimate
.fudged(operations.len() as u64, p.version)
.ok_or(Error::IntOverflow("fudged_estimate"))?,
)
} else {
None
};
@@ -1255,8 +1519,8 @@ fn extents_sorted(operations: &[InstallOperation]) -> bool {
///
/// Returns the ranges of indices of `operations` that were updated.
pub fn compress_modified_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
input: &(dyn ReadAt + Sync),
output: &(dyn WriteAt + Sync),
block_size: u32,
partition_info: &mut PartitionInfo,
operations: &mut [InstallOperation],
@@ -1305,7 +1569,7 @@ pub fn compress_modified_image(
let extents_size: usize = util::try_cast(extents_size)
.map_err(|e| Error::IntOutOfBounds("extents_size", e))?;
let mut reader = input.reopen_boxed().map_err(Error::ChunkRead)?;
let mut reader = UserPosFile::new(input);
reader
.seek(SeekFrom::Start(extents_start))
.map_err(Error::ChunkRead)?;
@@ -1352,10 +1616,12 @@ pub fn compress_modified_image(
let modified_group_operations = compressed_data_group
.into_par_iter()
.map(|(data, i, operation)| {
let mut writer = output.reopen_boxed()?;
let mut writer = UserPosFile::new(output);
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
// Clippy doesn't know we're returning a Range.
#[allow(clippy::range_plus_one)]
Ok(i..i + 1)
})
.collect::<io::Result<Vec<_>>>()
+24 -21
View File
@@ -1,9 +1,9 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
io::{self, Read, Seek, SeekFrom, Write},
io::{self, Read, Seek, Write},
mem,
ops::Range,
};
@@ -11,7 +11,7 @@ use std::{
use crc32fast::Hasher;
use dlv_list::{Index, VecList};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, FromBytes, IntoBytes};
use zerocopy::{FromBytes, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::stream::ReadDiscardExt;
@@ -159,7 +159,7 @@ impl RawHeader {
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
}
if self.file_hdr_sz.get() < mem::size_of::<RawHeader>() as u16 {
if self.file_hdr_sz.get() < mem::size_of::<Self>() as u16 {
return Err(Error::InvalidFileHeaderSize(self.file_hdr_sz.get()));
} else if self.chunk_hdr_sz.get() < mem::size_of::<RawChunk>() as u16 {
return Err(Error::InvalidChunkHeaderSize(self.chunk_hdr_sz.get()));
@@ -173,7 +173,7 @@ impl RawHeader {
}
fn excess_raw_header_bytes(&self) -> u16 {
self.file_hdr_sz.get() - mem::size_of::<RawHeader>() as u16
self.file_hdr_sz.get() - mem::size_of::<Self>() as u16
}
fn excess_raw_chunk_bytes(&self) -> u16 {
@@ -225,7 +225,7 @@ impl RawChunk {
return Err(Error::InvalidChunkType {
index,
chunk_type: t,
})
});
}
};
@@ -374,6 +374,9 @@ impl fmt::Debug for ChunkData {
/// metadata they contain.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Chunk {
/// When [`Self::data`] is [`ChunkData::Data`], this is guaranteed to not
/// exceed the bounds of [`u32`] when multiplied by [`Header::block_size`].
/// For other types of data, a 64-bit signed or unsigned integer is needed.
pub bounds: ChunkBounds,
pub data: ChunkData,
}
@@ -517,14 +520,14 @@ impl ChunkList {
// Find the chunk to insert before. We save the last used index to
// optimize for sequential insertion and avoid needing to search the
// 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);
}
}
let mut insert_before = 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.
Some(last_used)
} else {
self.chunks.front_index()
};
while let Some(index) = insert_before {
if self.chunks.get(index).unwrap().bounds.start >= chunk.bounds.start {
@@ -669,7 +672,7 @@ fn hash_fill_chunk(
/// A type for reading sparse files.
pub struct SparseReader<R> {
inner: R,
seek: Option<fn(&mut R, SeekFrom) -> io::Result<u64>>,
seek_relative: Option<fn(&mut R, i64) -> io::Result<()>>,
header: RawHeader,
/// Starting block for next chunk.
block: u32,
@@ -686,7 +689,7 @@ impl<R: Read + Seek> SparseReader<R> {
/// efficiently skipped without reading them.
pub fn new_seekable(inner: R, crc_mode: CrcMode) -> Result<Self> {
let mut result = Self::new(inner, crc_mode)?;
result.seek = Some(Seek::seek);
result.seek_relative = Some(Seek::seek_relative);
Ok(result)
}
}
@@ -707,7 +710,7 @@ impl<R: Read> SparseReader<R> {
Ok(Self {
inner,
seek: None,
seek_relative: None,
header,
block: 0,
chunk: 0,
@@ -741,12 +744,12 @@ impl<R: Read> SparseReader<R> {
/// perform its own verification.
pub fn next_chunk(&mut self) -> Result<Option<Chunk>> {
if self.data_remain != 0 {
if let Some(seek) = self.seek {
if let Some(seek_relative) = self.seek_relative {
if self.hasher.is_some() {
return Err(Error::Crc32RandomRead);
}
seek(&mut self.inner, SeekFrom::Current(self.data_remain.into()))
seek_relative(&mut self.inner, self.data_remain.into())
.map_err(|e| Error::DataRead("data_remain", e))?;
self.data_remain = 0;
} else {
@@ -811,7 +814,7 @@ impl<R: Read> SparseReader<R> {
data = ChunkData::Crc32(expected.get());
}
_ => unreachable!(),
};
}
let chunk = Chunk {
bounds: ChunkBounds {
@@ -1023,7 +1026,7 @@ impl<W: Write> Write for SparseWriter<W> {
#[cfg(test)]
mod tests {
use super::{Chunk, ChunkBounds, ChunkData, ChunkList};
use super::*;
#[test]
fn chunk_list_merge() {
+501
View File
@@ -0,0 +1,501 @@
// SPDX-FileCopyrightText: 2025-2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
};
use bstr::ByteSlice;
use rawzip::{
CompressionMethod, RECOMMENDED_BUFFER_SIZE, ReaderAt, ZipArchive, ZipEntries, ZipEntry,
ZipFileHeaderRecord, ZipLocator, ZipReader, ZipSliceArchive, ZipSliceEntries, ZipSliceEntry,
ZipSliceVerifier, ZipVerifier,
extra_fields::{ExtraFieldId, ExtraFields},
};
use zerocopy::{FromZeros, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
stream::ReadAt,
};
pub trait ZipFileHeaderRecordExt<'a> {
fn file_path_utf8(&self) -> Result<&'a str, rawzip::Error>;
}
impl<'a> ZipFileHeaderRecordExt<'a> for ZipFileHeaderRecord<'a> {
fn file_path_utf8(&self) -> Result<&'a str, rawzip::Error> {
str::from_utf8(self.file_path().as_bytes())
.map_err(|e| rawzip::ErrorKind::InvalidUtf8(e).into())
}
}
/// Validate that the current entry's compressed data range does not overlap
/// previously visited entries' ranges. This approach is identical to what
/// rawzip recommends in their examples.
fn validate_and_add_range(
compressed_ranges: &mut Vec<(u64, u64)>,
current_range: (u64, u64),
path: &[u8],
) -> Result<(), rawzip::Error> {
let (current_start, current_end) = current_range;
let insert_pos = compressed_ranges
.binary_search_by_key(&current_start, |&(start, _)| start)
.unwrap_or_else(|pos| pos);
if insert_pos > 0 {
let (prev_start, prev_end) = compressed_ranges[insert_pos - 1];
if prev_end > current_start {
return Err(rawzip::ErrorKind::InvalidInput {
msg: format!("{:?} ({current_start}..{current_end}) overlaps previous range ({prev_start}..{prev_end})", path.as_bstr()),
}.into());
}
}
if insert_pos < compressed_ranges.len() {
let (next_start, next_end) = compressed_ranges[insert_pos];
if current_end > next_start {
return Err(rawzip::ErrorKind::InvalidInput {
msg: format!("{:?} ({current_start}..{current_end}) overlaps next range ({next_start}..{next_end})", path.as_bstr()),
}.into());
}
}
compressed_ranges.insert(insert_pos, current_range);
Ok(())
}
/// Validate that the entry's compression ratio is not excessively large, based
/// on a constant factor for [`CompressionMethod::Deflate`]. This approach is
/// identical to what rawzip recommends in their examples.
fn validate_compression_ratio(
compressed_size: u64,
uncompressed_size: u64,
path: &[u8],
) -> Result<(), rawzip::Error> {
if compressed_size > 0 && uncompressed_size / compressed_size > 1032 {
#[allow(clippy::cast_precision_loss)]
return Err(rawzip::ErrorKind::InvalidInput {
msg: format!(
"{:?} has excessively large compression ratio: {})",
path.as_bstr(),
uncompressed_size as f64 / compressed_size as f64,
),
}
.into());
}
Ok(())
}
#[derive(Debug)]
pub struct ZipEntriesSafe<'archive, 'buf, R> {
archive: &'archive ZipArchive<R>,
entries: ZipEntries<'archive, 'buf, R>,
compressed_ranges: Vec<(u64, u64)>,
}
impl<R: ReaderAt> ZipEntriesSafe<'_, '_, R> {
#[inline]
pub fn next_entry(
&mut self,
) -> Result<Option<(ZipFileHeaderRecord<'_>, ZipEntry<'_, R>)>, rawzip::Error> {
let cd_entry = self.entries.next_entry()?;
let Some(cd_entry) = cd_entry else {
return Ok(None);
};
validate_compression_ratio(
cd_entry.compressed_size_hint(),
cd_entry.uncompressed_size_hint(),
cd_entry.file_path().as_ref(),
)?;
let entry = self.archive.get_entry(cd_entry.wayfinder())?;
validate_and_add_range(
&mut self.compressed_ranges,
entry.compressed_data_range(),
cd_entry.file_path().as_ref(),
)?;
Ok(Some((cd_entry, entry)))
}
}
pub trait ZipEntriesSafeExt<R> {
fn entries_safe<'archive, 'buf>(
&'archive self,
buffer: &'buf mut [u8],
) -> ZipEntriesSafe<'archive, 'buf, R>;
}
impl<R> ZipEntriesSafeExt<R> for ZipArchive<R> {
fn entries_safe<'archive, 'buf>(
&'archive self,
buffer: &'buf mut [u8],
) -> ZipEntriesSafe<'archive, 'buf, R> {
let entries = self.entries(buffer);
ZipEntriesSafe {
archive: self,
entries,
compressed_ranges: Vec::new(),
}
}
}
impl<R, T: ZipEntriesSafeExt<R>> ZipEntriesSafeExt<R> for &T {
fn entries_safe<'archive, 'buf>(
&'archive self,
buffer: &'buf mut [u8],
) -> ZipEntriesSafe<'archive, 'buf, R> {
(**self).entries_safe(buffer)
}
}
impl<R, T: ZipEntriesSafeExt<R>> ZipEntriesSafeExt<R> for &mut T {
fn entries_safe<'archive, 'buf>(
&'archive self,
buffer: &'buf mut [u8],
) -> ZipEntriesSafe<'archive, 'buf, R> {
(**self).entries_safe(buffer)
}
}
#[derive(Debug)]
pub struct ZipSliceEntriesSafe<'data, T: AsRef<[u8]>> {
archive: &'data ZipSliceArchive<T>,
entries: ZipSliceEntries<'data>,
compressed_ranges: Vec<(u64, u64)>,
}
impl<'data, T: AsRef<[u8]>> ZipSliceEntriesSafe<'data, T> {
#[inline]
pub fn next_entry(
&mut self,
) -> Result<Option<(ZipFileHeaderRecord<'data>, ZipSliceEntry<'data>)>, rawzip::Error> {
let cd_entry = self.entries.next_entry()?;
let Some(cd_entry) = cd_entry else {
return Ok(None);
};
validate_compression_ratio(
cd_entry.compressed_size_hint(),
cd_entry.uncompressed_size_hint(),
cd_entry.file_path().as_ref(),
)?;
let entry = self.archive.get_entry(cd_entry.wayfinder())?;
validate_and_add_range(
&mut self.compressed_ranges,
entry.compressed_data_range(),
cd_entry.file_path().as_ref(),
)?;
Ok(Some((cd_entry, entry)))
}
}
pub trait ZipSliceEntriesSafeExt<T: AsRef<[u8]>> {
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T>;
}
impl<T: AsRef<[u8]>> ZipSliceEntriesSafeExt<T> for ZipSliceArchive<T> {
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
let entries = self.entries();
ZipSliceEntriesSafe {
archive: self,
entries,
compressed_ranges: Vec::new(),
}
}
}
impl<T: AsRef<[u8]>, U: ZipSliceEntriesSafeExt<T>> ZipSliceEntriesSafeExt<T> for &U {
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
(**self).entries_safe()
}
}
impl<T: AsRef<[u8]>, U: ZipSliceEntriesSafeExt<T>> ZipSliceEntriesSafeExt<T> for &mut U {
fn entries_safe(&self) -> ZipSliceEntriesSafe<'_, T> {
(**self).entries_safe()
}
}
fn compression_method_to_format(
compression_method: CompressionMethod,
) -> Result<CompressedFormat, rawzip::Error> {
match compression_method {
CompressionMethod::Store => Ok(CompressedFormat::None),
CompressionMethod::Deflate => Ok(CompressedFormat::Deflate),
c => Err(rawzip::ErrorKind::InvalidInput {
msg: format!("Unsupported compression method: {c:?}"),
}
.into()),
}
}
pub fn compressed_reader<'archive, R: ReaderAt>(
entry: &ZipEntry<'archive, R>,
compression_method: CompressionMethod,
) -> Result<CompressedReader<ZipReader<&'archive R>>, rawzip::Error> {
let format = compression_method_to_format(compression_method)?;
CompressedReader::with_format(entry.reader(), format)
.map_err(|e| rawzip::ErrorKind::InvalidInput { msg: e.to_string() }.into())
}
pub fn compressed_slice_reader<'archive>(
entry: &ZipSliceEntry<'archive>,
compression_method: CompressionMethod,
) -> Result<CompressedReader<Cursor<&'archive [u8]>>, rawzip::Error> {
let format = compression_method_to_format(compression_method)?;
let raw_reader = Cursor::new(entry.data());
CompressedReader::with_format(raw_reader, format)
.map_err(|e| rawzip::ErrorKind::InvalidInput { msg: e.to_string() }.into())
}
pub fn verifying_reader<'archive, R: ReaderAt>(
entry: &ZipEntry<'archive, R>,
compression_method: CompressionMethod,
) -> Result<ZipVerifier<CompressedReader<ZipReader<&'archive R>>, &'archive R>, rawzip::Error> {
compressed_reader(entry, compression_method).map(|r| entry.verifying_reader(r))
}
pub fn verifying_slice_reader<'archive>(
entry: &ZipSliceEntry<'archive>,
compression_method: CompressionMethod,
) -> Result<ZipSliceVerifier<CompressedReader<Cursor<&'archive [u8]>>>, rawzip::Error> {
compressed_slice_reader(entry, compression_method).map(|r| entry.verifying_reader(r))
}
pub fn compressed_writer<W: Write>(
writer: W,
compression_method: CompressionMethod,
) -> Result<CompressedWriter<W>, rawzip::Error> {
use compression::Error;
let format = compression_method_to_format(compression_method)?;
match CompressedWriter::new(writer, format) {
Ok(w) => Ok(w),
Err(
Error::Lz4EncodeInit(e)
| Error::XzEncodeInit(e)
| Error::LzmaDecodeInit(e)
| Error::LzmaEncodeInit(e),
) => Err(e.into()),
Err(Error::UnknownFormat | Error::AutoDetect(_)) => unreachable!(),
}
}
pub trait ZipArchiveReadAtExt {
fn from_read_at<R: ReadAt>(
file: R,
buffer: &mut [u8],
) -> Result<ZipArchive<ReaderAtWrapper<R>>, rawzip::Error> {
let end_offset = file.file_len()?;
ZipLocator::new()
.locate_in_reader(ReaderAtWrapper(file), buffer, end_offset)
.map_err(|(_, e)| e)
}
}
impl ZipArchiveReadAtExt for ZipArchive<()> {}
pub struct ReaderAtWrapper<R: ReadAt>(pub R);
impl<R: ReadAt> ReaderAt for ReaderAtWrapper<R> {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
ReadAt::read_at(&self.0, buf, offset)
}
}
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct ZipLocalHeader {
signature: little_endian::U32,
version_needed: little_endian::U16,
flags: little_endian::U16,
compression_method: little_endian::U16,
last_mod_time: little_endian::U16,
last_mod_date: little_endian::U16,
crc32: little_endian::U32,
compressed_size: little_endian::U32,
uncompressed_size: little_endian::U32,
file_name_len: little_endian::U16,
extra_field_len: little_endian::U16,
}
/// Convert a streaming zip into a non-streaming one. If any entry uses ZIP64,
/// the local header must contain an [`ExtraFieldId::ANDROID_ZIP_ALIGNMENT`]
/// extra field with sufficient size (16 bytes to be safe). This is used as
/// reserved space for creating a new [`ExtraFieldId::ZIP64`] extra field. Any
/// leftover space must be at least 4 bytes so that a new extra field can
/// consume the space. The existing data descriptor will remain in the gap
/// between entries and data will not be shifted.
pub fn make_non_streaming(file: impl Read + Write + Seek) -> Result<(), rawzip::Error> {
// rawzip currently does not expose the CRC32 value, so we'll have to read
// it ourselves.
struct EntryInfo {
local_header_offset: u64,
central_header_offset: u64,
crc32: u32,
compressed_size: u64,
uncompressed_size: u64,
local_extra_fields: Vec<u8>,
}
let mut to_update = vec![];
let mut central_buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let mut local_buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let archive = ZipArchive::from_seekable(file, &mut central_buffer)?;
let mut entries = archive.entries_safe(&mut central_buffer);
while let Some((cd_entry, entry)) = entries.next_entry()? {
let wf = cd_entry.wayfinder();
let local_header = entry.local_header(&mut local_buffer)?;
to_update.push(EntryInfo {
local_header_offset: cd_entry.local_header_offset(),
central_header_offset: cd_entry.central_directory_offset(),
crc32: cd_entry.crc32(),
compressed_size: wf.compressed_size_hint(),
uncompressed_size: wf.uncompressed_size_hint(),
local_extra_fields: local_header.extra_fields().remaining_bytes().to_vec(),
});
}
let mut file = archive.into_inner().into_inner();
for entry in to_update {
// Clear the central header's streaming flag.
let mut central_flags = little_endian::U16::new(0);
file.seek(SeekFrom::Start(entry.central_header_offset + 8))?;
file.read_exact(central_flags.as_mut_bytes())?;
central_flags &= !0x8;
file.seek_relative(-(central_flags.as_bytes().len() as i64))?;
file.write_all(central_flags.as_bytes())?;
file.seek(SeekFrom::Start(entry.local_header_offset))?;
let mut local_header = ZipLocalHeader::new_zeroed();
file.read_exact(local_header.as_mut_bytes())?;
// Clear the local header's streaming flag.
local_header.flags &= !0x8;
// Remove dependency on the data descriptor.
local_header.crc32.set(entry.crc32);
let compressed_is_zip64 = entry.compressed_size >= 0xffffffff;
let uncompressed_is_zip64 = entry.uncompressed_size >= 0xffffffff;
if compressed_is_zip64 {
local_header.compressed_size.set(0xffffffff);
} else {
local_header
.compressed_size
.set(entry.compressed_size as u32);
}
if uncompressed_is_zip64 {
local_header.uncompressed_size.set(0xffffffff);
} else {
local_header
.uncompressed_size
.set(entry.uncompressed_size as u32);
}
file.seek_relative(-(local_header.as_bytes().len() as i64))?;
file.write_all(local_header.as_bytes())?;
file.seek_relative(i64::from(local_header.file_name_len.get()))?;
if !compressed_is_zip64 && !uncompressed_is_zip64 {
continue;
}
let mut extra_fields = Vec::with_capacity(entry.local_extra_fields.len());
let mut patched_placeholder = false;
for (id, data) in ExtraFields::new(&entry.local_extra_fields) {
if id == ExtraFieldId::ANDROID_ZIP_ALIGNMENT {
let zip64_len =
8 * (usize::from(compressed_is_zip64) + usize::from(uncompressed_is_zip64));
// Any unused space needs to be at least 4 bytes, so we can
// properly write a new extra field for padding.
let have_needed_space = match data.len().cmp(&zip64_len) {
Ordering::Less => false,
Ordering::Equal => true,
Ordering::Greater => data.len() - zip64_len >= 4,
};
if !have_needed_space {
return Err(rawzip::ErrorKind::InvalidInput {
msg: format!(
"Invalid reserved ZIP64 local extra field size: {}",
data.len()
),
}
.into());
}
// The order is indeed backwards compared to the header
// fields (APPNOTE 4.5.3).
extra_fields.extend_from_slice(&ExtraFieldId::ZIP64.as_u16().to_le_bytes());
extra_fields.extend_from_slice(&(zip64_len as u16).to_le_bytes());
if uncompressed_is_zip64 {
extra_fields.extend_from_slice(&entry.uncompressed_size.to_le_bytes());
}
if compressed_is_zip64 {
extra_fields.extend_from_slice(&entry.compressed_size.to_le_bytes());
}
// Keep using ANDROID_ZIP_ALIGNMENT for padding.
if data.len() > zip64_len {
let padding_len = data.len() - zip64_len - 4;
extra_fields.extend_from_slice(&id.as_u16().to_le_bytes());
extra_fields.extend_from_slice(&(padding_len as u16).to_le_bytes());
extra_fields.resize(extra_fields.len() + padding_len, 0);
}
patched_placeholder = true;
} else if id == ExtraFieldId::ZIP64 {
return Err(rawzip::ErrorKind::InvalidInput {
msg: "Unexpected ZIP64 extra field present".to_owned(),
}
.into());
} else {
extra_fields.extend_from_slice(&id.as_u16().to_le_bytes());
extra_fields.extend_from_slice(&(data.len() as u16).to_le_bytes());
extra_fields.extend_from_slice(data);
}
}
assert_eq!(extra_fields.len(), entry.local_extra_fields.len());
if !patched_placeholder {
return Err(rawzip::ErrorKind::InvalidInput {
msg: "ZIP64 required, but no placeholder extra field found".to_owned(),
}
.into());
}
file.write_all(&extra_fields)?;
}
Ok(())
}
+1 -1
View File
@@ -4,8 +4,8 @@
use std::{
process::ExitCode,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
+2 -2
View File
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
//! Hack to format an integer as an octal string because toml_edit can't output
//! Hack to format an integer as an octal string because toml can't output
//! octal-formatted integers and many other toml parsers can't parse it either.
use std::{
@@ -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
+319 -178
View File
@@ -1,7 +1,8 @@
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2022-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
borrow::Cow,
cmp::Ordering,
collections::{HashMap, HashSet},
fmt::Write,
@@ -15,18 +16,15 @@ use std::{
};
use bstr::ByteSlice;
use liblzma::{
stream::{Check, Stream},
write::XzEncoder,
};
use lzma_rust2::{CheckType, XzOptions, XzWriter};
use rawzip::{RECOMMENDED_BUFFER_SIZE, ZipArchive};
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 crate::{
crypto::{self, RsaSigningKey},
@@ -35,9 +33,11 @@ use crate::{
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntry, CpioEntryData},
zip::{self, ZipEntriesSafeExt, ZipFileHeaderRecordExt, ZipSliceEntriesSafeExt},
},
patch::otacert::{self, OtaCertBuildFlags},
stream::{self, FromReader, HashingWriter, ReadSeek, SectionReader, ToWriter, WriteSeek},
util,
};
#[derive(Debug, Error)]
@@ -83,17 +83,19 @@ 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:?}")]
ZipOpen(PathBuf, #[source] ZipError),
ZipOpen(PathBuf, #[source] rawzip::Error),
#[error("Failed to list zip entries")]
ZipEntryList(#[source] rawzip::Error),
#[error("Missing zip entry: {0:?}")]
ZipEntryMissing(Cow<'static, str>),
#[error("Failed to open zip entry: {0:?}")]
ZipEntryOpen(&'static str, #[source] ZipError),
ZipEntryOpen(Cow<'static, str>, #[source] rawzip::Error),
#[error("Failed to read zip entry: {0:?}")]
ZipEntryRead(&'static str, #[source] io::Error),
#[error("Failed to open zip entry #{0}")]
ZipIndexOpen(usize, #[source] ZipError),
ZipEntryRead(Cow<'static, str>, #[source] io::Error),
#[error("Failed to open file: {0:?}")]
FileOpen(PathBuf, #[source] io::Error),
}
@@ -137,10 +139,13 @@ fn load_ramdisk(
}
fn save_ramdisk(
entries: &[CpioEntry],
entries: &mut [CpioEntry],
format: CompressedFormat,
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
cpio::sort(entries);
cpio::assign_inodes(entries, false).map_err(Error::RamdiskSaveCpio)?;
let raw_writer = Cursor::new(vec![]);
let mut writer =
CompressedWriter::new(raw_writer, format).map_err(Error::RamdiskSaveCompression)?;
@@ -154,6 +159,12 @@ fn save_ramdisk(
Ok(raw_writer.into_inner())
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct BootLayoutInfo {
pub has_recovery: bool,
}
#[derive(Clone, Eq, PartialEq)]
pub struct BootImageInfo {
pub header: Header,
pub footer: Footer,
@@ -169,11 +180,17 @@ pub trait BootImagePatch {
/// can be inspected, but during patching, only the boot image is available.
fn find_targets<'a>(
&self,
layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>>;
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
fn patch(
&self,
layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()>;
}
/// Root a boot image with Magisk.
@@ -194,11 +211,12 @@ 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..30800];
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;
const VER_XZ_BACKUP: RangeFrom<u32> = 26403..;
const VER_VENDOR_BOOT: RangeFrom<u32> = 30300..;
const ZIP_INIT_LD: &'static str = "lib/arm64-v8a/libinit-ld.so";
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
@@ -256,41 +274,54 @@ impl MagiskRootPatcher {
})
}
fn get_version(path: &Path) -> Result<u32> {
let reader = File::open(path)
.map(BufReader::new)
.map_err(|e| Error::FileOpen(path.to_owned(), e))?;
let mut zip = ZipArchive::new(reader).map_err(|e| Error::ZipOpen(path.to_owned(), e))?;
let entry = zip
.by_name(Self::ZIP_UTIL_FUNCTIONS)
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS, e))?;
let mut entry = BufReader::new(entry);
let mut line = String::new();
fn get_version(apk_path: &Path) -> Result<u32> {
let file = File::open(apk_path).map_err(|e| Error::FileOpen(apk_path.to_owned(), e))?;
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let archive = ZipArchive::from_file(file, &mut buffer)
.map_err(|e| Error::ZipOpen(apk_path.to_owned(), e))?;
let mut entries = archive.entries_safe(&mut buffer);
loop {
line.clear();
let n = entry
.read_line(&mut line)
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS, e))?;
if n == 0 {
return Err(Error::FindMagiskVersion(path.to_owned()));
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
if path != Self::ZIP_UTIL_FUNCTIONS {
continue;
}
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
trace!("Magisk version code line: {line:?}");
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map(BufReader::new)
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_UTIL_FUNCTIONS.into(), e))?;
let mut line = String::new();
let version = suffix
.parse()
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
return Ok(version);
loop {
line.clear();
let n = reader
.read_line(&mut line)
.map_err(|e| Error::ZipEntryRead(Self::ZIP_UTIL_FUNCTIONS.into(), e))?;
if n == 0 {
return Err(Error::FindMagiskVersion(apk_path.to_owned()));
}
if let Some(suffix) = line.trim_end().strip_prefix("MAGISK_VER_CODE=") {
trace!("Magisk version code line: {line:?}");
let version = suffix
.parse()
.map_err(|e| Error::ParseMagiskVersion(suffix.to_owned(), e))?;
return Ok(version);
}
}
}
Err(Error::ZipEntryMissing(Self::ZIP_UTIL_FUNCTIONS.into()))
}
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())
@@ -376,17 +407,15 @@ impl MagiskRootPatcher {
let mut new_path = b".backup/".to_vec();
new_path.extend(&old_entry.path);
let mut new_data = None;
let new_data = if xz_compress && let CpioEntryData::Data(data) = &old_entry.data {
new_path.extend(b".xz");
if xz_compress {
if 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)?;
Some(CpioEntryData::Data(buf))
} else {
None
};
new_entries.push(CpioEntry {
path: new_path,
@@ -423,6 +452,7 @@ impl BootImagePatch for MagiskRootPatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -432,18 +462,17 @@ impl BootImagePatch for MagiskRootPatcher {
targets.push("init_boot");
} else if boot_images.contains_key("boot") {
targets.push("boot");
};
}
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
let zip_reader = File::open(&self.apk_path)
.map(BufReader::new)
.map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
let mut zip =
ZipArchive::new(zip_reader).map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
// Load the first ramdisk. If it doesn't exist, we have to generate one
// from scratch.
let ramdisk = match boot_image {
@@ -469,60 +498,95 @@ impl BootImagePatch for MagiskRootPatcher {
// Delete the original init.
entries.retain(|e| e.path != b"init");
// Add magiskinit.
{
let mut zip_entry = zip
.by_name(Self::ZIP_MAGISKINIT)
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT, e))?;
let mut data = vec![];
zip_entry
.read_to_end(&mut data)
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT, e))?;
let file =
File::open(&self.apk_path).map_err(|e| Error::FileOpen(self.apk_path.clone(), e))?;
let mut buffer = vec![0u8; RECOMMENDED_BUFFER_SIZE];
let archive = ZipArchive::from_file(file, &mut buffer)
.map_err(|e| Error::ZipOpen(self.apk_path.clone(), e))?;
let mut zip_entries = archive.entries_safe(&mut buffer);
let mut found_magiskinit = false;
let mut found_libmagisk = false;
while let Some((cd_entry, entry)) = zip_entries.next_entry().map_err(Error::ZipEntryList)? {
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
// magiskinit is the only entry that is not xz-compressed.
if path == Self::ZIP_MAGISKINIT {
let mut reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(Self::ZIP_MAGISKINIT.into(), e))?;
let mut data = vec![];
reader
.read_to_end(&mut data)
.map_err(|e| Error::ZipEntryRead(Self::ZIP_MAGISKINIT.into(), e))?;
entries.push(CpioEntry::new_file(
b"init",
0o750,
CpioEntryData::Data(data),
));
found_magiskinit = true;
continue;
}
// Keep a 'static version of the zip path.
let (path, cpio_path): (_, &[u8]) = match path {
// Newer Magisk versions only include a single binary for the
// target ABI in the ramdisk. This was introduced in commit
// fb5ee86615ed3df830e8538f8b39b1b133caea34.
p if p == Self::ZIP_LIBMAGISK => {
debug!("Single libmagisk");
found_libmagisk = true;
(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz")
}
// Older Magisk versions include the 64-bit binary and,
// optionally, the 32-bit binary if the device supports it. We
// unconditionally include the magisk32 because the boot image
// itself doesn't have sufficient information to determine if a
// device is 64-bit only.
p if p == Self::ZIP_LIBMAGISK32 => {
debug!("Split libmagisk32");
found_libmagisk = true;
(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz")
}
p if p == Self::ZIP_LIBMAGISK64 => {
debug!("Split libmagisk64");
found_libmagisk = true;
(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz")
}
// The stub apk was introduced in commit
// ad0e6511e11ebec65aa9b5b916e1397342850319.
p if p == Self::ZIP_STUB => {
debug!("Magisk stub found");
(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz")
}
// init-ld was introduced in commit
// 33aebb59763b6ec27209563035303700e998633d.
p if p == Self::ZIP_INIT_LD => {
debug!("Magisk init-ld found");
(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz")
}
_ => continue,
};
let reader = zip::verifying_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(path.into(), e))?;
let buf = Self::xz_compress(path.as_bytes(), reader, cancel_signal)?;
entries.push(CpioEntry::new_file(
b"init",
0o750,
CpioEntryData::Data(data),
cpio_path,
0o644,
CpioEntryData::Data(buf),
));
}
let mut xz_files = HashMap::<&str, &[u8]>::new();
if zip.file_names().any(|n| n == Self::ZIP_LIBMAGISK) {
// Newer Magisk versions only include a single binary for the target
// ABI in the ramdisk. fb5ee86615ed3df830e8538f8b39b1b133caea34.
debug!("Single libmagisk");
xz_files.insert(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz");
} else {
// Older Magisk versions include the 64-bit binary and, optionally,
// the 32-bit binary if the device supports it. We unconditionally
// include the magisk32 because the boot image itself doesn't have
// sufficient information to determine if a device is 64-bit only.
debug!("Split libmagisk32/libmagisk64");
xz_files.insert(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz");
xz_files.insert(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz");
}
// Add stub apk, which only exists after Magisk commit
// ad0e6511e11ebec65aa9b5b916e1397342850319.
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
debug!("Magisk stub found");
xz_files.insert(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz");
}
// Add init-ld, which only exists after Magisk commit
// 33aebb59763b6ec27209563035303700e998633d
if zip.file_names().any(|n| n == Self::ZIP_INIT_LD) {
debug!("Magisk init-ld found");
xz_files.insert(Self::ZIP_INIT_LD, b"overlay.d/sbin/init-ld.xz");
}
for (source, target) in xz_files {
let reader = zip
.by_name(source)
.map_err(|e| Error::ZipEntryOpen(source, e))?;
let buf = Self::xz_compress(source.as_bytes(), reader, cancel_signal)?;
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
if !found_magiskinit {
return Err(Error::ZipEntryMissing(Self::ZIP_MAGISKINIT.into()));
} else if !found_libmagisk {
return Err(Error::ZipEntryMissing(Self::ZIP_LIBMAGISK.into()));
}
// Create Magisk .backup directory structure.
@@ -544,10 +608,17 @@ 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();
}
// We never install Magisk into the init_boot.cpio within the
// vendor_boot ramdisk. This config option was introduced in commit
// 742913ebcb10cf819a54699497359535047874f7.
if Self::VER_VENDOR_BOOT.contains(&self.version) {
magisk_config.push_str("VENDORBOOT=false\n");
}
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
@@ -569,9 +640,7 @@ impl BootImagePatch for MagiskRootPatcher {
));
// Repack ramdisk.
cpio::sort(&mut entries);
cpio::assign_inodes(&mut entries, false).map_err(Error::RamdiskSaveCpio)?;
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
let new_ramdisk = save_ramdisk(&mut entries, ramdisk_format, cancel_signal)?;
match boot_image {
BootImage::V0Through2(b) => b.ramdisk = new_ramdisk,
@@ -604,7 +673,7 @@ pub struct OtaCertPatcher {
}
impl OtaCertPatcher {
const OTACERTS_PATH: &'static [u8] = b"system/etc/security/otacerts.zip";
const OTACERTS_PATH: &'static str = "system/etc/security/otacerts.zip";
pub fn new(cert: Certificate) -> Self {
Self { cert }
@@ -630,29 +699,33 @@ impl OtaCertPatcher {
}
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
let Some(entry) = entries.iter().find(|e| e.path == Self::OTACERTS_PATH) else {
let Some(entry) = entries
.iter()
.find(|e| e.path == Self::OTACERTS_PATH.as_bytes())
else {
continue;
};
let CpioEntryData::Data(data) = &entry.data else {
continue;
};
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(|e| {
Error::ZipOpen(str::from_utf8(Self::OTACERTS_PATH).unwrap().into(), e)
})?;
let archive = ZipArchive::from_slice(data)
.map_err(|e| Error::ZipOpen(Self::OTACERTS_PATH.into(), e))?;
let mut entries = archive.entries_safe();
for index in 0..zip.len() {
let zip_entry = zip
.by_index(index)
.map_err(|e| Error::ZipIndexOpen(index, e))?;
if !zip_entry.name().ends_with(".x509.pem") {
debug!("Skipping invalid entry path: {}", zip_entry.name());
while let Some((cd_entry, entry)) = entries.next_entry().map_err(Error::ZipEntryList)? {
let path = cd_entry.file_path_utf8().map_err(Error::ZipEntryList)?;
if !path.ends_with(".x509.pem") {
debug!("Skipping invalid entry path: {path:?}");
continue;
}
let path = PathBuf::from(zip_entry.name());
let reader = zip::verifying_slice_reader(&entry, cd_entry.compression_method())
.map_err(|e| Error::ZipEntryOpen(path.to_owned().into(), e))?;
let certificate =
crypto::read_pem_cert(&path, zip_entry).map_err(Error::OtaCertLoad)?;
crypto::read_pem_cert(Path::new(path), reader).map_err(Error::OtaCertLoad)?;
certificates.push(certificate);
}
}
@@ -666,7 +739,10 @@ impl OtaCertPatcher {
cancel_signal: &AtomicBool,
) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
let Some(entry) = entries
.iter_mut()
.find(|e| e.path == Self::OTACERTS_PATH.as_bytes())
else {
return Ok(false);
};
@@ -675,7 +751,7 @@ impl OtaCertPatcher {
entry.data = CpioEntryData::Data(zip.to_vec());
// Repack ramdisk.
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
*ramdisk = save_ramdisk(&mut entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
@@ -688,6 +764,7 @@ impl BootImagePatch for OtaCertPatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -707,7 +784,10 @@ impl BootImagePatch for OtaCertPatcher {
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)
.map_err(|e| TargetsError::Load(name.to_owned(), e))?;
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
if entries
.iter()
.any(|e| e.path == Self::OTACERTS_PATH.as_bytes())
{
targets.push(name);
continue 'outer;
}
@@ -717,7 +797,12 @@ impl BootImagePatch for OtaCertPatcher {
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
let ramdisks = match boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
@@ -742,7 +827,7 @@ impl BootImagePatch for OtaCertPatcher {
// out of future updates if the OTA certificate mechanism has changed.
Err(Error::Validation(format!(
"No ramdisk contains {:?}",
Self::OTACERTS_PATH.as_bstr(),
Self::OTACERTS_PATH,
)))
}
}
@@ -756,34 +841,49 @@ impl DsuPubKeyPatcher {
const FIRST_STAGE_PATH: &'static [u8] = b"first_stage_ramdisk";
const DSU_KEYS_PATH: &'static [u8] = b"first_stage_ramdisk/avb";
const AVBROOT_KEY_PATH: &'static [u8] = b"first_stage_ramdisk/avb/avbroot.avbpubkey";
const RECOVERY_DSU_KEYS_PATH: &'static [u8] = b"avb";
const RECOVERY_AVBROOT_KEY_PATH: &'static [u8] = b"avb/avbroot.avbpubkey";
pub fn new(key: RsaPublicKey) -> Self {
Self { key }
}
fn patch_ramdisk(&self, ramdisk: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
fn patch_ramdisk(
&self,
layout: BootLayoutInfo,
ramdisk: &mut Vec<u8>,
cancel_signal: &AtomicBool,
) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
if !entries.iter_mut().any(|e| e.path == Self::FIRST_STAGE_PATH) {
return Ok(false);
}
if !entries.iter().any(|e| e.path == Self::DSU_KEYS_PATH) {
entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755));
let (dsu_keys_path, avbroot_key_path) = if layout.has_recovery {
(
Self::RECOVERY_DSU_KEYS_PATH,
Self::RECOVERY_AVBROOT_KEY_PATH,
)
} else {
(Self::DSU_KEYS_PATH, Self::AVBROOT_KEY_PATH)
};
debug!("DSU key path: {:?}", avbroot_key_path.as_bstr());
if !entries.iter().any(|e| e.path == dsu_keys_path) {
entries.push(CpioEntry::new_directory(dsu_keys_path, 0o755));
}
let binary_key = avb::encode_public_key(&self.key).map_err(Error::AvbEncodeKey)?;
let data = CpioEntryData::Data(binary_key);
if let Some(e) = entries
.iter_mut()
.find(|e| e.path == Self::AVBROOT_KEY_PATH)
{
if let Some(e) = entries.iter_mut().find(|e| e.path == avbroot_key_path) {
e.data = data;
} else {
entries.push(CpioEntry::new_file(Self::AVBROOT_KEY_PATH, 0o644, data));
};
entries.push(CpioEntry::new_file(avbroot_key_path, 0o644, data));
}
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
*ramdisk = save_ramdisk(&mut entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
@@ -796,6 +896,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
fn find_targets<'a>(
&self,
layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -809,6 +910,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
BootImage::VendorV3Through4(b) => &b.ramdisks,
};
let dsu_keys_path = if layout.has_recovery {
Self::RECOVERY_DSU_KEYS_PATH
} else {
Self::DSU_KEYS_PATH
};
for ramdisk in ramdisks {
if ramdisk.is_empty() {
continue;
@@ -819,10 +926,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
let mut found = false;
for entry in entries {
if entry.path == Self::DSU_KEYS_PATH {
if entry.path == dsu_keys_path {
dsu_keys_targets.push(name);
found = true;
} else if entry.path == Self::FIRST_STAGE_PATH {
// This exists even on devices with recovery partitions.
// init just doesn't switch root to it.
first_stage_targets.push(name);
found = true;
}
@@ -856,7 +965,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
}
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
let ramdisks = match boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
@@ -868,7 +982,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
continue;
}
if self.patch_ramdisk(ramdisk, cancel_signal)? {
if self.patch_ramdisk(layout, ramdisk, cancel_signal)? {
return Ok(());
}
}
@@ -930,16 +1044,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))
}
@@ -952,6 +1067,7 @@ impl BootImagePatch for PrepatchedImagePatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -971,12 +1087,17 @@ impl BootImagePatch for PrepatchedImagePatcher {
targets.push("init_boot");
} else if boot_images.contains_key("boot") {
targets.push("boot");
};
}
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
_cancel_signal: &AtomicBool,
) -> Result<()> {
let prepatched_image = self.load_prepatched_image()?;
// Level 0: Warnings that don't affect booting
@@ -1211,25 +1332,43 @@ fn save_boot_image(
Ok(())
}
pub trait BootImageOpener {
fn open_original(&self, name: &str) -> io::Result<Box<dyn ReadSeek + Sync>>;
fn open_replacement(&self, name: &str) -> io::Result<Box<dyn WriteSeek + Sync>> {
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("{name} boot image not found"),
))
}
}
pub fn load_boot_images<'a>(
names: &[&'a str],
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
opener: &(dyn BootImageOpener + Sync),
) -> TargetsResult<(HashMap<&'a str, BootImageInfo>, BootLayoutInfo)> {
let parent_span = Span::current();
names
let images = names
.par_iter()
.map(|&name| {
let _span = debug_span!(parent: &parent_span, "image", name).entered();
let mut reader =
open_input(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
let mut reader = opener
.open_original(name)
.map_err(|e| TargetsError::Open(name.to_owned(), e))?;
let info =
load_boot_image(&mut reader).map_err(|e| TargetsError::Load(name.to_owned(), e))?;
Ok((name, info))
})
.collect()
.collect::<TargetsResult<HashMap<_, _>>>()?;
let layout = BootLayoutInfo {
has_recovery: images.contains_key("recovery"),
};
Ok((images, layout))
}
/// Apply applicable patches to the list of specified boot images. For each
@@ -1239,8 +1378,7 @@ pub fn load_boot_images<'a>(
/// be opened from multiple threads, but at most once each.
pub fn patch_boot_images<'a>(
names: &[&'a str],
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
opener: &(dyn BootImageOpener + Sync),
key: &RsaSigningKey,
patchers: &[Box<dyn BootImagePatch + Sync>],
cancel_signal: &AtomicBool,
@@ -1253,7 +1391,7 @@ pub fn patch_boot_images<'a>(
}
// Preparse all images. Some patchers need to inspect every candidate.
let mut images = load_boot_images(names, open_input)?;
let (mut images, layout) = load_boot_images(names, opener)?;
// Find the targets that each patcher wants to patch.
let all_targets = patchers
@@ -1261,14 +1399,15 @@ pub fn patch_boot_images<'a>(
.map(|p| {
let _span =
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
p.find_targets(&images, cancel_signal).and_then(|targets| {
if targets.is_empty() {
Err(TargetsError::NoTargets(p.patcher_name()))
} else {
debug!("Found patcher targets: {targets:?}");
Ok(targets)
}
})
p.find_targets(layout, &images, cancel_signal)
.and_then(|targets| {
if targets.is_empty() {
Err(TargetsError::NoTargets(p.patcher_name()))
} else {
debug!("Found patcher targets: {targets:?}");
Ok(targets)
}
})
})
.collect::<TargetsResult<Vec<_>>>()?;
@@ -1295,8 +1434,8 @@ pub fn patch_boot_images<'a>(
.try_for_each(|(&name, (info, patchers))| -> TargetsResult<()> {
patchers.iter().try_for_each(|p| {
let _span =
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
p.patch(&mut info.boot_image, cancel_signal)
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name(), image = name).entered();
p.patch(layout, &mut info.boot_image, cancel_signal)
.map_err(|e| TargetsError::Patch(name.to_owned(), e))
})
})?;
@@ -1304,7 +1443,9 @@ pub fn patch_boot_images<'a>(
// Resign and write new images.
groups.par_iter_mut().try_for_each(|(&name, (info, _))| {
let _span = debug_span!(parent: &parent_span, "image", name).entered();
let mut writer = open_output(name).map_err(|e| TargetsError::Open(name.to_owned(), e))?;
let mut writer = opener
.open_replacement(name)
.map_err(|e| TargetsError::Open(name.to_owned(), e))?;
save_boot_image(&mut writer, info, key).map_err(|e| TargetsError::Save(name.to_owned(), e))
})?;
+32 -16
View File
@@ -1,15 +1,18 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{borrow::Cow, cmp::Ordering, io::Cursor, path::Path};
use bitflags::bitflags;
use rawzip::{CompressionMethod, ZipArchiveWriter};
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 crate::{crypto, format::ota};
use crate::{
crypto,
format::{ota, zip},
};
#[derive(Debug, Error)]
pub enum Error {
@@ -18,7 +21,7 @@ pub enum Error {
#[error("New otacerts.zip is too large to fit in {0} bytes")]
ZipTooLarge(usize),
#[error("Failed to write otacerts zip")]
ZipWrite(#[source] ZipError),
ZipWrite(#[source] rawzip::Error),
#[error("Failed to write certificate to otacerts zip")]
CertWrite(#[source] crypto::Error),
}
@@ -71,17 +74,23 @@ bitflags! {
/// Create an `otacerts.zip` file containing the specified certificate.
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
let raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let mut writer = ZipArchiveWriter::new(raw_writer);
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
CompressionMethod::Deflated
CompressionMethod::Deflate
} else {
CompressionMethod::Stored
CompressionMethod::Store
};
let options = FileOptions::default().compression_method(compression_method);
let name = "ota.x509.pem";
writer.start_file(name, options).map_err(Error::ZipWrite)?;
let (entry_writer, data_config) = writer
.new_file(name)
.compression_method(compression_method)
.start()
.map_err(Error::ZipWrite)?;
let compressed_writer =
zip::compressed_writer(entry_writer, compression_method).map_err(Error::ZipWrite)?;
let mut data_writer = data_config.wrap(compressed_writer);
let cert = if flags.is_empty() {
Cow::Borrowed(cert)
@@ -93,10 +102,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();
@@ -110,9 +119,16 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
Cow::Owned(modified)
};
crypto::write_pem_cert(Path::new(name), &mut writer, &cert).map_err(Error::CertWrite)?;
crypto::write_pem_cert(Path::new(name), &mut data_writer, &cert).map_err(Error::CertWrite)?;
let raw_writer = writer.finish().map_err(Error::ZipWrite)?;
data_writer
.finish()
.and_then(|(w, d)| w.finish()?.finish(d))
.map_err(Error::ZipWrite)?;
let mut raw_writer = writer.finish().map_err(Error::ZipWrite)?;
zip::make_non_streaming(&mut raw_writer).map_err(Error::ZipWrite)?;
Ok(raw_writer.into_inner())
}
+36 -52
View File
@@ -1,27 +1,28 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
io::{self, Cursor, SeekFrom},
io::{self, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use memchr::memmem;
use rawzip::ZipArchive;
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;
use crate::{
crypto::RsaSigningKey,
format::{
avb::{self, AppendedDescriptorMut, Footer},
ota,
zip::{ZipFileHeaderRecordExt, ZipSliceEntriesSafeExt},
},
patch::otacert,
stream::{self, ReadFixedSizeExt, ReadSeekReopen, SectionReader, WriteSeekReopen},
stream::{self, ReadFixedSizeExt, ReadWriteAt, UserPosFile},
util,
};
@@ -68,25 +69,28 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
trace!("Found zip bounds: {:?}", start..end);
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
let mut zip_reader = ZipArchive::new(reader).ok()?;
let archive = ZipArchive::from_slice(&data[start..end]).ok()?;
let mut entries = archive.entries_safe();
let mut matches = 0;
if zip_reader.is_empty() {
while let Some((cd_entry, _)) = entries.next_entry().ok()? {
let path = cd_entry.file_path_utf8().ok()?;
if !path.ends_with(".x509.pem") {
// otacerts.zip files only contain files named this way.
trace!("Excluded due to invalid name: {path:?}");
return None;
}
matches += 1;
}
if matches == 0 {
// otacerts.zip files contain at least one cert.
trace!("Zip is empty");
return None;
}
for index in 0..zip_reader.len() {
let entry = zip_reader.by_index_raw(index).ok()?;
if !entry.name().ends_with(".x509.pem") {
// otacerts.zip files only contain files named this way.
trace!("Excluded due to invalid name: {:?}", entry.name());
return None;
}
}
debug!("Found otacerts.zip candidate");
// There's one or more entries and every one is named *.x509.pem.
@@ -96,8 +100,7 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
/// Replace `otacerts.zip` with a new one containing the new certificate, but
/// padded to the same size. If the new zip is too large, the certificate will
/// be modified to remove unnecessary components until it fits. All operations
/// run in parallel where possible. The input and output must refer to the same
/// file and will be reopened from multiple threads.
/// run in parallel where possible.
///
/// Returns two sorted and non-overlapping lists of byte ranges that were
/// modified. The first list are the byte regions within the filesystem data
@@ -108,8 +111,7 @@ fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
/// modified.
#[allow(clippy::type_complexity)]
pub fn patch_system_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
raw_file: &(dyn ReadWriteAt + Sync),
certificate: &Certificate,
key: &RsaSigningKey,
cancel_signal: &AtomicBool,
@@ -122,8 +124,7 @@ pub fn patch_system_image(
let parent_span = Span::current();
let (mut header, footer, image_size) =
avb::load_image(input.reopen_boxed().map_err(Error::ReadData)?)
.map_err(Error::AvbUpdate)?;
avb::load_image(UserPosFile::new(raw_file)).map_err(Error::AvbUpdate)?;
let Some(mut footer) = footer else {
return Err(Error::NoFooter);
};
@@ -144,15 +145,13 @@ pub fn patch_system_image(
let offset = chunk * CHUNK_SIZE;
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
let mut reader = input.reopen_boxed().map_err(Error::ReadData)?;
reader
.seek(SeekFrom::Start(offset))
let mut file = UserPosFile::new(raw_file);
file.seek(SeekFrom::Start(offset))
.map_err(Error::ReadData)?;
let buf = reader
let buf = file
.read_vec_exact(size as usize)
.map_err(Error::ReadData)?;
let mut writer = output.reopen_boxed().map_err(Error::WriteData)?;
let mut ranges = Vec::<Range<u64>>::new();
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
@@ -171,10 +170,9 @@ pub fn patch_system_image(
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
writer
.seek(SeekFrom::Start(bounds.start))
file.seek(SeekFrom::Start(bounds.start))
.map_err(Error::WriteData)?;
writer.write_all(&new_zip).map_err(Error::WriteData)?;
file.write_all(&new_zip).map_err(Error::WriteData)?;
ranges.push(bounds);
}
@@ -190,26 +188,12 @@ 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)
.update(raw_file, update_ranges, cancel_signal)
.map_err(Error::AvbUpdate)?;
if !header.public_key.is_empty() {
@@ -218,8 +202,8 @@ pub fn patch_system_image(
header.sign(key).map_err(Error::AvbUpdate)?;
}
let writer = output.reopen_boxed().map_err(Error::WriteData)?;
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))
let file = UserPosFile::new(raw_file);
avb::write_appended_image(file, &header, &mut footer, Some(image_size))
.map_err(Error::AvbUpdate)?;
let AppendedDescriptorMut::HashTree(descriptor) =
+83
View File
@@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: 2026 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#![allow(clippy::all)]
#![allow(clippy::nursery)]
#![allow(clippy::pedantic)]
@@ -12,3 +16,82 @@ pub mod build {
pub mod chromeos_update_engine {
include!(concat!(env!("OUT_DIR"), "/chromeos_update_engine.rs"));
}
/// Implement [`serde::Serialize`] and [`serde::Deserialize`] for prost enum
/// type and create a bridge to use with `#[serde(with = "...")]` since enum
/// fields are stored as their underlying repr. `$type` must be a fully
/// qualified type.
macro_rules! serde_derive_prost_enum {
($type:ty, $module:ident $(,)?) => {
impl serde::Serialize for $type {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.as_str_name().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for $type {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct EnumVisitor;
impl serde::de::Visitor<'_> for EnumVisitor {
type Value = $type;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a string containing a valid enum variant")
}
fn visit_str<R>(self, v: &str) -> Result<Self::Value, R>
where
R: serde::de::Error,
{
match <$type>::from_str_name(v) {
Some(e) => Ok(e),
None => Err(serde::de::Error::custom(format!(
"invalid enum variant: {v}",
))),
}
}
}
deserializer.deserialize_any(EnumVisitor)
}
}
mod $module {
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: Copy + std::fmt::Display,
$type: TryFrom<T>,
<$type as TryFrom<T>>::Error: std::fmt::Display,
{
let value = <$type>::try_from(*data).map_err(|e| {
serde::ser::Error::custom(format!("invalid enum value: {data}: {e}"))
})?;
serde::Serialize::serialize(&value, serializer)
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
T: From<$type>,
{
let value: $type = serde::Deserialize::deserialize(deserializer)?;
Ok(value.into())
}
}
};
}
serde_derive_prost_enum!(
crate::protobuf::build::tools::releasetools::ota_metadata::OtaType,
ota_type,
);
+315 -200
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},
io::{self, Read, Seek, SeekFrom, Write},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
};
@@ -15,39 +15,32 @@ use ring::digest::Context;
use crate::util;
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
/// is not a valid construct in Rust yet.
pub trait ReadSeek: Read + Seek {}
/// This is only needed because `dyn Read + Seek` is not a valid construct in
/// Rust yet.
pub trait ReadSeek: Read + Seek {
// https://github.com/rust-lang/rust/issues/145752
fn issue_145752(&self) {}
}
impl<R: Read + Seek> ReadSeek for R {}
/// A trait for seekable writers. This is only needed because `dyn Write + Seek`
/// is not a valid construct in Rust yet.
pub trait WriteSeek: Write + Seek {}
/// This is only needed because `dyn Write + Seek` is not a valid construct in
/// Rust yet.
pub trait WriteSeek: Write + Seek {
// https://github.com/rust-lang/rust/issues/145752
fn issue_145752(&self) {}
}
impl<W: Write + Seek> WriteSeek for W {}
/// A trait for seekable and reopenable readers.
pub trait ReadSeekReopen: ReadSeek {
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>>;
/// This is only needed because `dyn Read + Write + Seek` is not a valid
/// construct in Rust yet.
pub trait ReadWriteSeek: ReadSeek + WriteSeek {
// https://github.com/rust-lang/rust/issues/145752
fn issue_145752(&self) {}
}
impl<R: ReadSeek + Reopen + 'static> ReadSeekReopen for R {
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>> {
Ok(Box::new(self.reopen()?))
}
}
/// A trait for seekable and reopenable writers.
pub trait WriteSeekReopen: WriteSeek {
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>>;
}
impl<W: WriteSeek + Reopen + 'static> WriteSeekReopen for W {
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>> {
Ok(Box::new(self.reopen()?))
}
}
impl<W: ReadSeek + WriteSeek> ReadWriteSeek for W {}
/// Common function for reading a structure from a reader.
pub trait FromReader<R: Read>: Sized {
@@ -144,25 +137,121 @@ impl<R: Read> ReadFixedSizeExt for R {
}
}
/// Extensions for file-like types to reopen themselves.
pub trait Reopen: Sized {
/// Open a new handle to the same file. The new handle is independently
/// seekable and the file offset is initially set to 0.
fn reopen(&self) -> io::Result<Self>;
/// Extensions for file-like types to query the file size. No guarantees are
/// made about the state of the underlying file position after performing any
/// operation.
pub trait FileLen {
fn file_len(&self) -> io::Result<u64>;
}
impl<R: Read + Reopen> Reopen for BufReader<R> {
fn reopen(&self) -> io::Result<Self> {
Ok(Self::new(self.get_ref().reopen()?))
macro_rules! file_len_blanket_impl {
($type:ty) => {
impl<F: ?Sized + FileLen> FileLen for $type {
fn file_len(&self) -> io::Result<u64> {
(**self).file_len()
}
}
};
}
file_len_blanket_impl!(&F);
file_len_blanket_impl!(Arc<F>);
file_len_blanket_impl!(Box<F>);
/// Extensions for file-like types that support multi-threaded reads at specific
/// offsets. No guarantees are made about the state of underlying file position
/// after performing any operation.
pub trait ReadAt: FileLen {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize>;
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
let n = self.read_at(buf, offset)?;
if n != buf.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Expected to read {} bytes at {offset}, but reached EOF after {n} bytes",
buf.len(),
),
));
}
Ok(())
}
}
impl<W: Write + Reopen> Reopen for BufWriter<W> {
fn reopen(&self) -> io::Result<Self> {
Ok(Self::new(self.get_ref().reopen()?))
}
macro_rules! read_at_blanket_impl {
($type:ty) => {
impl<R: ?Sized + ReadAt> ReadAt for $type {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
(**self).read_at(buf, offset)
}
fn read_exact_at(&self, buf: &mut [u8], offset: u64) -> io::Result<()> {
(**self).read_exact_at(buf, offset)
}
}
};
}
read_at_blanket_impl!(&R);
read_at_blanket_impl!(Arc<R>);
read_at_blanket_impl!(Box<R>);
/// Extensions for file-like types that support multi-threaded writes at
/// specific offsets. The behavior is unspecified if writes would overlap. No
/// guarantees are made about the state of the underlying file position after
/// performing any operation.
pub trait WriteAt: FileLen {
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize>;
fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
let n = self.write_at(buf, offset)?;
if n != buf.len() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Expected to write {} bytes at {offset}, but reached EOF after {n} bytes",
buf.len(),
),
));
}
Ok(())
}
fn file_flush(&self) -> io::Result<()>;
}
macro_rules! write_at_blanket_impl {
($type:ty) => {
impl<W: ?Sized + WriteAt> WriteAt for $type {
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
(**self).write_at(buf, offset)
}
fn write_all_at(&self, buf: &[u8], offset: u64) -> io::Result<()> {
(**self).write_all_at(buf, offset)
}
fn file_flush(&self) -> io::Result<()> {
(**self).file_flush()
}
}
};
}
write_at_blanket_impl!(&W);
write_at_blanket_impl!(Arc<W>);
write_at_blanket_impl!(Box<W>);
/// This is only needed because `dyn ReadAt + WriteAt` is not a valid construct
/// in Rust yet.
pub trait ReadWriteAt: ReadAt + WriteAt {
// https://github.com/rust-lang/rust/issues/145752
fn issue_145752(&self) {}
}
impl<F: ReadAt + WriteAt> ReadWriteAt for F {}
/// A reader wrapper that implements [`Seek`], but only for reporting the
/// current file position.
pub struct CountingReader<R> {
@@ -320,14 +409,6 @@ impl<R: Read + Seek> SectionReader<R> {
}
}
impl<R: Read + Seek + Reopen> Reopen for SectionReader<R> {
fn reopen(&self) -> io::Result<Self> {
let inner = self.inner.reopen()?;
Self::new(inner, self.start, self.size)
}
}
impl<R: Read + Seek> Read for SectionReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let to_read = self.size.saturating_sub(self.pos).min(buf.len() as u64) as usize;
@@ -370,93 +451,179 @@ impl<R: Read + Seek> Seek for SectionReader<R> {
}
}
/// A file wrapper that uses a userspace file offset. A reopened instance uses
/// the same underlying kernel file descriptor, but a new userspace file offset,
/// initially set to 0.
#[derive(Debug)]
pub struct PSeekFile {
// The lock is needed because flush() takes a `&mut self`.
file: Arc<RwLock<File>>,
offset: u64,
/// A reader wrapper that only allows reading a specific section of a file.
pub struct SectionReaderAt<R> {
inner: R,
start: u64,
size: u64,
}
impl PSeekFile {
pub fn new(file: File) -> Self {
Self {
file: Arc::new(RwLock::new(file)),
offset: 0,
}
impl<R: ReadAt> SectionReaderAt<R> {
pub fn new(inner: R, start: u64, size: u64) -> io::Result<Self> {
Ok(Self { inner, start, size })
}
pub fn set_len(&self, size: u64) -> io::Result<()> {
let file_locked = self.file.read().unwrap();
file_locked.set_len(size)
pub fn into_inner(self) -> R {
self.inner
}
}
impl<R> FileLen for SectionReaderAt<R> {
fn file_len(&self) -> io::Result<u64> {
Ok(self.size)
}
}
impl<R: ReadAt> ReadAt for SectionReaderAt<R> {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
let to_read = self.size.saturating_sub(offset).min(buf.len() as u64) as usize;
self.inner.read_at(&mut buf[..to_read], self.start + offset)
}
}
/// Regular files support parallel reads.
impl ReadAt for File {
/// Read data from offset. The kernel's file position *will* be changed.
#[cfg(windows)]
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
use std::os::windows::fs::FileExt;
self.file.read().unwrap().seek_read(buf, self.offset)
FileExt::seek_read(self, buf, offset)
}
/// Read data from offset. The kernel's file position will *not* be changed.
#[cfg(unix)]
fn read_at(&self, buf: &mut [u8]) -> io::Result<usize> {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
use std::os::unix::fs::FileExt;
self.file.read().unwrap().read_at(buf, self.offset)
FileExt::read_at(self, buf, offset)
}
}
/// Regular files support parallel writes.
impl WriteAt for File {
/// Write data to offset. The kernel's file position *will* be changed.
#[cfg(windows)]
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
use std::os::windows::fs::FileExt;
self.file.read().unwrap().seek_write(buf, self.offset)
FileExt::seek_write(self, buf, offset)
}
/// Write data to offset. The kernel's file position will *not* be changed.
#[cfg(unix)]
fn write_at(&self, buf: &[u8]) -> io::Result<usize> {
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
use std::os::unix::fs::FileExt;
self.file.read().unwrap().write_at(buf, self.offset)
FileExt::write_at(self, buf, offset)
}
fn file_flush(&self) -> io::Result<()> {
(&*self).flush()
}
}
impl Reopen for PSeekFile {
fn reopen(&self) -> io::Result<Self> {
Ok(Self {
file: self.file.clone(),
offset: 0,
})
impl FileLen for File {
fn file_len(&self) -> io::Result<u64> {
(&*self).seek(SeekFrom::End(0))
}
}
impl Read for PSeekFile {
/// A file wrapper that implements [`ReadAt`] and [`WriteAt`] on top of
/// [`Read`], [`Write`], and [`Seek`] via a mutex that makes operations
/// single-threaded. This is the inverse of [`UserPosFile`].
pub struct MutexFile<F>(Mutex<F>);
impl<F> MutexFile<F> {
pub fn new(file: F) -> Self {
Self(Mutex::new(file))
}
pub fn into_inner(self) -> F {
self.0.into_inner().unwrap()
}
}
impl<F: Seek> FileLen for MutexFile<F> {
fn file_len(&self) -> io::Result<u64> {
let mut inner = self.0.lock().unwrap();
inner.seek(SeekFrom::End(0))
}
}
impl<F: Read + Seek> ReadAt for MutexFile<F> {
fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result<usize> {
let mut inner = self.0.lock().unwrap();
let pos = inner.stream_position()?;
inner.seek(SeekFrom::Start(offset))?;
let result = inner.read(buf);
inner.seek(SeekFrom::Start(pos))?;
result
}
}
impl<F: Write + Seek> WriteAt for MutexFile<F> {
fn write_at(&self, buf: &[u8], offset: u64) -> io::Result<usize> {
let mut inner = self.0.lock().unwrap();
let pos = inner.stream_position()?;
inner.seek(SeekFrom::Start(offset))?;
let result = inner.write(buf);
inner.seek(SeekFrom::Start(pos))?;
result
}
fn file_flush(&self) -> io::Result<()> {
let mut inner = self.0.lock().unwrap();
inner.flush()
}
}
/// A file wrapper than implements the standard [`Read`], [`Write`], and
/// [`Seek`] traits on top of [`ReadAt`] and [`WriteAt`]. The file position is
/// unique for every instance, even if the underlying file is shared. This is
/// the inverse of [`MutexFile`].
pub struct UserPosFile<F> {
file: F,
offset: u64,
}
impl<F> UserPosFile<F> {
pub fn new(file: F) -> Self {
Self { file, offset: 0 }
}
}
impl<F: ReadAt> Read for UserPosFile<F> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.read_at(buf)?;
let n = self.file.read_at(buf, self.offset)?;
self.offset += n as u64;
Ok(n)
}
}
impl Write for PSeekFile {
impl<F: WriteAt> Write for UserPosFile<F> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.write_at(buf)?;
let n = self.file.write_at(buf, self.offset)?;
self.offset += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.file.write().unwrap().flush()
self.file.file_flush()
}
}
impl Seek for PSeekFile {
impl<F: FileLen> Seek for UserPosFile<F> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.offset = match pos {
SeekFrom::Start(o) => o,
SeekFrom::End(o) => {
let file_size = self.file.read().unwrap().metadata()?.len();
let file_size = self.file.file_len()?;
file_size
.to_i64()
.and_then(|s| s.checked_add(o))
@@ -485,70 +652,6 @@ impl Seek for PSeekFile {
}
}
/// A small wrapper around a [`Cursor`] that allows multiple instances to share
/// the same underlying file. All reads, writes, and seeks are single-threaded.
/// This is useful for scenarios where data needs to be copied from multiple
/// readers into different parts of the same [`SharedCursor`] writer and the
/// read operation is significantly more expensive than the write operation (eg.
/// due to decompression).
#[derive(Default)]
pub struct SharedCursor {
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
offset: u64,
}
impl SharedCursor {
pub fn new() -> Self {
Self::default()
}
}
impl Reopen for SharedCursor {
fn reopen(&self) -> io::Result<Self> {
Ok(Self {
inner: self.inner.clone(),
offset: 0,
})
}
}
impl Read for SharedCursor {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let mut inner = self.inner.lock().unwrap();
inner.seek(SeekFrom::Start(self.offset))?;
let n = inner.read(buf)?;
self.offset += n as u64;
Ok(n)
}
}
impl Write for SharedCursor {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut inner = self.inner.lock().unwrap();
inner.seek(SeekFrom::Start(self.offset))?;
let n = inner.write(buf)?;
self.offset += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
let mut inner = self.inner.lock().unwrap();
inner.flush()
}
}
impl Seek for SharedCursor {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let mut inner = self.inner.lock().unwrap();
self.offset = inner.seek(pos)?;
Ok(self.offset)
}
}
/// Returns an I/O error with the [`io::ErrorKind::Interrupted`] type if
/// `cancel_signal` is true. This should be called frequently in I/O loops for
/// cancellation to be responsive.
@@ -603,12 +706,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 +727,8 @@ pub fn copy(
break;
}
inspect(&buf[..n]);
writer.write_all(&buf[..n])?;
copied += n as u64;
@@ -630,6 +737,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::{
@@ -639,10 +751,9 @@ mod tests {
use ring::digest::Context;
use super::{
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
Reopen, SectionReader, SharedCursor, WriteZerosExt,
};
use crate::stream::FileLen;
use super::*;
const FOOBAR_SHA256: [u8; 32] = [
0xc3, 0xab, 0x8f, 0xf1, 0x37, 0x20, 0xe8, 0xad, 0x90, 0x47, 0xdd, 0x39, 0x46, 0x6b, 0x3c,
@@ -767,7 +878,7 @@ mod tests {
assert_eq!(&buf[..4], b"nner");
buf = *b"\0\0\0\0\0";
reader.seek(SeekFrom::Current(-5)).unwrap();
reader.seek_relative(-5).unwrap();
reader.read_exact(&mut buf[..3]).unwrap();
assert_eq!(&buf[..3], b"inn");
@@ -776,94 +887,98 @@ mod tests {
}
#[test]
fn pseek_file() {
let raw_file = tempfile::tempfile().unwrap();
let mut a = PSeekFile::new(raw_file);
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
fn section_reader_at() {
let raw_reader = MutexFile::new(Cursor::new(b"fooinnerbar"));
let reader = SectionReaderAt::new(raw_reader, 3, 5).unwrap();
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
b.write_all(b"world").unwrap();
c.seek(SeekFrom::Start(0)).unwrap();
c.write_all(b"hi").unwrap();
let mut buf = [0u8; 5];
reader.read_exact_at(&mut buf[3..5], 3).unwrap();
reader.read_exact_at(&mut buf[..3], 0).unwrap();
assert_eq!(&buf, b"inner");
let mut buf = [0u8; 11];
a.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hillorworld");
let n = a.read_discard(1).unwrap();
let n = reader.read_at(&mut buf, 5).unwrap();
assert_eq!(n, 0);
}
#[test]
fn shared_cursor() {
let mut a = SharedCursor::default();
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
fn mutex_file() {
let file = MutexFile::new(Cursor::new(Vec::new()));
assert_eq!(file.file_len().unwrap(), 0);
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
b.write_all(b"world").unwrap();
c.seek(SeekFrom::Start(0)).unwrap();
c.write_all(b"hi").unwrap();
file.write_all_at(b"bar", 3).unwrap();
assert_eq!(file.file_len().unwrap(), 6);
let mut buf = [0u8; 11];
a.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hillorworld");
file.write_all_at(b"foo", 0).unwrap();
assert_eq!(file.file_len().unwrap(), 6);
let n = a.read_discard(1).unwrap();
assert_eq!(n, 0);
let data = file.into_inner().into_inner();
assert_eq!(data, b"foobar");
}
#[test]
fn copy() {
fn user_pos_file() {
let mut raw_file = tempfile::tempfile().unwrap();
raw_file.write_all(b"foobar").unwrap();
let mut file = UserPosFile::new(raw_file);
let mut buf = [0u8; 3];
file.rewind().unwrap();
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"foo");
let pos = file.seek(SeekFrom::End(-3)).unwrap();
assert_eq!(pos, 3);
file.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"bar");
}
#[test]
fn copy_functions() {
let cancel_signal = AtomicBool::new(false);
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();
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 = 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 = copy_n(&mut reader, &mut writer, 6, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
reader.rewind().unwrap();
writer.rewind().unwrap();
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
let n = copy(&mut reader, &mut writer, &cancel_signal).unwrap();
assert_eq!(n, 6);
assert_eq!(writer.get_ref(), b"foobar");
// Reader early EOF.
reader.seek(SeekFrom::Start(3)).unwrap();
writer.rewind().unwrap();
let n = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap();
let n = copy(&mut reader, &mut writer, &cancel_signal).unwrap();
assert_eq!(n, 3);
// Writer early EOF.
reader.rewind().unwrap();
writer.seek(SeekFrom::Start(3)).unwrap();
let err = super::copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
let err = copy(&mut reader, &mut writer, &cancel_signal).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WriteZero);
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 = 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();
let err = 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::*;
+16 -14
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
@@ -18,7 +18,7 @@ use avbroot::{
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
KernelCmdlineDescriptor, PropertyDescriptor,
},
stream::SharedCursor,
stream::{MutexFile, UserPosFile},
};
fn get_test_key() -> RsaSigningKey {
@@ -306,16 +306,17 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let writer = MutexFile::new(Cursor::new(Vec::new()));
let mut pos_writer = UserPosFile::new(&writer);
let cancel_signal = AtomicBool::new(false);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
pos_writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
d.update(&writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
@@ -334,10 +335,10 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
avb::write_appended_image(&mut pos_writer, &header, &mut footer, Some(image_size)).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
pos_writer.rewind().unwrap();
pos_writer.read_to_end(&mut data).unwrap();
// Verify checksum of the output.
assert_eq!(
@@ -413,16 +414,17 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut writer = SharedCursor::default();
let writer = MutexFile::new(Cursor::new(Vec::new()));
let mut pos_writer = UserPosFile::new(&writer);
let cancel_signal = AtomicBool::new(false);
// Write the raw partition data.
writer.write_all(&raw_data).unwrap();
pos_writer.write_all(&raw_data).unwrap();
// Generate and write the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.update(&writer, &writer, None, &cancel_signal).unwrap();
d.update(&writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
@@ -441,10 +443,10 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Write vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
avb::write_appended_image(&mut pos_writer, &header, &mut footer, None).unwrap();
let mut data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut data).unwrap();
pos_writer.rewind().unwrap();
pos_writer.read_to_end(&mut data).unwrap();
// Verify checksum of the output.
assert_eq!(
+9 -11
View File
@@ -34,20 +34,13 @@ allow = [
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"bzip2-1.0.6",
"GPL-3.0",
"GPL-3.0-only",
"ISC",
"MIT",
"Unicode-3.0",
"Zlib",
]
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[bans]
multiple-versions = "warn"
multiple-versions-include-dev = true
@@ -61,13 +54,18 @@ executables = "deny"
include-dependencies = true
include-workspace = true
bypass = [
# Copies of unmodified crashwrangler objects for old macOS versions.
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
# honggfuzz/third_party/mac/CrashReport_*.o are copies of unmodified
# crashwrangler objects for old macOS versions. Unfortunately, when running
# cargo hfuzz, the compiled hongfuzz binary and corresponding object files
# are written to the source directory too.
{ name = "honggfuzz", allow-globs = ["honggfuzz/*.o", "honggfuzz/honggfuzz"] },
# 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",
]
+4 -11
View File
@@ -4,7 +4,8 @@ version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
publish = false
publish.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,24 +15,16 @@ avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
rawzip = "0.4.0"
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 = { version = "1.0.1", 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
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
[features]
static = ["avbroot/static"]
[lints]
workspace = true
+30 -25
View File
@@ -12,8 +12,11 @@ security_patch_level = "2024-01-01"
# Google Pixel 7 Pro
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
[profile.pixel_v4_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_gki.vabc]
# CoW v3 is used starting with the Google Pixel 9a.
version = { V3 = { compression_factor = 65536 } }
algo = { kind = "Lz4" }
force_compression_factor = false
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
@@ -49,18 +52,21 @@ data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes_streaming]
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
patched = "6c27ffb07f4497af8539f8283e506066af9417580230c3209c9875fc15d5069d"
original = "ea96196191e3a4133db4aff45d47aa3468514e29e0a724faac8081cbf4adf808"
patched = "c073cbb271099aaf338a8c70f3565ec70db47a87b4c9bb3efffd96ccee1a0c27"
[profile.pixel_v4_gki.hashes_seekable]
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
patched = "e4fc12523ffc312796b92210bc1e3bbb70dd60797a47daae76c8b5852e48b382"
original = "f6615ae355eba38689d24aa535981d09175d4832e7c65c04cdd89aa95d21f09d"
patched = "65ce91a95574a308ffc5279696efe7f6f7e4e847795dc9ae43d9ffd012a7ce3f"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[profile.pixel_v4_non_gki]
vabc_algo = "Lz4"
[profile.pixel_v4_non_gki.vabc]
version = "V2"
algo = { kind = "Lz4" }
# delta_generator sets it for v2, even though it's not used.
force_compression_factor = true
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
@@ -90,18 +96,20 @@ data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes_streaming]
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
patched = "6832ded3e98a14edc8c5ea7284fcea0b958fa710ebf222c27116faec8dfefe2e"
original = "cb2a2e406d2b4c68c8a38f819256a10ada0ed5a4732822bec35bdf7bcdc458eb"
patched = "45cbe96e01b8b2e3ba5e4a2c990e7ff08bee285316a4a7a314aab3ffddf27d0d"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
patched = "114f8936e917d7e4a71bc1521adb3c8e676de3a738f8c7c505b86464d20bd95c"
original = "9450c212c34fe55453b52345c7cc22f72397dc80d9ddc792eafc20b461c2afc9"
patched = "111fdfae91977d80cd141f6c3dc69f3af45bb7fcdf3e2e442bc3f0ad5fa77faa"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[profile.pixel_v3]
vabc_algo = "Lz4"
[profile.pixel_v3.vabc]
version = "V2"
algo = { kind = "Gz" }
force_compression_factor = false
[profile.pixel_v3.partitions.boot]
avb.signed = true
@@ -132,19 +140,16 @@ data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes_streaming]
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
patched = "1f28d9210a17e233cd5da4af55b07db764b19eeab991394514170b405240464f"
original = "4b864b906a13ec98b1d6c3440a5eca7404ec63d073080e80a3b84afabcbd2fa2"
patched = "da3b9a4d6a58bb5aaadfc63006cb36b7af9c1c5945e81b38587c245bd5ad1f5a"
[profile.pixel_v3.hashes_seekable]
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
patched = "27b80c7be9c1e527ea26abe3dabde245c580e6f26ec084204278fbfd81a39f83"
original = "e0e93ee11de56992f3a5f543858c1d11858b4deb76a91a4caf4fb0f3f34be855"
patched = "71682a4f31a5920784a0e2e38a6ee5fe3c65213436c7bf1ce347b121cd01a367"
# Google Pixel 4a
# What's unique: boot (boot v2)
[profile.pixel_v2]
vabc_algo = "Gz"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
@@ -168,9 +173,9 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes_streaming]
original = "bd2f19cf3d2285e35e8b36d44f75ed910e8e0be44c3ebd29f17a812521ba754b"
patched = "cf65d5b90500af54cd1204a646379bb852825061bc7c3f973b7a042f353f75ad"
original = "abc0ad7f80020018101437c2494d6564a877df663d208a123267fc100734c175"
patched = "132fb4d90e3c19a7a5ee0da2082767228255228ea5fb66811df1ccd5ff600bb5"
[profile.pixel_v2.hashes_seekable]
original = "7f96ebf7366e0b60c91ac1e5f196a2189ffdb0bbc73f77804a736466fcab7315"
patched = "c2d9d60d73c038da39f82073ffadb459c96b901d66db7af11f59da58e0dd53e4"
original = "57d4ac5ab7d362a593f3c02f3d2044b5400c10db0f732741a2d18e025d4231ec"
patched = "b365eb32f463ac4ba13f7bed4e6535a68483379640dc6e842c591f194db042c2"
+24 -21
View File
@@ -1,14 +1,13 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use avbroot::format::payload::VabcAlgo;
use avbroot::format::payload::{CowVersion, VabcAlgo};
use serde::{Deserialize, Serialize};
use toml_edit::DocumentMut;
#[derive(Serialize, Deserialize)]
#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Sha256Hash(
#[serde(
serialize_with = "hex::serialize",
@@ -17,7 +16,7 @@ pub struct Sha256Hash(
pub [u8; 32],
);
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OtaInfo {
pub device: String,
@@ -29,7 +28,7 @@ pub struct OtaInfo {
pub security_patch_level: String,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Avb {
pub signed: bool,
@@ -55,7 +54,7 @@ pub enum BootVersion {
VendorV4,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BootData {
pub version: BootVersion,
@@ -71,19 +70,19 @@ pub enum DmVerityContent {
SystemOtacerts,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DmVerityData {
pub content: DmVerityContent,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VbmetaData {
pub deps: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Data {
Boot(BootData),
@@ -91,30 +90,38 @@ pub enum Data {
Vbmeta(VbmetaData),
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hashes {
pub original: Sha256Hash,
pub patched: Sha256Hash,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Partition {
pub avb: Avb,
pub data: Data,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Copy, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VabcSettings {
pub version: CowVersion,
pub algo: VabcAlgo,
pub force_compression_factor: bool,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub vabc_algo: Option<VabcAlgo>,
pub vabc: Option<VabcSettings>,
pub partitions: BTreeMap<String, Partition>,
pub hashes_streaming: Hashes,
pub hashes_seekable: Hashes,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub ota_info: OtaInfo,
@@ -122,12 +129,8 @@ pub struct Config {
pub profile: BTreeMap<String, Profile>,
}
pub fn load_config(path: &Path) -> Result<(Config, DocumentMut)> {
pub fn load_config(path: &Path) -> Result<Config> {
let contents =
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
let config: Config = toml_edit::de::from_str(&contents)
.with_context(|| format!("Failed to parse config: {path:?}"))?;
let document: DocumentMut = contents.parse().unwrap();
Ok((config, document))
toml::de::from_str(&contents).with_context(|| format!("Failed to parse config: {path:?}"))
}
+140 -109
View File
@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023-2026 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
// SPDX-License-Identifier: GPL-3.0-only
@@ -14,12 +14,12 @@ use std::{
path::{Path, PathBuf},
slice,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{Context, Result, anyhow, bail};
use avbroot::{
cli::ota::{ExtractCli, PatchCli, VerifyCli},
crypto::{self, PassphraseSource, RsaSigningKey},
@@ -36,24 +36,26 @@ use avbroot::{
cpio::{self, CpioEntry, CpioEntryData},
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, PayloadHeader, PayloadWriter},
payload::{self, CowVersion, PayloadHeader, PayloadWriter, VabcParams},
zip,
},
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},
stream::{self, FromReader, HashingReader, ToWriter},
util,
};
use clap::Parser;
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
use rawzip::{CompressionMethod, ZipArchiveWriter};
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 crate::{
cli::{Cli, Command, HelperCli, ListCli, PassSource, ProfileGroup, TestCli},
@@ -66,11 +68,11 @@ use crate::{
fn hash_file(path: &Path, cancel_signal: &AtomicBool) -> Result<[u8; 32]> {
info!("Calculating hash: {path:?}");
let raw_reader =
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))?;
let buf_reader = BufReader::new(raw_reader);
let raw_reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let context = ring::digest::Context::new(&ring::digest::SHA256);
let mut hashing_reader = HashingReader::new(buf_reader, context);
let mut hashing_reader = HashingReader::new(raw_reader, context);
stream::copy(&mut hashing_reader, io::sink(), cancel_signal)?;
@@ -95,15 +97,15 @@ fn verify_hash(path: &Path, sha256: &[u8; 32], cancel_signal: &AtomicBool) -> Re
}
fn append_avb(
file: &mut PSeekFile,
file: &File,
name: &str,
avb: &Avb,
avb: Avb,
hash_tree: bool,
ota_info: &OtaInfo,
key_avb: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<()> {
let image_size = file.seek(SeekFrom::End(0))?;
let image_size = (&*file).seek(SeekFrom::End(0))?;
let salt = ring::digest::digest(&ring::digest::SHA256, b"avbroot");
let descriptors = vec![
if hash_tree {
@@ -125,7 +127,7 @@ fn append_avb(
reserved: [0u8; 60],
};
descriptor.update(file, file, None, cancel_signal)?;
descriptor.update(file, None, cancel_signal)?;
Descriptor::HashTree(descriptor)
} else {
@@ -139,8 +141,8 @@ fn append_avb(
reserved: [0u8; 60],
};
file.rewind()?;
descriptor.update(&mut *file, cancel_signal)?;
(&*file).rewind()?;
descriptor.update(file, cancel_signal)?;
Descriptor::Hash(descriptor)
},
@@ -188,7 +190,7 @@ fn append_avb(
reserved: Default::default(),
};
let eof_size = file.seek(SeekFrom::End(0))?;
let eof_size = (&*file).seek(SeekFrom::End(0))?;
let full_image_size = eof_size
.checked_add(8192)
.and_then(|s| padding::round(s, 4096))
@@ -293,9 +295,9 @@ fn create_ramdisk(
#[allow(clippy::too_many_arguments)]
fn create_boot_image(
file: &mut PSeekFile,
file: &File,
name: &str,
avb: &Avb,
avb: Avb,
boot_data: &BootData,
ota_info: &OtaInfo,
key_avb: &RsaSigningKey,
@@ -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(),
@@ -410,7 +412,7 @@ fn create_boot_image(
}
};
boot_image.to_writer(&mut *file)?;
boot_image.to_writer(file)?;
append_avb(file, name, avb, false, ota_info, key_avb, cancel_signal)
.with_context(|| format!("Failed to append AVB metadata for {name}"))?;
@@ -420,10 +422,10 @@ fn create_boot_image(
#[allow(clippy::too_many_arguments)]
fn create_dm_verity_image(
file: &mut PSeekFile,
file: &File,
name: &str,
avb: &Avb,
dm_verity_data: &DmVerityData,
avb: Avb,
dm_verity_data: DmVerityData,
ota_info: &OtaInfo,
key_avb: &RsaSigningKey,
cert_ota: &Certificate,
@@ -431,16 +433,16 @@ fn create_dm_verity_image(
) -> Result<()> {
match dm_verity_data.content {
DmVerityContent::SystemOtacerts => {
file.write_all(b"arbitrary_prefix")?;
(&*file).write_all(b"arbitrary_prefix")?;
let data = otacert::create_zip(cert_ota, OtaCertBuildFlags::empty())?;
file.write_all(&data)?;
(&*file).write_all(&data)?;
file.write_all(b"arbitrary_suffix")?;
(&*file).write_all(b"arbitrary_suffix")?;
}
}
padding::write_zeros(&mut *file, 4096)?;
padding::write_zeros(file, 4096)?;
append_avb(file, name, avb, true, ota_info, key_avb, cancel_signal)
.with_context(|| format!("Failed to append AVB metadata for {name}"))?;
@@ -449,19 +451,18 @@ fn create_dm_verity_image(
}
fn create_vbmeta_image(
file: &mut PSeekFile,
file: &File,
name: &str,
avb: &Avb,
avb: Avb,
vbmeta_data: &VbmetaData,
inputs: &BTreeMap<String, PSeekFile>,
inputs: &BTreeMap<String, File>,
key: &RsaSigningKey,
) -> Result<()> {
let mut descriptors = Vec::new();
for dep in &vbmeta_data.deps {
let reader = inputs[dep].reopen()?;
let (child_header, _, _) =
avb::load_image(reader).with_context(|| format!("Failed to parse AVB image: {dep}"))?;
let (child_header, _, _) = avb::load_image(&inputs[dep])
.with_context(|| format!("Failed to parse AVB image: {dep}"))?;
if child_header.public_key.is_empty() {
descriptors.extend(child_header.descriptors);
@@ -509,7 +510,7 @@ fn create_partition_images(
key_avb: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<BTreeMap<String, PSeekFile>> {
) -> Result<BTreeMap<String, File>> {
let mut topo = TopologicalSort::<&String>::new();
for (name, partition) in partitions {
@@ -528,16 +529,15 @@ fn create_partition_images(
};
let partition = &partitions[name];
let mut file = tempfile::tempfile()
.map(PSeekFile::new)
let file = tempfile::tempfile()
.with_context(|| format!("Failed to create temp file for {name}"))?;
match &partition.data {
Data::Boot(data) => {
create_boot_image(
&mut file,
&file,
name,
&partition.avb,
partition.avb,
data,
ota_info,
key_avb,
@@ -548,10 +548,10 @@ fn create_partition_images(
}
Data::DmVerity(data) => {
create_dm_verity_image(
&mut file,
&file,
name,
&partition.avb,
data,
partition.avb,
*data,
ota_info,
key_avb,
cert_ota,
@@ -560,7 +560,7 @@ fn create_partition_images(
.with_context(|| format!("Failed to create dm-verity image: {name}"))?;
}
Data::Vbmeta(data) => {
create_vbmeta_image(&mut file, name, &partition.avb, data, &files, key_avb)
create_vbmeta_image(&file, name, partition.avb, data, &files, key_avb)
.with_context(|| format!("Failed to create vbmeta image: {name}"))?;
}
}
@@ -574,7 +574,7 @@ fn create_partition_images(
fn create_payload(
writer: impl Write,
partitions: &BTreeMap<String, Partition>,
inputs: &BTreeMap<String, PSeekFile>,
inputs: &BTreeMap<String, File>,
ota_info: &OtaInfo,
profile: &Profile,
key_ota: &RsaSigningKey,
@@ -587,24 +587,30 @@ fn create_payload(
.collect::<Vec<_>>();
let mut payload_partitions = vec![];
let mut compressed = BTreeMap::<&String, PSeekFile>::new();
let mut compressed = BTreeMap::<&String, File>::new();
for (name, file) in inputs {
let writer = tempfile::tempfile()
.map(PSeekFile::new)
.with_context(|| format!("Failed to create temp file for: {name}"))?;
let vabc_algo = if dynamic_partitions_names.contains(name) {
profile.vabc_algo
let vabc_params = if dynamic_partitions_names.contains(name) {
profile.vabc.map(|v| VabcParams {
version: v.version,
algo: v.algo,
})
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(file, &writer, name, 4096, vabc_algo, cancel_signal)?;
payload::compress_image(file, &writer, name, 4096, vabc_params, cancel_signal)?;
compressed.insert(name, writer);
let is_v3 = profile
.vabc
.is_some_and(|e| matches!(e.version, CowVersion::V3 { .. }));
payload_partitions.push(PartitionUpdate {
partition_name: name.clone(),
run_postinstall: None,
@@ -624,8 +630,8 @@ fn create_payload(
fec_roots: None,
version: None,
merge_operations: vec![],
estimate_cow_size: cow_estimate,
estimate_op_count_max: None,
estimate_cow_size: cow_estimate.map(|e| e.size),
estimate_op_count_max: cow_estimate.and_then(|e| is_v3.then_some(e.num_ops)),
});
}
@@ -645,11 +651,19 @@ fn create_payload(
partition_names: dynamic_partitions_names,
}],
snapshot_enabled: Some(true),
vabc_enabled: Some(true),
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
cow_version: Some(2),
// Everything below is meant to be unset if VABC is not
// supported.
vabc_enabled: profile.vabc.map(|_| true),
vabc_compression_param: profile.vabc.map(|v| v.algo.to_string()),
cow_version: profile.vabc.map(|v| match v.version {
CowVersion::V2 => 2,
CowVersion::V3 { .. } => 3,
}),
vabc_feature_set: None,
compression_factor: None,
compression_factor: profile.vabc.and_then(|v| match v.version {
CowVersion::V2 => v.force_compression_factor.then_some(64 * 1024),
CowVersion::V3 { compression_factor } => Some(compression_factor.into()),
}),
}),
partial_update: None,
apex_info: vec![],
@@ -690,11 +704,11 @@ fn create_payload(
.with_context(|| format!("Failed to copy from image: {name}"))?;
}
let (_, _, properties, metadata_size) = payload_writer
let (_, header, properties) = payload_writer
.finish()
.context("Failed to finalize payload")?;
Ok((properties, metadata_size))
Ok((properties, header.blob_offset))
}
#[allow(clippy::too_many_arguments)]
@@ -725,42 +739,32 @@ fn create_ota(
.truncate(true)
.open(output)
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
let mut zip_writer = match zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(raw_writer);
ZipWriter::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(raw_writer);
ZipWriter::new(signing_writer)
}
let signing_writer = match zip_mode {
ZipMode::Streaming => SigningWriter::new_streaming(raw_writer),
ZipMode::Seekable => SigningWriter::new_seekable(raw_writer),
};
let options = FileOptions::default()
.compression_method(CompressionMethod::Stored)
.large_file(false);
let mut zip_writer = ZipArchiveWriter::new(signing_writer);
let mut entries = vec![];
let mut properties = None;
let mut payload_metadata_size = None;
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)
let (entry_writer, data_config) = zip_writer
.new_file(path)
.start()
.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}"))?;
let mut writer = CountingWriter::new(&mut zip_writer);
let offset = entry_writer.stream_offset();
let mut data_writer = data_config.wrap(entry_writer);
match path {
ota::PATH_OTACERT => {
crypto::write_pem_cert(Path::new(path), &mut writer, cert_ota)
crypto::write_pem_cert(Path::new(path), &mut data_writer, cert_ota)
.with_context(|| format!("Failed to write entry: {path}"))?;
}
ota::PATH_PAYLOAD => {
let (p, m) = create_payload(
&mut writer,
&mut data_writer,
&profile.partitions,
&inputs,
ota_info,
@@ -774,18 +778,20 @@ fn create_ota(
payload_metadata_size = Some(m);
}
ota::PATH_PROPERTIES => {
writer
data_writer
.write_all(properties.as_ref().unwrap().as_bytes())
.with_context(|| format!("Failed to write payload properties: {path}"))?;
}
_ => unreachable!(),
}
// Cannot fail.
let size = writer.stream_position()?;
let size = data_writer
.finish()
.and_then(|(w, d)| w.finish(d))
.with_context(|| format!("Failed to finalize zip entry: {path}"))?;
entries.push(ZipEntry {
name: path.to_owned(),
path: path.to_owned(),
offset,
size,
});
@@ -819,18 +825,15 @@ fn create_ota(
spl_downgrade: false,
};
let data_descriptor_size = match zip_mode {
ZipMode::Streaming => 16,
ZipMode::Seekable => 0,
};
let next_offset = zip_writer.stream_offset();
ota::add_metadata(
&entries,
&mut zip_writer,
// Offset where next entry would begin.
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
next_offset,
&metadata,
payload_metadata_size.unwrap(),
zip_mode,
)
.context("Failed to write new OTA metadata")?;
@@ -848,12 +851,19 @@ fn create_ota(
}
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 raw_writer = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(output)
.with_context(|| format!("Failed to open for writing: {output:?}"))?;
let mut zip_writer = ZipArchiveWriter::new(raw_writer);
let compression_method = CompressionMethod::Deflate;
for path in [
"assets/stub.apk",
"assets/util_functions.sh",
"lib/arm64-v8a/libinit-ld.so",
"lib/arm64-v8a/libmagisk64.so",
"lib/arm64-v8a/libmagiskinit.so",
@@ -867,13 +877,31 @@ fn create_fake_magisk(output: &Path) -> Result<()> {
"lib/x86_64/libmagisk64.so",
"lib/x86_64/libmagiskinit.so",
] {
zip_writer.start_file(path, FileOptions::default())?;
write!(zip_writer, "dummy contents for {path}")?;
let (entry_writer, data_config) = zip_writer
.new_file(path)
.compression_method(compression_method)
.start()
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let compressed_writer = zip::compressed_writer(entry_writer, compression_method)
.with_context(|| format!("Failed to begin new zip entry: {path}"))?;
let mut data_writer = data_config.wrap(compressed_writer);
if path == "assets/util_functions.sh" {
// avbroot looks for the version number in this file.
data_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
} else {
write!(data_writer, "dummy contents for {path}")?;
}
data_writer
.finish()
.and_then(|(w, d)| w.finish()?.finish(d))?;
}
// avbroot looks for the version number in this file.
zip_writer.start_file("assets/util_functions.sh", FileOptions::default())?;
zip_writer.write_all(b"MAGISK_VER_CODE=27000\n")?;
let raw_writer = zip_writer.finish()?;
zip::make_non_streaming(raw_writer)
.with_context(|| format!("Failed to convert to non-streaming zip: {output:?}"))?;
Ok(())
}
@@ -1039,6 +1067,8 @@ fn patch_image(
OsStr::new("--replace"),
OsStr::new("system"),
system_image_file.as_os_str(),
OsStr::new("--re-sign"),
OsStr::new("boot"),
OsStr::new("--key-avb"),
avb_key_file.as_os_str(),
OsStr::new("--pass-avb-file"),
@@ -1127,7 +1157,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 = ZipArchiveWriter::new(Cursor::new(Vec::new()));
let empty_zip = zip_writer.finish()?.into_inner();
entry.data = CpioEntryData::Data(empty_zip);
@@ -1175,7 +1205,7 @@ fn filter_profiles<'a>(config: &'a Config, cli: &'a ProfileGroup) -> Result<BTre
}
fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
let (config, _) = config::load_config(&cli.config.config)?;
let config = config::load_config(&cli.config.config)?;
let profiles = filter_profiles(&config, &cli.profile)?;
if profiles.is_empty() {
@@ -1189,6 +1219,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
Some(_) => None,
None => Some(TempDir::new().context("Failed to create temp directory")?),
};
#[allow(clippy::option_if_let_else)]
let work_dir = match &cli.config.work_dir {
Some(w) => w.as_path(),
None => work_temp_dir.as_ref().unwrap().path(),
@@ -1208,10 +1239,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 [
@@ -1220,8 +1247,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");
@@ -1310,7 +1338,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
}
fn list_subcommand(cli: &ListCli) -> Result<()> {
let (config, _) = config::load_config(&cli.config.config)?;
let config = config::load_config(&cli.config.config)?;
for profile in config.profile.keys() {
println!("{profile}");
@@ -1324,7 +1352,7 @@ fn helper_mode() -> Result<()> {
let cli = HelperCli::parse();
let private_key_path = {
let parent = cli.public_key.parent().unwrap_or(Path::new("."));
let parent = cli.public_key.parent().unwrap_or_else(|| Path::new("."));
let name = cli
.public_key
.file_name()
@@ -1398,7 +1426,10 @@ fn main() -> Result<()> {
if env::var_os(ENV_HELPER_MODE).is_some() {
return helper_mode();
}
env::set_var(ENV_HELPER_MODE, "true");
// SAFETY: No multithreading at this point.
unsafe {
env::set_var(ENV_HELPER_MODE, "true");
}
// Set up a cancel signal so we can properly clean up any temporary files.
let cancel_signal = Arc::new(AtomicBool::new(false));
+1
View File
@@ -1,4 +1,5 @@
/hfuzz_target/
/hfuzz_workspace/
HONGGFUZZ.REPORT.TXT
*.honggfuzz.cov
*.fuzz
+2 -1
View File
@@ -4,7 +4,8 @@ version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
publish = false
publish.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+9 -3
View File
@@ -10,7 +10,13 @@ While avbroot's parsers are all memory-safe, it is still possible for panics or
cargo install honggfuzz
```
2. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension.
2. [Optional] Generate sample files to use as the initial fuzzing corpus.
```bash
cargo xtask fuzz-corpus
```
3. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension.
The list of targets can be queried programmatically with:
@@ -18,7 +24,7 @@ While avbroot's parsers are all memory-safe, it is still possible for panics or
cargo read-manifest | jq -r '.targets[].name'
```
3. Run the fuzzer.
4. Run the fuzzer.
```bash
cargo hfuzz run <fuzz target>
@@ -41,7 +47,7 @@ While avbroot's parsers are all memory-safe, it is still possible for panics or
When a crash occurs, the `Crashes` counter will increment and the input data that triggered the crash will be written to `hfuzz_workspace/<fuzz target>/*.fuzz`. New files are only written for unique crashes.
4. If a crash occurs, run the following command to trigger the crash in a debugger.
5. If a crash occurs, run the following command to trigger the crash in a debugger.
```bash
cargo hfuzz run-debug <fuzz target> \
@@ -0,0 +1,38 @@
image_size = 12288
[header]
required_libavb_version_major = 1
required_libavb_version_minor = 0
algorithm_type = "Sha256Rsa4096"
hash = "3035539ec16eb6e6645a9022dd62379bf448cc53178b91fb68507eb6565b5478"
signature = "25f59424238d43e8e247583f276d45caeed1396e5d6298c4f4f5db4bdb74453ece1783143900157b35bdd99fe29123fe98f0876663b1c985a1426bdfbdf0b31b710ebe54cbb759d4eb1aa6ecc6964c5bccd364f1a9359e53d79797ec0bdc8e453a9a6c559e95ab4d8309422ad182bdc210b3d815b277b2fc8c827ff5849997ca9cf4fcb9fae69902ba865478401c2a4976f07499f27e8f8e3e609679dd32b495c27dfe9dae4de6eb7e9a86abe30da6f0e5454f85cf170eb3ea75250d697770d0028c78c0be12280b7599019ac03a05612384f81263821b8d3cbd902e4c2b6442afed2fb7800dfc4be999f77274afa4d97512147b8ccf791e59a91e75deebd4469538d71bc9272e4c7fbbc657f3d269ec269a617314a8ceb4a4920f9654fd549c8def8a3d5a54c38871cb822af8733a575f69a187717debca30884872206f04cb98aade78c765007e2d9b024185f153ec769de1684e98b0ce41fd0dbefd9aca814887ee0917f067708c66f1e5586c578f3a4f4148f71e5ce7dd84f150b99b1547e3ca6c041fafd085bb987f16a6056a4c28875d151872bda5e316b08bb43cfb2db78759b50e4cf016f0b0b0118e03ee25508da5ce9eee77f97fdeab0831a55c43760d1ba0b4cd0c8570840549f356f32c0abf8666daf478a4cad9b84cda52430137ad12ee15425a0bf948e362c6f2da0f4ff3c57683e0b8760a014e1dbd936c11"
public_key = "0000100080af29ebacee1fc0a795d91674aaeb4f55f4d36ba6f899ddb543be5d1cc878d45d776fe6211a330c0843322334b690dbbb9dc13fa47642abc75889dbba79c82a9dba19c64d4c099f36240775834e9d86013f433a1aaab0bed1f2212dc53cf2d967ded53209c1e5eb5b563d00d307c021b9d2d084e80cdd20a695225ce31e7fb407ccebdec5cda2eb44ed079df3edf31751e77ce43ae9681bc7f6cc4e58945366bb655550e07dd215eb516815e9457feef9301b772f92291fd5758782f20734b9ac75e7469f42ffd4055ae43a8301a37e56dfda4ede5e639fec57ab7268d8787190268952a27e6b8e3ffc503759af317da4a076fd36fe6499f15de27d81dca566f500838d13a442b01ecd1ffdb655ea2ef779bab13cd8eab649402cc1a3f81444ed24717a7c3fa65ac2b8d3625450aae15b75d3dc37e962f516799cb555063eeb262bb51f9af465fc992e07f3aa66917ce086abd9a60716f04fe155efe7901e4132a4dcdcdcf7db23e4eade6b3664bee87a7fd4c6b3247beeb0553263845d6547efda705f58cab911d9a98fb7cdd807bfdbf3dbf1f81ac3e837ba75ea5512be7246bfe768aa1edf23c469db87df980bbb939fd1f28948340882350e4c58f0450329f8088da6b908b4c4c78689eb18b3c06b683b12e518bde5cd57e7fb43792df94c38f643675e5c92b1c0a95f45256969a4bc6ea1db6682745d423cc676f97f57cf2d493d850bfba9362a858faa355e2bd0c9b1a46f6581c9c0751399788bd37fc5e597a402a3bc05bb5959677a48ce4aa9fa6ed5add36a7e11117152610b0e8f86692669825206eb5d297d7f77d114bc867736b6d6eb4309329569d78dfb910cb66132c5a3f415bd1ededfb61fd58c958db4c7a2c17578f537d24b5405e545d1618ae58be3525c9659d6baf2634255e7ca26fcfde1cbc4401e8ca062babede5f5b3f2ad5cbecd3d9332098f402666a9ef85643e3f4ab214a67a5ca91f31f810eab89df57d2f9ef35e9a5f46a99228af059cdd1f0685f1b9405beb0e44fe7f590b50feea1554e8bdaf7be7fd22e6f050a7751731cd7f8737b5acb92378da9d59068106da25b30882abad4629dff6ca9c4c0cf5337bf2d2a9e8c6182dcd6d7c681da01df6c11d0b335cf1f982156393c4e572df540cee2c53a2c146dec9644be4e81cce20df4cb136b3a08fad9a186be892d6653330d11cac1e96d308fd0d2896c8a141edfbdcac827cdf97ac581f493721c9aa1305667df312277c8ab6700c3a978e6f9d00b389faaeaa99b5e023b72c5f5ce885fa2ba816ffc735599fad1e3876d91a501efbbf1315f42a503ddd7a26a784a3f290a86f43dd580c03fc4125716d6b534e35daf1156fcaec37ac8bc9e881d5ec1715632ff82b4dc4e3aabf48c5db6f3608ce2b8bb48bceae621b07f231254dd6f1f7b4584e7102969f523252a6839dca065"
public_key_metadata = "000102030405060708090a0b0c0d0e0f"
rollback_index = 1677974400
flags = 0
rollback_index_location = 0
release_string = "MaxLengthMaxLengthMaxLengthMaxLengthMaxLengthMa"
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[header.descriptors]]
type = "Property"
key = "foobar"
value = 'Invalid\x20UTF-8:\x20\xFF'
[[header.descriptors]]
type = "Hash"
image_size = 6
hash_algorithm = "sha256"
partition_name = "vbmeta_appended_hash"
salt = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
root_digest = "2454ef9d58f39d544eb2d92294131c0b3b1473a276418d0b1362693fdafa685b"
flags = 0
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567"
[footer]
version_major = 1
version_minor = 0
original_image_size = 6
vbmeta_offset = 4096
vbmeta_size = 2176
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef01234567"
@@ -0,0 +1 @@
foobar
@@ -0,0 +1,46 @@
image_size = 28672
[header]
required_libavb_version_major = 1
required_libavb_version_minor = 0
algorithm_type = "Sha256Rsa4096"
hash = "2bf01b15b31a729ca8053bc1303b0a4a638ee4819cc3106fd36ff996b703e249"
signature = "8c4f1daf4174f4347e623b57e314d4ac7717014c7476aa2f54ab3a71f3143fb366398dd2a165e092085ce34ed1412a7bf9ce8008f2061164edc3e42ef8b7f2a7e38ebc1f05059b0b829fa82cceef91aad67ffb843c79a9c6944f0f08d7523111a8c25dbfeab9aedb0cbd682f906d33a044be072fa22657024d4d056823310f38b65cad5e293b26142798a3a49c880eb4646bfbf612294f90792b652532d5e9bb5ca79699f968ec811a3aeb038d6033ac82a571f098659294db36666da4380c8a74eb7af8dfe32e5136301dad0e49d71e726d15c0cf659850eaa878133302835fcbe624122aee63b72a960d1c1432561afa55f07a9e57c6791c813131a7c0f6a0e1036036bc613e38cff14a1353b496416f06b1a3c224d8cb51218d251c8720624c936541519167e150dc2ac92091110fae8a08bac660b6ea4ef12f0c710bc7deada678a4e8d00b4803980e40c1899e9a5208b55d06bc519004e2bbfb15c9bec614c496de1f7fe103d9173067084f0170b9f6837f179689c3a4aa11b94e54aa3a49c76838ed5d31fece91a95fc089af72833e453c64c58a8270ec868ae7a4154717f3c98a940e51a197c9ebed4188ff1969faf4d9e59618713af785b538c4f41568dd4ba9b6dff68ab18222b4195938c1e500640218d1d4f6ef014c76deab8e18782e5fff6988ef6e630ff38dcc6709de7548cc1f56d817a0dbfe5df95e6a0f53"
public_key = "0000100080af29ebacee1fc0a795d91674aaeb4f55f4d36ba6f899ddb543be5d1cc878d45d776fe6211a330c0843322334b690dbbb9dc13fa47642abc75889dbba79c82a9dba19c64d4c099f36240775834e9d86013f433a1aaab0bed1f2212dc53cf2d967ded53209c1e5eb5b563d00d307c021b9d2d084e80cdd20a695225ce31e7fb407ccebdec5cda2eb44ed079df3edf31751e77ce43ae9681bc7f6cc4e58945366bb655550e07dd215eb516815e9457feef9301b772f92291fd5758782f20734b9ac75e7469f42ffd4055ae43a8301a37e56dfda4ede5e639fec57ab7268d8787190268952a27e6b8e3ffc503759af317da4a076fd36fe6499f15de27d81dca566f500838d13a442b01ecd1ffdb655ea2ef779bab13cd8eab649402cc1a3f81444ed24717a7c3fa65ac2b8d3625450aae15b75d3dc37e962f516799cb555063eeb262bb51f9af465fc992e07f3aa66917ce086abd9a60716f04fe155efe7901e4132a4dcdcdcf7db23e4eade6b3664bee87a7fd4c6b3247beeb0553263845d6547efda705f58cab911d9a98fb7cdd807bfdbf3dbf1f81ac3e837ba75ea5512be7246bfe768aa1edf23c469db87df980bbb939fd1f28948340882350e4c58f0450329f8088da6b908b4c4c78689eb18b3c06b683b12e518bde5cd57e7fb43792df94c38f643675e5c92b1c0a95f45256969a4bc6ea1db6682745d423cc676f97f57cf2d493d850bfba9362a858faa355e2bd0c9b1a46f6581c9c0751399788bd37fc5e597a402a3bc05bb5959677a48ce4aa9fa6ed5add36a7e11117152610b0e8f86692669825206eb5d297d7f77d114bc867736b6d6eb4309329569d78dfb910cb66132c5a3f415bd1ededfb61fd58c958db4c7a2c17578f537d24b5405e545d1618ae58be3525c9659d6baf2634255e7ca26fcfde1cbc4401e8ca062babede5f5b3f2ad5cbecd3d9332098f402666a9ef85643e3f4ab214a67a5ca91f31f810eab89df57d2f9ef35e9a5f46a99228af059cdd1f0685f1b9405beb0e44fe7f590b50feea1554e8bdaf7be7fd22e6f050a7751731cd7f8737b5acb92378da9d59068106da25b30882abad4629dff6ca9c4c0cf5337bf2d2a9e8c6182dcd6d7c681da01df6c11d0b335cf1f982156393c4e572df540cee2c53a2c146dec9644be4e81cce20df4cb136b3a08fad9a186be892d6653330d11cac1e96d308fd0d2896c8a141edfbdcac827cdf97ac581f493721c9aa1305667df312277c8ab6700c3a978e6f9d00b389faaeaa99b5e023b72c5f5ce885fa2ba816ffc735599fad1e3876d91a501efbbf1315f42a503ddd7a26a784a3f290a86f43dd580c03fc4125716d6b534e35daf1156fcaec37ac8bc9e881d5ec1715632ff82b4dc4e3aabf48c5db6f3608ce2b8bb48bceae621b07f231254dd6f1f7b4584e7102969f523252a6839dca065"
public_key_metadata = "000102030405060708090a0b0c0d0e0f"
rollback_index = 1677974400
flags = 0
rollback_index_location = 0
release_string = "MaxLengthMaxLengthMaxLengthMaxLengthMaxLengthMa"
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[header.descriptors]]
type = "Property"
key = "foobar"
value = 'Invalid\x20UTF-8:\x20\xFF'
[[header.descriptors]]
type = "HashTree"
dm_verity_version = 1
image_size = 8192
tree_offset = 8192
tree_size = 4096
data_block_size = 4096
hash_block_size = 4096
fec_num_roots = 2
fec_offset = 12288
fec_size = 8192
hash_algorithm = "sha256"
partition_name = "vbmeta_appended_hash_tree"
salt = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
root_digest = "bfb9f01152b3315096319d03697b4013d1674063418deec55920686ad13537a8"
flags = 0
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567"
[footer]
version_major = 1
version_minor = 0
original_image_size = 8192
vbmeta_offset = 20480
vbmeta_size = 2240
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef01234567"
File diff suppressed because one or more lines are too long
+61
View File
@@ -0,0 +1,61 @@
image_size = 4096
[header]
required_libavb_version_major = 1
required_libavb_version_minor = 0
algorithm_type = "Sha256Rsa4096"
hash = "892d9bd80a0b8b5e5daed1cdedfc252e3fe5573ffa812fa485c2eaec17632f3b"
signature = "752e7688a4de06eab55d37b56c4d7158e6e0477916f5217904edb85c946e4bfa4c0fb5d8aed0d1a0cc084c528cc303209075caa1067bdba7c0cc970d024c022ffe61feabc0eaf45755938d2b9b248ae07d369388b92c23c0f0f31a4a5ac1a4e92ebd2b24fff7953448a8f6827363fd3d32055977d708f72c9224b1a6616b59e79cdb20a4c17959081904d394fdfa27573f0b428fd7179ae3607c93e99a07badd6908e3f76893a346267126798503807d4a58904262afca7544349a796b04f658511724207c5a645c2976faec055f44c44ea21d63c7b2af21a8ffb7f4007249e8f81f8e2746e20965ced023b952bc48cb9c5ae11587b771f47383cfc1f5d021b1024128fc02a5a2f458d131a08975470d9373a9b70336d647943ed9e04bdaa0bbf19bfb087e0e6454cd178152eb826244bbfad20315dc20ae9a889ea99ac0045f438726bb882bf54c87fecfb3056aac7f2d649523b8cbcc8dd5a54917740e440ad7ffdaefde7e98b557798054d94653005213f377ce560fdec934435b2049b09e613906c12da713b97d31966238e7708a47501c0c91c6a9cfb4911fef54acacfa3e83f29a542d8a7d2c0f7b3b92815502ca9307e40c009415e575aab7f3b998a48d4a0a6d59adf3c8bd253620a9cbf53ea5fdb84bce5e21aca3d8d749406770725fc22f925e9a53f1c58eddc5e4dbd68d8537e7dda5035c20c54a5045434c6505"
public_key = "0000100080af29ebacee1fc0a795d91674aaeb4f55f4d36ba6f899ddb543be5d1cc878d45d776fe6211a330c0843322334b690dbbb9dc13fa47642abc75889dbba79c82a9dba19c64d4c099f36240775834e9d86013f433a1aaab0bed1f2212dc53cf2d967ded53209c1e5eb5b563d00d307c021b9d2d084e80cdd20a695225ce31e7fb407ccebdec5cda2eb44ed079df3edf31751e77ce43ae9681bc7f6cc4e58945366bb655550e07dd215eb516815e9457feef9301b772f92291fd5758782f20734b9ac75e7469f42ffd4055ae43a8301a37e56dfda4ede5e639fec57ab7268d8787190268952a27e6b8e3ffc503759af317da4a076fd36fe6499f15de27d81dca566f500838d13a442b01ecd1ffdb655ea2ef779bab13cd8eab649402cc1a3f81444ed24717a7c3fa65ac2b8d3625450aae15b75d3dc37e962f516799cb555063eeb262bb51f9af465fc992e07f3aa66917ce086abd9a60716f04fe155efe7901e4132a4dcdcdcf7db23e4eade6b3664bee87a7fd4c6b3247beeb0553263845d6547efda705f58cab911d9a98fb7cdd807bfdbf3dbf1f81ac3e837ba75ea5512be7246bfe768aa1edf23c469db87df980bbb939fd1f28948340882350e4c58f0450329f8088da6b908b4c4c78689eb18b3c06b683b12e518bde5cd57e7fb43792df94c38f643675e5c92b1c0a95f45256969a4bc6ea1db6682745d423cc676f97f57cf2d493d850bfba9362a858faa355e2bd0c9b1a46f6581c9c0751399788bd37fc5e597a402a3bc05bb5959677a48ce4aa9fa6ed5add36a7e11117152610b0e8f86692669825206eb5d297d7f77d114bc867736b6d6eb4309329569d78dfb910cb66132c5a3f415bd1ededfb61fd58c958db4c7a2c17578f537d24b5405e545d1618ae58be3525c9659d6baf2634255e7ca26fcfde1cbc4401e8ca062babede5f5b3f2ad5cbecd3d9332098f402666a9ef85643e3f4ab214a67a5ca91f31f810eab89df57d2f9ef35e9a5f46a99228af059cdd1f0685f1b9405beb0e44fe7f590b50feea1554e8bdaf7be7fd22e6f050a7751731cd7f8737b5acb92378da9d59068106da25b30882abad4629dff6ca9c4c0cf5337bf2d2a9e8c6182dcd6d7c681da01df6c11d0b335cf1f982156393c4e572df540cee2c53a2c146dec9644be4e81cce20df4cb136b3a08fad9a186be892d6653330d11cac1e96d308fd0d2896c8a141edfbdcac827cdf97ac581f493721c9aa1305667df312277c8ab6700c3a978e6f9d00b389faaeaa99b5e023b72c5f5ce885fa2ba816ffc735599fad1e3876d91a501efbbf1315f42a503ddd7a26a784a3f290a86f43dd580c03fc4125716d6b534e35daf1156fcaec37ac8bc9e881d5ec1715632ff82b4dc4e3aabf48c5db6f3608ce2b8bb48bceae621b07f231254dd6f1f7b4584e7102969f523252a6839dca065"
public_key_metadata = "000102030405060708090a0b0c0d0e0f"
rollback_index = 1677974400
flags = 0
rollback_index_location = 0
release_string = "MaxLengthMaxLengthMaxLengthMaxLengthMaxLengthMa"
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
[[header.descriptors]]
type = "Property"
key = "foobar"
value = 'Invalid\x20UTF-8:\x20\xFF'
[[header.descriptors]]
type = "HashTree"
dm_verity_version = 1
image_size = 4096
tree_offset = 0
tree_size = 2048
data_block_size = 4096
hash_block_size = 4096
fec_num_roots = 1
fec_offset = 2048
fec_size = 2048
hash_algorithm = "sha512"
partition_name = "hashtreed_partition"
salt = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
root_digest = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
flags = 0
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567"
[[header.descriptors]]
type = "Hash"
image_size = 6
hash_algorithm = "sha256"
partition_name = "hashed_partition"
salt = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
root_digest = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
flags = 0
reserved = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef01234567"
[[header.descriptors]]
type = "KernelCmdline"
flags = 1
cmdline = "foobar"
[[header.descriptors]]
type = "ChainPartition"
rollback_index_location = 1
partition_name = "chained_partition"
public_key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
flags = 4275878552
reserved = "76543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
+20
View File
@@ -0,0 +1,20 @@
type = "V0Through2"
kernel_addr = 19088743
ramdisk_addr = 2309737967
second_addr = 38177486
tags_addr = 324508639
page_size = 4096
os_version = 1985229328
name = "NameNameNameName"
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineC"
id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
]
extra_cmdline = "ExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtr"
+1
View File
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
+1
View File
@@ -0,0 +1 @@
second data
+23
View File
@@ -0,0 +1,23 @@
type = "V0Through2"
kernel_addr = 19088743
ramdisk_addr = 2309737967
second_addr = 38177486
tags_addr = 324508639
page_size = 4096
os_version = 1985229328
name = "NameNameNameName"
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineC"
id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
]
extra_cmdline = "ExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtr"
[v1_extra]
recovery_dtbo_offset = 81985529216486895
+1
View File
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
@@ -0,0 +1 @@
recovery_dtbo data
+1
View File
@@ -0,0 +1 @@
second data
+26
View File
@@ -0,0 +1,26 @@
type = "V0Through2"
kernel_addr = 19088743
ramdisk_addr = 2309737967
second_addr = 38177486
tags_addr = 324508639
page_size = 4096
os_version = 1985229328
name = "NameNameNameName"
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineC"
id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
]
extra_cmdline = "ExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtraCmdlineExtr"
[v1_extra]
recovery_dtbo_offset = 81985529216486895
[v2_extra]
dtb_addr = 18364758544493064720
+1
View File
@@ -0,0 +1 @@
dtb data
+1
View File
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
@@ -0,0 +1 @@
recovery_dtbo data
+1
View File
@@ -0,0 +1 @@
second data
+9
View File
@@ -0,0 +1,9 @@
type = "V3Through4"
os_version = 19088743
reserved = [
1122867,
1146447479,
2291772091,
3437096703,
]
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmd"
+1
View File
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
+11
View File
@@ -0,0 +1,11 @@
type = "V3Through4"
os_version = 19088743
reserved = [
1122867,
1146447479,
2291772091,
3437096703,
]
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmd"
[v4_extra]
+1
View File
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
@@ -0,0 +1,11 @@
type = "V3Through4"
os_version = 19088743
reserved = [
1122867,
1146447479,
2291772091,
3437096703,
]
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmd"
[v4_extra]
@@ -0,0 +1 @@
kernel data
@@ -0,0 +1 @@
ramdisk data
@@ -0,0 +1,25 @@
image_size = 2048
[header]
required_libavb_version_major = 1
required_libavb_version_minor = 0
algorithm_type = "Sha256Rsa4096"
hash = "73b770b8199f1e709ce1cc37ee7f699faa99bf7e88c6eff429a6eda1feb6a1f9"
signature = "58c16f72718891a11f8d1ad92b8053c20d701ac3cc997ee7889d45c999ee91b05795d59d6feac44c316fe15f92dd06e9ae3bc570d8f414191bdaab7aae4c37822cb2f6d34fdcccc65a853d2dc72ecec311db000e27fbdc7bc767b80c1a0b4e88e193ce4f878ce15cac2a4b361294eddc193d36e148768b4c6b0de4001cf2e12fe2b8be84d5217a4c3ea12c0e040900a112f6e519363635a3b3108191b5858c90b62a4d459a6d66218978e002ed07478472ff34269ca575a53eb396e3e13972f115827c741b82e5988eb6f59f953e2ee5ad9fc6ab5d86754f282011e4a511c8ce4ed9d8b077a2b5f1a3e8943c222debaab02e02b6e461f98820a8f90a470ea222b625831e280a8d9bdf96bde33bf830bc2e39a62d52103f3b8988025da86f55a5f075ea362e45c0f92279216dfdf47ead516dfd5d9da546fd5a93e325a1706c0241f71a4d4cd5abec836fd2be0812b9716ce126498a67b0d6610adc94bb9cf9b137af57b1f413155428a4bc0318aff369cdcae69008f883b48d3ba6210c8f90cb473c8101c2381ad1ab2d2b17cdf3a8e65d118beb2283b140630e3f4800159d208c34abac3d9493e321948205e1a6df529a36ec47dac50b06a71aa2350f87738022ad8175c514dfb6acca04b9bac4a002ff587639cab9f1d94e68f13739d7d72b30010db4cf0e62ea796d6207e3b9264b9ec980076c6be7dbd9973e6278225be0"
public_key = "0000100080af29ebacee1fc0a795d91674aaeb4f55f4d36ba6f899ddb543be5d1cc878d45d776fe6211a330c0843322334b690dbbb9dc13fa47642abc75889dbba79c82a9dba19c64d4c099f36240775834e9d86013f433a1aaab0bed1f2212dc53cf2d967ded53209c1e5eb5b563d00d307c021b9d2d084e80cdd20a695225ce31e7fb407ccebdec5cda2eb44ed079df3edf31751e77ce43ae9681bc7f6cc4e58945366bb655550e07dd215eb516815e9457feef9301b772f92291fd5758782f20734b9ac75e7469f42ffd4055ae43a8301a37e56dfda4ede5e639fec57ab7268d8787190268952a27e6b8e3ffc503759af317da4a076fd36fe6499f15de27d81dca566f500838d13a442b01ecd1ffdb655ea2ef779bab13cd8eab649402cc1a3f81444ed24717a7c3fa65ac2b8d3625450aae15b75d3dc37e962f516799cb555063eeb262bb51f9af465fc992e07f3aa66917ce086abd9a60716f04fe155efe7901e4132a4dcdcdcf7db23e4eade6b3664bee87a7fd4c6b3247beeb0553263845d6547efda705f58cab911d9a98fb7cdd807bfdbf3dbf1f81ac3e837ba75ea5512be7246bfe768aa1edf23c469db87df980bbb939fd1f28948340882350e4c58f0450329f8088da6b908b4c4c78689eb18b3c06b683b12e518bde5cd57e7fb43792df94c38f643675e5c92b1c0a95f45256969a4bc6ea1db6682745d423cc676f97f57cf2d493d850bfba9362a858faa355e2bd0c9b1a46f6581c9c0751399788bd37fc5e597a402a3bc05bb5959677a48ce4aa9fa6ed5add36a7e11117152610b0e8f86692669825206eb5d297d7f77d114bc867736b6d6eb4309329569d78dfb910cb66132c5a3f415bd1ededfb61fd58c958db4c7a2c17578f537d24b5405e545d1618ae58be3525c9659d6baf2634255e7ca26fcfde1cbc4401e8ca062babede5f5b3f2ad5cbecd3d9332098f402666a9ef85643e3f4ab214a67a5ca91f31f810eab89df57d2f9ef35e9a5f46a99228af059cdd1f0685f1b9405beb0e44fe7f590b50feea1554e8bdaf7be7fd22e6f050a7751731cd7f8737b5acb92378da9d59068106da25b30882abad4629dff6ca9c4c0cf5337bf2d2a9e8c6182dcd6d7c681da01df6c11d0b335cf1f982156393c4e572df540cee2c53a2c146dec9644be4e81cce20df4cb136b3a08fad9a186be892d6653330d11cac1e96d308fd0d2896c8a141edfbdcac827cdf97ac581f493721c9aa1305667df312277c8ab6700c3a978e6f9d00b389faaeaa99b5e023b72c5f5ce885fa2ba816ffc735599fad1e3876d91a501efbbf1315f42a503ddd7a26a784a3f290a86f43dd580c03fc4125716d6b534e35daf1156fcaec37ac8bc9e881d5ec1715632ff82b4dc4e3aabf48c5db6f3608ce2b8bb48bceae621b07f231254dd6f1f7b4584e7102969f523252a6839dca065"
public_key_metadata = ""
rollback_index = 0
flags = 0
rollback_index_location = 0
release_string = "avbtool 1.2.0"
reserved = "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
[[header.descriptors]]
type = "Hash"
image_size = 12288
hash_algorithm = "sha256"
partition_name = "boot"
salt = "6430306466303064"
root_digest = "abd5483e11e7940cb9bf387587a4a1659981a1b83962b7c1faf1b0cd6307d449"
flags = 0
reserved = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
@@ -0,0 +1,8 @@
type = "VendorV3Through4"
page_size = 4096
kernel_addr = 19088743
ramdisk_addr = 2309737967
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdl"
tags_addr = 4275878552
name = "NameNameNameName"
dtb_addr = 1985229328
+1
View File
@@ -0,0 +1 @@
dtb data
@@ -0,0 +1 @@
ramdisk data
+96
View File
@@ -0,0 +1,96 @@
type = "VendorV3Through4"
page_size = 2048
kernel_addr = 19088743
ramdisk_addr = 2309737967
cmdline = "CmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdlineCmdl"
tags_addr = 4275878552
name = "NameNameNameName"
dtb_addr = 1985229328
[[v4_extra.ramdisk_metas]]
ramdisk_type = 0
ramdisk_name = "NoneNoneNoneNoneNoneNoneNoneNone"
board_id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
4491468,
290822621,
577153774,
863484927,
4290475827,
4004144674,
3717813521,
3431482368,
]
[[v4_extra.ramdisk_metas]]
ramdisk_type = 1
ramdisk_name = "PlatformPlatformPlatformPlatform"
board_id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
4491468,
290822621,
577153774,
863484927,
4290475827,
4004144674,
3717813521,
3431482368,
]
[[v4_extra.ramdisk_metas]]
ramdisk_type = 2
ramdisk_name = "RecoveryRecoveryRecoveryRecovery"
board_id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
4491468,
290822621,
577153774,
863484927,
4290475827,
4004144674,
3717813521,
3431482368,
]
[[v4_extra.ramdisk_metas]]
ramdisk_type = 3
ramdisk_name = "DlkmDlkmDlkmDlkmDlkmDlkmDlkmDlkm"
board_id = [
1122867,
1146447479,
2291772091,
3437096703,
4293844428,
3148519816,
2003195204,
857870592,
4491468,
290822621,
577153774,
863484927,
4290475827,
4004144674,
3717813521,
3431482368,
]
@@ -0,0 +1 @@
bootconfig data
+1
View File
@@ -0,0 +1 @@
dtb data
@@ -0,0 +1 @@
ramdisk 0 data
@@ -0,0 +1 @@
ramdisk 1 data
@@ -0,0 +1 @@
ramdisk 2 data
@@ -0,0 +1 @@
ramdisk 3 data
+35
View File
@@ -0,0 +1,35 @@
format = "None"
[[entries]]
path = "symlink"
data = "target"
file_type = "Symlink"
file_mode = "777"
nlink = 1
[[entries]]
path = "directory"
file_type = "Directory"
file_mode = "755"
nlink = 1
[[entries]]
path = "file"
file_type = "Regular"
file_mode = "644"
nlink = 1
[[entries]]
path = "reserved"
inode = 12345
file_type = "Reserved"
file_mode = "4777"
uid = 12345678
gid = 87654321
nlink = 2
mtime = 1700000000
dev_maj = 2222
dev_min = 3333
rdev_maj = 4444
rdev_min = 5555
crc32 = 4275878409
+1
View File
@@ -0,0 +1 @@
foobar
+1
View File
@@ -0,0 +1 @@
datadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadata
+80
View File
@@ -0,0 +1,80 @@
image_type = "Empty"
metadata_max_size = 65536
metadata_slot_count = 3
logical_block_size = 4096
[[slots]]
major_version = 10
minor_version = 2
flags = "VIRTUAL_AB_DEVICE"
[[slots.groups]]
name = "default"
flags = ""
partitions = []
[[slots.groups]]
name = "google_dynamic_partitions_a"
flags = ""
maximum_size = 8527020032
[[slots.groups.partitions]]
name = "system_a"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "system_dlkm_a"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "system_ext_a"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "product_a"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "vendor_a"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "vendor_dlkm_a"
attributes = "READONLY"
[[slots.groups]]
name = "google_dynamic_partitions_b"
flags = ""
maximum_size = 8527020032
[[slots.groups.partitions]]
name = "system_b"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "system_dlkm_b"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "system_ext_b"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "product_b"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "vendor_b"
attributes = "READONLY"
[[slots.groups.partitions]]
name = "vendor_dlkm_b"
attributes = "READONLY"
[[slots.block_devices]]
first_logical_sector = 2048
alignment = 1048576
alignment_offset = 0
size = 8531214336
partition_name = "super"
flags = ""
+1
View File
@@ -0,0 +1 @@
datadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadatadata
@@ -1 +0,0 @@
../../../../avbroot/tests/data/vbmeta_appended_hash.img

Some files were not shown because too many files have changed in this diff Show More