Compare commits

...

278 Commits

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

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

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

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

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

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

Fixes: #433

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

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

Discussion: #426

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Discussion: #417

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

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

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-02-04 00:13:27 -05:00
Andrew Gunnerson 43db728b9d Version 3.12.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:20:06 -05:00
Andrew Gunnerson fbe9f629ab CHANGELOG.md: Add entry for PR #411
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:11:40 -05:00
Andrew Gunnerson 80d5f19223 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:10:42 -05:00
Andrew Gunnerson cff5ac6b2e CHANGELOG.md: Add entry for PR #410
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:04:35 -05:00
Andrew Gunnerson 2e3b5db9fe cli/key: Rename extract-avb to encode-avb
For consistency with decode-avb. The old syntax will remain supported
indefinitely for backwards compatibility.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 18:01:24 -05:00
Andrew Gunnerson 58a279f3a4 CHANGELOG.md: Add entry for PR #409
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 17:53:08 -05:00
Andrew Gunnerson 1484cd47c3 cli/ota: Add support for extracting OTA certificate and AVB public key
This also adds a new --none option so that these two components can be
extracted without extracting any partition images.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 17:50:09 -05:00
Andrew Gunnerson 2ee38b716b CHANGELOG.md: Add entry for PR #408
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 16:48:08 -05:00
Andrew Gunnerson d4eb231dd4 cli/ota: Add support for extracting specific images
Previously, we only supported extracting all images or the subset of
images that could potentially be patched by avbroot. This was
unnecessarily slow if the user only needed to extract a specific image.

This commit also deprecates and hides the `--boot-only` option, though
the functionality will remain indefinitely.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2025-01-26 16:43:35 -05:00
Andrew Gunnerson cf064e145d Version 3.11.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 20:05:57 -05:00
Andrew Gunnerson 3b0c97a93d CHANGELOG.md: Add entry for PR #404
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 20:05:30 -05:00
Andrew Gunnerson 2d4f08f48b Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:56:02 -05:00
Andrew Gunnerson a753304dff CHANGELOG.md: Add entry for PR #403
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:39:42 -05:00
Andrew Gunnerson 9480e2ffa4 cpio: Read and write entire header at once
Avoids doing 14 small reads per cpio entry.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 19:37:58 -05:00
Andrew Gunnerson 140d0ddd8b CHANGELOG.md: Add entry for PR #402
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 15:08:11 -05:00
Andrew Gunnerson e0b114cf71 Switch to upstream master branch of bzip2-rs
The only remaining fix from our fork has been merged. This can be
switched to a stable release after the next release.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 14:59:44 -05:00
Andrew Gunnerson 78449f686d CHANGELOG.md: Add entry for PR #401
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 14:54:51 -05:00
Andrew Gunnerson a2fb807803 Remove all uses of implicit error propagation
Implicit error propagation was originally used because it was convenient
and made it easy to just bubble up errors via the ? operator without
thinking. However, there have been too many situations where this
resulted in error messages that were completely useless in
troubleshooting the problem. "I/O error", even with a specific reason
attached, is useless where there are potentially hundreds of operations
where I/O can fail.

I no longer think implicit error propagation is a good idea, so this
commit removes every single use of #[from] in every custom error type.
There are now many more error variants, allowing more context to be
attached to the underlying errors.

Previously, it was easy to encounter error messages like:

    Caused by:
        0: Failed to patch payload: payload.bin
        1: Failed to patch boot images: boot, init_boot, vendor_boot
        2: Boot image error
        3: I/O error
        4: failed to fill whole buffer

This is a terrible error message because it doesn't mention which of the
3 boot images failed to parse, nor does it mention during which I/O
operation it encountered EOF. With this commit, this sort of information
is now included. For example, if the boot image happened to be truncated
in the middle of the ramdisk, the error message would now be:

    Caused by:
        0: Failed to patch payload: payload.bin
        1: Failed to patch boot images: boot, init_boot, vendor_boot
        2: Failed to load boot image: init_boot
        3: Failed to read boot image data: Boot::V3::ramdisk
        4: failed to fill whole buffer

Changes:

* Remove all uses of #[from] from thiserror-derived error types.
* Errors during parsing and serialization of RSA private keys, RSA
  public keys, and X509 certificates now include the file path.
* Removed unnecessary uses of BufReader and BufWriter when reading and
  writing RSA private keys, RSA public keys, and X509 certificates,
  since they need to be fully read into memory anyway.
* Use ReadFixedSizeExt instead of read_exact() where possible.
* Use &'static str instead of String in error fields where all possible
  values are known at compile-time to avoid unnecessary heap allocation.
* Use ok_or() instead of ok_or_else() to construct errors when the error
  variant uses known data and does not require heap allocation.
* Using DebugString instead of String in error variants that store a
  preformatted debug string.

While working on this commit, an unrelated bug was found and fixed:

* Fix vendor v4 boot images that were truncated within the padding
  following the bootconfig section being treated as valid.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-25 02:34:17 -05:00
Andrew Gunnerson 6de5cb783a CHANGELOG.md: Add entry for PR #398
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 22:09:03 -05:00
Andrew Gunnerson 12d6f7f78c util: Wrap std's range types instead of inventing our own
This makes the type potentially more useful outside of just our current
use case of just error messages. Ranges where the starting value is
exclusive are no longer supported, but weren't used anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 21:56:13 -05:00
Andrew Gunnerson 3c0a77df21 CHANGELOG.md: Add entry for PR #397
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 21:28:20 -05:00
Andrew Gunnerson d1b6bce227 Update bzip2-rs to 0.5.0 and switch to Rust backend implementation
The bzip2-rs library now has a new maintainer (same folks that maintain
the sudo-rs project). Version 0.5.0 was recently released, which
includes some much needed bug fixes, but is missing one final one, so we
still need to keep our fork for now.

This commit also switches the backend implementation from the official C
implementation to a Rust implementation maintained by the bzip2-rs
folks. This leaves xz as the only C dependency remaining in avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 20:23:40 -05:00
Andrew Gunnerson 10e0748d18 CHANGELOG.md: Add entry for PR #395
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 03:58:28 -05:00
Andrew Gunnerson 9cf63c7036 Treat Magisk versions newer than latest as supporting all features
Previously, when patching with a Magisk version newer than the latest
supported version and using --ignore-magisk-warnings, the upper bound of
VER_PREINIT_DEVICE and VER_XZ_BACKUP would prevent those features from
being used. It makes more sense to assume that a newer version supports
all the same features as the latest supported version instead.

Issue: #393

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 03:53:11 -05:00
Andrew Gunnerson 291b3c887b CHANGELOG.md: Add entry for PR #394
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-11 00:01:39 -05:00
Andrew Gunnerson 5d7eb13fbc Fix crash when ignoring warning about missing preinit device
MagiskRootPatcher was previously assuming that there is a preinit device
value if the Magisk version requires it.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-10 23:58:36 -05:00
Andrew Gunnerson 05cd74719e Version 3.10.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:20:39 -05:00
Andrew Gunnerson 7e0d5584d9 CHANGELOG.md: Add entry for PR #392
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 19:01:08 -05:00
Andrew Gunnerson b351d47f28 Update dependencies and fix most pedantic clippy warnings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 18:46:40 -05:00
Andrew Gunnerson 421b0a5207 CHANGELOG.md: Add entry for PR #391
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 17:14:21 -05:00
Andrew Gunnerson cad08a6a2f Add support for Magisk 28100
There is nothing new that breaks compatibility with avbroot.

Fixes: #389

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:21:05 -05:00
Andrew Gunnerson 0a32dfaec3 CHANGELOG.md: Add entry for PR #390
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:19:46 -05:00
Andrew Gunnerson b03b2ade6a Improve parser error messages
* Remove the ReadString trait and perform UTF-8 conversions explicitly.
  UTF-8 conversion errors are now individual errors instead of being
  hidden in an Io error.
* Add new IntOutOfBounds errors that include the actual field value and
  the range of values that are valid for the field. Integer casting also
  uses this error type where possible.
* Replace remaining uses of FieldOutOfBounds with IntOverflow, which is
  returned when checked arithmetic fails. For code simplicity, it does
  not store intermediate values.
* Replace ReadFieldError and WriteFieldError with the generic Io error
  to simplify the code. They were used inconsistently anyway.
* Use fully qualified field names in avb and bootimage error messages
  since the same name frequently appears in multiple structs.
* Replace InvalidFieldValue with more specific errors in bootimage.
* Replace all stringly-typed errors in lp and sparse with (very)
  specific errors.
* Fix the wording in a number of errors.
* Add new ReadFixedSizeExt trait for reading fixed size arrays and vecs
  from Read types.
* Remove some fallible casts from u32 to usize and from usize to u64.
  avbroot only supports 32-bit and 64-bit systems anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-08 02:08:28 -05:00
