Compare commits

...

59 Commits

Author SHA1 Message Date
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
Andrew Gunnerson 02ae9cd0e3 Version 3.7.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:42:42 -04:00
Andrew Gunnerson 9c92d32a80 CHANGELOG.md: Add entry for PR #357
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:42:19 -04:00
Andrew Gunnerson 296df25fb9 Ensure that PrepatchedImagePatcher runs before other patchers
On older devices, like the Pixel 4a, where `boot` is used for both
Android and recovery mode, the image will be patched by OtaCertPatcher
and PrepatchedImagePatcher. OtaCertPatcher was always set to run first,
so when PrepatchedImagePatcher used the user-supplied image as-is, prior
modifications got wiped out. This made is so users could no longer flash
further patched OTAs.

This is an unfortunate regression that was introduced in avbroot 2.0.0.
The e2e tests never caught this issue because the --prepatched test was
being fed the boot image previously patched by --magisk. That already
had valid certs so the result of OtaCertPatcher's modifications being
lost were not visible. This commit also fixes the e2e tests so that this
type of issue will be caught in the future.

Fixes: #356

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-21 18:17:46 -04:00
Andrew Gunnerson 352352b4d4 CHANGELOG.md: Add entry for PR #355
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 20:06:57 -04:00
Andrew Gunnerson e28ef42c0e Switch to passterm library for password prompts
This library behaves exactly the same as rpassword, but has zero
dependencies so we no longer need to pull in the ancient windows-sys
0.48.x libraries.

We still transitively depend on both windows-sys 0.52.x and 0.59.x
though.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 17:46:52 -04:00
Andrew Gunnerson 5775c86e5b CHANGELOG.md: Add entry for PR #354
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 16:19:31 -04:00
Andrew Gunnerson 3dc8e07c0f cli/payload: Allow inspecting delta payloads
The pack and unpack commands can never support delta payloads, but
there's no reason not to allow the repack and info commands to read
them.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-15 16:12:17 -04:00
Andrew Gunnerson 040dcd1a5c Return ExitCode from main
std::process::exit() calls the exit syscall, which doesn't run
destructors. It doesn't matter for avbroot, but better to use ExitCode
anyway.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 22:36:41 -04:00
Andrew Gunnerson 41a578975f CHANGELOG.md: Add entry for PR #347
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 02:23:05 -04:00
Andrew Gunnerson e9638d25b0 Add support for packing and unpacking Android sparse images
This supports all features of Android sparse images, including holes,
and CRC32 (both full image checksum and CRC32 chunks).

Partial sparse images, like those included in GrapheneOS' new optimized
factory images, can also be packed and unpacked with these new commands,
unlike AOSP's simg2img and img2simg tools.

This new functionality is not relevant for avbroot's main use case, but
is useful for unpacking certain factory images for comparison with OTAs
during troubleshooting.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-09-01 02:14:11 -04:00
Andrew Gunnerson e25080ddef Merge pull request #346 from bugreportion/patch-1
README.ru.md: update translation
2024-08-31 12:43:48 -04:00
Ivan Katrovsky 0e7d778cf5 README.ru.md: update translation
* https://github.com/chenxiaolong/avbroot/commit/8e52a9cf8c6f3bd6ea281d93364ac3cb9eddd806
* https://github.com/chenxiaolong/avbroot/commit/4a1dab40694b50cc4b914ff9bbfc869344afed9c
* https://github.com/chenxiaolong/avbroot/commit/24320d4fae309cd4f19a8ccff0ab923905d04e8f
* https://github.com/chenxiaolong/avbroot/commit/7113fb32efc9d1db5c76529d28317fcd3e42ebcb
* https://github.com/chenxiaolong/avbroot/commit/8ca1a289a8111da76ac0cc25ec64fdb2b051f643

Signed-off-by: Ivan Katrovsky <notbugreporter@proton.me>
2024-08-31 02:52:50 +03:00
Andrew Gunnerson 8ca1a289a8 README.md: Split setting ANDROID_PRODUCT_OUT env var to separate step
Also add examples for powershell and cmd instead of assuming bash
syntax.

Fixes: #340

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-30 19:12:56 -04:00
Andrew Gunnerson dfbc2f807f CHANGELOG.md: Add entry for PR #343
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-28 18:37:45 -04:00
Andrew Gunnerson e55cf3d679 lp: Sparse files are not required when packing and unpacking empty images
Empty images don't contain any extent metadata so the partition sizes
are just discarded.

Don't write parsers when you're tired, folks!

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-28 18:30:57 -04:00
Andrew Gunnerson 7c38c5609b CHANGELOG.md: Add entry for PR #342
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-27 23:57:59 -04:00
Andrew Gunnerson 13c910274e Add support for packing and unpacking logical partition images
This supports both empty and normal LP images, including those that span
multiple files/devices.

Currently, repacked files are semantically equivalent, but not exactly
identical. avbroot's data structure for the metadata does not preserve
the arbitrary partition ordering of the LP image. Instead, to make the
API a bit nicer, it only preserves the relative partition ordering
within partition groups.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-27 23:32:29 -04:00
Andrew Gunnerson dd40f64918 CHANGELOG.md: Add entry for PR #337
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-20 21:37:57 -04:00
Andrew Gunnerson 7113fb32ef Add support for outputting zip files with no data descriptors
This commit adds a new `--zip-mode` parameter to `avbroot ota patch` to
control whether the patched OTA zip is written with data descriptors or
not. By default, the `streaming` mode is used, which matches the current
behavior where the zip is hashed for signing as it is being written. The
new `seekable` mode fully writes the zip before rereading it to hash the
contents.

The new mode is useful for devices with broken zip parsers that fail to
properly handle data descriptors.

All of the end-to-end tests have been duplicated to test both modes.

Adding the seekable mode necessitated a couple other changes:

* BufWriter is no longer used. Type erasure is very painful in Rust, so
  we need to keep the writer types the same for both the streaming and
  seekable modes. BufWriter is unusable in the seekable mode because we
  need to be able to read back what was written, which isn't supported.

* HolePunchingWriter has been removed. It was a simple way to produce
  sparse files by seeking whenever a write buffer consists fully of
  zeros. When combined with BufWriter, there was previously never a
  situation where this was undesirable. However, with the new seekable
  mode and the zip library's pattern of writing one field at a time, the
  final 2 zero bytes (representing an empty archive comment) is never
  written and the file size is not increased either.

  Removing this is not a big deal since we no longer use stripped OTAs
  for the end-to-end tests. Those were really the only OTAs that
  benefitted from sparse files. A real OTA has very few zero bytes due
  to compression.

Issue: #328

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-20 21:31:33 -04:00
Andrew Gunnerson 9c9d656ea2 CHANGELOG.md: Add entry for PR #336
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 15:03:26 -04:00
Andrew Gunnerson e06674a7f0 Print a useful error message when there's no TTY for a password prompt
ENXIO (No such device or address) and ENOTTY (Inappropriate ioctl for
device) are not very user-friendly error messages.

Issue: https://github.com/chenxiaolong/my-avbroot-setup/issues/2

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 14:57:46 -04:00
Andrew Gunnerson cf5ef13e47 Version 3.6.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:38 -04:00
Andrew Gunnerson 5fada419cb CHANGELOG.md: Add entry for PR #335
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-19 00:03:05 -04:00
Andrew Gunnerson eeea9f41b4 cli/args: Use tracing::Level directly
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:58:11 -04:00
Andrew Gunnerson 0bebf120c6 CHANGELOG.md: Add entry for PR #334
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:57:40 -04:00
Andrew Gunnerson 264c602fdb cli/ota: Remove unnecessary mutex
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:49:41 -04:00
Andrew Gunnerson 343e2e279c CHANGELOG.md: Add entry for PR #333
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:44:45 -04:00
Andrew Gunnerson 59ca759262 Add support for gzip VABC algorithm
Older devices, like the Pixel 4a 5G (bramble) use gzip instead of lz4.

This commit also reworks the CoW size estimate calculation to add the
same constant headroom that AOSP's delta_generator adds. Previously,
avbroot was already adding an additional 1% to account for differences
in compression ratios across compression library implementations. This
papered over the issue for large partitions, but small partitions could
still have a CoW size estimate that's too small. Adding the constant
headroom prevents ENOSPC when flashing those partitions.