Andrew Gunnerson c198646c7c Add documentation for Android 16's developer switch for 16K page size kernels
avbroot will not support patching these internal OTAs because it
requires modifying filesystems and creating incremental OTAs. However,
it should be possible to do this manually if someone really wants to try
out a 16K page size kernel.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-06 18:09:34 -05:00
Andrew Gunnerson d815a78e9a CHANGELOG.md: Add entry for PR #386
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:47:31 -05:00
Andrew Gunnerson 99c5800f96 Update dependencies and pin Github Actions actions to specific commits
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:42:35 -05:00
Andrew Gunnerson c801fbb069 CHANGELOG.md: Add entry for PR #385
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:37:54 -05:00
Andrew Gunnerson 0b250086a3 protobuf: Update update_metadata.proto from AOSP
Neither of the two new fields need any special handling in avbroot. This
just allows us to preserve the values from the original OTA.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:33:34 -05:00
Andrew Gunnerson f6c6c8ab40 CHANGELOG.md: Add entry for PR #384
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:27:38 -05:00
Andrew Gunnerson 5c353a5f42 Remove unused byteorder crate
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:57 -05:00
Andrew Gunnerson ad932ed445 sparse: Switch to read_from_io()/write_to_io()
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:44 -05:00
Andrew Gunnerson d04d5451dc stream: Remove unused padded string functions
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:30 -05:00
Andrew Gunnerson 1d5db9f731 bootimage: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:04:28 -05:00
Andrew Gunnerson 409b9298a5 avb: Switch to zerocopy
This commit also fixes a bug where avb::Header::release_string was
allowed to take the full 48-bytes, which was incorrect because libavb
expects the field to be NULL-terminated. This was not a problem in
practice because the release string is usually short and even if it
wasn't, the 80 reserved bytes that immediately follow it are all zeros.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 17:01:43 -05:00
Andrew Gunnerson 516238d907 hashtree: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:59:56 -05:00
Andrew Gunnerson 39d5fbf76d payload: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:35 -05:00
Andrew Gunnerson 20f5bdacc7 compression: Switch to zerocopy
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:58:08 -05:00
Andrew Gunnerson 5b8eefa867 fec: Switch to zerocopy
This commit also adds support for parsing FEC images with unknown extra
custom fields.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:55 -05:00
Andrew Gunnerson b2610fa0e9 Update zerocopy to 0.8.11
>=0.8.10 is needed for the new read_from_io()/write_to_io() helper
functions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:57:39 -05:00
Andrew Gunnerson 8a5550a43c Add new traits for zero padding and unpadding strings
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-12-01 16:56:55 -05:00
Ivan Katrovsky e16f4c7fa7 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/10c425dedea262678fda3c0c097b82fdf4352f08
2024-11-13 17:54:14 +03:00
Andrew Gunnerson 983e6c40a5 Version 3.9.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 20:04:59 -05:00
Andrew Gunnerson 8729854727 CHANGELOG.md: Add entry for PR #377
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:55:44 -05:00
Andrew Gunnerson 0d1beb7734 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:54:50 -05:00
Andrew Gunnerson 2cd0d69238 CHANGELOG.md: Add entry for PR #376
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:43:50 -05:00
Andrew Gunnerson c0e65264e5 sparse: Allow parsing files with unknown fields
The official AOSP implementation does, so we should too. This allows
unpacking Samsung's sparse images, which have an extra 4 bytes in both
the file header and chunk headers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-11 19:41:27 -05:00
Andrew Gunnerson 37b15a2b6a CHANGELOG.md: Add entry for PR #374
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:56 -05:00
Andrew Gunnerson e4994fbe98 format/payload: Allow verifying signatures without an unpadded size
Older OTAs created before the payload metadata format supported EC
signatures will not have the `unpadded_signature_size` field set. In
this case, we'll just use the full length of `data`, which is what
update_engine also does.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:38:28 -05:00
Andrew Gunnerson cf1bacab30 CHANGELOG.md: Add entry for PR #373
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:27:24 -05:00
Andrew Gunnerson 4832121160 format/ota: Allow verifying OTAs without metadata.pb
While patched OTAs produced by avbroot always include the protobuf
version of the OTA metadata, the original OTA may not. Verifying those
with `avbroot ota verify` is a valid use case.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-08 17:25:00 -05:00
Andrew Gunnerson c614e61744 CHANGELOG.md: Add entry for PR #371
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:13:45 -05:00
Andrew Gunnerson 9ebce1666c Add option to skip verifying recovery's OTA cert too
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-07 22:04:11 -05:00
Andrew Gunnerson fb34198ffc CHANGELOG.md: Add entry for PR #370
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 18:00:13 -05:00
Andrew Gunnerson 4a5eab4ba0 crypto: Fix minor clippy warning
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:59:26 -05:00
Andrew Gunnerson 1e1818ad8f CHANGELOG.md: Add entry for PR #369
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:58:59 -05:00
Andrew Gunnerson 3f25ad7c76 Add more context for avb::Header::set_algo_for_key() calls
Previously, it was not always obvious which key was problematic.

Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:53:01 -05:00
Andrew Gunnerson 37e28eb040 CHANGELOG.md: Add entry for PR #367
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 17:51:41 -05:00
Andrew Gunnerson ce87757fd1 patch/boot: Avoid loading boot images when there are no patchers
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:04 -05:00
Andrew Gunnerson 10c425dede Add option to skip inserting OTA cert into recovery image
Issue: #366

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-05 09:30:03 -05:00
Andrew Gunnerson 8a0d147993 CHANGELOG.md: Add entry for PR #368
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:54:23 -05:00
Andrew Gunnerson 062aa21485 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-11-04 18:32:58 -05:00
Andrew Gunnerson 7ffeb5e5cb Version 3.8.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:25:18 -04:00
Andrew Gunnerson a9a1aa55e9 CHANGELOG.md: Add entry for PR #364
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:18:58 -04:00
Andrew Gunnerson 0ca2872111 Update dependencies
zerocopy 0.8 is the only dependency with breaking changes.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 22:16:59 -04:00
Andrew Gunnerson a403c26b54 CHANGELOG.md: Add entry for PR #363
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 21:36:47 -04:00
Andrew Gunnerson 4afa1d1ba6 cli/avb: Add subcommand for computing vbmeta digest
This computes the special SHA256 digest that is equal to the
ro.boot.vbmeta.digest property value on a real device.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-13 21:29:04 -04:00
Andrew Gunnerson 9f669bc53e Version 3.7.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:12:48 -04:00
Andrew Gunnerson fc89159f09 CHANGELOG.md: Add entry for PR #362
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:04:25 -04:00
Andrew Gunnerson 18f43a0e57 Add support for Magisk 28000
There is nothing new that isn't already supported.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-10-10 18:03:04 -04:00
Andrew Gunnerson af7262a0d2 cli/sparse: Fix clippy warning on non-Linux/Android builds
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 21:21:11 -04:00
Andrew Gunnerson 5a7885674e ci.yml: Use target name in Rust cache key
Otherwise, the x86_64-unknown-linux-gnu build running on Ubuntu 22.04
can use the cache from the aarch64-linux-android31 build that originally
ran on Ubuntu 24.04. This fails due to some cached components having
been compiled against a newer version of glibc.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 21:13:33 -04:00
Andrew Gunnerson c974ab5ec3 Use Rust conventional comment style for license headers
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-22 14:58:01 -04:00
135 changed files with 9842 additions and 5071 deletions
+17 -10
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,23 +28,26 @@ 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:
- aarch64-apple-darwin
- x86_64-apple-darwin
combine: lipo
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
- os: ubuntu-24.04
- os: ubuntu-latest
name: aarch64-linux-android31
targets:
- aarch64-linux-android
android_api: '31'
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# For git describe
fetch-depth: 0
@@ -65,7 +70,7 @@ jobs:
run: |
cargo install \
--git https://github.com/chenxiaolong/cargo-android \
--tag v0.1.1
--tag v0.1.3
- name: Get version
id: get_version
@@ -84,14 +89,16 @@ jobs:
done
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
key: ${{ matrix.artifact.name }}
- name: Clippy
shell: bash
run: |
for target in ${TARGETS}; do
cargo android \
clippy --release --workspace --features static \
clippy --release --workspace \
--target "${target}"
done
@@ -100,7 +107,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
build --release --workspace --features static \
build --release --workspace \
--target "${target}"
done
@@ -109,7 +116,7 @@ jobs:
run: |
for target in ${TARGETS}; do
cargo android \
test --release --workspace --features static \
test --release --workspace \
--target "${target}"
done
@@ -118,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
@@ -153,7 +160,7 @@ jobs:
run: cp LICENSE README.md target/output/
- name: Archive executable
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ matrix.artifact.name }}
path: |
+2 -3
View File
@@ -1,4 +1,3 @@
---
name: cargo-deny
on:
push:
@@ -11,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Run cargo-deny
uses: EmbarkStudios/cargo-deny-action@v1
uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2.0.15
+2 -3
View File
@@ -1,4 +1,3 @@
---
name: Github Release
on:
push:
@@ -25,10 +24,10 @@ jobs:
echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Create release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
with:
tag_name: v${{ steps.get_version.outputs.version }}
name: Version ${{ steps.get_version.outputs.version }}
+307
View File
@@ -7,6 +7,189 @@
to update the actual links at the bottom of the file.
-->
### 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](./README.md#changing-virtual-ab-cow-compression-algorithm) for more details.
* Switch back to the ring library now that it is maintained again ([PR #438])
* Update dependencies ([PR #439])
### Version 3.14.0
* Report as many errors as possible before failing in `avbroot ota verify` and improve error messages ([Discussion #426], [PR #428], [PR #430])
* Fix new clippy warnings introduced in Rust 1.85 ([PR #429])
* Fix massive performance regression introduced in 3.13.0 for OTAs that use gzip for virtual A/B CoW compression ([Issue #433], [PR #434])
* Update dependencies ([PR #435])
### Version 3.13.0
* Fix parsing Samsung `super.img` files in `avbroot lp` due to Samsung putting their own data structures in a region that's supposed to be filled with zeros ([PR #415])
* Add advanced option to skip replacing the OTA certificate in the system image ([Discussion #417], [PR #418])
* Switch to stable bzip2-rs release and use zlib-rs as the backend for flate2 ([PR #421])
* Switch to the aws-lc cryptography library for SHA1 and SHA2 hashing ([PR #422])
* The ring library is no longer maintained
* Fix incorrect `Partitions aren't protected by AVB: system` warning when using `--skip-system-ota-cert` ([PR #423])
* Discard unneeded temp file sooner when using `--skip-system-ota-cert` ([PR #424])
* Make `avbroot lp`'s parser less strict so that it can load on-device `super` partitions ([PR #425])
* The on-disk layout on virtual A/B devices violates some requirements stated in AOSP's documentation
* Update dependencies ([PR #427])
### Version 3.12.0
* Add new `-p <name>` option to `avbroot ota extract` for extracting specific partitions ([PR #408])
* Deprecate the `--boot-only` option in `avbroot ota extract` ([PR #408])
* The option will remain indefinitely for backwards compatibility, but is hidden from `--help`
* Add support for extracting the embedded OTA certificate and AVB public key in `avbroot ota extract` ([PR #409])
* Rename `avbroot key extract-avb` to `avbroot key encode-avb` for consistency with `avbroot key decode-avb` ([PR #410])
* The old syntax will remain supported indefinitely for backwards compatibility, but is hidden from `--help`
* Update dependencies ([PR #411])
### Version 3.11.0
* Fix crash when ignoring warning about `--magisk-preinit-device` not being specified ([PR #394])
* When using `--ignore-magisk-warnings`, assume that unsupported Magisk versions newer than the latest supported version are capable of all features ([Issue #393], [PR #395])
* Update bzip2-rs and switch to the Rust backend ([PR #397], [PR #402])
* Minor code cleanup for custom integer range type ([PR #398])
* Improve errors to make them less ambiguous about what went wrong ([PR #401])
* Fix bug where a vendor v4 boot image that was truncated in the bootconfig padding section would be accepted as valid ([PR #401])
* Avoid performing many small I/O operations when reading and writing cpio archives ([PR #403])
* Update dependencies ([PR #404])
### Version 3.10.0
* Switch to using zerocopy library for all binary file format parsers ([PR #384])
* Update to latest AOSP protobuf schema for the `payload.bin` metadata file format ([PR #385])
* Update dependencies and pin Github Actions actions to specific commits ([PR #386], [PR #392])
* Improve error messages from file format parsers ([PR #390])
* Add support for Magisk 28100 ([PR #391])
### Version 3.9.0
* Update all dependencies ([PR #368], [PR #377])
* Add advanced option to skip replacing the OTA certificate in the recovery image ([Issue #366], [PR #367], [PR #371])
* Improve error message when an incompatible RSA key is used for AVB signing ([Issue #366], [PR #369])
* Fix clippy warnings ([PR #370])
* Allow `avbroot ota verify` to verify OTAs that lack `META-INF/com/android/metadata.pb` ([Issue #366], [PR #373])
* Allow `avbroot ota verify` to verify OTAs where the payload signature does not set `unpadded_signature_size` ([Issue #366], [PR #374])
* Allow `avbroot sparse` to parse sparse images with unknown fields (matches AOSP implementation) ([PR #376])
### Version 3.8.0
* Add `avbroot avb digest` subcommand for computing the special vbmeta digest ([PR #363])
* Update all dependencies ([PR #364])
### Version 3.7.1
* Add support for Magisk 28000 ([PR #362])
### Version 3.7.0
* Fix a nasty regression since version 2.0.0 where recovery mode's `otacerts.zip` modifications were lost when using `--prepatched` with Magisk on some older devices, like the Pixel 4a ([Issue #356], [PR #357])
@@ -232,6 +415,8 @@ Behind-the-scenes changes:
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
[Discussion #417]: https://github.com/chenxiaolong/avbroot/discussions/417
[Discussion #426]: https://github.com/chenxiaolong/avbroot/discussions/426
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
@@ -256,6 +441,19 @@ Behind-the-scenes changes:
[Issue #328]: https://github.com/chenxiaolong/avbroot/issues/328
[Issue #332]: https://github.com/chenxiaolong/avbroot/issues/332
[Issue #356]: https://github.com/chenxiaolong/avbroot/issues/356
[Issue #366]: https://github.com/chenxiaolong/avbroot/issues/366
[Issue #393]: https://github.com/chenxiaolong/avbroot/issues/393
[Issue #433]: https://github.com/chenxiaolong/avbroot/issues/433
[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
[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
@@ -364,3 +562,112 @@ Behind-the-scenes changes:
[PR #354]: https://github.com/chenxiaolong/avbroot/pull/354
[PR #355]: https://github.com/chenxiaolong/avbroot/pull/355
[PR #357]: https://github.com/chenxiaolong/avbroot/pull/357
[PR #362]: https://github.com/chenxiaolong/avbroot/pull/362
[PR #363]: https://github.com/chenxiaolong/avbroot/pull/363
[PR #364]: https://github.com/chenxiaolong/avbroot/pull/364
[PR #367]: https://github.com/chenxiaolong/avbroot/pull/367
[PR #368]: https://github.com/chenxiaolong/avbroot/pull/368
[PR #369]: https://github.com/chenxiaolong/avbroot/pull/369
[PR #370]: https://github.com/chenxiaolong/avbroot/pull/370
[PR #371]: https://github.com/chenxiaolong/avbroot/pull/371
[PR #373]: https://github.com/chenxiaolong/avbroot/pull/373
[PR #374]: https://github.com/chenxiaolong/avbroot/pull/374
[PR #376]: https://github.com/chenxiaolong/avbroot/pull/376
[PR #377]: https://github.com/chenxiaolong/avbroot/pull/377
[PR #384]: https://github.com/chenxiaolong/avbroot/pull/384
[PR #385]: https://github.com/chenxiaolong/avbroot/pull/385
[PR #386]: https://github.com/chenxiaolong/avbroot/pull/386
[PR #390]: https://github.com/chenxiaolong/avbroot/pull/390
[PR #391]: https://github.com/chenxiaolong/avbroot/pull/391
[PR #392]: https://github.com/chenxiaolong/avbroot/pull/392
[PR #394]: https://github.com/chenxiaolong/avbroot/pull/394
[PR #395]: https://github.com/chenxiaolong/avbroot/pull/395
[PR #397]: https://github.com/chenxiaolong/avbroot/pull/397
[PR #398]: https://github.com/chenxiaolong/avbroot/pull/398
[PR #401]: https://github.com/chenxiaolong/avbroot/pull/401
[PR #402]: https://github.com/chenxiaolong/avbroot/pull/402
[PR #403]: https://github.com/chenxiaolong/avbroot/pull/403
[PR #404]: https://github.com/chenxiaolong/avbroot/pull/404
[PR #408]: https://github.com/chenxiaolong/avbroot/pull/408
[PR #409]: https://github.com/chenxiaolong/avbroot/pull/409
[PR #410]: https://github.com/chenxiaolong/avbroot/pull/410
[PR #411]: https://github.com/chenxiaolong/avbroot/pull/411
[PR #415]: https://github.com/chenxiaolong/avbroot/pull/415
[PR #418]: https://github.com/chenxiaolong/avbroot/pull/418
[PR #421]: https://github.com/chenxiaolong/avbroot/pull/421
[PR #422]: https://github.com/chenxiaolong/avbroot/pull/422
[PR #423]: https://github.com/chenxiaolong/avbroot/pull/423
[PR #424]: https://github.com/chenxiaolong/avbroot/pull/424
[PR #425]: https://github.com/chenxiaolong/avbroot/pull/425
[PR #427]: https://github.com/chenxiaolong/avbroot/pull/427
[PR #428]: https://github.com/chenxiaolong/avbroot/pull/428
[PR #429]: https://github.com/chenxiaolong/avbroot/pull/429
[PR #430]: https://github.com/chenxiaolong/avbroot/pull/430
[PR #434]: https://github.com/chenxiaolong/avbroot/pull/434
[PR #435]: https://github.com/chenxiaolong/avbroot/pull/435
[PR #437]: https://github.com/chenxiaolong/avbroot/pull/437
[PR #438]: https://github.com/chenxiaolong/avbroot/pull/438
[PR #439]: https://github.com/chenxiaolong/avbroot/pull/439
[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
Generated
+579 -551
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -4,7 +4,17 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.7.0"
version = "3.24.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"
missing_fields_in_debug = "warn"
redundant_clone = "deny"
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+22 -3
View File
@@ -30,8 +30,6 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
* To force an image to be signed, use `--key <path> --force`.
* To force an image to be unsigned, use `--force` without specifying `--key`.
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
@@ -60,12 +58,33 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
avbroot avb verify -i <root vbmeta image> -p <public key>
```
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All images are expected to be in the same directory as the vbmeta image being verified. Missing images are ignored by default because the vbmeta images in some OTAs reference partitions that only exist on a real device. `--fail-if-missing` can be used to override this.
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
### Verifying AVB hashes and signatures on device
```bash
# Run from a root adb shell:
avbroot avb verify-device [-p <public key>]
```
This subcommand is like `avbroot avb verify`, except that it verifies the actual partitions on the device instead of a directory of image files. This is only available in the Android build of avbroot.
If `-p` is omitted, the signatures are verified against the public key SHA-256 digest reported by the bootloader. This is the same digest shown on screen every time the device boots.
### Computing vbmeta digest
```bash
avbroot avb digest -i <root vbmeta image>
```
This subcommand computes the vbmeta digest, which is defined as the SHA256 digest of the root vbmeta partition's header, followed by the chained partitions' headers (if any) in the order that they are listed. Chained partitions more than one level deep are ignored.
This digest is equal to the value of the `ro.boot.vbmeta.digest` property or the `RootOfTrust.verifiedBootHash` hardware attestation field.
## `avbroot boot`
### Unpacking a boot image
+112 -20
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
@@ -121,7 +121,7 @@ When patching OTAs for multiple devices, generating unique keys for each device
2. Convert the public key portion of the AVB signing key to the AVB public key metadata format. This is the format that the bootloader requires when setting the custom root of trust.
```bash
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
avbroot key encode-avb -k avb.key -o avb_pkmd.bin
```
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
@@ -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
@@ -297,8 +303,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
```bash
avbroot ota extract \
--input /path/to/ota.zip \
--directory . \
--boot-only
--partition <name> # init_boot or boot, depending on device
```
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
@@ -385,6 +390,29 @@ Note that avbroot will validate that the prepatched image is compatible with the
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
### Skipping OTA certificate patches
avbroot can skip modifying `otacerts.zip` with the `--skip-system-ota-cert` and `--skip-recovery-ota-cert` options. **Do not use these unless you have a good reason to do so.**
When `--skip-system-ota-cert` is used, the OTA certificates in the `system` partition will not be modified. This prevents custom OTA updater apps from installing further patched OTAs while booted into Android.
When `--skip-recovery-ota-cert` is used, the OTA certificates in the `vendor_boot` or `recovery` partition will not be modified. **This prevents sideloading further patched OTAs from recovery mode.**
If `--skip-recovery-ota-cert` is used because the OTA certificate was already manually added to the boot image, then [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done. The verification process is only capable of checking the boot image's copy of the OTA certificates, not the system image's copy of them.
### Skipping all patches
To have avbroot make the absolute minimal changes:
* Specify `--skip-system-ota-cert`
* Specify `--skip-recovery-ota-cert`
* Specify `--rootless`
* Omit `--dsu`
This will re-sign the `vbmeta` partition and the OTA with the custom keys, but leave all other partitions untouched.
**This should only be used for advanced troubleshooting.** Without the OTA certificate patches, the resulting OTA will not be able to install further updates.
### Replacing partitions
avbroot supports replacing entire partitions in the OTA, even partitions that are not boot images (eg. `vendor_dlkm`). A partition can be replaced by passing in `--replace <partition name> /path/to/partition.img`.
@@ -409,6 +437,18 @@ Verified boot is disabled by vbmeta's header flags: 0x3
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
### Changing virtual A/B CoW compression algorithm
The virtual A/B CoW compression algorithm can be changed by passing in `--vabc-algo <algo>` with `gz` or `lz4`. OTAs normally use an algorithm that is compatible with the initial version of Android shipped on the device.
* Devices launching with Android 12 support `gz` and `brotli` (unsupported by avbroot)
* Devices launching with Android 14 support `lz4`
* Devices launching with Android 15 support `zstd` (unsupported by avbroot)
Picking a fast algorithm, like lz4, can speed up OTA installation significantly when installing via a custom OTA updater app. However, there is no performance difference when sideloading an OTA from recovery mode.
Note that the currently running version of Android must support the specified compression algorithm or else the OTA will fail to install. For example, trying to install an Android 14 OTA that uses lz4 CoW compression will fail if the running system is Android 13.
### Non-interactive use
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
@@ -445,17 +485,20 @@ avbroot prompts for the private key passphrases interactively by default. To run
* Use unencrypted private keys. This is strongly discouraged.
### Extracting the entire OTA
### Extracting an OTA
To extract all images contained within the OTA's `payload.bin`, run:
To extract the partition images contained within an OTA's `payload.bin`, run:
```bash
avbroot ota extract \
--input /path/to/ota.zip \
--directory extracted \
--all
--directory extracted
```
By default, this only extracts the images that could potentially be patched by avbroot. To extract all images, use the `--all` option. To extract specific images, use the `--partition <name>` option, which can be specified multiple times.
This command also supports extracting the embedded OTA certificate and AVB public key using the `--cert-ota` and `--public-key-avb` options. To extract only these components, pass in `--none` to skip extracting partition images.
### Zip write mode
By default, avbroot uses streaming writes for the output OTA during patching. This means it computes the sha256 digest for the digital signature as the file is being written. This mode causes the zip file to contain data descriptors, which is part of the zip standard and works on the vast majority of devices. However, some devices may have broken zip file parsers and fail to properly read OTA zip files containing data descriptors. If this is the case, pass in `--zip-mode seekable` when patching.
@@ -484,6 +527,57 @@ By default, this behavior is compatible with the `--signing_helper` option in AO
Note that avbroot will verify the signature returned by helper program against the public key. This ensures that the patching process will fail appropriately if the wrong private key was used.
### 16K page size developer option
On recent devices running Android 16 and newer, there may be an option in Android's developer options to switch to a 16K page size kernel. This will not work when running an avbroot-patched OS. The switch internally works by flashing incremental OTAs:
* `/vendor/boot_otas/boot_ota_16k.zip` to switch to the 16K page size kernel (requires the `boot` partition to be currently flashed with the 4K kernel)
* `/vendor/boot_otas/boot_ota_4k.zip` to switch to the 4K page size kernel (requires the `boot` partition to be currently flashed with the 16K kernel)
These `boot_otas` are unflashable when running an avbroot-patched OS because the `payload.bin` inside of them are signed by the OEM's key. These are also not proper OTA files. They don't contain any OTA metadata and the zip file itself is not signed. It's nothing more than a plain old zip file that stores a signed `payload.bin`.
There are no plans to add support for patching these `boot_otas`. It requires support for modifying filesystems and handling incremental OTAs, both of which are very non-trivial.
Folks who are determined to make this work anyway can try these manual steps to sign these `boot_otas` with your own key. Since the incremental OTAs are not being regenerated, the `boot` partition must be left unmodified when running `avbroot ota patch`.
1. Unpack `vendor.img` with avbroot and [afsr](https://github.com/chenxiaolong/afsr).
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Extract `payload.bin` from `boot_otas/boot_ota_16k.zip`.
3. Re-sign `payload.bin` with your OTA key.
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Create a new zip of `payload.bin` and `payload_properties.txt`. The files must be stored uncompressed (eg. with `zip -0`).
5. Repeat the procedure for `boot_otas/boot_ota_4k.zip`.
6. Repack `vendor.img` and sign it with your AVB key.
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Patch the (normal) OTA with:
```bash
avbroot ota patch \
--replace vendor <modified vendor> \
<normal arguments...>
```
## Building from source
Make sure the [Rust toolchain](https://www.rust-lang.org/) is installed. Then run:
@@ -496,8 +590,6 @@ The output binary is written to `target/release/avbroot`.
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
### Android cross-compilation
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
+118 -28
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.
@@ -43,7 +41,7 @@ avbroot модифицирует следующие образы:
## Использование
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
1. Убедитесь, что вы ознакомились и поняли указанные выше [предостережения.](#предостережения)
2. Скачайте последнюю версию со страницы [релизов.](https://github.com/chenxiaolong/avbroot/releases) Чтобы сверить цифровую подпись, см. раздел [проверки цифровых подписей.](#проверка-цифровых-подписей)
@@ -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.
@@ -119,10 +119,10 @@ avbroot модифицирует следующие образы:
2. Преобразуйте публичную часть ключа подписи AVB в формат метаданных публичного ключа AVB. Именно этот формат используется в загрузчике устройства для установки пользовательского ключа.
```bash
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
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-обновление, а затем повторяя попытку загрузки и установки при неудачной проверке подписи.
@@ -295,6 +301,7 @@ Magisk версии 25211 и новее требует наличие разде
--input /path/to/ota.zip \
--directory . \
--boot-only
--partition <название раздела> # init_boot или boot, в зависимости от устройства
```
2. Теперь нужно пропатчить загрузочный образ с помощью приложения Magisk. Это **ДОЛЖНО** быть сделано именно на целевом устройстве или устройстве той же модели! Имя раздела будет неверным и не подойдет, если пропатчить образ на устройстве иной модели.
@@ -316,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
@@ -377,15 +384,38 @@ avbroot может подменить используемый загрузоч
Обратите внимание, что avbroot проверяет совместимость предварительно пропатченного образа с оригинальным. Например, если поля заголовка образа не совпадают, или вовсе указан иной, незагрузочный образ, то процесс патча будет прерван. Эти проверки, конечно, ничего не гарантируют, но должны предостеречь от случайного использования некорректного образа. Чтобы обойти базовые проверки безопасности, укажите аргумент `--ignore-prepatched-compat`. Если вы хотите убрать вообще все проверки (чего делать крайне не рекомендуется), укажите его дважды.
### Пропуск патчей для root-доступа
### Пропуск патча для root-доступа
avbroot можно использовать для простого переподписания OTA, указав аргумент `--rootless` вместо `--magisk`/`--prepatched`. В таком случае пропатченный OTA не будет рутирован. Единственная модификация, которая будет применена – это замена сертификата проверки OTA, чтобы систему можно было обновлять с помощью будущих пропатченных OTA.
### Подмена разделов
### Пропуск патчинга сертификата OTA
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, указав аргумент `--replace <имя раздела> /путь/к/образу.img`.
Вы можете пропустить изменение otacerts.zip, используя аргументы `--skip-system-ota-cert` и `--skip-recovery-ota-cert`. **Не используйте их без веской причины.**
Единственное, что меняется – это то, откуда считывается раздел. При использовании `--replace` вместо использования образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Таким образом, заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
При использовании `--skip-system-ota-cert`, сертификаты OTA в образе `system` изменены не будут. Это не позволит сторонним приложениям для OTA-обновлений устанавливать будущие пропатченные OTA из-под загруженной системы.
При использовании `--skip-recovery-ota-cert`, сертификаты OTA в образах `vendor_boot` или `recovery` изменены не будут. **Это не позволит устанавливать будущие пропатченные OTA в режиме Recovery.**
Если вы используете аргумент `--skip-recovery-ota-cert`, потому что уже добавили сертификат OTA в загрузочный образ вручную, рекомендуетcя [проверить пропатченный OTA](#проверка-ota), дабы удостовериться, что замена произведена корректно. Процесс верификации проверяет только копию сертификатов OTA в загрузочном образе, не проверяя копию в образе системы.
### Пропуск всех патчей
Чтобы внести самый минимум изменений, укажите аргументы:
* `--skip-system-ota-cert`
* `--skip-recovery-ota-cert`
* `--rootless`
* не используйте аргумент `--dsu`.
Так, пользовательскими ключами будут переподписаны лишь образ `vbmeta` и OTA, остальные разделы останутся нетронутыми.
**Это следует использовать только для устранения неполадок.** Без патчей сертификатов, поверх полученного OTA не получится установить никакие обновления.
### Подмена образов
avbroot поддерживает подмену целых образов в OTA, даже тех, что не являются загрузочными (например, `vendor_dlkm`). Образ можно заменить, используя аргумент `--replace <имя раздела> /путь/к/образу.img`.
Единственное, что меняется – это то, откуда считывается файл. При использовании `--replace` вместо образа раздела из оригинального `payload.bin` в OTA, он берется напрямую по указанному вами пути. Заменяющие образы разделов должны иметь правильные колонтитулы vbmeta, соответствующие оригинальным.
Это не влияет на ход применения пачтей. Например, при использовании Magisk, патч получения root-прав применяется к загрузочному образу одинаково, независимо от того, был ли он получен из оригинального `payload.bin` или это файл, указанный через `--replace`.
@@ -399,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 в неинтерактивном режиме, можно:
@@ -435,17 +477,20 @@ Verified boot is disabled by vbmeta's header flags: 0x3
* Использовать незашифрованные приватные ключи. Крайне не рекомендуется.
### Извлечение всей OTA
### Извлечение образов из OTA
Чтобы извлечь все образы, содержащиеся в `payload.bin`, используйте команду:
Чтобы извлечь образы разделов, содержащихся в `payload.bin`, используйте команду:
```bash
avbroot ota extract \
--input /путь/к/ota.zip \
--directory extracted \
--all
--directory extracted
```
По умолчанию извлекаются только те образы, которые потенциально могут быть пропатчены с помощью avbroot. Чтобы извлечь все образы, используйте опцию `--all`. Для извлечения конкретных образов используйте опцию `--partition <название раздела>`, которую можно указать несколько раз.
Эта команда также поддерживает извлечение встроенного сертификата OTA и публичного ключа AVB с помощью опций `--cert-ota` и `--public-key-avb`. Чтобы извлечь только эти компоненты, укажите аргумент `--none`, чтобы пропустить извлечение образов разделов.
### Режим записи ZIP
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
@@ -474,6 +519,53 @@ avbroot поддерживает делегирование всех опера
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
### Размер страницы 16 КБ в настройках для разработчиков
На современных устройствах с Android 16 и выше, в настройках для разработчиков может появиться опция переключения на ядро с размером страницы 16 КБ. Однако, эта функция не будет работать в системе, пропатченной с помощью avbroot, поскольку переключение данной настройки осуществляется путём установки инкрементальной OTA:
* `/vendor/boot_otas/boot_ota_16k.zip` — используется для переключения на ядро с размером страницы 16 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 4K)
* `/vendor/boot_otas/boot_ota_4k.zip` — используется для переключения на ядро с размером страницы 4 КБ (в разделе `boot` уже должно быть прошито ядро с размером страницы 16K)
Эти файлы (в `boot_otas`) невозможно прошить на системе, пропатченной avbroot, потому что `payload.bin` внутри них подписан ключом производителя. Кроме того, это неполноценные OTA-файлы: у них нет метаданных, характерных для OTA, а сам zip-файл не подписан. Это просто обычный архив, который содержит подписанный `payload.bin`.
Поддержка `boot_otas` не планируется. Это потребует реализации функционала для модификации ФС в инкрементальных OTA и их дальнейшей обработки, что сделать очень непросто.
Если вы всё же хотите завести эту функцию, можно попробовать вручную подписать файлы в `boot_otas` собственным ключом. Поскольку инкрементальные OTA не пересоздаются, раздел `boot` должен оставаться без изменений во время выполнения команды `avbroot ota patch`.
1. Распакуйте `vendor.img` с помощью avbroot и [afsr](https://github.com/chenxiaolong/afsr):
```bash
avbroot avb unpack -i vendor.img
afsr unpack -i raw.img
```
2. Извлеките `payload.bin` из `boot_otas/boot_ota_16k.zip`.
3. Переподпишите `payload.bin` вашим OTA-ключом:
```bash
avbroot payload repack \
-i payload.bin.orig \
-o payload.bin \
-k ota.key \
--output-properties payload_properties.txt
```
4. Создайте новый zip, включающий `payload.bin` и `payload_properties.txt`. Файлы должны быть добавлены без сжатия (например, с помощью `zip -0`).
5. Повторите эту процедуру для `boot_otas/boot_ota_4k.zip`.
6. Соберите `vendor.img` обратно и подпишите его вашим AVB-ключом:
```bash
afsr pack -o raw.img
avbroot avb pack -o vendor.img -k avb.key --recompute-size
```
7. Пропатчите обычный OTA-архив с прошивкой, подменив `vendor` на модифицированный образ:
```bash
avbroot ota patch \
--replace vendor <модифицированный vendor.img> \
<дальше указываются аргументы, как при обычном патчинге>
```
## Сборка из исходного кода
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
@@ -486,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, выполните:
+28 -39
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,79 +14,67 @@ anyhow = "1.0.75"
base64 = "0.22.1"
bitflags = { version = "2.4.1", features = ["serde"] }
bstr = "1.6.2"
byteorder = "1.4.3"
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"] }
# We can't upgrade to 0.10.0 until x509-cert updates it too, since it's part of
# the public API.
const-oid = "0.9.5"
crc32fast = "1.4.2"
ctrlc = "3.4.0"
dlv-list = "0.5.2"
flate2 = "1.0.27"
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.12.0"
lzma-rust2 = "0.15.2"
memchr = "2.6.0"
miniz_oxide = "0.8.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
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
# instructions. sha2 is still used for signing purposes.
# https://github.com/RustCrypto/hashes/issues/327
ring = "0.17.0"
ring = "0.17.14"
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
serde = { version = "1.0.188", features = ["derive"] }
sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "1.0.47"
toml_edit = { version = "0.22.9", features = ["serde"] }
thiserror = "2.0.3"
toml = { version = "0.9.11", features = ["serde"] }
topological-sort = "0.2.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
x509-cert = { version = "0.2.4", features = ["builder"] }
zerocopy = "0.7.35"
zerocopy-derive = "0.7.35"
# There are multiple upstream bugs that cause infinite loops in the Drop
# implementation of write::BzDecoder. Unfortunately, the project is no longer
# maintained, so we have to maintain our own fork with the necessary fixes.
[dependencies.bzip2]
git = "https://github.com/chenxiaolong/bzip2-rs"
rev = "6e0f9836ec87b19261461b6cc1772e14aff8e851"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
features = ["deflate"]
zerocopy = { version = "0.8.10", features = ["std"] }
zerocopy-derive = "0.8.5"
[target.'cfg(unix)'.dependencies]
libc = "0.2.158"
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
rustix = { version = "1.0.3", default-features = false, features = ["process"] }
[target.'cfg(target_os = "android")'.dependencies]
system-properties = { git = "https://github.com/chenxiaolong/system-properties", tag = "v0.2.1" }
[build-dependencies]
constcat = "0.5.0"
prost-build = "0.13.1"
protox = "0.7.0"
constcat = "0.6.0"
prost-build = "0.14.1"
protox = "0.9.0"
[dev-dependencies]
assert_matches = "1.5.0"
[features]
static = ["bzip2/static", "liblzma/static"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
[lints]
workspace = true
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{env, ffi::OsStr, fs, io, path::Path};
+8
View File
@@ -316,6 +316,10 @@ message PartitionUpdate {
// as a hint. If set to 0, libsnapshot should use alternative
// methods for estimating size.
optional uint64 estimate_cow_size = 19;
// Information about the cow used by Cow Writer to specify
// number of cow operations to be written
optional uint64 estimate_op_count_max = 20;
}
message DynamicPartitionGroup {
@@ -368,6 +372,10 @@ message DynamicPartitionMetadata {
// A collection of knobs to tune Virtual AB Compression
optional VABCFeatureSet vabc_feature_set = 6;
// Max bytes to be compressed at once during ota. Options: 4k, 8k, 16k, 32k,
// 64k, 128k
optional uint64 compression_factor = 7;
}
// Definition has been duplicated from
+8 -12
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
@@ -12,7 +10,7 @@ use std::{
use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, Level};
use tracing::{Level, debug};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
@@ -32,26 +30,24 @@ pub enum Command {
Payload(payload::PayloadCli),
Sparse(sparse::SparseCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
#[command(hide = true)]
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
#[command(hide = true)]
Extract(ota::ExtractCli),
/// (Deprecated: Use `avbroot boot magisk-info` instead.)
#[command(hide = true)]
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())
+386 -142
View File
@@ -1,28 +1,23 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fs::{self, File},
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
ffi::OsString,
fmt,
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Cursor, Seek, SeekFrom, Write},
path::{Path, PathBuf},
str,
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},
@@ -30,7 +25,7 @@ use crate::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, PSeekFile, Reopen},
stream::{self, ReadFixedSizeExt, ToWriter, UserPosFile, check_cancel},
util,
};
@@ -57,7 +52,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 {
@@ -83,7 +78,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)
@@ -91,25 +86,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(
@@ -117,14 +100,13 @@ fn write_raw(
reader: &mut BufReader<File>,
size: u64,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
) -> Result<File> {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
let mut writer = BufWriter::new(file);
@@ -149,7 +131,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()?;
@@ -163,7 +145,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)?;
@@ -186,7 +168,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
@@ -197,13 +179,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)
@@ -315,14 +295,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);
}
}
@@ -342,7 +321,13 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
}
let originally_signed = !info.header.signature.is_empty();
let mut sign_action = if originally_signed && &info.header != orig_header {
let sign_action = if key_group.force {
if key_group.key.is_some() {
SignAction::Sign
} else {
SignAction::Clear
}
} 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
@@ -351,14 +336,6 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
SignAction::None
};
if key_group.force {
sign_action = if key_group.key.is_some() {
SignAction::Sign
} else {
SignAction::Clear
};
}
match sign_action {
SignAction::None => {
if originally_signed {
@@ -400,7 +377,9 @@ fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup)
RsaSigningKey::Internal(private_key)
};
info.header.set_algo_for_key(&signing_key)?;
info.header
.set_algo_for_key(&signing_key)
.context("Failed to set signature algorithm")?;
info.header
.sign(&signing_key)
.context("Failed to sign new AVB header")?;
@@ -427,22 +406,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<()> {
@@ -450,12 +489,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:?}"))?;
@@ -467,19 +501,36 @@ pub fn verify_headers(
if let Some(k) = &public_key {
let prefix = format!("{name} has a signed vbmeta header");
if let Some(e) = expected_key {
if k == e {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
match trust_method {
TrustMethod::Key(expected) => {
if k == expected {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
}
}
TrustMethod::KeyDigest(expected_sha256) => {
let encoded = avb::encode_public_key(k)?;
let digest = Sha256::digest(&encoded);
if digest.as_slice() == expected_sha256 {
info!("{prefix}");
} else {
bail!("{prefix}, but is signed by an untrusted key");
}
}
TrustMethod::Anything => {
warn!("{prefix}, but parent does not list a trusted key");
}
} else {
warn!("{prefix}, but parent does not list a trusted key");
}
} else {
info!("{name} has an unsigned vbmeta header");
}
if header.flags != 0 {
warn!("{name} has insecure flags: {:#x}", header.flags);
}
for descriptor in &header.descriptors {
let Some(target_name) = descriptor.partition_name() else {
continue;
@@ -499,8 +550,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)?;
}
_ => {}
}
@@ -515,30 +567,27 @@ pub fn verify_headers(
/// work.
fn verify_and_repair(
name: Option<&str>,
mut file: PSeekFile,
file: &File,
descriptor: AppendedDescriptorRef,
repair: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let _span = debug_span!("image", name = name.unwrap_or_default()).entered();
let suffix = match name {
Some(n) => format!(" for: {n}"),
None => String::new(),
};
let suffix = name.map_or_else(String::new, |n| format!(" for: {n}"));
match descriptor {
AppendedDescriptorRef::HashTree(d) => {
info!("Verifying hash tree descriptor{suffix}");
match d.verify(&file, cancel_signal) {
Err(e @ avb::Error::HashTree(_)) if repair => {
Err(e @ avb::Error::HashTreeVerify(_)) if 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).map(|_| {
d.verify(&file, cancel_signal).inspect(|()| {
info!("Successfully repaired data{suffix}");
})
}
@@ -549,8 +598,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}"))?;
}
}
@@ -561,45 +609,124 @@ 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,
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: &Path,
name: &str,
context: &mut ring::digest::Context,
max_depth: u8,
seen: &mut HashSet<String>,
cancel_signal: &AtomicBool,
) -> Result<()> {
if max_depth == 0 {
return Ok(());
}
check_cancel(cancel_signal)?;
seen.insert(name.to_owned());
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)
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
// We don't have a good way to get the length of the header, so we serialize
// what we just parsed and compare it to the raw file so ensure that the
// round-tripped data is identical.
let raw_header = {
let mut writer = Cursor::new(Vec::new());
header
.to_writer(&mut writer)
.with_context(|| format!("Failed to serialize header: {path:?}"))?;
writer.into_inner()
};
let header_offset = footer.map(|f| f.vbmeta_offset).unwrap_or_default();
raw_reader
.seek(SeekFrom::Start(header_offset))
.with_context(|| format!("Failed to seek file: {path:?}"))?;
let raw_header_orig = raw_reader
.read_vec_exact(raw_header.len())
.with_context(|| format!("Failed to reread AVB header: {path:?}"))?;
if raw_header != raw_header_orig {
bail!("Serialized header does not match original header: {path:?}");
}
context.update(&raw_header);
for descriptor in &header.descriptors {
if let avb::Descriptor::ChainPartition(d) = descriptor {
compute_digest_recursive(
directory,
&d.partition_name,
context,
max_depth - 1,
seen,
cancel_signal,
)
})
.collect()
)?;
}
}
Ok(())
}
/// Compute the vbmeta digest. This is defined as the digest of the header in
/// 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: &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);
compute_digest_recursive(directory, name, &mut context, 2, &mut seen, cancel_signal)?;
let digest = context.finish();
Ok(digest.as_ref().try_into().unwrap())
}
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
@@ -608,7 +735,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,
@@ -639,13 +766,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.
@@ -668,8 +794,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)?;
@@ -677,13 +802,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.
@@ -699,21 +823,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 mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
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 verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let directory = util::parent_path(&cli.input);
let name = cli
.input
.file_stem()
@@ -721,19 +864,73 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
let opener = ImageOpener::with_dir(directory);
verify_headers(
&directory,
verify_internal(
cli.public_key.as_deref(),
None,
&opener,
name,
public_key.as_ref(),
&mut seen,
&mut descriptors,
)?;
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
cli.repair,
!cli.fail_if_missing,
cancel_signal,
)
}
info!("Successfully verified all vbmeta signatures and hashes");
#[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));
Ok(())
}
@@ -745,6 +942,9 @@ 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),
}
}
@@ -825,6 +1025,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,
@@ -947,6 +1151,43 @@ 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.
///
/// This value is equal to what is reported by the ro.boot.vbmeta.digest
/// property on a real device.
#[derive(Debug, Parser)]
struct DigestCli {
/// Path to input AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
@@ -957,6 +1198,9 @@ enum AvbCommand {
#[command(alias = "dump")]
Info(InfoCli),
Verify(VerifyCli),
#[cfg(target_os = "android")]
VerifyDevice(VerifyDeviceCli),
Digest(DigestCli),
}
/// Pack, unpack, and inspect AVB-protected images.
+57 -17
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{self, File},
@@ -9,7 +7,7 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{bail, Context, Result};
use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use crate::{
@@ -43,14 +41,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:?}"))?;
@@ -166,30 +164,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(())
@@ -341,6 +353,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,
@@ -350,10 +366,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,
@@ -363,10 +387,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,
@@ -376,6 +408,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,
@@ -384,6 +420,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.
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io;
+35 -39
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{self, File},
@@ -11,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};
@@ -76,7 +73,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)
@@ -84,7 +81,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:?}"))?;
@@ -93,17 +90,17 @@ fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
/// Open reader to the corresponding file inside the tree if the entry is a
/// regular file. Unsafe paths will result in an error.
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
fn open_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
let sub_path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let path = util::path_join(tree, sub_path)?;
let mut reader = tree
.open(path)
.map(|f| BufReader::new(f.into_std()))
let mut reader = File::open(&path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let file_size = reader
@@ -124,21 +121,21 @@ fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<Fil
/// Open writer to the corresponding file inside the tree if the entry is a
/// regular file. Intermediate directories are automatically created as needed.
/// Unsafe paths will result in an error.
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
fn create_tree_file(tree: &Path, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
let sub_path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let parent = util::parent_path(path);
let path = util::path_join(tree, sub_path)?;
let parent = util::parent_path(&path);
tree.create_dir_all(parent)
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
let writer = tree
.create(path)
.map(|f| BufWriter::new(f.into_std()))
let writer = File::create(&path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok(Some(writer))
@@ -173,25 +170,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)?;
@@ -211,12 +207,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);
@@ -310,6 +302,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.
+5 -11
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{File, OpenOptions},
@@ -15,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:?}"))
}
@@ -93,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(())
+4 -7
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fs::{File, OpenOptions},
@@ -15,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:?}"))
}
+9 -8
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
ffi::OsString,
@@ -48,7 +46,7 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
crypto::write_pem_cert_file(&c.output, &cert)
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
}
KeyCommand::ExtractAvb(c) => {
KeyCommand::ExtractAvb(c) | KeyCommand::EncodeAvb(c) => {
let public_key = if let Some(p) = &c.input.key {
let passphrase = get_passphrase_source(&c.passphrase, p);
let private_key = crypto::read_pem_key_file(p, &passphrase)
@@ -152,12 +150,12 @@ struct GenerateCertCli {
validity: u64,
}
/// Extract the AVB public key from a private key or certificate.
/// Convert a key or certificate to an AVB-encoded public key.
///
/// The public key is stored in both the private key and the certificate. Either
/// one can be used interchangeably.
#[derive(Debug, Parser)]
struct ExtractAvbCli {
struct EncodeAvbCli {
/// Path to output AVB public key.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
@@ -185,7 +183,10 @@ struct DecodeAvbCli {
enum KeyCommand {
GenerateKey(GenerateKeyCli),
GenerateCert(GenerateCertCli),
ExtractAvb(ExtractAvbCli),
/// (Deprecated: Use `avbroot key encode-avb` instead.)
#[command(hide = true)]
ExtractAvb(EncodeAvbCli),
EncodeAvb(EncodeAvbCli),
DecodeAvb(DecodeAvbCli),
}
+47 -86
View File
@@ -1,18 +1,14 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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,
@@ -20,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<_>>>()
}
@@ -57,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:?}"))?;
@@ -131,22 +124,19 @@ fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
/// Use the CLI-specified slot or automatically select one if all slots are
/// identical.
fn get_slot_number(metadata: &Metadata, cli_slot: Option<u32>) -> Result<usize> {
match cli_slot {
Some(n) => {
let n = n as usize;
if n >= metadata.slots.len() {
bail!("Slot out of range: {n}");
}
Ok(n)
if let Some(n) = cli_slot {
let n = n as usize;
if n >= metadata.slots.len() {
bail!("Slot out of range: {n}");
}
None => {
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
bail!("A slot must be specified because they are not all identical");
}
Ok(0)
Ok(n)
} else {
if metadata.slots.windows(2).any(|w| w[0] != w[1]) {
bail!("A slot must be specified because they are not all identical");
}
Ok(0)
}
}
@@ -171,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.
@@ -191,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];
@@ -221,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()?)
@@ -256,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];
@@ -276,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<()> {
@@ -308,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
@@ -403,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];
@@ -423,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<()> {
@@ -508,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];
@@ -528,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<()> {
@@ -578,6 +535,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 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod args;
pub mod avb;
+941 -470
View File
File diff suppressed because it is too large Load Diff
+56 -47
View File
@@ -1,19 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
ffi::OsString,
fs::{self, File},
io::{BufReader, BufWriter, Seek, SeekFrom},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
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;
@@ -21,7 +18,8 @@ use crate::{
cli::ota,
crypto::{self, PassphraseSource, RsaSigningKey},
format::payload::{PayloadHeader, PayloadWriter},
stream::{self, FromReader, PSeekFile},
stream::{self, FromReader},
util,
};
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
@@ -54,14 +52,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:?}"))?;
@@ -115,27 +113,26 @@ fn unpack_subcommand(
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))?;
if !cli.no_output_images {
fs::create_dir_all(&cli.output_images)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?;
ota::extract_payload(
&PSeekFile::new(reader.into_inner()),
&directory,
0,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
ota::extract_payload(
&reader.into_inner(),
&cli.output_images,
0,
payload_size,
&header,
&header
.manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.cloned()
.collect(),
cancel_signal,
)?;
}
Ok(())
}
@@ -149,34 +146,42 @@ fn pack_subcommand(
let mut header = read_info(&cli.input_info)?;
let authority = ambient_authority();
let directory = Dir::open_ambient_dir(&cli.input_images, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?;
for p in &header.manifest.partitions {
let name = &p.partition_name;
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
}
}
// Pre-open all of the image files.
let input_files = header
.manifest
.partitions
.iter()
.map(|p| {
let path = format!("{}.img", p.partition_name);
let file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
let path =
util::path_join_single(&cli.input_images, format!("{}.img", p.partition_name))?;
let file = File::open(&path)
.map(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.
@@ -383,6 +388,10 @@ struct UnpackCli {
default_value = "payload_images"
)]
output_images: PathBuf,
/// Do not output images.
#[arg(long, conflicts_with = "output_images")]
no_output_images: bool,
}
/// Pack a payload binary.
+13 -14
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2024-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
@@ -12,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, AsBytes};
use zerocopy::{IntoBytes, little_endian};
use crate::{
format::{
@@ -30,7 +28,7 @@ use crate::{
struct CompactView<'a, T>(&'a [T]);
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
impl<T: fmt::Debug> fmt::Debug for CompactView<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
@@ -112,7 +110,7 @@ fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
#[cfg(any(target_os = "linux", target_os = "android"))]
fn find_allocated_regions(
path: &Path,
reader: &mut File,
reader: &File,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<u64>>> {
use rustix::{fs::SeekFrom, io::Errno};
@@ -124,13 +122,13 @@ fn find_allocated_regions(
loop {
stream::check_cancel(cancel_signal)?;
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
start = match rustix::fs::seek(reader, SeekFrom::Data(end)) {
Ok(offset) => offset,
Err(e) if e == Errno::NXIO => break,
Err(e) => return Err(e).with_context(|| format!("Failed to seek to data: {path:?}")),
};
end = rustix::fs::seek(&*reader, SeekFrom::Hole(start as i64))
end = rustix::fs::seek(reader, SeekFrom::Hole(start))
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
result.push(start..end);
@@ -319,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(_) => {}
@@ -377,12 +375,13 @@ fn pack_subcommand(
} else {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let regions = find_allocated_regions(&cli.input, &mut reader, cancel_signal)?;
let regions = find_allocated_regions(&cli.input, &reader, cancel_signal)?;
(regions, false)
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
{
#[allow(clippy::single_range_in_vec_init)]
(vec![0..file_size], true)
}
};
+164 -112
View File
@@ -1,13 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
env::{self, VarError},
ffi::{OsStr, OsString},
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Read, Write},
io::{self, Read, Write},
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
time::Duration,
@@ -23,41 +21,43 @@ 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;
#[derive(Debug, Error)]
pub enum Error {
#[error("Signature algorithm not supported: {0:?}")]
UnsupportedAlgorithm(SignatureAlgorithm),
#[error("RSA key size ({}) not supported", .0 * 8)]
UnsupportedKey(usize),
UnsupportedKeySize(usize),
#[error("Invalid digest length ({0} bytes) for {1:?}")]
InvalidDigestLength(usize, SignatureAlgorithm),
#[error("Invalid signature length ({0} bytes) for {1:?}")]
InvalidSignatureLength(usize, SignatureAlgorithm),
#[error("Failed to run command: {0}")]
CommandSpawnFailed(String, #[source] io::Error),
#[error("Command failed with status: {1}: {0}")]
CommandExecutionFailed(String, ExitStatus),
#[error("Failed to run command: {0:?}")]
CommandSpawn(DebugString, #[source] io::Error),
#[error("Command failed with status: {1}: {0:?}")]
CommandExecution(DebugString, ExitStatus),
#[error("Signature from signing helper does not match public key: {0:?}")]
SigningHelperBadSignature(PathBuf),
#[error("Passphrase prompt requires an interactive terminal")]
@@ -70,28 +70,40 @@ pub enum Error {
InvalidEnvVar(OsString, #[source] VarError),
#[error("PEM has start tag, but no end tag")]
PemNoEndTag,
#[error("Failed to load encrypted private key")]
#[error("Failed to load encrypted RSA private key")]
LoadKeyEncrypted(#[source] pkcs8::Error),
#[error("Failed to load unencrypted private key")]
#[error("Failed to load unencrypted RSA private key")]
LoadKeyUnencrypted(#[source] pkcs8::Error),
#[error("Failed to save encrypted private key")]
#[error("Failed to save encrypted RSA private key")]
SaveKeyEncrypted(#[source] pkcs8::Error),
#[error("Failed to save unencrypted private key")]
#[error("Failed to save unencrypted RSA private key")]
SaveKeyUnencrypted(#[source] pkcs8::Error),
#[error("Failed to load RSA public key")]
LoadPubKey(#[source] pkcs8::spki::Error),
#[error("Failed to save RSA public key")]
SavePubKey(#[source] pkcs8::spki::Error),
#[error("Failed to load X509 certificate")]
LoadCert(#[source] x509_cert::der::Error),
#[error("Failed to save X509 certificate")]
SaveCert(#[source] x509_cert::der::Error),
#[error("Failed to generate RSA key")]
RsaGenerate(#[source] Box<rsa::Error>),
#[error("Failed to RSA sign digest")]
RsaSign(#[source] rsa::Error),
RsaSign(#[source] Box<rsa::Error>),
#[error("Failed to RSA verify signature")]
RsaVerify(#[source] rsa::Error),
#[error("X509 error")]
X509(#[from] x509_cert::builder::Error),
#[error("SPKI error")]
Spki(#[from] pkcs8::spki::Error),
#[error("DER error")]
Der(#[from] x509_cert::der::Error),
#[error("RSA error")]
Rsa(#[from] rsa::Error),
#[error("I/O error")]
Io(#[from] io::Error),
RsaVerify(#[source] Box<rsa::Error>),
#[error("Failed to generate X509 certificate")]
CertGenerate(#[source] x509_cert::builder::Error),
#[error("Invalid parameters for X509 certificate generation")]
CertParams(#[source] x509_cert::der::Error),
#[error("Failed to CMS sign digest")]
CmsSign(#[source] x509_cert::der::Error),
#[error("Failed to parse CMS signature")]
CmsParse(#[source] x509_cert::der::Error),
#[error("Failed to read file: {0:?}")]
ReadFile(PathBuf, #[source] io::Error),
#[error("Failed to write file: {0:?}")]
WriteFile(PathBuf, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -132,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 {
@@ -147,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)));
@@ -177,8 +190,9 @@ impl PassphraseSource {
first
}
Self::EnvVar(v) => env::var(v).map_err(|e| Error::InvalidEnvVar(v.clone(), e))?,
Self::File(p) => fs::read_to_string(p)?
.trim_end_matches(&['\r', '\n'])
Self::File(p) => fs::read_to_string(p)
.map_err(|e| Error::ReadFile(p.clone(), e))?
.trim_end_matches(['\r', '\n'])
.to_owned(),
};
@@ -189,7 +203,7 @@ impl PassphraseSource {
fn check_key_size(size: usize) -> Result<()> {
// RustCrypto does not support 8192-bit keys.
if size > 4096 / 8 {
return Err(Error::UnsupportedKey(size));
return Err(Error::UnsupportedKeySize(size));
}
Ok(())
@@ -237,8 +251,8 @@ impl RsaSigningKey {
/// Get the public key portion of the signing key.
pub fn to_public_key(&self) -> RsaPublicKey {
match self {
RsaSigningKey::Internal(key) => key.to_public_key(),
RsaSigningKey::External { public_key, .. } => public_key.clone(),
Self::Internal(key) => key.to_public_key(),
Self::External { public_key, .. } => public_key.clone(),
}
}
@@ -258,7 +272,9 @@ impl RsaSigningKey {
};
match self {
Self::Internal(key) => key.sign(scheme, digest).map_err(Error::RsaSign),
Self::Internal(key) => key
.sign(scheme, digest)
.map_err(|e| Error::RsaSign(Box::new(e))),
Self::External {
program,
public_key,
@@ -294,7 +310,7 @@ impl RsaSigningKey {
let mut child = command
.spawn()
.map_err(|e| Error::CommandSpawnFailed(format!("{command:?}"), e))?;
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
// We don't bother with spawning a thread. The pipe capacity on
// all major OSs is significantly larger than the digest, so we
@@ -306,14 +322,22 @@ impl RsaSigningKey {
// * macOS: 4 KiB, 16 KiB (usually), or 64 KiB
// * Windows: 4 KiB
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())?;
child.stdin.as_mut().unwrap().write_all(&padded_digest)?;
let padded_digest = pkcs1v15_sign_pad(&scheme.prefix, digest, public_key.size())
.map_err(|e| Error::RsaSign(Box::new(e)))?;
child
.stdin
.as_mut()
.unwrap()
.write_all(&padded_digest)
.map_err(|e| Error::WriteFile("<signing helper stdin>".into(), e))?;
let child = child.wait_with_output()?;
let child = child
.wait_with_output()
.map_err(|e| Error::CommandSpawn(DebugString::new(&command), e))?;
if !child.status.success() {
return Err(Error::CommandExecutionFailed(
format!("{command:?}"),
return Err(Error::CommandExecution(
DebugString::new(&command),
child.status,
));
} else if child.stdout.len() != self.size() {
@@ -357,7 +381,7 @@ impl RsaPublicKeyExt for RsaPublicKey {
};
self.verify(scheme, digest, signature)
.map_err(Error::RsaVerify)
.map_err(|e| Error::RsaVerify(Box::new(e)))
}
}
@@ -366,7 +390,7 @@ pub fn generate_rsa_key_pair() -> Result<RsaPrivateKey> {
let mut rng = rand::thread_rng();
// avbroot supports 4096-bit keys only.
let key = RsaPrivateKey::new(&mut rng, 4096)?;
let key = RsaPrivateKey::new(&mut rng, 4096).map_err(|e| Error::RsaGenerate(Box::new(e)))?;
Ok(key)
}
@@ -378,20 +402,27 @@ pub fn generate_cert(
validity: Duration,
subject: &str,
) -> Result<Certificate> {
let public_key_der = key.to_public_key().to_public_key_der()?;
let public_key_der = key
.to_public_key()
.to_public_key_der()
.map_err(Error::SavePubKey)?;
let signing_key = SigningKey::<Sha256>::new(key.clone());
let builder = CertificateBuilder::new(
Profile::Root,
SerialNumber::from(serial),
Validity::from_now(validity)?,
subject.parse()?,
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())?,
Validity::from_now(validity).map_err(Error::CertParams)?,
subject.parse().map_err(Error::CertParams)?,
SubjectPublicKeyInfoOwned::from_der(public_key_der.as_bytes())
.map_err(Error::CertParams)?,
&signing_key,
)?;
)
.map_err(Error::CertGenerate)?;
let mut rng = rand::thread_rng();
let cert = builder.build_with_rng(&mut rng)?;
let cert = builder
.build_with_rng(&mut rng)
.map_err(Error::CertGenerate)?;
Ok(cert)
}
@@ -444,80 +475,92 @@ fn reformat_pem(data: &[u8]) -> Result<Vec<u8>> {
}
/// Read PEM-encoded certificate from a reader.
pub fn read_pem_cert(mut reader: impl Read) -> Result<Certificate> {
pub fn read_pem_cert(path: &Path, mut reader: impl Read) -> Result<Certificate> {
let mut data = vec![];
reader.read_to_end(&mut data)?;
reader
.read_to_end(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
let data = reformat_pem(&data)?;
let certificate = Certificate::from_pem(data)?;
let certificate = Certificate::from_pem(data).map_err(Error::LoadCert)?;
Ok(certificate)
}
/// Write PEM-encoded certificate to a writer.
pub fn write_pem_cert(mut writer: impl Write, cert: &Certificate) -> Result<()> {
let data = cert.to_pem(LineEnding::LF)?;
pub fn write_pem_cert(path: &Path, mut writer: impl Write, cert: &Certificate) -> Result<()> {
let data = cert.to_pem(LineEnding::LF).map_err(Error::SaveCert)?;
writer.write_all(data.as_bytes())?;
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded certificate from a file.
pub fn read_pem_cert_file(path: &Path) -> Result<Certificate> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_cert(reader)
read_pem_cert(path, reader)
}
/// Write PEM-encoded certificate to a file.
pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_cert(writer, cert)
write_pem_cert(path, writer, cert)
}
/// Read PEM-encoded PKCS8 public key from a reader.
pub fn read_pem_public_key(mut reader: impl Read) -> Result<RsaPublicKey> {
pub fn read_pem_public_key(path: &Path, mut reader: impl Read) -> Result<RsaPublicKey> {
let mut data = String::new();
reader.read_to_string(&mut data)?;
reader
.read_to_string(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
let key = RsaPublicKey::from_public_key_pem(&data)?;
let key = RsaPublicKey::from_public_key_pem(&data).map_err(Error::LoadPubKey)?;
Ok(key)
}
/// Write PEM-encoded PKCS8 public key to a writer.
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
let data = key.to_public_key_pem(LineEnding::LF)?;
pub fn write_pem_public_key(path: &Path, mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
let data = key
.to_public_key_pem(LineEnding::LF)
.map_err(Error::SavePubKey)?;
writer.write_all(data.as_bytes())?;
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded PKCS8 public key from a file.
pub fn read_pem_public_key_file(path: &Path) -> Result<RsaPublicKey> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_public_key(reader)
read_pem_public_key(path, reader)
}
/// Write PEM-encoded PKCS8 public key to a file.
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
let writer = File::create(path).map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_public_key(writer, key)
write_pem_public_key(path, writer, key)
}
/// Read PEM-encoded PKCS8 private key from a reader.
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
pub fn read_pem_key(
path: &Path,
mut reader: impl Read,
source: &PassphraseSource,
) -> Result<RsaPrivateKey> {
let mut data = String::new();
reader.read_to_string(&mut data)?;
reader
.read_to_string(&mut data)
.map_err(|e| Error::ReadFile(path.to_owned(), e))?;
if data.contains("ENCRYPTED") {
let passphrase = source.acquire(false)?;
@@ -530,6 +573,7 @@ pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<
/// Write PEM-encoded PKCS8 private key to a writer.
pub fn write_pem_key(
path: &Path,
mut writer: impl Write,
key: &RsaPrivateKey,
source: &PassphraseSource,
@@ -572,20 +616,24 @@ pub fn write_pem_key(
.encrypt_with_params(pbes2_params, passphrase)
.map_err(Error::SaveKeyEncrypted)?;
secret_doc.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)?
secret_doc
.to_pem(EncryptedPrivateKeyInfo::PEM_LABEL, LineEnding::LF)
.map_err(pkcs8::Error::Asn1)
.map_err(Error::SaveKeyEncrypted)?
};
writer.write_all(data.as_bytes())?;
writer
.write_all(data.as_bytes())
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
Ok(())
}
/// Read PEM-encoded PKCS8 private key from a file.
pub fn read_pem_key_file(path: &Path, source: &PassphraseSource) -> Result<RsaPrivateKey> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let reader = File::open(path).map_err(|e| Error::ReadFile(path.to_owned(), e))?;
read_pem_key(reader, source)
read_pem_key(path, reader, source)
}
/// Save PEM-encoded PKCS8 private key to a file.
@@ -605,16 +653,18 @@ pub fn write_pem_key_file(
options.mode(0o600);
}
let file = options.open(path)?;
let writer = BufWriter::new(file);
let writer = options
.open(path)
.map_err(|e| Error::WriteFile(path.to_owned(), e))?;
write_pem_key(writer, key, source)
write_pem_key(path, writer, key, source)
}
/// Get the RSA public key from a certificate.
pub fn get_public_key(cert: &Certificate) -> Result<RsaPublicKey> {
let public_key =
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())?;
RsaPublicKey::try_from(cert.tbs_certificate.subject_public_key_info.owned_to_ref())
.map_err(Error::LoadPubKey)?;
Ok(public_key)
}
@@ -628,27 +678,26 @@ pub fn cert_matches_key(cert: &Certificate, key: &RsaSigningKey) -> Result<bool>
/// Parse a CMS [`SignedData`] structure from raw DER-encoded data.
pub fn parse_cms(data: &[u8]) -> Result<SignedData> {
let ci = ContentInfo::from_der(data)?;
let sd = ci.content.decode_as::<SignedData>()?;
let ci = ContentInfo::from_der(data).map_err(Error::CmsParse)?;
let sd = ci
.content
.decode_as::<SignedData>()
.map_err(Error::CmsParse)?;
Ok(sd)
}
/// Get a list of all standard X509 certificates contained within a
/// Get an iterator to all standard X509 certificates contained within a
/// [`SignedData`] structure.
pub fn get_cms_certs(sd: &SignedData) -> Vec<Certificate> {
sd.certificates.as_ref().map_or_else(Vec::new, |certs| {
certs
.0
.iter()
.filter_map(|cc| {
if let CertificateChoices::Certificate(c) = cc {
Some(c.clone())
} else {
None
}
})
.collect()
pub fn iter_cms_certs(sd: &SignedData) -> impl Iterator<Item = &Certificate> {
sd.certificates.iter().flat_map(|certs| {
certs.0.iter().filter_map(|cc| {
if let CertificateChoices::Certificate(c) = cc {
Some(c)
} else {
None
}
})
})
}
@@ -671,14 +720,16 @@ pub fn cms_sign_external(
let signed_data = SignedData {
version: CmsVersion::V1,
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])?,
digest_algorithms: DigestAlgorithmIdentifiers::try_from(vec![digest_algorithm.clone()])
.map_err(Error::CmsSign)?,
encap_content_info: EncapsulatedContentInfo {
econtent_type: const_oid::db::rfc5911::ID_DATA,
econtent: None,
},
certificates: Some(CertificateSet::try_from(vec![
CertificateChoices::Certificate(cert.clone()),
])?),
certificates: Some(
CertificateSet::try_from(vec![CertificateChoices::Certificate(cert.clone())])
.map_err(Error::CmsSign)?,
),
crls: None,
signer_infos: SignerInfos::try_from(vec![SignerInfo {
version: CmsVersion::V1,
@@ -692,14 +743,15 @@ pub fn cms_sign_external(
oid: const_oid::db::rfc5912::SHA_256_WITH_RSA_ENCRYPTION,
parameters: None,
},
signature: SignatureValue::new(signature)?,
signature: SignatureValue::new(signature).map_err(Error::CmsSign)?,
unsigned_attrs: None,
}])?,
}])
.map_err(Error::CmsSign)?,
};
let signed_data = ContentInfo {
content_type: const_oid::db::rfc5911::ID_SIGNED_DATA,
content: Any::encode_from(&signed_data)?,
content: Any::encode_from(&signed_data).map_err(Error::CmsSign)?,
};
Ok(signed_data)
+6 -8
View File
@@ -1,12 +1,10 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
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)]
@@ -68,7 +66,7 @@ where
{
struct EscapedStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
impl<T> Visitor<'_> for EscapedStrVisitor<T>
where
T: FromEscaped,
<T as FromEscaped>::Error: fmt::Display,
@@ -153,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);
}
}
+815 -538
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+72 -37
View File
@@ -1,21 +1,20 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
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, XzOptions, XzReader, XzWriter};
use serde::{Deserialize, Serialize};
use thiserror::Error;
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";
@@ -24,10 +23,12 @@ static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
pub enum Error {
#[error("Unknown compression format")]
UnknownFormat,
#[error("XZ stream error")]
XzStream(#[from] liblzma::stream::Error),
#[error("I/O error")]
Io(#[from] io::Error),
#[error("I/O error when autodetecting compression format")]
AutoDetect(#[source] io::Error),
#[error("Failed to initialize legacy LZ4 encoder")]
Lz4Init(#[source] io::Error),
#[error("Failed to initialize XZ encoder")]
XzInit(#[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -61,7 +62,7 @@ impl<W: Write> Lz4LegacyEncoder<W> {
let compressed = lz4_flex::block::compress(&self.buf[..self.n_filled]);
let writer = self.writer.as_mut().unwrap();
writer.write_u32::<LittleEndian>(compressed.len() as u32)?;
writer.write_all(&(compressed.len() as u32).to_le_bytes())?;
writer.write_all(&compressed)?;
self.n_filled = 0;
@@ -108,6 +109,7 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum CompressedFormat {
None,
Deflate,
Gzip,
Lz4Legacy,
Xz,
@@ -115,34 +117,29 @@ pub enum CompressedFormat {
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>>),
}
impl<R: Read + Seek> CompressedReader<R> {
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
let mut magic = [0u8; 6];
reader.read_exact(&mut magic)?;
reader.rewind()?;
if &magic[0..2] == GZIP_MAGIC {
Ok(Self::Gzip(GzDecoder::new(reader)))
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(XzDecoder::new(reader)))
} else if raw_if_unknown {
Ok(Self::None(reader))
} else {
Err(Error::UnknownFormat)
impl<R: Read> CompressedReader<R> {
pub fn with_format(reader: R, format: CompressedFormat) -> Self {
match format {
CompressedFormat::None => Self::None(reader),
CompressedFormat::Deflate => Self::Deflate(DeflateDecoder::new(reader)),
CompressedFormat::Gzip => Self::Gzip(GzDecoder::new(reader)),
CompressedFormat::Lz4Legacy => Self::Lz4(FrameDecoder::new(reader)),
CompressedFormat::Xz => Self::Xz(Box::new(XzReader::new(reader, false))),
}
}
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,
@@ -152,6 +149,7 @@ impl<R: Read + Seek> CompressedReader<R> {
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(),
@@ -159,10 +157,31 @@ impl<R: Read + Seek> CompressedReader<R> {
}
}
impl<R: Read + Seek> CompressedReader<R> {
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
let magic = reader.read_array_exact::<6>().map_err(Error::AutoDetect)?;
reader.rewind().map_err(Error::AutoDetect)?;
if &magic[0..2] == GZIP_MAGIC {
Ok(Self::Gzip(GzDecoder::new(reader)))
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(Box::new(XzReader::new(reader, false))))
} else if raw_if_unknown {
Ok(Self::None(reader))
} else {
Err(Error::UnknownFormat)
}
}
}
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),
@@ -170,25 +189,37 @@ impl<R: Read> Read for CompressedReader<R> {
}
}
#[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(XzWriter<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 => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
CompressedFormat::Lz4Legacy => {
let encoder = Lz4LegacyEncoder::new(writer).map_err(Error::Lz4Init)?;
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)?;
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
let mut options = XzOptions::with_preset(6);
options.set_check_sum_type(CheckType::Crc32);
let xz_writer = XzWriter::new(writer, options).map_err(Error::XzInit)?;
Ok(Self::Xz(xz_writer))
}
}
}
@@ -196,6 +227,7 @@ 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,
@@ -205,6 +237,7 @@ impl<W: Write> CompressedWriter<W> {
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(),
@@ -216,6 +249,7 @@ 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),
@@ -225,6 +259,7 @@ impl<W: Write> Write for CompressedWriter<W> {
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(),
+221 -110
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{HashMap, HashSet},
@@ -15,6 +13,8 @@ use bstr::ByteSlice;
use num_traits::{ToPrimitive, Zero};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zerocopy::{FromBytes, IntoBytes};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
escape,
@@ -50,55 +50,92 @@ const VEC_CAP_THRESHOLD: usize = 16384;
pub enum Error {
#[error("Unknown magic: {0:?}")]
UnknownMagic([u8; 6]),
#[error("Path is not NULL-terminated: {:?}", .0.as_bstr())]
PathNotNullTerminated(Vec<u8>),
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
HardLinksNotSupported(Vec<u8>),
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
#[error("Entry of type {0} should not have data: {path:?}", path = .1.as_bstr())]
EntryHasData(CpioEntryType, Vec<u8>),
#[error("No inodes available for device {0:x},{1:x}")]
DeviceFull(u32, u32),
#[error("{0:?} field exceeds integer bounds")]
IntegerTooLarge(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
#[error("No inodes available for device {major:x},{minor:x}")]
DeviceFull { major: u32, minor: u32 },
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("{0:?} contains invalid hex integer")]
InvalidHexInt(&'static str, #[source] InvalidHexCharError),
#[error("Failed to read cpio data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write cpio data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Read u32 formatted as an ASCII 8-char wide hex string.
fn read_int(mut reader: impl Read) -> io::Result<u32> {
let mut buf = [0u8; 8];
reader.read_exact(&mut buf)?;
#[derive(Debug, Error)]
#[error("{0:?}: Invalid hex char: {1:?}")]
pub struct InvalidHexCharError(RawHexU32, char);
let mut value = 0;
/// ASCII-encoded hex integer value used in cpio header fields.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHexU32([u8; 8]);
for b in buf {
let c = b as char;
let digit = c.to_digit(16).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
)
})?;
value <<= 4;
value |= digit;
impl fmt::Debug for RawHexU32 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0.as_bstr())
}
Ok(value)
}
/// Write u32 formatted as an ASCII 8-char wide hex string.
fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
let mut buf = [b'0'; 8];
let mut index = 7;
#[allow(clippy::fallible_impl_from)]
impl From<u32> for RawHexU32 {
fn from(mut value: u32) -> Self {
let mut buf = [b'0'; 8];
while value != 0 {
buf[index] = char::from_digit(value & 0xf, 16).unwrap() as u8;
value >>= 4;
index -= 1;
for c in buf.iter_mut().rev() {
*c = char::from_digit(value & 0xf, 16).unwrap() as u8;
value >>= 4;
}
Self(buf)
}
}
writer.write_all(&buf)
impl TryFrom<RawHexU32> for u32 {
type Error = InvalidHexCharError;
fn try_from(raw_value: RawHexU32) -> std::result::Result<Self, Self::Error> {
let mut value = 0;
for b in raw_value.0 {
let c = b as char;
let digit = c.to_digit(16).ok_or(InvalidHexCharError(raw_value, c))?;
value <<= 4;
value |= digit;
}
Ok(value)
}
}
/// Raw on-disk layout for the cpio header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`MAGIC_NEW`] or [`MAGIC_NEW_CRC`].
magic: [u8; 6],
inode: RawHexU32,
mode: RawHexU32,
uid: RawHexU32,
gid: RawHexU32,
nlink: RawHexU32,
mtime: RawHexU32,
file_size: RawHexU32,
dev_maj: RawHexU32,
dev_min: RawHexU32,
rdev_maj: RawHexU32,
rdev_min: RawHexU32,
path_size: RawHexU32,
crc32: RawHexU32,
}
/// Read a chunk of bytes from the reader. If `size` is less than
@@ -213,17 +250,14 @@ impl CpioEntryData {
pub fn size(&self) -> Result<u32> {
let size = match self {
Self::Size(s) => *s,
Self::Data(d) => d
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
Self::Data(d) => d.len().to_u32().ok_or(Error::IntOverflow("data_size"))?,
};
Ok(size)
}
fn is_size(&self) -> bool {
matches!(self, CpioEntryData::Size(_))
matches!(self, Self::Size(_))
}
}
@@ -391,41 +425,45 @@ impl<R: Read> FromReader<R> for CpioEntry {
fn from_reader(reader: R) -> Result<Self> {
let mut reader = CountingReader::new(reader);
let mut magic = [0u8; 6];
reader.read_exact(&mut magic)?;
let header =
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
if magic != *MAGIC_NEW && magic != *MAGIC_NEW_CRC {
return Err(Error::UnknownMagic(magic));
if header.magic != *MAGIC_NEW && header.magic != *MAGIC_NEW_CRC {
return Err(Error::UnknownMagic(header.magic));
}
let inode = read_int(&mut reader)?;
let mode = read_int(&mut reader)?;
let uid = read_int(&mut reader)?;
let gid = read_int(&mut reader)?;
let nlink = read_int(&mut reader)?;
let mtime = read_int(&mut reader)?;
let file_size = read_int(&mut reader)?;
let dev_maj = read_int(&mut reader)?;
let dev_min = read_int(&mut reader)?;
let rdev_maj = read_int(&mut reader)?;
let rdev_min = read_int(&mut reader)?;
let path_size = read_int(&mut reader)?;
let crc32 = read_int(&mut reader)?;
macro_rules! get_field {
($name:ident) => {
let $name = u32::try_from(header.$name)
.map_err(|e| Error::InvalidHexInt(stringify!($name), e))?;
};
}
get_field!(inode);
get_field!(mode);
get_field!(uid);
get_field!(gid);
get_field!(nlink);
get_field!(mtime);
get_field!(file_size);
get_field!(dev_maj);
get_field!(dev_min);
get_field!(rdev_maj);
get_field!(rdev_min);
get_field!(path_size);
get_field!(crc32);
let mut path = read_data(
&mut reader,
path_size.to_usize().unwrap(),
&AtomicBool::new(false),
)?;
)
.map_err(|e| Error::DataRead("path", e))?;
if path.last() != Some(&b'\0') {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Filename is not NULL-terminated",
)
.into());
return Err(Error::PathNotNullTerminated(path));
}
path.pop();
padding::read_discard(&mut reader, 4)?;
padding::read_discard(&mut reader, 4).map_err(|e| Error::DataRead("path_padding", e))?;
let file_type = CpioEntryType::from_mode(mode);
let data = match file_type {
@@ -437,8 +475,10 @@ impl<R: Read> FromReader<R> for CpioEntry {
&mut reader,
file_size.to_usize().unwrap(),
&AtomicBool::new(false),
)?;
padding::read_discard(&mut reader, 4)?;
)
.map_err(|e| Error::DataRead("content", e))?;
padding::read_discard(&mut reader, 4)
.map_err(|e| Error::DataRead("content_padding", e))?;
CpioEntryData::Data(content)
}
@@ -477,7 +517,7 @@ impl<W: Write> ToWriter<W> for CpioEntry {
.len()
.checked_add(1)
.and_then(|s| s.to_u32())
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
.ok_or(Error::IntOverflow("path_size"))?;
let file_size = self.data.size()?;
if file_size != 0
@@ -487,35 +527,47 @@ impl<W: Write> ToWriter<W> for CpioEntry {
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
}
if self.crc32 == 0 {
writer.write_all(MAGIC_NEW)?;
} else {
writer.write_all(MAGIC_NEW_CRC)?;
}
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
write_int(&mut writer, self.inode)?;
write_int(&mut writer, mode)?;
write_int(&mut writer, self.uid)?;
write_int(&mut writer, self.gid)?;
write_int(&mut writer, self.nlink)?;
write_int(&mut writer, self.mtime)?;
write_int(&mut writer, file_size)?;
write_int(&mut writer, self.dev_maj)?;
write_int(&mut writer, self.dev_min)?;
write_int(&mut writer, self.rdev_maj)?;
write_int(&mut writer, self.rdev_min)?;
write_int(&mut writer, path_size)?;
write_int(&mut writer, self.crc32)?;
let raw_header = RawHeader {
magic: if self.crc32 == 0 {
*MAGIC_NEW
} else {
*MAGIC_NEW_CRC
},
inode: self.inode.into(),
mode: mode.into(),
uid: self.uid.into(),
gid: self.gid.into(),
nlink: self.nlink.into(),
mtime: self.mtime.into(),
file_size: file_size.into(),
dev_maj: self.dev_maj.into(),
dev_min: self.dev_min.into(),
rdev_maj: self.rdev_maj.into(),
rdev_min: self.rdev_min.into(),
path_size: path_size.into(),
crc32: self.crc32.into(),
};
writer.write_all(&self.path)?;
writer.write_zeros_exact(1)?;
padding::write_zeros(&mut writer, 4)?;
raw_header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("header", e))?;
writer
.write_all(&self.path)
.map_err(|e| Error::DataWrite("path", e))?;
writer
.write_zeros_exact(1)
.map_err(|e| Error::DataWrite("path", e))?;
padding::write_zeros(&mut writer, 4).map_err(|e| Error::DataWrite("path_padding", e))?;
if let CpioEntryData::Data(d) = &self.data {
writer.write_all(d)?;
padding::write_zeros(&mut writer, 4)?;
writer
.write_all(d)
.map_err(|e| Error::DataWrite("content", e))?;
padding::write_zeros(&mut writer, 4)
.map_err(|e| Error::DataWrite("content_padding", e))?;
}
Ok(())
@@ -559,7 +611,8 @@ impl<R: Read> CpioReader<R> {
return Ok(None);
}
self.skip_data()?;
self.skip_data()
.map_err(|e| Error::DataRead("content", e))?;
let entry = CpioEntry::from_reader(&mut self.reader)?;
@@ -624,7 +677,8 @@ impl<W: Write> CpioWriter<W> {
}
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
self.finish_entry()?;
self.finish_entry()
.map_err(|e| Error::DataWrite("content", e))?;
entry.to_writer(&mut self.writer)?;
@@ -638,13 +692,15 @@ impl<W: Write> CpioWriter<W> {
}
pub fn finish(mut self) -> Result<W> {
self.finish_entry()?;
self.finish_entry()
.map_err(|e| Error::DataWrite("content", e))?;
self.start_entry(&CpioEntry::new_trailer())?;
// Pad until the end of the block.
if self.pad_to_block_size {
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)
.map_err(|e| Error::DataWrite("block_padding", e))?;
}
Ok(self.writer.finish().0)
@@ -682,14 +738,15 @@ pub fn load(
let mut entries = vec![];
while let Some(mut entry) = cpio_reader.next_entry()? {
stream::check_cancel(cancel_signal)?;
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("entry", e))?;
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
return Err(Error::HardLinksNotSupported(entry.path.clone()));
return Err(Error::HardLinksNotSupported(entry.path));
}
if let CpioEntryData::Size(s) = entry.data {
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)
.map_err(|e| Error::DataWrite("data", e))?;
entry.data = CpioEntryData::Data(data);
}
@@ -712,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();
@@ -731,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;
}
}
@@ -745,7 +800,10 @@ pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()
while set.contains(&unused) {
if unused == *last {
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
return Err(Error::DeviceFull {
major: entry.dev_maj,
minor: entry.dev_min,
});
}
unused = next_non_zero(unused);
@@ -769,7 +827,7 @@ pub fn save(
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
for entry in entries {
stream::check_cancel(cancel_signal)?;
stream::check_cancel(cancel_signal).map_err(|e| Error::DataWrite("entry", e))?;
cpio_writer.start_entry(entry)?;
// CpioEntryData::Data will have already been written.
@@ -779,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);
}
}
+170 -160
View File
@@ -1,33 +1,35 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::HashSet,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
io::{self, Read, SeekFrom, Write},
mem,
ops::Range,
sync::atomic::AtomicBool,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
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},
util::{self, NumBytes},
stream::{
self, FromReader, ReadAt, ReadSeek, ReadWriteAt, ReadWriteSeek, ToWriter, UserPosFile,
WriteSeek, WriteZerosExt,
},
util::{self, NumBytes, OutOfBoundsError},
};
// Not to be confused with the 255-byte RS block size.
const FEC_BLOCK_SIZE: usize = 4096;
const FEC_HEADER_SIZE: usize = 60;
const FEC_MAGIC: u32 = 0xFECFECFE;
const FEC_VERSION: u32 = 0;
@@ -66,9 +68,15 @@ pub enum Error {
#[error("Expected FEC digest {expected}, but have {actual}")]
InvalidFecDigest { expected: String, actual: String },
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[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}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -96,7 +104,7 @@ impl Codeword {
&mut self.data[..usize::from(self.rs_k)]
}
fn parity(&mut self) -> &[u8] {
fn parity(&self) -> &[u8] {
&self.data[usize::from(self.rs_k)..]
}
@@ -157,10 +165,11 @@ impl Fec {
input: file_size,
block: block_size,
});
} else if block_size > FEC_MAX_BLOCK_SIZE {
return Err(Error::FieldOutOfBounds("block_size"));
}
util::check_bounds(block_size, ..=FEC_MAX_BLOCK_SIZE)
.map_err(|e| Error::IntOutOfBounds("block_size", e))?;
let rs_k = 255 - parity;
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
return Err(Error::UnsupportedParity(parity));
@@ -174,11 +183,11 @@ impl Fec {
.checked_mul(u64::from(parity))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.and_then(|s| s.to_usize())
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
.ok_or(Error::IntOverflow("fec_data_size"))?;
rounds
.checked_mul(u64::from(rs_k))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
.ok_or(Error::IntOverflow("fec_grid_size"))?;
Ok(Self {
file_size,
@@ -212,9 +221,8 @@ impl Fec {
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > self.file_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
util::check_bounds(last.end, ..=self.file_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
let block_size = u64::from(self.block_size);
@@ -241,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<()> {
@@ -269,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<()> {
@@ -292,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 {
@@ -302,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)
@@ -310,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);
@@ -323,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(())
@@ -353,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<()> {
@@ -363,7 +366,9 @@ impl Fec {
"FEC buffer length does not match block size",
);
let grid = self.read_round(reader, round)?;
let grid = self
.read_round(reader, round)
.map_err(|e| Error::DataRead("round", e))?;
let encode = verityrs::FN_ENCODE[&self.rs_k];
let parity = usize::from(self.parity());
@@ -377,14 +382,16 @@ 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,
"FEC buffer length does not match block size",
);
let grid = self.read_round(reader, round)?;
let grid = self
.read_round(reader, round)
.map_err(|e| Error::DataRead("round", e))?;
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
let parity = usize::from(self.parity());
@@ -403,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> {
@@ -414,7 +420,9 @@ impl Fec {
"FEC buffer length does not match block size",
);
let mut grid = self.read_round(reader, round)?;
let mut grid = self
.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());
let mut num_corrected = 0;
@@ -432,7 +440,8 @@ 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))?;
}
Ok(num_corrected)
@@ -444,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();
@@ -452,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)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed()?;
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)
}
@@ -468,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,
@@ -487,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)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed()?;
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
@@ -505,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<()> {
@@ -520,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)?;
.try_for_each(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed()?;
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.
@@ -544,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> {
@@ -562,20 +563,37 @@ impl Fec {
.par_chunks_exact(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<u64> {
stream::check_cancel(cancel_signal)?;
stream::check_cancel(cancel_signal).map_err(|e| Error::DataRead("(init)", e))?;
let reader = input.reopen_boxed()?;
let writer = output.reopen_boxed()?;
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)
}
}
/// Raw on-disk layout for the FEC image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`FEC_MAGIC`].
magic: little_endian::U32,
/// Image version. This should be equal to [`FEC_VERSION`].
version: little_endian::U32,
/// Size of this [`RawHeader`].
header_size: little_endian::U32,
/// Number of parity bytes per 255-byte Reed-Solomon codeword.
parity: little_endian::U32,
/// Size of the FEC data.
fec_size: little_endian::U32,
/// Size of the actual data.
data_size: little_endian::U64,
/// SHA-256 digest of the FEC data.
digest: [u8; 32],
}
/// A type for reading and writing AOSP's standalone FEC image format.
///
/// The FEC data parser in this implementation is strict. All header fields,
@@ -602,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 = {
let mut file = input.reopen_boxed()?;
file.seek(SeekFrom::End(0))?
};
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)?;
@@ -623,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<()> {
@@ -633,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)
}
@@ -653,42 +664,34 @@ 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
/// writing it to both of the header locations at the end of the file.
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
let fec_size = self
.fec
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
fn build_header(&self) -> Result<RawHeader> {
let fec_size: u32 =
util::try_cast(self.fec.len()).map_err(|e| Error::IntOutOfBounds("fec_size", e))?;
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
writer.write_u32::<LittleEndian>(self.parity.into())?;
writer.write_u32::<LittleEndian>(fec_size)?;
writer.write_u64::<LittleEndian>(self.data_size)?;
writer.write_all(digest.as_ref())?;
let header = RawHeader {
magic: FEC_MAGIC.into(),
version: FEC_VERSION.into(),
header_size: (mem::size_of::<RawHeader>() as u32).into(),
parity: u32::from(self.parity).into(),
fec_size: fec_size.into(),
data_size: self.data_size.into(),
digest: digest.as_ref().try_into().unwrap(),
};
Ok(writer.into_inner())
Ok(header)
}
}
@@ -699,48 +702,47 @@ impl<R: Read> FromReader<R> for FecImage {
// Avoid requiring seekable readers since we need to read everything
// into memory anyway.
let mut fec = Vec::new();
reader.read_to_end(&mut fec)?;
reader
.read_to_end(&mut fec)
.map_err(|e| Error::DataRead("fec", e))?;
if fec.len() < FEC_BLOCK_SIZE {
return Err(Error::DataTooSmall);
}
// Make sure both headers match.
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
let header2_offset = fec.len() - FEC_HEADER_SIZE;
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
let (header, _) =
RawHeader::ref_from_prefix(&fec[header1_offset..]).map_err(|_| Error::DataTooSmall)?;
let header_size = header.header_size.get() as usize;
if header_size > FEC_BLOCK_SIZE / 2 {
// ref_from_prefix() already handles the "too small" case.
return Err(Error::InvalidHeaderSize(header.header_size.get()));
}
let header2_offset = fec.len() - header_size;
// Make sure both headers match, accounting for potential custom fields.
let header1_raw = &fec[header1_offset..][..header_size];
let header2_raw = &fec[header2_offset..][..header_size];
if header1_raw != header2_raw {
return Err(Error::HeadersDifferent);
}
let mut header_reader = Cursor::new(header1_raw);
let magic = header_reader.read_u32::<LittleEndian>()?;
if magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
if header.magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic.get()));
}
let version = header_reader.read_u32::<LittleEndian>()?;
if version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(version));
if header.version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(header.version.get()));
}
let header_size = header_reader.read_u32::<LittleEndian>()?;
if header_size != FEC_HEADER_SIZE as u32 {
return Err(Error::InvalidHeaderSize(header_size));
}
let parity: u8 =
util::try_cast(header.parity.get()).map_err(|e| Error::IntOutOfBounds("parity", e))?;
let parity = header_reader
.read_u32::<LittleEndian>()?
.to_u8()
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
let fec_size = header_reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let fec_size = header.fec_size.get() as usize;
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
if fec_size != actual_fec_size {
return Err(Error::InvalidHeaderFecSize {
@@ -749,25 +751,22 @@ impl<R: Read> FromReader<R> for FecImage {
});
}
let input_size = header_reader.read_u64::<LittleEndian>()?;
let data_size = header.data_size.get();
let mut digest = [0u8; 32];
header_reader.read_exact(&mut digest)?;
// Chop off headers.
fec.resize(fec_size, 0);
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
if digest != actual_digest.as_ref() {
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec[..fec_size]);
if header.digest != actual_digest.as_ref() {
return Err(Error::InvalidFecDigest {
expected: hex::encode(digest),
expected: hex::encode(header.digest),
actual: hex::encode(actual_digest),
});
}
// Chop off headers.
fec.resize(fec_size, 0);
Ok(Self {
fec,
data_size: input_size,
data_size,
parity,
})
}
@@ -779,10 +778,18 @@ impl<W: Write> ToWriter<W> for FecImage {
fn to_writer(&self, mut writer: W) -> Result<()> {
let header = self.build_header()?;
writer.write_all(&self.fec)?;
writer.write_all(&header)?;
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
writer.write_all(&header)?;
writer
.write_all(&self.fec)
.map_err(|e| Error::DataWrite("fec_data", e))?;
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("fec_header_1", e))?;
writer
.write_zeros_exact((FEC_BLOCK_SIZE - 2 * header.as_bytes().len()) as u64)
.map_err(|e| Error::DataWrite("fec_header_padding", e))?;
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("fec_header_2", e))?;
Ok(())
}
@@ -791,14 +798,14 @@ impl<W: Write> ToWriter<W> for FecImage {
#[cfg(test)]
mod tests {
use std::{
io::Seek,
sync::{atomic::AtomicBool, Arc},
io::{Cursor, Seek},
sync::{Arc, atomic::AtomicBool},
};
use assert_matches::assert_matches;
use rand::RngCore;
use crate::stream::SharedCursor;
use crate::stream::MutexFile;
use super::*;
@@ -827,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();
@@ -846,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)
};
@@ -866,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)
@@ -874,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)
@@ -913,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();
+156 -127
View File
@@ -1,29 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
str,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use zerocopy::{FromBytes, IntoBytes, little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::{
format::{avb, padding},
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
util::{self, NumBytes},
format::{
avb,
padding::{self, ZeroPadding},
},
stream::{self, FromReader, ReadAt, ReadFixedSizeExt, ToWriter, UserPosFile},
util::{self, NumBytes, OutOfBoundsError},
};
#[derive(Debug, Error)]
@@ -42,12 +44,20 @@ pub enum Error {
InvalidHeaderMagic([u8; 16]),
#[error("Invalid hash tree header version: {0}")]
InvalidHeaderVersion(u16),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("Hashing algorithm not supported: {:?}", .0.as_bstr())]
UnsupportedHashAlgorithm(Vec<u8>),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
IntOutOfBounds(&'static str, #[source] OutOfBoundsError),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[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}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write hash tree data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -83,13 +93,12 @@ impl HashTree {
level_size = blocks
.checked_mul(digest_size as u64)
.and_then(|s| padding::round(s, u64::from(self.block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
.ok_or(Error::IntOverflow("level_size"))?;
// Depending on the chosen block size, the original file size could
// overflow a usize without the first level's size doing the same.
let level_size_usize = level_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
let level_size_usize: usize =
util::try_cast(level_size).map_err(|e| Error::IntOutOfBounds("level_size", e))?;
ranges.push(0..level_size_usize);
}
@@ -111,9 +120,8 @@ impl HashTree {
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > image_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
util::check_bounds(last.end, ..=image_size)
.map_err(|e| Error::IntOutOfBounds("ranges", e))?;
}
let block_size = u64::from(self.block_size);
@@ -181,13 +189,13 @@ 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,
) -> io::Result<()> {
assert!(
size > self.block_size as u64,
size > u64::from(self.block_size),
"Images smaller than block size must use a normal hash",
);
@@ -199,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],
@@ -231,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
@@ -253,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>],
@@ -262,9 +264,10 @@ impl HashTree {
) -> Result<Vec<u8>> {
// Small files are hashed directly.
if image_size <= u64::from(self.block_size) {
let mut reader = input.reopen_boxed()?;
let mut buf = vec![0u8; image_size as usize];
reader.read_exact(&mut buf)?;
let mut reader = UserPosFile::new(input);
let buf = reader
.read_vec_exact(image_size as usize)
.map_err(Error::InputDigest)?;
let mut context = self.salted_context.clone();
context.update(&buf);
@@ -289,7 +292,8 @@ impl HashTree {
prev_size as u64,
level_data,
cancel_signal,
)?;
)
.map_err(Error::InputDigest)?;
} else if let Some(r) = ranges {
// Read partial blocks from file.
let block_ranges = self.blocks_for_ranges(image_size, r)?;
@@ -300,10 +304,12 @@ impl HashTree {
&block_ranges,
level_data,
cancel_signal,
)?;
)
.map_err(Error::InputDigest)?;
} else {
// Read entire file.
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)
.map_err(Error::InputDigest)?;
}
// No need to explicitly ensure the level is padded to the block
@@ -322,12 +328,12 @@ 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>)> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
let mut hash_tree_data = vec![0u8; hash_tree_size];
let root_digest = self.calculate(
@@ -346,14 +352,14 @@ 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],
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
@@ -375,14 +381,14 @@ 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],
cancel_signal: &AtomicBool,
) -> Result<()> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.first().map(|r| r.end).unwrap_or(0);
let hash_tree_size = offsets.first().map_or(0, |r| r.end);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
@@ -417,6 +423,28 @@ impl HashTree {
}
}
/// Raw on-disk layout for our custom hash tree image header.
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HashTreeImage::MAGIC`].
magic: [u8; 16],
/// Image version. This should be equal to [`HashTreeImage::VERSION`].
version: little_endian::U16,
/// Size of the actual data.
image_size: little_endian::U64,
/// Block size.
block_size: little_endian::U32,
/// Hash algorithm.
algorithm: [u8; 16],
/// Salt size.
salt_size: little_endian::U16,
/// Root digest size.
root_digest_size: little_endian::U16,
/// Hash tree size.
hash_tree_size: little_endian::U32,
}
/// A type for reading and writing a custom hash tree image format.
///
/// File format:
@@ -444,6 +472,7 @@ pub struct HashTreeImage {
impl fmt::Debug for HashTreeImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashTreeImage")
.field("image_size", &self.image_size)
.field("block_size", &self.block_size)
.field("algorithm", &self.algorithm)
.field("salt", &hex::encode(&self.salt))
@@ -457,25 +486,22 @@ impl HashTreeImage {
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
const VERSION: u16 = 1;
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::ring_algorithm(name, false)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
fn digest_algorithm(name: &str) -> Result<&'static Algorithm> {
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 = {
let mut file = input.reopen_boxed()?;
file.seek(SeekFrom::End(0))?
};
let ring_algorithm = Self::ring_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
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)?;
Ok(Self {
@@ -491,12 +517,12 @@ 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<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
let digest_algorithm = Self::digest_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, digest_algorithm, &self.salt);
self.root_digest = hash_tree.update(
input,
@@ -510,13 +536,9 @@ impl HashTreeImage {
}
/// Check that a file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
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);
hash_tree.verify(
input,
@@ -532,40 +554,37 @@ impl<R: Read> FromReader<R> for HashTreeImage {
type Error = Error;
fn from_reader(mut reader: R) -> Result<Self> {
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
let header =
RawHeader::read_from_io(&mut reader).map_err(|e| Error::DataRead("header", e))?;
if header.magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(header.magic));
}
let version = reader.read_u16::<LittleEndian>()?;
if version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(version));
if header.version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(header.version.get()));
}
let image_size = reader.read_u64::<LittleEndian>()?;
let block_size = reader.read_u32::<LittleEndian>()?;
let algorithm = reader.read_string_padded(16)?;
let salt_size = reader.read_u16::<LittleEndian>()?;
let root_digest_size = reader.read_u16::<LittleEndian>()?;
let hash_tree_size = reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let algorithm = header.algorithm.trim_end_padding();
let algorithm = str::from_utf8(algorithm)
.map_err(|_| Error::UnsupportedHashAlgorithm(algorithm.to_vec()))?;
let mut salt = vec![0u8; usize::from(salt_size)];
reader.read_exact(&mut salt)?;
let salt = reader
.read_vec_exact(usize::from(header.salt_size))
.map_err(|e| Error::DataRead("header", e))?;
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
reader.read_exact(&mut root_digest)?;
let root_digest = reader
.read_vec_exact(usize::from(header.root_digest_size))
.map_err(|e| Error::DataRead("root_digest", e))?;
let mut hash_tree = vec![0u8; hash_tree_size];
reader.read_exact(&mut hash_tree)?;
let hash_tree = reader
.read_vec_exact(header.hash_tree_size.get() as usize)
.map_err(|e| Error::DataRead("hash_tree", e))?;
Ok(Self {
image_size,
block_size,
algorithm,
image_size: header.image_size.get(),
block_size: header.block_size.get(),
algorithm: algorithm.to_owned(),
salt,
root_digest,
hash_tree,
@@ -577,33 +596,42 @@ impl<W: Write> ToWriter<W> for HashTreeImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let salt_size = self
.salt
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
let root_digest_size = self
.root_digest
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
let hash_tree_size = self
.hash_tree
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let algorithm = self
.algorithm
.as_bytes()
.to_padded_array::<16>()
.ok_or_else(|| Error::UnsupportedHashAlgorithm(self.algorithm.as_bytes().to_vec()))?;
writer.write_all(Self::MAGIC)?;
writer.write_u16::<LittleEndian>(Self::VERSION)?;
writer.write_u64::<LittleEndian>(self.image_size)?;
writer.write_u32::<LittleEndian>(self.block_size)?;
writer.write_string_padded(&self.algorithm, 16)?;
writer.write_u16::<LittleEndian>(salt_size)?;
writer.write_u16::<LittleEndian>(root_digest_size)?;
writer.write_u32::<LittleEndian>(hash_tree_size)?;
writer.write_all(&self.salt)?;
writer.write_all(&self.root_digest)?;
writer.write_all(&self.hash_tree)?;
let salt_size: u16 =
util::try_cast(self.salt.len()).map_err(|e| Error::IntOutOfBounds("salt_size", e))?;
let root_digest_size: u16 = util::try_cast(self.root_digest.len())
.map_err(|e| Error::IntOutOfBounds("root_digest_size", e))?;
let hash_tree_size: u32 = util::try_cast(self.hash_tree.len())
.map_err(|e| Error::IntOutOfBounds("hash_tree_size", e))?;
let header = RawHeader {
magic: *Self::MAGIC,
version: Self::VERSION.into(),
image_size: self.image_size.into(),
block_size: self.block_size.into(),
algorithm,
salt_size: salt_size.into(),
root_digest_size: root_digest_size.into(),
hash_tree_size: hash_tree_size.into(),
};
header
.write_to_io(&mut writer)
.map_err(|e| Error::DataWrite("header", e))?;
writer
.write_all(&self.salt)
.map_err(|e| Error::DataWrite("salt", e))?;
writer
.write_all(&self.root_digest)
.map_err(|e| Error::DataWrite("root_digest", e))?;
writer
.write_all(&self.hash_tree)
.map_err(|e| Error::DataWrite("hash_tree", e))?;
Ok(())
}
@@ -615,7 +643,7 @@ mod tests {
use assert_matches::assert_matches;
use crate::stream::SharedCursor;
use crate::stream::MutexFile;
use super::*;
@@ -648,7 +676,7 @@ mod tests {
);
assert_matches!(
hash_tree.blocks_for_ranges(16384, &[0..16385]),
Err(Error::FieldOutOfBounds(_))
Err(Error::IntOutOfBounds(_, _))
);
}
@@ -656,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();
@@ -673,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();
@@ -697,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)
@@ -734,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)
+426 -305
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod avb;
pub mod bootimage;
@@ -15,3 +13,4 @@ pub mod padding;
pub mod payload;
pub mod sparse;
pub mod verityrs;
pub mod zip;
+551 -254
View File
File diff suppressed because it is too large Load Diff
+34 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Read, Seek, Write};
@@ -47,3 +45,35 @@ pub fn write_zeros(mut writer: impl Write + Seek, page_size: u64) -> io::Result<
Ok(padding)
}
pub trait ZeroPadding {
/// Trim trailing zeros. Intermediate zeros before the last non-zero byte
/// are kept.
fn trim_end_padding(&self) -> &[u8];
/// Return the slice as an array padded with zeros at the end.
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]>;
}
impl ZeroPadding for [u8] {
fn trim_end_padding(&self) -> &[u8] {
let first_ending_zero = self
.iter()
.rposition(|b| *b != 0)
.map(|pos| pos + 1)
.unwrap_or_default();
&self[..first_ending_zero]
}
fn to_padded_array<const N: usize>(&self) -> Option<[u8; N]> {
if self.len() > N {
return None;
}
let mut result = [0u8; N];
result[..self.len()].copy_from_slice(self);
Some(result)
}
}
File diff suppressed because it is too large Load Diff
+181 -139
View File
@@ -1,11 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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,
};
@@ -13,7 +11,10 @@ use std::{
use crc32fast::Hasher;
use dlv_list::{Index, VecList};
use thiserror::Error;
use zerocopy::{byteorder::little_endian, AsBytes, FromBytes, FromZeroes, Unaligned};
use zerocopy::{FromBytes, IntoBytes, byteorder::little_endian};
use zerocopy_derive::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
use crate::stream::ReadDiscardExt;
/// Magic value for [`RawHeader::magic`].
const HEADER_MAGIC: u32 = 0xed26ff3a;
@@ -34,23 +35,78 @@ pub const MINOR_VERSION: u16 = 0;
#[derive(Debug, Error)]
pub enum Error {
#[error("Sparse header: {0}")]
Header(String),
#[error("Sparse chunk #{0}: {1}")]
Chunk(u32, String),
#[error("Sparse reader: {0}")]
Reader(String),
#[error("Sparse writer: {0}")]
Writer(String),
#[error("I/O error")]
Io(#[from] io::Error),
// Header errors.
#[error("Invalid magic: {0:#010x}")]
InvalidMagic(u32),
#[error("Unsupported major version: {0}")]
UnsupportedMajorVersion(u16),
#[error("Invalid file header size: {0} < {size}", size = mem::size_of::<RawHeader>())]
InvalidFileHeaderSize(u16),
#[error("Invalid chunk header size: {0} < {size}", size = mem::size_of::<RawChunk>())]
InvalidChunkHeaderSize(u16),
#[error("Invalid block size (must be a non-zero multiple of 4): {0}")]
InvalidBlockSize(u32),
// Chunk errors.
#[error("Chunk #{index}: Size overflow: {chunk_size} * {block_size}")]
ChunkSizeOverflow {
index: u32,
chunk_size: u32,
block_size: u32,
},
#[error("Chunk #{index}: Invalid type: {chunk_type}")]
InvalidChunkType { index: u32, chunk_type: u16 },
#[error("Chunk #{index}: Data size too large: {data_size}")]
DataSizeTooLarge { index: u32, data_size: u32 },
#[error("Chunk #{index}: Block count overflow: {start_block} + {chunk_size}")]
BlockCountOverflow {
index: u32,
start_block: u32,
chunk_size: u32,
},
#[error("Chunk #{index}: End block {end_block} exceeds total blocks {total_blocks}")]
EndBlockExceedsTotal {
index: u32,
end_block: u32,
total_blocks: u32,
},
#[error("Chunk #{index}: CRC32 chunk is not empty")]
Crc32ChunkNotEmpty { index: u32, chunk_size: u32 },
#[error("Chunk #{index}: Expected total size {expected_size}, but have {total_size}")]
InvalidChunkSize {
index: u32,
expected_size: u32,
total_size: u32,
},
// Reader errors.
#[error("Must fully consume data when CRC validation is enabled")]
Crc32RandomRead,
#[error("Previous chunk still has {0} unread bytes")]
UnreadChunkData(u32),
#[error("Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}")]
MismatchedCrc32Checkpoint { expected: u32, actual: u32 },
#[error("Expected final CRC32 {expected:08x}, but have {actual:08x}")]
MismatchedCrc32Final { expected: u32, actual: u32 },
// Writer errors.
#[error("Minor version not supported for writing: {0}")]
UnsupportedMinorVersion(u16),
#[error("Previous chunk still has {0} unwritten bytes")]
UnwrittenChunkData(u32),
#[error("Already wrote all chunk headers")]
TooManyChunks,
#[error("Gap between end of last chunk {prev_end} and start of new chunk {cur_start}")]
GapBetweenChunks { prev_end: u32, cur_start: u32 },
// Wrapped errors.
#[error("Failed to read sparse data: {0}")]
DataRead(&'static str, #[source] io::Error),
#[error("Failed to write sparse data: {0}")]
DataWrite(&'static str, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Raw on-disk layout for the header.
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
#[repr(packed)]
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawHeader {
/// Magic value. This should be equal to [`HEADER_MAGIC`].
magic: little_endian::U32,
@@ -96,45 +152,38 @@ impl fmt::Debug for RawHeader {
impl RawHeader {
fn validate(&self) -> Result<()> {
if self.magic.get() != HEADER_MAGIC {
return Err(Error::Header(format!(
"Invalid magic: {:#010x}",
self.magic.get(),
)));
return Err(Error::InvalidMagic(self.magic.get()));
}
if self.major_version.get() != MAJOR_VERSION {
return Err(Error::Header(format!(
"Unsupported major version: {}",
self.major_version.get(),
)));
return Err(Error::UnsupportedMajorVersion(self.major_version.get()));
}
if self.file_hdr_sz.get() != mem::size_of::<RawHeader>() as u16 {
return Err(Error::Header(format!(
"Invalid file header size: {}",
self.file_hdr_sz.get(),
)));
} else if self.chunk_hdr_sz.get() != mem::size_of::<RawChunk>() as u16 {
return Err(Error::Header(format!(
"Invalid chunk header size: {}",
self.chunk_hdr_sz.get(),
)));
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()));
}
if self.blk_sz.get() == 0 || self.blk_sz.get() % 4 != 0 {
return Err(Error::Header(format!(
"Invalid block size: {}",
self.blk_sz.get(),
)));
return Err(Error::InvalidBlockSize(self.blk_sz.get()));
}
Ok(())
}
fn excess_raw_header_bytes(&self) -> u16 {
self.file_hdr_sz.get() - mem::size_of::<Self>() as u16
}
fn excess_raw_chunk_bytes(&self) -> u16 {
self.chunk_hdr_sz.get() - mem::size_of::<RawChunk>() as u16
}
}
/// Raw on-disk layout for the chunk header.
#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)]
#[repr(packed)]
#[derive(Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned)]
#[repr(C, packed)]
struct RawChunk {
/// Chunk type. Must be [`CHUNK_TYPE_RAW`], [`CHUNK_TYPE_FILL`],
/// [`CHUNK_TYPE_DONT_CARE`], or [`CHUNK_TYPE_CRC32`].
@@ -165,69 +214,58 @@ impl RawChunk {
.chunk_sz
.get()
.checked_mul(header.blk_sz.get())
.ok_or_else(|| {
Error::Chunk(
index,
format!(
"Chunk size overflow: {} * {}",
self.chunk_sz.get(),
header.blk_sz.get(),
),
)
.ok_or_else(|| Error::ChunkSizeOverflow {
index,
chunk_size: self.chunk_sz.get(),
block_size: header.blk_sz.get(),
})?,
CHUNK_TYPE_FILL | CHUNK_TYPE_CRC32 => 4,
CHUNK_TYPE_DONT_CARE => 0,
t => return Err(Error::Chunk(index, format!("Invalid chunk type: {t}"))),
t => {
return Err(Error::InvalidChunkType {
index,
chunk_type: t,
});
}
};
data_size
.checked_add(mem::size_of::<Self>() as u32)
.ok_or_else(|| Error::Chunk(index, format!("Data size too large: {data_size}")))
.checked_add(header.chunk_hdr_sz.into())
.ok_or(Error::DataSizeTooLarge { index, data_size })
}
fn validate(&self, index: u32, header: &RawHeader, start_block: u32) -> Result<()> {
let end_block = start_block
.checked_add(self.chunk_sz.get())
.ok_or_else(|| {
Error::Chunk(
index,
format!(
"Block count overflow: {start_block} + {}",
self.chunk_sz.get(),
),
)
.ok_or_else(|| Error::BlockCountOverflow {
index,
start_block,
chunk_size: self.chunk_sz.get(),
})?;
if end_block > header.total_blks.get() {
return Err(Error::Chunk(
return Err(Error::EndBlockExceedsTotal {
index,
format!(
"End block {end_block} exceeds total blocks {}",
header.total_blks.get(),
),
))?;
end_block,
total_blocks: header.total_blks.get(),
})?;
}
if self.chunk_type.get() == CHUNK_TYPE_CRC32 && self.chunk_sz.get() != 0 {
return Err(Error::Chunk(
return Err(Error::Crc32ChunkNotEmpty {
index,
format!(
"CRC32 chunk has non-zero blocks: {:?}",
start_block..end_block,
),
));
chunk_size: self.chunk_sz.get(),
});
}
let expected_size = self.expected_size(index, header)?;
if expected_size != self.total_sz.get() {
return Err(Error::Chunk(
return Err(Error::InvalidChunkSize {
index,
format!(
"Expected total size {expected_size}, but have {}",
self.total_sz.get(),
),
));
expected_size,
total_size: self.total_sz.get(),
});
}
Ok(())
@@ -336,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,
}
@@ -479,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 {
@@ -631,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,
@@ -648,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)
}
}
@@ -658,14 +699,18 @@ impl<R: Read> SparseReader<R> {
/// data chunks if they are not needed. If the underlying file is seekable
/// and skipping chunks is needed, use [`Self::new_seekable`] instead.
pub fn new(mut inner: R, crc_mode: CrcMode) -> Result<Self> {
let mut header = RawHeader::new_zeroed();
inner.read_exact(header.as_bytes_mut())?;
let header =
RawHeader::read_from_io(&mut inner).map_err(|e| Error::DataRead("header", e))?;
header.validate()?;
inner
.read_discard(header.excess_raw_header_bytes().into())
.map_err(|e| Error::DataRead("header_excess", e))?;
Ok(Self {
inner,
seek: None,
seek_relative: None,
header,
block: 0,
chunk: 0,
@@ -699,20 +744,16 @@ 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::Reader(
"Cannot skip data when CRC validation is enabled".into(),
));
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 {
return Err(Error::Reader(format!(
"Previous chunk still has {} bytes remaining",
self.data_remain,
)));
return Err(Error::UnreadChunkData(self.data_remain));
}
}
@@ -720,11 +761,15 @@ impl<R: Read> SparseReader<R> {
return Ok(None);
}
let mut raw_chunk = RawChunk::new_zeroed();
self.inner.read_exact(raw_chunk.as_bytes_mut())?;
let raw_chunk =
RawChunk::read_from_io(&mut self.inner).map_err(|e| Error::DataRead("chunk", e))?;
raw_chunk.validate(self.chunk, &self.header, self.block)?;
self.inner
.read_discard(self.header.excess_raw_chunk_bytes().into())
.map_err(|e| Error::DataRead("chunk_excess", e))?;
let data: ChunkData;
match raw_chunk.chunk_type.get() {
@@ -735,8 +780,8 @@ impl<R: Read> SparseReader<R> {
data = ChunkData::Data;
}
CHUNK_TYPE_FILL => {
let mut fill_value = little_endian::U32::new_zeroed();
self.inner.read_exact(fill_value.as_bytes_mut())?;
let fill_value = little_endian::U32::read_from_io(&mut self.inner)
.map_err(|e| Error::DataRead("chunk_fill_value", e))?;
if let Some(hasher) = &mut self.hasher {
hash_fill_chunk(&raw_chunk, fill_value, &self.header, hasher);
@@ -752,23 +797,24 @@ impl<R: Read> SparseReader<R> {
data = ChunkData::Hole;
}
CHUNK_TYPE_CRC32 => {
let mut expected = little_endian::U32::new_zeroed();
self.inner.read_exact(expected.as_bytes_mut())?;
let expected = little_endian::U32::read_from_io(&mut self.inner)
.map_err(|e| Error::DataRead("chunk_crc32", e))?;
if let Some(hasher) = &mut self.hasher {
let actual = hasher.clone().finalize();
if actual != expected.get() {
return Err(Error::Reader(format!(
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Checkpoint {
expected: expected.get(),
actual,
});
}
}
data = ChunkData::Crc32(expected.get());
}
_ => unreachable!(),
};
}
let chunk = Chunk {
bounds: ChunkBounds {
@@ -792,9 +838,7 @@ impl<R: Read> SparseReader<R> {
let actual = hasher.finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Final { expected, actual });
}
}
}
@@ -838,10 +882,7 @@ impl<W: Write> SparseWriter<W> {
/// file to be seekable, so the [`Header`] must be fully known up front.
pub fn new(mut inner: W, header: Header) -> Result<Self> {
if header.minor_version != MINOR_VERSION {
return Err(Error::Writer(format!(
"Minor version not supported for writing: {}",
header.minor_version,
)));
return Err(Error::UnsupportedMinorVersion(header.minor_version));
}
let header = RawHeader {
@@ -858,7 +899,9 @@ impl<W: Write> SparseWriter<W> {
header.validate()?;
inner.write_all(header.as_bytes())?;
header
.write_to_io(&mut inner)
.map_err(|e| Error::DataWrite("header", e))?;
Ok(Self {
inner,
@@ -876,21 +919,18 @@ impl<W: Write> SparseWriter<W> {
/// [`ChunkData::Data`], the data must be fully written first.
pub fn start_chunk(&mut self, chunk: Chunk) -> Result<()> {
if self.data_remain != 0 {
return Err(Error::Writer(format!(
"Previous chunk still has {} bytes remaining",
self.data_remain,
)));
return Err(Error::UnwrittenChunkData(self.data_remain));
}
if self.chunk == self.header.total_chunks.get() {
return Err(Error::Writer("Already wrote all chunk headers".into()));
return Err(Error::TooManyChunks);
}
if chunk.bounds.start != self.block {
return Err(Error::Writer(format!(
"Gap between end of last chunk {} and start of new chunk {}",
self.block, chunk.bounds.start,
)));
return Err(Error::GapBetweenChunks {
prev_end: self.block,
cur_start: chunk.bounds.start,
});
}
let mut raw_chunk = RawChunk {
@@ -912,7 +952,9 @@ impl<W: Write> SparseWriter<W> {
self.chunk += 1;
self.block = chunk.bounds.end;
self.inner.write_all(raw_chunk.as_bytes())?;
raw_chunk
.write_to_io(&mut self.inner)
.map_err(|e| Error::DataWrite("chunk", e))?;
match chunk.data {
ChunkData::Data => {
@@ -920,7 +962,9 @@ impl<W: Write> SparseWriter<W> {
raw_chunk.total_sz.get() - u32::from(self.header.chunk_hdr_sz.get());
}
ChunkData::Fill(fill_value) => {
self.inner.write_all(&fill_value.to_le_bytes())?;
self.inner
.write_all(&fill_value.to_le_bytes())
.map_err(|e| Error::DataWrite("chunk_fill_value", e))?;
hash_fill_chunk(
&raw_chunk,
@@ -933,13 +977,13 @@ impl<W: Write> SparseWriter<W> {
hash_fill_chunk(&raw_chunk, 0.into(), &self.header, &mut self.hasher);
}
ChunkData::Crc32(expected) => {
self.inner.write_all(&expected.to_le_bytes())?;
self.inner
.write_all(&expected.to_le_bytes())
.map_err(|e| Error::DataWrite("chunk_crc32", e))?;
let actual = self.hasher.clone().finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected checkpoint CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Checkpoint { expected, actual });
}
}
}
@@ -954,9 +998,7 @@ impl<W: Write> SparseWriter<W> {
let actual = self.hasher.finalize();
if actual != expected {
return Err(Error::Reader(format!(
"Expected final CRC32 {expected:08x}, but have {actual:08x}",
)));
return Err(Error::MismatchedCrc32Final { expected, actual });
}
}
@@ -984,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() {
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
// The gf256 library uses compile-time proc macro code generation. Since
// dm-verity supports RS(255, 231) through RS(255, 253), we'll generate RS
+494
View File
@@ -0,0 +1,494 @@
// SPDX-FileCopyrightText: 2025 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)?;
Ok(CompressedReader::with_format(entry.reader(), format))
}
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());
Ok(CompressedReader::with_format(raw_reader, format))
}
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::Lz4Init(e) | Error::XzInit(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>(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(())
}
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
//! Since avbroot is primarily an application and not a library, the semver
//! versioning covers the CLI only. All Rust APIs can change at any time, even
+4 -6
View File
@@ -1,13 +1,11 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
process::ExitCode,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
atomic::{AtomicBool, Ordering},
},
};
@@ -28,7 +26,7 @@ fn main() -> ExitCode {
}
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
Ok(_) => ExitCode::SUCCESS,
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
error!("{e:?}");
+5 -7
View File
@@ -1,9 +1,7 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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::{
@@ -12,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
@@ -30,7 +28,7 @@ where
{
struct OctalStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
impl<T> Visitor<'_> for OctalStrVisitor<T>
where
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
+520 -295
View File
File diff suppressed because it is too large Load Diff
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
pub mod boot;
pub mod otacert;
+41 -26
View File
@@ -1,17 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
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 {
@@ -19,12 +20,10 @@ pub enum Error {
ZipTooSmall(usize),
#[error("New otacerts.zip is too large to fit in {0} bytes")]
ZipTooLarge(usize),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("x509 DER error")]
Der(#[from] x509_cert::der::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
#[error("Failed to write otacerts zip")]
ZipWrite(#[source] rawzip::Error),
#[error("Failed to write certificate to otacerts zip")]
CertWrite(#[source] crypto::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -75,16 +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);
writer.start_file("ota.x509.pem", options)?;
let name = "ota.x509.pem";
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)
@@ -92,12 +98,14 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
let mut modified = cert.clone();
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
modified.signature = BitString::from_bytes(&[])?;
// An empty ASN.1 bit string is always valid.
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();
@@ -111,9 +119,16 @@ pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8
Cow::Owned(modified)
};
crypto::write_pem_cert(&mut writer, &cert)?;
crypto::write_pem_cert(Path::new(name), &mut data_writer, &cert).map_err(Error::CertWrite)?;
let raw_writer = writer.finish()?;
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())
}
+70 -71
View File
@@ -1,29 +1,28 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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, ReadSeekReopen, SectionReader, WriteSeekReopen},
stream::{self, ReadFixedSizeExt, ReadWriteAt, UserPosFile},
util,
};
@@ -35,14 +34,16 @@ pub enum Error {
NoFooter,
#[error("No hash tree descriptor found in vbmeta header")]
NoHashTreeDescriptor,
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("AVB error")]
Avb(#[from] avb::Error),
#[error("OTA certificate error")]
OtaCert(#[from] otacert::Error),
#[error("I/O error")]
Io(#[from] io::Error),
#[error("{0:?} overflowed integer bounds during calculations")]
IntOverflow(&'static str),
#[error("Failed to update AVB header")]
AvbUpdate(#[source] avb::Error),
#[error("Failed to generate replacement otacerts zip")]
OtaCertZip(#[source] otacert::Error),
#[error("Failed to read image data")]
ReadData(#[source] io::Error),
#[error("Failed to write image data")]
WriteData(#[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -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,
@@ -121,11 +123,14 @@ pub fn patch_system_image(
let parent_span = Span::current();
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
let (mut header, footer, image_size) =
avb::load_image(UserPosFile::new(raw_file)).map_err(Error::AvbUpdate)?;
let Some(mut footer) = footer else {
return Err(Error::NoFooter);
};
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
let AppendedDescriptorMut::HashTree(descriptor) =
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
else {
return Err(Error::NoHashTreeDescriptor);
};
@@ -135,17 +140,18 @@ pub fn patch_system_image(
let modified_ranges = (0..num_chunks)
.into_par_iter()
.map(|chunk| -> Result<Vec<Range<u64>>> {
stream::check_cancel(cancel_signal)?;
stream::check_cancel(cancel_signal).map_err(Error::ReadData)?;
let offset = chunk * CHUNK_SIZE;
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
let mut buf = vec![0u8; size as usize];
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let mut file = UserPosFile::new(raw_file);
file.seek(SeekFrom::Start(offset))
.map_err(Error::ReadData)?;
let buf = file
.read_vec_exact(size as usize)
.map_err(Error::ReadData)?;
let mut writer = output.reopen_boxed()?;
let mut ranges = Vec::<Range<u64>>::new();
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
@@ -157,14 +163,16 @@ pub fn patch_system_image(
};
let zip_size = bounds_rel.end - bounds_rel.start;
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
let new_zip = otacert::create_zip_with_size(certificate, zip_size)
.map_err(Error::OtaCertZip)?;
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
stream::check_cancel(cancel_signal)?;
stream::check_cancel(cancel_signal).map_err(Error::WriteData)?;
writer.seek(SeekFrom::Start(bounds.start))?;
writer.write_all(&new_zip)?;
file.seek(SeekFrom::Start(bounds.start))
.map_err(Error::WriteData)?;
file.write_all(&new_zip).map_err(Error::WriteData)?;
ranges.push(bounds);
}
@@ -180,36 +188,27 @@ 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();
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
let update_ranges = Some(modified_ranges.as_slice());
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())
};
descriptor.update(input, output, update_ranges, cancel_signal)?;
descriptor
.update(raw_file, update_ranges, cancel_signal)
.map_err(Error::AvbUpdate)?;
if !header.public_key.is_empty() {
debug!("Signing system image");
header.set_algo_for_key(key)?;
header.sign(key)?;
header.set_algo_for_key(key).map_err(Error::AvbUpdate)?;
header.sign(key).map_err(Error::AvbUpdate)?;
}
let writer = output.reopen_boxed()?;
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) = header.appended_descriptor_mut()? else {
let AppendedDescriptorMut::HashTree(descriptor) =
header.appended_descriptor_mut().map_err(Error::AvbUpdate)?
else {
return Err(Error::NoHashTreeDescriptor);
};
@@ -217,15 +216,15 @@ pub fn patch_system_image(
let hash_tree_end = descriptor
.tree_offset
.checked_add(descriptor.tree_size)
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
.ok_or(Error::IntOverflow("hash_tree_end"))?;
let fec_data_end = descriptor
.fec_offset
.checked_add(descriptor.fec_size)
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
.ok_or(Error::IntOverflow("fec_data_end"))?;
let header_end = footer
.vbmeta_offset
.checked_add(footer.vbmeta_size)
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
.ok_or(Error::IntOverflow("avb_end"))?;
let footer_start = image_size - Footer::SIZE as u64;
let other_ranges = util::merge_overlapping(&[
+4
View File
@@ -1,3 +1,7 @@
#![allow(clippy::all)]
#![allow(clippy::nursery)]
#![allow(clippy::pedantic)]
pub mod build {
pub mod tools {
pub mod releasetools {
+322 -281
View File
@@ -1,56 +1,46 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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,
},
};
use bstr::ByteSlice;
use num_traits::ToPrimitive;
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 {
@@ -124,91 +114,144 @@ impl<W: Write> WriteZerosExt for W {
}
}
/// Extensions for readers to read strings.
pub trait ReadStringExt {
/// Read exact sized string.
fn read_string_exact(&mut self, size: usize) -> io::Result<String>;
/// Extensions for readers to read fixed-size buffers.
pub trait ReadFixedSizeExt {
/// Read fixed-size array.
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]>;
/// Read string with maximum size and trim trailing zeros.
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String>;
/// Read fixed-sized [`Vec`].
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>>;
}
impl<R: Read> ReadStringExt for R {
fn read_string_exact(&mut self, size: usize) -> io::Result<String> {
impl<R: Read> ReadFixedSizeExt for R {
fn read_array_exact<const N: usize>(&mut self) -> io::Result<[u8; N]> {
let mut buf = [0u8; N];
self.read_exact(&mut buf)?;
Ok(buf)
}
fn read_vec_exact(&mut self, size: usize) -> io::Result<Vec<u8>> {
let mut buf = vec![0u8; size];
self.read_exact(&mut buf)?;
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
}
fn read_string_padded(&mut self, max_size: usize) -> io::Result<String> {
let mut buf = vec![0u8; max_size];
self.read_exact(&mut buf)?;
let after_last_non_zero = buf
.iter()
.rev()
.position(|&b| b != 0)
.map_or(0, |i| buf.len() - i);
buf.resize(after_last_non_zero, 0);
buf.shrink_to_fit();
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
Ok(buf)
}
}
/// Extensions for writers to write strings.
pub trait WriteStringExt {
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()>;
/// 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<W: Write> WriteStringExt for W {
fn write_string_padded(&mut self, data: &str, max_size: usize) -> io::Result<()> {
if data.len() > max_size {
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::InvalidInput,
format!("{data:?} exceeds maximum size of {max_size} bytes"),
io::ErrorKind::UnexpectedEof,
format!(
"Expected to read {} bytes at {offset}, but reached EOF after {n} bytes",
buf.len(),
),
));
}
self.write_all(data.as_bytes())?;
let num_zeros = (max_size - data.len()) as u64;
self.write_zeros_exact(num_zeros)?;
Ok(())
}
}
/// 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>;
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)
}
}
};
}
impl<R: Read + Reopen> Reopen for BufReader<R> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufReader::new(self.get_ref().reopen()?))
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<()>;
}
impl<W: Write + Reopen> Reopen for BufWriter<W> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufWriter::new(self.get_ref().reopen()?))
}
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> {
@@ -366,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;
@@ -416,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))
@@ -531,72 +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::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.
@@ -651,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];
@@ -670,6 +727,8 @@ pub fn copy(
break;
}
inspect(&buf[..n]);
writer.write_all(&buf[..n])?;
copied += n as u64;
@@ -678,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::{
@@ -687,10 +751,9 @@ mod tests {
use ring::digest::Context;
use super::{
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, 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,
@@ -726,32 +789,6 @@ mod tests {
assert_eq!(&writer.into_inner(), b"\0\0foo\0");
}
#[test]
fn read_string() {
let mut reader = Cursor::new(b"foo\0\0bar\0\0");
assert_eq!(reader.read_string_exact(3).unwrap(), "foo");
assert_eq!(reader.read_string_exact(0).unwrap(), "");
reader.rewind().unwrap();
assert_eq!(reader.read_string_padded(3).unwrap(), "foo");
reader.rewind().unwrap();
assert_eq!(reader.read_string_padded(10).unwrap(), "foo\0\0bar");
}
#[test]
fn write_string() {
let mut writer = Cursor::new([0xffu8; 8]);
writer.write_string_padded("foobar", 8).unwrap();
assert_eq!(writer.get_ref(), b"foobar\0\0");
writer.rewind().unwrap();
writer.write_string_padded("foobarhi", 8).unwrap();
assert_eq!(writer.get_ref(), b"foobarhi");
}
#[test]
fn counting_reader() {
let raw_reader = Cursor::new(b"foobar");
@@ -841,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");
@@ -850,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);
}
}
+426 -29
View File
@@ -1,11 +1,18 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
use std::{
cmp::Ordering,
fmt::{self, Display},
mem,
ops::{
Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive,
},
path::{Component, Path, PathBuf},
};
use num_traits::PrimInt;
use num_traits::{NumCast, PrimInt};
use thiserror::Error;
pub const ZEROS: [u8; 16384] = [0u8; 16384];
@@ -23,6 +30,263 @@ impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
}
}
/// Stores a precomputed [`Debug`] string.
#[derive(Clone)]
pub struct DebugString(String);
impl DebugString {
pub fn new(value: impl fmt::Debug) -> Self {
Self(format!("{value:?}"))
}
}
impl fmt::Debug for DebugString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Hash, PartialEq, Eq)]
pub enum AnyRange<T> {
Range(Range<T>),
RangeFrom(RangeFrom<T>),
RangeFull(RangeFull),
RangeInclusive(RangeInclusive<T>),
RangeTo(RangeTo<T>),
RangeToInclusive(RangeToInclusive<T>),
}
impl<T> AnyRange<T> {
pub fn with_bounds(start: Bound<T>, end: Bound<T>) -> Option<Self> {
let result = match (start, end) {
(Bound::Included(s), Bound::Excluded(e)) => Self::Range(s..e),
(Bound::Included(s), Bound::Unbounded) => Self::RangeFrom(s..),
(Bound::Unbounded, Bound::Unbounded) => Self::RangeFull(..),
(Bound::Included(s), Bound::Included(e)) => Self::RangeInclusive(s..=e),
(Bound::Unbounded, Bound::Excluded(e)) => Self::RangeTo(..e),
(Bound::Unbounded, Bound::Included(e)) => Self::RangeToInclusive(..=e),
(Bound::Excluded(_), _) => return None,
};
Some(result)
}
}
impl<T: PartialOrd<T>> AnyRange<T> {
pub fn contains<U>(&self, item: &U) -> bool
where
T: PartialOrd<U>,
U: ?Sized + PartialOrd<T>,
{
<Self as RangeBounds<T>>::contains(self, item)
}
}
impl<T: fmt::Debug> fmt::Debug for AnyRange<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Range(r) => r.fmt(f),
Self::RangeFrom(r) => r.fmt(f),
Self::RangeFull(r) => r.fmt(f),
Self::RangeInclusive(r) => r.fmt(f),
Self::RangeTo(r) => r.fmt(f),
Self::RangeToInclusive(r) => r.fmt(f),
}
}
}
impl<T> RangeBounds<T> for AnyRange<T> {
fn start_bound(&self) -> Bound<&T> {
match self {
Self::Range(r) => r.start_bound(),
Self::RangeFrom(r) => r.start_bound(),
Self::RangeFull(r) => r.start_bound(),
Self::RangeInclusive(r) => r.start_bound(),
Self::RangeTo(r) => r.start_bound(),
Self::RangeToInclusive(r) => r.start_bound(),
}
}
fn end_bound(&self) -> Bound<&T> {
match self {
Self::Range(r) => r.end_bound(),
Self::RangeFrom(r) => r.end_bound(),
Self::RangeFull(r) => r.end_bound(),
Self::RangeInclusive(r) => r.end_bound(),
Self::RangeTo(r) => r.end_bound(),
Self::RangeToInclusive(r) => r.end_bound(),
}
}
}
impl<T> From<Range<T>> for AnyRange<T> {
fn from(value: Range<T>) -> Self {
Self::Range(value)
}
}
impl<T> From<RangeFrom<T>> for AnyRange<T> {
fn from(value: RangeFrom<T>) -> Self {
Self::RangeFrom(value)
}
}
impl<T> From<RangeFull> for AnyRange<T> {
fn from(value: RangeFull) -> Self {
Self::RangeFull(value)
}
}
impl<T> From<RangeInclusive<T>> for AnyRange<T> {
fn from(value: RangeInclusive<T>) -> Self {
Self::RangeInclusive(value)
}
}
impl<T> From<RangeTo<T>> for AnyRange<T> {
fn from(value: RangeTo<T>) -> Self {
Self::RangeTo(value)
}
}
impl<T> From<RangeToInclusive<T>> for AnyRange<T> {
fn from(value: RangeToInclusive<T>) -> Self {
Self::RangeToInclusive(value)
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LargeInt {
Signed(i64),
Unsigned(u64),
}
impl fmt::Display for LargeInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(n) => n.fmt(f),
Self::Unsigned(n) => n.fmt(f),
}
}
}
/// A non-generic type that can represent any 64-bit or smaller primitive
/// integer range.
#[derive(Clone, PartialEq, Eq)]
pub enum LargeIntRange {
Signed(AnyRange<i64>),
Unsigned(AnyRange<u64>),
}
impl fmt::Debug for LargeIntRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Signed(r) => r.fmt(f),
Self::Unsigned(r) => r.fmt(f),
}
}
}
/// An error returned when a value is not within a specific range.
#[derive(Clone, Debug, Error)]
#[error("Integer value {value} not in bounds: {range:?}")]
pub struct OutOfBoundsError {
value: LargeInt,
range: LargeIntRange,
}
/// Verify that `value` is within `bounds` and then return `value` if it is.
pub fn check_bounds<T: PrimInt>(
value: T,
range: impl Into<AnyRange<T>>,
) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
let range = range.into();
if !range.contains(&value) {
let value = if T::min_value() != T::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
let start = match range.start_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
let end = match range.end_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
LargeIntRange::Signed(AnyRange::with_bounds(start, end).unwrap())
} else {
let start = match range.start_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
let end = match range.end_bound() {
Bound::Excluded(n) => Bound::Excluded(NumCast::from(*n).unwrap()),
Bound::Included(n) => Bound::Included(NumCast::from(*n).unwrap()),
Bound::Unbounded => Bound::Unbounded,
};
LargeIntRange::Unsigned(AnyRange::with_bounds(start, end).unwrap())
};
return Err(OutOfBoundsError { value, range });
}
Ok(value)
}
/// Try to cast `value` to primitive integer type `T`. If it does not fit, the
/// error will indicate the valid range of values.
pub fn try_cast<T: PrimInt, V: PrimInt>(value: V) -> Result<T, OutOfBoundsError> {
const {
assert!(
mem::size_of::<T>() <= 8,
"Integer must be 64 bits or smaller"
);
}
NumCast::from(value).ok_or_else(|| {
let value = if V::min_value() != V::zero() {
LargeInt::Signed(NumCast::from(value).unwrap())
} else {
LargeInt::Unsigned(NumCast::from(value).unwrap())
};
let range = if T::min_value() != T::zero() {
let min = NumCast::from(T::min_value()).unwrap();
let max = NumCast::from(T::max_value()).unwrap();
LargeIntRange::Signed((min..=max).into())
} else {
let min = NumCast::from(T::min_value()).unwrap();
let max = NumCast::from(T::max_value()).unwrap();
LargeIntRange::Unsigned((min..=max).into())
};
OutOfBoundsError { value, range }
})
}
/// Check if a byte slice is all zeros.
pub fn is_zero(mut buf: &[u8]) -> bool {
while !buf.is_empty() {
@@ -40,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
@@ -62,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);
@@ -115,29 +440,101 @@ 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::*;
#[test]
fn test_any_range() {
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Excluded(1)).unwrap();
assert_eq!(range, AnyRange::from(0..1));
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Unbounded).unwrap();
assert_eq!(range, AnyRange::from(0..));
let range = AnyRange::<i32>::with_bounds(Bound::Unbounded, Bound::Unbounded).unwrap();
assert_eq!(range, AnyRange::from(..));
let range = AnyRange::with_bounds(Bound::Included(0), Bound::Included(1)).unwrap();
assert_eq!(range, AnyRange::from(0..=1));
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Excluded(1)).unwrap();
assert_eq!(range, AnyRange::from(..1));
let range = AnyRange::with_bounds(Bound::Unbounded, Bound::Included(1)).unwrap();
assert_eq!(range, AnyRange::from(..=1));
}
#[test]
fn test_check_bounds() {
check_bounds(i64::MIN, ..).unwrap();
check_bounds(i64::MAX, ..).unwrap();
check_bounds(u64::MIN, ..).unwrap();
check_bounds(u64::MAX, ..).unwrap();
check_bounds(0, -1..=1).unwrap();
let err = check_bounds(i8::MAX, 0..=0).unwrap_err();
assert_eq!(err.value, LargeInt::Signed(127));
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(0..=0)));
let err = check_bounds(u8::MAX, 0..=0).unwrap_err();
assert_eq!(err.value, LargeInt::Unsigned(255));
assert_eq!(err.range, LargeIntRange::Unsigned(AnyRange::from(0..=0)));
}
#[test]
fn test_try_cast() {
let value: u8 = try_cast(255u16).unwrap();
assert_eq!(value, 255);
let err = try_cast::<i8, _>(256u16).unwrap_err();
assert_eq!(err.value, LargeInt::Unsigned(256));
assert_eq!(err.range, LargeIntRange::Signed(AnyRange::from(-128..=127)));
}
#[test]
fn test_ranges_overlaps() {
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
assert!(!ranges_overlaps(&[0..4], &(0..0)));
assert!(ranges_overlaps(&[0..4], &(0..4)));
assert!(ranges_overlaps(&[0..4], &(1..4)));
assert!(ranges_overlaps(&[0..4], &(0..3)));
assert!(!ranges_overlaps(&[0..4], &(4..5)));
assert!(ranges_overlaps(&[5..8], &(5..9)));
assert!(ranges_overlaps(&[5..8], &(4..8)));
assert!(ranges_overlaps(&[5..8], &(4..9)));
assert!(ranges_overlaps(&[0..4, 5..8], &(4..5)));
assert!(ranges_overlaps(&[0..4, 5..8], &(0..9)));
}
#[test]
fn test_ranges_contains() {
assert_eq!(ranges_contains(&[0..4], &0), true);
assert_eq!(ranges_contains(&[0..4], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
assert!(ranges_contains(&[0..4], &0));
assert!(!ranges_contains(&[0..4], &4));
assert!(!ranges_contains(&[0..4, 5..8], &4));
assert!(ranges_contains(&[0..4, 5..8], &6));
}
}
+41 -41
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
io::{Cursor, Read, Seek, Write},
@@ -20,7 +18,7 @@ use avbroot::{
ChainPartitionDescriptor, Descriptor, Footer, HashDescriptor, HashTreeDescriptor, Header,
KernelCmdlineDescriptor, PropertyDescriptor,
},
stream::SharedCursor,
stream::{MutexFile, UserPosFile},
};
fn get_test_key() -> RsaSigningKey {
@@ -120,7 +118,7 @@ fn round_trip_root_image() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
@@ -138,11 +136,11 @@ fn round_trip_root_image() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xc4, 0xa5, 0xda, 0x3e, 0x09, 0xa2, 0xc8, 0x70, 0xcb, 0xf0, 0x96, 0x79, 0x0e, 0x1e,
0x80, 0xae, 0x5e, 0x37, 0x81, 0x27, 0x24, 0xc3, 0x6c, 0xa9, 0x42, 0x9e, 0x2c, 0xb1,
0x81, 0xad, 0xce, 0xee, 0x8d, 0x4f, 0x76, 0x45, 0x54, 0xc1, 0x31, 0x6a, 0xa7, 0x81,
0x5c, 0x59, 0xa8, 0xe8, 0x76, 0xab, 0xed, 0x5b, 0x07, 0x07, 0x38, 0xdd, 0x09, 0x86,
0x05, 0x39, 0x23, 0x2d, 0x7b, 0xcc, 0x57, 0x06,
0x3b, 0x01, 0xf6, 0x04, 0x04, 0x6e, 0x6f, 0x60, 0x9c, 0xb0, 0x8b, 0x8a, 0x43, 0xf7,
0x91, 0x2e, 0xc4, 0x1b, 0xc0, 0x7f, 0xa1, 0xe4, 0xe6, 0x59, 0x14, 0x08, 0xbe, 0x83,
0xae, 0x0a, 0x0f, 0x0a, 0x4a, 0x15, 0x91, 0x0e, 0x4d, 0x18, 0x31, 0x48, 0x20, 0xe8,
0x44, 0x62, 0x07, 0x98, 0x43, 0x30, 0xee, 0x2d, 0x20, 0x28, 0xc3, 0x94, 0xc6, 0x0e,
0x86, 0xa3, 0xa7, 0x17, 0x36, 0xfd, 0x50, 0x7c,
],
);
@@ -188,7 +186,7 @@ fn round_trip_appended_hash_image() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -237,11 +235,11 @@ fn round_trip_appended_hash_image() {
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0x09, 0x98, 0x0c, 0x9d, 0x11, 0x50, 0xde, 0xb1, 0x55, 0x3b, 0x00, 0x76, 0xbe, 0x25,
0xfd, 0xe6, 0x46, 0x22, 0xbd, 0x9a, 0x05, 0x86, 0xea, 0x07, 0x4d, 0x8f, 0x7b, 0x15,
0x36, 0x20, 0x0d, 0xf0, 0x7e, 0x96, 0xd2, 0x58, 0xde, 0xf2, 0xa6, 0x91, 0x6d, 0x01,
0x7b, 0x03, 0x96, 0x70, 0xf8, 0x3b, 0x76, 0x74, 0xf0, 0xbf, 0x47, 0xe0, 0xd2, 0xd4,
0x5d, 0xbf, 0xb7, 0x9c, 0xf5, 0xf8, 0xaf, 0x3c,
0x91, 0x38, 0x61, 0xc0, 0x68, 0x2a, 0x8b, 0xd8, 0x01, 0xa6, 0xe4, 0x4c, 0x1d, 0x27,
0x93, 0x1b, 0xa4, 0x63, 0xd1, 0xbb, 0xf1, 0x64, 0x05, 0xf2, 0xa1, 0xa0, 0xb3, 0x35,
0xe1, 0xc5, 0xac, 0x4f, 0x98, 0xb3, 0x0a, 0xed, 0xfc, 0xee, 0xa2, 0x6a, 0x77, 0xf4,
0xe5, 0x69, 0xa0, 0xcd, 0x7a, 0xd1, 0xfe, 0x1d, 0x07, 0xd1, 0x25, 0xc6, 0x22, 0xe0,
0x25, 0xcb, 0xe9, 0x75, 0x50, 0xe4, 0xae, 0x59,
],
);
@@ -296,7 +294,7 @@ fn round_trip_appended_hash_tree_image_fixed_size() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -308,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"),
}
@@ -336,20 +335,20 @@ 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!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
0x92, 0xdd, 0x4d, 0xc5, 0xb0, 0x5b, 0x4f, 0x65, 0x97, 0x5a, 0x72, 0x66, 0xde, 0x82,
0xc2, 0x2f, 0x33, 0x86, 0x8b, 0x65, 0x67, 0x80, 0x1d, 0xca, 0xd6, 0x2c, 0xfc, 0xca,
0xaf, 0x4c, 0x56, 0x64, 0x3a, 0xd1, 0x06, 0x01, 0xda, 0x2e, 0x05, 0x67, 0xd1, 0x01,
0xe3, 0xcb, 0x7b, 0x1e, 0xeb, 0x05, 0x89, 0xeb, 0x80, 0xcc, 0x17, 0x0c, 0x24, 0x73,
0x0d, 0xcb, 0x36, 0xfa, 0x17, 0xbd, 0x20, 0x7e,
],
);
@@ -403,7 +402,7 @@ fn round_trip_appended_hash_tree_image_minimum_size() {
rollback_index: 1677974400,
flags: 0,
rollback_index_location: 0,
release_string: repeat_str("MaxLength", 48),
release_string: repeat_str("MaxLength", 47),
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
};
let mut footer = Footer {
@@ -415,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"),
}
@@ -443,20 +443,20 @@ 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!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
[
0xd6, 0x69, 0x19, 0x6a, 0x36, 0xc8, 0x1c, 0xe9, 0xc4, 0x85, 0xbe, 0xff, 0x43, 0xb1,
0x9f, 0xd4, 0x1d, 0x6c, 0xf9, 0xd2, 0xf3, 0xa6, 0x5f, 0x66, 0x41, 0xd5, 0xf3, 0xfd,
0x28, 0xdb, 0x14, 0x67, 0xc6, 0xa8, 0xef, 0xc4, 0xd4, 0x67, 0x6c, 0xb8, 0x66, 0xbb,
0x56, 0x5a, 0x4a, 0xf5, 0xd8, 0x92, 0x7c, 0x42, 0xbc, 0x47, 0xdb, 0x94, 0x38, 0x15,
0x4b, 0x2d, 0xd0, 0x28, 0x1f, 0xd1, 0x45, 0xa9,
0xcf, 0x6b, 0x90, 0xcf, 0x77, 0x76, 0x62, 0x12, 0xc2, 0x22, 0xe6, 0xd5, 0x5b, 0xab,
0x82, 0xd8, 0x6c, 0x93, 0xa3, 0x35, 0x5b, 0x77, 0xe0, 0x38, 0x12, 0x48, 0x90, 0x0c,
0xee, 0xbf, 0x95, 0x31, 0xff, 0xc7, 0xf5, 0xb9, 0x4f, 0x18, 0x57, 0x46, 0x37, 0xbb,
0xce, 0x7b, 0xa7, 0x26, 0x18, 0x5a, 0x3c, 0x41, 0xb2, 0x2e, 0xb7, 0x86, 0x51, 0xdc,
0xf6, 0x26, 0x86, 0xf3, 0xc7, 0x96, 0x23, 0xed,
],
);
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::Cursor;
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{Cursor, Read, Seek, Write};
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{self, Cursor};
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{io::Cursor, num::NonZeroU64};
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{Cursor, Read, Write};
+12 -15
View File
@@ -33,19 +33,12 @@ allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"CC0-1.0",
"GPL-3.0",
"bzip2-1.0.6",
"GPL-3.0-only",
"ISC",
"MIT",
"OpenSSL",
"Unicode-DFS-2016",
]
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
"Unicode-3.0",
"Zlib",
]
[bans]
@@ -61,14 +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/bzip2-rs",
"https://github.com/chenxiaolong/zip",
"https://github.com/chenxiaolong/system-properties",
]
+7 -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,21 +15,16 @@ avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
ring = "0.17.0"
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 = "0.9.11", 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 = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
original = "ea96196191e3a4133db4aff45d47aa3468514e29e0a724faac8081cbf4adf808"
patched = "179923431ff94148186d8b0a457636c8a60057d047d55a4fc4da3a162e73eeed"
[profile.pixel_v4_gki.hashes_seekable]
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
original = "f6615ae355eba38689d24aa535981d09175d4832e7c65c04cdd89aa95d21f09d"
patched = "b006858b78cddef0e3db1d77454cd945111ce15708121e3a483ff711abedec8b"
# 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 = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
original = "cb2a2e406d2b4c68c8a38f819256a10ada0ed5a4732822bec35bdf7bcdc458eb"
patched = "45cbe96e01b8b2e3ba5e4a2c990e7ff08bee285316a4a7a314aab3ffddf27d0d"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
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 = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
original = "4b864b906a13ec98b1d6c3440a5eca7404ec63d073080e80a3b84afabcbd2fa2"
patched = "da3b9a4d6a58bb5aaadfc63006cb36b7af9c1c5945e81b38587c245bd5ad1f5a"
[profile.pixel_v3.hashes_seekable]
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
original = "e0e93ee11de56992f3a5f543858c1d11858b4deb76a91a4caf4fb0f3f34be855"
patched = "71682a4f31a5920784a0e2e38a6ee5fe3c65213436c7bf1ce347b121cd01a367"
# Google Pixel 4a
# What's unique: boot (boot v2)
[profile.pixel_v2]
vabc_algo = "Gzip"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
@@ -168,9 +173,9 @@ data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes_streaming]
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
original = "abc0ad7f80020018101437c2494d6564a877df663d208a123267fc100734c175"
patched = "132fb4d90e3c19a7a5ee0da2082767228255228ea5fb66811df1ccd5ff600bb5"
[profile.pixel_v2.hashes_seekable]
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
original = "57d4ac5ab7d362a593f3c02f3d2044b5400c10db0f732741a2d18e025d4231ec"
patched = "b365eb32f463ac4ba13f7bed4e6535a68483379640dc6e842c591f194db042c2"
+2 -4
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{ffi::OsString, path::PathBuf};
+25 -24
View File
@@ -1,16 +1,13 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// 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",
@@ -19,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,
@@ -31,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,
@@ -57,7 +54,7 @@ pub enum BootVersion {
VendorV4,
}
#[derive(Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct BootData {
pub version: BootVersion,
@@ -73,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),
@@ -93,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,
@@ -124,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:?}"))
}
+134 -105
View File
@@ -1,8 +1,6 @@
/*
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2023-2025 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
// SPDX-License-Identifier: GPL-3.0-only
mod cli;
mod config;
@@ -16,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},
@@ -38,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},
@@ -97,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 {
@@ -127,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 {
@@ -141,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)
},
@@ -190,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))
@@ -295,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,
@@ -376,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(),
@@ -412,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}"))?;
@@ -422,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,
@@ -433,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}"))?;
@@ -451,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);
@@ -511,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 {
@@ -530,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,
@@ -550,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,
@@ -562,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}"))?;
}
}
@@ -576,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,
@@ -589,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,
@@ -626,7 +630,8 @@ fn create_payload(
fec_roots: None,
version: None,
merge_operations: vec![],
estimate_cow_size: cow_estimate,
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)),
});
}
@@ -646,10 +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: 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![],
@@ -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(&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(())
}
@@ -1127,7 +1155,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 +1203,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 +1217,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 +1237,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 +1245,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 +1336,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 +1350,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 +1424,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
+5 -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
@@ -16,3 +17,6 @@ publish = false
[target.'cfg(unix)'.dependencies]
avbroot = { path = "../avbroot" }
honggfuzz = "0.5.55"
[lints]
workspace = true
+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,
]

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