Fixes: #332

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2024-08-18 23:33:43 -04:00
66 changed files with 6585 additions and 807 deletions
+2
View File
@@ -85,6 +85,8 @@ jobs:
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.artifact.name }}
- name: Clippy
shell: bash
+63
View File
@@ -7,6 +7,43 @@
to update the actual links at the bottom of the file.
-->
### 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])
* This affected older devices without `vendor_boot` or `recovery` partitions.
* **This caused sideloading patched OTA updates from recovery mode to break on the affected devices.** To fix the problem without wiping the device and starting fresh, please follow the [steps in the PR](https://github.com/chenxiaolong/avbroot/pull/357#issuecomment-2365343050).
* Print a useful error message when trying to prompt for a passphrase without an interactive terminal ([PR #336])
* Add a new `--zip-mode seekable` option to allow writing OTA zip files without data descriptors ([Issue #328], [PR #337])
* Add new commands for packing and unpacking logical partition images (`super.img`) ([PR #342], [PR #343])
* Add new commands for packing and unpacking Android sparse images ([PR #347])
* Allow `avbroot payload repack` and `avbroot payload info` commands to read delta payloads ([PR #354])
* Switch to passterm library for password prompts ([PR #355])
### Version 3.6.0
* Add support for gzip compression when computing CoW size estimates ([Issue #332], [PR #333])
* This allows `--replace` to successfully replace dynamic partitions on legacy devices, like the Pixel 4a 5G
* Minor code cleanup ([PR #334], [PR #335])
### Version 3.5.0
* Update all dependencies ([PR #329])
@@ -236,6 +273,9 @@ Behind-the-scenes changes:
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
[Issue #310]: https://github.com/chenxiaolong/avbroot/issues/310
[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
[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
@@ -333,3 +373,26 @@ Behind-the-scenes changes:
[PR #323]: https://github.com/chenxiaolong/avbroot/pull/323
[PR #329]: https://github.com/chenxiaolong/avbroot/pull/329
[PR #331]: https://github.com/chenxiaolong/avbroot/pull/331
[PR #333]: https://github.com/chenxiaolong/avbroot/pull/333
[PR #334]: https://github.com/chenxiaolong/avbroot/pull/334
[PR #335]: https://github.com/chenxiaolong/avbroot/pull/335
[PR #336]: https://github.com/chenxiaolong/avbroot/pull/336
[PR #337]: https://github.com/chenxiaolong/avbroot/pull/337
[PR #342]: https://github.com/chenxiaolong/avbroot/pull/342
[PR #343]: https://github.com/chenxiaolong/avbroot/pull/343
[PR #347]: https://github.com/chenxiaolong/avbroot/pull/347
[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
Generated
+272 -257
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "3.5.0"
version = "3.9.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+104
View File
@@ -66,6 +66,16 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
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`.
### 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
@@ -253,8 +263,58 @@ avbroot hash-tree verify -i <input data file> -H <input hash tree file>
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
## `avbroot lp`
This set of commands is for working with LP (logical partition) images. These are the containers for dynamically-allocated partitions, like `system`. All LP images are supported:
* Empty images: These are the `super_empty.img` images in the factory images for newer Google Pixel devices. They define the layout of the `super` partition, but don't contain any actual data. They also do not contain a backup copy of the metadata. As an optimization, `fastboot` can fill in the actual data during flashing to avoid needing to reboot to fastbootd mode.
* Normal images backed by a single device: These are standalone `super.img` images and are how logical partitions are physically stored on disk in most newer devices. They contain a backup copy of all metadata as well as actual partition data.
* Normal images backed by multiple devices: These are images split across multiple files/partitions and are used on devices where support for LP was retrofitted. For example, the LP setup on newer Android builds for the Google Pixel 3a XL reuse the legacy `system` and `vendor` partitions because there is no `super` partition. These are similar to the single-file LP setups, except that data can be stored across all of the LP images. However, the metadata is only stored on the first LP image.
### Unpacking an LP image
```bash
avbroot lp unpack -i <input LP image> [-i <input LP image>]...
```
This subcommand unpacks the LP metadata to `lp.toml` and the partition images to the `lp_images` directory (for normal images).
If there are multiple images, they must be specified in order. If the order is not known, run `avbroot lp info` on each of the images. The one that successfully parses is the first image and the `block_devices` field in the output specifies the full ordering.
An LP image can have multiple slots. If the LP image originated from a factory image or OTA, all slots are likely identical. If the LP image was dumped from a real device that installed OTA updates in the past, the slots may differ. If the slots are not identical, then the `--slot` option is required to specify which slot to unpack.
### Packing an LP image
```bash
avbroot lp pack -o <output LP image> [-o <output LP image>]...
```
This subcommand packs a new LP image from the `lp.toml` file and `lp_images` directory (for normal images). Any `.img` files in the `lp_images` directory that don't have a corresponding entry in `lp.toml` are silently ignored.
All metadata slots in the newly packed LP image will be identical.
### Repacking an LP image
```bash
avbroot lp repack -i <input LP image> [-i <input LP image>]... -o <output LP image> [-o <output LP image>]...
```
This subcommand is logically equivalent to `avbroot lp unpack` followed by `avbroot lp pack`, except more efficient. Instead of unpacking and packing all partition images, the raw data is directly copied from the old LP image to the new LP image.
When `--slot` is specified, this is useful for discarding unwanted metadata slots and the partition data exclusive to them.
### Showing LP image metadata
```bash
avbroot lp info -i <first LP image>
```
This subcommand shows the LP image metadata, including all metadata slots. If there are multiple images, only the first one is needed because it is the only one that stores the metadata.
## `avbroot payload`
This set of commands is for working with payload binary files (`payload.bin`). The `unpack` and `pack` commands can only work with full payloads because they require the complete data to be available, but the `repack` and `info` commands also work with delta payloads.
### Unpacking a payload binary
```bash
@@ -292,3 +352,47 @@ avbroot payload info -i <payload>
```
This subcommand shows all of the payload header fields (which will likely be extremely long).
## `avbroot sparse`
This set of commands is for working with Android sparse images. All features of the file format are supported, including hole chunks and CRC32 checksums.
### Unpacking a sparse image
```bash
avbroot sparse unpack -o <input sparse image> -o <output raw image>
```
This subcommand unpacks a sparse image to a raw image. If the sparse image contains CRC32 checksums, they will be validated during unpacking. If the sparse image contains holes, the output image will be created as a native sparse file.
Certain fastboot factory images may have multiple sparse images, like `super_1.img`, `super_2.img`, etc., where they all touch a disjoint set of regions on the same partition. These can be unpacked by running this subcommand for each sparse image and specifying the `--preserve` option along with using the same output file. This preserves the existing data in the output file when unpacking each sparse image.
### Packing a sparse image
```bash
avbroot sparse pack -i <input raw image> -o <output sparse image>
```
This subcommand packs a new sparse image from a raw image. The default block size is 4096 bytes, which can be changed with the `--block-size` option.
By default, this will pack the entire input file. However, on Linux, there is an optimization where all holes in the input file, if it is a native sparse file, will be stored as hole chunks instead of `0`-filled chunks in the output sparse image.
To pack a partial sparse image, such as those used in the special fastboot factory images mentioned above, pass in `--region <start> <end>`. This option can be specified multiple times to pack multiple regions.
Unlike AOSP's `img2simg` tool, which never writes CRC32 checksums, this subcommand will write checksums if the input file has no holes and the entire file is being packed.
### Repacking a sparse image
```bash
avbroot sparse repack -i <input sparse image> -o <output sparse image>
```
This subcommand is logically equivalent to `avbroot sparse unpack` followed by `avbroot sparse pack`, except more efficient. This is useful for roundtrip testing of avbroot's sparse file parser.
### Showing sparse image metadata
```bash
avbroot sparse info -i <input sparse image>
```
This subcommand shows the sparse image metadata, including the header and all chunks.
+38 -6
View File
@@ -161,17 +161,37 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
If you prefer to extract and flash all OS partitions just to be safe, pass in `--all`.
5. Flash the partition images that were extracted.
5. Set the `ANDROID_PRODUCT_OUT` environment variable to the directory containing the extracted files.
For sh/bash/zsh (Linux, macOS, WSL):
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
export ANDROID_PRODUCT_OUT=extracted
```
For PowerShell (Windows):
```powershell
$env:ANDROID_PRODUCT_OUT = "extracted"
```
For cmd (Windows):
```bat
set ANDROID_PRODUCT_OUT=extracted
```
6. Flash the partition images that were extracted.
```bash
fastboot flashall --skip-reboot
```
Note that this only flashes the OS partitions. The bootloader and modem/radio partitions are left untouched due to fastboot limitations. If they are not already up to date or if unsure, after fastboot completes, follow the steps in the [updates section](#updates) to sideload the patched OTA once. Sideloading OTAs always ensures that all partitions are up to date.
Alternatively, for Pixel devices, running `flash-base.sh` from the factory image will also update the bootloader and modem.
6. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
7. Set up the custom AVB public key in the bootloader after rebooting from fastbootd to bootloader.
```bash
fastboot reboot-bootloader
@@ -179,7 +199,7 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
```
7. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
8. **[Optional]** Before locking the bootloader, reboot into Android once to confirm that everything is properly signed.
Install the Magisk or KernelSU app and run the following command:
@@ -193,7 +213,7 @@ 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
```
8. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
9. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
```bash
fastboot flashing lock
@@ -205,7 +225,7 @@ 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.
9. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
10. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
## Updates
@@ -365,6 +385,12 @@ Note that avbroot will validate that the prepatched image is compatible with the
avbroot can be used for just re-signing an OTA by specifying `--rootless` instead of `--magisk`/`--prepatched`. With this option, the patched OTA will not be rooted. The only modification applied is the replacement of the OTA verification certificate so that the OS can be upgraded with future (patched) OTAs.
### Skipping recovery OTA certificate patches
avbroot can skip modifying `otacerts.zip` in the recovery image with the `--skip-recovery-ota-cert` option. **Do not do this unless you have a good reason to do so.** (For example, if you've already manually inserted the OTA certificate into a boot image specified with `--prepatched` or `--replace`.) When this option is used with `--rootless` (and `--dsu` is not specified), then no modifications are performed on any boot image besides ensuring they are properly signed.
When manually adding the OTA certificate to a boot image, [verifying the patched OTA](#verifying-otas) afterwards is recommended to ensure that it was properly done.
### 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`.
@@ -436,6 +462,12 @@ avbroot ota extract \
--all
```
### 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.
The seekable mode writes zip files without data descriptors, but as the name implies, requires seeking around the file instead of writing it sequentially. The sha256 digest for the digital signature is computed after the zip file has been fully written.
### Signing with an external program
avbroot supports delegating all RSA signing operations to an external program with the `--signing-helper` option. When using this option, the `--key-avb` and `--key-ota` options must be given a public key instead of a private key.
+66 -24
View File
@@ -128,7 +128,7 @@ avbroot модифицирует следующие образы:
avbroot key generate-cert -k ota.key -o ota.crt
```
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
avbroot совместим с любым стандартным 4096-битным приватным ключом RSA в кодировке PKCS#8 и сертификатом X509 в кодировке PEM, например с теми, которые генерируются openssl.
Если вы потеряете ключ(-и) подписи AVB или OTA, вы больше не сможете подписывать новые OTA-архивы. Придется генерировать новые ключи подписи и разблокировать загрузчик (что приведет к стиранию всех данных). В таком случае возвращайтесь к инструкции в разделе [использования.](#использование)
@@ -159,17 +159,37 @@ avbroot совместим с любым стандартным 4096-битны
Если вы на всякий случай хотите прошить вообще все разделы из ОТА, извлечь их можно, указав аргумент `--all`.
5. Прошейте извлеченные образы разделов.
5. Установите переменную окружения `ANDROID_PRODUCT_OUT`, указав директорию с извлеченными файлами.
Для sh/bash/zsh (Linux, macOS, WSL):
```bash
ANDROID_PRODUCT_OUT=extracted fastboot flashall --skip-reboot
export ANDROID_PRODUCT_OUT=extracted
```
Для PowerShell (Windows):
```powershell
$env:ANDROID_PRODUCT_OUT = "extracted"
```
Для cmd (Командная строка или Терминал) (Windows):
```bat
set ANDROID_PRODUCT_OUT=extracted
```
6. Прошейте извлеченные образы разделов.
```bash
fastboot flashall --skip-reboot
```
Обратите внимание, что так прошиваются лишь те образы, что относятся к системе. Разделы загрузчика и модема же остаются нетронутыми из-за ограничений fastboot. Если они не обновлены до необходимой версии, или вы не уверены в этом, после прошивки перейдите к пункту [обновлений](#обновления) и установите пропатченный OTA-архив сайдлоадом в режиме Recovery. Прошивка полного OTA гарантирует, что абсолютно все разделы будут обновлены.
Для устройств Pixel есть ещё один вариант: запуск скрипта `flash-base.sh` из папки заводских образов (factory images) обновит загрузчик и модем.
6. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
7. После перезагрузки из fastbootd в загрузчик (bootloader), установите пользовательский публичный ключ AVB в загрузчик:
```bash
fastboot reboot-bootloader
@@ -177,7 +197,7 @@ avbroot совместим с любым стандартным 4096-битны
fastboot flash avb_custom_key /путь/к/avb_pkmd.bin
```
7. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
8. **[Опционально]** Перед блокировкой загрузчика загрузитесь в систему, дабы убедиться, что все подписано правильно.
Установите приложение Magisk или KernelSU и выполните следующую команду:
@@ -191,7 +211,7 @@ avbroot совместим с любым стандартным 4096-битны
init: [libfs_avb]Returning avb_handle with status: Success
```
8. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
9. Перезагрузитесь в fastboot и заблокируйте загрузчик. Это снова приведет к стиранию данных.
```bash
fastboot flashing lock
@@ -203,7 +223,7 @@ avbroot совместим с любым стандартным 4096-битны
**ПРЕДУПРЕЖДЕНИЕ**: Если вы прошили CalyxOS, мастер настройки [автоматически отключит опцию `Заводской разблокировки`.](https://github.com/CalyxOS/platform_packages_apps_SetupWizard/blob/7d2df25cedcbff83ddb608e628f9d97b38259c26/src/org/lineageos/setupwizard/SetupWizardApp.java#L135-L140) Не забудьте снова включить её вручную в настройках для разработчиков. Для перестраховки можете использовать [модуль `OEMUnlockOnBoot`,](https://github.com/chenxiaolong/OEMUnlockOnBoot) который автоматически включает пункт Заводской разблокировки при каждом запуске системы.
9. Готово! Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
10. Готово! Установка последующих обновлений системы, Magisk или KernelSU, описывается в [следующем разделе.](#обновления)
## Обновления
@@ -426,6 +446,34 @@ avbroot ota extract \
--all
```
### Режим записи ZIP
По умолчанию, avbroot использует потоковую запись для вывода OTA во время патчинга. Это означает, что он вычисляет дайджест sha256 для цифровой подписи одновременно с записью файла. Такой режим приводит к тому, что в ZIP-файле появляются описатели данных, что является частью стандарта ZIP и работает на подавляющем большинстве устройств. Однако некоторые устройства могут иметь некорректно работающие парсеры ZIP-файлов и не смогут правильно прочитать ZIP-файлы OTA, содержащие описатели данных. Если это так, используйте опцию `--zip-mode seekable` при патчинге.
Режим seekable записывает ZIP-файлы без описателей данных, но, как следует из названия, требует перемещения по файлу, вместо последовательной записи. Дайджест sha256 для цифровой подписи вычисляется после того, как ZIP-файл был полностью записан.
### Подписание с использованием внешней программы
avbroot поддерживает делегирование всех операций подписания RSA внешней программе с помощью опции `--signing-helper`. При использовании этой опции, для `--key-avb` и `--key-ota` должен быть указан публичный ключ вместо приватного.
Для каждой операции подписания, avbroot будет вызывать программу с параметрами:
```bash
<helper> <algorithm> <public key>
```
Алгоритм (`<algorithm>`) — это один из `SHA{256,512}_RSA{2048,4096}`, а публичный ключ (`<public key>`) — это тот, что был передан в avbroot. Внешняя программа может использовать публичный ключ для поиска соответствующего приватного ключа (например, на аппаратном модуле безопасности). avbroot запишет дайджест, отформатированный по PKCS#1 v1.5, в `stdin`, а внешняя программа должна выполнить операцию сырого подписания RSA и записать сырую подпись (октетная строка, соответствующая размеру ключа) в `stdout`.
По умолчанию, это поведение совместимо с опцией `--signing_helper` в avbtool от AOSP. Однако avbroot дополнительно расширяет аргументы для поддержки неинтерактивного использования. Если используются опции `--pass-{avb,ota}-file` или `--pass-{avb,ota}-env-var`, то внешняя программа будет вызвана с двумя дополнительными аргументами, указывающими на файл пароля или переменную окружения.
```bash
<helper> <algorithm> <public key> file <pass file>
# или
<helper> <algorithm> <public key> env <env file>
```
Обратите внимание, что avbroot проверит подпись, возвращенную внешней программой, на соответствие с публичным ключом. Это гарантирует, что процесс патчинга завершится ошибкой, если был использован неправильный приватный ключ.
## Сборка из исходного кода
Убедитесь, что у вас установлен [набор инструментов Rust.](https://www.rust-lang.org/ru/) Затем выполните:
@@ -440,25 +488,19 @@ cargo build --release
По умолчанию исполняемый файл ссылается на системные библиотеки bzip2 и liblzma, от которых зависит avbroot. Чтобы скомпилировать и статически связать эти две библиотеки, укажите аргумент `--features static`.
### Кросс-компиляция на Android
Чтобы использовать кросс-компиляцию на Android, установите [cargo-android](https://github.com/chenxiaolong/cargo-android) и воспользуйтесь оболочкой `cargo android`. Чтобы создать релизную сборку для aarch64, выполните:
```bash
cargo android build --release --target aarch64-linux-android
```
Возможно выполнение тестов, если хост работает под управлением Linux, установлен qemu-user-static, а исполняемый файл собран с `RUSTFLAGS=-C target-feature=+crt-static` и `--features static`.
## Проверка цифровых подписей
Сначала сохраните открытый ключ в файл, перечисляющий ключи, которым можно доверять. Это тот же ключ, который указан [в профиле автора.](https://github.com/chenxiaolong/)
```bash
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
```
Затем проверьте подпись zip-файла, используя список доверенных ключей.
```bash
ssh-keygen -Y verify -f avbroot_trusted_keys -I avbroot -n file -s <file>.zip.sig < <file>.zip
```
Если файл успешно проверен, вывод будет следующим:
```
Good "file" signature for avbroot with ED25519 key SHA256:Ct0HoRyrFLrnF9W+A/BKEiJmwx7yWkgaW/JvghKrboA
```
Чтобы проверить цифровые подписи, [следуйте этой инструкции.](https://github.com/chenxiaolong/chenxiaolong/blob/master/VERIFY_SSH_SIGNATURES.md)
## Вклад
+12 -3
View File
@@ -11,7 +11,7 @@ publish = false
[dependencies]
anyhow = "1.0.75"
base64 = "0.22.1"
bitflags = "2.4.1"
bitflags = { version = "2.4.1", features = ["serde"] }
bstr = "1.6.2"
byteorder = "1.4.3"
cap-std = "3.0.0"
@@ -20,15 +20,19 @@ clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
const-oid = "0.9.5"
crc32fast = "1.4.2"
ctrlc = "3.4.0"
dlv-list = "0.5.2"
flate2 = "1.0.27"
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
liblzma = "0.3.0"
lz4_flex = "0.11.1"
memchr = "2.6.0"
miniz_oxide = "0.8.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
passterm = "2.0.3"
phf = { version = "0.11.2", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
prost = "0.13.1"
@@ -40,18 +44,19 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
# instructions. sha2 is still used for signing purposes.
# https://github.com/RustCrypto/hashes/issues/327
ring = "0.17.0"
rpassword = "7.2.0"
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"
thiserror = "2.0.3"
toml_edit = { version = "0.22.9", 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.8.5"
zerocopy-derive = "0.8.5"
# There are multiple upstream bugs that cause infinite loops in the Drop
# implementation of write::BzDecoder. Unfortunately, the project is no longer
@@ -68,6 +73,7 @@ default-features = false
features = ["deflate"]
[target.'cfg(unix)'.dependencies]
libc = "0.2.158"
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
[build-dependencies]
@@ -80,3 +86,6 @@ assert_matches = "1.5.0"
[features]
static = ["bzip2/static", "liblzma/static"]
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
+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};
+11 -42
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,
@@ -15,7 +13,7 @@ use clap::{Parser, Subcommand, ValueEnum};
use tracing::{debug, Level};
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload, sparse};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -27,8 +25,10 @@ pub enum Command {
Fec(fec::FecCli),
HashTree(hashtree::HashTreeCli),
Key(key::KeyCli),
Lp(lp::LpCli),
Ota(ota::OtaCli),
Payload(payload::PayloadCli),
Sparse(sparse::SparseCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
@@ -37,39 +37,6 @@ pub enum Command {
MagiskInfo(boot::MagiskInfoCli),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl LogLevel {
fn as_level(self) -> Level {
match self {
Self::Trace => Level::TRACE,
Self::Debug => Level::DEBUG,
Self::Info => Level::INFO,
Self::Warn => Level::WARN,
Self::Error => Level::ERROR,
}
}
}
impl Default for LogLevel {
fn default() -> Self {
Self::Info
}
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub enum LogFormat {
Short,
@@ -96,8 +63,8 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t)]
@@ -124,11 +91,11 @@ impl FormatTime for ShortUptime {
}
}
pub fn init_logging(log_level: LogLevel, log_format: LogFormat) {
pub fn init_logging(log_level: Level, log_format: LogFormat) {
let builder = tracing_subscriber::fmt()
.with_writer(io::stderr)
.with_ansi(io::stderr().is_terminal())
.with_max_level(log_level.as_level());
.with_max_level(log_level);
match log_format {
LogFormat::Short => {
@@ -163,8 +130,10 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
Command::Fec(c) => fec::fec_main(&c, cancel_signal),
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
Command::Key(c) => key::key_main(&c),
Command::Lp(c) => lp::lp_main(&c, cancel_signal),
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Payload(c) => payload::payload_main(&c, cancel_signal),
Command::Sparse(c) => sparse::sparse_main(&c, cancel_signal),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
+123 -7
View File
@@ -1,13 +1,11 @@
/*
* 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::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
fs::{self, File},
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
str,
sync::atomic::AtomicBool,
@@ -30,7 +28,7 @@ use crate::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, PSeekFile, Reopen},
stream::{self, check_cancel, PSeekFile, Reopen, ToWriter},
util,
};
@@ -400,7 +398,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")?;
@@ -602,6 +602,90 @@ pub fn verify_descriptors(
.collect()
}
fn compute_digest_recursive(
directory: &Dir,
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());
ensure_name_is_safe(name)?;
let path = format!("{name}.img");
let mut raw_reader = directory
.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 mut raw_header_orig = vec![0u8; raw_header.len()];
raw_reader
.read_exact(&mut raw_header_orig)
.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,
)?;
}
}
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: &Dir, 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<()> {
let (info, mut reader) = read_avb_image(&cli.input)?;
display_info(&cli.display, &info);
@@ -738,6 +822,25 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
Ok(())
}
fn digest_subcommand(cli: &DigestCli, cancel_signal: &AtomicBool) -> Result<()> {
let authority = ambient_authority();
let parent_path = util::parent_path(&cli.input);
let directory = Dir::open_ambient_dir(parent_path, authority)
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
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(())
}
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
@@ -745,6 +848,7 @@ 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),
AvbCommand::Digest(c) => digest_subcommand(c, cancel_signal),
}
}
@@ -949,6 +1053,17 @@ struct VerifyCli {
repair: bool,
}
/// 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)]
enum AvbCommand {
Unpack(UnpackCli),
@@ -957,6 +1072,7 @@ enum AvbCommand {
#[command(alias = "dump")]
Info(InfoCli),
Verify(VerifyCli),
Digest(DigestCli),
}
/// Pack, unpack, and inspect AVB-protected images.
+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::{
fs::{self, File},
+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;
+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::{
fs::{self, File},
+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::{
fs::{File, OpenOptions},
+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::{
fs::{File, OpenOptions},
+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,
+679
View File
@@ -0,0 +1,679 @@
// SPDX-FileCopyrightText: 2024 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 clap::{CommandFactory, Parser, Subcommand};
use rayon::iter::{
IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator,
};
use crate::{
format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE},
stream::{self, FromReader, PSeekFile, Reopen, ToWriter},
};
fn open_lp_inputs(paths: &[impl AsRef<Path>]) -> Result<(Vec<PSeekFile>, Metadata)> {
let mut 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:?}"))
})
.collect::<Result<Vec<_>>>()?;
let metadata = Metadata::from_reader(&mut 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>> {
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:?}"))
})
.collect::<Result<Vec<_>>>()
}
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)
.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)
.with_context(|| format!("Failed to serialize metadata info TOML: {path:?}"))?;
fs::write(path, data)
.with_context(|| format!("Failed to write metadata info TOML: {path:?}"))?;
Ok(())
}
fn display_metadata(cli: &LpCli, metadata: &Metadata) {
if !cli.quiet {
println!("{metadata:#?}");
}
}
struct CopyExtent {
device_index: usize,
lp_offset: u64,
out_offset: u64,
size: u64,
}
/// Split extents into smaller ones for parallelization.
fn split_extents(extents: &[Extent]) -> Vec<CopyExtent> {
// 64 MiB is the smallest size we'll parallelize by.
const CHUNK_SIZE: u64 = 64 * 1024 * 1024;
let mut result = vec![];
let mut out_offset = 0;
for extent in extents {
let mut remain = extent.num_sectors * u64::from(SECTOR_SIZE);
match extent.extent_type {
ExtentType::Linear {
start_sector,
block_device_index,
} => {
let mut lp_offset = start_sector * u64::from(SECTOR_SIZE);
// 64 MiB is the smallest size we'll parallelize by.
let num_chunks = remain.div_ceil(64 * 1024 * 1024);
for _ in 0..num_chunks {
let chunk_size = CHUNK_SIZE.min(remain);
result.push(CopyExtent {
device_index: block_device_index,
out_offset,
lp_offset,
size: chunk_size,
});
out_offset += chunk_size;
lp_offset += chunk_size;
remain -= chunk_size;
}
}
ExtentType::Zero => out_offset += remain,
}
}
result
}
/// 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)
}
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)
}
}
}
/// Remove all slots aside from the specified one and return the old slot count.
fn retain_slot(metadata: &mut Metadata, slot: usize) -> usize {
let slot_count = metadata.slots.len();
metadata.slots.swap(0, slot);
metadata.slots.truncate(1);
slot_count
}
/// Duplicate the first slot until the required number of slots is reached.
fn fill_slots(metadata: &mut Metadata) {
let required = match metadata.image_type {
ImageType::Normal => metadata.metadata_slot_count as usize,
ImageType::Empty => 1,
};
for _ in metadata.slots.len()..required {
metadata.slots.extend_from_within(0..=0);
}
}
fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut inputs = cli
.input
.iter()
.map(|p| {
File::open(p)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open LP image for reading: {p:?}"))
})
.collect::<Result<Vec<_>>>()?;
let mut metadata = Metadata::from_reader(&mut inputs[0])
.with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?;
// Display and write only the selected slot.
let slot_number = get_slot_number(&metadata, cli.slot)?;
retain_slot(&mut metadata, slot_number);
display_metadata(lp_cli, &metadata);
write_info(&cli.output_info, &metadata)?;
// For empty images, there's no data to unpack.
if metadata.image_type == ImageType::Empty {
return Ok(());
}
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))?;
let slot = &metadata.slots[0];
if slot.block_devices.len() != inputs.len() {
bail!(
"Need {} input images, but have {}",
slot.block_devices.len(),
inputs.len(),
);
}
// Preopen all image output files.
let mut paths = vec![];
let mut files = vec![];
for group in &slot.groups {
let mut group_paths = vec![];
let mut group_files = vec![];
for partition in &group.partitions {
// A partition name with unsafe characters fails during parsing.
let path = format!("{}.img", partition.name);
let file = directory
.create(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
file.set_len(partition.size()?)
.with_context(|| format!("Failed to truncate file: {path:?}"))?;
group_paths.push(path);
group_files.push(file);
}
paths.push(group_paths);
files.push(group_files);
}
slot.groups
.par_iter()
.enumerate()
// Flatten grouped partitions.
.flat_map(|(g_index, g)| {
g.partitions
.par_iter()
.enumerate()
.map(move |(p_index, p)| (g_index, p_index, p))
})
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|(g_index, p_index, p)| {
split_extents(&p.extents)
.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()?;
let r_path = &cli.input[extent.device_index];
let w_path = &paths[g_index][p_index];
reader
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.out_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut metadata = read_info(&cli.input_info)?;
if metadata.slots.len() != 1 {
bail!("There must be exactly one metadata slot");
}
let slot = &mut metadata.slots[0];
let mut outputs = open_lp_outputs(&cli.output)?;
if slot.block_devices.len() != outputs.len() {
bail!(
"Need {} output images, but have {}",
slot.block_devices.len(),
outputs.len(),
);
}
if metadata.image_type == ImageType::Normal {
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
output
.set_len(block_device.size)
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
}
}
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 mut file = directory
.open(&path)
.map(|f| PSeekFile::new(f.into_std()))
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let size = file
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to seek file: {path:?}"))?;
if size % u64::from(SECTOR_SIZE) != 0 {
bail!("File size is not {SECTOR_SIZE}B aligned: {size}: {path:?}");
}
// This will be filled out properly later during reallocation.
partition.extents.push(Extent {
num_sectors: size / u64::from(SECTOR_SIZE),
extent_type: ExtentType::Linear {
start_sector: 0,
block_device_index: 0,
},
});
group_paths.push(path);
group_files.push(file);
}
paths.push(group_paths);
files.push(group_files);
}
// Now that we have all the partition sizes, actually allocate extents
// for them on the block devices.
slot.reallocate_extents()
.context("Failed to allocate extents")?;
}
// Display only the selected slot and make the rest identical.
let _ = slot;
display_metadata(lp_cli, &metadata);
fill_slots(&mut metadata);
let slot = &metadata.slots[0];
// Write the new metadata.
metadata
.to_writer(&mut outputs[0])
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
// For empty images, there's no data to pack.
if metadata.image_type == ImageType::Empty {
return Ok(());
}
slot.groups
.par_iter()
.enumerate()
// Flatten grouped partitions.
.flat_map(|(g_index, g)| {
g.partitions
.par_iter()
.enumerate()
.map(move |(p_index, p)| (g_index, p_index, p))
})
// Flatten extents in all partitions and split them to smaller chunks
// for better parallelism.
.flat_map(|(g_index, p_index, p)| {
split_extents(&p.extents)
.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()?;
let r_path = &paths[g_index][p_index];
let w_path = &cli.output[extent.device_index];
reader
.seek(SeekFrom::Start(extent.out_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
// Show a clap-style error if the number of inputs and outputs aren't equal.
if cli.input.len() != cli.output.len() {
let (arg_id, actual_len, expected_len) = if cli.input.len() < cli.output.len() {
("input", cli.input.len(), cli.output.len())
} else {
("output", cli.output.len(), cli.input.len())
};
let mut command = RepackCli::command();
command.build();
let arg = command
.get_arguments()
.find(|a| a.get_id() == arg_id)
.expect("argument not found");
let mut error =
clap::Error::new(clap::error::ErrorKind::WrongNumberOfValues).with_cmd(&command);
error.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()),
);
error.insert(
clap::error::ContextKind::ActualNumValues,
clap::error::ContextValue::Number(actual_len as isize),
);
error.insert(
clap::error::ContextKind::ExpectedNumValues,
clap::error::ContextValue::Number(expected_len as isize),
);
// We don't show the usage because only Command::_build_subcommand() can
// create an appropriate Command instance for showing the subcommand
// usage and there's no way to call that, directly or indirectly.
error.exit();
}
let (inputs, mut metadata) = open_lp_inputs(&cli.input)?;
let mut outputs = open_lp_outputs(&cli.output)?;
// Display only the selected slot and make the rest identical.
let slot_number = get_slot_number(&metadata, cli.slot)?;
retain_slot(&mut metadata, slot_number);
display_metadata(lp_cli, &metadata);
fill_slots(&mut metadata);
let slot = &metadata.slots[0];
if slot.block_devices.len() != inputs.len() {
bail!(
"Need {} images, but have {}",
slot.block_devices.len(),
inputs.len(),
);
}
// Write the new metadata.
metadata
.to_writer(&mut outputs[0])
.with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?;
// Explicitly set the file size in case there are dm-zero extents, which are
// ignored below.
if metadata.image_type == ImageType::Normal {
for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() {
output
.set_len(block_device.size)
.with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?;
}
}
slot.groups
.par_iter()
// Flatten grouped partitions.
.flat_map(|group| &group.partitions)
// 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()?;
let r_path = &cli.input[extent.device_index];
let w_path = &cli.output[extent.device_index];
reader
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {r_path:?}"))?;
writer
.seek(SeekFrom::Start(extent.lp_offset))
.with_context(|| format!("Failed to seek file: {w_path:?}"))?;
stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal)
.with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(())
}
fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> {
let (_, metadata) = open_lp_inputs(&[&cli.input])?;
// Unlike the other subcommands, we show all metadata slots here.
display_metadata(lp_cli, &metadata);
Ok(())
}
pub fn lp_main(cli: &LpCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
LpCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
LpCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
LpCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
LpCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack an LP image.
///
/// The LP image metadata is written to the info TOML file. For normal images,
/// each partition is extracted to `<partition name>.img` in the output images
/// directory. For empty images, the output images directory is unused.
///
/// If any partition names are unsafe to use in a path, the extraction process
/// will fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input LP images.
///
/// If there are multiple images, they must be specified in order. If the
/// order is unknown, run `avbroot lp info` against the `super` image and
/// look at the `block_devices` field.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
input: Vec<PathBuf>,
/// Path to output info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
output_info: PathBuf,
/// Path to output images directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
output_images: PathBuf,
/// The LP metadata slot to use.
///
/// This slot is the only slot where data extents are copied from. Any data
/// referenced exclusively by other slots (if any) will be ignored.
///
/// This option is required if not all slots are identical.
#[arg(short, long)]
slot: Option<u32>,
}
/// Pack an LP image.
///
/// For normal images, the number of metadata slots written is equal to the
/// `metadata_slot_count` value in the info TOML. Each slot has identical
/// metadata. It is not possible to write multiple slots with different metadata
/// using this tool. For empty images, only a single slot is written, regardless
/// of the value of `metadata_slot_count`, as required by the file format.
///
/// The new LP image will *only* contain images listed in the info TOML file and
/// they are added in the order listed. The input images directory is not used
/// when packing an empty image.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output LP images.
///
/// If there are multiple images, they must be specified in the same order
/// as the block device entries are listed in the info TOML.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
output: Vec<PathBuf>,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")]
input_info: PathBuf,
/// Path to input images directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")]
input_images: PathBuf,
}
/// Repack an LP image.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input LP images.
///
/// If there are multiple images, they must be specified in order. If the
/// order is unknown, run `avbroot lp info` against the `super` image and
/// look at the `block_devices` field.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
input: Vec<PathBuf>,
/// Path to output LP images.
///
/// The number of output images must equal the number of input images.
#[arg(short, long, value_name = "FILE", value_parser, required = true)]
output: Vec<PathBuf>,
/// The LP metadata slot to use.
///
/// This slot is the only slot where data extents are copied to the output
/// images. Any data referenced exclusively by other slots (if any) will be
/// ignored.
///
/// This option is required if not all slots are identical.
#[arg(short, long)]
slot: Option<u32>,
}
/// Display LP image metadata.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input LP image.
///
/// If there are multiple images, this should refer to the first one, which
/// is usually the `super` image. The other images are not needed when
/// inspecting the metadata because the metadata is only stored in the first
/// image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum LpCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect LP images.
#[derive(Debug, Parser)]
pub struct LpCli {
#[command(subcommand)]
command: LpCommand,
/// Don't print LP metadata information.
#[arg(short, long, global = true)]
quiet: bool,
}
+4 -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;
@@ -11,5 +9,7 @@ pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod key;
pub mod lp;
pub mod ota;
pub mod payload;
pub mod sparse;
+105 -43
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
borrow::Cow,
@@ -32,9 +30,9 @@ use crate::{
crypto::{self, PassphraseSource, RsaSigningKey},
format::{
avb::{self, Descriptor, Header},
ota::{self, SigningWriter, ZipEntry},
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, PayloadHeader, PayloadWriter},
payload::{self, PayloadHeader, PayloadWriter, VabcAlgo},
},
patch::{
boot::{
@@ -47,8 +45,8 @@ use crate::{
build::tools::releasetools::OtaMetadata, chromeos_update_engine::DeltaArchiveManifest,
},
stream::{
self, CountingWriter, FromReader, HashingWriter, HolePunchingWriter, PSeekFile,
ReadSeekReopen, Reopen, SectionReader, ToWriter, WriteSeekReopen,
self, CountingWriter, FromReader, HashingWriter, PSeekFile, ReadSeekReopen, Reopen,
SectionReader, ToWriter, WriteSeekReopen,
},
util,
};
@@ -203,7 +201,7 @@ fn patch_boot_images<'a, 'b: 'a>(
let boot_partitions = required_images.iter_boot().collect::<Vec<_>>();
info!(
"Patching boot images: {}",
"Candidate boot images: {}",
joined(sorted(boot_partitions.iter())),
);
@@ -595,7 +593,9 @@ fn update_vbmeta_headers(
// have no dependencies and are only being processed to ensure that the
// flags are set to a sane value.
if parent_header != &orig_parent_header {
parent_header.set_algo_for_key(key)?;
parent_header
.set_algo_for_key(key)
.with_context(|| format!("Failed to set signature algorithm: {name}"))?;
parent_header
.sign(key)
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
@@ -676,12 +676,11 @@ pub fn compress_image(
// Otherwise, compress the entire image. If VABC is enabled, we need to
// update the CoW size estimate or else the CoW block device may run out of
// space during flashing.
let need_cow = partition.estimate_cow_size.is_some();
if need_cow {
let vabc_algo = if partition.estimate_cow_size.is_some() {
info!("Needs updated CoW size estimate: {name}");
// Only CoW v2 + lz4 seems to exist in the wild currently, so that is
// all we support.
// Only CoW v2 seems to exist in the wild currently, so that is all we
// support.
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
bail!("Dynamic partition metadata is missing");
};
@@ -696,13 +695,17 @@ pub fn compress_image(
}
let compression = dpm.vabc_compression_param();
if compression != "lz4" {
let Some(vabc_algo) = VabcAlgo::new(compression) else {
bail!("Unsupported VABC compression: {compression}");
}
}
};
Some(vabc_algo)
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(&*file, &writer, name, block_size, need_cow, cancel_signal)?;
payload::compress_image(&*file, &writer, name, block_size, vabc_algo, cancel_signal)?;
partition.new_partition_info = Some(partition_info);
partition.operations = operations;
@@ -726,15 +729,13 @@ fn patch_ota_payload(
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
let header = PayloadHeader::from_reader(payload.reopen_boxed()?)
let mut header = PayloadHeader::from_reader(payload.reopen_boxed()?)
.context("Failed to load OTA payload header")?;
if !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
let header = Mutex::new(header);
let mut header_locked = header.lock().unwrap();
let all_partitions = header_locked
let all_partitions = header
.manifest
.partitions
.iter()
@@ -752,7 +753,7 @@ fn patch_ota_payload(
// Determine what images need to be patched. For simplicity, we pre-read all
// vbmeta images since they're tiny. They're discarded later if the they
// don't need to be modified.
let required_images = RequiredImages::new(&header_locked.manifest);
let required_images = RequiredImages::new(&header.manifest);
let vbmeta_images = required_images.iter_vbmeta().collect::<HashSet<_>>();
// The set of source images to be inserted into the new payload, replacing
@@ -764,7 +765,7 @@ fn patch_ota_payload(
payload,
&required_images,
external_images,
&header_locked,
&header,
cancel_signal,
)?;
@@ -806,7 +807,7 @@ fn patch_ota_payload(
&mut vbmeta_order,
clear_vbmeta_flags,
key_avb,
header_locked.manifest.block_size().into(),
header.manifest.block_size().into(),
)?;
// Unmodified vbmeta images no longer need to be kept around either.
@@ -818,7 +819,7 @@ fn patch_ota_payload(
let modified_operations = compress_image(
&name,
&mut input_file.file,
&mut header_locked,
&mut header,
// We can only perform the optimization of avoiding
// recompression if the image came from the original payload.
if name == system_target && !external_images.contains_key(&name) {
@@ -836,7 +837,7 @@ fn patch_ota_payload(
info!("Generating new OTA payload");
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
let mut payload_writer = PayloadWriter::new(writer, header.clone(), key_ota.clone())
.context("Failed to write payload header")?;
let mut orig_payload_reader = payload.reopen_boxed().context("Failed to open payload")?;
@@ -854,7 +855,7 @@ fn patch_ota_payload(
let pi = payload_writer.partition_index().unwrap();
let oi = payload_writer.operation_index().unwrap();
let orig_partition = &header_locked.manifest.partitions[pi];
let orig_partition = &header.manifest.partitions[pi];
let orig_operation = &orig_partition.operations[oi];
let data_offset = orig_operation
.data_offset
@@ -884,7 +885,7 @@ fn patch_ota_payload(
// Otherwise, copy from the original payload.
let data_offset = data_offset
.checked_add(header_locked.blob_offset)
.checked_add(header.blob_offset)
.ok_or_else(|| anyhow!("data_offset overflow in partition #{pi} operation #{oi}"))?;
orig_payload_reader
@@ -915,6 +916,7 @@ fn patch_ota_zip(
external_images: &HashMap<String, PathBuf>,
mut boot_patchers: Vec<Box<dyn BootImagePatch + Sync>>,
clear_vbmeta_flags: bool,
zip_mode: ZipMode,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
@@ -1077,7 +1079,16 @@ fn patch_ota_zip(
info!("Generating new OTA metadata");
let data_descriptor_size = if last_entry_used_zip64 { 24 } else { 16 };
let data_descriptor_size = match zip_mode {
ZipMode::Streaming => {
if last_entry_used_zip64 {
24
} else {
16
}
}
ZipMode::Seekable => 0,
};
let metadata = ota::add_metadata(
&entries,
zip_writer,
@@ -1085,6 +1096,7 @@ fn patch_ota_zip(
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
&metadata.unwrap(),
payload_metadata_size.unwrap(),
zip_mode,
)
.context("Failed to write new OTA metadata")?;
@@ -1272,7 +1284,6 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
}
let mut boot_patchers = Vec::<Box<dyn BootImagePatch + Sync>>::new();
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
if let Some(magisk) = &cli.root.magisk {
boot_patchers.push(Box::new(
@@ -1285,6 +1296,8 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
.context("Failed to create Magisk boot image patcher")?,
));
} else if let Some(prepatched) = &cli.root.prepatched {
// NOTE: This patcher must run first! Otherwise, it'll wipe out any
// ramdisk changes made by other patchers.
boot_patchers.push(Box::new(PrepatchedImagePatcher::new(
prepatched,
cli.ignore_prepatched_compat + 1,
@@ -1293,6 +1306,12 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
assert!(cli.root.rootless);
};
if cli.skip_recovery_ota_cert {
warn!("Not inserting OTA cert into recovery image; sideloading further updates may fail");
} else {
boot_patchers.push(Box::new(OtaCertPatcher::new(cert_ota.clone())));
}
if cli.dsu {
boot_patchers.push(Box::new(DsuPubKeyPatcher::new(key_avb.to_public_key())));
}
@@ -1312,10 +1331,16 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
)
.context("Failed to open temporary output file")?;
let temp_path = temp_writer.path().to_owned();
let hole_punching_writer = HolePunchingWriter::new(temp_writer);
let buffered_writer = BufWriter::new(hole_punching_writer);
let signing_writer = SigningWriter::new(buffered_writer);
let mut zip_writer = ZipWriter::new_streaming(signing_writer);
let mut zip_writer = match cli.zip_mode {
ZipMode::Streaming => {
let signing_writer = SigningWriter::new_streaming(temp_writer);
ZipWriter::new_streaming(signing_writer)
}
ZipMode::Seekable => {
let signing_writer = SigningWriter::new_seekable(temp_writer);
ZipWriter::new(signing_writer)
}
};
let (metadata, payload_metadata_size) = patch_ota_zip(
&raw_reader,
@@ -1324,6 +1349,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
&external_images,
boot_patchers,
cli.clear_vbmeta_flags,
cli.zip_mode,
&key_avb,
&key_ota,
&cert_ota,
@@ -1334,13 +1360,9 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
let signing_writer = zip_writer
.finish()
.context("Failed to finalize output zip")?;
let buffered_writer = signing_writer
.finish(&key_ota, &cert_ota)
let mut temp_writer = signing_writer
.finish(&key_ota, &cert_ota, cancel_signal)
.context("Failed to sign output zip")?;
let hole_punching_writer = buffered_writer
.into_inner()
.context("Failed to flush output zip")?;
let mut temp_writer = hole_punching_writer.into_inner();
temp_writer.flush().context("Failed to flush output zip")?;
// We do a lot of low-level hackery. Reopen and verify offsets.
@@ -1668,9 +1690,11 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
verify_partition_hashes(&temp_dir, &header, &unique_images, cancel_signal)?;
info!("Checking ramdisk's otacerts.zip");
if cli.skip_recovery_ota_cert {
warn!("Not verifying recovery ramdisk's otacerts.zip");
} else {
info!("Checking recovery ramdisk's otacerts.zip");
{
let required_images = RequiredImages::new(&header.manifest);
let boot_images =
boot::load_boot_images(&required_images.iter_boot().collect::<Vec<_>>(), |name| {
@@ -1906,6 +1930,17 @@ pub struct PatchCli {
)]
pub ignore_prepatched_compat: u8,
/// Skip adding OTA certificate to recovery image.
///
/// DO NOT USE THIS unless you've manually added the certificate to the
/// recovery image already. Otherwise, sideloading further updates will not
/// be possible.
///
/// When this option is used with --rootless, the boot images in the OTA
/// will not be modified.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_recovery_ota_cert: bool,
/// Add AVB public key to trusted keys for DSU.
#[arg(long, help_heading = HEADING_OTHER)]
pub dsu: bool,
@@ -1914,6 +1949,24 @@ pub struct PatchCli {
#[arg(long, help_heading = HEADING_OTHER)]
pub clear_vbmeta_flags: bool,
/// Zip creation mode for the output OTA zip.
///
/// The streaming mode produces zip files that contain data descriptors.
/// The zip file is hashed as it is being written. This mode is the default
/// and works with the vast majority of devices.
///
/// The seekable mode produces zip files that do not use data descriptors.
/// The zip file is reread and hashed after it has been fully written. The
/// output file is more likely to be compatible with devices that have
/// broken zip file parsers.
#[arg(
long,
value_name = "MODE",
default_value_t = ZipMode::Streaming,
help_heading = HEADING_OTHER
)]
pub zip_mode: ZipMode,
/// (Deprecated: no longer needed)
#[arg(
long,
@@ -1974,6 +2027,15 @@ pub struct VerifyCli {
/// valid, not that they are trusted.
#[arg(long, value_name = "FILE", value_parser)]
pub public_key_avb: Option<PathBuf>,
/// Skip verifying OTA certificate in recovery image.
///
/// This should not be used unless the OTA uses a special boot image format
/// that avbroot cannot parse. This certificate check ensures that the OTA
/// is configured properly to allow sideloading further OTAs signed by the
/// same key.
#[arg(long, help_heading = HEADING_OTHER)]
pub skip_recovery_ota_cert: bool,
}
#[allow(clippy::large_enum_variant)]
+8 -10
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::{
collections::HashMap,
@@ -24,13 +22,13 @@ use crate::{
stream::{self, FromReader, PSeekFile},
};
fn open_reader(path: &Path) -> Result<(BufReader<File>, PayloadHeader)> {
fn open_reader(path: &Path, allow_delta: bool) -> Result<(BufReader<File>, PayloadHeader)> {
let mut reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open payload for reading: {path:?}"))?;
let header = PayloadHeader::from_reader(&mut reader)
.with_context(|| format!("Failed to read payload header: {path:?}"))?;
if !header.is_full_ota() {
if !allow_delta && !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
@@ -106,7 +104,7 @@ fn unpack_subcommand(
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, header) = open_reader(&cli.input)?;
let (mut reader, header) = open_reader(&cli.input, false)?;
let payload_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
@@ -253,7 +251,7 @@ fn repack_subcommand(
) -> Result<()> {
let signing_key = load_key(&cli.key)?;
let (mut reader, header) = open_reader(&cli.input)?;
let (mut reader, header) = open_reader(&cli.input, true)?;
info!("Generating new OTA payload");
@@ -309,7 +307,7 @@ fn repack_subcommand(
}
fn info_subcommand(payload_cli: &PayloadCli, cli: &InfoCli) -> Result<()> {
let (_, header) = open_reader(&cli.input)?;
let (_, header) = open_reader(&cli.input, true)?;
display_header(payload_cli, &header);
@@ -461,7 +459,7 @@ enum PayloadCommand {
Info(InfoCli),
}
/// Inspect OTA payloads.
/// Pack, unpack, and inspect OTA payloads.
#[derive(Debug, Parser)]
pub struct PayloadCli {
#[command(subcommand)]
+631
View File
@@ -0,0 +1,631 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
fmt,
fs::{File, OpenOptions},
io::{Read, Seek, SeekFrom, Write},
ops::Range,
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use clap::{Parser, Subcommand};
use crc32fast::Hasher;
use zerocopy::{little_endian, IntoBytes};
use crate::{
format::{
padding,
sparse::{
self, Chunk, ChunkBounds, ChunkData, ChunkList, CrcMode, Header, SparseReader,
SparseWriter,
},
},
stream,
};
struct CompactView<'a, T>(&'a [T]);
impl<'a, T: fmt::Debug> fmt::Debug for CompactView<'a, T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut list = f.debug_list();
for item in self.0 {
// No alternate mode for no inner newlines.
list.entry(&format_args!("{item:?}"));
}
list.finish()
}
}
#[derive(Clone)]
struct Metadata {
header: Header,
chunks: Vec<Chunk>,
}
impl fmt::Debug for Metadata {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Metadata")
.field("header", &self.header)
.field("chunks", &CompactView(&self.chunks))
.finish()
}
}
fn open_reader(path: &Path) -> Result<File> {
File::open(path).with_context(|| format!("Failed to open for reading: {path:?}"))
}
fn open_writer(path: &Path, truncate: bool) -> Result<File> {
OpenOptions::new()
.write(true)
.create(true)
.truncate(truncate)
.open(path)
.with_context(|| format!("Failed to open for writing: {path:?}"))
}
fn display_metadata(cli: &SparseCli, metadata: &Metadata) {
if !cli.quiet {
println!("{metadata:#?}");
}
}
/// Splits large data chunks to ensure that none exceed 64 MiB. This is not
/// necessary in most cases, but is kept to match the behavior of AOSP's
/// libsparse.
fn split_chunks(chunks: &[Chunk], block_size: u32) -> Vec<Chunk> {
const MAX_BYTES: u32 = 64 * 1024 * 1024;
let max_blocks_per_chunk = MAX_BYTES / block_size;
let mut result = vec![];
for mut chunk in chunks.iter().copied() {
if chunk.data == ChunkData::Data {
while chunk.bounds.len() > max_blocks_per_chunk {
result.push(Chunk {
bounds: ChunkBounds {
start: chunk.bounds.start,
end: chunk.bounds.start + max_blocks_per_chunk,
},
data: chunk.data,
});
chunk.bounds.start += max_blocks_per_chunk;
}
}
result.push(chunk);
}
result
}
/// [Linux only] Find allocated regions of the file. This avoids needing to read
/// unused portions of the file if it is a native sparse file.
#[cfg(any(target_os = "linux", target_os = "android"))]
fn find_allocated_regions(
path: &Path,
reader: &mut File,
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<u64>>> {
use rustix::{fs::SeekFrom, io::Errno};
let mut result = vec![];
let mut start;
let mut end = 0;
loop {
stream::check_cancel(cancel_signal)?;
start = match rustix::fs::seek(&*reader, SeekFrom::Data(end as i64)) {
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))
.with_context(|| format!("Failed to seek to hole: {path:?}"))?;
result.push(start..end);
}
Ok(result)
}
/// Compute chunk boundaries for the list of potentially overlapping file byte
/// regions. If `exact_bounds` is true, then the regions must be block-aligned.
/// Otherwise, the lower boundaries are aligned down and the upper boundaries
/// are aligned up.
fn get_chunks_for_regions(
block_size: u32,
file_size: u64,
file_regions: &[Range<u64>],
exact_bounds: bool,
) -> Result<(u32, Vec<ChunkBounds>)> {
let block_size_64 = u64::from(block_size);
let file_blocks: u32 = (file_size / u64::from(block_size))
.try_into()
.map_err(|_| anyhow!("File size {file_size} too large for block size {block_size}"))?;
let mut chunk_list = ChunkList::new();
chunk_list.set_len(file_blocks);
for region in file_regions {
let mut start_byte = region.start;
let mut end_byte = region.end;
if exact_bounds {
if start_byte % block_size_64 != 0 || end_byte % block_size_64 != 0 {
bail!("File region bounds are not block-aligned: {region:?}");
}
} else {
start_byte = start_byte / block_size_64 * block_size_64;
end_byte = padding::round(end_byte, block_size_64).unwrap();
}
let start_block: u32 = (start_byte / block_size_64).try_into().map_err(|_| {
anyhow!("Region start offset {start_byte} too large for block size {block_size}")
})?;
let end_block: u32 = (end_byte / block_size_64).try_into().map_err(|_| {
anyhow!("Region end offset {end_byte} too large for block size {block_size}")
})?;
chunk_list.insert_data(ChunkBounds {
start: start_block,
end: end_block,
});
}
let chunks = chunk_list.iter_allocated().map(|c| c.bounds).collect();
Ok((file_blocks, chunks))
}
/// Compute the sparse [`Chunk`]s needed to cover the specified regions.
fn compute_chunks(
path: &Path,
reader: &mut File,
block_size: u32,
file_blocks: u32,
block_regions: &[ChunkBounds],
cancel_signal: &AtomicBool,
) -> Result<(ChunkList, u32)> {
let mut chunk_list = ChunkList::new();
let mut hasher = Some(Hasher::new());
let mut buf = vec![0u8; block_size as usize];
let mut block = 0;
chunk_list.set_len(file_blocks);
for bounds in block_regions {
if bounds.start != block {
// Not contiguous so we cannot compute the checksum.
hasher = None;
}
let offset = u64::from(bounds.start) * u64::from(block_size);
reader
.seek(SeekFrom::Start(offset))
.with_context(|| format!("Failed to seek file: {path:?}"))?;
for block in *bounds {
stream::check_cancel(cancel_signal)?;
reader
.read_exact(&mut buf)
.with_context(|| format!("Failed to read full block: {path:?}"))?;
if let Some(h) = &mut hasher {
h.update(&buf);
}
let new_bounds = ChunkBounds {
start: block,
end: block + 1,
};
if buf.chunks_exact(4).all(|c| c == &buf[..4]) {
let fill_value = u32::from_le_bytes(buf[..4].try_into().unwrap());
chunk_list.insert_fill(new_bounds, fill_value);
} else {
chunk_list.insert_data(new_bounds);
}
}
block = bounds.end;
}
if block != file_blocks {
hasher = None;
}
let crc32 = hasher.map(|h| h.finalize()).unwrap_or_default();
Ok((chunk_list, crc32))
}
fn unpack_subcommand(
sparse_cli: &SparseCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
let mut writer = open_writer(&cli.output, !cli.preserve)?;
if cli.preserve {
let expected_size =
u64::from(metadata.header.num_blocks) * u64::from(metadata.header.block_size);
let file_size = writer
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.output))?;
if file_size < expected_size {
writer
.set_len(expected_size)
.with_context(|| format!("Failed to set file size: {:?}", cli.output))?;
}
writer
.seek(SeekFrom::Start(0))
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
match chunk.data {
ChunkData::Fill(value) => {
let fill_value = little_endian::U32::from(value);
let buf = vec![fill_value; metadata.header.block_size as usize / 4];
for _ in chunk.bounds {
stream::check_cancel(cancel_signal)?;
writer
.write_all(buf.as_bytes())
.with_context(|| format!("Failed to write data: {:?}", cli.output))?;
}
}
ChunkData::Data => {
// This cannot overflow.
let to_copy = chunk.bounds.len() * metadata.header.block_size;
stream::copy_n(
&mut sparse_reader,
&mut writer,
to_copy.into(),
cancel_signal,
)
.with_context(|| {
format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output)
})?;
}
ChunkData::Hole => {
// This cannot overflow.
let to_skip = chunk.bounds.len() * metadata.header.block_size;
writer
.seek(SeekFrom::Current(to_skip.into()))
.with_context(|| format!("Failed to seek file: {:?}", cli.output))?;
}
ChunkData::Crc32(_) => {}
}
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
sparse_reader
.finish()
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
Ok(())
}
fn pack_subcommand(
sparse_cli: &SparseCli,
cli: &PackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
if cli.block_size == 0 || cli.block_size % 4 != 0 {
bail!(
"Block size must be a non-zero multiple of 4: {}",
cli.block_size,
);
}
let mut reader = open_reader(&cli.input)?;
let file_size = reader
.seek(SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {:?}", cli.input))?;
if file_size % u64::from(cli.block_size) != 0 {
bail!(
"File size {file_size} is not a multiple of block size {}",
cli.block_size,
);
}
// Compute the byte regions to pack into the sparse file.
let (file_regions, exact_bounds) = if !cli.region.is_empty() {
let regions = cli
.region
.chunks_exact(2)
.map(|c| c[0]..c[1])
.collect::<Vec<_>>();
(regions, false)
} else {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
let regions = find_allocated_regions(&cli.input, &mut 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)
}
};
// Get the file regions as non-overlapping and sorted block regions.
let (file_blocks, block_regions) =
get_chunks_for_regions(cli.block_size, file_size, &file_regions, exact_bounds)?;
// Compute the checksum (if possible) and the list of actual chunks.
let (chunk_list, crc32) = compute_chunks(
&cli.input,
&mut reader,
cli.block_size,
file_blocks,
&block_regions,
cancel_signal,
)?;
let chunks = split_chunks(&chunk_list.to_chunks(), cli.block_size);
let metadata = Metadata {
header: Header {
major_version: sparse::MAJOR_VERSION,
minor_version: sparse::MINOR_VERSION,
block_size: cli.block_size,
num_blocks: chunk_list.len(),
// This can't overflow because the number of chunks is always
// smaller than the number of blocks (because we don't add CRC32
// chunks).
num_chunks: chunks.len() as u32,
// This will be zero if the regions don't span the entire file.
crc32,
},
chunks,
};
display_metadata(sparse_cli, &metadata);
let writer = open_writer(&cli.output, true)?;
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
for chunk in metadata.chunks {
sparse_writer
.start_chunk(chunk)
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
if chunk.data == ChunkData::Data {
let offset = u64::from(chunk.bounds.start) * u64::from(cli.block_size);
reader
.seek(SeekFrom::Start(offset))
.with_context(|| format!("Failed to seek file: {:?}", cli.input))?;
let to_copy = u64::from(chunk.bounds.len()) * u64::from(cli.block_size);
stream::copy_n(&mut reader, &mut sparse_writer, to_copy, cancel_signal).with_context(
|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output),
)?;
}
}
sparse_writer
.finish()
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
Ok(())
}
fn repack_subcommand(
sparse_cli: &SparseCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Validate)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
let writer = open_writer(&cli.output, true)?;
let mut sparse_writer = SparseWriter::new(writer, metadata.header)
.with_context(|| format!("Failed to initialize sparse file: {:?}", cli.output))?;
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
sparse_writer
.start_chunk(chunk)
.with_context(|| format!("Failed to start chunk: {:?}", cli.output))?;
if chunk.data == ChunkData::Data {
// This cannot overflow.
let to_copy = chunk.bounds.len() * metadata.header.block_size;
stream::copy_n(
&mut sparse_reader,
&mut sparse_writer,
to_copy.into(),
cancel_signal,
)
.with_context(|| format!("Failed to copy data: {:?} -> {:?}", cli.input, cli.output))?;
}
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
sparse_reader
.finish()
.with_context(|| format!("Failed to finalize reader: {:?}", cli.input))?;
sparse_writer
.finish()
.with_context(|| format!("Failed to finalize writer: {:?}", cli.output))?;
Ok(())
}
fn info_subcommand(sparse_cli: &SparseCli, cli: &InfoCli) -> Result<()> {
let reader = open_reader(&cli.input)?;
let mut sparse_reader = SparseReader::new_seekable(reader, CrcMode::Ignore)
.with_context(|| format!("Failed to read sparse file: {:?}", cli.input))?;
let mut metadata = Metadata {
header: sparse_reader.header(),
chunks: vec![],
};
while let Some(chunk) = sparse_reader
.next_chunk()
.with_context(|| format!("Failed to read chunk: {:?}", cli.input))?
{
metadata.chunks.push(chunk);
}
display_metadata(sparse_cli, &metadata);
Ok(())
}
pub fn sparse_main(cli: &SparseCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
SparseCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
SparseCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
SparseCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
SparseCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack a sparse image.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output raw image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Preserve existing data in the output file.
///
/// This is useful when unpacking multiple sparse files into a single output
/// file because they contain disjoint blocks of data.
#[arg(long)]
preserve: bool,
}
/// Pack a sparse image.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output sparse image.
///
/// If `--region` is not used and the input file is not a (native) sparse
/// file on Linux, then the output sparse image is written with a CRC32
/// checksum in the header.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input raw image.
///
/// On Linux, if this is a (native) sparse file, then the unallocated
/// sections of the file will be skipped and will be stored in the output
/// file as hole chunks.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Block size.
#[arg(short, long, value_name = "BYTES", default_value_t = 4096)]
block_size: u32,
/// Pack certain byte regions from the file.
///
/// The start offset will be aligned down to the block size and the end
/// offset will be aligned up. This option can be specified any number of
/// times and in any order. Overlapping regions are allowed.
///
/// Unused regions will be stored in the sparse file as hole chunks.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
region: Vec<u64>,
}
/// Repack a sparse image.
///
/// This command is equivalent to running `unpack` and `pack`, except without
/// storing the unpacked data to disk.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
/// Display sparse image metadata.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input sparse image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
}
#[derive(Debug, Subcommand)]
enum SparseCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect sparse images.
#[derive(Debug, Parser)]
pub struct SparseCli {
#[command(subcommand)]
command: SparseCommand,
/// Don't print sparse image metadata.
#[arg(short, long, global = true)]
quiet: bool,
}
+30 -7
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::{self, VarError},
@@ -21,6 +19,7 @@ use cms::{
SignedData, SignerIdentifier, SignerInfo, SignerInfos,
},
};
use passterm::PromptError;
use pkcs8::{
pkcs5::{pbes2, scrypt},
DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, EncryptedPrivateKeyInfo,
@@ -59,6 +58,10 @@ pub enum Error {
CommandExecutionFailed(String, ExitStatus),
#[error("Signature from signing helper does not match public key: {0:?}")]
SigningHelperBadSignature(PathBuf),
#[error("Passphrase prompt requires an interactive terminal")]
NotInteractive(#[source] io::Error),
#[error("Failed to prompt for passphrase")]
PassphrasePrompt(#[source] PromptError),
#[error("Passphrases do not match")]
ConfirmPassphrase,
#[error("Failed to read environment variable: {0:?}")]
@@ -136,13 +139,33 @@ impl PassphraseSource {
}
}
fn prompt(prompt: &str) -> Result<String> {
match passterm::prompt_password_tty(Some(prompt)) {
Ok(p) => Ok(p),
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));
}
}
return Err(Error::PassphrasePrompt(PromptError::IOError(io_e)));
}
Err(Error::PassphrasePrompt(e))
}
}
}
pub fn acquire(&self, confirm: bool) -> Result<String> {
let passphrase = match self {
Self::Prompt(p) => {
let first = rpassword::prompt_password(p)?;
let first = Self::prompt(p)?;
if confirm {
let second = rpassword::prompt_password("Confirm: ")?;
let second = Self::prompt("Confirm: ")?;
if first != second {
return Err(Error::ConfirmPassphrase);
@@ -153,7 +176,7 @@ impl PassphraseSource {
}
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'])
.trim_end_matches(['\r', '\n'])
.to_owned(),
};
+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::{fmt, marker::PhantomData};
+3 -5
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,
@@ -103,7 +101,7 @@ pub enum Error {
UnsupportedAlgorithm(AlgorithmType),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("Incorrect key size ({}) for algorithm {1:?}", .0 * 8)]
#[error("Incorrect key size ({bytes}) for algorithm {1:?}", bytes = .0 * 8)]
IncorrectKeySize(usize, AlgorithmType),
#[error("RSA key size ({}) is not compatible with any AVB signing algorithm", .0 * 8)]
UnsupportedKey(usize),
+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::{
fmt,
+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::{self, Read, Seek, Write};
+3 -5
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::{
collections::{HashMap, HashSet},
@@ -52,7 +50,7 @@ pub enum Error {
UnknownMagic([u8; 6]),
#[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),
+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::{
collections::HashSet,
+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::{
fmt,
File diff suppressed because it is too large Load Diff
+4 -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 avb;
pub mod bootimage;
@@ -9,7 +7,9 @@ pub mod compression;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod lp;
pub mod ota;
pub mod padding;
pub mod payload;
pub mod sparse;
pub mod verityrs;
+290 -57
View File
@@ -1,15 +1,15 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::BTreeMap,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
iter,
sync::atomic::AtomicBool,
};
use clap::ValueEnum;
use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
use memchr::memmem;
@@ -300,6 +300,7 @@ fn compute_property_files(
pf_name: &str,
entries: &[ZipEntry],
max_length: Option<usize>,
want_pb: bool,
) -> Result<String> {
let compute = |path: &'static str| -> Result<String> {
let entry = entries
@@ -334,10 +335,14 @@ fn compute_property_files(
if max_length.is_none() {
tokens.push(format!("metadata:{}", " ".repeat(15)));
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
if want_pb {
tokens.push(format!("metadata.pb:{}", " ".repeat(15)));
}
} else {
tokens.push(compute(PATH_METADATA)?);
tokens.push(compute(PATH_METADATA_PB)?);
if want_pb {
tokens.push(compute(PATH_METADATA_PB)?);
}
}
let mut joined = tokens.join(",");
@@ -374,6 +379,18 @@ fn add_payload_metadata_entry(
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum ZipMode {
Streaming,
Seekable,
}
impl fmt::Display for ZipMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.to_possible_value().ok_or(fmt::Error)?.get_name())
}
}
/// Add metadata files to the output OTA zip. `zip_entries` is the list of
/// [`ZipEntry`] already written to `zip_writer`. `next_offset` is the current
/// file offset (where the next zip entry's local header begins).
@@ -389,6 +406,7 @@ pub fn add_metadata(
next_offset: u64,
metadata: &OtaMetadata,
payload_metadata_size: u64,
zip_mode: ZipMode,
) -> Result<OtaMetadata> {
let mut metadata = metadata.clone();
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
@@ -402,14 +420,18 @@ pub fn add_metadata(
for pf in [PF_NAME, PF_STREAMING_NAME] {
metadata.property_files.insert(
pf.to_owned(),
compute_property_files(pf, &zip_entries, None)?,
compute_property_files(pf, &zip_entries, None, true)?,
);
}
// Add the placeholders to a temporary zip to compute final property files.
let (temp_legacy_offset, temp_modern_offset) = {
let (legacy_raw, modern_raw) = serialize_metadata(&metadata)?;
let mut writer = ZipWriter::new_streaming(Cursor::new(Vec::new()));
let raw_writer = Cursor::new(Vec::new());
let mut writer = match zip_mode {
ZipMode::Streaming => ZipWriter::new_streaming(raw_writer),
ZipMode::Seekable => ZipWriter::new(raw_writer),
};
writer.start_file_with_extra_data(PATH_METADATA, options)?;
let legacy_offset = writer.end_extra_data()?;
@@ -435,7 +457,7 @@ pub fn add_metadata(
// Compute the final property files using the offsets of the fake entries.
for (key, value) in &mut metadata.property_files {
*value = compute_property_files(key, &zip_entries, Some(value.len()))?;
*value = compute_property_files(key, &zip_entries, Some(value.len()), true)?;
}
// Add the final metadata files to the real zip.
@@ -477,8 +499,11 @@ pub fn verify_metadata(
add_payload_metadata_entry(&mut zip_entries, payload_metadata_size)?;
let metadata_pb = zip_entries.iter().find(|e| e.name == PATH_METADATA_PB);
for (key, value) in &metadata.property_files {
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
let new_value =
compute_property_files(key, &zip_entries, Some(value.len()), metadata_pb.is_some())?;
if *value != new_value {
return Err(Error::MismatchedPropertyFiles {
expected: value.clone(),
@@ -610,11 +635,20 @@ pub fn parse_zip_ota_info(
) -> Result<(OtaMetadata, Certificate, PayloadHeader, String)> {
let mut zip = ZipArchive::new(reader)?;
let metadata = {
let mut entry = zip.by_name(PATH_METADATA_PB)?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
OtaMetadata::decode(buf.as_slice())?
let metadata = match zip.by_name(PATH_METADATA_PB) {
Ok(mut entry) => {
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
parse_protobuf_metadata(&buf)?
}
e @ Err(ZipError::FileNotFound) => {
drop(e);
let mut entry = zip.by_name(PATH_METADATA)?;
let mut buf = String::new();
entry.read_to_string(&mut buf)?;
parse_legacy_metadata(&buf)?
}
Err(e) => return Err(e.into()),
};
let certificate = {
@@ -637,17 +671,83 @@ pub fn parse_zip_ota_info(
Ok((metadata, certificate, header, properties))
}
/// Ensure that we're using a non-zip64 EOCD and there's no archive comment.
fn validate_eocd(eocd: &[u8]) -> io::Result<()> {
if &eocd[..4] != b"PK\x05\x06" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"EOCD magic not found",
));
} else if &eocd[20..22] != b"\0\0" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Archive comment is not 0 bytes",
));
}
Ok(())
}
/// Compute the digital signature for the specified digest, formatted as a zip
/// file archive comment. The returned buffer includes both the 2-byte comment
/// size field and the comment itself. It should be written to the end of the
/// zip file after truncating the original 2-byte comment size field.
fn compute_signature_comment(
key: &RsaSigningKey,
cert: &Certificate,
digest: ring::digest::Digest,
) -> Result<Vec<u8>> {
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
let cms_signature_der = cms_signature.to_der()?;
// Includes placeholder for the EOCD comment size field.
let mut buf = vec![0; 2];
// NULL-terminated readable message and actual signature.
buf.extend(COMMENT_MESSAGE);
buf.extend(&cms_signature_der);
// 6-byte OTA footer.
let comment_size = buf.len() - 2 + 6;
// Absolute value of the offset of the signature from the end of the archive
// comment.
buf.extend((cms_signature_der.len() as u16 + 6).to_le_bytes());
// Magic value.
buf.extend(b"\xff\xff");
// Archive comment size (for use by the OTA signature verifier).
buf.extend(((comment_size) as u16).to_le_bytes());
if let Some(o) = memmem::find(&buf[2..], ZIP_EOCD_MAGIC) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Archive comment contains EOCD magic at offset {o}"),
)
.into());
}
// Archive comment size (for the EOCD comment size field).
buf[..2].copy_from_slice(&((comment_size) as u16).to_le_bytes());
Ok(buf)
}
/// A writer that produces a signapk-style signed zip file with a whole-file
/// signature stored in the zip archive comment. The data will be left in an
/// unusable state if [`Self::finish()`] is not called.
pub struct SigningWriter<W: Write> {
///
/// This writer works with streaming zip files created with data descriptors.
/// The data is hashed as it is being written.
pub struct StreamingSigningWriter<W> {
inner: HashingWriter<W>,
// Android only supports non-zip64 EOCD.
queue: [u8; 22],
used: usize,
}
impl<W: Write> SigningWriter<W> {
impl<W: Write> StreamingSigningWriter<W> {
pub fn new(inner: W) -> Self {
Self {
inner: HashingWriter::new(inner, Context::new(&ring::digest::SHA256)),
@@ -661,59 +761,24 @@ impl<W: Write> SigningWriter<W> {
return Err(
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
);
} else if &self.queue[..4] != b"PK\x05\x06" {
return Err(io::Error::new(io::ErrorKind::InvalidData, "EOCD magic not found").into());
} else if &self.queue[20..22] != b"\0\0" {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Archive comment is not 0 bytes",
)
.into());
}
validate_eocd(&self.queue)?;
// Chop off the archive comment size field and write the remaining data.
self.inner.write_all(&self.queue[..20])?;
let (mut raw_writer, context) = self.inner.finish();
let digest = context.finish();
let cms_signature = crypto::cms_sign_external(key, cert, digest.as_ref())?;
let cms_signature_der = cms_signature.to_der()?;
let mut comment = COMMENT_MESSAGE.to_vec();
comment.extend(&cms_signature_der);
let comment_size = comment.len() + 6;
// Absolute value of the offset of the signature from the end of the
// archive comment.
comment.extend((cms_signature_der.len() as u16 + 6).to_le_bytes());
// Magic value.
comment.extend(b"\xff\xff");
// EOCD archive comment size.
comment.extend(((comment_size) as u16).to_le_bytes());
if let Some(o) = memmem::find(&comment, ZIP_EOCD_MAGIC) {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Archive comment contains EOCD magic at offset {o}"),
)
.into());
}
// Write EOCD comment size field, which was removed before.
raw_writer.write_all(&((comment_size) as u16).to_le_bytes())?;
// Finally, write the comment.
raw_writer.write_all(&comment)?;
let size_and_comment = compute_signature_comment(key, cert, digest)?;
raw_writer.write_all(&size_and_comment)?;
Ok(raw_writer)
}
}
impl<W: Write> Write for SigningWriter<W> {
impl<W: Write> Write for StreamingSigningWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let (front, back) = buf.split_at(buf.len().saturating_sub(self.queue.len()));
if front.is_empty() {
@@ -745,3 +810,171 @@ impl<W: Write> Write for SigningWriter<W> {
self.inner.flush()
}
}
/// A writer that produces a signapk-style signed zip file with a whole-file
/// signature stored in the zip archive comment. The data will be left in an
/// unusable state if [`Self::finish()`] is not called.
///
/// This writer works with zip files written without data descriptors. The data
/// is hashed during [`Self::finish()`].
pub struct SeekableSigningWriter<W> {
inner: W,
}
impl<W: Read + Write + Seek> SeekableSigningWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
pub fn finish(
mut self,
key: &RsaSigningKey,
cert: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<W> {
let file_size = self.seek(SeekFrom::End(0))?;
// Android only supports non-zip64 EOCD.
if file_size < 22 {
return Err(
io::Error::new(io::ErrorKind::InvalidData, "Too small to contain EOCD").into(),
);
}
self.seek_relative(-22)?;
let mut eocd = [0u8; 22];
self.read_exact(&mut eocd)?;
validate_eocd(&eocd)?;
// Compute the digest of everything up until the comment size field.
let mut hashing_writer = HashingWriter::new(
io::sink(),
ring::digest::Context::new(&ring::digest::SHA256),
);
self.rewind()?;
stream::copy_n(&mut self, &mut hashing_writer, file_size - 2, cancel_signal)?;
let digest = hashing_writer.finish().1.finish();
let size_and_comment = compute_signature_comment(key, cert, digest)?;
self.inner.write_all(&size_and_comment)?;
Ok(self.inner)
}
}
impl<W: Read> Read for SeekableSigningWriter<W> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf)
}
}
impl<W: Write> Write for SeekableSigningWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.inner.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<W: Seek> Seek for SeekableSigningWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
}
#[allow(clippy::large_enum_variant)]
enum SigningWriterInner<W> {
Streaming {
inner: StreamingSigningWriter<W>,
},
Seekable {
inner: SeekableSigningWriter<W>,
/// We need to store this because [`SigningWriter::finish`] needs to be
/// in `impl<W: Write>`, where [`SeekableSigningWriter::finish`] is not
/// available.
finish: fn(
SeekableSigningWriter<W>,
key: &RsaSigningKey,
cert: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<W>,
},
}
/// A writer that produces a signapk-style signed zip file with a whole-file
/// signature stored in the zip archive comment. The data will be left in an
/// unusable state if [`Self::finish()`] is not called.
///
/// This is a partially type-erased wrapper around [`StreamingSigningWriter`]
/// and [`SeekableSigningWriter`].
pub struct SigningWriter<W>(SigningWriterInner<W>);
impl<W: Write> SigningWriter<W> {
pub fn new_streaming(inner: W) -> Self {
Self(SigningWriterInner::Streaming {
inner: StreamingSigningWriter::new(inner),
})
}
pub fn finish(
self,
key: &RsaSigningKey,
cert: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<W> {
match self.0 {
SigningWriterInner::Streaming { inner } => inner.finish(key, cert),
SigningWriterInner::Seekable { inner, finish } => {
finish(inner, key, cert, cancel_signal)
}
}
}
}
impl<W: Read + Write + Seek> SigningWriter<W> {
pub fn new_seekable(inner: W) -> Self {
Self(SigningWriterInner::Seekable {
inner: SeekableSigningWriter::new(inner),
finish: SeekableSigningWriter::finish,
})
}
}
impl<W: Read> Read for SigningWriter<W> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match &mut self.0 {
SigningWriterInner::Streaming { .. } => panic!("Called with streaming writer"),
SigningWriterInner::Seekable { inner, .. } => inner.read(buf),
}
}
}
impl<W: Write> Write for SigningWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match &mut self.0 {
SigningWriterInner::Streaming { inner } => inner.write(buf),
SigningWriterInner::Seekable { inner, .. } => inner.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match &mut self.0 {
SigningWriterInner::Streaming { inner } => inner.flush(),
SigningWriterInner::Seekable { inner, .. } => inner.flush(),
}
}
}
impl<W: Seek> Seek for SigningWriter<W> {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match &mut self.0 {
SigningWriterInner::Streaming { .. } => panic!("Called with streaming writer"),
SigningWriterInner::Seekable { inner, .. } => inner.seek(pos),
}
}
}
+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::{self, Read, Seek, Write};
+75 -31
View File
@@ -1,10 +1,9 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
collections::{HashMap, HashSet},
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
@@ -207,10 +206,10 @@ fn verify_digest(digest: &[u8], signatures: &Signatures, cert: &Certificate) ->
let Some(data) = &signature.data else {
continue;
};
let Some(size) = signature.unpadded_signature_size else {
continue;
};
let without_padding = &data[..size as usize];
let size = signature
.unpadded_signature_size
.map_or(data.len(), |s| s as usize);
let without_padding = &data[..size];
match public_key.verify_sig(SignatureAlgorithm::Sha256WithRsa, digest, without_padding) {
Ok(_) => return Ok(()),
@@ -887,19 +886,50 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
Ok((data, digest_compressed))
}
fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum VabcAlgo {
Lz4,
Gzip,
}
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let compressed = lz4_flex::block::compress(&raw_data[..n]);
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
impl VabcAlgo {
pub fn new(name: &str) -> Option<Self> {
match name {
"lz4" => Some(Self::Lz4),
"gz" => Some(Self::Gzip),
_ => None,
}
}
total
fn compressed_size(&self, mut raw_data: &[u8], block_size: u32) -> u64 {
let mut total = 0;
while !raw_data.is_empty() {
let n = raw_data.len().min(block_size as usize);
let compressed = match self {
Self::Lz4 => lz4_flex::block::compress(&raw_data[..n]),
// We use the miniz_oxide backend for flate2, but flate2 doesn't
// expose a nice function for compressing to a vec, so just use
// miniz_oxide directly.
Self::Gzip => miniz_oxide::deflate::compress_to_vec_zlib(&raw_data[..n], 9),
};
total += compressed.len().min(n) as u64;
raw_data = &raw_data[n..];
}
total
}
}
impl fmt::Display for VabcAlgo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Lz4 => f.write_str("lz4"),
Self::Gzip => f.write_str("gz"),
}
}
}
/// Compress the image and return the corresponding information to insert into
@@ -910,18 +940,18 @@ fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
/// update [`InstallOperation::data_offset`] in each operation manually because
/// the initial values are relative to 0.
///
/// If `need_cow_estimate` is true, the VABC CoW v2 + lz4 size estimate will be
/// computed. The caller must update [`PartitionUpdate::estimate_cow_size`] with
/// this value or else update_engine may fail to flash the partition due to
/// running out of space on the CoW block device. CoW v2 + other algorithms and
/// also CoW v3 are currently unsupported because there currently are no known
/// OTAs that use those configurations.
/// If `vabc_algo` is set, the VABC CoW v2 size estimate will be computed. The
/// caller must update [`PartitionUpdate::estimate_cow_size`] with this value or
/// else update_engine may fail to flash the partition due to running out of
/// space on the CoW block device. CoW v2 + other algorithms and also CoW v3 are
/// currently unsupported because there currently are no known OTAs that use
/// those configurations.
pub fn compress_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
partition_name: &str,
block_size: u32,
need_cow_estimate: bool,
vabc_algo: Option<VabcAlgo>,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
@@ -980,8 +1010,8 @@ pub fn compress_image(
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let cow_size = if need_cow_estimate {
compress_cow_size(&raw_data, block_size)
let cow_size = if let Some(algo) = vabc_algo {
algo.compressed_size(&raw_data, block_size)
} else {
0
};
@@ -1028,11 +1058,25 @@ pub fn compress_image(
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
let cow_estimate = if need_cow_estimate {
// Because lz4_flex compresses better than official lz4.
let fudge = cow_estimate / 100;
let cow_estimate = if vabc_algo.is_some() {
// lz4_flex and miniz_oxide usually compress better than the lz4 and
// zlib implementations used by libsnapshot_cow. Make up for this by
// adding percentage-based overhead.
cow_estimate += cow_estimate / 100;
Some(cow_estimate + fudge)
// We also need to account for constant overhead, especially with
// smaller partitions. We can match what delta_generator normally adds
// in CowWriterV2::InitPos() exactly. Since we only ever create full
// OTAs, we can assume that all CoW operations are kCowReplaceOp.
// sizeof(CowHeader).
cow_estimate += 38;
// header_.buffer_size (equal to BUFFER_REGION_DEFAULT_SIZE).
cow_estimate += 2 * 1024 * 1024;
// CowOptions::cluster_ops * sizeof(CowOperationV2).
cow_estimate += 200 * 20;
Some(cow_estimate)
} else {
None
};
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
// 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
+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
+11 -10
View File
@@ -1,18 +1,19 @@
/*
* 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::sync::{
atomic::{AtomicBool, Ordering},
Arc,
use std::{
process::ExitCode,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use tracing::error;
static LOGGING_INITIALIZED: AtomicBool = AtomicBool::new(false);
fn main() {
fn main() -> ExitCode {
// Set up a cancel signal so we can properly clean up any temporary files.
let cancel_signal = Arc::new(AtomicBool::new(false));
{
@@ -25,14 +26,14 @@ fn main() {
}
match avbroot::cli::args::main(&LOGGING_INITIALIZED, &cancel_signal) {
Ok(_) => {}
Ok(_) => ExitCode::SUCCESS,
Err(e) => {
if LOGGING_INITIALIZED.load(Ordering::SeqCst) {
error!("{e:?}");
} else {
eprintln!("{e:?}");
}
std::process::exit(1);
ExitCode::FAILURE
}
}
}
+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
//! Hack to format an integer as an octal string because toml_edit can't output
//! octal-formatted integers and many other toml parsers can't parse it either.
+8 -5
View File
@@ -1,7 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
// SPDX-FileCopyrightText: 2022-2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{
cmp::Ordering,
@@ -153,7 +151,7 @@ impl MagiskRootPatcher {
// RULESDEVICE config option, which stored the writable block device as an
// rdev major/minor pair, which was not consistent across reboots and was
// replaced by PREINITDEVICE
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..27100];
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..28100];
const VER_PREINIT_DEVICE: Range<u32> =
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
@@ -1136,6 +1134,11 @@ pub fn patch_boot_images<'a>(
) -> Result<HashSet<&'a str>> {
let parent_span = Span::current();
if patchers.is_empty() {
debug!("Skip loading boot images; nothing to patch");
return Ok(HashSet::new());
}
// Preparse all images. Some patchers need to inspect every candidate.
let mut images = load_boot_images(names, open_input)?;
+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;
+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::{borrow::Cow, cmp::Ordering, io::Cursor};
+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, SeekFrom},
+9 -59
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::{
fs::File,
@@ -211,7 +209,7 @@ impl<W: Write + Reopen> Reopen for BufWriter<W> {
/// A reader wrapper that implements [`Seek`], but only for reporting the
/// current file position.
pub struct CountingReader<R: Read> {
pub struct CountingReader<R> {
inner: R,
offset: u64,
}
@@ -249,7 +247,7 @@ impl<R: Read> Seek for CountingReader<R> {
/// A writer wrapper that implements [`Seek`], but only for reporting the
/// current file position.
pub struct CountingWriter<W: Write> {
pub struct CountingWriter<W> {
inner: W,
offset: u64,
}
@@ -290,7 +288,7 @@ impl<W: Write> Seek for CountingWriter<W> {
}
/// A reader wrapper that hashes data as it's being read.
pub struct HashingReader<R: Read> {
pub struct HashingReader<R> {
inner: R,
context: Context,
}
@@ -314,7 +312,7 @@ impl<R: Read> Read for HashingReader<R> {
}
/// A writer wrapper that hashes data as it's being written.
pub struct HashingWriter<W: Write> {
pub struct HashingWriter<W> {
inner: W,
context: Context,
}
@@ -342,7 +340,7 @@ impl<W: Write> Write for HashingWriter<W> {
}
/// A reader wrapper that only allows reading a specific section of a file.
pub struct SectionReader<R: Read + Seek> {
pub struct SectionReader<R> {
inner: R,
start: u64,
size: u64,
@@ -416,38 +414,6 @@ impl<R: Read + Seek> Seek for SectionReader<R> {
}
}
/// A writer wrapper that seeks instead of writing when a write buffer consists
/// solely of zeros.
#[derive(Debug)]
pub struct HolePunchingWriter<W: Write + Seek> {
inner: W,
}
impl<W: Write + Seek> HolePunchingWriter<W> {
pub fn new(inner: W) -> Self {
Self { inner }
}
pub fn into_inner(self) -> W {
self.inner
}
}
impl<W: Write + Seek> Write for HolePunchingWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if util::is_zero(buf) {
self.inner.seek(SeekFrom::Current(buf.len() as i64))?;
Ok(buf.len())
} else {
self.inner.write(buf)
}
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
/// 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.
@@ -720,9 +686,8 @@ mod tests {
use ring::digest::Context;
use super::{
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
PSeekFile, ReadDiscardExt, ReadStringExt, Reopen, SectionReader, SharedCursor,
WriteStringExt, WriteZerosExt,
CountingReader, CountingWriter, HashingReader, HashingWriter, PSeekFile, ReadDiscardExt,
ReadStringExt, Reopen, SectionReader, SharedCursor, WriteStringExt, WriteZerosExt,
};
const FOOBAR_SHA256: [u8; 32] = [
@@ -882,21 +847,6 @@ mod tests {
assert_eq!(raw_reader.stream_position().unwrap(), 6);
}
#[test]
fn hole_punching_writer() {
let raw_writer = Cursor::new(b"foobar foobar".to_owned());
let mut writer = HolePunchingWriter::new(raw_writer);
writer.write_all(b"hello").unwrap();
writer.write_all(b"").unwrap();
writer.write_all(b"\0").unwrap();
writer.write_all(b"\0\0").unwrap();
writer.write_all(b"world").unwrap();
let raw_writer = writer.into_inner();
assert_eq!(&raw_writer.into_inner(), b"hellor fworld");
}
#[test]
fn pseek_file() {
let raw_file = tempfile::tempfile().unwrap();
+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::{cmp::Ordering, fmt, ops::Range, path::Path};
+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, 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::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};
+426
View File
@@ -0,0 +1,426 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::{io::Cursor, num::NonZeroU64};
use avbroot::{
format::lp::{
BlockDevice, BlockDeviceFlags, Extent, ExtentType, HeaderFlags, ImageType, Metadata,
MetadataSlot, Partition, PartitionAttributes, PartitionGroup, PartitionGroupFlags,
},
stream::{FromReader, ToWriter},
};
fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) {
let mut writer = Cursor::new(Vec::new());
metadata.to_writer(&mut writer).unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
sha512,
);
let mut reader = Cursor::new(&data);
let new_metadata = Metadata::from_reader(&mut reader).unwrap();
assert_eq!(&new_metadata, metadata);
}
#[test]
fn round_trip_empty_image() {
// Layout from Google Pixel 9 Pro XL stock factory image:
// komodo-ad1a.240530.047-factory-bb04e484.zip -> super_empty.img
let metadata = Metadata {
image_type: ImageType::Empty,
metadata_max_size: 65536,
metadata_slot_count: 3,
logical_block_size: 4096,
slots: vec![MetadataSlot {
major_version: 10,
minor_version: 2,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions_a".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
PartitionGroup {
name: "google_dynamic_partitions_b".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
],
block_devices: vec![BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 8531214336,
partition_name: "super".into(),
flags: BlockDeviceFlags::empty(),
}],
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
}],
};
// This is semantically equivalent, but not identical. The Metadata data
// structure only retains the order of partitions within a group, but not
// globally. This checksum is meant to protect against unintended future
// changes.
let sha512 = [
0xfa, 0xdf, 0xf2, 0xb6, 0x74, 0xec, 0x78, 0x7d, 0x0f, 0x7d, 0x17, 0x54, 0xcf, 0x1b, 0x53,
0x13, 0x66, 0x13, 0x5e, 0x8e, 0xcc, 0x84, 0xa2, 0x63, 0xaf, 0x0d, 0x68, 0x96, 0xc6, 0x40,
0x4e, 0x83, 0xe4, 0xe9, 0xef, 0x61, 0xdc, 0x2a, 0x25, 0x5f, 0xa2, 0x7d, 0x29, 0x0b, 0xb6,
0x26, 0x93, 0x59, 0xc9, 0xa8, 0x56, 0x3b, 0x3d, 0x3d, 0x15, 0x6b, 0xee, 0x78, 0x56, 0x78,
0xa1, 0x83, 0x6d, 0x70,
];
round_trip(&metadata, &sha512);
}
#[test]
fn round_trip_normal_image() {
// Layout from Google Pixel 9 Pro XL GrapheneOS factory image:
// komodo-install-2024082500.zip -> super_1.img
let slot = MetadataSlot {
major_version: 10,
minor_version: 2,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions_a".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 2465952,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 0,
},
}],
},
Partition {
name: "system_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 23720,
extent_type: ExtentType::Linear {
start_sector: 2469888,
block_device_index: 0,
},
}],
},
Partition {
name: "system_ext_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 786144,
extent_type: ExtentType::Linear {
start_sector: 2494464,
block_device_index: 0,
},
}],
},
Partition {
name: "product_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 1396432,
extent_type: ExtentType::Linear {
start_sector: 3280896,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 1959024,
extent_type: ExtentType::Linear {
start_sector: 4677632,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor_dlkm_a".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![Extent {
num_sectors: 55008,
extent_type: ExtentType::Linear {
start_sector: 6637568,
block_device_index: 0,
},
}],
},
],
},
PartitionGroup {
name: "google_dynamic_partitions_b".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: NonZeroU64::new(8527020032),
partitions: vec![
Partition {
name: "system_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "system_ext_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "product_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
Partition {
name: "vendor_dlkm_b".into(),
attributes: PartitionAttributes::READONLY,
extents: vec![],
},
],
},
],
block_devices: vec![BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 8531214336,
partition_name: "super".into(),
flags: BlockDeviceFlags::empty(),
}],
flags: HeaderFlags::VIRTUAL_AB_DEVICE,
};
let metadata = Metadata {
image_type: ImageType::Normal,
metadata_max_size: 65536,
metadata_slot_count: 3,
logical_block_size: 4096,
slots: vec![slot; 3],
};
// This is semantically equivalent, but not identical. The Metadata data
// structure only retains the order of partitions within a group, but not
// globally. This checksum is meant to protect against unintended future
// changes.
let sha512 = [
0x3b, 0xad, 0xd4, 0x22, 0xa1, 0x5a, 0xc5, 0xdf, 0x72, 0x7d, 0x92, 0x35, 0x04, 0x8a, 0x75,
0xd9, 0x33, 0x0d, 0xaa, 0x9e, 0x97, 0xd4, 0x13, 0x28, 0x5e, 0x0f, 0x12, 0x0c, 0xf2, 0xb3,
0xdc, 0x35, 0x89, 0x65, 0x40, 0xb0, 0x67, 0xb1, 0x54, 0x09, 0x52, 0x3e, 0x78, 0x3d, 0x3f,
0xa7, 0xf7, 0xa0, 0x77, 0xa8, 0xfc, 0xb7, 0x93, 0x19, 0xcd, 0x43, 0xea, 0x9a, 0x74, 0x65,
0x54, 0x3c, 0xaa, 0x12,
];
round_trip(&metadata, &sha512);
}
#[test]
fn round_trip_retrofit_image() {
// Layout from Google Pixel 3a XL stock factory image:
// bonito-ota-sp2a.220505.008-37a410d5.zip -> system.img
let slot = MetadataSlot {
major_version: 10,
minor_version: 0,
groups: vec![
PartitionGroup {
name: "default".into(),
flags: PartitionGroupFlags::empty(),
maximum_size: None,
partitions: vec![],
},
PartitionGroup {
name: "google_dynamic_partitions".into(),
flags: PartitionGroupFlags::SLOT_SUFFIXED,
maximum_size: NonZeroU64::new(4068474880),
partitions: vec![
Partition {
name: "system".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 1757416,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 0,
},
}],
},
Partition {
name: "vendor".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 991848,
extent_type: ExtentType::Linear {
start_sector: 1761280,
block_device_index: 0,
},
}],
},
Partition {
name: "product".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![
Extent {
num_sectors: 3627008,
extent_type: ExtentType::Linear {
start_sector: 2754560,
block_device_index: 0,
},
},
Extent {
num_sectors: 538240,
extent_type: ExtentType::Linear {
start_sector: 2048,
block_device_index: 1,
},
},
],
},
Partition {
name: "system_ext".into(),
attributes: PartitionAttributes::READONLY
| PartitionAttributes::SLOT_SUFFIXED,
extents: vec![Extent {
num_sectors: 490744,
extent_type: ExtentType::Linear {
start_sector: 540672,
block_device_index: 1,
},
}],
},
],
},
],
block_devices: vec![
BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 3267362816,
partition_name: "system".into(),
flags: BlockDeviceFlags::SLOT_SUFFIXED,
},
BlockDevice {
first_logical_sector: 2048,
alignment: 1048576,
alignment_offset: 0,
size: 805306368,
partition_name: "vendor".into(),
flags: BlockDeviceFlags::SLOT_SUFFIXED,
},
],
flags: HeaderFlags::empty(),
};
let metadata = Metadata {
image_type: ImageType::Normal,
metadata_max_size: 65536,
metadata_slot_count: 2,
logical_block_size: 4096,
slots: vec![slot; 2],
};
// First 274432 bytes of system.img. Unlike the other test cases, this is
// identical to the original image because there is only one partition group
// with partitions, so the group-level ordering is the same as the global
// ordering.
let sha512 = [
0xb9, 0x97, 0xf5, 0x83, 0x39, 0x37, 0x90, 0x0a, 0xb6, 0x46, 0xdd, 0x27, 0x57, 0xf1, 0xf3,
0xbd, 0x8f, 0xc4, 0x63, 0x07, 0x6f, 0xf4, 0x19, 0xc0, 0x02, 0x28, 0x48, 0x99, 0x54, 0xbb,
0xb3, 0xbf, 0x67, 0x95, 0xc4, 0xa7, 0x99, 0xf4, 0xa9, 0xc4, 0xf4, 0x1d, 0xf7, 0x59, 0x28,
0xeb, 0xbc, 0x85, 0x46, 0xd1, 0x7d, 0x65, 0x0f, 0xbe, 0x21, 0xf6, 0xf2, 0xa2, 0x20, 0x5b,
0xda, 0xde, 0xfa, 0x50,
];
round_trip(&metadata, &sha512);
}
+171
View File
@@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
use std::io::{Cursor, Read, Write};
use avbroot::format::sparse::{
self, Chunk, ChunkBounds, ChunkData, CrcMode, Header, SparseReader, SparseWriter,
};
#[derive(Clone, Copy)]
struct TestChunk {
chunk: Chunk,
data: &'static [u8],
}
fn round_trip(block_size: u32, crc32: u32, test_chunks: &[TestChunk], sha512: &[u8; 64]) {
let num_blocks = test_chunks.iter().map(|d| d.chunk.bounds.len()).sum();
let header = Header {
major_version: sparse::MAJOR_VERSION,
minor_version: sparse::MINOR_VERSION,
block_size,
num_blocks,
num_chunks: test_chunks.len() as u32,
crc32,
};
let writer = Cursor::new(Vec::new());
let mut sparse_writer = SparseWriter::new(writer, header).unwrap();
for test_chunk in test_chunks {
sparse_writer.start_chunk(test_chunk.chunk).unwrap();
if !test_chunk.data.is_empty() {
sparse_writer.write_all(test_chunk.data).unwrap();
}
}
let writer = sparse_writer.finish().unwrap();
let data = writer.into_inner();
assert_eq!(
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
sha512,
);
let reader = Cursor::new(&data);
let mut sparse_reader = SparseReader::new(reader, CrcMode::Validate).unwrap();
assert_eq!(sparse_reader.header(), header);
let mut test_chunks_iter = test_chunks.iter();
while let Some(chunk) = sparse_reader.next_chunk().unwrap() {
let test_chunk = test_chunks_iter.next().unwrap();
assert_eq!(chunk, test_chunk.chunk);
if !test_chunk.data.is_empty() {
let mut buf = vec![];
sparse_reader.read_to_end(&mut buf).unwrap();
assert_eq!(buf, test_chunk.data);
}
}
assert!(test_chunks_iter.next().is_none());
}
#[test]
fn round_trip_full_image() {
let block_size = 8;
let file_crc32 = 0xf6e23567;
let test_chunks = [
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 0, end: 1 },
data: ChunkData::Data,
},
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 1 },
data: ChunkData::Crc32(0x88aa689f),
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 2 },
data: ChunkData::Fill(0x01234567),
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 2, end: 3 },
data: ChunkData::Data,
},
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 3, end: 3 },
data: ChunkData::Crc32(0xf6e23567),
},
data: b"",
},
];
let sha512 = [
0x19, 0x5f, 0xa7, 0xdb, 0x18, 0xc6, 0xb9, 0x0e, 0xce, 0x4b, 0x4f, 0x35, 0x36, 0x79, 0x46,
0x02, 0x7a, 0x45, 0x66, 0x63, 0x0e, 0xd9, 0x76, 0x93, 0x2b, 0x88, 0xe2, 0xbc, 0x0b, 0xd9,
0x1f, 0x21, 0x51, 0x92, 0x00, 0x2e, 0xe3, 0xa2, 0xff, 0x24, 0xea, 0xef, 0x24, 0xd5, 0x24,
0xf0, 0x46, 0xf3, 0x10, 0x32, 0xf4, 0xa6, 0x3b, 0x9d, 0xcd, 0xc5, 0x57, 0xf4, 0xc0, 0xe8,
0x01, 0xe8, 0x1d, 0xb3,
];
round_trip(block_size, file_crc32, &test_chunks, &sha512);
}
#[test]
fn round_trip_partial_image() {
let block_size = 8;
let file_crc32 = 0;
let test_chunks = [
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 0, end: 1 },
data: ChunkData::Hole,
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 1, end: 2 },
data: ChunkData::Data,
},
data: b"\x00\x01\x02\x03\x04\x05\x06\x07",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 2, end: 3 },
data: ChunkData::Hole,
},
data: b"",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 3, end: 4 },
data: ChunkData::Data,
},
data: b"\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
},
TestChunk {
chunk: Chunk {
bounds: ChunkBounds { start: 4, end: 5 },
data: ChunkData::Hole,
},
data: b"",
},
];
let sha512 = [
0xee, 0x07, 0xc5, 0x4d, 0x85, 0xee, 0x69, 0x91, 0x61, 0x07, 0x10, 0xed, 0xec, 0x13, 0x5e,
0xfb, 0xc3, 0x7d, 0xcf, 0x1f, 0x2a, 0x13, 0xf0, 0xb6, 0x85, 0xb4, 0xee, 0xe9, 0xd7, 0xa1,
0x12, 0x79, 0x14, 0x16, 0x30, 0x7a, 0x81, 0xf9, 0x4f, 0x72, 0xb2, 0xdd, 0x33, 0xbe, 0x5d,
0x55, 0x70, 0xa9, 0xe3, 0x94, 0x29, 0x40, 0x29, 0x8f, 0x35, 0x23, 0xf8, 0x78, 0x7f, 0xfe,
0xd6, 0x4b, 0x60, 0x16,
];
round_trip(block_size, file_crc32, &test_chunks, &sha512);
}
+1
View File
@@ -33,6 +33,7 @@ allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"CC0-1.0",
"GPL-3.0",
"ISC",
"MIT",
+40 -12
View File
@@ -12,6 +12,9 @@ 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.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -45,13 +48,20 @@ data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v4_gki.hashes]
original = "6b140c378d21eae2fa4fc581bce13a689b21bd32f5fba865698d1fd322f2f8c6"
patched = "a68b3a44c0cf015a225f92837c05fd0dec6e159584c5880eff30d12daa7123ff"
[profile.pixel_v4_gki.hashes_streaming]
original = "c00f891f941f3dddb28966f7b07f3acea773bee104dace82b37c2d1341f09422"
patched = "ce9d8ee97828d233809742a5d3f23aa27b042675b1935ca9e3df0592c55788fd"
[profile.pixel_v4_gki.hashes_seekable]
original = "96a6c366b5de1c3b10d4d6cb4ca503c83ac4cd9ca952a965cceb041990ba7022"
patched = "e7b4609ba7a23609211dcae143bc43f091f286fbbb3a9301c02ee25614d35deb"
# 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.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -79,13 +89,20 @@ data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
[profile.pixel_v4_non_gki.hashes]
original = "31963e6f81986c6686111f50e36b89e4d85ee5c02bc8e5ecd560528bc98d6fe7"
patched = "a13173a006ad94b25db682bb14fde2e36891417727d6541bdf5f9bca57d26751"
[profile.pixel_v4_non_gki.hashes_streaming]
original = "4d692bc777b568b0626d3c08d2e6f83f1b472db5ad903486daaec6a78d0cc26e"
patched = "e27673e4f30933710c11d51f0e73849068cbe9bc9f54e6076bdd93f9a5c8ea0a"
[profile.pixel_v4_non_gki.hashes_seekable]
original = "ea27ecd9718c17b63400b2548680bb3cee93ce63b4fc44ff9654ca0d9c5372a8"
patched = "3456b14e014cf565a808a9e834d9105a23539f07b2c460db19c9384aadbc3b93"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[profile.pixel_v3]
vabc_algo = "Lz4"
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
@@ -114,13 +131,20 @@ data.type = "boot"
data.version = "vendor_v3"
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
[profile.pixel_v3.hashes]
original = "e684aacb54464098c1b8e3f499efe35dff10ea792e89d71a83404620d0108b3e"
patched = "49e810ae76154bccf2dc7d810b9eec19d23a5b8fa32381469660488d0a25121b"
[profile.pixel_v3.hashes_streaming]
original = "f432dc7931520feb238474aa707dd5299747562ffe6129f3f763b5f11ac473ab"
patched = "3850a2e73bd783a1ec4a70c59f37d2374e017c20df7ab4b591182b14d187c18e"
[profile.pixel_v3.hashes_seekable]
original = "7d29ecc6780953c22052a576b8dc85066c8667a875e918a786a08ff4545b47d1"
patched = "9f6342940b7cfbeb27b0567f006bb35cbee910ef038ec535403c662d5252ca71"
# 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"
@@ -143,6 +167,10 @@ avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "ee9568797d9195985f14753b89949d8ebb08c8863a32eceeeec6e8d94661b1cf"
patched = "e413f1f87ee5ba53d402edad03b0df8af451b5b0323202a9d32b8327d433d340"
[profile.pixel_v2.hashes_streaming]
original = "1b45235b58054009cc496f6c3ee11d3dc16ed5c388c861761e26a6fce83103a0"
patched = "193b2dc70dd465d686f35c7b7f74d2cc1b06a55e48cf5c2e4df0f667e03032fc"
[profile.pixel_v2.hashes_seekable]
original = "52284308fae10cbaf09ade14e92f3bbe6149751a42bff15432982fcef8d890ab"
patched = "7ad74ac87ddcaf34938017e6149a646041d70926e31ecda93e156e9397467b3b"
+6 -7
View File
@@ -1,12 +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::{ffi::OsString, path::PathBuf};
use avbroot::cli::args::{LogFormat, LogLevel};
use avbroot::cli::args::LogFormat;
use clap::{Args, Parser, Subcommand, ValueEnum};
use tracing::Level;
#[derive(Debug, Args)]
pub struct ProfileGroup {
@@ -69,8 +68,8 @@ pub struct Cli {
pub command: Command,
/// Lowest log message severity to output.
#[arg(long, global = true, value_name = "LEVEL", default_value_t)]
pub log_level: LogLevel,
#[arg(long, global = true, value_name = "LEVEL", default_value_t = Level::INFO)]
pub log_level: Level,
/// Output format for log messages.
#[arg(long, global = true, value_name = "FORMAT", default_value_t = LogFormat::Medium)]
+6 -5
View File
@@ -1,11 +1,10 @@
/*
* 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::{collections::BTreeMap, fs, path::Path};
use anyhow::{Context, Result};
use avbroot::format::payload::VabcAlgo;
use serde::{Deserialize, Serialize};
use toml_edit::DocumentMut;
@@ -109,8 +108,10 @@ pub struct Partition {
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub vabc_algo: Option<VabcAlgo>,
pub partitions: BTreeMap<String, Partition>,
pub hashes: Hashes,
pub hashes_streaming: Hashes,
pub hashes_seekable: Hashes,
}
#[derive(Serialize, Deserialize)]
+182 -95
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-2024 Andrew Gunnerson
// SPDX-FileCopyrightText: 2023 Pascal Roeleven
// SPDX-License-Identifier: GPL-3.0-only
mod cli;
mod config;
@@ -11,9 +9,10 @@ use std::{
collections::{BTreeMap, BTreeSet},
env,
ffi::{OsStr, OsString},
fs::{self, File},
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
fs::{self, File, OpenOptions},
io::{self, BufReader, Cursor, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
slice,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
@@ -33,9 +32,9 @@ use avbroot::{
self, BootImage, BootImageV0Through2, BootImageV3Through4, RamdiskMeta, V1Extra,
V2Extra, V4Extra, VendorBootImageV3Through4, VendorV4Extra,
},
compression::{CompressedFormat, CompressedWriter},
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntry, CpioEntryData},
ota::{self, SigningWriter, ZipEntry},
ota::{self, SigningWriter, ZipEntry, ZipMode},
padding,
payload::{self, PayloadHeader, PayloadWriter},
},
@@ -46,7 +45,7 @@ use avbroot::{
DeltaArchiveManifest, DynamicPartitionGroup, DynamicPartitionMetadata, PartitionUpdate,
},
},
stream::{self, CountingWriter, HashingReader, PSeekFile, Reopen, ToWriter},
stream::{self, CountingWriter, FromReader, HashingReader, PSeekFile, Reopen, ToWriter},
};
use clap::Parser;
use rsa::{rand_core::OsRng, traits::PublicKeyParts, BigUint};
@@ -577,6 +576,7 @@ fn create_payload(
partitions: &BTreeMap<String, Partition>,
inputs: &BTreeMap<String, PSeekFile>,
ota_info: &OtaInfo,
profile: &Profile,
key_ota: &RsaSigningKey,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
@@ -594,14 +594,14 @@ fn create_payload(
.map(PSeekFile::new)
.with_context(|| format!("Failed to create temp file for: {name}"))?;
let (partition_info, operations, cow_estimate) = payload::compress_image(
file,
&writer,
name,
4096,
dynamic_partitions_names.contains(name),
cancel_signal,
)?;
let vabc_algo = if dynamic_partitions_names.contains(name) {
profile.vabc_algo
} else {
None
};
let (partition_info, operations, cow_estimate) =
payload::compress_image(file, &writer, name, 4096, vabc_algo, cancel_signal)?;
compressed.insert(name, writer);
@@ -645,7 +645,7 @@ fn create_payload(
}],
snapshot_enabled: Some(true),
vabc_enabled: Some(true),
vabc_compression_param: Some("lz4".to_owned()),
vabc_compression_param: profile.vabc_algo.map(|a| a.to_string()),
cow_version: Some(2),
vabc_feature_set: None,
}),
@@ -695,15 +695,19 @@ fn create_payload(
Ok((properties, metadata_size))
}
#[allow(clippy::too_many_arguments)]
fn create_ota(
output: &Path,
ota_info: &OtaInfo,
profile: &Profile,
zip_mode: ZipMode,
key_avb: &RsaSigningKey,
key_ota: &RsaSigningKey,
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<()> {
info!("Generating OTA: {output:?}");
let inputs = create_partition_images(
&profile.partitions,
ota_info,
@@ -712,11 +716,23 @@ fn create_ota(
cancel_signal,
)?;
let raw_writer =
File::create(output).with_context(|| format!("Failed to open for writing: {output:?}"))?;
let buffered_writer = BufWriter::new(raw_writer);
let signing_writer = SigningWriter::new(buffered_writer);
let mut zip_writer = ZipWriter::new_streaming(signing_writer);
let raw_writer = OpenOptions::new()
.read(zip_mode == ZipMode::Seekable)
.write(true)
.create(true)
.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 options = FileOptions::default()
.compression_method(CompressionMethod::Stored)
.large_file(false);
@@ -746,6 +762,7 @@ fn create_ota(
&profile.partitions,
&inputs,
ota_info,
profile,
key_ota,
cancel_signal,
)
@@ -800,13 +817,18 @@ fn create_ota(
spl_downgrade: false,
};
let data_descriptor_size = match zip_mode {
ZipMode::Streaming => 16,
ZipMode::Seekable => 0,
};
ota::add_metadata(
&entries,
&mut zip_writer,
// Offset where next entry would begin.
entries.last().map(|e| e.offset + e.size).unwrap() + 16,
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
&metadata,
payload_metadata_size.unwrap(),
zip_mode,
)
.context("Failed to write new OTA metadata")?;
@@ -814,7 +836,7 @@ fn create_ota(
.finish()
.context("Failed to finalize output zip")?;
let mut buffered_writer = signing_writer
.finish(key_ota, cert_ota)
.finish(key_ota, cert_ota, cancel_signal)
.context("Failed to sign output zip")?;
buffered_writer
.flush()
@@ -984,10 +1006,12 @@ impl KeySet {
new_keys_with_prefix!(new_for_test, "TEST_KEY_DO_NOT_USE_");
}
#[allow(clippy::too_many_arguments)]
fn patch_image(
input_file: &Path,
output_file: &Path,
system_image_file: &Path,
zip_mode: ZipMode,
extra_args: &[&OsStr],
keys: &KeySet,
signing_helper: bool,
@@ -1001,6 +1025,8 @@ fn patch_image(
(&keys.avb_key_file, &keys.ota_key_file)
};
let zip_mode_arg = zip_mode.to_string();
// We're intentionally using the CLI interface.
let mut args: Vec<&OsStr> = vec![
OsStr::new("patch"),
@@ -1022,6 +1048,8 @@ fn patch_image(
OsStr::new("--cert-ota"),
keys.ota_cert_file.as_os_str(),
OsStr::new("--dsu"),
OsStr::new("--zip-mode"),
OsStr::new(&zip_mode_arg),
];
let argv0: OsString;
@@ -1071,6 +1099,56 @@ fn verify_image(input_file: &Path, keys: &KeySet, cancel_signal: &AtomicBool) ->
Ok(())
}
fn clean_boot_image_certs(path: &Path, cancel_signal: &AtomicBool) -> Result<()> {
info!("Removing OTA certs and DSU public keys: {path:?}");
let mut file = OpenOptions::new().read(true).write(true).open(path)?;
let mut boot_image = BootImage::from_reader(&mut file)?;
let ramdisks = match &mut boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
};
if ramdisks.is_empty() {
bail!("No ramdisk found: {path:?}");
}
let raw_ramdisk_reader = Cursor::new(&ramdisks[0]);
let ramdisk_reader = CompressedReader::new(raw_ramdisk_reader, false)?;
let ramdisk_format = ramdisk_reader.format();
let mut entries = cpio::load(ramdisk_reader, false, cancel_signal)?;
// Wipe out OTA certificates.
if let Some(entry) = entries
.iter_mut()
.find(|e| e.path == b"system/etc/security/otacerts.zip")
{
let mut zip_writer = ZipWriter::new(Cursor::new(Vec::new()));
let empty_zip = zip_writer.finish()?.into_inner();
entry.data = CpioEntryData::Data(empty_zip);
}
// Wipe out DSU public keys.
entries.retain(|e| !e.path.starts_with(b"first_stage_ramdisk/avb/"));
let raw_ramdisk_writer = Cursor::new(Vec::new());
let mut ramdisk_writer = CompressedWriter::new(raw_ramdisk_writer, ramdisk_format)?;
cpio::save(&mut ramdisk_writer, &entries, false, cancel_signal)?;
let raw_ramdisk_writer = ramdisk_writer.finish()?;
ramdisks[0] = raw_ramdisk_writer.into_inner();
// We're creating an input for --prepatched, so no need for AVB metadata.
file.set_len(0)?;
file.rewind()?;
boot_image.to_writer(&mut file)?;
Ok(())
}
fn filter_profiles<'a>(config: &'a Config, cli: &'a ProfileGroup) -> Result<BTreeSet<&'a str>> {
let mut profiles = config
.profile
@@ -1128,93 +1206,102 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
];
for name in profiles {
let _span = info_span!("profile", name).entered();
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe profile name: {name}");
}
info!("Generating OTA");
let profile = &config.profile[name];
// Can't used NamedTempFile because avbroot does atomic replaces.
let profile_dir = work_dir.join(name);
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");
for (zip_mode, hashes) in [
(ZipMode::Streaming, &profile.hashes_streaming),
(ZipMode::Seekable, &profile.hashes_seekable),
] {
let _span = info_span!("profile", name, %zip_mode).entered();
fs::create_dir_all(&profile_dir)
.with_context(|| format!("Failed to create directory: {profile_dir:?}"))?;
// Can't used NamedTempFile because avbroot does atomic replaces.
let profile_dir = work_dir.join(name);
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");
create_ota(
&out_original,
&config.ota_info,
profile,
&orig_keys.avb_key,
&orig_keys.ota_key,
&orig_keys.ota_cert,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to create OTA"))?;
fs::create_dir_all(&profile_dir)
.with_context(|| format!("Failed to create directory: {profile_dir:?}"))?;
verify_image(&out_original, &orig_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify original OTA"))?;
create_ota(
&out_original,
&config.ota_info,
profile,
zip_mode,
&orig_keys.avb_key,
&orig_keys.ota_key,
&orig_keys.ota_cert,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to create OTA"))?;
verify_hash(&out_original, &profile.hashes.original.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
verify_image(&out_original, &orig_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify original OTA"))?;
// Patch once using Magisk.
extract_image(&out_original, &profile_dir, cancel_signal)
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
verify_hash(&out_original, &hashes.original.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
let system_image = profile_dir.join("system.img");
// Patch once using Magisk.
extract_image(&out_original, &profile_dir, cancel_signal)
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
patch_image(
&out_original,
&out_magisk,
&system_image,
&args_magisk,
&test_keys,
false,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
let system_image = profile_dir.join("system.img");
verify_image(&out_magisk, &test_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
patch_image(
&out_original,
&out_magisk,
&system_image,
zip_mode,
&args_magisk,
&test_keys,
false,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
verify_hash(&out_magisk, &profile.hashes.patched.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
verify_image(&out_magisk, &test_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
// Patch again, but this time, use the previously patched boot image
// instead of applying the Magisk patch.
extract_image(&out_magisk, &profile_dir, cancel_signal)
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
verify_hash(&out_magisk, &hashes.patched.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
let mut magisk_image = profile_dir.join("init_boot.img");
if !magisk_image.exists() {
magisk_image = profile_dir.join("boot.img");
// Patch again, but this time, use the previously patched boot image
// instead of applying the Magisk patch.
extract_image(&out_magisk, &profile_dir, cancel_signal)
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
let mut magisk_image = profile_dir.join("init_boot.img");
if !magisk_image.exists() {
magisk_image = profile_dir.join("boot.img");
}
// Wipe out changes that we expect to be made to prepatched images.
clean_boot_image_certs(&magisk_image, cancel_signal)?;
let args_prepatched = [OsStr::new("--prepatched"), magisk_image.as_os_str()];
patch_image(
&out_original,
&out_prepatched,
&system_image,
zip_mode,
&args_prepatched,
&test_keys,
true,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
verify_image(&out_prepatched, &test_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
verify_hash(&out_prepatched, &hashes.patched.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
}
let args_prepatched = [OsStr::new("--prepatched"), magisk_image.as_os_str()];
patch_image(
&out_original,
&out_prepatched,
&system_image,
&args_prepatched,
&test_keys,
true,
cancel_signal,
)
.with_context(|| format!("[{name}] Failed to patch OTA"))?;
verify_image(&out_prepatched, &test_keys, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA"))?;
verify_hash(&out_prepatched, &profile.hashes.patched.0, cancel_signal)
.with_context(|| format!("[{name}] Failed to verify patched OTA hash"))?;
}
Ok(())
+3
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
+3
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
+3
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::{io::Cursor, sync::atomic::AtomicBool};
+3
View File
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: 2023 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::{io::Cursor, sync::atomic::AtomicBool};
+24
View File
@@ -0,0 +1,24 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
use avbroot::{format::lp::Metadata, stream::FromReader};
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let reader = Cursor::new(data);
let _ = Metadata::from_reader(reader);
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
+30
View File
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: 2024 Andrew Gunnerson
// SPDX-License-Identifier: GPL-3.0-only
#[cfg(not(windows))]
mod fuzz {
use std::io::{self, Cursor};
use avbroot::format::sparse::{ChunkData, CrcMode, SparseReader};
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let reader = Cursor::new(data);
if let Ok(mut sparse_reader) = SparseReader::new(reader, CrcMode::Ignore) {
while let Ok(Some(chunk)) = sparse_reader.next_chunk() {
if chunk.data == ChunkData::Data {
let _ = io::copy(&mut sparse_reader, &mut io::sink());
}
}
}
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
+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::{
collections::BTreeMap,
+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
mod changelog;
mod version;
+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::{
fs::{self, File},