Compare commits

..

243 Commits

Author SHA1 Message Date
Andrew Gunnerson 833ca931e8 Version 3.0.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:57:22 -05:00
Andrew Gunnerson 9b6677d28a CHANGELOG.md: Remove entry for PR #220
It was obsoleted by #226.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:55:06 -05:00
Andrew Gunnerson 12e59fcf53 CHANGELOG.md: Add entry for PR #245
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:52:30 -05:00
Andrew Gunnerson 4bc7363f70 Merge pull request #245 from chenxiaolong/deps
Update all dependencies
2023-12-29 20:51:53 -05:00
Andrew Gunnerson b5ad9dddf6 Update all dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:44:12 -05:00
Andrew Gunnerson f0aba89549 CHANGELOG.md: Add entry for PR #243
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 20:41:22 -05:00
Andrew Gunnerson c9633682ef Merge pull request #243 from chenxiaolong/repair_mode
README.md: Document Repair Mode
2023-12-29 20:39:22 -05:00
Andrew Gunnerson da17935972 CHANGELOG.md: Add entry for PR #241
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:18:39 -05:00
Andrew Gunnerson c2140a85e9 Merge pull request #241 from chenxiaolong/e2e
e2e: Switch to using mock OTAs for testing
2023-12-29 16:17:31 -05:00
Andrew Gunnerson 3a3582ce4c e2e: Switch to using mock OTAs for testing
This commit replaces the previous approach of patching real OTAs with
patching mock OTAs. The motivation for this change is to make it
possible to test the system partition otacerts.zip patching without
needing to download huge files. Adding the system image to the stripped
OTAs would increase the file size by an order of magnitude.

The mock OTAs are generated from a set of profiles defined in e2e.toml.
The four included profiles are meant to mimic the OTAs used for testing
before:

* pixel_v4_gki     ~= cheetah
* pixel_v4_non_gki ~= bluejay
* pixel_v3         ~= bramble
* pixel_v2         ~= sunfish

There is no equivalent profile for ossi because newer OnePlus devices no
longer support custom signing keys properly.

The mock OTAs are perfectly valid, structure and signature-wise. They
just don't include any real partition data where possible. They are
initially signed with a different set of keys to ensure that the changes
made by the patching process are actually visible.

With how small the mock OTAs are, testing every profile only takes about
two seconds. Thus, the Github Actions workflow was adjusted to just run
e2e in the same job as the build.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 16:07:57 -05:00
Andrew Gunnerson b029116a5a CHANGELOG.md: Add entry for PR #244
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 15:48:12 -05:00
Andrew Gunnerson af1b01646d Merge pull request #244 from chenxiaolong/system_patch_performance
Optimize repacking of modified system image into payload
2023-12-29 15:47:09 -05:00
Andrew Gunnerson e85102709f Optimize repacking of modified system image into payload
When the original payload extents are all in order and have no gaps, we
can efficiently copy unmodified chunks of the system image from the
original payload into the new payload. Only the chunks containing the
modified regions (`otacerts.zip`, hash tree, FEC data, and AVB metadata)
need to be recompressed. This massively reduces the CPU usage since
usually only <20 MiB need to be recompressed.

If the conditions for the optimized path aren't satisfied (eg. extents
aren't in order or `--replace` is used), then it falls back to splitting
and compressing the entire system image.

This fixes the performance issues introduced in #240. This is probably
the best that we can do given that we now always patch the system image.

Issue: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-29 00:02:35 -05:00
Andrew Gunnerson 124629dc45 README.md: Document Repair Mode
Closes: #216

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:10:26 -05:00
Andrew Gunnerson e89c4bdc9e CHANGELOG.md: Add entry for PR #240
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-28 18:00:23 -05:00
Andrew Gunnerson c1095b8321 Merge pull request #240 from chenxiaolong/system_otacerts
Add support for replacing otacerts.zip in the system image
2023-12-28 17:49:02 -05:00
Andrew Gunnerson 17504a7f81 Add support for replacing otacerts.zip in the system image
Previously, overriding otacerts.zip in the system partition required the
user to flash a Magisk/KernelSU module that would bind mount over the
file during boot. While this worked well enough, it's insufficient for
unrooted setups, which has become more important since unrooting is the
only safe way to use the new OEM repair mode feature. With the stock
otacerts.zip, the OEM's default OTA updater app could run and install an
OS upgrade that's not signed by the user's key.

With this commit, the raw otacerts.zip bytes in the system partition are
directly replaced with a new zip that contains the user's certificate.
This method was inspired by @pascallj's comment in #216 suggesting
intentionally corrupting the otacerts.zip data in the filesystem.

Because avbroot does not have filesystem parsers for ext4/f2fs/erofs, we
rely on a heuristic-based search on the raw filesystem image. The file
is always smaller than one block (which is at least 4096 bytes on all
known devices), so the file data is stored contiguously on disk and in
the case of erofs, won't be compressed. None of the three filesystems
are copy-on-write and thus, have no filesystem-level data checksums. For
the dm-verity layer one level up, avbroot already knows how to recompute
the hash tree and FEC data.

To ensure that there are no false positives, any match that the search
finds must correctly parse as a valid zip and every entry within the zip
must have a filename that ends in .x509.pem. This matches what
update_engine expects from a proper otacerts.zip file.

Since the new approach is doing a raw search and replace, the old and
new files must have the same size. When the new zip is smaller, null
bytes are added to the zip archive comment field to pad to the correct
size. When the new zip is larger, avbroot will attempt the following to
try and make the file size smaller:

1. Enable zip deflate compression
2. Strip the X.509 signature from the certificate
3. Clear out the issuer RDN sequence from the certificate
4. Clear out the subject RDN sequence from the certificate

The latter three changes work because Android never performs any PKI
operations with the certificate. There is no CA certificate chain. The
X.509 certificate file is nothing more than a way to transport an RSA
public key.

avbroot requires the user's key to be RSA 4096. If the original zip had
the same key size, then none of these shrinking methods are needed. If
it contained an RSA 2048 key, then the first two modifications are
usually sufficient. The latter two modifications should only be needed
if the user picked a really long subject value when generating the
certificate.

With these new changes, the OTA patching time will approximately double
on a system with an SSD and modern CPU. This is dominated by the time it
takes to XZ-compress the system partition image. The compression is
already parallelized and scales linearly with the number of cores.
There's likely not much more that can be done to further speed this up.

Finally, these new changes are currently excluded from the e2e tests
because including the system partition in the stripped OTAs would
increase the file size by an order of magnitude. This could potentially
be solved in the future by generating our own small OTAs to use for
testing instead of running against real device OTAs.

Fixes: #225

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-24 18:23:03 -05:00
Andrew Gunnerson d8190143bf CHANGELOG.md: Add entry for PR #237
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 16:17:06 -05:00
Andrew Gunnerson 0944abcea2 Merge pull request #237 from chenxiaolong/autodetect
Allow boot image autodetection to inspect boot images
2023-12-22 16:14:52 -05:00
Andrew Gunnerson 249c36d8c3 Allow boot image autodetection to inspect boot images
During patching, all boot images are now extracted and the individual
patchers can inspect them to determine which ones need modifications.
This replaces the previous mechanism of detecting which boot images to
patch based on the name alone.

With this new method, the --boot-partition and --otacerts-partitions
options are no longer needed. The former option is kept (but ignored
with a warning message) for backwards compatibility, but the latter is
completely removed because it never made it to a stable release.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-22 00:35:32 -05:00
Andrew Gunnerson 446e87289f CHANGELOG.md: Add entry for PR #234
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:14:44 -05:00
Andrew Gunnerson 04b8e3c315 Merge pull request #234 from chenxiaolong/fec_param_order
fec: Change update() parameter order to be more consistent
2023-12-17 22:14:09 -05:00
Andrew Gunnerson 03809a1e88 fec: Change update() parameter order to be more consistent
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 22:07:00 -05:00
Andrew Gunnerson 1fcb29532d CHANGELOG.md: Add entry for PR #233
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 21:08:28 -05:00
Andrew Gunnerson b383c8a0ab Merge pull request #233 from chenxiaolong/hash_tree
Split dm-verify hash tree logic out of avb module
2023-12-17 21:06:07 -05:00
Andrew Gunnerson 7ea8e8fa9e Split dm-verify hash tree logic out of avb module
* Refactor hash tree computation to work on a preallocated hash tree
  buffer. This makes it possible to partially update a hash tree, which
  is now supported.

* Add new subcommands for working with hash trees. There's no standard
  header format for dm-verity information, so these commands write hash
  tree files with a custom header. The commands are not really useful
  outside of debugging avbroot's hash tree implementation.

  Using AVB was considered, but it has no support for the hash tree data
  living in a separate file from the input. If other parties agree on a
  standard header in the future, avbroot will switch to that format.

* Add tests for the hash tree implementation.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 20:48:00 -05:00
Andrew Gunnerson ce43b3c657 CHANGELOG.md: Add entry for PR #232
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:08:43 -05:00
Andrew Gunnerson 05e5e59d65 Merge pull request #232 from chenxiaolong/small_hash_tree
avb: hashtree: Fix incorrect I/O read size for input smaller than one block
2023-12-17 15:07:51 -05:00
Andrew Gunnerson 72b8120bf9 avb: hashtree: Fix incorrect I/O read size for input smaller than one block
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 15:00:50 -05:00
Andrew Gunnerson 8110f82b17 CHANGELOG.md: Add entry for PR #231
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:59:47 -05:00
Andrew Gunnerson 64dbfa241e Merge pull request #231 from chenxiaolong/overlap
util: merge_overlapping: Skip empty and invalid ranges
2023-12-17 14:59:11 -05:00
Andrew Gunnerson 7041c8ef2c util: merge_overlapping: Skip empty and invalid ranges
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 14:50:21 -05:00
Andrew Gunnerson 7296eed51f CHANGELOG.md: Add entry for PR #230
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:48:36 -05:00
Andrew Gunnerson 698810d256 Merge pull request #230 from chenxiaolong/fec_update
fec: Add support for partially updating FEC data
2023-12-17 00:47:45 -05:00
Andrew Gunnerson acf57272a7 fec: Add support for partially updating FEC data
FEC data can be updated efficiently with "round" granularity when the
regions where the input file was modified are known.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-17 00:39:04 -05:00
Andrew Gunnerson 2b50e4527f CHANGELOG.md: Add entry for PR #229
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 23:09:09 -05:00
Andrew Gunnerson cd7b5ab23c Merge pull request #229 from chenxiaolong/partition_hashes
cli/ota: Also verify whole-partition hashes
2023-12-16 23:07:45 -05:00
Andrew Gunnerson f73c451d23 cli/ota: Also verify whole-partition hashes
Previously, when running `avbroot ota verify`, only the payload chunk
hashes were verified.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:59:33 -05:00
Andrew Gunnerson a801e18595 CHANGELOG.md: Add entry for PR #228
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:51:06 -05:00
Andrew Gunnerson 3932ff9092 Merge pull request #228 from chenxiaolong/parallel_compress
Split new partition images into chunks and compress in parallel
2023-12-16 22:49:22 -05:00
Andrew Gunnerson 1a36fbd46c Split new partition images into chunks and compress in parallel
Previously, all replacement partition images (those that have been
patched or `--replace`d) were compressed as a whole, which would be very
slow for larger images. Instead, we'll split the images into 2 MiB
chunks and compress them in parallel. This more closely matches what
AOSP's payload_generator does and scales linearly with the number of CPU
cores. The compression is less efficient, but the file size generally
only increases by 10s of KiB.

This commit also reworks the implementation so that patched images are
stored in temp files instead of in memory again. In hindsight, doing
everything in memory only made things more complex and causes the memory
usage to blow up when doing things like `--replace system <path>`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-16 22:38:18 -05:00
Andrew Gunnerson 077a80f4ce CHANGELOG.md: Add entry for PR #226
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:41:30 -05:00
Andrew Gunnerson 1e43930f3c Merge pull request #226 from chenxiaolong/critical
cli/ota: Limit critical partition check to bootloader-verified partitions
2023-12-12 21:39:39 -05:00
Andrew Gunnerson d00e53fb2a cli/ota: Limit critical partition check to bootloader-verified partitions
dm-verity partitions don't necessarily have AVB descriptors inside the
root vbmeta images. The fstab might verify them against a public key on
disk using the `avb_keys` fs_mgr option. We have no way to statically
check for this scenario and it's becoming increasingly common on newer
OnePlus devices. Instead, we'll just limit the checks to partitions that
are verified by the bootloader.

Fixes: #223

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:24 -05:00
Andrew Gunnerson 0b5f3b30cf CHANGELOG.md: Add entry for PR #227
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:30:09 -05:00
Andrew Gunnerson 10c2a0969f Merge pull request #227 from chenxiaolong/symlink
workflows/ci: Fix overwriting symlink for target/output
2023-12-12 21:29:25 -05:00
Andrew Gunnerson 83218a747d workflows/ci: Fix overwriting symlink for target/output
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 21:19:26 -05:00
Andrew Gunnerson 6fb7d568cd CHANGELOG.md: Add entry for PR #224
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 19:12:11 -05:00
Andrew Gunnerson 6cbf690e17 Merge pull request #224 from chenxiaolong/static
workflows/ci: Build statically linked executables where possible
2023-12-12 19:07:02 -05:00
Andrew Gunnerson e097f98404 workflows/ci: Build statically linked executables where possible
This way, the precompiled executables can run on distros that ship an
older version of glibc or don't use glibc at all.

Fixes: #222

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:34:02 -05:00
Andrew Gunnerson d5605eabc0 workflows/ci: Use Rust triple instead of LLVM triple
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-12 18:05:00 -05:00
Andrew Gunnerson d1bcf2da80 CHANGELOG.md: Add entry for PR #221
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-11 18:23:49 -05:00
Andrew Gunnerson bbf8e28a28 Merge pull request #221 from chenxiaolong/otacerts
cli/ota: Add new --otacerts-partition option for the patch subcommand
2023-12-11 18:22:03 -05:00
Andrew Gunnerson dc93b17888 cli/ota: Add new --otacerts-partition option
The autodetection logic for `@otacerts` is based on the presence of the
`recovery`, `vendor_boot`, and `boot` partitions (in that order). Some
devices have `vendor_boot`, but put `system/etc/security/otacerts.zip`
inside `boot`.

With the way things are written now, we don't have the ability to
inspect the actual partition images for the autodetection. It is based
on the name only. So, for now, we'll just allow the user to override the
autodetected partition similar to what we already do with the
`--boot-partition` option.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 23:32:14 -05:00
Andrew Gunnerson f83a408dc4 CHANGELOG.md: Add entry for PR #220
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 16:03:08 -05:00
Andrew Gunnerson 798fddc4d5 Merge pull request #220 from chenxiaolong/odm
cli/ota: Fine tune `is_critical_to_avb()` for odm partition
2023-12-10 16:01:43 -05:00
Andrew Gunnerson dfed06dcf8 cli/ota: Fine tune is_critical_to_avb() for odm partition
AOSP primarily cares about `odm` and `odm_dlkm`, so update the logic to
check for that specifically. This fixes patching some OTAs that include
an unprotected OEM-specific `odm_ext` image.

Issue: #218

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 15:54:02 -05:00
Andrew Gunnerson 02fd92a640 CHANGELOG.md: Add entry for PR #219
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:47:33 -05:00
Andrew Gunnerson dd4faf72ae Merge pull request #219 from chenxiaolong/decode-avb
key: Add new decode-avb subcommand
2023-12-10 13:43:58 -05:00
Andrew Gunnerson adbe249edb key: Add new decode-avb subcommand
This does the reverse of `avbroot key extract-avb`. It's useful for
reconstructing a PKCS8-encoded RSA public key when working with the
`public_key` fields in `avb.toml`.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-10 13:29:45 -05:00
Andrew Gunnerson b7028b13a2 CHANGELOG.md: Add entry for PR #214
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:44:11 -05:00
Andrew Gunnerson 8122b0eece Merge pull request #214 from chenxiaolong/RUSTSEC-2023-0071
deny.toml: Temporarily ignore RUSTSEC-2023-0071
2023-12-03 18:41:17 -05:00
Andrew Gunnerson 0613de4ee4 deny.toml: Temporarily ignore RUSTSEC-2023-0071
https://rustsec.org/advisories/RUSTSEC-2023-0071

This is a side-channel vulnerability where secrets can be leaked to an
attacker that is able to measure the timing of a large number of RSA
operations. As of 2023-12-03, there is no released version of the rsa
crate that contains a fix.

For avbroot specifically, this vulnerability is not too critical for a
couple reasons:

1. avbroot performs RSA signing only at the end of lengthy processes
   that involve a lot of disk I/O. It's very expensive to run avbroot
   the millions of times needed to capture a sufficient amount of timing
   data.
2. During a single run of avbroot, it will only perform RSA signing a
   handful of times. To get sufficient measurements, the attacker would
   need to rerun avbroot. If they are able to rerun avbroot, then they
   are also able to just read and steal the private key directly.

avbroot has no network capabilities, so this is not inherently remotely
exploitable.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 18:02:48 -05:00
Andrew Gunnerson 1a18eb7f85 README.md: Document OnePlus boot issues
Issue: #186
Issue: #195
Issue: #212

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-12-03 14:58:38 -05:00
Andrew Gunnerson 1e1ca9dcbf CHANGELOG.md: Add entry for PR #211
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:51:39 -05:00
Andrew Gunnerson 6924783f48 Merge pull request #211 from chenxiaolong/lints
Fix new lint warnings from rustc and clippy 1.74.0
2023-11-22 19:50:27 -05:00
Andrew Gunnerson 98071daa33 Fix new lint warnings from rustc and clippy 1.74.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:30:52 -05:00
Andrew Gunnerson 8e1adae947 CHANGELOG.md: Add entry for PR #210
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 19:29:00 -05:00
Andrew Gunnerson d6705f4f00 Merge pull request #210 from chenxiaolong/avb_2.0_format_1.3
avb: Add support for AVB 2.0 format 1.3.0
2023-11-22 19:27:24 -05:00
Andrew Gunnerson a648df1695 avb: Add support for AVB 2.0 format 1.3.0
Format version 1.3.0 adds a new 32-bit big endian `flags` field to
chain descriptors, carved out from the `reserved` array. There is only
one possible flag, which indicates that the target partition is not A/B
and so the bootloader should not append the `_a` or `_b` suffix.

There are no known AVB images in the wild where the first four bytes of
the `reserved` field are not zero. Thus, to keep the logic simple, the
AVB parser will unconditionally parse those bytes as if they were the
`flags` field, regardless of the file format version.

AOSP changes:

* https://android.googlesource.com/platform/external/avb/+/a1fe228b86543a21739c51352f5ce72f134fccfa%5E%21/
* https://android.googlesource.com/platform/external/avb/+/d00d02c390e267ef43d93562864dd6e45966c435%5E%21/

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-22 18:47:35 -05:00
Andrew Gunnerson a7c872be3e Version 2.3.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:59:43 -05:00
Andrew Gunnerson cf1ab6ecca CHANGELOG.md: Add entry for PR #208
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:58:53 -05:00
Andrew Gunnerson 7364e8d725 Merge pull request #208 from chenxiaolong/descriptors
cli/ota: Merge property and kernel cmdline descriptors from child into parent
2023-11-15 18:57:26 -05:00
Andrew Gunnerson 935a86e72c cli/ota: Merge property and kernel cmdline descriptors from child into parent
Some devices use the legacy Android/ChromiumOS-specific `dm=` kernel
command line option to configure dm-verity without userspace helpers.
These options are specified in kernel command line descriptors in the
system partition's vbmeta header, which need to be merged into the
parent vbmeta image for the bootloader to see them.

This commit adds support for merging property descriptors and kernel
command line descriptors. Property descriptors are merged based on an
exact string match of the property key. Kernel command line descriptors
are merged based on the (non-empty) text before the first equal sign.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 18:39:55 -05:00
Andrew Gunnerson a9a6107043 CHANGELOG.md: Add entry for PR #207
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 17:46:49 -05:00
Andrew Gunnerson 3c1d5a8bb3 Merge pull request #207 from chenxiaolong/xz
compression: Add support for XZ-compressed ramdisks
2023-11-15 17:11:23 -05:00
Andrew Gunnerson 26ec8098e5 compression: Add support for XZ-compressed ramdisks
`lineage-20.0-20231109-nightly-taimen-signed.zip` is an example of an
OTA that uses XZ-compressed ramdisks.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 16:37:00 -05:00
Andrew Gunnerson 2a293147b1 Version 2.3.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:19:15 -05:00
Andrew Gunnerson aea12c8d58 CHANGELOG.md: Add entry for PR #206
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:18:43 -05:00
Andrew Gunnerson 5545b0fe1b Merge pull request #206 from chenxiaolong/promote
cli/avb: Promote insecure hash algorithms to secure ones
2023-11-14 19:17:18 -05:00
Andrew Gunnerson 9c818fb165 cli/avb: Promote insecure hash algorithms to secure ones
This is done unconditionally because there shouldn't be any real-world
device that uses AVB2 and has a kernel compiled without sha256 support.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 19:10:07 -05:00
Andrew Gunnerson d174af8969 CHANGELOG.md: Add entry for PR #205
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:54:14 -05:00
Andrew Gunnerson 2cf11094de Merge pull request #205 from chenxiaolong/cmdline
cli/avb: Update kernel cmdline descriptor for devices that use `dm=`
2023-11-14 18:52:16 -05:00
Andrew Gunnerson 6f565969b7 cli/avb: Update kernel cmdline descriptor for devices that use dm=
Older Pixel devices (and ChromiumOS) specify the dm-verity options on
the kernel command line using a custom `dm=` parameter instead of using
dm-init or a userspace helper. This commit updates the avb pack and
repack commands to automatically update the relevant kernel command line
descriptor if it exists.

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-14 18:36:07 -05:00
Andrew Gunnerson da2eb6b717 Merge pull request #204 from chenxiaolong/readme
README.md: Make sections easier to follow
2023-11-13 15:50:01 -05:00
Andrew Gunnerson 2f8d0264eb README.md: Make sections easier to follow
* Split out requirements from the warnings/caveats section and move it
  to the top. Hopefully this helps new users determine whether they can
  even use avbroot quicker.
* Split usage section into separate usage, initial install, and updates
  sections. This should hopefully make the steps much easier to follow
  without a bunch of steps being prefixed with `[Initial setup only]`.
* Explicitly state what arguments are required for Magisk, KernelSU, and
  unrooted setups instead of directing users to the advanced usage
  section.
* Don't assume that everyone will use Magisk in the other sections.
* Explicitly state the assumption that the device should already running
  the OS build that the user wants to patch during initial install.
* Add step to updates section for installing an updated Magisk or
  KernelSU app.
* Move command for building the modules from the modules section to the
  building from source section.
* Update sample error message in the clear vbmeta flags section to match
  what avbroot will actually print out.
* General rewording to try and make things clearer (especially reducing
  parenthesized parts).

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-13 03:51:17 -05:00
Andrew Gunnerson ec1fe74900 CHANGELOG.md: Add entry for PR #202
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:53:13 -05:00
Andrew Gunnerson b3b7c7b738 Merge pull request #202 from chenxiaolong/error
cli/ota: Improve error reporting when descriptor types are mismatched
2023-11-12 18:51:56 -05:00
Andrew Gunnerson 2f1ee1ae4f cli/ota: Improve error reporting when descriptor types are mismatched
Issue: #201

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-12 18:32:55 -05:00
Andrew Gunnerson cb62996b9f Version 2.3.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:49 -05:00
Andrew Gunnerson 54c06835c5 CHANGELOG.md: Add entry for PR #199
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:59:16 -05:00
Andrew Gunnerson 65a1b80da5 Merge pull request #199 from chenxiaolong/magisk_26.4
boot.rs: Bump Magisk version upper bound to 26500
2023-11-06 16:58:50 -05:00
Andrew Gunnerson 76ff1ddcda boot.rs: Bump Magisk version upper bound to 26500
There are no upstream changes that impact avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-06 16:40:14 -05:00
Andrew Gunnerson 0a4dda14cd README.md: Clarify that dmesg step should be run on the device
Fixes: #198

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-29 14:02:57 -04:00
Andrew Gunnerson bde8dbfbcd Version 2.3.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:10:51 -04:00
Andrew Gunnerson fbafbafe90 CHANGELOG.md: Fix typo: Reword -> Rework
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:03:51 -04:00
Andrew Gunnerson a14cab71e5 CHANGELOG.md: Add entry for PR #197
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 18:01:17 -04:00
Andrew Gunnerson c2f4297bb7 Merge pull request #197 from chenxiaolong/deps
Update dependencies
2023-10-27 17:59:15 -04:00
Andrew Gunnerson 9d63c9759d Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 17:44:25 -04:00
Andrew Gunnerson 73b893f3ed changelog.txt: Add entry for PR #196
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 17:42:00 -04:00
Andrew Gunnerson fd7bbf5751 Merge pull request #196 from chenxiaolong/legacy
ota: Add support for legacy OTA metadata
2023-10-27 17:36:03 -04:00
Andrew Gunnerson 409867a8e5 ota: Add support for legacy OTA metadata
Android 11 OTAs use the same `payload.bin` format, but lack the
`metadata.pb` protobuf representation of the OTA metadata. This commit
adds support for parsing the legacy plain-text `metadata` format. Like
before, the output files will still contain both the legacy and
protobuf representations.

Note that the legacy format allowed OEMs to specify arbitrary key/value
pairs. These will be discarded during patching because they cannot be
represented in the protobuf format, which is used in avbroot's internal
representation.

Issue: #195

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-27 15:15:13 -04:00
Andrew Gunnerson f96a2887df README.md: Add instructions for reverting to stock firmware
Fixes: #194

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-17 15:47:27 -04:00
Andrew Gunnerson 8140e25620 Merge pull request #193 from pascallj/patch-1
Update README.md to use new options
2023-10-15 15:55:52 -04:00
Pascal Roeleven ce05477ab1 Update README.md to use new options
During the migration to avbroot 2.0, these options have been renamed. Reflect this in the Readme.

Signed-off-by: Pascal Roeleven <dev@pascalroeleven.nl>
2023-10-15 21:36:35 +02:00
Andrew Gunnerson 1c4800c0cb CHANGELOG.md: Add entry for PR #191
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-14 17:05:53 -04:00
Andrew Gunnerson d402fdf5f1 Merge pull request #191 from chenxiaolong/sign
boot: Avoid setting signature algorithm when image is indirectly signed
2023-10-14 17:04:29 -04:00
Andrew Gunnerson 4930527598 boot: Avoid setting signature algorithm when image is indirectly signed
Previously, the AVB `algorithm_type` field was unconditionally being set
to a value that is compatible with the AVB private key. However, for
indirectly-signed boot images, the value should be set to `None`. Pixel
bootloaders accept the incorrect value, but other devices' bootloaders
might not.

Issue: #186

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 18:46:52 -04:00
Andrew Gunnerson 9b970c6d25 CHANGELOG.md: Add entry for PR #190
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 18:04:44 -04:00
Andrew Gunnerson e7fb4004cd Merge pull request #190 from chenxiaolong/sha1
avb: Allow computing insecure SHA1 hashes for verification
2023-10-13 18:03:04 -04:00
Andrew Gunnerson 2773dcddcf avb: Allow computing insecure SHA1 hashes for verification
Some partitions, like system_ext in the Pixel Experience build for
`avicii`, use SHA1 for the dm-verity hash tree. This is technically
valid, so allow the `avb verify` command to compute these hashes. SHA1
will still be rejected when creating AVB images.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 17:33:45 -04:00
Andrew Gunnerson 36765290fd CHANGELOG.md: Add entry for PR #189
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-13 17:32:33 -04:00
Andrew Gunnerson 6d8268e472 Merge pull request #189 from chenxiaolong/reopen
Move reopen functionality to a new trait
2023-10-13 17:31:15 -04:00
Andrew Gunnerson 2e8e86766b Move reopen functionality to a new trait
This is still not the ideal API, but it makes the code quite a bit more
readable since we no longer have to pass around closures everywhere that
multithreaded reads and writes to the same file are needed.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 22:09:45 -04:00
Andrew Gunnerson 8ae1c54c13 CHANGELOG.md: Add entry for PR #188
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 20:47:03 -04:00
Andrew Gunnerson 5212d4de51 Merge pull request #188 from chenxiaolong/avb_check
Add more checks to ensure AVB is actually providing security
2023-10-11 20:43:07 -04:00
Andrew Gunnerson 61bb9d6a81 Add more checks to ensure AVB is actually providing security
avbroot currently has a check to ensure that AVB signature verification
isn't completely disabled by non-zero vbmeta header flags. This is done
to ensure that the user isn't given a false sense of security when the
OS is built in an insecure way. This commit makes a few changes better
reject insecure OTAs.

The first change is extending the non-zero flags check to all vbmeta
images. Previously, only vbmeta images that needed to be updated as a
result of patching/re-signing were checked.

The second change is ensuring that there are no critical partitions
present in `payload.bin`, but missing from the vbmeta descriptors. For
example, if the vbmeta descriptor for `system.img` is missing, the
patching process will fail with a fatal error that cannot be bypassed.

Unfortunately, it's not feasible to check every partition because there
is no known device where every partition is protected by AVB. Google's
Tensor-based devices are the best and only have a single partition not
protected by AVB: `modem`. Qualcomm-based devices have many partitions
not protected by AVB. OnePlus devices have many partitions that are
checked directly (against the appended header) and thus, aren't listed
in any vbmeta image.

Due to this, only partitions that AOSP understands are checked. OEM-
specific partitions (which, security-wise, may be just as important) are
not checked.

The third change is ensuring that there's only a single root of trust
among the vbmeta images. This way, it's impossible to have, for example,
a `vbmeta_unused` image that contains all the descriptors and an empty
`vbmeta` image that's actually read by the bootloader.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-11 20:28:32 -04:00
Andrew Gunnerson 6d86fbf8a0 CHANGELOG.md: Add entry for PR #184
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-08 20:56:35 -04:00
Andrew Gunnerson 621ccc254c Merge pull request #184 from chenxiaolong/patch
Group `ota patch --help` options into sections
2023-10-08 20:55:19 -04:00
Andrew Gunnerson f41ef844e0 Group ota patch --help options into sections
There are a ton of options and simply displaying them in a giant list is
not very readable.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-07 17:04:38 -04:00
Andrew Gunnerson 42f8769e48 CHANGELOG.md: Add entry for PR #183
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-07 16:38:46 -04:00
Andrew Gunnerson d2187676d1 Merge pull request #183 from chenxiaolong/avb
avb.rs: Fix missing help text for --ignore-invalid
2023-10-07 16:36:52 -04:00
Andrew Gunnerson 3de5194c46 avb.rs: Fix missing help text for --ignore-invalid
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-06 21:05:52 -04:00
Andrew Gunnerson e0c1d4ad3a CHANGELOG.md: Add entry for PR #182
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 17:53:39 -04:00
Andrew Gunnerson c94af49009 Merge pull request #182 from chenxiaolong/payload
payload: Bump maximum manifest size to 4 MiB
2023-10-04 17:51:51 -04:00
Andrew Gunnerson 5ed7dd7dca payload: Bump maximum manifest size to 4 MiB
The Android 13 -> 14 incremental OTA has a 1.7 MiB manifest.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 15:49:20 -04:00
Andrew Gunnerson dac4e8d328 Version 2.2.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-04 14:58:01 -04:00
Andrew Gunnerson 3cc078ccfb CHANGELOG.md: Add entry for PR #181
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 21:03:32 -04:00
Andrew Gunnerson b47b51114f Merge pull request #181 from chenxiaolong/deps
Update ring to 0.17.0
2023-10-03 21:02:26 -04:00
Andrew Gunnerson 09048a00d2 Update ring to 0.17.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 20:51:16 -04:00
Andrew Gunnerson ca0e80521f README.md: Fix typo in magisk-info subcommand
Fixes: #180

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-03 14:02:57 -04:00
Andrew Gunnerson 6491106df0 Add names to all Github Actions workflows
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 19:33:58 -04:00
Andrew Gunnerson 2e8fa31207 Merge pull request #179 from chenxiaolong/ci
ci.yml: Only cancel previous runs for pull requests
2023-10-01 19:24:48 -04:00
Andrew Gunnerson d69fdaadae ci.yml: Only cancel previous runs for pull requests
Fixes: #177

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 19:09:58 -04:00
Andrew Gunnerson f0a631951a CHANGELOG.md: Add entry for PR #178
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 16:39:13 -04:00
Andrew Gunnerson 83265e584a Merge pull request #178 from chenxiaolong/cpio
cpio: Only reassign inodes when missing
2023-10-01 16:38:13 -04:00
Andrew Gunnerson ddb8911efd cpio: Only reassign inodes when missing
This way, archives with hard links can pass through `cpio unpack` and
`cpio pack`, even though there's no explicit support for hard links.

This also changes the trailer entry logic to not set an inode number.
AOSP's mkbootfs and magiskboot both start at 300000 and increment by one
for each entry, including the trailer. However, GNU cpio, bsdcpio,
busybox, and toybox all set it to 0, which makes more sense given that
it doesn't represent anything on disk.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-10-01 16:16:04 -04:00
Andrew Gunnerson 72be84b2fc CHANGELOG.md: Add entry for PR #176
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 23:19:53 -04:00
Andrew Gunnerson 81d77d8610 Merge pull request #176 from chenxiaolong/prost
Switch to prost for protobuf encoding/decoding
2023-09-30 23:18:55 -04:00
Andrew Gunnerson 4651b755ba Switch to prost for protobuf encoding/decoding
There are several things we had to work around with quick-protobuf, like
forcing no_std mode to use BTreeMaps and avoiding helper functions to
read and write non-size-delimited messages. In addition, the pb-rs code
generator doesn't support adding #[derive]s to enums and the existing
support for adding derives to structs is broken due to incorrect string
concatenation.

Prost doesn't have these limitations and bugs. I originally avoided it
because prost_build required the external `protoc` binary, but now that
the protox library exists, the code generation can be done entirely in
Rust without external tools.

Prost also fully supports adding custom attributes to structs, enums,
and fields. This will be helpful for future payload unpack and pack
commands where the payload manifest would have to be serialized to TOML.

The e2e checksums had to be updated because prost's serialized bytes on
the wire differ from quick-protobuf, despite having the same semantic
meaning. Since all checksums need to be updated anyway, the Magisk apk
and OTA images have all been updated to the latest versions.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 22:27:46 -04:00
Andrew Gunnerson 979699e3dc CHANGELOG.md: Add entry for PR #175
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 20:03:06 -04:00
Andrew Gunnerson beee2c7c90 Merge pull request #175 from chenxiaolong/boot
boot: Rename header.toml to boot.toml for consistency
2023-09-30 20:00:11 -04:00
Andrew Gunnerson 206916e544 boot: Rename header.toml to boot.toml for consistency
This also changes the serialization to use a `type` field instead of
spewing the enum variant name everywhere.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:43:04 -04:00
Andrew Gunnerson aaed44ad73 CHANGELOG.md: Add entry for PR #174
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:42:47 -04:00
Andrew Gunnerson 37b33f803a Merge pull request #174 from chenxiaolong/deps
Update dependencies
2023-09-30 19:41:30 -04:00
Andrew Gunnerson e72b55dc57 Update dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 19:23:30 -04:00
Andrew Gunnerson 5b01feb344 CHANGELOG.md: Add entry for PR #173
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 17:51:18 -04:00
Andrew Gunnerson 2f7556b530 Merge pull request #173 from chenxiaolong/cpio
Add CLI for packing and unpacking cpio archive
2023-09-30 17:49:54 -04:00
Andrew Gunnerson f25196f0a2 Add CLI for packing and unpacking cpio archive
The new commands behave exactly like the existing `avb` and `boot`
subcommands. This completes the last piece of the puzzle for exposing
useful interfaces to avbroot's internal parsers.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-30 00:50:30 -04:00
Andrew Gunnerson 2735a6558e CHANGELOG.md: Add entry for PR #172
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-29 19:10:39 -04:00
Andrew Gunnerson 0c03fceb96 Merge pull request #172 from chenxiaolong/cpio
Add streaming CPIO reader and writer
2023-09-29 19:09:20 -04:00
Andrew Gunnerson 164274eb97 Add streaming CPIO reader and writer
The Magisk boot image patcher still reads all the entries into memory
for simplicity, but everything else now uses the streaming reader.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-29 18:47:37 -04:00
Andrew Gunnerson 61129e8ec7 Version 2.1.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:53:15 -04:00
Andrew Gunnerson e1f4fa1ee8 CHANGELOG.md: Add entry for PR #171
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:51:15 -04:00
Andrew Gunnerson d15a29e9a7 Merge pull request #171 from chenxiaolong/openat
Use handle-based directory operations instead of path-based
2023-09-28 21:37:44 -04:00
Andrew Gunnerson 26a3bafc3d Use handle-based directory operations instead of path-based
This prevents avbroot writing to locations where it shouldn't due to
manipulation from external processes. For example, replacing the output
directory for `avbroot ota extract` with a symlink.

Regardless of what symlinks or other changes an external process does to
the directory, the cap-std library will use the appropriate `openat2()`
options (or equivalent) to prevent writes outside of the directory.

Fixes: #166

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 21:26:35 -04:00
Andrew Gunnerson 999e31cdf3 CHANGELOG.md: Add entry for PR #170
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:53:01 -04:00
Andrew Gunnerson ae2ebbf2aa Merge pull request #170 from chenxiaolong/payload
payload: Add size limit for protobuf manifest
2023-09-28 19:52:21 -04:00
Andrew Gunnerson fea860a382 payload: Add size limit for protobuf manifest
This is the only part of the payload that we have to read entirely into
memory. The limit is currently set to 1 MiB, which is around ~5-6x the
manifest size in all the e2e test suite's images.

Issue: #169

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:38:38 -04:00
Andrew Gunnerson 98339aee13 CHANGELOG.md: Add entry for PR #169
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:38:19 -04:00
Andrew Gunnerson f8772ef327 Merge pull request #169 from chenxiaolong/ota
ota: Don't preallocate buffer when reading OTA metadata
2023-09-28 19:33:47 -04:00
Andrew Gunnerson 7d62ddc64e ota: Don't preallocate buffer when reading OTA metadata
This way we can't run out of memory if the zip entry lists an invalid
file size that's much bigger than the actual file.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:22:17 -04:00
Andrew Gunnerson d93378cc24 CHANGELOG.md: Add entry for PR #168
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:19:14 -04:00
Andrew Gunnerson 4885e978ee Merge pull request #168 from chenxiaolong/fec
fec: Ensure FEC data size and grid size won't overflow
2023-09-28 19:18:33 -04:00
Andrew Gunnerson ecb2caf47d fec: Ensure FEC data size and grid size won't overflow
This also limits the block size to 16384. There are currently no known
images that use anything larger than 4096.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 19:07:04 -04:00
Andrew Gunnerson 525e2a409e CHANGELOG.md: Add entry for PR #167
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 18:55:07 -04:00
Andrew Gunnerson 7e89e9081a Merge pull request #167 from chenxiaolong/fuzz
Add fuzzer for FEC image parser
2023-09-28 18:54:25 -04:00
Andrew Gunnerson 2a57343094 Add fuzzer for FEC image parser
Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-28 18:41:47 -04:00
Andrew Gunnerson cd779e62cb CHANGELOG.md: Add entry for PR #165
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 23:50:08 -04:00
Andrew Gunnerson 96fb8ddc9c Merge pull request #165 from chenxiaolong/cpio
Add fuzzer for cpio
2023-09-27 23:49:23 -04:00
Andrew Gunnerson 0aba185c85 Add fuzzer for cpio
Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 23:21:04 -04:00
Andrew Gunnerson 36dcbe092f CHANGELOG.md: Add entry for PR #164
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 22:22:34 -04:00
Andrew Gunnerson aab9f5216f Merge pull request #164 from chenxiaolong/cpio
cpio: Read data by incremental reallocation after a threshold
2023-09-27 22:21:55 -04:00
Andrew Gunnerson 2a2515c5ef cpio: Read data by incremental reallocation after a threshold
This prevents avbroot from being killed for excessive memory usage due
to the entry header specifying a very large file name or file size
(larger than what's actually contained in the file).

This threshold for switching to incremental reallocation is 1 KiB for
file names and 1 MiB for file data.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 21:21:29 -04:00
Andrew Gunnerson 111ec6ff1e CHANGELOG.md: Add entry for PR #133
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:55:48 -04:00
Andrew Gunnerson cfd1dfb2eb Merge pull request #133 from chenxiaolong/deny
cargo-deny: Block executables in dependencies
2023-09-27 20:54:45 -04:00
Andrew Gunnerson d7977bab93 cargo-deny: Block executables in dependencies
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:44:22 -04:00
Andrew Gunnerson c29d78181a CHANGELOG.md: Add entry for PR #163
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 20:36:59 -04:00
Andrew Gunnerson 42a9a9a12b Merge pull request #163 from chenxiaolong/fuzz
avb: Use checked addition when computing auth/aux block size
2023-09-27 20:35:54 -04:00
Andrew Gunnerson e293331952 avb: Use checked addition when computing auth/aux block size
(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:43:15 -04:00
Andrew Gunnerson bf97444f4b CHANGELOG.md: Add entry for PR #162
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:42:48 -04:00
Andrew Gunnerson 1fa068b465 Merge pull request #162 from chenxiaolong/fuzz
bootimage: Fix potential divide-by-0 and multiplication overflow
2023-09-27 19:41:53 -04:00
Andrew Gunnerson f85b6eec46 bootimage: Fix panic when validating vendor v4 table size
The multiplication can overflow.

(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:53 -04:00
Andrew Gunnerson 07f2e30430 bootimage: Ensure page_size is never 0
Otherwise, we'll panic due to dividing by 0.

(Found by honggfuzz)

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:52 -04:00
Andrew Gunnerson 9f34198f07 CHANGELOG.md: Add entry for PR #161
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:28:23 -04:00
Andrew Gunnerson 9d8e82651e Merge pull request #161 from chenxiaolong/honggfuzz
Add initial honggfuzz fuzzing infrastructure
2023-09-27 19:27:00 -04:00
Andrew Gunnerson 5b22d7b603 Add initial honggfuzz fuzzing infrastructure
This initially includes fuzzers for the AVB and boot image parsers. The
initial input corpus are the same test files we use for the round trip
tests.

Issue: #160

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-27 19:15:42 -04:00
Andrew Gunnerson 11d19de0a1 CHANGELOG.md: Add entry for PR #158, #159
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 23:21:44 -04:00
Andrew Gunnerson fc4c2e63da Merge pull request #159 from chenxiaolong/limits
bootimage: Enforce size limits to prevent DoS
2023-09-26 23:17:40 -04:00
Andrew Gunnerson ee28a57a74 bootimage: Enforce size limits to prevent DoS
The kernel, ramdisk(s), second stage bootloader, and device tree
components are limited to 64 MiB. In practice, no device has boot
partitions larger than 64 MiB, so this limit should never be hit. The
vendor v4 bootconfig component is limited to 1 KiB, which is about ~25x
the size of the Pixel 7 Pro's bootconfig.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 23:02:24 -04:00
Andrew Gunnerson 101512b894 Merge pull request #158 from chenxiaolong/limits
avb: Enforce size limits to prevent DoS
2023-09-26 20:52:18 -04:00
Andrew Gunnerson 50913db375 avb: Enforce size limits to prevent DoS
AVB 2.0 mandates that the header is 64 KiB or less, so enforce this both
when parsing and when writing vbmeta headers. Hash trees and FEC data
are also read into memory, so enforce limits corresponding to a 4 GiB
partition image.

Since these limits are so small, this lets us remove nearly all checked
integer conversions since a direct cast will never result in truncation.

Issue: #157

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-26 20:38:19 -04:00
Andrew Gunnerson 699ea92dbf CHANGELOG.md: Add entry for PR #156
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-21 01:47:08 -04:00
Andrew Gunnerson 667b99254c Merge pull request #156 from chenxiaolong/passphrase
Consolidate logic for determining passphrase source priority
2023-09-21 01:45:46 -04:00
Andrew Gunnerson e41814a425 Consolidate logic for determining passphrase source priority
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-20 23:35:25 -04:00
Andrew Gunnerson dff2fff028 Version 2.1.0
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:56:58 -04:00
Andrew Gunnerson 67bca2e58c CHANGELOG.md: Add entry for PR #153
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:54:51 -04:00
Andrew Gunnerson 1f08cb6d40 Merge pull request #154 from chenxiaolong/changelog
Shorten changelog links
2023-09-19 23:53:28 -04:00
Andrew Gunnerson ed68f09cb9 Shorten changelog links
The longer PR links have weird line wrapping behavior on the Github web
UI, making it more difficult to read.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 23:09:23 -04:00
Andrew Gunnerson 64c11c2dc3 Merge pull request #153 from chenxiaolong/error
Disambiguate some error fields and add more error context
2023-09-19 23:08:15 -04:00
Andrew Gunnerson 5115134b5a Disambiguate some error fields and add more error context
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:54:43 -04:00
Andrew Gunnerson a2d81d6bfe CHANGELOG.md: Add entry for PR #152
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:53:14 -04:00
Andrew Gunnerson c5709b90f6 Merge pull request #152 from chenxiaolong/avb
Add CLI for packing and unpacking AVB images
2023-09-19 22:49:07 -04:00
Andrew Gunnerson 2928b96f1f Add CLI for packing and unpacking AVB images
This supports all of the `format::avb` functionality, including
repairing dm-verity images. When packing images, all offsets, sizes,
digests, hash trees, FEC data, signatures, etc. are automatically
recomputed. The goal is that the user can edit any partition image
without needing to think about AVB at all.

A new `--repair` option has also been added to the `avb verify` command
to automatically attempt to all dm-verity images.

As a side effect of the changes, RSA2048 keys are now supported.

This commit also removes the `Clone` implementation from the `PSeekFile`
and `SharedCursor` types. These types use the same underlying file or
memory buffer when cloned, allowing parallel threads to read and write
files using the normal `Read`/`Write` APIs. The intention is that
cloning one of these instances would behave as if a new file handle to
the same file was opened. However, the file offset was also copied
instead of being set to 0, which is confusing. A new `reopen()` method
has been added that explicitly sets the initial offset to 0.

Closes: #148

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-19 22:16:03 -04:00
Andrew Gunnerson 5638a8bb4d CHANGELOG.md: Add entry for PR #150
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 21:27:06 -04:00
Andrew Gunnerson a582d0b3e6 Merge pull request #150 from chenxiaolong/escape
Switch to bstr for escaping mostly UTF-8 binary data
2023-09-18 21:25:49 -04:00
Andrew Gunnerson 1d08ffa527 Switch to bstr for escaping mostly UTF-8 binary data
bstr is Unicode-aware and won't split multi-byte codepoints into
multiple `\x##`.

This commit also adds an `escape` module for use with serde so we can
serialize mostly UTF-8 binary data into human-readable files, like TOML.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 21:10:47 -04:00
Andrew Gunnerson a95ce56246 CHANGELOG.md: Add entry for PR #149
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 17:18:26 -04:00
Andrew Gunnerson ca6ce1fb37 Merge pull request #149 from chenxiaolong/stderr
Print status and warning messages to stderr
2023-09-18 17:16:56 -04:00
Andrew Gunnerson d7a46dcaaf Print status and warning messages to stderr
Also, fix warning!() to allow 0 arguments to match status!() and
println!().

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 16:48:25 -04:00
Andrew Gunnerson a4978aaac0 CHANGELOG.md: Add entry for PR #146
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 16:39:13 -04:00
Andrew Gunnerson 6ca4023b0d Merge pull request #146 from chenxiaolong/fec
Add support for AVB FEC
2023-09-18 16:37:17 -04:00
Andrew Gunnerson acc3308d37 Hash tree is two words
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:58:38 -04:00
Andrew Gunnerson 9ca3a690df Verify FEC data in HashtreeDescriptor::verify()
This also fixes the wrong error being reported when the hash tree does
not match and adds additional checks to ensure that there are no gaps
between the image data, hash tree, and FEC data.

Issue: #145

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:57:40 -04:00
Andrew Gunnerson 335c04711e Add support for AVB FEC
This commit adds initial support for the FEC (forward error correction)
used by dm-verity. The new `avbroot fec` commands operate on FEC data
stored in AOSP's standalone FEC file format. It is compatible with
AOSP's `fec` tool.

Issue: #145

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 15:57:09 -04:00
Andrew Gunnerson fb7be77480 CHANGELOG.md: Add entry for PR #147
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 00:27:56 -04:00
Andrew Gunnerson e683fce4ab Merge pull request #147 from chenxiaolong/atomicbool
Stopping spewing Arc everywhere for the cancel signal
2023-09-18 00:24:29 -04:00
Andrew Gunnerson 7493cdc5fa Stopping spewing Arc everywhere for the cancel signal
There's no reason all these functions need to care about the ownership
of the cancel signal.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-18 00:12:09 -04:00
Andrew Gunnerson 8f49f49154 Merge pull request #143 from AgentOak/additional-readme-notes
README.md: Add additional notes about lost keys and OTA updaters
2023-09-12 16:22:19 -04:00
Jan Erik Petersen bc6fdd8513 README.md: Add additional notes about lost keys and OTA updaters 2023-09-12 21:09:26 +02:00
Andrew Gunnerson f6c86473c3 Merge pull request #141 from chenxiaolong/sync
e2e: Switch to attohttpc for HTTP downloads
2023-09-11 18:40:32 -04:00
Andrew Gunnerson d0d51f562c e2e: Switch to attohttpc for HTTP downloads
We don't use async for anything else, so switching to a synchronous HTTP
library lets us get rid of the entire async ecosystem and removes 47
packages from e2e's dependency tree.

The only downside is that ^C worst case takes 5 seconds (TCP connect and
read timeouts) or whatever the OS's DNS lookup timeout is. That's good
enough for a test suite.

Performance wise, e2e still easily saturates a gigabit internet
connection.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-09 23:34:48 -04:00
Andrew Gunnerson f31c4134e4 Version 2.0.3
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 14:01:00 -04:00
Andrew Gunnerson dd0d7f334b CHANGELOG.md: Add entry for PR #140
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:59:43 -04:00
Andrew Gunnerson e4a2a4c24c Merge pull request #140 from chenxiaolong/version
Add `--version` option
2023-09-07 13:58:50 -04:00
Andrew Gunnerson cc9aab197c CHANGELOG.md: Add entry for PR #139
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:58:23 -04:00
Andrew Gunnerson 92da30f53c Merge pull request #139 from chenxiaolong/xz
Upgrade xz to 5.4.4 and enable all encoders and decoders
2023-09-07 13:52:14 -04:00
Andrew Gunnerson 26142b0271 Upgrade xz to 5.4.4 and enable all encoders and decoders
When using the xz2/static feature, the xz2 crate uses a bundled version
of xz 5.2 and doesn't enable all of the available encoders and decoders.
This prevents certain payload data from being decompressed.

Fixes: #138

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:40:34 -04:00
Andrew Gunnerson c8df3286df Add --version option
Issue: #138

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-07 13:37:24 -04:00
Andrew Gunnerson 65979beff4 Version 2.0.2
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:16:01 -04:00
Andrew Gunnerson 47cf015f15 CHANGELOG.md: Add entry for PR #137
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:15:19 -04:00
Andrew Gunnerson e40e036159 Merge pull request #137 from chenxiaolong/anyhow
Remove unnecessary use of anyhow macro
2023-09-06 23:13:04 -04:00
Andrew Gunnerson 0d2a158ead Remove unnecessary use of anyhow macro
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 23:04:18 -04:00
Andrew Gunnerson ddabb19b4a CHANGELOG.md: Add entry for PR #136
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 22:51:58 -04:00
Andrew Gunnerson 3c05eb300c Merge pull request #136 from chenxiaolong/offset
PayloadWriter: Only set data_offset for operations that reference blobs
2023-09-06 22:50:31 -04:00
Andrew Gunnerson 2ffb1dfdbd PayloadWriter: Only set data_offset for operations that reference blobs
This fixes `data_offset` being set for `ZERO` and `DISCARD` operations,
which prevents some images (eg. `ossi`) from being flashed due to
update_engine's strict field validation.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 22:40:15 -04:00
Andrew Gunnerson f93dc55f70 CHANGELOG.md: Add entry for PR #135
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 20:05:41 -04:00
Andrew Gunnerson 393e65f6b4 Merge pull request #135 from chenxiaolong/full
Move full OTA check to patch/extract subcommand function
2023-09-06 20:03:47 -04:00
Andrew Gunnerson 3f86a67038 Move full OTA check to patch/extract subcommand function
This way, the payload-related functions can be used to parse incremental
OTA files.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-06 19:51:48 -04:00
Andrew Gunnerson 4337170f57 Version 2.0.1
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 17:11:28 -04:00
Andrew Gunnerson a73365592f CHANGELOG.md: Add entry for PR #132
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 16:41:28 -04:00
Andrew Gunnerson 0018e8a5ba Merge pull request #132 from chenxiaolong/magisk
boot.rs: Allow Magisk 263xx
2023-09-04 16:39:14 -04:00
Andrew Gunnerson adf0014da2 boot.rs: Allow Magisk 263xx
There are no changes that break avbroot.

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-09-04 16:30:39 -04:00
90 changed files with 10752 additions and 4905 deletions
@@ -1,48 +0,0 @@
name: Preload img cache
inputs:
cache-key-prefix:
description: 'Device cache-key prefix'
required: true
device:
description: 'Device name'
required: true
runs:
using: "composite"
steps:
- uses: actions/cache@v3
id: cache-img
with:
key: ${{ inputs.cache-key-prefix }}${{ inputs.device }}
# Make sure any changes to path are also reflected in ci.yml setup
path: e2e/files/${{ inputs.device }}-sparse.tar
- if: ${{ steps.cache-img.outputs.cache-hit }}
name: Extracting image from sparse archive
shell: sh
working-directory: e2e/files
run: tar -xf ${{ inputs.device }}-sparse.tar
- name: Restore e2e executable
if: ${{ ! steps.cache-img.outputs.cache-hit }}
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Downloading device image for ${{ inputs.device }}
if: ${{ ! steps.cache-img.outputs.cache-hit }}
shell: sh
working-directory: e2e
run: ../target/release/e2e download --stripped -d ${{ inputs.device }}
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
name: Creating sparse archive from image
shell: sh
working-directory: e2e/files
run: |
tar --sparse -cf ${{ inputs.device }}-sparse.tar \
${{ inputs.device }}/*.stripped
@@ -1,31 +0,0 @@
name: Preload Magisk
inputs:
cache-key:
description: 'Magisk cache-key'
required: true
runs:
using: "composite"
steps:
- uses: actions/cache@v3
id: cache-magisk
with:
key: ${{ inputs.cache-key }}
# Make sure any changes to path are also reflected in ci.yml setup
path: e2e/files/magisk
- name: Restore e2e executable
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Downloading Magisk
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
shell: sh
working-directory: e2e
run: ../target/release/e2e download --magisk
+37 -149
View File
@@ -5,9 +5,10 @@ on:
- master
pull_request:
# This allows a subsequently queued workflow run to interrupt previous runs
# This allows a subsequently queued workflow run to interrupt previous runs, but
# only in pull requests.
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
group: '${{ github.workflow }} @ ${{ github.head_ref || github.sha }}'
cancel-in-progress: true
jobs:
@@ -15,7 +16,7 @@ jobs:
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -C strip=symbols
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
strategy:
fail-fast: false
matrix:
@@ -39,28 +40,42 @@ jobs:
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
>> "${GITHUB_OUTPUT}"
- name: Get Rust LLVM target triple
- name: Get Rust target triple
id: get_target
shell: bash
env:
RUSTC_BOOTSTRAP: '1'
run: |
echo -n 'name=' >> "${GITHUB_OUTPUT}"
rustc -Z unstable-options --print target-spec-json \
| jq -r '."llvm-target"' \
>> "${GITHUB_OUTPUT}"
rustc -vV | sed -n 's|host: ||p' >> "${GITHUB_OUTPUT}"
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
- name: Clippy
run: cargo clippy --release --workspace --features static
shell: bash
run: |
cargo clippy --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
- name: Build
run: cargo build --release --workspace --features static
shell: bash
run: |
cargo build --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
- name: Tests
run: cargo test --release --workspace --features static
shell: bash
run: |
cargo test --release --workspace --features static \
--target ${{ steps.get_target.outputs.name }}
- name: End to end tests
shell: bash
run: |
cargo run --release -p e2e --features static \
--target ${{ steps.get_target.outputs.name }} \
-- test -a -c e2e/e2e.toml
- name: Archive documentation
uses: actions/upload-artifact@v3
@@ -70,148 +85,21 @@ jobs:
LICENSE
README.md
# Due to https://github.com/rust-lang/rust/issues/78210, we have to use
# the --target option, which puts all output files in a different path.
# Symlink that path to the normal output directory so that we don't need
# to specify the Rust triple everywhere.
- name: Symlink target directory
shell: bash
run: |
rm -rf target/output
ln -s ${{ steps.get_target.outputs.name }}/release target/output
# This is separate so we can have a flat directory structure.
- name: Archive executable
uses: actions/upload-artifact@v3
with:
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
path: |
target/release/avbroot
target/release/avbroot.exe
- name: Cache e2e executable
uses: actions/cache@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
path: |
target/release/e2e
target/release/e2e.exe
setup:
name: Prepare workflow data
runs-on: ubuntu-latest
needs: build
timeout-minutes: 2
outputs:
config-path: ${{ steps.load-config.outputs.config-path }}
device-list: ${{ steps.load-config.outputs.device-list }}
magisk-key: ${{ steps.cache-keys.outputs.magisk-key }}
img-key-prefix: ${{ steps.cache-keys.outputs.img-key-prefix }}
img-hit: ${{ steps.get-img-cache.outputs.cache-matched-key }}
steps:
- uses: actions/checkout@v3
- name: Restore e2e executable
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
- name: Loading test config
id: load-config
working-directory: e2e
run: |
echo 'config-path=e2e/e2e.toml' >> "${GITHUB_OUTPUT}"
echo -n 'device-list=' >> "${GITHUB_OUTPUT}"
../target/release/e2e list \
| jq -cnR '[inputs | select(length > 0)]' \
>> "${GITHUB_OUTPUT}"
- name: Generating cache keys
id: cache-keys
run: |
{
echo "img-key-prefix=img-${{ hashFiles(steps.load-config.outputs.config-path) }}-"; \
echo "magisk-key=magisk-${{ hashFiles(steps.load-config.outputs.config-path) }}";
} >> $GITHUB_OUTPUT
- name: Checking for cached device images
id: get-img-cache
uses: actions/cache/restore@v3
with:
key: ${{ steps.cache-keys.outputs.img-key-prefix }}
lookup-only: true
path: |
e2e/files/${{ fromJSON(steps.load-config.outputs.device-list)[0] }}-sparse.tar
- name: Checking for cached magisk apk
id: get-magisk-cache
uses: actions/cache/restore@v3
with:
key: ${{ steps.cache-keys.outputs.magisk-key }}
lookup-only: true
path: e2e/files/magisk
- name: Preloading Magisk cache
if: ${{ ! steps.get-magisk-cache.outputs.cache-hit }}
uses: ./.github/actions/preload-magisk-cache
with:
cache-key: ${{ steps.cache-keys.outputs.magisk-key }}
preload-img:
name: Preload device images
runs-on: ubuntu-latest
needs: setup
timeout-minutes: 5
# Assume that preloading always succesfully cached all images before.
# If for some reason only some got cached, on the first run, the cache will not be preloaded
# which will result in some being downloaded multiple times when running the tests.
if: ${{ ! needs.setup.outputs.img-hit }}
strategy:
matrix:
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
steps:
- uses: actions/checkout@v3
- name: Preloading image cache
uses: ./.github/actions/preload-img-cache
with:
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
device: ${{ matrix.device }}
tests:
name: Run test for ${{ matrix.device }} on ${{ matrix.os }}
runs-on: ubuntu-latest
needs:
- setup
- preload-img
timeout-minutes: 10
# Continue on skipped but not on failures or cancels
if: ${{ always() && ! failure() && ! cancelled() }}
strategy:
matrix:
device: ${{ fromJSON(needs.setup.outputs.device-list) }}
os:
- ubuntu-latest
- windows-latest
- macos-latest
steps:
- uses: actions/checkout@v3
- name: Restoring Magisk cache
uses: ./.github/actions/preload-magisk-cache
with:
cache-key: ${{ needs.setup.outputs.magisk-key }}
- name: Restoring image cache
uses: ./.github/actions/preload-img-cache
with:
cache-key-prefix: ${{ needs.setup.outputs.img-key-prefix }}
device: ${{ matrix.device }}
- name: Restore e2e executable
uses: actions/cache/restore@v3
with:
key: e2e-${{ github.sha }}-${{ runner.os }}
fail-on-cache-miss: true
path: |
target/release/e2e
target/release/e2e.exe
# Finally run tests
- name: Run test for ${{ matrix.device }}
working-directory: e2e
run: ../target/release/e2e test --stripped -d ${{ matrix.device }}
target/output/avbroot
target/output/avbroot.exe
+1
View File
@@ -1,4 +1,5 @@
---
name: cargo-deny
on:
push:
branches:
+1
View File
@@ -1,4 +1,5 @@
---
name: Modules
on:
push:
branches:
+1
View File
@@ -1,4 +1,5 @@
---
name: Github Release
on:
push:
# Uncomment to test against a branch
+215 -3
View File
@@ -1,15 +1,227 @@
<!--
When adding new changelog entries, use [Issue #0] to link to issues and
[PR #0 @user] to link to pull requests. Then run:
[PR #0] to link to pull requests. Then run:
cargo xtask update-changelog
to update the actual links at the bottom of the file.
-->
### Version 3.0.0
Happy New Year! This release brings two major changes:
1. The OTA certificates (`otacerts.zip`) in the system partition are now patched. The `clearotacerts` module from avbroot (or the `customotacerts` module from Custota) are no longer needed and can be safely uninstalled.
This makes it possible to use Pixel's new Repair Mode safely. To do so, follow the instructions in the [documentation here](./README.md#repair-mode).
2. Autodetection for boot partitions is now significantly more reliable. For KernelSU users or folks who have more obscure devices, the `--boot-partition` option is no longer required (and is now ignored).
Full list of changes:
* Add support for AVB 2.0 format 1.3.0 (for Android 15) ([PR #210])
* Add new `avbroot key decode-avb` command for converting AVB-encoded public keys to the standard PKCS8-encoded format ([PR #219])
* Improve autodetection of boot images ([Issue #218], [PR #221], [PR #237])
* Build precompiled executables as statically linked executables ([Issue #222], [PR #224], [PR #227])
* Limit critical partition check to bootloader-verified partitions ([Issue #223], [PR #226])
* Improve patching performance by spliiting new partition images into chunks and compressing them in parallel ([PR #228])
* Also verify whole-partition hashes when running `avbroot ota verify` ([PR #229])
* Add support for patching `otacerts.zip` on the system partition ([Issue #225], [PR #240], [PR #244])
* Document how to use Repair Mode safely ([Issue #216], [PR #243])
Behind-the-scenes changes:
* Fix lint warnings introduced in Rust 1.74.0 ([PR #211])
* Temporarily silence [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071) warning in cargo-deny ([PR #214])
* Add support for partially updating FEC data ([PR #230], [PR #231], [PR #234])
* Fix hash tree calculation for images smaller than one block ([PR #232])
* Refactor hash tree code and add tests, CLI commands, and support for partial updates ([PR #233])
* Generate mock OTAs to use for end-to-end tests ([PR #241])
* Update all dependencies ([PR #245])
### Version 2.3.3
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
### Version 2.3.2
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
* Automatically update legacy `dm=` kernel command line descriptor when packing AVB images ([Issue #203], [PR #205])
* Automatically promote insecure hash algorithms (eg. sha1) to sha256 when packing AVB images ([Issue #203], [PR #206])
### Version 2.3.1
* Mark Magisk 264xx as supported ([PR #199])
### Version 2.3.0
* Fix missing `--help` text for `avbroot avb unpack`'s `--ignore-invalid` option ([PR #183])
* Group `avbroot ota patch --help` output into more readable sections ([PR #184])
* Add more checks to ensure that the OTA has a secure AVB setup ([PR #188])
* OTAs with blatantly insecure or missing AVB configuration are now more likely to be rejected by avbroot to avoid providing a false sense of security.
* Allow `avbroot avb verify` and `avbroot ota verify` to work for dm-verity partitions that use insecure SHA1 hashes ([PR #190])
* Add support for legacy Android 11 OTAs ([Discussion #195], [PR #196])
Behind-the-scenes changes:
* Bump maximum payload manifest size to 4 MiB ([PR #182])
* Rework file handle reopen functionality to use traits instead of callbacks ([PR #189])
* Don't set signature algorithm field for indirectly signed boot images ([PR #191])
* Update dependencies ([PR #197])
### Version 2.2.0
It's Android 14 release day! All versions of avbroot, including the old Python version, are compatible with Android 14 OTAs.
Changes:
* Add new unpack and pack commands for cpio archives (ramdisks) ([PR #173], [PR #178])
* Rename `header.toml` to `boot.toml` for the boot image unpack and pack commands ([PR #175])
* Also changes the file format a bit to make it more readable.
Behind-the-scenes changes:
* Add streaming CPIO reader and writer ([PR #172])
* Update dependencies ([PR #174], [PR #181])
* Switch to prost for protobuf encoding/decoding ([PR #176])
### Version 2.1.1
This release is all about hardening avbroot against untrusted (or corrupted) inputs. While all of avbroot's parsers are memory-safe, it's still possible for crashes to occur due to logic issues like allocating too much memory or dividing by zero. With this release, most of these potential issues have been fixed and fuzz tests have been added to help find more of these situations.
On the filesystem side of things, it is no longer possible for a nefarious program to cause avbroot to write to unintended locations by eg. swapping out an output directory or temp directory with a symlink while it is running.
Behind-the-scenes changes:
* Consolidate logic for handling `--pass-file` and `--pass-env-var` ([PR #156])
* cargo-deny: Block executables in dependencies ([PR #133])
* Implement size limits for parsers to prevent allocating too much memory ([Issue #157], [PR #158], [PR #159], [PR #164], [PR #168], [PR #169], [PR #170])
* Add fuzzers to help catch panics/crashes ([Issue #160], [PR #161], [PR #162], [PR #163], [PR #165], [PR #167])
* Use handle-based directory operations instead of path-based directory operations ([Issue #166], [PR #171])
### Version 2.1.0
* Add support for dm-verify FEC (forward error correction) ([Issue #145], [PR #146])
* `ota verify` and `avb verify` will now check the FEC data.
* Print status and warning messages to stderr ([PR #149])
* Add new `avb unpack`, `avb pack`, and `avb repack` commands for AVB images ([Issue #144], [Issue #148], [PR #152])
* `avb verify` now optionally accepts `--repair` to fix corrupted dm-verity images.
Behind-the-scenes changes:
* Remove unnecessary use of `Arc` ([PR #147])
* Use bstr crate to escape mostly UTF-8 binary data ([PR #150])
* Improve error fields and error contest ([PR #153])
### Version 2.0.3
* Upgrade xz version in precompiled binaries ([Issue #138], [PR #139])
* This fixes the `ota extract` and `ota verify` commands in some multithreaded situations.
* Add `--version` option to print out avbroot's version ([Issue #138], [PR #140])
### Version 2.0.2
* Fix `data_offset` being set for payload operations that don't need it ([PR #136])
* This fixes patched stock OnePlus images from being rejected when flashing.
Behind-the-scenes changes:
* Move full OTA check to CLI functions to allow library functions to parse delta OTAs ([PR #135])
* Remove unnecessary use of `anyhow` macro ([PR #137])
### Version 2.0.1
* Add support for Magisk 263xx ([PR #132])
### Version 2.0.0
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130 @chenxiaolong])
* Initial Rust release. The old Python implementation can be found in the `python` branch. ([PR #130])
<!-- Do not manually edit the lines below. Use `cargo xtask update-changelog` to regenerate. -->
[PR #130 @chenxiaolong]: https://github.com/chenxiaolong/avbroot/pull/130
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
[Issue #138]: https://github.com/chenxiaolong/avbroot/issues/138
[Issue #144]: https://github.com/chenxiaolong/avbroot/issues/144
[Issue #145]: https://github.com/chenxiaolong/avbroot/issues/145
[Issue #148]: https://github.com/chenxiaolong/avbroot/issues/148
[Issue #157]: https://github.com/chenxiaolong/avbroot/issues/157
[Issue #160]: https://github.com/chenxiaolong/avbroot/issues/160
[Issue #166]: https://github.com/chenxiaolong/avbroot/issues/166
[Issue #201]: https://github.com/chenxiaolong/avbroot/issues/201
[Issue #203]: https://github.com/chenxiaolong/avbroot/issues/203
[Issue #216]: https://github.com/chenxiaolong/avbroot/issues/216
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
[Issue #225]: https://github.com/chenxiaolong/avbroot/issues/225
[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
[PR #135]: https://github.com/chenxiaolong/avbroot/pull/135
[PR #136]: https://github.com/chenxiaolong/avbroot/pull/136
[PR #137]: https://github.com/chenxiaolong/avbroot/pull/137
[PR #139]: https://github.com/chenxiaolong/avbroot/pull/139
[PR #140]: https://github.com/chenxiaolong/avbroot/pull/140
[PR #146]: https://github.com/chenxiaolong/avbroot/pull/146
[PR #147]: https://github.com/chenxiaolong/avbroot/pull/147
[PR #149]: https://github.com/chenxiaolong/avbroot/pull/149
[PR #150]: https://github.com/chenxiaolong/avbroot/pull/150
[PR #152]: https://github.com/chenxiaolong/avbroot/pull/152
[PR #153]: https://github.com/chenxiaolong/avbroot/pull/153
[PR #156]: https://github.com/chenxiaolong/avbroot/pull/156
[PR #158]: https://github.com/chenxiaolong/avbroot/pull/158
[PR #159]: https://github.com/chenxiaolong/avbroot/pull/159
[PR #161]: https://github.com/chenxiaolong/avbroot/pull/161
[PR #162]: https://github.com/chenxiaolong/avbroot/pull/162
[PR #163]: https://github.com/chenxiaolong/avbroot/pull/163
[PR #164]: https://github.com/chenxiaolong/avbroot/pull/164
[PR #165]: https://github.com/chenxiaolong/avbroot/pull/165
[PR #167]: https://github.com/chenxiaolong/avbroot/pull/167
[PR #168]: https://github.com/chenxiaolong/avbroot/pull/168
[PR #169]: https://github.com/chenxiaolong/avbroot/pull/169
[PR #170]: https://github.com/chenxiaolong/avbroot/pull/170
[PR #171]: https://github.com/chenxiaolong/avbroot/pull/171
[PR #172]: https://github.com/chenxiaolong/avbroot/pull/172
[PR #173]: https://github.com/chenxiaolong/avbroot/pull/173
[PR #174]: https://github.com/chenxiaolong/avbroot/pull/174
[PR #175]: https://github.com/chenxiaolong/avbroot/pull/175
[PR #176]: https://github.com/chenxiaolong/avbroot/pull/176
[PR #178]: https://github.com/chenxiaolong/avbroot/pull/178
[PR #181]: https://github.com/chenxiaolong/avbroot/pull/181
[PR #182]: https://github.com/chenxiaolong/avbroot/pull/182
[PR #183]: https://github.com/chenxiaolong/avbroot/pull/183
[PR #184]: https://github.com/chenxiaolong/avbroot/pull/184
[PR #188]: https://github.com/chenxiaolong/avbroot/pull/188
[PR #189]: https://github.com/chenxiaolong/avbroot/pull/189
[PR #190]: https://github.com/chenxiaolong/avbroot/pull/190
[PR #191]: https://github.com/chenxiaolong/avbroot/pull/191
[PR #196]: https://github.com/chenxiaolong/avbroot/pull/196
[PR #197]: https://github.com/chenxiaolong/avbroot/pull/197
[PR #199]: https://github.com/chenxiaolong/avbroot/pull/199
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
[PR #220]: https://github.com/chenxiaolong/avbroot/pull/220
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
[PR #228]: https://github.com/chenxiaolong/avbroot/pull/228
[PR #229]: https://github.com/chenxiaolong/avbroot/pull/229
[PR #230]: https://github.com/chenxiaolong/avbroot/pull/230
[PR #231]: https://github.com/chenxiaolong/avbroot/pull/231
[PR #232]: https://github.com/chenxiaolong/avbroot/pull/232
[PR #233]: https://github.com/chenxiaolong/avbroot/pull/233
[PR #234]: https://github.com/chenxiaolong/avbroot/pull/234
[PR #237]: https://github.com/chenxiaolong/avbroot/pull/237
[PR #240]: https://github.com/chenxiaolong/avbroot/pull/240
[PR #241]: https://github.com/chenxiaolong/avbroot/pull/241
[PR #243]: https://github.com/chenxiaolong/avbroot/pull/243
[PR #244]: https://github.com/chenxiaolong/avbroot/pull/244
[PR #245]: https://github.com/chenxiaolong/avbroot/pull/245
Generated
+683 -966
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,10 +1,10 @@
[workspace]
default-members = ["avbroot"]
members = ["avbroot", "e2e", "xtask"]
members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "2.0.0"
version = "3.0.0"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+180 -13
View File
@@ -6,10 +6,46 @@ Note that while avbroot maintains a stable command line interface for the patchi
## `avbroot avb`
### Unpacking an AVB image
```bash
avbroot avb unpack -i <input AVB image>
```
This subcommand unpacks the vbmeta header and footer into `avb.toml`. If a footer is present, then the corresponding raw partition image is extracted into `raw.img`. Root vbmeta images (eg. `vbmeta` and `vbmeta_vendor`) do not have footers, while appended vbmeta images (eg. `boot` and `system`) do.
The vbmeta descriptor digests are validated during unpacking. For dm-verity images, if the image is corrupt, avbroot will attempt to use the FEC data to repair the file. If there is unrepairable data corruption, the command will fail, though the corrupted `raw.img` will still be fully written. If, for whatever reason, a successful exit status of 0 is needed even for corrupted files, use `--ignore-invalid`.
### Packing an AVB image
```bash
avbroot avb pack -o <output AVB image> [--key <AVB private key>]
```
This subcommand packs a new AVB image from the `avb.toml` file and, for appended vbmeta images, the `raw.img` file.
* If the original image was signed and the new data is unmodified, then the original signature is used as-is. (This means just unpacking and packing an image will always result in a byte-for-byte identical file.)
* If the original image was signed and the new data is modified, then the newly packed image will be signed with the `--key`.
* If the original image was not signed, then the newly packed image is not signed.
* To force an image to be signed, use `--key <path> --force`.
* To force an image to be unsigned, use `--force` without specifying `--key`.
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
### Repacking an AVB image
```bash
avbroot avb repack -i <input AVB image> -o <output AVB image>
```
This subcommand is equivalent to `avbroot avb unpack` followed by `avbroot avb pack`, except it doesn't need to write any intermediate files to disk.
This is useful for repairing a dm-verify image or for re-signing any image with a specific key.
### Showing vbmeta header and footer information
```bash
avbroot avb dump -i <image>
avbroot avb info -i <image>
```
This subcommand shows all of the vbmeta header and footer fields. `vbmeta` partition images will only have a header, while partitions with actual data (eg. boot images) will have both a header and a footer.
@@ -20,10 +56,12 @@ This subcommand shows all of the vbmeta header and footer fields. `vbmeta` parti
avbroot avb verify -i <root vbmeta image> -p <public key>
```
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hashtree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
This subcommand verifies the vbmeta header signature and the hashes for all vbmeta descriptors (including hash tree descriptors). If the vbmeta image has a chain descriptor for another partition, that partition image will be verified as well (recursively). All partitions are expected to be in the same directory as the vbmeta image being verified.
If `-p` is omitted, the signatures and hashes are checked only for validity, not that they are trusted.
By default, this command will not write to any file and fails if an image is corrupt or invalid. To attempt to repair corrupted dm-verity images, pass in `--repair`.
## `avbroot boot`
### Unpacking a boot image
@@ -32,7 +70,7 @@ If `-p` is omitted, the signatures and hashes are checked only for validity, not
avbroot boot unpack -i <input boot image>
```
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `header.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
This subcommand unpacks all of the components of the boot image into the current directory by default (see `--help`). The header fields are saved to `boot.toml` and each blob section is saved to a separate file. Each blob is written to disk as-is, without decompression.
### Packing a boot image
@@ -58,26 +96,155 @@ avbroot boot info -i <input boot image>
All of the `boot` subcommands show the boot image information. This specific subcommand just does it without performing any other operation. To show avbroot's internal representation of the information, pass in `-d`.
## `avbroot ramdisk`
## `avbroot cpio`
### Dumping a cpio archive
### Unpacking a cpio archive
```bash
avbroot ramdisk dump -i <cpio archive>
avbroot cpio unpack -i <input cpio archive>
```
This subcommand dumps all information about a cpio archive to stdout. This includes the compression format, all header fields (including the trailer entry), and all data. If an entry's data can be decoded as UTF-8, then it is printed out as text. Otherwise, the binary data is printed out `\x##`-encoded for non-ASCII bytes. The escape-encoded data is truncated to 512 bytes by default to avoid outputting too much data, but this behavior can be disabled with `--no-truncate`.
This subcommand unpacks the cpio archive. The list of file entries and their metadata, like permissions, are written to `cpio.toml`. The contents of regular files are extracted to `cpio_tree/`. Other file types, like symlinks and block devices, are not extracted at all. Their information only exists in the TOML file.
The files inside the tree will have default permissions, ownership, and modification timestamps. This metadata exists only inside the TOML file in order to ensure that the behavior is the same across all platforms.
Both uncompressed archives and compressed archives (gzip or legacy lz4) are supported.
### Packing a cpio archive
```bash
avbroot cpio pack -o <output cpio archive>
```
This subcommand packs a new cpio archive from the file entries listed in `cpio.toml` and the file contents in `cpio_tree/`. Note that **only entries listed in the TOML file are packed**. Extra files inside the tree are silently ignored.
The files are packed in the order listed in the TOML file. When packing ramdisks specifically, it's important to ensure that files are listed after their parent directories (which also **must** exist). Otherwise, the kernel will ignore them. As long as the file paths don't contain anything weird (eg. `a//b` or `a/./b`), sorting the entires with `--sort` should do the trick.
The new archive will be written in the same format (compressed or uncompressed) as the original archive.
### Repacking a cpio archive
```bash
avbroot ramdisk repack -i <input cpio archive> -o <output cpio archive>
avbroot cpio repack -i <input cpio archive> -o <output cpio archive>
```
This subcommand repacks a cpio archive, including recompression if needed. This is useful for roundtrip testing of avbroot's cpio parser and compression handling. The uncompressed output should be identical to the uncompressed input, except:
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
* files are sorted by name
* inodes are reassigned, starting from 300000
* there is no excess padding at the end of the file
### Showing information about a cpio archive
The compressed output may differ from what other tools produce due to differences in compression levels and header metadata. avbroot avoids specifying header information where possible (eg. gzip timestamp) for reproducibility.
```bash
avbroot cpio info -i <input cpio archive>
```
All of the `cpio` subcommands show details about all the entries in the archive. This specific subcommand just does it without performing any other operation.
## `avbroot fec`
This set of commands is for working with dm-verity FEC (forward error correction) data. The FEC data allows small errors in partition data to be corrected. This increases reliability of the system because when dm-verity encounters data that doesn't match the expected checksum, it will either trigger a kernel panic or reboot the system.
The same raw FEC data can be stored in several ways:
* cryptsetup's `veritysetup` does not use any file format at all. It must be told the FEC location and parameters using the `--fec-*` options.
* AOSP's AVB 2.0 stores the FEC data inside the partition as `[Partition data][Hash tree][FEC data]`. The location and parameters are stored in the vbmeta hash tree descriptors.
* AOSP's `fec` tool stores the FEC data in a standalone file with a header containing the FEC parameters.
The `avbroot fec` commands use AOSP's standalone FEC file format.
The FEC data is not generated from a sequential read of the input file, but rather from an interleaved read. If the input file's offsets are visualized as a 2D table:
```
| 0 1 2 3 ... 4095 |
| 4096 4097 4098 4099 ... 8191 |
| 8192 8193 8194 8195 ... 12287 |
| .... .... .... .... ... ..... |
```
then the file access pattern can be thought of as being column-by-column instead of row-by-row.
Data correction happens at the codeword level. A Reed-Solomon codeword is 255 bytes where some portion is file data and the rest is parity data. AOSP and avbroot both default to 253 bytes of data and 2 bytes of parity information. Each column in the table represents the 253-byte data portion of the codeword. Larger files have more columns.
A contiguous sequence of corrupted data will span multiple columns. Since error correction happens at the column level, this interleaving increases the chances of recovery. For more details about the specifics, see the implementation in [`fec.rs`](./avbroot/src/format/fec.rs).
### Generating FEC data
```bash
avbroot fec generate -i <input data file> -f <output FEC file>
```
The default behavior is to use 2 bytes of parity information per 253 bytes of input data. Within each 253-byte column described above, this is sufficient for correcting a single corrupted byte in the column (`⌊parity / 2⌋` bytes in general).
The number of parity bytes (between 2 and 24, inclusive) can be configured using `--parity`.
### Updating FEC data
```bash
avbroot fec update -i <input data file> -f <FEC file> [-r <start> <end>]...
```
This will update the FEC data corresponding to the specified regions. This can be significantly faster than generating new FEC data from scratch for large files if the regions where data was modified are known.
### Verifying a file
```bash
avbroot fec verify -i <input data file> -f <input FEC file>
```
This will check if the input file has any corrupted bytes. This command runs significantly faster than `avbroot fec repair` and is useful if only detection of corrupted data is needed.
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, there can be false positives where the corrupted data is reported as being valid.
### Repairing a file
```bash
avbroot fec repair -i <input/output data file> -f <input FEC file>
```
This will repair the file in place. As described above, in each column, up to `parity / 2` bytes can be corrected.
Note that FEC is **not** a replacement for checksums, like SHA-256. When there are too many errors, the file can potentially be "successfully repaired" to some incorrect data.
## `avbroot hash-tree`
This set of commands is for working with dm-verity hash tree data. They are not especially useful outside of debugging avbroot itself because the output format is custom. There is a custom header that sits in front of the standard dm-verity hash tree data.
| Offsets | Type | Description |
|------------|--------|--------------------------------|
| 0..16 | ASCII | `avbroot!hashtree` magic bytes |
| 16..18 | U16LE | Version (currently 1) |
| 18..26 | U64LE | Image size |
| 26..30 | U32LE | Block size |
| 30..46 | ASCII | Hash algorithm |
| 46..48 | U16LE | Salt size |
| 48..50 | U16LE | Root digest size |
| 50..54 | U32LE | Hash tree size |
| (Variable) | BINARY | Salt |
| (Variable) | BINARY | Root digest |
| (Variable) | BINARY | Hash tree |
For more information on the hash tree data, see the [Linux kernel documentation](https://docs.kernel.org/admin-guide/device-mapper/verity.html#hash-tree) or avbroot's implementation in [`hashtree.rs`](./avbroot/src/format/hashtree.rs).
### Generating hash tree
```bash
avbroot hash-tree generate -i <input data file> -H <output hash tree file>
```
The default behavior is to use a block size of 4096, the `sha256` algorithm, and an empty salt. These can be changed with the `-b`, `-a`, and `-s` options, respectively.
All parameters needed for verification are included in the hash tree file's header.
### Updating hash tree
```bash
avbroot hash-tree update -i <input data file> -H <hash tree file> [-r <start> <end>]...
```
This will update the hash tree data corresponding to the specified regions. This can be significantly faster than generating new hash tree data from scratch for large files if the regions where data was modified are known.
### Verifying a file
```bash
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.
+185 -77
View File
@@ -1,49 +1,115 @@
# avbroot
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU.
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
## Requirements
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
* `payload.bin` exists
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
## Patches
avbroot applies two patches to the boot images:
avbroot applies the following patches to the partition images:
* Magisk is applied to the `boot` or `init_boot` image, depending on device, as if it were done from the Magisk app.
* The `boot` or `init_boot` image, depending on device, is patched to enable root access. For Magisk, the patch is equivalent to what would be normally done by the Magisk app.
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded after the bootloader has been locked. It also prevents accidental flashing of the original OTA package while booted into recovery.
* The `boot`, `recovery`, or `vendor_boot` image, depending on device, is patched to replace the OTA signature verification certificates with the custom OTA signing certificate. This allows future patched OTAs to be sideloaded from recovery mode after the bootloader has been locked. It also prevents accidental flashing of the original unpatched OTA.
* The `system` or `my_engineering` image, depending on device, is also patched to replace the OTA signature verification certificates. This prevents the OS' system updater app from installing an unpatched OTA and also allows the use of custom OTA updater apps.
## Warnings and Caveats
* The device must use (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning sceme, open the OTA zip file and check that:
* **Always leave the `OEM unlocking` checkbox enabled when using a locked bootloader with root.** This is critically important. Root access allows the boot partition to potentially be overwritten, either accidentally or intentionally, with an image that is not properly signed. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* `payload.bin` exists
* `META-INF/com/android/metadata.pb` exists
* `META-INF/com/android/metadata` contains the line: `ota-type=AB`
Repeat: **_ALWAYS leave `OEM unlocking` enabled if rooted._**
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders support this, as well as most OnePlus devices. Other devices may support it as well, but there's no easy way to check without just trying it.
* Any operation that causes an improperly-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
* **Do not ever disable the `OEM unlocking` checkbox when using a locked bootloader with root.** This is critically important. With root access, it is possible to corrupt the running system, for example by zeroing out the boot partition. In this scenario, if the checkbox is turned off, both the OS and recovery mode will be made unbootable and `fastboot flashing unlock` will not be allowed. This effectively renders the device **_hard bricked_**.
* Performing a unpatched A/B OTA update while booted into Android via the OS' default updater. This can be blocked via a Magisk/KernelSU module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
* Any operation that causes an unsigned or differently-signed boot image to be flashed will result in the device being unbootable and unrecoverable without unlocking the bootloader again (and thus, triggering a data wipe). This includes:
* The `Direct install` method for updating Magisk. Magisk updates **must** be done by repatching the OTA, not via the app.
* Performing a regular (unpatched) A/B OTA update. This can be blocked via a Magisk module (see: [Blocking A/B OTA Updates](#blocking-ab-ota-updates)).
If the boot image is ever modified, **do not reboot**. [Open an issue](https://github.com/chenxiaolong/avbroot/issues/new) for support and be very clear about what steps were done that lead to the situation. If Android is still running and root access works, it might be possible to recover without wiping and starting over.
* The `Direct install` method for updating Magisk. Magisk updates must be done by repatching as well.
## Usage
1. Make sure the [caveats listed above](#warnings-and-caveats) are understood. It is possible to hard brick by doing the wrong thing!
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
avbroot is a standalone executable. It does not need to be installed and can be run from anywhere.
3. Follow the steps to [generate signing keys](#generating-keys).
4. Patch the OTA zip. The base command is:
```bash
avbroot ota patch \
--input /path/to/ota.zip \
--key-avb /path/to/avb.key \
--key-ota /path/to/ota.key \
--cert-ota /path/to/ota.crt \
```
Add the following additional arguments to the end of the command depending on how you want to configure root access.
* To enable root access with Magisk:
```bash
--magisk /path/to/magisk.apk \
--magisk-preinit-device <name>
```
If you don't know the Magisk preinit partition name, see the [Magisk preinit device section](#magisk-preinit-device) for steps on how to find it.
If you prefer to manually patch the boot image via the Magisk app instead of letting avbroot handle it, use the following arguments instead:
```bash
--prepatched /path/to/magisk_patched-xxxxx_yyyyy.img
```
* To enable root access with KernelSU:
```bash
--prepatched /path/to/kernelsu/boot.img
```
* To leave the OS unrooted:
```bash
--rootless
```
For more details on the options above, see the [advanced usage section](#advanced-usage).
If `--output` is not specified, then the output file is written to `<input>.patched`.
5. The patched OTA is ready to go! To flash it for the first time, follow the steps in the [initial setup section](#initial-setup). For updates, follow the steps in the [updates section](#updates).
## Generating Keys
avbroot signs a few components while patching an OTA zip:
avbroot signs several components while patching an OTA zip:
* the root `vbmeta` image
* the boot image `vbmeta` footers (if the original ones were signed)
* the boot images
* the vbmeta images
* the OTA payload
* the OTA zip itself
The boot-related components are signed with an AVB key and OTA-related components are signed with an OTA key. They can be the same RSA keypair, though the following steps show how to generate two separate keys.
The first two components are signed with an AVB key and latter two components are signed with an OTA key. They can be the same key, though the following steps show how to generate two separate keys.
1. Generate the AVB and OTA signing keys:
When patching OTAs for multiple devices, generating unique keys for each device is strongly recommended because it prevents an OTA for the wrong device being accidentally flashed.
1. Generate the AVB and OTA signing keys.
```bash
avbroot key generate-key -o avb.key
@@ -56,42 +122,23 @@ The boot-related components are signed with an AVB key and OTA-related component
avbroot key extract-avb -k avb.key -o avb_pkmd.bin
```
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery for verifying OTA updates.
3. Generate a self-signed certificate for the OTA signing key. This is used by recovery to verify OTA updates when sideloading.
```bash
avbroot key generate-cert -k ota.key -o ota.crt
```
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and X509 certificate (eg. like those generated by openssl).
The commands above are provided for convenience. avbroot is compatible with any standard PKCS8-encoded 4096-bit RSA private key and PEM-encoded X509 certificate, like those generated by openssl.
## Usage
If you lose your AVB or OTA signing key, you will no longer be able to sign new OTA zips. You will have to generate new signing keys and unlock your bootloader again (including a data wipe). Follow the [Usage section](#usage) as if doing an initial setup.
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
## Initial setup
2. Download the latest version from the [releases page](https://github.com/chenxiaolong/avbroot/releases). To verify the digital signature, see the [verifying digital signatures](#verifying-digital-signatures) section.
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
3. Follow the steps to [generate signing keys](#generating-keys).
2. When setting things up for the first time, the device must already be running the correct OS. Flash the original unpatched OTA if needed.
4. Patch the full OTA ZIP.
```bash
avbroot ota patch \
--input /path/to/ota.zip \
--privkey-avb /path/to/avb.key \
--privkey-ota /path/to/ota.key \
--cert-ota /path/to/ota.crt \
--magisk /path/to/magisk.apk
```
If `--output` is not specified, then the output file is written to `<input>.patched`.
**NOTE:** If you are using Magisk version >=25211, you need to know the preinit partition name (`--magisk-preinit-device <name>`). For details, see the [Magisk preinit device section](#magisk-preinit-device).
If you prefer to use an existing boot image patched by the Magisk app or you want to use KernelSU, see the [advanced usage section](#advanced-usage).
5. **[Initial setup only]** Unlock the bootloader. This will trigger a data wipe.
6. **[Initial setup only]** Extract the patched images from the patched OTA.
3. Extract the partition images from the patched OTA that are different from the original.
```bash
avbroot ota extract \
@@ -99,71 +146,124 @@ The commands above are provided for convenience. avbroot is compatible with any
--directory extracted
```
7. **[Initial setup only]** Flash the patched images and the AVB public key metadata. This sets up the custom root of trust. Future updates are done by simply sideloading patched OTA zips.
4. Flash the partition images that were extracted.
This can be done by manually running `fastboot flash <partition> extracted/<partition>.img` for each image in the `extracted/` directory or by using the following script:
```bash
# Flash the boot images that were extracted
for image in extracted/*.img; do
partition=$(basename "${image}")
partition=${partition%.img}
fastboot flash "${partition}" "${image}"
done
```
# Flash the AVB signing public key
5. Set up the custom AVB public key in the bootloader.
```bash
fastboot erase avb_custom_key
fastboot flash avb_custom_key /path/to/avb_pkmd.bin
```
8. **[Initial setup only]** Run `dmesg | grep libfs_avb` as root to verify that AVB is working properly. A message similar to the following is expected:
6. **[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:
```bash
adb shell su -c 'dmesg | grep libfs_avb'
```
If AVB is working properly, the following message should be printed out:
```bash
init: [libfs_avb]Returning avb_handle with status: Success
```
9. **[Initial setup only]** Lock the bootloader. This will trigger a data wipe again. **Do not uncheck `OEM unlocking`!**
7. Reboot back into fastboot and lock the bootloader. This will trigger a data wipe again.
Remember: **Do not uncheck `OEM unlocking`!**
**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 [avbroot's `oemunlockonboot` Magisk module](#oemunlockonboot-enable-oem-unlocking-on-every-boot) to automatically ensure OEM unlocking is enabled on every boot.
8. That's it! To install future OS, Magisk, or KernelSU updates, see the [next section](#updates).
For extra safety, consider flashing [avbroot's Magisk/KernelSU modules](#avbroot-modules).
## Updates
To update Android or Magisk:
Updates to Android, Magisk, and KernelSU are all done the same way by patching (or repatching) the OTA.
1. Follow step 4 in [the previous section](#usage) to patch the new OTA (or an existing OTA with a newer Magisk APK).
1. If Magisk or KernelSU is being updated, first install their new `.apk`. If you happen to open the app, make sure it **does not** flash the boot image. Cancel the boot image update prompts if needed.
2. Reboot to recovery mode. If stuck at a `No command` screen, press the volume up button once while holding down the power button.
2. Follow the step in the [usage section](#usage) to patch the new OTA.
3. Sideload the patched OTA.
3. Reboot to recovery mode. If the screen is stuck at a `No command` message, press the volume up button once while holding down the power button.
4. Reboot.
4. Sideload the patched OTA with `adb sideload`.
## avbroot Magisk modules
5. That's it!
avbroot's Magisk modules can be found on the [releases page](https://github.com/chenxiaolong/avbroot/releases) or they can be built locally by running:
## Reverting to stock firmware
```bash
cargo xtask modules -a
```
To stop using avbroot and revert to the stock firmware:
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
### `clearotacerts`: Blocking A/B OTA Updates
2. Erase the custom AVB public key.
Unpatched OTA updates are already blocked in recovery because the original OTA certificate has been replaced with the custom certificate. To disable automatic OTAs while booted into Android, turn off `Automatic system updates` in Android's Developer Options.
```bash
fastboot erase avb_custom_key
```
The `clearotacerts` module additionally makes A/B OTAs fail while booted into Android to prevent accidental manual updates. The module simply overrides `/system/etc/security/otacerts.zip` at runtime with an empty zip so that even if an OTA is downloaded, signature verification will fail.
3. Flash the stock firmware.
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
4. That's it! There are no other remnants to clean up.
## OTA updates
avbroot replaces `/system/etc/security/otacerts.zip` in both the system and recovery partitions with a new zip that contains the custom OTA signing certificate. This prevents an unpatched OTA from inadvertently being installed both when booted into Android and when sideloading from recovery.
Disabling the system updater app is recommended to prevent it from even attempting to install an unpatched OTA. To do so:
* Stock OS: Turn off `Automatic system updates` in Android's Developer Options.
* Custom OS: Disable the system updater app (or block its network access) from Settings -> Apps -> See all apps -> (three-dot menu) -> Show system -> (find updater app).
This is especially important for some custom OS's because their system updater app may get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails.
To self-host a custom OTA server, see [Custota](https://github.com/chenxiaolong/Custota).
## Repair mode
Some devices now ship with a Repair Mode feature that boots the system with a fresh `userdata` image so that repair technicians are able to run on-device diagnostics without needing the user's credentials to unlock the device.
When the device is rooted, it is unsafe to use Repair Mode. Unless you are using release builds of Magisk/KernelSU signed with your own keys, it's trivial for someone to just install the Magisk/KernelSU app while in repair mode to gain root access with no authentication.
To safely use Repair Mode:
1. Unroot the device by repatching the OTA with the `--rootless` option (instead of `--magisk` or `--prepatched`) and flashing it.
2. Turn on Repair Mode.
3. After receiving the repaired device, exit Repair Mode.
4. Flash the (rooted) patched OTA as normal.
Because the unrooting and rooting are done by flashing OTAs, the device's data will not be wiped.
## avbroot modules
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
### `oemunlockonboot`: Enable OEM unlocking on every boot
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS's initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
To help reduce the risk of OEM unlocking being accidentally disabled (or intentionally disabled as part of some OS' initial setup wizard), this module will attempt to enable the OEM unlocking option on every boot.
The logs for this module can be found at `/data/local/tmp/avbroot_oem_unlock.log`.
## Magisk preinit device
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition's block device name to be specified via `--magisk-preinit-device <name>`. To find the partition name:
Magisk versions 25211 and newer require a writable partition for storing custom SELinux rules that need to be accessed during early boot stages. This can only be determined on a real device, so avbroot requires the partition to be explicitly specified via `--magisk-preinit-device <name>`. To find the partition name:
1. Extract the boot image from the original/unpatched OTA:
@@ -174,9 +274,9 @@ Magisk versions 25211 and newer require a writable partition for storing custom
--boot-only
```
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device! The partition name will be incorrect if patched from Magisk on a different device.
2. Patch the boot image via the Magisk app. This **MUST** be done on the target device or a device of the same model! The partition name will be incorrect if patched from Magisk on a different device model.
The Magisk app will include a line like the following in the output:
The Magisk app will print out a line like the following in the output:
```
- Pre-init storage partition device ID: <name>
@@ -185,7 +285,7 @@ Magisk versions 25211 and newer require a writable partition for storing custom
Alternatively, avbroot can print out what Magisk detected by running:
```bash
avbroot ota magisk-info \
avbroot boot magisk-info \
--image magisk_patched-*.img
```
@@ -206,6 +306,8 @@ avbroot ota verify \
--public-key-avb /path/to/avb_pkmd.bin
```
This command works for any OTA, regardless if it's patched or unpatched.
If the `--cert-ota` and `--public-key-avb` options are omitted, then the signatures are only checked for validity, not that they are trusted.
## Tab completion
@@ -248,15 +350,13 @@ Invoke-Expression (& avbroot completion -s powershell)
### Using a prepatched boot image
avbroot can replace the boot image with a prepatched image instead of applying the Magisk root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
For KernelSU, also pass in `--boot-partition @gki_kernel` for both the `patch` and `extract` commands. avbroot defaults to Magisk's semantics where the boot image containing the GKI ramdisk is needed, whereas KernelSU requires the boot image containing the GKI kernel. This only affects devices launching with Android 13, where the GKI kernel and ramdisk are in different partitions (`boot` vs. `init_boot`), but it is safe and recommended to always use this option for KernelSU.
avbroot can replace the boot image with a prepatched image instead of applying the root patch itself. This is useful for using a boot image patched by the Magisk app or for KernelSU. To use a prepatched Magisk boot image or a KernelSU boot image, pass in `--prepatched <boot image>` instead of `--magisk <apk>`. When using `--prepatched`, avbroot will skip applying the Magisk root patch, but will still apply the OTA certificate patch.
Note that avbroot will validate that the prepatched image is compatible with the original. If, for example, the header fields do not match or a boot image section is missing, then the patching process will abort. The checks are not foolproof, but should help protect against accidental use of the wrong boot image. To bypass a somewhat "safe" subset of the checks, use `--ignore-prepatched-compat`. To ignore all checks (strongly discouraged!), pass it in twice.
### Skipping root patches
avbroot can be used for just resigning 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.
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.
### Replacing partitions
@@ -271,7 +371,7 @@ This has no impact on what patches are applied. For example, when using Magisk,
Some Android builds may ship with a root `vbmeta` image with the flags set such that AVB is effectively disabled. When avbroot encounters these images, the patching process will fail with a message like:
```
ValueError: vbmeta flags disable AVB: 0x3
Verified boot is disabled by vbmeta's header flags: 0x3
```
To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
@@ -280,7 +380,7 @@ To forcibly enable AVB (by clearing the flags), pass in `--clear-vbmeta-flags`.
avbroot prompts for the private key passphrases interactively by default. To run avbroot non-interactively, either:
* Supply the passphrases via files:
* Supply the passphrases via files.
```bash
avbroot ota patch \
@@ -335,11 +435,19 @@ The output binary is written to `target/release/avbroot`.
Debug builds work too, but they will run significantly slower (in the sha256 computations) due to compiler optimizations being turned off.
By default, the build links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
To build avbroot's modules from source, run:
```bash
cargo xtask modules -a
```
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable must be set to the Android SDK path.
## Verifying digital signatures
First, save the public key to a file listing the keys to be trusted.
First, save the public key to a file listing the keys to be trusted. This is the same key listed in [the author's profile](https://github.com/chenxiaolong/).
```bash
echo 'avbroot ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOe6/tBnO7xZhAWXRj3ApUYgn+XZ0wnQiXM8B7tPgv4' > avbroot_trusted_keys
+19 -7
View File
@@ -11,21 +11,26 @@ publish = false
[dependencies]
anyhow = "1.0.75"
base64 = "0.21.3"
bitflags = "2.4.1"
bstr = "1.6.2"
byteorder = "1.4.3"
cap-std = "2.0.0"
cap-tempfile = "2.0.0"
clap = { version = "4.4.1", features = ["derive"] }
clap_complete = "4.4.0"
cms = { version = "0.2.2", features = ["std"] }
const-oid = "0.9.5"
ctrlc = "3.4.0"
flate2 = "1.0.27"
hex = "0.4.3"
gf256 = { version = "0.3.0", features = ["rs"] }
hex = { version = "0.4.3", features = ["serde"] }
lz4_flex = "0.11.1"
memchr = "2.6.0"
num-bigint-dig = "0.8.4"
num-traits = "0.2.16"
phf = { version = "0.11.2", features = ["macros"] }
pkcs8 = { version = "0.10.2", features = ["encryption", "pem"] }
quick-protobuf = "0.8.1"
prost = "0.12.1"
rand = "0.8.5"
rayon = "1.7.0"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
@@ -33,7 +38,7 @@ regex = { version = "1.9.4", default-features = false, features = ["perf", "std"
# because sha2 is significantly slower on older x86_64 CPUs without the SHA-NI
# instructions. sha2 is still used for signing purposes.
# https://github.com/RustCrypto/hashes/issues/327
ring = "0.16.20"
ring = "0.17.0"
rpassword = "7.2.0"
rsa = { version = "0.9.2", features = ["sha1", "sha2"] }
serde = { version = "1.0.188", features = ["derive"] }
@@ -41,10 +46,9 @@ sha1 = "0.10.5"
sha2 = "0.10.7"
tempfile = "3.8.0"
thiserror = "1.0.47"
toml_edit = { version = "0.19.14", features = ["serde"] }
toml_edit = { version = "0.21.0", features = ["serde"] }
topological-sort = "0.2.2"
x509-cert = { version = "0.2.4", features = ["builder"] }
xz2 = "0.1.7"
# There's an upstream bug that causes an infinite loop in the write::BzDecoder
# destructor if the decoder is fed invalid data. While this never happens during
@@ -55,6 +59,14 @@ xz2 = "0.1.7"
git = "https://github.com/jongiddy/bzip2-rs"
rev = "2aefcb4d3634de1df226c73d93f758d65228bb8c"
# The upstream xz2 crate uses an old version of liblzma when compiling with the
# `static` feature and doesn't enable all of the encoders and decoders. This
# causes certain payload data to fail to decompress.
# https://github.com/chenxiaolong/avbroot/issues/138
[dependencies.xz2]
git = "https://github.com/chenxiaolong/xz2-rs"
rev = "fe2050b9c3395db15d8610f1dabb505440c1a556"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
@@ -66,8 +78,8 @@ features = ["deflate"]
rustix = { version = "0.38.9", default-features = false, features = ["process"] }
[build-dependencies]
# Disable the clap feature since it pulls in an ancient version of clap.
pb-rs = { version = "0.10.0", default-features = false }
prost-build = "0.12.1"
protox = "0.5.0"
[dev-dependencies]
assert_matches = "1.5.0"
+5 -11
View File
@@ -5,8 +5,6 @@
use std::{env, ffi::OsStr, fs, io, path::Path};
use pb_rs::{types::FileDescriptor, ConfigBuilder};
fn main() {
let out_dir = Path::new(&env::var("OUT_DIR").unwrap()).join("protobuf");
let in_dir = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("protobuf");
@@ -30,14 +28,10 @@ fn main() {
fs::create_dir_all(&out_dir).unwrap();
let config = ConfigBuilder::new(&protos, None, Some(&out_dir), &[in_dir])
.unwrap()
.dont_use_cow(true)
// We're using this as a means to force quick-protobuf to use BTreeMap
// instead of HashMap so that the serialized messages are reproducible.
// https://github.com/tafia/quick-protobuf/issues/251
.nostd(true)
.build();
let file_descriptors = protox::compile(&protos, [&in_dir]).unwrap();
FileDescriptor::run(&config).unwrap();
prost_build::Config::new()
.btree_map(["."])
.compile_fds(file_descriptors)
.unwrap();
}
+10 -5
View File
@@ -3,12 +3,12 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::sync::{atomic::AtomicBool, Arc};
use std::sync::atomic::AtomicBool;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::cli::{avb, boot, completion, key, ota, ramdisk};
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -16,9 +16,11 @@ pub enum Command {
Avb(avb::AvbCli),
Boot(boot::BootCli),
Completion(completion::CompletionCli),
Cpio(cpio::CpioCli),
Fec(fec::FecCli),
HashTree(hashtree::HashTreeCli),
Key(key::KeyCli),
Ota(ota::OtaCli),
Ramdisk(ramdisk::RamdiskCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
@@ -28,21 +30,24 @@ pub enum Command {
}
#[derive(Debug, Parser)]
#[command(version)]
pub struct Cli {
#[command(subcommand)]
pub command: Command,
}
pub fn main(cancel_signal: &Arc<AtomicBool>) -> Result<()> {
pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Avb(c) => avb::avb_main(&c, cancel_signal),
Command::Boot(c) => boot::boot_main(&c),
Command::Completion(c) => completion::completion_main(&c),
Command::Cpio(c) => cpio::cpio_main(&c, cancel_signal),
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::Ota(c) => ota::ota_main(&c, cancel_signal),
Command::Ramdisk(c) => ramdisk::ramdisk_main(&c),
// Deprecated aliases.
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
+762 -87
View File
@@ -5,25 +5,411 @@
use std::{
collections::{HashMap, HashSet},
ffi::OsStr,
ffi::{OsStr, OsString},
fs::{self, File},
io::{self, BufReader},
io::{self, BufReader, BufWriter, Seek, SeekFrom, Write},
path::{Path, PathBuf},
str,
sync::{atomic::AtomicBool, Arc},
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, bail, Context, Result};
use clap::{Parser, Subcommand};
use cap_std::{
ambient_authority,
fs::{Dir, OpenOptions},
};
use clap::{Args, Parser, Subcommand};
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use rsa::RsaPublicKey;
use serde::{Deserialize, Serialize};
use crate::{
cli::{status, warning},
format::avb::{self, Descriptor},
stream::PSeekFile,
crypto::{self, PassphraseSource},
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, PSeekFile, Reopen},
util,
};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct AvbInfo {
header: Header,
footer: Option<Footer>,
image_size: u64,
}
fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
let file = File::open(path)
.with_context(|| format!("Failed to open AVB image for reading: {path:?}"))?;
let mut reader = BufReader::new(file);
let (header, footer, image_size) = avb::load_image(&mut reader)
.with_context(|| format!("Failed to load AVB image: {path:?}"))?;
let info = AvbInfo {
header,
footer,
image_size,
};
Ok((info, reader))
}
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
let mut writer = BufWriter::new(file);
if let Some(f) = &mut info.footer {
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
.context("Failed to write appended AVB image")?;
} else {
avb::write_root_image(&mut writer, &info.header, 4096)
.context("Failed to write root AVB image")?;
}
writer.flush().context("Failed to flush writes")?;
Ok(())
}
/// Read AVB information from TOML file.
fn read_info(path: &Path) -> Result<AvbInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read AVB info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse AVB info TOML: {path:?}"))?;
Ok(info)
}
/// Write AVB information to TOML file.
fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize AVB info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write AVB info TOML: {path:?}"))?;
Ok(())
}
/// Packing with insecure algorithms is intentionally not supported, so promote
/// to a secure algorithm if needed.
fn promote_insecure_hash_algorithm(algorithm: &str) -> &str {
const INSECURE_ALGORITHMS: &[&str] = &["sha1"];
const NEW_ALGORITHM: &str = "sha256";
if INSECURE_ALGORITHMS.contains(&algorithm) {
warning!("Changing insecure hash algorithm {algorithm} to {NEW_ALGORITHM}");
NEW_ALGORITHM
} else {
algorithm
}
}
/// Copy `size` bytes from `reader` into a new file `path` that's opened as
/// both readable and writable.
fn write_raw(
path: &Path,
reader: &mut BufReader<File>,
size: u64,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
let file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open raw image for writing: {path:?}"))?;
let mut writer = BufWriter::new(file);
reader
.rewind()
.with_context(|| format!("Failed to seek file: {path:?}"))?;
stream::copy_n(reader, &mut writer, size, cancel_signal)
.with_context(|| format!("Failed to copy raw image: {path:?}"))?;
let file = writer
.into_inner()
.with_context(|| format!("Failed to flush writes: {path:?}"))?;
Ok(file)
}
/// Copy a raw image into `path` and verify the descriptor. If corruption is
/// detected and FEC data is available, then a repair is attempted.
fn write_raw_and_verify(
path: &Path,
reader: &mut BufReader<File>,
info: &AvbInfo,
ignore_invalid: bool,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
let f = info.footer.as_ref().expect("Not an appended image");
let descriptor = info.header.appended_descriptor()?;
// The hash tree and FEC need to be present for verification. We don't
// verify against the original file because a repair would require writes.
let copy_size = match descriptor {
AppendedDescriptorRef::HashTree(d) => d.image_size + d.tree_size + d.fec_size,
AppendedDescriptorRef::Hash(d) => d.image_size,
};
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
// Chop off the old hash tree and FEC data.
raw_file.set_len(f.original_image_size)?;
if let Err(e) = result {
if ignore_invalid {
warning!("{e:?}");
} else {
return Err(e);
}
}
Ok(raw_file)
}
/// Copy a raw image into `path` and update the appended descriptor with the new
/// digests, offsets, and sizes.
fn write_raw_and_update(
path: &Path,
reader: &mut BufReader<File>,
info: &mut AvbInfo,
cancel_signal: &AtomicBool,
) -> Result<PSeekFile> {
assert!(info.footer.is_some(), "Not an appended image");
let image_size = reader
.seek(SeekFrom::End(0))
.context("Failed to get input data size")?;
let mut raw_file = write_raw(path, reader, image_size, cancel_signal)?;
match info.header.appended_descriptor_mut()? {
AppendedDescriptorMut::HashTree(d) => {
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
d.image_size = image_size;
d.update(&raw_file, &raw_file, None, cancel_signal)
.context("Failed to update hash tree descriptor")?;
}
AppendedDescriptorMut::Hash(d) => {
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
d.image_size = image_size;
raw_file.rewind()?;
d.update(&mut raw_file, cancel_signal)
.context("Failed to update hash descriptor")?;
}
}
Ok(raw_file)
}
/// Compute the kernel command line arguments to allow the kernel to set up the
/// dm-verity block device without dm-init or userspace helpers. This is handled
/// by init/do_mounts_dm.c in older Pixel devices (and ChromiumOS).
fn compute_dm_verity_cmdline(descriptor: &HashTreeDescriptor) -> String {
use std::fmt::Write;
let mut result = String::new();
// Number of device mapper devices.
result.push_str("dm=\"1");
// Block device name.
result.push_str(" vroot");
// Block device UUID.
result.push_str(" none");
// Block device write mode.
result.push_str(" ro");
// Number of device mapper targets.
result.push_str(" 1,");
// Starting sector.
result.push('0');
// Sector count.
write!(&mut result, " {}", descriptor.image_size / 512).unwrap();
// dm-verity version.
write!(&mut result, " verity {}", descriptor.dm_verity_version).unwrap();
// Data block device (replaced by bootloader at runtime).
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// Hash block device (replaced by bootloader at runtime).
result.push_str(" PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// Data block size.
write!(&mut result, " {}", descriptor.data_block_size).unwrap();
// Hash block size.
write!(&mut result, " {}", descriptor.hash_block_size).unwrap();
// Number of data blocks.
write!(
&mut result,
" {}",
descriptor.image_size / u64::from(descriptor.data_block_size),
)
.unwrap();
// Hash starting block (in units of the hash block size).
write!(
&mut result,
" {}",
descriptor.image_size / u64::from(descriptor.hash_block_size),
)
.unwrap();
// Hash algorithm.
write!(&mut result, " {}", descriptor.hash_algorithm).unwrap();
// Root digest.
write!(&mut result, " {}", &hex::encode(&descriptor.root_digest)).unwrap();
// Salt.
write!(&mut result, " {}", &hex::encode(&descriptor.salt)).unwrap();
// Number of optional arguments.
let num_optional_args = if descriptor.fec_num_roots != 0 { 10 } else { 2 }
+ u8::from(descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0);
write!(&mut result, " {num_optional_args}").unwrap();
if descriptor.flags & HashTreeDescriptor::FLAG_CHECK_AT_MOST_ONCE != 0 {
// [n + 1] Only check blocks once instead of on each access.
result.push_str(" check_at_most_once");
}
// [0] Corruption handling mode (replaced by bootloader at runtime).
result.push_str(" $(ANDROID_VERITY_MODE)");
// [1] Force return zeros and skip validation for blocks expected to contain
// only zeros.
result.push_str(" ignore_zero_blocks");
if descriptor.fec_num_roots != 0 {
// [2-3] Enable FEC (replaced by bootloader at runtime).
result.push_str(" use_fec_from_device PARTUUID=$(ANDROID_SYSTEM_PARTUUID)");
// [4-5] Number of parity bytes per FEC codeword.
write!(&mut result, " fec_roots {}", descriptor.fec_num_roots).unwrap();
let fec_block_offset = descriptor.fec_offset / u64::from(descriptor.data_block_size);
// [6-7] Number of data blocks covered by FEC.
write!(&mut result, " fec_blocks {fec_block_offset}").unwrap();
// [8-9] Starting block (in data block size units) of FEC.
write!(&mut result, " fec_start {fec_block_offset}").unwrap();
}
// Root filesystem block device.
result.push_str("\" root=/dev/dm-0");
result
}
/// Update the dm-verity kernel command line descriptor to match the hash tree
/// descriptor. This is a no-op if there's no matching existing kernel command
/// line descriptor to update. Returns whether the descriptor was updated.
fn update_dm_verity_cmdline(info: &mut AvbInfo) -> Result<bool> {
assert!(info.footer.is_some(), "Not an appended image");
let new_cmdline = match info.header.appended_descriptor()? {
AppendedDescriptorRef::HashTree(d) => compute_dm_verity_cmdline(d),
AppendedDescriptorRef::Hash(_) => return Ok(false),
};
for d in &mut info.header.descriptors {
if let Descriptor::KernelCmdline(d) = d {
if d.flags & KernelCmdlineDescriptor::FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED != 0
&& d.cmdline.starts_with("dm=")
&& d.cmdline != new_cmdline
{
d.cmdline = new_cmdline;
return Ok(true);
}
}
}
Ok(false)
}
/// Sign or clear header signatures based on whether the original header was
/// signed. If the original header was signed and is unchanged, then the
/// original signature is used as-is. If the force option is specified, then
/// the header is signed or cleared based on the presence of the private key
/// parameter.
fn sign_or_clear(info: &mut AvbInfo, orig_header: &Header, key_group: &KeyGroup) -> Result<()> {
enum SignAction {
None,
Sign,
Clear,
}
let originally_signed = !info.header.signature.is_empty();
let mut sign_action = if originally_signed && &info.header != orig_header {
SignAction::Sign
} else {
// If the original image was signed, we can preserve the existing
// signature since no changes were made. If the original image was not
// signed, then there's nothing to do anyway.
SignAction::None
};
if key_group.force {
sign_action = if key_group.key.is_some() {
SignAction::Sign
} else {
SignAction::Clear
};
}
match sign_action {
SignAction::None => {
if originally_signed {
status!("Preserving original AVB header signature");
} else {
status!("Leaving AVB header unsigned");
}
}
SignAction::Sign => {
if originally_signed {
status!("Replacing AVB header signature");
} else {
status!("Signing AVB header");
}
let Some(key_path) = &key_group.key else {
bail!("Need to sign new AVB header, but no private key was specified");
};
let source = PassphraseSource::new(
key_path,
key_group.pass_file.as_deref(),
key_group.pass_env_var.as_deref(),
);
let private_key = crypto::read_pem_key_file(key_path, &source)
.with_context(|| format!("Failed to load key: {key_path:?}"))?;
info.header.set_algo_for_key(&private_key)?;
info.header
.sign(&private_key)
.context("Failed to sign new AVB header")?;
}
SignAction::Clear => {
if originally_signed {
status!("Clearing AVB header signature");
} else {
status!("Leaving AVB header unsigned");
}
info.header.algorithm_type = AlgorithmType::None;
info.header.clear_sig();
}
}
Ok(())
}
/// Dump AVB information to stdout.
fn display_info(display: &DisplayGroup, info: &AvbInfo) {
if !display.quiet {
println!("{info:#?}");
}
}
/// Ensure that the partition name won't cause directory traversals.
fn ensure_name_is_safe(name: &str) -> Result<()> {
if Path::new(name).file_name() != Some(OsStr::new(name)) {
bail!("Unsafe partition name: {name}");
@@ -34,9 +420,9 @@ fn ensure_name_is_safe(name: &str) -> Result<()> {
/// Recursively verify an image's vbmeta header and all of the chained images.
/// `seen` is used to prevent cycles. `descriptors` will contain all of the hash
/// and hashtree descriptors that need to be verified.
/// and hash tree descriptors that need to be verified.
pub fn verify_headers(
directory: &Path,
directory: &Dir,
name: &str,
expected_key: Option<&RsaPublicKey>,
seen: &mut HashSet<String>,
@@ -48,16 +434,17 @@ pub fn verify_headers(
ensure_name_is_safe(name)?;
let path = directory.join(format!("{name}.img"));
let raw_reader =
File::open(&path).with_context(|| anyhow!("Failed to open for reading: {path:?}"))?;
let path = format!("{name}.img");
let raw_reader = directory
.open(&path)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let (header, _, _) = avb::load_image(BufReader::new(raw_reader))
.with_context(|| anyhow!("Failed to load vbmeta structures: {path:?}"))?;
.with_context(|| format!("Failed to load vbmeta structures: {path:?}"))?;
// Verify the header's signature.
let public_key = header
.verify()
.with_context(|| anyhow!("Failed to verify header signature: {path:?}"))?;
.with_context(|| format!("Failed to verify header signature: {path:?}"))?;
if let Some(k) = &public_key {
let prefix = format!("{name} has a signed vbmeta header");
@@ -81,7 +468,7 @@ pub fn verify_headers(
};
match descriptor {
avb::Descriptor::Hashtree(_) | avb::Descriptor::Hash(_) => {
avb::Descriptor::HashTree(_) | avb::Descriptor::Hash(_) => {
if let Some(prev) = descriptors.get(target_name) {
if prev != descriptor {
bail!("{name} descriptor does not match previous encounter");
@@ -92,7 +479,7 @@ pub fn verify_headers(
}
avb::Descriptor::ChainPartition(d) => {
let target_key = avb::decode_public_key(&d.public_key).with_context(|| {
anyhow!("Failed to decode chained public key for: {target_name}")
format!("Failed to decode chained public key for: {target_name}")
})?;
verify_headers(directory, target_name, Some(&target_key), seen, descriptors)?;
@@ -104,16 +491,70 @@ pub fn verify_headers(
Ok(())
}
/// Verify the descriptor for a file. For hash tree descriptors, if FEC data is
/// available and `repair` is true, then attempt to repair data in the event of
/// corruption. `file` must be opened as read-write for the repair operation to
/// work.
fn verify_and_repair(
name: Option<&str>,
mut file: PSeekFile,
descriptor: AppendedDescriptorRef,
repair: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let suffix = match name {
Some(n) => format!(" for: {n}"),
None => String::new(),
};
match descriptor {
AppendedDescriptorRef::HashTree(d) => {
status!("Verifying hash tree descriptor{suffix}");
match d.verify(&file, cancel_signal) {
Err(e @ avb::Error::HashTree(_)) if repair => {
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
warning!("Attempting to repair using FEC data{suffix}");
d.repair(&file, &file, cancel_signal)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.verify(&file, cancel_signal).map(|_| {
status!("Successfully repaired data{suffix}");
})
}
ret => ret,
}
.with_context(|| format!("Failed to verify hash tree descriptor{suffix}"))?;
}
AppendedDescriptorRef::Hash(d) => {
status!("Verifying hash descriptor{suffix}");
file.rewind()?;
d.verify(file, cancel_signal)
.with_context(|| format!("Failed to verify hash descriptor{suffix}"))?;
}
}
Ok(())
}
/// Verify hash and hash tree descriptor digests and FEC data against their
/// corresponding input files.
pub fn verify_descriptors(
directory: &Path,
directory: &Dir,
descriptors: &HashMap<String, Descriptor>,
cancel_signal: &Arc<AtomicBool>,
repair: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
descriptors
.par_iter()
.map(|(name, descriptor)| {
let path = directory.join(format!("{name}.img"));
let reader = match File::open(&path).map(PSeekFile::new) {
let path = format!("{name}.img");
let file = match directory
.open_with(&path, OpenOptions::new().read(true).write(repair))
.map(|f| PSeekFile::new(f.into_std()))
{
Ok(f) => f,
// Some devices, like bluejay, have vbmeta descriptors that
// refer to partitions that exist on the device, but not in the
@@ -127,91 +568,315 @@ pub fn verify_descriptors(
}
};
match descriptor {
Descriptor::Hashtree(d) => {
status!("Verifying hashtree descriptor for: {name}");
d.verify(
|| Ok(Box::new(BufReader::new(reader.clone()))),
cancel_signal,
)
.with_context(|| anyhow!("Failed to verify hashtree descriptor for: {name}"))?;
}
Descriptor::Hash(d) => {
status!("Verifying hash descriptor for: {name}");
d.verify(BufReader::new(reader), cancel_signal)
.with_context(|| anyhow!("Failed to verify hash descriptor for: {name}"))?;
}
_ => unreachable!("Non-verifiable descriptor: {descriptor:?}"),
}
Ok(())
verify_and_repair(
Some(name),
file,
descriptor.try_into()?,
repair,
cancel_signal,
)
})
.collect()
}
pub fn avb_main(cli: &AvbCli, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
match &cli.command {
AvbCommand::Dump(c) => {
let raw_reader = File::open(&c.input)
.with_context(|| anyhow!("Failed to open for reading: {:?}", c.input))?;
let reader = BufReader::new(raw_reader);
let (header, footer, image_size) = avb::load_image(reader)
.with_context(|| anyhow!("Failed to load vbmeta structures: {:?}", c.input))?;
fn unpack_subcommand(cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> {
let (info, mut reader) = read_avb_image(&cli.input)?;
display_info(&cli.display, &info);
println!("Image size: {image_size}");
println!("Header: {header:#?}");
println!("Footer: {footer:#?}");
}
AvbCommand::Verify(c) => {
let public_key = if let Some(p) = &c.public_key {
let data = fs::read(p).with_context(|| anyhow!("Failed to read file: {p:?}"))?;
let key = avb::decode_public_key(&data)
.with_context(|| anyhow!("Failed to decode public key: {p:?}"))?;
write_info(&cli.output_info, &info)?;
Some(key)
} else {
None
};
let directory = c.input.parent().unwrap_or_else(|| Path::new("."));
let name = c
.input
.file_stem()
.with_context(|| anyhow!("Path is not a file: {:?}", c.input))?
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", c.input))?;
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
verify_headers(
directory,
name,
public_key.as_ref(),
&mut seen,
&mut descriptors,
)?;
verify_descriptors(directory, &descriptors, cancel_signal)?;
status!("Successfully verified all vbmeta signatures and hashes");
}
if info.footer.is_some() {
write_raw_and_verify(
&cli.output_raw,
&mut reader,
&info,
cli.ignore_invalid,
cancel_signal,
)?;
}
Ok(())
}
/// Dump AVB header and footer information.
fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut info = read_info(&cli.input_info)?;
let orig_header = info.header.clone();
let file = if info.footer.is_some() {
let mut reader = File::open(&cli.input_raw)
.map(BufReader::new)
.with_context(|| {
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
})?;
let file = write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?;
update_dm_verity_cmdline(&mut info)?;
file
} else {
File::create(&cli.output)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open output for writing: {:?}", cli.output))?
};
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
display_info(&cli.display, &info);
Ok(())
}
fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> {
let (mut info, mut reader) = read_avb_image(&cli.input)?;
let orig_header = info.header.clone();
let file = if info.footer.is_some() {
let file = write_raw_and_verify(&cli.output, &mut reader, &info, false, cancel_signal)?;
// Write new hash tree and FEC data instead of copying the original.
// There could have been errors in the original FEC data itself.
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
d.update(&file, &file, None, cancel_signal)?;
}
update_dm_verity_cmdline(&mut info)?;
file
} else {
File::create(&cli.output)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for writing: {:?}", cli.output))?
};
sign_or_clear(&mut info, &orig_header, &cli.key)?;
write_avb_image(file, &mut info)?;
// We display the info at the very end after both the header and footer are
// updated so that incorrect/incomplete information isn't shown.
display_info(&cli.display, &info);
Ok(())
}
fn info_subcommand(cli: &InfoCli) -> Result<()> {
let (info, _) = read_avb_image(&cli.input)?;
display_info(&cli.display, &info);
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let public_key = if let Some(p) = &cli.public_key {
let data = fs::read(p).with_context(|| format!("Failed to read file: {p:?}"))?;
let key = avb::decode_public_key(&data)
.with_context(|| format!("Failed to decode public key: {p:?}"))?;
Some(key)
} else {
None
};
let authority = ambient_authority();
let parent_path = util::parent_path(&cli.input);
let directory = Dir::open_ambient_dir(parent_path, authority)
.with_context(|| format!("Failed to open directory: {parent_path:?}"))?;
let name = cli
.input
.file_stem()
.with_context(|| format!("Path is not a file: {:?}", cli.input))?
.to_str()
.ok_or_else(|| anyhow!("Invalid UTF-8: {:?}", cli.input))?;
let mut seen = HashSet::<String>::new();
let mut descriptors = HashMap::<String, Descriptor>::new();
verify_headers(
&directory,
name,
public_key.as_ref(),
&mut seen,
&mut descriptors,
)?;
verify_descriptors(&directory, &descriptors, cli.repair, cancel_signal)?;
status!("Successfully verified all vbmeta signatures and hashes");
Ok(())
}
pub fn avb_main(cli: &AvbCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
AvbCommand::Unpack(c) => unpack_subcommand(c, cancel_signal),
AvbCommand::Pack(c) => pack_subcommand(c, cancel_signal),
AvbCommand::Repack(c) => repack_subcommand(c, cancel_signal),
AvbCommand::Info(c) => info_subcommand(c),
AvbCommand::Verify(c) => verify_subcommand(c, cancel_signal),
}
}
#[derive(Debug, Args)]
struct DisplayGroup {
/// Don't print AVB image information.
#[arg(short, long, global = true)]
quiet: bool,
}
#[derive(Debug, Args)]
struct KeyGroup {
/// Path to private key for signing.
///
/// A private key is needed if packing an image where the original header
/// was signed and the header needs to be modified (eg. for a new checksum).
/// If the header was originally not signed, then the private key is not
/// used, unless --force is specified.
#[arg(short, long, value_name = "FILE", value_parser)]
key: Option<PathBuf>,
/// Force signing or clearing signature.
///
/// If --key is specified, then the output is signed, regardless if the
/// original image was signed. Similarly, if --key is not specified, then
/// the output is left unsigned, even if the image is unchanged and it's
/// possible to preserve an existing signature.
#[arg(short, long)]
force: bool,
/// Environment variable containing private key passphrase.
#[arg(long, value_name = "ENV_VAR", value_parser, group = "pass")]
pass_env_var: Option<OsString>,
/// File containing private key passphrase.
#[arg(long, value_name = "FILE", value_parser, group = "pass")]
pass_file: Option<PathBuf>,
}
/// Unpack an AVB image.
///
/// This command splits an AVB image into an AVB info TOML file and a raw image.
/// For root AVB images (eg. vbmeta), only the info file is created, which will
/// contain a header section. For appended AVB images (eg. boot), the raw image
/// will be written and the info file will additionally have a footer section.
///
/// For appended AVB images that contain a hash tree descriptor (eg. dm-verity
/// protected system image), FEC data, if available, will be used to repair the
/// extracted raw image in the event of data corruption.
#[derive(Debug, Parser)]
struct DumpCli {
/// Path to input image.
struct UnpackCli {
/// Path to input AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output AVB info TOML.
///
/// The file will always contain a header section, but a footer section will
/// only be present if the input is an appended AVB image.
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
output_info: PathBuf,
/// Path to output raw image.
///
/// Only appended AVB images will have a raw image.
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
output_raw: PathBuf,
/// Ignore invalid digests or FEC data.
#[arg(long)]
ignore_invalid: bool,
#[command(flatten)]
display: DisplayGroup,
}
/// Pack an AVB image.
///
/// This command creates an AVB image from the AVB info toml file and the raw
/// image components. If the info file does not contain a footer, then a root
/// image is created. Otherwise, an appended image is created and a raw image
/// must be specified.
///
/// For appended images, the checksums in the header's descriptors and the
/// offsets in the footer will be updated. If the header fields are updated and
/// the header was originally signed, then the newly created image will also be
/// signed. If a signed image is repacked without modification, then the
/// original signature is used as-is.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input AVB info TOML.
///
/// If an appended image is being created, then the hash or hash tree
/// descriptor in the header will be updated with the new digest, offset,
/// and size field values. There must be exactly one hash or hash tree
/// descriptor. The offset and size fields in the footer will be updated as
/// well.
///
/// For root images, all header descriptor fields are left unmodified.
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
input_info: PathBuf,
/// Path to input raw image.
///
/// Appended AVB images require a raw image.
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
input_raw: PathBuf,
#[command(flatten)]
key: KeyGroup,
#[command(flatten)]
display: DisplayGroup,
}
/// Repack an AVB 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 AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
#[command(flatten)]
key: KeyGroup,
#[command(flatten)]
display: DisplayGroup,
}
/// Display AVB header and footer information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
#[command(flatten)]
display: DisplayGroup,
}
/// Verify vbmeta signatures.
///
/// If the header contains chain descriptors, then those images will be
/// recursively verified. For hash tree descriptors, the FEC (forward error
/// correction) data will also be verified.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input image.
/// Path to input AVB image.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
@@ -221,15 +886,25 @@ struct VerifyCli {
/// validity, not whether they are trusted.
#[arg(short, long, value_name = "FILE", value_parser)]
public_key: Option<PathBuf>,
/// Repair corrupted files using FEC data if possible.
///
/// Only images with hash tree descriptors can contain FEC data.
#[arg(short, long)]
repair: bool,
}
#[derive(Debug, Subcommand)]
enum AvbCommand {
Dump(DumpCli),
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
#[command(alias = "dump")]
Info(InfoCli),
Verify(VerifyCli),
}
/// Show information about AVB-protected images.
/// Pack, unpack, and inspect AVB-protected images.
#[derive(Debug, Parser)]
pub struct AvbCli {
#[command(subcommand)]
+22 -16
View File
@@ -9,11 +9,11 @@ use std::{
path::{Path, PathBuf},
};
use anyhow::{anyhow, bail, Context, Result};
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio},
format::{avb::Header, bootimage::BootImage, compression::CompressedReader, cpio::CpioReader},
stream::{FromReader, ToWriter},
};
@@ -33,7 +33,9 @@ fn write_image(path: &Path, image: &BootImage) -> Result<()> {
image
.to_writer(&mut writer)
.with_context(|| format!("Failed to write boot image: {path:?}"))?;
writer.flush()?;
writer
.flush()
.with_context(|| format!("Failed to flush boot image: {path:?}"))?;
Ok(())
}
@@ -82,7 +84,7 @@ fn read_avb_header_if_exists(path: &Path) -> Result<Option<Header>> {
Err(e) => Err(e).with_context(|| format!("Failed to open for reading: {path:?}"))?,
};
let header = Header::from_reader(BufReader::new(file))
.with_context(|| anyhow!("Failed to read vbmeta header: {path:?}"))?;
.with_context(|| format!("Failed to read vbmeta header: {path:?}"))?;
Ok(Some(header))
}
@@ -106,7 +108,7 @@ fn write_text_if_not_empty(path: &Path, text: &str) -> Result<()> {
fn write_avb_header(path: &Path, header: &Header) -> Result<()> {
let file =
File::create(path).with_context(|| anyhow!("Failed to open for writing: {path:?}"))?;
File::create(path).with_context(|| format!("Failed to open for writing: {path:?}"))?;
header.to_writer(BufWriter::new(file))?;
Ok(())
@@ -272,9 +274,9 @@ fn info_subcommand(boot_cli: &BootCli, cli: &InfoCli) -> Result<()> {
pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
let raw_reader = File::open(&cli.image)
.with_context(|| anyhow!("Failed to open for reading: {:?}", cli.image))?;
.with_context(|| format!("Failed to open for reading: {:?}", cli.image))?;
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))
.with_context(|| anyhow!("Failed to load boot image: {:?}", cli.image))?;
.with_context(|| format!("Failed to load boot image: {:?}", cli.image))?;
let mut ramdisks = vec![];
@@ -297,13 +299,17 @@ pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
for (i, ramdisk) in ramdisks.iter().enumerate() {
let reader = Cursor::new(ramdisk);
let reader = CompressedReader::new(reader, true)
.with_context(|| anyhow!("Failed to load ramdisk #{i}"))?;
let entries = cpio::load(reader, false)
.with_context(|| anyhow!("Failed to load ramdisk #{i} cpio"))?;
.with_context(|| format!("Failed to load ramdisk #{i}"))?;
let mut cpio_reader = CpioReader::new(reader, false);
if let Some(e) = entries.iter().find(|e| e.name == b".backup/.magisk") {
io::stdout().write_all(&e.content)?;
return Ok(());
while let Some(entry) = cpio_reader
.next_entry()
.with_context(|| format!("Failed to read ramdisk #{i} cpio entry"))?
{
if entry.path == b".backup/.magisk" {
io::copy(&mut cpio_reader, &mut io::stdout())?;
return Ok(());
}
}
}
@@ -328,7 +334,7 @@ struct UnpackCli {
input: PathBuf,
/// Path to output header TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
output_header: PathBuf,
/// Path to output kernel image.
@@ -388,7 +394,7 @@ struct PackCli {
output: PathBuf,
/// Path to input header TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "header.toml")]
#[arg(long, value_name = "FILE", value_parser, default_value = "boot.toml")]
input_header: PathBuf,
/// Path to input kernel image.
@@ -477,7 +483,7 @@ enum BootCommand {
MagiskInfo(MagiskInfoCli),
}
/// Pack or unpack boot images.
/// Pack, unpack, and inspect boot images.
#[derive(Debug, Parser)]
pub struct BootCli {
#[command(subcommand)]
+385
View File
@@ -0,0 +1,385 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::{self, File},
io::{BufReader, BufWriter, Seek},
path::{Path, PathBuf},
str,
sync::atomic::AtomicBool,
};
use anyhow::{anyhow, Context, Result};
use bstr::ByteSlice;
use cap_std::{ambient_authority, fs::Dir};
use clap::{Parser, Subcommand};
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
use crate::{
format::{
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntry, CpioEntryData, CpioEntryType, CpioReader, CpioWriter},
},
stream, util,
};
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct CpioInfo {
format: CompressedFormat,
entries: Vec<CpioEntry>,
}
fn open_reader(
path: &Path,
include_trailer: bool,
) -> Result<(
CpioReader<CompressedReader<BufReader<File>>>,
CompressedFormat,
)> {
let file =
File::open(path).with_context(|| format!("Failed to open cpio for reading: {path:?}"))?;
let reader = CompressedReader::new(BufReader::new(file), true)
.with_context(|| format!("Failed to open decompressor: {path:?}"))?;
let format = reader.format();
let cpio_reader = CpioReader::new(reader, include_trailer);
Ok((cpio_reader, format))
}
fn open_writer(
path: &Path,
format: CompressedFormat,
) -> Result<CpioWriter<CompressedWriter<BufWriter<File>>>> {
let file =
File::create(path).with_context(|| format!("Failed to open cpio for writing: {path:?}"))?;
let writer = CompressedWriter::new(BufWriter::new(file), format)
.with_context(|| format!("Failed to open compressor: {path:?}"))?;
let cpio_writer = CpioWriter::new(writer, false);
Ok(cpio_writer)
}
fn flush_writer(writer: CpioWriter<CompressedWriter<BufWriter<File>>>) -> Result<()> {
let compressed_writer = writer.finish().context("Failed to flush cpio writer")?;
let buf_writer = compressed_writer
.finish()
.context("Failed to flush compressor")?;
buf_writer.into_inner().context("Failed to flush file")?;
Ok(())
}
/// Read cpio information from TOML file.
fn read_info(path: &Path) -> Result<CpioInfo> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read cpio info TOML: {path:?}"))?;
let info = toml_edit::de::from_str(&data)
.with_context(|| format!("Failed to parse cpio info TOML: {path:?}"))?;
Ok(info)
}
/// Write cpio information to TOML file.
fn write_info(path: &Path, info: &CpioInfo) -> Result<()> {
let data = toml_edit::ser::to_string_pretty(info)
.with_context(|| format!("Failed to serialize cpio info TOML: {path:?}"))?;
fs::write(path, data).with_context(|| format!("Failed to write cpio info TOML: {path:?}"))?;
Ok(())
}
/// Open reader to the corresponding file inside the tree if the entry is a
/// regular file. Unsafe paths will result in an error.
fn open_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<(BufReader<File>, u32)>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let mut reader = tree
.open(path)
.map(|f| BufReader::new(f.into_std()))
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let file_size = reader
.seek(std::io::SeekFrom::End(0))
.with_context(|| format!("Failed to get file size: {path:?}"))?
.to_u32()
.ok_or_else(|| anyhow!("File is too large: {path:?}"))?;
reader
.rewind()
.with_context(|| format!("Failed to seek file: {path:?}"))?;
Ok(Some((reader, file_size)))
} else {
Ok(None)
}
}
/// Open writer to the corresponding file inside the tree if the entry is a
/// regular file. Intermediate directories are automatically created as needed.
/// Unsafe paths will result in an error.
fn create_tree_file(tree: &Dir, entry: &CpioEntry) -> Result<Option<BufWriter<File>>> {
if entry.file_type == CpioEntryType::Regular {
let path = entry
.path
.as_bstr()
.to_path()
.with_context(|| format!("Invalid entry path: {:?}", entry.path.as_bstr()))?;
let parent = util::parent_path(path);
tree.create_dir_all(parent)
.with_context(|| format!("Failed to create directory: {parent:?}"))?;
let writer = tree
.create(path)
.map(|f| BufWriter::new(f.into_std()))
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
Ok(Some(writer))
} else {
Ok(None)
}
}
fn display_format(cli: &CpioCli, format: CompressedFormat) {
if !cli.quiet {
println!("Compression format: {format:?}");
}
}
fn display_entry(cli: &CpioCli, entry: &CpioEntry) {
if !cli.quiet {
println!();
println!("{entry}");
}
}
fn unpack_subcommand(
cpio_cli: &CpioCli,
cli: &UnpackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut info = CpioInfo {
format,
entries: vec![],
};
display_format(cpio_cli, format);
let authority = ambient_authority();
Dir::create_ambient_dir_all(&cli.output_tree, authority)
.with_context(|| format!("Failed to create directory: {:?}", cli.output_tree))?;
let tree = Dir::open_ambient_dir(&cli.output_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.output_tree))?;
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
if let Some(mut writer) = create_tree_file(&tree, &entry)? {
let file_size = entry.data.size()?;
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
writer.into_inner().context("Failed to flush data")?;
}
info.entries.push(entry);
}
write_info(&cli.output_info, &info)?;
Ok(())
}
fn pack_subcommand(cpio_cli: &CpioCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
let mut info = read_info(&cli.input_info)?;
let mut writer = open_writer(&cli.output, info.format)?;
display_format(cpio_cli, info.format);
if cli.sort {
cpio::sort(&mut info.entries);
}
cpio::assign_inodes(&mut info.entries, true)?;
let authority = ambient_authority();
let tree = Dir::open_ambient_dir(&cli.input_tree, authority)
.with_context(|| format!("Failed to open directory: {:?}", cli.input_tree))?;
for entry in &mut info.entries {
let out = open_tree_file(&tree, entry)?;
if let Some((_, file_size)) = &out {
entry.data = CpioEntryData::Size(*file_size);
}
display_entry(cpio_cli, entry);
writer
.start_entry(entry)
.context("Failed to write cpio entry")?;
if let Some((mut reader, file_size)) = out {
stream::copy_n(&mut reader, &mut writer, file_size.into(), cancel_signal)
.context("Failed to copy data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn repack_subcommand(
cpio_cli: &CpioCli,
cli: &RepackCli,
cancel_signal: &AtomicBool,
) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, false)?;
let mut writer = open_writer(&cli.output, format)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
writer
.start_entry(&entry)
.context("Failed to write cpio entry")?;
if let CpioEntryData::Size(s) = &entry.data {
stream::copy_n(&mut reader, &mut writer, u64::from(*s), cancel_signal)
.context("Failed to copy cpio entry data")?;
}
}
flush_writer(writer)?;
Ok(())
}
fn info_subcommand(cpio_cli: &CpioCli, cli: &InfoCli) -> Result<()> {
let (mut reader, format) = open_reader(&cli.input, cli.trailer)?;
display_format(cpio_cli, format);
while let Some(entry) = reader.next_entry().context("Failed to read cpio entry")? {
display_entry(cpio_cli, &entry);
}
Ok(())
}
pub fn cpio_main(cli: &CpioCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
CpioCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal),
CpioCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal),
CpioCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal),
CpioCommand::Info(c) => info_subcommand(cli, c),
}
}
/// Unpack a cpio archive.
///
/// Regular files will be extracted to the output tree directory, but not any
/// other type of file (eg. symlinks). All file metadata is written to the info
/// TOML file, like the UID/GID, permissions, and symlink targets.
///
/// If any paths inside the cpio archive are unsafe, the extraction process will
/// fail and exit. Extracted files are never written outside of the tree
/// directory, even if an external process tries to interfere.
#[derive(Debug, Parser)]
struct UnpackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
output_info: PathBuf,
/// Path to output files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
output_tree: PathBuf,
}
/// Pack a cpio archive.
///
/// The new cpio archive will *only* contain files listed in the info TOML file.
/// Extra files inside the input tree directory that aren't listed will be
/// silently ignored. Entries are added to the archive in the order that they
/// are listed unless --sort is specified.
///
/// All fields inside the info TOML are used as-is. Missing fields in entries
/// are set to 0, aside from the inode number, which will be assigned a unique
/// value.
#[derive(Debug, Parser)]
struct PackCli {
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to input info TOML.
#[arg(long, value_name = "FILE", value_parser, default_value = "cpio.toml")]
input_info: PathBuf,
/// Path to input files directory.
#[arg(long, value_name = "DIR", value_parser, default_value = "cpio_tree")]
input_tree: PathBuf,
/// Sort entries before packing.
#[arg(long)]
sort: bool,
}
/// Repack a cpio archive.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
/// Display cpio entry information.
#[derive(Debug, Parser)]
struct InfoCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Show cpio trailer entry.
#[arg(long, global = true)]
trailer: bool,
}
#[derive(Debug, Subcommand)]
enum CpioCommand {
Unpack(UnpackCli),
Pack(PackCli),
Repack(RepackCli),
Info(InfoCli),
}
/// Pack, unpack, and inspect cpio archives.
#[derive(Debug, Parser)]
pub struct CpioCli {
#[command(subcommand)]
command: CpioCommand,
/// Don't print cpio entry information.
#[arg(short, long, global = true)]
quiet: bool,
}
+191
View File
@@ -0,0 +1,191 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::{File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::fec::FecImage,
stream::{FromReader, PSeekFile, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
fn read_fec(path: &Path) -> Result<FecImage> {
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let fec = FecImage::from_reader(reader)
.with_context(|| format!("Failed to read FEC data: {path:?}"))?;
Ok(fec)
}
fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
fec.to_writer(&mut writer)
.with_context(|| format!("Failed to write FEC data: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush FEC data: {path:?}"))?;
Ok(())
}
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = FecImage::generate(&input, cli.parity, cancel_signal)
.context("Failed to generate FEC data")?;
write_fec(&cli.fec, &fec)?;
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut fec = read_fec(&cli.fec)?;
fec.update(&input, &ranges, cancel_signal)
.context("Failed to update FEC data")?;
write_fec(&cli.fec, &fec)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = read_fec(&cli.fec)?;
fec.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
}
fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, true)?;
let fec = read_fec(&cli.fec)?;
// The separate buffered readers and writers are safe because the function
// guarantees that every thread touches disjoint offsets and every offset is
// read and written at most once.
fec.repair(&input, &input, cancel_signal)
.context("Failed to repair file")?;
Ok(())
}
pub fn fec_main(cli: &FecCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
FecCommand::Generate(c) => generate_subcommand(c, cancel_signal),
FecCommand::Update(c) => update_subcommand(c, cancel_signal),
FecCommand::Verify(c) => verify_subcommand(c, cancel_signal),
FecCommand::Repair(c) => repair_subcommand(c, cancel_signal),
}
}
/// Generate FEC data for a file.
#[derive(Debug, Parser)]
struct GenerateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
/// Number of parity bytes per RS block (min 2, max 24).
#[arg(short, long, value_name = "BYTES", default_value = "2")]
parity: u8,
}
/// Update FEC data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to FEC data.
///
/// The file will be modified in place.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
}
/// Repair a file.
#[derive(Debug, Parser)]
struct RepairCli {
/// Path to data.
///
/// The file will be modified in place.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input FEC data.
#[arg(short, long, value_name = "FILE", value_parser)]
fec: PathBuf,
}
#[derive(Debug, Subcommand)]
enum FecCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
Repair(RepairCli),
}
/// Generate dm-verity FEC data and verify/repair files.
///
/// These commands operate on FEC files with AOSP's header format.
#[derive(Debug, Parser)]
pub struct FecCli {
#[command(subcommand)]
command: FecCommand,
}
+176
View File
@@ -0,0 +1,176 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::{File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
sync::atomic::AtomicBool,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::hashtree::HashTreeImage,
stream::{FromReader, PSeekFile, ToWriter},
};
fn open_input(path: &Path, rw: bool) -> Result<PSeekFile> {
OpenOptions::new()
.read(true)
.write(rw)
.open(path)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open file: {path:?}"))
}
fn read_hash_tree(path: &Path) -> Result<HashTreeImage> {
let reader = File::open(path)
.map(BufReader::new)
.with_context(|| format!("Failed to open for reading: {path:?}"))?;
let hash_tree = HashTreeImage::from_reader(reader)
.with_context(|| format!("Failed to read hash tree data: {path:?}"))?;
Ok(hash_tree)
}
fn write_hash_tree(path: &Path, hash_tree: &HashTreeImage) -> Result<()> {
let mut writer = File::create(path)
.map(BufWriter::new)
.with_context(|| format!("Failed to open for writing: {path:?}"))?;
hash_tree
.to_writer(&mut writer)
.with_context(|| format!("Failed to write hash tree data: {path:?}"))?;
writer
.flush()
.with_context(|| format!("Failed to flush hash tree data: {path:?}"))?;
Ok(())
}
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let salt = hex::decode(&cli.salt).context("Invalid salt")?;
let input = open_input(&cli.input, false)?;
let hash_tree =
HashTreeImage::generate(&input, cli.block_size, &cli.algorithm, &salt, cancel_signal)
.context("Failed to generate hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn update_subcommand(cli: &UpdateCli, cancel_signal: &AtomicBool) -> Result<()> {
let ranges = cli
.range
.chunks_exact(2)
.map(|w| w[0]..w[1])
.collect::<Vec<_>>();
let input = open_input(&cli.input, false)?;
let mut hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.update(&input, &ranges, cancel_signal)
.context("Failed to update hash tree data")?;
write_hash_tree(&cli.hash_tree, &hash_tree)?;
Ok(())
}
fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let hash_tree = read_hash_tree(&cli.hash_tree)?;
hash_tree
.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
}
pub fn hash_tree_main(cli: &HashTreeCli, cancel_signal: &AtomicBool) -> Result<()> {
match &cli.command {
HashTreeCommand::Generate(c) => generate_subcommand(c, cancel_signal),
HashTreeCommand::Update(c) => update_subcommand(c, cancel_signal),
HashTreeCommand::Verify(c) => verify_subcommand(c, cancel_signal),
}
}
/// Generate hash tree data for a file.
#[derive(Debug, Parser)]
struct GenerateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Block size.
#[arg(short, long, value_name = "BYTES", default_value = "4096")]
block_size: u32,
/// Hash algorithm.
#[arg(short, long, value_name = "NAME", default_value = "sha256")]
algorithm: String,
/// Salt (in hex).
#[arg(short, long, value_name = "HEX", default_value = "")]
salt: String,
}
/// Update hash tree data after a file is modified.
#[derive(Debug, Parser)]
struct UpdateCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to hash tree data.
///
/// The file will be modified in place.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
/// Input file ranges that were updated.
///
/// This is a half-open range and can be specified multiple times.
#[arg(short, long, value_names = ["START", "END"], num_args = 2)]
range: Vec<u64>,
}
/// Verify that a file contains no errors.
#[derive(Debug, Parser)]
struct VerifyCli {
/// Path to input data.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to input hash tree data.
#[arg(short = 'H', long, value_name = "FILE", value_parser)]
hash_tree: PathBuf,
}
#[derive(Debug, Subcommand)]
enum HashTreeCommand {
Generate(GenerateCli),
Update(UpdateCli),
Verify(VerifyCli),
}
/// Generate dm-verity hash tree data and verify files.
///
/// These commands operate on a standard hash tree data prepended by a custom
/// header.
#[derive(Debug, Parser)]
pub struct HashTreeCli {
#[command(subcommand)]
command: HashTreeCommand,
}
+44 -22
View File
@@ -10,7 +10,7 @@ use std::{
time::Duration,
};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use clap::{Args, Parser, Subcommand};
use crate::{
@@ -18,59 +18,68 @@ use crate::{
format::avb,
};
fn get_passphrase(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
if let Some(v) = &group.pass_env_var {
PassphraseSource::EnvVar(v.clone())
} else if let Some(p) = &group.pass_file {
PassphraseSource::File(p.clone())
} else {
PassphraseSource::Prompt(format!("Enter passphrase for {key_path:?}: "))
}
fn get_passphrase_source(group: &PassphraseGroup, key_path: &Path) -> PassphraseSource {
PassphraseSource::new(
key_path,
group.pass_file.as_deref(),
group.pass_env_var.as_deref(),
)
}
pub fn key_main(cli: &KeyCli) -> Result<()> {
match &cli.command {
KeyCommand::GenerateKey(c) => {
let passphrase = get_passphrase(&c.passphrase, &c.output);
let source = get_passphrase_source(&c.passphrase, &c.output);
let private_key =
crypto::generate_rsa_key_pair().context("Failed to generate RSA keypair")?;
crypto::write_pem_key_file(&c.output, &private_key, &passphrase)
.with_context(|| anyhow!("Failed to write private key: {:?}", c.output))?;
crypto::write_pem_key_file(&c.output, &private_key, &source)
.with_context(|| format!("Failed to write private key: {:?}", c.output))?;
}
KeyCommand::GenerateCert(c) => {
let passphrase = get_passphrase(&c.passphrase, &c.key);
let private_key = crypto::read_pem_key_file(&c.key, &passphrase)
.with_context(|| anyhow!("Failed to load key: {:?}", c.key))?;
let source = get_passphrase_source(&c.passphrase, &c.key);
let private_key = crypto::read_pem_key_file(&c.key, &source)
.with_context(|| format!("Failed to load key: {:?}", c.key))?;
let validity = Duration::from_secs(c.validity * 24 * 60 * 60);
let cert = crypto::generate_cert(&private_key, rand::random(), validity, &c.subject)
.context("Failed to generate certificate")?;
crypto::write_pem_cert_file(&c.output, &cert)
.with_context(|| anyhow!("Failed to write certificate: {:?}", c.output))?;
.with_context(|| format!("Failed to write certificate: {:?}", c.output))?;
}
KeyCommand::ExtractAvb(c) => {
let public_key = if let Some(p) = &c.input.key {
let passphrase = get_passphrase(&c.passphrase, p);
let passphrase = get_passphrase_source(&c.passphrase, p);
let private_key = crypto::read_pem_key_file(p, &passphrase)
.with_context(|| anyhow!("Failed to load key: {p:?}"))?;
.with_context(|| format!("Failed to load key: {p:?}"))?;
private_key.to_public_key()
} else if let Some(p) = &c.input.cert {
let certificate = crypto::read_pem_cert_file(p)
.with_context(|| anyhow!("Failed to load certificate: {p:?}"))?;
.with_context(|| format!("Failed to load certificate: {p:?}"))?;
crypto::get_public_key(&certificate)?
crypto::get_public_key(&certificate)
.with_context(|| format!("Failed to extract public key: {p:?}"))?
} else {
unreachable!()
};
let encoded = avb::encode_public_key(&public_key)
.with_context(|| anyhow!("Failed to encode public key in AVB format"))?;
.context("Failed to encode public key in AVB format")?;
fs::write(&c.output, encoded)
.with_context(|| anyhow!("Failed to write public key: {:?}", c.output))?;
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
}
KeyCommand::DecodeAvb(c) => {
let encoded = fs::read(&c.key)
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
let public_key = avb::decode_public_key(&encoded)
.context("Failed to decode public key as AVB format")?;
crypto::write_pem_public_key_file(&c.output, &public_key)
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
}
}
@@ -153,11 +162,24 @@ struct ExtractAvbCli {
passphrase: PassphraseGroup,
}
/// Convert an AVB-encoded public key to a PKCS8-encoded public key.
#[derive(Debug, Parser)]
struct DecodeAvbCli {
/// Path to output PKCS8-encoded public key.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
/// Path to AVB-encoded public key.
#[arg(short, long, value_name = "FILE", value_parser)]
key: PathBuf,
}
#[derive(Debug, Subcommand)]
enum KeyCommand {
GenerateKey(GenerateKeyCli),
GenerateCert(GenerateCertCli),
ExtractAvb(ExtractAvbCli),
DecodeAvb(DecodeAvbCli),
}
/// Generate and convert keys.
+5 -3
View File
@@ -7,19 +7,21 @@ pub mod args;
pub mod avb;
pub mod boot;
pub mod completion;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod key;
pub mod ota;
pub mod ramdisk;
macro_rules! status {
($($arg:tt)*) => {
println!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
eprintln!("\x1b[1m[*] {}\x1b[0m", format!($($arg)*))
}
}
macro_rules! warning {
($($arg:tt)*) => {
println!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)+))
eprintln!("\x1b[1;31m[WARNING] {}\x1b[0m", format!($($arg)*))
}
}
+936 -526
View File
File diff suppressed because it is too large Load Diff
-153
View File
@@ -1,153 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fs::File,
path::{Path, PathBuf},
str,
};
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use crate::{
format::{
compression::{CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntryNew},
},
util::EscapedString,
};
static CONTENT_BEGIN: &str = "----- BEGIN UTF-8 CONTENT -----";
static CONTENT_END: &str = "----- END UTF-8 CONTENT -----";
static CONTENT_END_NO_NEWLINE: &str = "----- END UTF-8 CONTENT (NO NEWLINE) -----";
static BINARY_BEGIN: &str = "----- BEGIN BINARY CONTENT -----";
static BINARY_END: &str = "----- END BINARY CONTENT -----";
static BINARY_END_TRUNCATED: &str = "----- END BINARY CONTENT (TRUNCATED) -----";
static NO_DATA: &str = "----- NO DATA -----";
fn print_content(data: &[u8], truncate: bool) {
if data.is_empty() {
println!("{NO_DATA}");
return;
}
if !data.contains(&b'\0') {
if let Ok(s) = str::from_utf8(data) {
if !s.contains(CONTENT_BEGIN)
&& !s.contains(CONTENT_END)
&& !s.contains(CONTENT_END_NO_NEWLINE)
{
println!("{CONTENT_BEGIN}");
print!("{s}");
if data.last() == Some(&b'\n') {
println!("{CONTENT_END}");
} else {
println!();
println!("{CONTENT_END_NO_NEWLINE}");
}
return;
}
}
}
println!("{BINARY_BEGIN}");
if data.len() > 512 && truncate {
println!("{}", EscapedString::new_unquoted(&data[..512]));
println!("{BINARY_END_TRUNCATED}");
} else {
println!("{}", EscapedString::new_unquoted(&data));
println!("{BINARY_END}");
}
}
fn load_archive(
path: &Path,
include_trailer: bool,
) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
let file = File::open(path)?;
let reader = CompressedReader::new(file, true)?;
let format = reader.format();
let entries = cpio::load(reader, include_trailer)?;
Ok((entries, format))
}
fn save_archive(path: &Path, entries: &[CpioEntryNew], format: CompressedFormat) -> Result<()> {
let file = File::create(path)?;
let mut writer = CompressedWriter::new(file, format)?;
cpio::save(&mut writer, entries, false)?;
writer.finish()?;
Ok(())
}
pub fn ramdisk_main(cli: &RamdiskCli) -> Result<()> {
match &cli.command {
RamdiskCommand::Dump(c) => {
let (entries, format) = load_archive(&c.input, true)
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
println!("Compression format: {format:?}");
println!();
for entry in entries {
println!("{entry}");
print_content(&entry.content, !c.no_truncate);
println!();
}
}
RamdiskCommand::Repack(c) => {
let (entries, format) = load_archive(&c.input, false)
.with_context(|| format!("Failed to read cpio: {:?}", c.input))?;
save_archive(&c.output, &entries, format)
.with_context(|| format!("Failed to write cpio: {:?}", c.output))?;
}
}
Ok(())
}
/// Dump cpio headers and data.
#[derive(Debug, Parser)]
struct DumpCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Do not truncate binary file contents.
#[arg(long)]
no_truncate: bool,
}
/// Repack cpio archive.
#[derive(Debug, Parser)]
struct RepackCli {
/// Path to input cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
input: PathBuf,
/// Path to output cpio file.
#[arg(short, long, value_name = "FILE", value_parser)]
output: PathBuf,
}
#[derive(Debug, Subcommand)]
enum RamdiskCommand {
Dump(DumpCli),
Repack(RepackCli),
}
/// Show information about ramdisk cpio archives.
#[derive(Debug, Parser)]
pub struct RamdiskCli {
#[command(subcommand)]
command: RamdiskCommand,
}
+29 -2
View File
@@ -5,7 +5,7 @@
use std::{
env::{self, VarError},
ffi::OsString,
ffi::{OsStr, OsString},
fs::{self, File, OpenOptions},
io::{self, BufReader, BufWriter, Read, Write},
path::{Path, PathBuf},
@@ -61,7 +61,7 @@ pub enum Error {
#[error("DER error")]
Der(#[from] x509_cert::der::Error),
#[error("RSA error")]
RsaSign(#[from] rsa::Error),
Rsa(#[from] rsa::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
@@ -75,6 +75,16 @@ pub enum PassphraseSource {
}
impl PassphraseSource {
pub fn new(key_file: &Path, pass_file: Option<&Path>, env_var: Option<&OsStr>) -> Self {
if let Some(v) = env_var {
Self::EnvVar(v.to_owned())
} else if let Some(p) = pass_file {
Self::File(p.to_owned())
} else {
Self::Prompt(format!("Enter passphrase for {key_file:?}: "))
}
}
pub fn acquire(&self, confirm: bool) -> Result<String> {
let passphrase = match self {
Self::Prompt(p) => {
@@ -215,6 +225,23 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
write_pem_cert(writer, cert)
}
/// Write PEM-encoded PKCS8 public key to a writer.
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
let data = key.to_public_key_pem(LineEnding::LF)?;
writer.write_all(data.as_bytes())?;
Ok(())
}
/// Write PEM-encoded PKCS8 public key to a file.
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
let file = File::create(path)?;
let writer = BufWriter::new(file);
write_pem_public_key(writer, key)
}
/// Read PEM-encoded PKCS8 private key from a reader.
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
let mut data = String::new();
+163
View File
@@ -0,0 +1,163 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{fmt, marker::PhantomData};
use bstr::{ByteSlice, ByteVec};
use serde::{de::Visitor, Deserializer, Serializer};
use thiserror::Error;
#[derive(Clone, Debug, Error)]
pub enum Error {
#[error("Decoded string size ({actual}) does not match expected size ({expected})")]
BadLength { expected: usize, actual: usize },
}
pub trait FromEscaped: Sized {
type Error;
fn from_escaped(data: &str) -> Result<Self, Self::Error>;
}
impl FromEscaped for Vec<u8> {
type Error = Error;
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
Ok(Self::unescape_bytes(data))
}
}
impl<const N: usize> FromEscaped for [u8; N] {
type Error = Error;
fn from_escaped(data: &str) -> Result<Self, Self::Error> {
// Wasteful allocation, but bstr doesn't expose its decoder iterator in
// its public API.
let decoded = Vec::<u8>::from_escaped(data)?;
let mut buf = [0u8; N];
if decoded.len() != buf.len() {
return Err(Error::BadLength {
expected: buf.len(),
actual: decoded.len(),
});
}
buf.copy_from_slice(&decoded);
Ok(buf)
}
}
pub fn serialize<S, T>(data: T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<[u8]>,
{
let s = data.as_ref().escape_bytes().to_string();
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromEscaped,
<T as FromEscaped>::Error: fmt::Display,
{
struct EscapedStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for EscapedStrVisitor<T>
where
T: FromEscaped,
<T as FromEscaped>::Error: fmt::Display,
{
type Value = T;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "an escaped string")
}
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
FromEscaped::from_escaped(data).map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(EscapedStrVisitor(PhantomData))
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use serde::{Deserialize, Serialize};
use super::*;
#[test]
fn decode_vec() {
for s in ["", "abc", "你好", "💩", "\x00", "\t\r\n"] {
let escaped_s = s.as_bytes().escape_bytes().to_string();
let decoded = Vec::<u8>::from_escaped(&escaped_s).unwrap();
assert_eq!(decoded, s.as_bytes());
}
}
#[test]
fn decode_array() {
assert_matches!(
<[u8; 4]>::from_escaped(r"\t\r\n"),
Err(Error::BadLength {
expected: 4,
actual: 3,
})
);
assert_matches!(
<[u8; 4]>::from_escaped(r"\t\r\n\x00\x00"),
Err(Error::BadLength {
expected: 4,
actual: 5,
})
);
assert_matches!(
<[u8; 4]>::from_escaped(r"\t\r\n\x00"),
Ok(data) if data == *b"\t\r\n\x00"
);
assert_matches!(
<[u8; 0]>::from_escaped(r"\t"),
Err(Error::BadLength {
expected: 0,
actual: 1,
})
);
assert_matches!(
<[u8; 0]>::from_escaped(r""),
Ok(data) if data.is_empty()
);
}
#[test]
fn round_trip_serde() {
#[derive(Deserialize, Serialize)]
struct TestData {
#[serde(with = "super")]
contents: Vec<u8>,
}
let mut contents = b"foo\xffbar".to_vec();
contents.extend("💩".as_bytes());
let data = TestData { contents };
let serialized = toml_edit::ser::to_string(&data).unwrap();
assert_eq!(serialized, "contents = 'foo\\xFFbar💩'\n");
let new_data: TestData = toml_edit::de::from_str(&serialized).unwrap();
assert_eq!(data.contents, new_data.contents);
}
}
+750 -511
View File
File diff suppressed because it is too large Load Diff
+195 -108
View File
@@ -57,6 +57,15 @@ const VENDOR_HDR_V3_SIZE: u32 = 2112;
const VENDOR_HDR_V4_EXTRA_SIZE: u32 = 16;
const VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE: u32 = 108;
/// Maximum size of any individual boot image component, like the kernel. This
/// limit is currently 64 MiB, which should be sufficient since there is no
/// known device where the entire boot image exceeds this size.
const COMPONENT_MAX_SIZE: u32 = 64 * 1024 * 1024;
/// Maximum size of the bootconfig component in vendor v4 images. This limit is
/// currently 1 KiB, which is ~25x the size of the Pixel 7 Pro stock image's
/// bootconfig.
const BOOTCONFIG_MAX_SIZE: u32 = 1024;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown boot image format")]
@@ -71,8 +80,8 @@ pub enum Error {
WriteFieldError(&'static str, #[source] io::Error),
#[error("{0:?} field: invalid value: {1}")]
InvalidFieldValue(&'static str, u32),
#[error("{0:?} field exceeds integer bounds")]
IntegerTooLarge(&'static str),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("Invalid data: {0}")]
InvalidData(&'static str),
#[error("VTS signature is missing hash descriptor")]
@@ -257,6 +266,16 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
return Err(Error::UnknownHeaderVersion(header_version));
}
if kernel_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("kernel_size"));
} else if ramdisk_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("ramdisk_size"));
} else if second_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("second_size"));
} else if page_size == 0 {
return Err(Error::InvalidFieldValue("page_size", 0));
}
let os_version = reader.read_u32::<LittleEndian>()?;
let name = reader
@@ -281,6 +300,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
let mut v1_data = if header_version >= 1 {
let recovery_dtbo_size = reader.read_u32::<LittleEndian>()?;
if recovery_dtbo_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
}
let recovery_dtbo_offset = reader.read_u64::<LittleEndian>()?;
let header_size = reader.read_u32::<LittleEndian>()?;
@@ -305,6 +328,10 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
let mut v2_data = if header_version == 2 {
let dtb_size = reader.read_u32::<LittleEndian>()?;
if dtb_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("dtb_size"));
}
let dtb_addr = reader.read_u64::<LittleEndian>()?;
let v2_extra = V2Extra {
@@ -329,29 +356,39 @@ impl<R: Read> FromReader<R> for BootImageV0Through2 {
let mut ramdisk = vec![];
let mut second = vec![];
kernel.resize(kernel_size.to_usize().unwrap(), 0);
reader.read_exact(&mut kernel)?;
kernel.resize(kernel_size as usize, 0);
reader
.read_exact(&mut kernel)
.map_err(|e| Error::ReadFieldError("kernel", e))?;
padding::read_discard(&mut reader, page_size.into())?;
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
reader.read_exact(&mut ramdisk)?;
ramdisk.resize(ramdisk_size as usize, 0);
reader
.read_exact(&mut ramdisk)
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
padding::read_discard(&mut reader, page_size.into())?;
second.resize(second_size.to_usize().unwrap(), 0);
reader.read_exact(&mut second)?;
second.resize(second_size as usize, 0);
reader
.read_exact(&mut second)
.map_err(|e| Error::ReadFieldError("second", e))?;
padding::read_discard(&mut reader, page_size.into())?;
if let Some(v1) = &mut v1_data {
v1.v1_extra
.recovery_dtbo
.resize(v1.recovery_dtbo_size.to_usize().unwrap(), 0);
reader.read_exact(&mut v1.v1_extra.recovery_dtbo)?;
.resize(v1.recovery_dtbo_size as usize, 0);
reader
.read_exact(&mut v1.v1_extra.recovery_dtbo)
.map_err(|e| Error::ReadFieldError("recovery_dtbo", e))?;
padding::read_discard(&mut reader, page_size.into())?;
}
if let Some(v2) = &mut v2_data {
v2.v2_extra.dtb.resize(v2.dtb_size.to_usize().unwrap(), 0);
reader.read_exact(&mut v2.v2_extra.dtb)?;
v2.v2_extra.dtb.resize(v2.dtb_size as usize, 0);
reader
.read_exact(&mut v2.v2_extra.dtb)
.map_err(|e| Error::ReadFieldError("dtb", e))?;
padding::read_discard(&mut reader, page_size.into())?;
}
@@ -381,30 +418,35 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
type Error = Error;
fn to_writer(&self, writer: W) -> Result<()> {
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("kernel_size"));
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("ramdisk_size"));
} else if self.second.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("second_size"));
} else if self.page_size == 0 {
return Err(Error::InvalidFieldValue("page_size", 0));
}
if let Some(v1) = &self.v1_extra {
if v1.recovery_dtbo.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("recovery_dtbo_size"));
}
}
if let Some(v2) = &self.v2_extra {
if v2.dtb.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("dtb_size"));
}
}
let mut writer = CountingWriter::new(writer);
let kernel_size = self
.kernel
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
let ramdisk_size = self
.ramdisk
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
let second_size = self
.second
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("second_size"))?;
writer.write_all(&BOOT_MAGIC)?;
writer.write_u32::<LittleEndian>(kernel_size)?;
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
writer.write_u32::<LittleEndian>(ramdisk_size)?;
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
writer.write_u32::<LittleEndian>(second_size)?;
writer.write_u32::<LittleEndian>(self.second.len() as u32)?;
writer.write_u32::<LittleEndian>(self.second_addr)?;
writer.write_u32::<LittleEndian>(self.tags_addr)?;
writer.write_u32::<LittleEndian>(self.page_size)?;
@@ -427,46 +469,44 @@ impl<W: Write> ToWriter<W> for BootImageV0Through2 {
.map_err(|e| Error::WriteFieldError("extra_cmdline", e))?;
if let Some(v1) = &self.v1_extra {
let recovery_dtbo_size = v1
.recovery_dtbo
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("recovery_dtbo_size"))?;
writer.write_u32::<LittleEndian>(recovery_dtbo_size)?;
writer.write_u32::<LittleEndian>(v1.recovery_dtbo.len() as u32)?;
writer.write_u64::<LittleEndian>(v1.recovery_dtbo_offset)?;
writer.write_u32::<LittleEndian>(self.header_size())?;
}
if let Some(v2) = &self.v2_extra {
let dtb_size = v2
.dtb
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
writer.write_u32::<LittleEndian>(dtb_size)?;
writer.write_u32::<LittleEndian>(v2.dtb.len() as u32)?;
writer.write_u64::<LittleEndian>(v2.dtb_addr)?;
}
padding::write_zeros(&mut writer, self.page_size.into())?;
writer.write_all(&self.kernel)?;
writer
.write_all(&self.kernel)
.map_err(|e| Error::WriteFieldError("kernel", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
writer.write_all(&self.ramdisk)?;
writer
.write_all(&self.ramdisk)
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
writer.write_all(&self.second)?;
writer
.write_all(&self.second)
.map_err(|e| Error::WriteFieldError("second", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
if let Some(v1) = &self.v1_extra {
writer.write_all(&v1.recovery_dtbo)?;
writer
.write_all(&v1.recovery_dtbo)
.map_err(|e| Error::WriteFieldError("recovery_dtbo", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
}
if let Some(v2) = &self.v2_extra {
writer.write_all(&v2.dtb)?;
writer
.write_all(&v2.dtb)
.map_err(|e| Error::WriteFieldError("dtb", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
}
@@ -572,12 +612,23 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
return Err(Error::UnknownHeaderVersion(header_version));
}
if kernel_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("kernel_size"));
} else if ramdisk_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("ramdisk_size"));
}
let cmdline = reader
.read_string_padded(BOOT_ARGS_SIZE + BOOT_EXTRA_ARGS_SIZE)
.map_err(|e| Error::ReadFieldError("cmdline", e))?;
let signature_size = if header_version == 4 {
Some(reader.read_u32::<LittleEndian>()?)
let signature_size = reader.read_u32::<LittleEndian>()?;
if signature_size > HDR_V4_SIGNATURE_SIZE as u32 {
return Err(Error::FieldOutOfBounds("signature_size"));
}
Some(signature_size)
} else {
None
};
@@ -591,20 +642,26 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
let mut kernel = vec![];
let mut ramdisk = vec![];
kernel.resize(kernel_size.to_usize().unwrap(), 0);
reader.read_exact(&mut kernel)?;
kernel.resize(kernel_size as usize, 0);
reader
.read_exact(&mut kernel)
.map_err(|e| Error::ReadFieldError("kernel", e))?;
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
ramdisk.resize(ramdisk_size.to_usize().unwrap(), 0);
reader.read_exact(&mut ramdisk)?;
ramdisk.resize(ramdisk_size as usize, 0);
reader
.read_exact(&mut ramdisk)
.map_err(|e| Error::ReadFieldError("ramdisk", e))?;
padding::read_discard(&mut reader, PAGE_SIZE.into())?;
// Don't preserve the signature. It is only used for VTS tests and is
// not relevant for booting.
let v4_extra = if let Some(s) = signature_size {
// OnePlus images have an invalid signature consisting of all zeros.
let mut data = vec![0u8; s.to_usize().unwrap()];
reader.read_exact(&mut data)?;
let mut data = vec![0u8; s as usize];
reader
.read_exact(&mut data)
.map_err(|e| Error::ReadFieldError("signature", e))?;
let signature = if s > 0 && !util::is_zero(&data) {
Some(Header::from_reader(Cursor::new(data))?)
@@ -634,22 +691,17 @@ impl<R: Read> FromReader<R> for BootImageV3Through4 {
impl BootImageV3Through4 {
fn to_writer_internal(&self, writer: impl Write, skip_v4_sig: bool) -> Result<()> {
if self.kernel.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("kernel_size"));
} else if self.ramdisk.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("ramdisk_size"));
}
let mut writer = CountingWriter::new(writer);
let kernel_size = self
.kernel
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("kernel_size"))?;
let ramdisk_size = self
.ramdisk
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("ramdisk_size"))?;
writer.write_all(&BOOT_MAGIC)?;
writer.write_u32::<LittleEndian>(kernel_size)?;
writer.write_u32::<LittleEndian>(ramdisk_size)?;
writer.write_u32::<LittleEndian>(self.kernel.len() as u32)?;
writer.write_u32::<LittleEndian>(self.ramdisk.len() as u32)?;
writer.write_u32::<LittleEndian>(self.os_version)?;
writer.write_u32::<LittleEndian>(self.header_size())?;
@@ -673,7 +725,7 @@ impl BootImageV3Through4 {
// The VTS signature is always a fixed size.
if size > HDR_V4_SIGNATURE_SIZE {
return Err(Error::IntegerTooLarge("signature_size"));
return Err(Error::FieldOutOfBounds("signature_size"));
}
padding::write_zeros(&mut sig_writer, HDR_V4_SIGNATURE_SIZE)?;
@@ -688,15 +740,21 @@ impl BootImageV3Through4 {
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
writer.write_all(&self.kernel)?;
writer
.write_all(&self.kernel)
.map_err(|e| Error::WriteFieldError("kernel", e))?;
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
writer.write_all(&self.ramdisk)?;
writer
.write_all(&self.ramdisk)
.map_err(|e| Error::WriteFieldError("ramdisk", e))?;
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
if !skip_v4_sig {
if let Some(sig) = v4_signature {
writer.write_all(&sig)?;
writer
.write_all(&sig)
.map_err(|e| Error::WriteFieldError("signature", e))?;
padding::write_zeros(&mut writer, PAGE_SIZE.into())?;
}
}
@@ -910,9 +968,17 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
}
let page_size = reader.read_u32::<LittleEndian>()?;
if page_size == 0 {
return Err(Error::InvalidFieldValue("page_size", 0));
}
let kernel_addr = reader.read_u32::<LittleEndian>()?;
let ramdisk_addr = reader.read_u32::<LittleEndian>()?;
let vendor_ramdisk_size = reader.read_u32::<LittleEndian>()?;
if vendor_ramdisk_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
}
let cmdline = reader
.read_string_padded(VENDOR_BOOT_ARGS_SIZE)
@@ -925,7 +991,12 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
.map_err(|e| Error::ReadFieldError("name", e))?;
let header_size = reader.read_u32::<LittleEndian>()?;
let dtb_size = reader.read_u32::<LittleEndian>()?;
if dtb_size > COMPONENT_MAX_SIZE {
return Err(Error::FieldOutOfBounds("dtb_size"));
}
let dtb_addr = reader.read_u64::<LittleEndian>()?;
struct V4Data {
@@ -938,14 +1009,18 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
let table_size = reader.read_u32::<LittleEndian>()?;
let table_entry_num = reader.read_u32::<LittleEndian>()?;
let table_entry_size = reader.read_u32::<LittleEndian>()?;
let bootconfig_size = reader.read_u32::<LittleEndian>()?;
if bootconfig_size > BOOTCONFIG_MAX_SIZE {
return Err(Error::FieldOutOfBounds("bootconfig_size"));
}
if table_entry_size != VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE {
return Err(Error::InvalidFieldValue(
"vendor_ramdisk_table_entry_size",
table_entry_size,
));
} else if table_size != table_entry_num * table_entry_size {
} else if table_entry_num.checked_mul(table_entry_size) != Some(table_size) {
return Err(Error::InvalidFieldValue(
"vendor_ramdisk_table_size",
table_size,
@@ -972,8 +1047,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
let mut ramdisks = vec![];
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size.to_usize().unwrap()];
reader.read_exact(&mut vendor_ramdisk_data)?;
let mut vendor_ramdisk_data = vec![0u8; vendor_ramdisk_size as usize];
reader
.read_exact(&mut vendor_ramdisk_data)
.map_err(|e| Error::ReadFieldError("vendor_ramdisk_data", e))?;
padding::read_discard(&mut reader, page_size.into())?;
// For v3, this is just one big ramdisk. For v4, we have to wait until
@@ -984,8 +1061,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
vendor_ramdisk_data = vec![];
}
let mut dtb = vec![0u8; dtb_size.to_usize().unwrap()];
reader.read_exact(&mut dtb)?;
let mut dtb = vec![0u8; dtb_size as usize];
reader
.read_exact(&mut dtb)
.map_err(|e| Error::ReadFieldError("dtb", e))?;
padding::read_discard(&mut reader, page_size.into())?;
if let Some(v4) = &mut v4_data {
@@ -994,6 +1073,10 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
for _ in 0..v4.vendor_ramdisk_table_entry_num {
let ramdisk_size = reader.read_u32::<LittleEndian>()?;
if ramdisk_size > vendor_ramdisk_size {
return Err(Error::FieldOutOfBounds("ramdisk_size"));
}
let ramdisk_offset = reader.read_u32::<LittleEndian>()?;
let ramdisk_type = reader.read_u32::<LittleEndian>()?;
@@ -1010,7 +1093,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
return Err(Error::InvalidFieldValue("ramdisk_offset", ramdisk_offset));
}
let mut ramdisk = vec![0u8; ramdisk_size.to_usize().unwrap()];
let mut ramdisk = vec![0u8; ramdisk_size as usize];
ramdisk_reader.read_exact(&mut ramdisk)?;
ramdisks.push(ramdisk);
@@ -1033,7 +1116,7 @@ impl<R: Read> FromReader<R> for VendorBootImageV3Through4 {
padding::read_discard(&mut reader, page_size.into())?;
v4.v4_extra.bootconfig = reader
.read_string_padded(v4.bootconfig_size.to_usize().unwrap())
.read_string_padded(v4.bootconfig_size as usize)
.map_err(|e| Error::ReadFieldError("bootconfig", e))?;
}
@@ -1070,27 +1153,29 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
return Err(Error::InvalidData("v3 only supports one ramdisk"));
}
let mut writer = CountingWriter::new(writer);
let vendor_ramdisk_size = self.ramdisks.iter().map(|r| r.len()).sum::<usize>();
if vendor_ramdisk_size > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("vendor_ramdisk_size"));
} else if self.dtb.len() > COMPONENT_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("dtb_size"));
} else if self.page_size == 0 {
return Err(Error::InvalidFieldValue("page_size", 0));
}
let vendor_ramdisk_size = self
.ramdisks
.iter()
.map(|r| r.len())
.sum::<usize>()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_size"))?;
let dtb_size = self
.dtb
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("dtb_size"))?;
if let Some(v4) = &self.v4_extra {
if v4.bootconfig.len() > BOOTCONFIG_MAX_SIZE as usize {
return Err(Error::FieldOutOfBounds("bootconfig_size"));
}
}
let mut writer = CountingWriter::new(writer);
writer.write_all(&VENDOR_BOOT_MAGIC)?;
writer.write_u32::<LittleEndian>(self.header_version())?;
writer.write_u32::<LittleEndian>(self.page_size)?;
writer.write_u32::<LittleEndian>(self.kernel_addr)?;
writer.write_u32::<LittleEndian>(self.ramdisk_addr)?;
writer.write_u32::<LittleEndian>(vendor_ramdisk_size)?;
writer.write_u32::<LittleEndian>(vendor_ramdisk_size as u32)?;
writer
.write_string_padded(&self.cmdline, VENDOR_BOOT_ARGS_SIZE)
@@ -1103,7 +1188,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
.map_err(|e| Error::WriteFieldError("name", e))?;
writer.write_u32::<LittleEndian>(self.header_size())?;
writer.write_u32::<LittleEndian>(dtb_size)?;
writer.write_u32::<LittleEndian>(self.dtb.len() as u32)?;
writer.write_u64::<LittleEndian>(self.dtb_addr)?;
if let Some(v4) = &self.v4_extra {
@@ -1111,38 +1196,37 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
.ramdisks
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_entry_num"))?;
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_entry_num"))?;
let vendor_ramdisk_table_size = vendor_ramdisk_table_entry_num
.checked_mul(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)
.and_then(|v| v.to_u32())
.ok_or_else(|| Error::IntegerTooLarge("vendor_ramdisk_table_size"))?;
let bootconfig_size = v4
.bootconfig
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("bootconfig"))?;
.ok_or_else(|| Error::FieldOutOfBounds("vendor_ramdisk_table_size"))?;
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_size)?;
writer.write_u32::<LittleEndian>(vendor_ramdisk_table_entry_num)?;
writer.write_u32::<LittleEndian>(VENDOR_RAMDISK_TABLE_ENTRY_V4_SIZE)?;
writer.write_u32::<LittleEndian>(bootconfig_size)?;
writer.write_u32::<LittleEndian>(v4.bootconfig.len() as u32)?;
}
padding::write_zeros(&mut writer, self.page_size.into())?;
for ramdisk in &self.ramdisks {
writer.write_all(ramdisk)?;
writer
.write_all(ramdisk)
.map_err(|e| Error::WriteFieldError("vendor_ramdisk_data", e))?;
}
padding::write_zeros(&mut writer, self.page_size.into())?;
writer.write_all(&self.dtb)?;
writer
.write_all(&self.dtb)
.map_err(|e| Error::WriteFieldError("dtb", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
if let Some(v4) = &self.v4_extra {
let mut ramdisk_offset = 0;
for (ramdisk, meta) in self.ramdisks.iter().zip(&v4.ramdisk_metas) {
let ramdisk_size = ramdisk.len().to_u32().unwrap();
let ramdisk_size = ramdisk.len() as u32;
writer.write_u32::<LittleEndian>(ramdisk_size)?;
writer.write_u32::<LittleEndian>(ramdisk_offset)?;
@@ -1160,7 +1244,9 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
}
padding::write_zeros(&mut writer, self.page_size.into())?;
writer.write_all(v4.bootconfig.as_bytes())?;
writer
.write_all(v4.bootconfig.as_bytes())
.map_err(|e| Error::WriteFieldError("bootconfig", e))?;
padding::write_zeros(&mut writer, self.page_size.into())?;
}
@@ -1169,6 +1255,7 @@ impl<W: Write> ToWriter<W> for VendorBootImageV3Through4 {
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum BootImage {
V0Through2(BootImageV0Through2),
V3Through4(BootImageV3Through4),
+30 -4
View File
@@ -8,17 +8,26 @@ use std::io::{self, Read, Seek, Write};
use byteorder::{LittleEndian, WriteBytesExt};
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
use lz4_flex::frame::FrameDecoder;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use xz2::{
read::XzDecoder,
stream::{Check, Stream},
write::XzEncoder,
};
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown compression format")]
UnknownFormat,
#[error("XZ stream error")]
XzStream(#[from] xz2::stream::Error),
#[error("I/O error")]
IoError(#[from] io::Error),
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -96,30 +105,34 @@ impl<W: Write> Write for Lz4LegacyEncoder<W> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum CompressedFormat {
None,
Gzip,
Lz4Legacy,
Xz,
}
pub enum CompressedReader<R: Read> {
None(R),
Gzip(GzDecoder<R>),
Lz4(FrameDecoder<R>),
Xz(XzDecoder<R>),
}
impl<R: Read + Seek> CompressedReader<R> {
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
let mut magic = [0u8; 4];
let mut magic = [0u8; 6];
reader.read_exact(&mut magic)?;
reader.rewind()?;
if &magic[0..2] == GZIP_MAGIC {
Ok(Self::Gzip(GzDecoder::new(reader)))
} else if &magic == LZ4_LEGACY_MAGIC {
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
Ok(Self::Lz4(FrameDecoder::new(reader)))
} else if &magic == XZ_MAGIC {
Ok(Self::Xz(XzDecoder::new(reader)))
} else if raw_if_unknown {
Ok(Self::None(reader))
} else {
@@ -132,6 +145,7 @@ impl<R: Read + Seek> CompressedReader<R> {
Self::None(_) => CompressedFormat::None,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
}
}
@@ -140,6 +154,7 @@ impl<R: Read + Seek> CompressedReader<R> {
Self::None(r) => r,
Self::Gzip(r) => r.into_inner(),
Self::Lz4(r) => r.into_inner(),
Self::Xz(r) => r.into_inner(),
}
}
}
@@ -150,6 +165,7 @@ impl<R: Read> Read for CompressedReader<R> {
Self::None(r) => r.read(buf),
Self::Gzip(r) => r.read(buf),
Self::Lz4(r) => r.read(buf),
Self::Xz(r) => r.read(buf),
}
}
}
@@ -158,6 +174,7 @@ pub enum CompressedWriter<W: Write> {
None(W),
Gzip(GzEncoder<W>),
Lz4Legacy(Lz4LegacyEncoder<W>),
Xz(XzEncoder<W>),
}
impl<W: Write> CompressedWriter<W> {
@@ -168,6 +185,11 @@ impl<W: Write> CompressedWriter<W> {
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
}
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
CompressedFormat::Xz => {
// Some kernels are compiled without support for the default CRC64.
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
}
}
}
@@ -176,6 +198,7 @@ impl<W: Write> CompressedWriter<W> {
Self::None(_) => CompressedFormat::None,
Self::Gzip(_) => CompressedFormat::Gzip,
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
Self::Xz(_) => CompressedFormat::Xz,
}
}
@@ -184,6 +207,7 @@ impl<W: Write> CompressedWriter<W> {
Self::None(w) => Ok(w),
Self::Gzip(w) => w.finish(),
Self::Lz4Legacy(w) => w.finish(),
Self::Xz(w) => w.finish(),
}
}
}
@@ -194,6 +218,7 @@ impl<W: Write> Write for CompressedWriter<W> {
Self::None(w) => w.write(buf),
Self::Gzip(w) => w.write(buf),
Self::Lz4Legacy(w) => w.write(buf),
Self::Xz(w) => w.write(buf),
}
}
@@ -202,6 +227,7 @@ impl<W: Write> Write for CompressedWriter<W> {
Self::None(w) => w.flush(),
Self::Gzip(w) => w.flush(),
Self::Lz4Legacy(w) => w.flush(),
Self::Xz(w) => w.flush(),
}
}
}
+557 -138
View File
@@ -4,18 +4,26 @@
*/
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fmt,
io::{self, Read, Write},
io::{self, Cursor, Read, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use num_traits::ToPrimitive;
use bstr::ByteSlice;
use num_traits::{ToPrimitive, Zero};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
escape,
format::padding,
stream::{CountingReader, CountingWriter, FromReader, ToWriter, WriteZerosExt},
util::{EscapedString, NumBytes},
octal,
stream::{
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
},
util::NumBytes,
};
const MAGIC_NEW: &[u8; 6] = b"070701";
@@ -34,16 +42,24 @@ const C_ISCTG: u32 = 0o110000;
const IO_BLOCK_SIZE: u64 = 512;
/// The threshold when reading data where memory allocation switches from
/// allocating the exact size to resizing as necessary.
const VEC_CAP_THRESHOLD: usize = 16384;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown magic: {0:?}")]
UnknownMagic([u8; 6]),
#[error("Hard links are not supported: {0}")]
HardLinksNotSupported(EscapedString<Vec<u8>>),
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
HardLinksNotSupported(Vec<u8>),
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
EntryHasData(CpioEntryType, Vec<u8>),
#[error("No inodes available for device {0:x},{1:x}")]
DeviceFull(u32, u32),
#[error("{0:?} field exceeds integer bounds")]
IntegerTooLarge(&'static str),
#[error("I/O error")]
IoError(#[from] io::Error),
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
@@ -60,7 +76,7 @@ fn read_int(mut reader: impl Read) -> io::Result<u32> {
let digit = c.to_digit(16).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("{0}: Invalid hex char: {1}", EscapedString::new(&buf), c),
format!("{:?}: Invalid hex char: {c}", buf.as_bstr()),
)
})?;
@@ -85,32 +101,200 @@ fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
writer.write_all(&buf)
}
fn file_type(mode: u32) -> u32 {
mode & 0o170000
/// Read a chunk of bytes from the reader. If `size` is less than
/// [`VEC_CAP_THRESHOLD`], then the buffer is allocated with the exact size.
/// Otherwise, the buffer starts with a capacity of [`VEC_CAP_THRESHOLD`] and
/// grows as necessary. This avoids allocating excessive memory when the entry
/// specifies an excessively large value that's not backed by actual data.
fn read_data(reader: impl Read, size: usize, cancel_signal: &AtomicBool) -> io::Result<Vec<u8>> {
let buf = Vec::with_capacity(size.min(VEC_CAP_THRESHOLD));
let mut cursor = Cursor::new(buf);
stream::copy_n(reader, &mut cursor, size as u64, cancel_signal)?;
Ok(cursor.into_inner())
}
#[derive(Clone, Default, PartialEq, Eq)]
pub struct CpioEntryNew {
pub ino: u32,
pub mode: u32,
pub uid: u32,
pub gid: u32,
pub nlink: u32,
pub mtime: u32,
pub dev_maj: u32,
pub dev_min: u32,
pub rdev_maj: u32,
pub rdev_min: u32,
pub chksum: u32,
pub name: Vec<u8>,
pub content: Vec<u8>,
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum CpioEntryType {
Pipe,
Char,
Directory,
Block,
Regular,
Symlink,
Socket,
Reserved,
Unknown(u16),
}
impl fmt::Debug for CpioEntryNew {
impl CpioEntryType {
pub fn from_mode(mode: u32) -> Self {
match mode & 0o170000 {
S_IFIFO => Self::Pipe,
S_IFCHR => Self::Char,
S_IFDIR => Self::Directory,
S_IFBLK => Self::Block,
S_IFREG => Self::Regular,
S_IFLNK => Self::Symlink,
S_IFSOCK => Self::Socket,
C_ISCTG => Self::Reserved,
m => Self::Unknown(m as u16),
}
}
pub fn to_mode(self) -> u32 {
match self {
Self::Pipe => S_IFIFO,
Self::Char => S_IFCHR,
Self::Directory => S_IFDIR,
Self::Block => S_IFBLK,
Self::Regular => S_IFREG,
Self::Symlink => S_IFLNK,
Self::Socket => S_IFSOCK,
Self::Reserved => C_ISCTG,
Self::Unknown(m) => m.into(),
}
}
}
impl fmt::Display for CpioEntryType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CpioEntryNew")
.field("ino", &self.ino)
.field("mode", &self.mode)
match self {
Self::Pipe => write!(f, "pipe"),
Self::Char => write!(f, "character device"),
Self::Directory => write!(f, "directory"),
Self::Block => write!(f, "block device"),
Self::Regular => write!(f, "regular file"),
Self::Symlink => write!(f, "symbolic link"),
Self::Socket => write!(f, "socket"),
Self::Reserved => write!(f, "reserved"),
Self::Unknown(m) => write!(f, "unknown ({m:o})"),
}
}
}
impl Default for CpioEntryType {
fn default() -> Self {
Self::Unknown(0)
}
}
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum CpioEntryData {
/// Size of entry's data. [`CpioReader`] and [`CpioWriter`] use this for
/// [`CpioEntryType::Regular`] entries to allow for lazy reads and writes.
Size(u32),
/// Entry's data. For [`CpioEntryType::Symlink`] entries, this is the
/// link target. For [`CpioEntryType::Regular`] entries, this is the file
/// content. [`CpioReader`] will never use this when reading regular files.
/// [`CpioWriter`] will write this immediately and lazy writes will not be
/// allowed.
Data(#[serde(with = "escape")] Vec<u8>),
}
impl fmt::Debug for CpioEntryData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Size(s) => f.debug_tuple("Size").field(s).finish(),
Self::Data(d) => f.debug_tuple("Data").field(&NumBytes(d.len())).finish(),
}
}
}
impl Default for CpioEntryData {
fn default() -> Self {
Self::Size(0)
}
}
impl CpioEntryData {
pub fn size(&self) -> Result<u32> {
let size = match self {
Self::Size(s) => *s,
Self::Data(d) => d
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("data_size"))?,
};
Ok(size)
}
fn is_size(&self) -> bool {
matches!(self, CpioEntryData::Size(_))
}
}
#[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpioEntry {
/// File path.
#[serde(with = "escape")]
pub path: Vec<u8>,
/// File data.
#[serde(default, skip_serializing_if = "CpioEntryData::is_size")]
pub data: CpioEntryData,
/// Inode number.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub inode: u32,
/// File type portion of the `st_mode`-style mode.
pub file_type: CpioEntryType,
/// Permissions portion of the `st_mode`-style mode.
#[serde(default, skip_serializing_if = "Zero::is_zero", with = "octal")]
pub file_mode: u16,
/// Owner user ID.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub uid: u32,
/// Owner group ID.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub gid: u32,
/// Number of paths referencing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub nlink: u32,
/// Modification timestamp in Unix time.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub mtime: u32,
/// Major ID (class of device) for the device containing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub dev_maj: u32,
/// Minor ID (specific device instance) for the device containing the inode.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub dev_min: u32,
/// Major ID (class of device) represented by this entry. This is only
/// relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub rdev_maj: u32,
/// Minor ID (specific device instance) represented by this entry. This is
/// only relevant for [`CpioEntryType::Char`] and [`CpioEntryType::Block`].
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub rdev_min: u32,
/// CRC32 checksum.
#[serde(default, skip_serializing_if = "Zero::is_zero")]
pub crc32: u32,
}
impl fmt::Debug for CpioEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CpioEntry")
.field("path", &self.path.as_bstr())
.field("data", &self.data)
.field("inode", &self.inode)
.field("file_type", &self.file_type)
.field("file_mode", &self.file_mode)
.field("uid", &self.uid)
.field("gid", &self.gid)
.field("nlink", &self.nlink)
@@ -119,84 +303,89 @@ impl fmt::Debug for CpioEntryNew {
.field("dev_min", &self.dev_min)
.field("rdev_maj", &self.rdev_maj)
.field("rdev_min", &self.rdev_min)
.field("chksum", &self.chksum)
.field("name", &EscapedString::new(&self.name))
.field("content", &NumBytes(self.content.len()))
.field("crc32", &self.crc32)
.finish()
}
}
impl fmt::Display for CpioEntryNew {
impl fmt::Display for CpioEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let file_type_str = match file_type(self.mode) {
S_IFIFO => Cow::Borrowed("pipe"),
S_IFCHR => Cow::Borrowed("character device"),
S_IFDIR => Cow::Borrowed("directory"),
S_IFBLK => Cow::Borrowed("block device"),
S_IFREG => Cow::Borrowed("regular file"),
S_IFLNK => Cow::Borrowed("symbolic link"),
S_IFSOCK => Cow::Borrowed("socket"),
C_ISCTG => Cow::Borrowed("reserved"),
m => Cow::Owned(format!("unknown ({m:o})")),
};
writeln!(f, "Filename: {}", EscapedString::new(&self.name))?;
writeln!(f, "Filetype: {file_type_str}")?;
writeln!(f, "Inode: {}", self.ino)?;
writeln!(f, "Mode: {:o}", self.mode)?;
writeln!(f, "UID: {}", self.uid)?;
writeln!(f, "GID: {}", self.gid)?;
writeln!(f, "Links: {}", self.nlink)?;
writeln!(f, "Modified: {}", self.mtime)?;
writeln!(f, "Device: {:x},{:x}", self.dev_maj, self.dev_min)?;
writeln!(f, "Device ID: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
writeln!(f, "Checksum: {:x}", self.chksum)?;
writeln!(f, "Content: {:?}", NumBytes(self.content.len()))?;
writeln!(f, "Path: {:?}", self.path.as_bstr())?;
match &self.data {
CpioEntryData::Size(s) => {
writeln!(f, "Data: {:?}", &NumBytes(*s))?;
}
CpioEntryData::Data(d) => {
if self.file_type == CpioEntryType::Symlink {
writeln!(f, "Data: {:?}", d.as_bstr())?;
} else {
writeln!(f, "Data: {:?}", &NumBytes(d.len()))?;
}
}
}
writeln!(f, "Inode: {}", self.inode)?;
writeln!(f, "Type: {}", self.file_type)?;
writeln!(f, "Mode: {:o}", self.file_mode)?;
writeln!(f, "UID: {}", self.uid)?;
writeln!(f, "GID: {}", self.gid)?;
writeln!(f, "Links: {}", self.nlink)?;
writeln!(f, "Modtime: {}", self.mtime)?;
writeln!(f, "Idevice: {:x},{:x}", self.dev_maj, self.dev_min)?;
writeln!(f, "Rdevice: {:x},{:x}", self.rdev_maj, self.rdev_min)?;
write!(f, "CRC32: {:x}", self.crc32)?;
Ok(())
}
}
impl CpioEntryNew {
impl CpioEntry {
pub fn new_trailer() -> Self {
Self {
path: CPIO_TRAILER.to_vec(),
// Must be 1 for CRC format.
nlink: 1,
name: CPIO_TRAILER.to_vec(),
..Default::default()
}
}
pub fn new_symlink(link_target: &[u8], name: &[u8]) -> Self {
pub fn new_symlink(path: &[u8], link_target: &[u8]) -> Self {
Self {
mode: S_IFLNK | 0o777,
path: path.to_owned(),
data: CpioEntryData::Data(link_target.to_owned()),
file_type: CpioEntryType::Symlink,
file_mode: 0o777,
nlink: 1,
name: name.to_owned(),
content: link_target.to_owned(),
..Default::default()
}
}
pub fn new_directory(name: &[u8]) -> Self {
pub fn new_directory(path: &[u8], mode: u16) -> Self {
Self {
mode: S_IFDIR,
path: path.to_owned(),
file_type: CpioEntryType::Directory,
file_mode: mode,
nlink: 1,
name: name.to_owned(),
..Default::default()
}
}
pub fn new_file(name: &[u8]) -> Self {
pub fn new_file(path: &[u8], mode: u16, data: CpioEntryData) -> Self {
Self {
mode: S_IFREG,
path: path.to_owned(),
data,
file_type: CpioEntryType::Regular,
file_mode: mode,
nlink: 1,
name: name.to_owned(),
..Default::default()
}
}
pub fn is_trailer(&self) -> bool {
self.path == CPIO_TRAILER
}
}
impl<R: Read> FromReader<R> for CpioEntryNew {
impl<R: Read> FromReader<R> for CpioEntry {
type Error = Error;
fn from_reader(reader: R) -> Result<Self> {
@@ -209,39 +398,61 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
return Err(Error::UnknownMagic(magic));
}
let ino = read_int(&mut reader)?;
let inode = read_int(&mut reader)?;
let mode = read_int(&mut reader)?;
let uid = read_int(&mut reader)?;
let gid = read_int(&mut reader)?;
let nlink = read_int(&mut reader)?;
let mtime = read_int(&mut reader)?;
let filesize = read_int(&mut reader)?;
let file_size = read_int(&mut reader)?;
let dev_maj = read_int(&mut reader)?;
let dev_min = read_int(&mut reader)?;
let rdev_maj = read_int(&mut reader)?;
let rdev_min = read_int(&mut reader)?;
let namesize = read_int(&mut reader)?;
let chksum = read_int(&mut reader)?;
let path_size = read_int(&mut reader)?;
let crc32 = read_int(&mut reader)?;
let mut name = vec![0u8; namesize.to_usize().unwrap()];
reader.read_exact(&mut name)?;
if name.last() != Some(&b'\0') {
let mut path = read_data(
&mut reader,
path_size.to_usize().unwrap(),
&AtomicBool::new(false),
)?;
if path.last() != Some(&b'\0') {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Filename is not NULL-terminated",
)
.into());
}
name.pop();
path.pop();
padding::read_discard(&mut reader, 4)?;
let mut content = vec![0u8; filesize.to_usize().unwrap()];
reader.read_exact(&mut content)?;
padding::read_discard(&mut reader, 4)?;
let file_type = CpioEntryType::from_mode(mode);
let data = match file_type {
// Handled by CpioReader for streaming reads.
CpioEntryType::Regular => CpioEntryData::Size(file_size),
// Symlinks store the target in the file data.
CpioEntryType::Symlink => {
let content = read_data(
&mut reader,
file_size.to_usize().unwrap(),
&AtomicBool::new(false),
)?;
padding::read_discard(&mut reader, 4)?;
CpioEntryData::Data(content)
}
// No other entry type should have data.
t if file_size != 0 => return Err(Error::EntryHasData(t, path)),
_ => CpioEntryData::Size(0),
};
Ok(Self {
ino,
mode,
path,
data,
inode,
file_type,
file_mode: (mode & 0o7777) as u16,
uid,
gid,
nlink,
@@ -250,76 +461,236 @@ impl<R: Read> FromReader<R> for CpioEntryNew {
dev_min,
rdev_maj,
rdev_min,
chksum,
name,
content,
crc32,
})
}
}
impl<W: Write> ToWriter<W> for CpioEntryNew {
impl<W: Write> ToWriter<W> for CpioEntry {
type Error = Error;
fn to_writer(&self, writer: W) -> Result<()> {
let mut writer = CountingWriter::new(writer);
let filesize = self
.content
.len()
.to_u32()
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
let namesize = self
.name
let path_size = self
.path
.len()
.checked_add(1)
.and_then(|s| s.to_u32())
.ok_or_else(|| Error::IntegerTooLarge("filesize"))?;
.ok_or_else(|| Error::IntegerTooLarge("path_size"))?;
if self.chksum == 0 {
let file_size = self.data.size()?;
if file_size != 0
&& self.file_type != CpioEntryType::Regular
&& self.file_type != CpioEntryType::Symlink
{
return Err(Error::EntryHasData(self.file_type, self.path.clone()));
}
if self.crc32 == 0 {
writer.write_all(MAGIC_NEW)?;
} else {
writer.write_all(MAGIC_NEW_CRC)?;
}
write_int(&mut writer, self.ino)?;
write_int(&mut writer, self.mode)?;
let mode = self.file_type.to_mode() | u32::from(self.file_mode & 0o7777);
write_int(&mut writer, self.inode)?;
write_int(&mut writer, mode)?;
write_int(&mut writer, self.uid)?;
write_int(&mut writer, self.gid)?;
write_int(&mut writer, self.nlink)?;
write_int(&mut writer, self.mtime)?;
write_int(&mut writer, filesize)?;
write_int(&mut writer, file_size)?;
write_int(&mut writer, self.dev_maj)?;
write_int(&mut writer, self.dev_min)?;
write_int(&mut writer, self.rdev_maj)?;
write_int(&mut writer, self.rdev_min)?;
write_int(&mut writer, namesize)?;
write_int(&mut writer, self.chksum)?;
write_int(&mut writer, path_size)?;
write_int(&mut writer, self.crc32)?;
writer.write_all(&self.name)?;
writer.write_all(&self.path)?;
writer.write_zeros_exact(1)?;
padding::write_zeros(&mut writer, 4)?;
writer.write_all(&self.content)?;
padding::write_zeros(&mut writer, 4)?;
if let CpioEntryData::Data(d) = &self.data {
writer.write_all(d)?;
padding::write_zeros(&mut writer, 4)?;
}
Ok(())
}
}
pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntryNew>> {
let mut entries = vec![];
pub struct CpioReader<R: Read> {
reader: R,
include_trailer: bool,
range: Option<Range<u64>>,
done: bool,
}
loop {
let entry = CpioEntryNew::from_reader(&mut reader)?;
if file_type(entry.mode) != S_IFDIR && entry.nlink > 1 {
return Err(Error::HardLinksNotSupported(EscapedString::new(entry.name)));
impl<R: Read> CpioReader<R> {
pub fn new(reader: R, include_trailer: bool) -> Self {
Self {
reader,
include_trailer,
range: None,
done: false,
}
}
pub fn into_inner(self) -> R {
self.reader
}
fn skip_data(&mut self) -> io::Result<()> {
if let Some(range) = &mut self.range {
// This cannot overflow because cpio file sizes are 32 bit.
let n = range.end - range.start + padding::calc(range.end, 4);
self.reader.read_discard_exact(n)?;
self.range = None;
}
if entry.name == CPIO_TRAILER {
if include_trailer {
entries.push(entry);
Ok(())
}
pub fn next_entry(&mut self) -> Result<Option<CpioEntry>> {
if self.done {
return Ok(None);
}
self.skip_data()?;
let entry = CpioEntry::from_reader(&mut self.reader)?;
if entry.is_trailer() {
self.done = true;
if !self.include_trailer {
return Ok(None);
}
break;
} else if let CpioEntryData::Size(s) = entry.data {
self.range = Some(0..u64::from(s));
}
Ok(Some(entry))
}
}
impl<R: Read> Read for CpioReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let Some(range) = &mut self.range else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No entry opened",
));
};
let to_read = (range.end - range.start).min(buf.len() as u64) as usize;
let n = self.reader.read(&mut buf[..to_read])?;
range.start += n as u64;
Ok(n)
}
}
pub struct CpioWriter<W: Write> {
writer: CountingWriter<W>,
pad_to_block_size: bool,
range: Option<Range<u64>>,
max_inode: u32,
}
impl<W: Write> CpioWriter<W> {
pub fn new(writer: W, pad_to_block_size: bool) -> Self {
Self {
writer: CountingWriter::new(writer),
pad_to_block_size,
range: None,
max_inode: 0,
}
}
fn finish_entry(&mut self) -> io::Result<()> {
if let Some(range) = &mut self.range {
// This cannot overflow because cpio file sizes are 32 bit.
let n = range.end - range.start + padding::calc(range.end, 4);
self.writer.write_zeros_exact(n)?;
self.range = None;
}
Ok(())
}
pub fn start_entry(&mut self, entry: &CpioEntry) -> Result<()> {
self.finish_entry()?;
entry.to_writer(&mut self.writer)?;
if let CpioEntryData::Size(s) = entry.data {
self.range = Some(0..u64::from(s));
}
self.max_inode = self.max_inode.max(entry.inode);
Ok(())
}
pub fn finish(mut self) -> Result<W> {
self.finish_entry()?;
self.start_entry(&CpioEntry::new_trailer())?;
// Pad until the end of the block.
if self.pad_to_block_size {
padding::write_zeros(&mut self.writer, IO_BLOCK_SIZE)?;
}
Ok(self.writer.finish().0)
}
}
impl<W: Write> Write for CpioWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let Some(range) = &mut self.range else {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"No entry started",
));
};
let to_write = (range.end - range.start).min(buf.len() as u64) as usize;
let n = self.writer.write(&buf[..to_write])?;
range.start += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
pub fn load(
reader: impl Read,
include_trailer: bool,
cancel_signal: &AtomicBool,
) -> Result<Vec<CpioEntry>> {
let mut cpio_reader = CpioReader::new(reader, include_trailer);
let mut entries = vec![];
while let Some(mut entry) = cpio_reader.next_entry()? {
stream::check_cancel(cancel_signal)?;
if entry.file_type != CpioEntryType::Directory && entry.nlink > 1 {
return Err(Error::HardLinksNotSupported(entry.path.clone()));
}
if let CpioEntryData::Size(s) = entry.data {
let data = read_data(&mut cpio_reader, s.to_usize().unwrap(), cancel_signal)?;
entry.data = CpioEntryData::Data(data);
}
entries.push(entry);
@@ -328,35 +699,83 @@ pub fn load(mut reader: impl Read, include_trailer: bool) -> Result<Vec<CpioEntr
Ok(entries)
}
pub fn sort(entries: &mut [CpioEntryNew]) {
entries.sort_by(|a, b| a.name.cmp(&b.name));
pub fn sort(entries: &mut [CpioEntry]) {
entries.sort_by(|a, b| a.path.cmp(&b.path));
}
pub fn reassign_inodes(entries: &mut [CpioEntryNew]) {
let mut inode = 300000;
for entry in entries {
entry.ino = inode;
inode += 1;
}
}
pub fn save(writer: impl Write, entries: &[CpioEntryNew], pad_to_block_size: bool) -> Result<()> {
let mut writer = CountingWriter::new(writer);
for entry in entries {
entry.to_writer(&mut writer)?;
/// Assign inodes to entries. If `missing_only` is true, then inodes are only
/// assigned if the inode field in an entry is set to 0.
///
/// New inodes are assigned starting immediately after the highest inode number
/// for a given ([`CpioEntry::dev_maj`], [`CpioEntry::dev_min`]) pair. If there
/// are no existing inodes assigned for a device, then the numbers begin at
/// 300000.
pub fn assign_inodes(entries: &mut [CpioEntry], missing_only: bool) -> Result<()> {
fn next_non_zero(i: u32) -> u32 {
if i == u32::MAX {
1
} else {
i.wrapping_add(1)
}
}
let mut trailer = CpioEntryNew::new_trailer();
// 1 higher than the highest inode if possible.
trailer.ino = entries.iter().map(|e| e.ino).max().map_or(0, |i| i + 1);
trailer.to_writer(&mut writer)?;
// (dev maj, dev min) -> (inode set, last assigned inode)
let mut inodes: HashMap<(u32, u32), (HashSet<u32>, u32)> = HashMap::new();
// Pad until the end of the block.
if pad_to_block_size {
padding::write_zeros(&mut writer, IO_BLOCK_SIZE)?;
if missing_only {
for entry in &mut *entries {
if entry.inode != 0 {
let key = (entry.dev_maj, entry.dev_min);
let (set, last) = inodes.entry(key).or_default();
set.insert(entry.inode);
*last = (*last).max(entry.inode);
}
}
}
for entry in entries {
if entry.inode == 0 {
let key = (entry.dev_maj, entry.dev_min);
let (set, last) = inodes
.entry(key)
.or_insert_with(|| (HashSet::new(), 299999));
let mut unused = next_non_zero(*last);
while set.contains(&unused) {
if unused == *last {
return Err(Error::DeviceFull(entry.dev_maj, entry.dev_min));
}
unused = next_non_zero(unused);
}
entry.inode = unused;
set.insert(unused);
*last = unused;
}
}
Ok(())
}
pub fn save(
writer: impl Write,
entries: &[CpioEntry],
pad_to_block_size: bool,
cancel_signal: &AtomicBool,
) -> Result<()> {
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
for entry in entries {
stream::check_cancel(cancel_signal)?;
cpio_writer.start_entry(entry)?;
// CpioEntryData::Data will have already been written.
}
cpio_writer.finish()?;
Ok(())
}
+933
View File
@@ -0,0 +1,933 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
collections::HashSet,
fmt,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
prelude::{IndexedParallelIterator, ParallelIterator},
slice::{ParallelSlice, ParallelSliceMut},
};
use thiserror::Error;
use crate::{
format::verityrs,
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
util::{self, NumBytes},
};
// Not to be confused with the 255-byte RS block size.
const FEC_BLOCK_SIZE: usize = 4096;
const FEC_HEADER_SIZE: usize = 60;
const FEC_MAGIC: u32 = 0xFECFECFE;
const FEC_VERSION: u32 = 0;
const FEC_MAX_BLOCK_SIZE: u32 = 16384;
#[derive(Debug, Error)]
pub enum Error {
#[error("FEC with parity byte count of {0} is not supported")]
UnsupportedParity(u8),
#[error("Cannot calculate FEC for empty data")]
InputEmpty,
#[error("Input size ({input}) is not a multiple of FEC block size ({block})")]
NotBlockAligned { input: u64, block: u32 },
#[error("FEC should have size {expected} for input size {input}, but has size {actual}")]
InvalidFecSize {
input: u64,
expected: usize,
actual: usize,
},
#[error("Cannot repair data due to too many errors")]
TooManyErrors,
#[error("Input data contains errors")]
HasErrors,
#[error("Data is too small to contain FEC headers")]
DataTooSmall,
#[error("The two FEC headers are different")]
HeadersDifferent,
#[error("Invalid FEC header magic: {0:#x}")]
InvalidHeaderMagic(u32),
#[error("Unsupported FEC header version: {0}")]
UnsupportedHeaderVersion(u32),
#[error("Invalid FEC header size: {0}")]
InvalidHeaderSize(u32),
#[error("FEC size in header {value} does not match available data ({available})")]
InvalidHeaderFecSize { value: usize, available: usize },
#[error("Expected FEC digest {expected}, but have {actual}")]
InvalidFecDigest { expected: String, actual: String },
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// A small wrapper around a byte array to represent a single Reed-Solomon
/// codeword for any `RS(255, K)`.
struct Codeword {
data: [u8; 255],
rs_k: u8,
}
impl Codeword {
fn new(rs_k: u8) -> Self {
Self {
data: [0u8; 255],
rs_k,
}
}
fn data(&self) -> &[u8] {
&self.data[..usize::from(self.rs_k)]
}
fn data_mut(&mut self) -> &mut [u8] {
&mut self.data[..usize::from(self.rs_k)]
}
fn parity(&mut self) -> &[u8] {
&self.data[usize::from(self.rs_k)..]
}
fn parity_mut(&mut self) -> &mut [u8] {
&mut self.data[usize::from(self.rs_k)..]
}
fn all(&self) -> &[u8] {
&self.data
}
fn all_mut(&mut self) -> &mut [u8] {
&mut self.data
}
}
/// A type for performing FEC generation, verification, and error correction for
/// a specific file size and Reed Solomon configuration. The implementation uses
/// dm-verity's interleaving access pattern.
///
/// The interleaving access pattern can be visualized by placing the file
/// offsets in a two-dimensional grid. For example, when reading a 2072576-byte
/// file for calculating RS(255, 253):
///
/// ```text
/// | <-------- Round 0 --------> | <-------- Round 1 --------> |
/// |-----------------------------|-----------------------------|
/// ^ | 0 1 ... 4095 | 4096 4097 ... 8191 |
/// | | 8192 8192 ... 12287 | 12288 12289 ... 16383 |
/// rs_k | 16384 16385 ... 20479 | 20480 20481 ... 24575 |
/// | | ....... ....... ... ....... | ....... ....... ... ....... |
/// v | 2064384 2064385 ... 2068479 | 2068480 2068481 ... 2072575 |
/// ```
///
/// A regular sequential read of the file is traversing the grid row-by-row,
/// while an interleaving read is traversing the grid column-by-column. Each
/// column is always `rs_k` items tall, so each column forms the data portion of
/// an RS codeword. The number of columns is always a multiple of the FEC block
/// size. Since RS operates on fixed-size codewords and a file size might not
/// always fill the grid completely, out-of-bounds offsets are treated as if
/// they contain a `\0` byte.
///
/// All operations are multithreaded with I/O operations parallelized at the
/// "round" level and RS operations parallelized at the column level.
pub struct Fec {
file_size: u64,
block_size: u32,
rs_k: u8,
rounds: u64,
}
impl Fec {
pub fn new(file_size: u64, block_size: u32, parity: u8) -> Result<Self> {
if file_size == 0 {
return Err(Error::InputEmpty);
} else if file_size % u64::from(block_size) != 0 {
return Err(Error::NotBlockAligned {
input: file_size,
block: block_size,
});
} else if block_size > FEC_MAX_BLOCK_SIZE {
return Err(Error::FieldOutOfBounds("block_size"));
}
let rs_k = 255 - parity;
if !verityrs::FN_ENCODE.contains_key(&rs_k) {
return Err(Error::UnsupportedParity(parity));
}
let blocks = util::div_ceil(file_size, u64::from(block_size));
let rounds = util::div_ceil(blocks, u64::from(rs_k));
// Check upfront so we don't need to do checked multiplication later.
rounds
.checked_mul(u64::from(parity))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.and_then(|s| s.to_usize())
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_size"))?;
rounds
.checked_mul(u64::from(rs_k))
.and_then(|s| s.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("fec_grid_size"))?;
Ok(Self {
file_size,
block_size,
rs_k,
rounds,
})
}
/// Get the number of parity bytes per codeword.
#[inline]
fn parity(&self) -> u8 {
255 - self.rs_k
}
/// Get the size of the FEC data needed to cover the entire file.
#[inline]
fn fec_size(&self) -> usize {
usize::from(self.parity()) * self.rounds as usize * self.block_size as usize
}
/// Get the backing file offset for the specified `offset` in the
/// interleaved view.
fn backing_offset(&self, offset: u64) -> u64 {
let rs_k = u64::from(self.rs_k);
offset / rs_k + offset % rs_k * self.rounds * u64::from(self.block_size)
}
/// Get the rounds that correspond to the specified ranges.
fn rounds_for_ranges(&self, ranges: &[Range<u64>]) -> Result<HashSet<u64>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > self.file_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
}
let block_size = u64::from(self.block_size);
let mut result = HashSet::new();
for range in ranges {
let start_block = range.start / block_size;
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
};
for block in start_block..end_block {
result.insert(block % self.rounds);
}
}
Ok(result)
}
/// Read a raw sequential block from the backing file, starting at offset
/// `offset` in the interleaved view. This reads a horizontal block-aligned
/// slice in the file offset grid.
fn read_seq_block(
&self,
mut reader: impl Read + Seek,
offset: u64,
buf: &mut [u8],
) -> io::Result<()> {
assert_eq!(
buf.len(),
self.block_size as usize,
"Buffer does not match block size",
);
let backing_offset = self.backing_offset(offset);
// Out of bounds offsets are treated as if they contain zeros.
if backing_offset >= self.file_size {
buf.fill(0);
} else {
reader.seek(SeekFrom::Start(backing_offset))?;
reader.read_exact(buf)?;
}
Ok(())
}
/// Write a raw sequential block to the backing file, starting at offset
/// `offset` in the interleaved view. This writes a horizontal block-aligned
/// slice in the file offset grid.
fn write_seq_block(
&self,
mut writer: impl Write + Seek,
offset: u64,
buf: &[u8],
) -> io::Result<()> {
assert_eq!(
buf.len(),
self.block_size as usize,
"Buffer does not match block size",
);
let backing_offset = self.backing_offset(offset);
// Out of bounds offsets are ignored.
if backing_offset < self.file_size {
writer.seek(SeekFrom::Start(backing_offset))?;
writer.write_all(buf)?;
}
Ok(())
}
/// Read the nth round from the file. The data is laid out sequentially
/// (row-by-row).
fn read_round(&self, mut reader: impl Read + Seek, round: u64) -> io::Result<Vec<u8>> {
let mut grid = vec![0u8; usize::from(self.rs_k) * self.block_size as usize];
for row in 0..self.rs_k {
let interleaved_offset =
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
let row_start = usize::from(row) * self.block_size as usize;
let row_end = row_start + self.block_size as usize;
let row_slice = &mut grid[row_start..row_end];
self.read_seq_block(&mut reader, interleaved_offset, row_slice)?;
}
Ok(grid)
}
/// Write the nth round to the file. The data is expected to be laid out
/// sequentially (row-by-row).
fn write_round(
&self,
mut writer: impl Write + Seek,
round: u64,
grid: &[u8],
) -> io::Result<()> {
for row in 0..self.rs_k {
let interleaved_offset =
round * u64::from(self.rs_k) * u64::from(self.block_size) + u64::from(row);
let row_start = usize::from(row) * self.block_size as usize;
let row_end = row_start + self.block_size as usize;
let row_slice = &grid[row_start..row_end];
self.write_seq_block(&mut writer, interleaved_offset, row_slice)?;
}
Ok(())
}
/// Get the nth RS codeword from a round's grid.
fn get_codeword(&self, grid: &[u8], column: usize) -> Codeword {
let mut codeword = Codeword::new(self.rs_k);
let data = codeword.data_mut();
for row in 0..usize::from(self.rs_k) {
data[row] = grid[row * self.block_size as usize + column];
}
codeword
}
/// Put the nth RS codeword into a round's grid.
fn put_codeword(&self, grid: &mut [u8], column: usize, codeword: &Codeword) {
let data = codeword.data();
for row in 0..usize::from(self.rs_k) {
grid[row * self.block_size as usize + column] = data[row];
}
}
/// Generate FEC data for a single round.
fn generate_one_round(
&self,
reader: impl Read + Seek,
round: u64,
fec: &mut [u8],
) -> Result<()> {
assert_eq!(
fec.len(),
usize::from(self.parity()) * self.block_size as usize,
"FEC buffer length does not match block size",
);
let grid = self.read_round(reader, round)?;
let encode = verityrs::FN_ENCODE[&self.rs_k];
let parity = usize::from(self.parity());
for (column, buf) in fec.chunks_exact_mut(parity).enumerate() {
let mut codeword = self.get_codeword(&grid, column);
encode(codeword.all_mut());
buf.copy_from_slice(codeword.parity());
}
Ok(())
}
/// Verify file data for a single round.
fn verify_one_round(&self, reader: impl Read + Seek, round: u64, fec: &[u8]) -> Result<()> {
assert_eq!(
fec.len(),
usize::from(self.parity()) * self.block_size as usize,
"FEC buffer length does not match block size",
);
let grid = self.read_round(reader, round)?;
let is_correct = verityrs::FN_IS_CORRECT[&self.rs_k];
let parity = usize::from(self.parity());
for (column, buf) in fec.chunks_exact(parity).enumerate() {
let mut codeword = self.get_codeword(&grid, column);
codeword.parity_mut().copy_from_slice(buf);
if !is_correct(codeword.all()) {
return Err(Error::HasErrors);
}
}
Ok(())
}
/// Repair file data for a single round.
fn repair_one_round(
&self,
reader: impl Read + Seek,
writer: impl Write + Seek,
round: u64,
fec: &[u8],
) -> Result<u64> {
assert_eq!(
fec.len(),
usize::from(self.parity()) * self.block_size as usize,
"FEC buffer length does not match block size",
);
let mut grid = self.read_round(reader, round)?;
let correct_errors = verityrs::FN_CORRECT_ERRORS[&self.rs_k];
let parity = usize::from(self.parity());
let mut num_corrected = 0;
for (column, buf) in fec.chunks_exact(parity).enumerate() {
let mut codeword = self.get_codeword(&grid, column);
codeword.parity_mut().copy_from_slice(buf);
let n = correct_errors(codeword.all_mut()).ok_or(Error::TooManyErrors)?;
if n > 0 {
self.put_codeword(&mut grid, column, &codeword);
}
num_corrected += n as u64;
}
if num_corrected > 0 {
self.write_round(writer, round, &grid)?;
}
Ok(num_corrected)
}
/// Generate FEC data for the file. The file size must match the file size
/// given to [`Self::new()`].
///
/// This function is multithreaded and uses rayon's global thread pool.
pub fn generate(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let fec_size = self.fec_size();
let mut fec = vec![0u8; fec_size];
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = input.reopen_boxed()?;
self.generate_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(fec)
}
/// Update FEC data coreesponding to the specified file ranges.
///
/// This function is multithreaded and uses rayon's global thread pool.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
fec: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec_size = self.fec_size();
if fec.len() != fec_size {
return Err(Error::InvalidFecSize {
input: self.file_size,
expected: fec_size,
actual: fec.len(),
});
}
let rounds_to_update = self.rounds_for_ranges(ranges)?;
fec.par_chunks_exact_mut(fec_size / self.rounds as usize)
.enumerate()
.filter(|(round, _)| rounds_to_update.contains(&(*round as u64)))
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = input.reopen_boxed()?;
self.generate_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(())
}
/// Verify that the file contains no errors. This is significantly faster
/// than [`Self::repair()`] if only error detection, not correction, is
/// needed.
///
/// This function is multithreaded and uses rayon's global thread pool.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec_size = self.fec_size();
if fec.len() != fec_size {
return Err(Error::InvalidFecSize {
input: self.file_size,
expected: fec_size,
actual: fec.len(),
});
}
fec.par_chunks_exact(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = input.reopen_boxed()?;
self.verify_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
Ok(())
}
/// Repair the file. Up to `parity / 2` bytes per codeword can be repaired.
/// If the file is successfully repaired, the number of repaired bytes is
/// returned. If the file is corrupt beyond repair, [`Error::TooManyErrors`]
/// is returned. It's not safe to assume that as much data as possible has
/// been repaired when [`Error::TooManyErrors`] is returned due to fail-fast
/// behavior.
///
/// This function corrects errors at unknown locations only. Correcting
/// erasures at known locations is not supported.
///
/// This function is multithreaded and uses rayon's global thread pool.
pub fn repair(
&self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<u64> {
let fec_size = self.fec_size();
if fec.len() != fec_size {
return Err(Error::InvalidFecSize {
input: self.file_size,
expected: fec_size,
actual: fec.len(),
});
}
let num_corrected = fec
.par_chunks_exact(fec_size / self.rounds as usize)
.enumerate()
.map(|(round, buf)| -> Result<u64> {
stream::check_cancel(cancel_signal)?;
let reader = input.reopen_boxed()?;
let writer = output.reopen_boxed()?;
self.repair_one_round(reader, writer, round as u64, buf)
})
.collect::<Result<Vec<u64>>>()?
.into_iter()
.sum();
Ok(num_corrected)
}
}
/// A type for reading and writing AOSP's standalone FEC image format.
///
/// The FEC data parser in this implementation is strict. All header fields,
/// like the version, header size, and digest, must be valid and both copies of
/// the header must match.
#[derive(Clone, PartialEq, Eq)]
pub struct FecImage {
pub fec: Vec<u8>,
pub data_size: u64,
pub parity: u8,
}
impl fmt::Debug for FecImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FecImage")
.field("fec", &NumBytes(self.fec.len()))
.field("data_size", &self.data_size)
.field("parity", &self.parity)
.finish()
}
}
impl FecImage {
/// Generate FEC data for a file. `parity` is the number of parity bytes per
/// 255-byte Reed-Solomon codeword.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
parity: u8,
cancel_signal: &AtomicBool,
) -> Result<Self> {
let data_size = {
let mut file = input.reopen_boxed()?;
file.seek(SeekFrom::End(0))?
};
let fec = Fec::new(data_size, FEC_BLOCK_SIZE as u32, parity)?;
let fec_data = fec.generate(input, cancel_signal)?;
Ok(Self {
fec: fec_data,
data_size,
parity,
})
}
/// Update FEC data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.update(input, ranges, &mut self.fec, cancel_signal)
}
/// Check that a file contains no errors. This is significantly faster than
/// [`Self::repair()`] if performing a repair is not necessary.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.verify(input, &self.fec, cancel_signal)
}
/// Repair a file using this instance's FEC data. The maximum correctable
/// errors per 255-byte codeword is half of [`Self::parity`]. Returns the
/// number of bytes corrected if the file is successfully repaired or
/// [`Error::TooManyErrors`] if the file cannot be repaired. This function
/// fails fast. If an RS codeword cannot be repaired, other potentially
/// repairable codewords may not be repaired.
///
/// Note that if there are too many errors inside a certain codeword, it's
/// possible for there to be a false positive where the corrupted codeword
/// is "corrected" into an incorrect value. FEC error detection is not a
/// replacement for cryptographically secure digests.
///
/// The inputs and outputs should point to the same underlying file because
/// only regions where errors are corrected are written. It is guaranteed
/// that multiple threads will always read and write disjoint file offsets.
pub fn repair(
&self,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<u64> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.repair(input, output, &self.fec, cancel_signal)
}
/// Build one instance of the FEC header. The caller is responsible for
/// writing it to both of the header locations at the end of the file.
fn build_header(&self) -> Result<[u8; FEC_HEADER_SIZE]> {
let fec_size = self
.fec
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let mut writer = Cursor::new([0u8; FEC_HEADER_SIZE]);
let digest = ring::digest::digest(&ring::digest::SHA256, &self.fec);
writer.write_u32::<LittleEndian>(FEC_MAGIC)?;
writer.write_u32::<LittleEndian>(FEC_VERSION)?;
writer.write_u32::<LittleEndian>(FEC_HEADER_SIZE as u32)?;
writer.write_u32::<LittleEndian>(self.parity.into())?;
writer.write_u32::<LittleEndian>(fec_size)?;
writer.write_u64::<LittleEndian>(self.data_size)?;
writer.write_all(digest.as_ref())?;
Ok(writer.into_inner())
}
}
impl<R: Read> FromReader<R> for FecImage {
type Error = Error;
fn from_reader(mut reader: R) -> Result<Self> {
// Avoid requiring seekable readers since we need to read everything
// into memory anyway.
let mut fec = Vec::new();
reader.read_to_end(&mut fec)?;
if fec.len() < FEC_BLOCK_SIZE {
return Err(Error::DataTooSmall);
}
// Make sure both headers match.
let header1_offset = fec.len() - FEC_BLOCK_SIZE;
let header2_offset = fec.len() - FEC_HEADER_SIZE;
let header1_raw = &fec[header1_offset..header1_offset + FEC_HEADER_SIZE];
let header2_raw = &fec[header2_offset..header2_offset + FEC_HEADER_SIZE];
if header1_raw != header2_raw {
return Err(Error::HeadersDifferent);
}
let mut header_reader = Cursor::new(header1_raw);
let magic = header_reader.read_u32::<LittleEndian>()?;
if magic != FEC_MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
}
let version = header_reader.read_u32::<LittleEndian>()?;
if version != FEC_VERSION {
return Err(Error::UnsupportedHeaderVersion(version));
}
let header_size = header_reader.read_u32::<LittleEndian>()?;
if header_size != FEC_HEADER_SIZE as u32 {
return Err(Error::InvalidHeaderSize(header_size));
}
let parity = header_reader
.read_u32::<LittleEndian>()?
.to_u8()
.ok_or_else(|| Error::FieldOutOfBounds("parity"))?;
let fec_size = header_reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("fec_size"))?;
let actual_fec_size = fec.len() - FEC_BLOCK_SIZE;
if fec_size != actual_fec_size {
return Err(Error::InvalidHeaderFecSize {
value: fec_size,
available: actual_fec_size,
});
}
let input_size = header_reader.read_u64::<LittleEndian>()?;
let mut digest = [0u8; 32];
header_reader.read_exact(&mut digest)?;
// Chop off headers.
fec.resize(fec_size, 0);
let actual_digest = ring::digest::digest(&ring::digest::SHA256, &fec);
if digest != actual_digest.as_ref() {
return Err(Error::InvalidFecDigest {
expected: hex::encode(digest),
actual: hex::encode(actual_digest),
});
}
Ok(Self {
fec,
data_size: input_size,
parity,
})
}
}
impl<W: Write> ToWriter<W> for FecImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let header = self.build_header()?;
writer.write_all(&self.fec)?;
writer.write_all(&header)?;
writer.write_zeros_exact((FEC_BLOCK_SIZE - 2 * FEC_HEADER_SIZE) as u64)?;
writer.write_all(&header)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::{
io::Seek,
sync::{atomic::AtomicBool, Arc},
};
use assert_matches::assert_matches;
use rand::RngCore;
use crate::stream::SharedCursor;
use super::*;
#[test]
fn rounds_for_ranges() {
let size = 2 * 253 * 4096;
let fec = Fec::new(size, 4096, 2).unwrap();
assert_eq!(fec.rounds_for_ranges(&[0..0]).unwrap(), HashSet::new(),);
assert_eq!(
fec.rounds_for_ranges(&[0..size]).unwrap(),
HashSet::from([0, 1]),
);
assert_eq!(fec.rounds_for_ranges(&[0..1]).unwrap(), HashSet::from([0]),);
assert_eq!(
fec.rounds_for_ranges(&[4095..4096]).unwrap(),
HashSet::from([0]),
);
assert_eq!(
fec.rounds_for_ranges(&[4095..4097]).unwrap(),
HashSet::from([0, 1]),
);
assert_eq!(
fec.rounds_for_ranges(&[size - 1..size]).unwrap(),
HashSet::from([1]),
);
}
fn corrupt_byte(file: &mut SharedCursor, offset: u64) {
let mut buf = [0u8; 1];
file.seek(SeekFrom::Start(offset)).unwrap();
file.read_exact(&mut buf).unwrap();
buf[0] = buf[0].wrapping_add(1);
file.seek(SeekFrom::Start(offset)).unwrap();
file.write_all(&buf).unwrap();
}
fn run_test(block_size: u32, rs_k: u8) {
let cancel_signal = Arc::new(AtomicBool::new(false));
let parity = 255 - rs_k;
// Generate data big enough to span multiple rounds, but don't fill the
// offset grid to ensure that the out-of-bounds-is-0 behavior works.
let size = usize::from(rs_k) * block_size as usize * 3 - block_size as usize;
let mut file = SharedCursor::default();
let orig_digest = {
let mut buf = vec![0u8; size];
rand::thread_rng().fill_bytes(&mut buf);
file.write_all(&buf).unwrap();
ring::digest::digest(&ring::digest::SHA256, &buf)
};
let fec = Fec::new(size as u64, block_size, parity).unwrap();
assert_eq!(fec.rounds, 3);
let num_codewords = fec.rounds as usize * block_size as usize;
// Generate FEC data.
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
// Verify that there are no errors.
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
// Verify that errors are detected.
corrupt_byte(&mut file, 0);
assert_matches!(
fec.verify(&file, &fec_data, &cancel_signal),
Err(Error::HasErrors)
);
// Corrupt one byte in every single codeword.
for offset in 1..num_codewords {
corrupt_byte(&mut file, offset as u64);
}
// Verify that all the single-byte errors can be fixed. We don't test
// for Error::TooManyErrors because of the chance of false positives due
// to the nature of RS.
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
let repaired_digest = {
let mut buf = Vec::new();
file.rewind().unwrap();
file.read_to_end(&mut buf).unwrap();
ring::digest::digest(&ring::digest::SHA256, &buf)
};
assert_eq!(repaired_digest.as_ref(), orig_digest.as_ref());
// Intentionally update some data.
corrupt_byte(&mut file, 0);
let mut fec_data_updated = fec_data.clone();
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
fec.update(&file, &[0..1], &mut fec_data_updated, &cancel_signal)
.unwrap();
assert_eq!(fec_data_updated, fec_data);
}
#[test]
fn generate_update_verify_repair() {
for block_size in [1, 2, 4, 8, 16, 32, 64] {
for rs_k in verityrs::FN_ENCODE.keys() {
println!("Testing block_size={block_size}, rs_k={rs_k}");
run_test(block_size, *rs_k);
}
}
}
#[test]
fn round_trip_image() {
let cancel_signal = Arc::new(AtomicBool::new(false));
let mut file = SharedCursor::default();
{
let mut buf = [0u8; FEC_BLOCK_SIZE];
rand::thread_rng().fill_bytes(&mut buf);
file.write_all(&buf).unwrap();
}
let image = FecImage::generate(&file, 2, &cancel_signal).unwrap();
let mut fec_file = Cursor::new(Vec::new());
image.to_writer(&mut fec_file).unwrap();
fec_file.rewind().unwrap();
let new_image = FecImage::from_reader(&mut fec_file).unwrap();
assert_eq!(image, new_image);
}
}
+742
View File
@@ -0,0 +1,742 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
fmt,
io::{self, Cursor, Read, SeekFrom, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_traits::ToPrimitive;
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
use ring::digest::{Algorithm, Context};
use thiserror::Error;
use crate::{
format::{avb, padding},
stream::{self, FromReader, ReadSeekReopen, ReadStringExt, ToWriter, WriteStringExt},
util::{self, NumBytes},
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Hash tree should have size {expected} for input size {input}, but has size {actual}")]
InvalidHashTreeSize {
input: u64,
expected: usize,
actual: usize,
},
#[error("Expected root digest {expected}, but have {actual}")]
InvalidRootDigest { expected: String, actual: String },
#[error("Expected hash tree {expected}, but have {actual}")]
InvalidHashTree { expected: String, actual: String },
#[error("Invalid hash tree header magic: {:?}", .0.as_bstr())]
InvalidHeaderMagic([u8; 16]),
#[error("Invalid hash tree header version: {0}")]
InvalidHeaderVersion(u16),
#[error("Hashing algorithm not supported: {0:?}")]
UnsupportedHashAlgorithm(String),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
pub struct HashTree<'a> {
block_size: u32,
algorithm: &'static Algorithm,
salt: &'a [u8],
}
impl<'a> HashTree<'a> {
pub fn new(block_size: u32, algorithm: &'static Algorithm, salt: &'a [u8]) -> Self {
Self {
block_size,
algorithm,
salt,
}
}
/// Compute the list of offset ranges that each level occupies in the hash
/// tree data. The items are returned with the bottom level's offsets first
/// in the list. Note that the bottom level is stored at the end of the hash
/// tree data.
fn compute_level_offsets(&self, image_size: u64) -> Result<Vec<Range<usize>>> {
let digest_size = self.algorithm.output_len().next_power_of_two();
let mut ranges = vec![];
let mut level_size = image_size;
while level_size > u64::from(self.block_size) {
let blocks = util::div_ceil(level_size, u64::from(self.block_size));
level_size = blocks
.checked_mul(digest_size as u64)
.and_then(|s| padding::round(s, u64::from(self.block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
// Depending on the chosen block size, the original file size could
// overflow a usize without the first level's size doing the same.
let level_size_usize = level_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("level_size"))?;
ranges.push(0..level_size_usize);
}
// The hash tree puts the leaves at the end.
let mut offset = 0;
for range in ranges.iter_mut().rev() {
let level_size = range.end - range.start;
range.start += offset;
range.end += offset;
offset += level_size;
}
Ok(ranges)
}
/// Convert a list of ranges of byte offsets to a sorted, non-overlapping
/// list of block ranges.
fn blocks_for_ranges(&self, image_size: u64, ranges: &[Range<u64>]) -> Result<Vec<Range<u64>>> {
let ranges = util::merge_overlapping(ranges);
if let Some(last) = ranges.last() {
if last.end > image_size {
return Err(Error::FieldOutOfBounds("ranges"));
}
}
let block_size = u64::from(self.block_size);
let mut result = Vec::new();
for range in ranges {
let start_block = range.start / block_size;
let end_block = if range.end % block_size == 0 {
range.end / block_size
} else {
util::div_ceil(range.end, block_size)
};
result.push(start_block..end_block);
}
Ok(util::merge_overlapping(&result))
}
/// Calculate the hash tree digests for a single level of the tree. If the
/// reader's position is block-aligned and `image_size` is a multiple of the
/// block size, then this function can also be used to calculate the digests
/// for a portion of a level.
fn hash_partial_level(
&self,
mut reader: impl Read,
mut size: u64,
mut level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
// Each digest must be a power of 2.
let digest_padding =
self.algorithm.output_len().next_power_of_two() - self.algorithm.output_len();
let mut buf = vec![0u8; self.block_size as usize];
while size > 0 {
stream::check_cancel(cancel_signal)?;
let n = size.min(buf.len() as u64) as usize;
reader.read_exact(&mut buf[..n])?;
// For undersized blocks, we still hash the whole buffer, except
// with padding.
buf[n..].fill(0);
let mut context = Context::new(self.algorithm);
context.update(self.salt);
context.update(&buf);
// Add the digest to the tree level. Each tree node must be a power
// of two.
let digest = context.finish();
level_data[..digest.as_ref().len()].copy_from_slice(digest.as_ref());
level_data = &mut level_data[digest.as_ref().len()..];
level_data[..digest_padding].fill(0);
level_data = &mut level_data[digest_padding..];
size -= n as u64;
}
Ok(())
}
/// Hash one full level in parallel.
fn hash_one_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
size: u64,
level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
assert!(
size > self.block_size as u64,
"Images smaller than block size must use a normal hash",
);
// Parallelize in larger chunks to avoid too much seek thrashing.
let digest_size = self.algorithm.output_len().next_power_of_two();
let multiplier = 1024u64;
level_data
.par_chunks_mut(digest_size * multiplier as usize)
.enumerate()
.map(|(chunk, out_data)| -> io::Result<()> {
let digests = out_data.len() / digest_size;
let in_start = (chunk as u64) * multiplier * u64::from(self.block_size);
let in_size = ((digests as u64) * u64::from(self.block_size)).min(size - in_start);
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Update parts of the hash tree level corresponding to the specified
/// blocks.
fn hash_partial_level_parallel(
&self,
input: &(dyn ReadSeekReopen + Sync),
size: u64,
block_ranges: &[Range<u64>],
level_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> io::Result<()> {
let digest_size = self.algorithm.output_len().next_power_of_two();
level_data
.par_chunks_exact_mut(digest_size)
.enumerate()
.filter(|(chunk, _)| util::ranges_contains(block_ranges, &(*chunk as u64)))
.map(|(chunk, out_data)| -> io::Result<()> {
let in_start = (chunk as u64) * u64::from(self.block_size);
let in_size = u64::from(self.block_size).min(size - in_start);
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(in_start))?;
self.hash_partial_level(reader, in_size, out_data, cancel_signal)
})
.collect::<io::Result<()>>()?;
Ok(())
}
/// Compute the hash tree and return the root digest. If `ranges` is
/// specified, then only the input file blocks containing those ranges are
/// recomputed.
///
/// `hash_tree_data` must match `level_offsets`. In other words, the ending
/// offset of the leaf layer of the tree must equal `hash_tree_data`'s size.
fn calculate(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
ranges: Option<&[Range<u64>]>,
level_offsets: &[Range<usize>],
hash_tree_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
// Small files are hashed directly.
if image_size <= u64::from(self.block_size) {
let mut reader = input.reopen_boxed()?;
let mut buf = vec![0u8; image_size as usize];
reader.read_exact(&mut buf)?;
let mut context = Context::new(self.algorithm);
context.update(self.salt);
context.update(&buf);
let digest = context.finish();
return Ok(digest.as_ref().to_vec());
}
// Large files use the hash tree.
for (i, level_range) in level_offsets.iter().enumerate() {
let (front, back) = hash_tree_data.split_at_mut(level_range.end);
let level_data = &mut front[level_range.clone()];
if i > 0 {
// Hash the previous level.
let prev_range = level_offsets[i - 1].clone();
let prev_size = prev_range.end - prev_range.start;
let prev_data = &back[..prev_size];
self.hash_partial_level(
Cursor::new(prev_data),
prev_size as u64,
level_data,
cancel_signal,
)?;
} else if let Some(r) = ranges {
// Read partial blocks from file.
let block_ranges = self.blocks_for_ranges(image_size, r)?;
self.hash_partial_level_parallel(
input,
image_size,
&block_ranges,
level_data,
cancel_signal,
)?;
} else {
// Read entire file.
self.hash_one_level_parallel(input, image_size, level_data, cancel_signal)?;
}
// No need to explicitly ensure the level is padded to the block
// size since the tree is initialized with zeros.
}
// Calculate the root hash.
let mut context = Context::new(self.algorithm);
context.update(self.salt);
context.update(&hash_tree_data[level_offsets.last().unwrap().clone()]);
let root_hash = context.finish().as_ref().to_vec();
Ok(root_hash)
}
/// Generate hash tree data for the file. Returns the root digest and the
/// hash tree data.
pub fn generate(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
cancel_signal: &AtomicBool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
let mut hash_tree_data = vec![0u8; hash_tree_size];
let root_digest = self.calculate(
input,
image_size,
None,
&offsets,
&mut hash_tree_data,
cancel_signal,
)?;
Ok((root_digest, hash_tree_data))
}
/// Update hash tree data corresponding to the specified file ranges.
/// Returns the new root digest.
pub fn update(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
ranges: &[Range<u64>],
hash_tree_data: &mut [u8],
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
expected: hash_tree_size,
actual: hash_tree_data.len(),
});
}
self.calculate(
input,
image_size,
Some(ranges),
&offsets,
hash_tree_data,
cancel_signal,
)
}
/// Verify that the file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
root_digest: &[u8],
hash_tree_data: &[u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
let offsets = self.compute_level_offsets(image_size)?;
let hash_tree_size = offsets.get(0).map(|r| r.end).unwrap_or(0);
if hash_tree_data.len() != hash_tree_size {
return Err(Error::InvalidHashTreeSize {
input: image_size,
expected: hash_tree_size,
actual: hash_tree_data.len(),
});
}
let (actual_root_digest, actual_hash_tree_data) =
self.generate(input, image_size, cancel_signal)?;
if root_digest != actual_root_digest {
return Err(Error::InvalidRootDigest {
expected: hex::encode(root_digest),
actual: hex::encode(&actual_root_digest),
});
}
if hash_tree_data != actual_hash_tree_data {
// These are multiple megabytes, so only report the hashes.
let expected = ring::digest::digest(self.algorithm, hash_tree_data);
let actual = ring::digest::digest(self.algorithm, &actual_hash_tree_data);
return Err(Error::InvalidHashTree {
expected: hex::encode(expected),
actual: hex::encode(actual),
});
}
Ok(())
}
}
/// A type for reading and writing a custom hash tree image format.
///
/// File format:
/// - [0 .. 16] - ASCII - "avbroot!hashtree"
/// - [16 .. 18] - U16LE - Version
/// - [18 .. 26] - U64LE - Image size
/// - [26 .. 30] - U32LE - Block size
/// - [30 .. 46] - ASCII - Hash algorithm
/// - [46 .. 48] - U16LE - Salt size
/// - [48 .. 50] - U16LE - Root digest size
/// - [50 .. 54] - U32LE - Hash tree size
/// - [<variable>] - BINARY - Salt
/// - [<variable>] - BINARY - Root digest
/// - [<variable>] - BINARY - Hash tree
#[derive(Clone, PartialEq, Eq)]
pub struct HashTreeImage {
pub image_size: u64,
pub block_size: u32,
pub algorithm: String,
pub salt: Vec<u8>,
pub root_digest: Vec<u8>,
pub hash_tree: Vec<u8>,
}
impl fmt::Debug for HashTreeImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HashTreeImage")
.field("block_size", &self.block_size)
.field("algorithm", &self.algorithm)
.field("salt", &hex::encode(&self.salt))
.field("root_digest", &hex::encode(&self.root_digest))
.field("hash_tree", &NumBytes(self.hash_tree.len()))
.finish()
}
}
impl HashTreeImage {
const MAGIC: &'static [u8; 16] = b"avbroot!hashtree";
const VERSION: u16 = 1;
pub fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
avb::ring_algorithm(name, false)
.map_err(|_| Error::UnsupportedHashAlgorithm(name.to_owned()))
}
/// Generate hash tree data for a file.
pub fn generate(
input: &(dyn ReadSeekReopen + Sync),
block_size: u32,
algorithm: &str,
salt: &[u8],
cancel_signal: &AtomicBool,
) -> Result<Self> {
let image_size = {
let mut file = input.reopen_boxed()?;
file.seek(SeekFrom::End(0))?
};
let ring_algorithm = Self::ring_algorithm(algorithm)?;
let hash_tree = HashTree::new(block_size, ring_algorithm, salt);
let (root_digest, hash_tree_data) = hash_tree.generate(input, image_size, cancel_signal)?;
Ok(Self {
image_size,
block_size,
algorithm: algorithm.to_owned(),
salt: salt.to_vec(),
root_digest,
hash_tree: hash_tree_data,
})
}
/// Update hash tree data coreesponding to the specified file ranges.
pub fn update(
&mut self,
input: &(dyn ReadSeekReopen + Sync),
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
self.root_digest = hash_tree.update(
input,
self.image_size,
ranges,
&mut self.hash_tree,
cancel_signal,
)?;
Ok(())
}
/// Check that a file contains no errors.
pub fn verify(
&self,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let ring_algorithm = Self::ring_algorithm(&self.algorithm)?;
let hash_tree = HashTree::new(self.block_size, ring_algorithm, &self.salt);
hash_tree.verify(
input,
self.image_size,
&self.root_digest,
&self.hash_tree,
cancel_signal,
)
}
}
impl<R: Read> FromReader<R> for HashTreeImage {
type Error = Error;
fn from_reader(mut reader: R) -> Result<Self> {
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if magic != *Self::MAGIC {
return Err(Error::InvalidHeaderMagic(magic));
}
let version = reader.read_u16::<LittleEndian>()?;
if version != Self::VERSION {
return Err(Error::InvalidHeaderVersion(version));
}
let image_size = reader.read_u64::<LittleEndian>()?;
let block_size = reader.read_u32::<LittleEndian>()?;
let algorithm = reader.read_string_padded(16)?;
let salt_size = reader.read_u16::<LittleEndian>()?;
let root_digest_size = reader.read_u16::<LittleEndian>()?;
let hash_tree_size = reader
.read_u32::<LittleEndian>()?
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
let mut salt = vec![0u8; usize::from(salt_size)];
reader.read_exact(&mut salt)?;
let mut root_digest = vec![0u8; usize::from(root_digest_size)];
reader.read_exact(&mut root_digest)?;
let mut hash_tree = vec![0u8; hash_tree_size];
reader.read_exact(&mut hash_tree)?;
Ok(Self {
image_size,
block_size,
algorithm,
salt,
root_digest,
hash_tree,
})
}
}
impl<W: Write> ToWriter<W> for HashTreeImage {
type Error = Error;
fn to_writer(&self, mut writer: W) -> Result<()> {
let salt_size = self
.salt
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("salt_size"))?;
let root_digest_size = self
.root_digest
.len()
.to_u16()
.ok_or_else(|| Error::FieldOutOfBounds("root_digest_size"))?;
let hash_tree_size = self
.hash_tree
.len()
.to_u32()
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_size"))?;
writer.write_all(Self::MAGIC)?;
writer.write_u16::<LittleEndian>(Self::VERSION)?;
writer.write_u64::<LittleEndian>(self.image_size)?;
writer.write_u32::<LittleEndian>(self.block_size)?;
writer.write_string_padded(&self.algorithm, 16)?;
writer.write_u16::<LittleEndian>(salt_size)?;
writer.write_u16::<LittleEndian>(root_digest_size)?;
writer.write_u32::<LittleEndian>(hash_tree_size)?;
writer.write_all(&self.salt)?;
writer.write_all(&self.root_digest)?;
writer.write_all(&self.hash_tree)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io::{Seek, Write};
use assert_matches::assert_matches;
use crate::stream::SharedCursor;
use super::*;
#[test]
fn calculate_level_ranges() {
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, &[]);
assert_eq!(
hash_tree.compute_level_offsets(0).unwrap(),
&[] as &[Range<usize>],
);
assert_eq!(
hash_tree.compute_level_offsets(1024 * 1024 * 1024).unwrap(),
&[69632..8458240, 4096..69632, 0..4096],
)
}
#[test]
fn blocks_for_ranges() {
let hash_tree = HashTree::new(4096, &ring::digest::SHA256, b"Salt");
assert_eq!(
hash_tree.blocks_for_ranges(16384, &[0..16384]).unwrap(),
&[0..4],
);
assert_eq!(hash_tree.blocks_for_ranges(16384, &[0..0]).unwrap(), &[],);
assert_eq!(
hash_tree
.blocks_for_ranges(16384, &[12287..12289, 0..1, 5000..5001])
.unwrap(),
&[0..4],
);
assert_matches!(
hash_tree.blocks_for_ranges(16384, &[0..16385]),
Err(Error::FieldOutOfBounds(_))
);
}
#[test]
fn generate_update_verify() {
let cancel_signal = AtomicBool::new(false);
let hash_tree = HashTree::new(64, &ring::digest::SHA256, b"Salt");
let mut input = SharedCursor::new();
// Try input smaller than one block.
let (root_digest, hash_tree_data) = hash_tree.generate(&input, 0, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x15, 0x0f, 0xe5, 0x51, 0x40, 0x30, 0xb1, 0x43, 0x4a, 0x5d, 0xea, 0xf4, 0x91, 0xec,
0xe9, 0x2c, 0x0e, 0x64, 0x97, 0x44, 0x7d, 0x6d, 0xe7, 0xbd, 0x6b, 0xa8, 0x5e, 0x8c,
0xae, 0x1e, 0x00, 0xa3
],
);
assert_eq!(hash_tree_data, &[]);
// Try larger input that spans multiple blocks are results in an actual
// hash tree being created.
input.write_all(&b"Data".repeat(25)).unwrap();
let (root_digest, mut hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(
root_digest,
&[
0x92, 0xc3, 0xd7, 0x4a, 0x64, 0x03, 0x4b, 0xcc, 0xa9, 0x9a, 0x44, 0xf6, 0x81, 0xa2,
0x4d, 0xdd, 0x97, 0xd3, 0xda, 0x84, 0xdc, 0xe2, 0x1b, 0x83, 0xd1, 0x7b, 0xab, 0x60,
0x59, 0xe8, 0x45, 0x59
],
);
assert_eq!(
hash_tree_data,
&[
0x7e, 0x33, 0x47, 0xb6, 0xf3, 0x7c, 0xde, 0x0e, 0xe2, 0x8d, 0x9e, 0x49, 0x8e, 0xd4,
0xbd, 0x53, 0x3a, 0xa1, 0xff, 0xeb, 0x4f, 0x6d, 0x5a, 0x5f, 0x55, 0x28, 0x37, 0x79,
0xd0, 0x25, 0x07, 0xd5, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Change some data and update the hash tree.
input.rewind().unwrap();
input.write_all(b"Changed").unwrap();
let root_digest = hash_tree
.update(&input, 100, &[0..7], &mut hash_tree_data, &cancel_signal)
.unwrap();
assert_eq!(
root_digest,
&[
0x8d, 0x03, 0xad, 0x18, 0xf2, 0x53, 0x13, 0x59, 0xf5, 0xbf, 0x68, 0x0e, 0x0c, 0x4a,
0x86, 0xe2, 0x6e, 0xaa, 0x3d, 0x4b, 0x0f, 0x1b, 0x57, 0xad, 0x92, 0xe7, 0xbf, 0x3e,
0xa6, 0xb1, 0x2e, 0xcc
],
);
assert_eq!(
hash_tree_data,
&[
0xfe, 0x46, 0xf7, 0x8c, 0xa1, 0xd9, 0xc8, 0xdd, 0x47, 0x9e, 0x6c, 0x32, 0x7c, 0x38,
0x7f, 0x09, 0xe1, 0x58, 0x92, 0xa3, 0xb6, 0xbd, 0x96, 0xef, 0x10, 0xe8, 0x30, 0xb0,
0x37, 0x8d, 0xef, 0x9a, 0xb7, 0x7f, 0x1a, 0x48, 0x92, 0x12, 0x91, 0xdb, 0x92, 0x04,
0x74, 0xf6, 0x86, 0x31, 0xfc, 0x64, 0xb6, 0xc8, 0x72, 0xb0, 0xf7, 0x7d, 0x24, 0xa4,
0x3c, 0x87, 0x1f, 0xc9, 0xd8, 0x17, 0x8a, 0xd9
],
);
// Updated hash tree should match newly generated tree.
let (new_root_digest, new_hash_tree_data) =
hash_tree.generate(&input, 100, &cancel_signal).unwrap();
assert_eq!(new_root_digest, root_digest);
assert_eq!(new_hash_tree_data, hash_tree_data);
// Data should validate successfully.
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap();
// But not if the data is corrupted.
input.rewind().unwrap();
input.write_all(b"Bad").unwrap();
hash_tree
.verify(&input, 100, &root_digest, &hash_tree_data, &cancel_signal)
.unwrap_err();
}
}
+3
View File
@@ -7,6 +7,9 @@ pub mod avb;
pub mod bootimage;
pub mod compression;
pub mod cpio;
pub mod fec;
pub mod hashtree;
pub mod ota;
pub mod padding;
pub mod payload;
pub mod verityrs;
+134 -38
View File
@@ -7,12 +7,13 @@ use std::{
collections::BTreeMap,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
iter,
sync::{atomic::AtomicBool, Arc},
sync::atomic::AtomicBool,
};
use cms::signed_data::SignedData;
use const_oid::{db::rfc5912, ObjectIdentifier};
use memchr::memmem;
use prost::Message;
use ring::digest::Context;
use rsa::{Pkcs1v15Sign, RsaPrivateKey};
use sha1::Sha1;
@@ -24,9 +25,8 @@ use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, Z
use crate::{
crypto,
format::payload::{self, PayloadHeader},
protobuf::build::tools::releasetools::{mod_OtaMetadata::OtaType, OtaMetadata},
protobuf::build::tools::releasetools::{ota_metadata::OtaType, OtaMetadata},
stream::{self, FromReader, HashingReader, HashingWriter},
util,
};
pub const PATH_METADATA: &str = "META-INF/com/android/metadata";
@@ -40,10 +40,12 @@ const NAME_PAYLOAD_METADATA: &str = "payload_metadata.bin";
pub const PF_NAME: &str = "ota-property-files";
pub const PF_STREAMING_NAME: &str = "ota-streaming-property-files";
const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
pub const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";
const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";
const LEGACY_SEP: &str = "|";
#[derive(Debug, Error)]
pub enum Error {
#[error("Cannot find OTA signature footer magic")]
@@ -64,8 +66,12 @@ pub enum Error {
UnsupportedDigestAlgorithm(ObjectIdentifier),
#[error("Unsupported signature algorithm: {0}")]
UnsupportedSignatureAlgorithm(ObjectIdentifier),
#[error("Expected entry offsets {0:?}, but have {1:?}")]
MismatchedPropertyFiles(String, String),
#[error("Invalid legacy metadata line: {0:?}")]
InvalidLegacyMetadataLine(String),
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
UnsupportedLegacyMetadataField { key: String, value: String },
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
MismatchedPropertyFiles { expected: String, actual: String },
#[error("Property files {0:?} exceed {1} byte reserved space")]
InsufficientReservedSpace(String, usize),
#[error("Invalid property file entry: {0:?}")]
@@ -76,8 +82,8 @@ pub enum Error {
CmsSign(#[from] crypto::Error),
#[error("Payload error")]
Payload(#[from] payload::Error),
#[error("Protobuf error")]
Protobuf(#[from] quick_protobuf::Error),
#[error("Failed to decode protobuf message")]
ProtobufDecode(#[from] prost::DecodeError),
#[error("SPKI error")]
Spki(#[from] pkcs8::spki::Error),
#[error("x509 DER error")]
@@ -92,21 +98,111 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
Ok(OtaMetadata::decode(data)?)
}
/// Synthesize protobuf structure from legacy plain-text metadata.
pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
let mut metadata = OtaMetadata::default();
for line in data.split('\n') {
if line.is_empty() {
continue;
}
let (key, value) = line
.split_once('=')
.ok_or_else(|| Error::InvalidLegacyMetadataLine(line.to_owned()))?;
let unsupported = || Error::UnsupportedLegacyMetadataField {
key: key.to_owned(),
value: value.to_owned(),
};
// Booleans are represented by the presence or absence of `<key>=yes`.
let parse_yes = || match value {
"yes" => Ok(true),
_ => Err(unsupported()),
};
let parse_list = || {
value
.split(LEGACY_SEP)
.map(|s| s.to_owned())
.collect::<Vec<_>>()
};
match key {
"ota-type" => {
match OtaType::from_str_name(value).ok_or_else(unsupported)? {
t @ (OtaType::Ab | OtaType::Block) => metadata.set_type(t),
// Not allowed by AOSP in the legacy format.
_ => return Err(unsupported()),
}
}
"ota-wipe" => metadata.wipe = parse_yes()?,
"ota-retrofit-dynamic-partitions" => {
metadata.retrofit_dynamic_partitions = parse_yes()?
}
"ota-downgrade" => metadata.downgrade = parse_yes()?,
"ota-required-cache" => {
metadata.required_cache = value.parse().map_err(|_| unsupported())?;
}
"post-build" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build = parse_list();
}
"post-build-incremental" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
}
"post-sdk-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.sdk_level = value.to_owned();
}
"post-security-patch-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.security_patch_level = value.to_owned();
}
"post-timestamp" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.timestamp = value.parse().map_err(|_| unsupported())?;
}
"pre-device" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.device = parse_list();
}
"pre-build" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build = parse_list();
}
"pre-build-incremental" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
}
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
k if k.ends_with("-property-files") => {
metadata
.property_files
.insert(key.to_owned(), value.to_owned());
}
_ => {
// Ignore. Some OEMs insert values that aren't defined in AOSP.
}
}
}
Ok(metadata)
}
/// Generate the legacy plain-text and modern protobuf serializations of the
/// given metadata instance.
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
const SEP: &str = "|";
use std::fmt::Write;
let mut pairs = BTreeMap::<String, String>::new();
match metadata.type_pb {
OtaType::AB => {
pairs.insert("ota-type".to_owned(), "AB".to_owned());
}
OtaType::BLOCK => {
pairs.insert("ota-type".to_owned(), "BLOCK".to_owned());
}
_ => {}
// Other types are not allowed by AOSP in the legacy format.
if let t @ (OtaType::Ab | OtaType::Block) = metadata.r#type() {
pairs.insert("ota-type".to_owned(), t.as_str_name().to_owned());
}
if metadata.wipe {
pairs.insert("ota-wipe".to_owned(), "yes".to_owned());
@@ -127,7 +223,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
);
if let Some(p) = &metadata.postcondition {
pairs.insert("post-build".to_owned(), p.build.join(SEP));
pairs.insert("post-build".to_owned(), p.build.join(LEGACY_SEP));
pairs.insert(
"post-build-incremental".to_owned(),
p.build_incremental.clone(),
@@ -141,9 +237,9 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
}
if let Some(p) = &metadata.precondition {
pairs.insert("pre-device".to_owned(), p.device.join(SEP));
pairs.insert("pre-device".to_owned(), p.device.join(LEGACY_SEP));
if !p.build.is_empty() {
pairs.insert("pre-build".to_owned(), p.build.join(SEP));
pairs.insert("pre-build".to_owned(), p.build.join(LEGACY_SEP));
pairs.insert(
"pre-build-incremental".to_owned(),
p.build_incremental.clone(),
@@ -157,11 +253,11 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
pairs.extend(metadata.property_files.clone());
let legacy_metadata = pairs
.into_iter()
.map(|(k, v)| format!("{k}={v}\n"))
.collect::<String>();
let modern_metadata = util::write_protobuf(metadata)?;
let legacy_metadata = pairs.into_iter().fold(String::new(), |mut output, (k, v)| {
let _ = writeln!(output, "{k}={v}");
output
});
let modern_metadata = metadata.encode_to_vec();
Ok((legacy_metadata, modern_metadata))
}
@@ -286,9 +382,9 @@ fn add_payload_metadata_entry(
/// 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).
/// `metadata_pb_raw` is the serialized OTA metadata protobuf message from the
/// original OTA. `payload_metadata_size` is the size of the new payload's
/// metadata and metadata signature regions.
/// `metadata` is the OTA metadata protobuf message from the original OTA.
/// `payload_metadata_size` is the size of the new payload's metadata and
/// metadata signature regions.
///
/// The zip file's backing file position MUST BE set to where the central
/// directory would start.
@@ -296,10 +392,10 @@ pub fn add_metadata(
zip_entries: &[ZipEntry],
zip_writer: &mut ZipWriter<impl Write>,
next_offset: u64,
metadata_pb_raw: &[u8],
metadata: &OtaMetadata,
payload_metadata_size: u64,
) -> Result<OtaMetadata> {
let mut metadata: OtaMetadata = util::read_protobuf(metadata_pb_raw)?;
let mut metadata = metadata.clone();
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
let mut zip_entries = zip_entries.to_owned();
@@ -389,7 +485,10 @@ pub fn verify_metadata(
for (key, value) in &metadata.property_files {
let new_value = compute_property_files(key, &zip_entries, Some(value.len()))?;
if *value != new_value {
return Err(Error::MismatchedPropertyFiles(value.clone(), new_value));
return Err(Error::MismatchedPropertyFiles {
expected: value.clone(),
actual: new_value,
});
}
}
@@ -452,10 +551,7 @@ fn parse_ota_sig(mut reader: impl Read + Seek) -> Result<(SignedData, u64)> {
/// CMS signed attributes are intentionally not supported because AOSP recovery
/// does not support them either. It expects the CMS [`SignedData`] structure to
/// be used for nothing more than a raw signature transport mechanism.
pub fn verify_ota(
mut reader: impl Read + Seek,
cancel_signal: &Arc<AtomicBool>,
) -> Result<Certificate> {
pub fn verify_ota(mut reader: impl Read + Seek, cancel_signal: &AtomicBool) -> Result<Certificate> {
let (sd, hashed_size) = parse_ota_sig(&mut reader)?;
// Make sure the certificate in the CMS structure matches the otacert zip
@@ -521,9 +617,9 @@ pub fn parse_zip_ota_info(
let metadata = {
let mut entry = zip.by_name(PATH_METADATA_PB)?;
let mut buf = vec![0u8; entry.size() as usize];
entry.read_exact(&mut buf)?;
util::read_protobuf::<OtaMetadata>(&buf)?
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
OtaMetadata::decode(buf.as_slice())?
};
let certificate = {
+398 -183
View File
@@ -6,7 +6,8 @@
use std::{
collections::{HashMap, HashSet},
io::{self, Cursor, Read, Seek, SeekFrom, Write},
sync::{atomic::AtomicBool, Arc},
ops::Range,
sync::atomic::AtomicBool,
};
use base64::engine::general_purpose::STANDARD;
@@ -14,8 +15,11 @@ use base64::Engine;
use byteorder::{BigEndian, ReadBytesExt};
use bzip2::write::BzDecoder;
use num_traits::ToPrimitive;
use quick_protobuf::MessageWrite;
use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use prost::Message;
use rayon::{
iter::{IndexedParallelIterator, IntoParallelRefMutIterator},
prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
};
use ring::digest::{Context, Digest};
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
use sha2::Sha256;
@@ -30,12 +34,12 @@ use xz2::{
use crate::{
crypto,
protobuf::chromeos_update_engine::{
mod_InstallOperation, mod_Signatures::Signature, DeltaArchiveManifest, Extent,
install_operation::Type, signatures::Signature, DeltaArchiveManifest, Extent,
InstallOperation, PartitionInfo, PartitionUpdate, Signatures,
},
stream::{
self, CountingReader, CountingWriter, FromReader, HashingWriter, ReadDiscardExt, ReadSeek,
SharedCursor, WriteSeek,
self, CountingReader, FromReader, HashingWriter, ReadDiscardExt, ReadSeekReopen, WriteSeek,
WriteSeekReopen,
},
util,
};
@@ -43,44 +47,57 @@ use crate::{
const OTA_MAGIC: &[u8; 4] = b"CrAU";
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
#[derive(Debug, Error)]
pub enum Error {
#[error("Unknown magic: {0:?}")]
UnknownMagic([u8; 4]),
#[error("Unsupported payload version: {0}")]
UnsupportedVersion(u64),
#[error("File is a delta OTA, not a full OTA")]
NotFullOta,
#[error("Payload contains no signatures")]
NoSignatures,
#[error("Blob offset should be {0}, but is {1}")]
InvalidBlobOffset(u64, u64),
#[error("Payload signatures offset should be {0}, but is {1}")]
InvalidPayloadSignaturesOffset(u64, u64),
#[error("Blob offset should be {expected}, but is {actual}")]
InvalidBlobOffset { expected: u64, actual: u64 },
#[error("Payload signatures offset should be {expected}, but is {actual}")]
InvalidPayloadSignaturesOffset { expected: u64, actual: u64 },
#[error("Invalid payload properties line: {0:?}")]
InvalidPropertiesLine(String),
#[error("Duplicate payload property: {0:?}")]
DuplicateProperty(String),
#[error("Payload property {0:?} ({1:?}) does not match expected value {2:?}")]
InvalidProperty(String, String, Option<String>),
#[error("Payload property {key:?} ({actual:?}) does not match expected value {expected:?}")]
InvalidProperty {
key: String,
expected: Option<String>,
actual: String,
},
#[error("Unsupported partition operation: {0:?}")]
UnsupportedOperation(mod_InstallOperation::Type),
#[error("Expected sha256 {0:?}, but have {1:?}")]
MismatchedDigest(Option<String>, String),
#[error("Size of {0} ({1}) is not aligned to the block size ({2})")]
InvalidPartitionSize(String, u64, u32),
UnsupportedOperation(Type),
#[error("Expected sha256 {expected:?}, but have {actual:?}")]
MismatchedDigest {
expected: Option<String>,
actual: String,
},
#[error("Size of {name} ({size}) is not aligned to the block size ({block_size})")]
InvalidPartitionSize {
name: String,
size: u64,
block_size: u32,
},
#[error("Destination extents are not in order")]
ExtentsNotInOrder,
#[error("Partition not found in payload: {0}")]
MissingPartition(String),
#[error("Partitions not found in payload: {0:?}")]
MissingPartitions(HashSet<String>),
#[error("{0:?} field is missing")]
MissingField(&'static str),
#[error("{0:?} field exceeds integer bounds")]
IntegerTooLarge(&'static str),
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("Protobuf error")]
Protobuf(#[from] quick_protobuf::Error),
#[error("Failed to decode protobuf message")]
ProtobufDecode(#[from] prost::DecodeError),
#[error("XZ stream error")]
XzStream(#[from] xz2::stream::Error),
#[error("RSA error")]
@@ -99,6 +116,15 @@ pub struct PayloadHeader {
pub blob_offset: u64,
}
impl PayloadHeader {
pub fn is_full_ota(&self) -> bool {
self.manifest
.partitions
.iter()
.all(|p| p.old_partition_info.is_none())
}
}
impl<R: Read> FromReader<R> for PayloadHeader {
type Error = Error;
@@ -121,21 +147,19 @@ impl<R: Read> FromReader<R> for PayloadHeader {
let manifest_size = reader
.read_u64::<BigEndian>()?
.to_usize()
.ok_or_else(|| Error::IntegerTooLarge("manifest_size"))?;
.and_then(|s| {
if s <= MANIFEST_MAX_SIZE {
Some(s)
} else {
None
}
})
.ok_or_else(|| Error::FieldOutOfBounds("manifest_size"))?;
let metadata_signature_size = reader.read_u32::<BigEndian>()?;
let mut manifest_raw = vec![0u8; manifest_size];
reader.read_exact(&mut manifest_raw)?;
let manifest: DeltaArchiveManifest = util::read_protobuf(&manifest_raw)?;
// Fail as soon as possible since it's impossible to support delta OTAs.
if manifest
.partitions
.iter()
.any(|p| p.old_partition_info.is_some())
{
return Err(Error::NotFullOta);
}
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
// Skip manifest signatures.
reader.read_discard_exact(metadata_signature_size.into())?;
@@ -166,6 +190,7 @@ fn sign_digest(digest: &[u8], key: &RsaPrivateKey) -> Result<Signatures> {
data: Some(digest_signed),
// Always fits in even a u16.
unpadded_signature_size: Some(unpadded_size as u32),
..Default::default()
};
let signatures = Signatures {
@@ -296,9 +321,10 @@ impl<W: Write> PayloadWriter<W> {
// The blob must contain all data in sequential order with no gaps.
for p in &mut header.manifest.partitions {
for op in &mut p.operations {
op.data_offset = Some(blob_size);
if let Some(length) = op.data_length {
// The field must be left unset when the blob contains no
// data for the operation.
op.data_offset = Some(blob_size);
blob_size += length;
}
}
@@ -310,14 +336,14 @@ impl<W: Write> PayloadWriter<W> {
ring::digest::digest(&ring::digest::SHA256, b"").as_ref(),
&key,
)?;
let dummy_sig_size = dummy_sig.get_size();
let dummy_sig_size = dummy_sig.encoded_len();
// Fill out the new payload signature information.
header.manifest.signatures_offset = Some(blob_size);
header.manifest.signatures_size = Some(dummy_sig_size as u64);
// Build new manifest.
let manifest_raw_new = util::write_protobuf(&header.manifest)?;
let manifest_raw_new = header.manifest.encode_to_vec();
// Excludes signatures (hashes are for signing).
let mut h_partial = Context::new(&ring::digest::SHA256);
@@ -345,7 +371,7 @@ impl<W: Write> PayloadWriter<W> {
// in the payload hash.
let metadata_hash = h_partial.clone().finish();
let metadata_sig = sign_digest(metadata_hash.as_ref(), &key)?;
let metadata_sig_raw = util::write_protobuf(&metadata_sig)?;
let metadata_sig_raw = metadata_sig.encode_to_vec();
write_hash!(inner, [h_full], &metadata_sig_raw)?;
Ok(Self {
@@ -373,7 +399,7 @@ impl<W: Write> PayloadWriter<W> {
// Append payload signature.
let payload_partial_hash = self.h_partial.clone().finish();
let payload_sig = sign_digest(payload_partial_hash.as_ref(), &self.key)?;
let payload_sig_raw = util::write_protobuf(&payload_sig)?;
let payload_sig_raw = payload_sig.encode_to_vec();
write_hash!(self.inner, [self.h_full], &payload_sig_raw)?;
// Everything before the blob.
@@ -404,7 +430,8 @@ impl<W: Write> PayloadWriter<W> {
/// [`InstallOperation::data_length`].
pub fn begin_next_operation(&mut self) -> Result<bool> {
if let Some(operation) = self.operation() {
// ZERO/DISCARD operations will not have a length.
// Only operations that reference data in the blob will have a
// length set.
if self.written < operation.data_length.unwrap_or(0) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
@@ -515,111 +542,13 @@ impl<W: Write> Write for PayloadWriter<W> {
}
}
/// A writer to produce a compressed partition image suitable for directly
/// inserting into a `payload.bin`'s blob section. The data is XZ-compressed at
/// a low compression level, primarily to collapse zeros, and the
/// [`PartitionUpdate`] instance is updated with the final size and hash. The
/// entire data will be represented as a single [`InstallOperation`] and
/// [`InstallOperation::data_offset`] will be set to `None`.
pub struct CompressedPartitionWriter<W: Write> {
inner: XzEncoder<HashingWriter<CountingWriter<W>>>,
block_size: u32,
h_uncompressed: Context,
written: u64,
}
impl<W: Write> CompressedPartitionWriter<W> {
pub fn new(writer: W, block_size: u32) -> Result<Self> {
let counting_writer = CountingWriter::new(writer);
let hashing_writer =
HashingWriter::new(counting_writer, Context::new(&ring::digest::SHA256));
// AOSP's payload_consumer does not support CRC during decompression.
// Also, we intentionally pick the lowest compression level since we
// primarily care about squishing zeros. The non-zero portions of boot
// images are usually already-compressed kernels and ramdisks.
let stream = Stream::new_easy_encoder(0, Check::None)?;
let xz_writer = XzEncoder::new_stream(hashing_writer, stream);
Ok(Self {
inner: xz_writer,
block_size,
h_uncompressed: Context::new(&ring::digest::SHA256),
written: 0,
})
}
/// Finish writing and update the [`PartitionUpdate`] instance with the new
/// size, hash, and install operation metadata.
pub fn finish(mut self, partition: &mut PartitionUpdate) -> Result<W> {
if self.written % u64::from(self.block_size) != 0 {
return Err(Error::InvalidPartitionSize(
partition.partition_name.clone(),
self.written,
self.block_size,
));
}
self.inner.flush()?;
let hashing_writer = self.inner.finish()?;
let digest_uncompressed = self.h_uncompressed.finish();
let (counting_writer, context_compressed) = hashing_writer.finish();
let digest_compressed = context_compressed.finish();
// XzEncoder::total_out() cannot be used for an exact byte count because
// XzEncoder::finish() writes data.
let (writer, size_compressed) = counting_writer.finish();
partition.new_partition_info = Some(PartitionInfo {
size: Some(self.written),
hash: Some(digest_uncompressed.as_ref().to_vec()),
});
let extent = Extent {
start_block: Some(0),
num_blocks: Some(self.written / u64::from(self.block_size)),
};
let operation = InstallOperation {
type_pb: mod_InstallOperation::Type::REPLACE_XZ,
// Must be manually updated by the caller.
data_offset: None,
data_length: Some(size_compressed),
src_extents: vec![],
src_length: None,
dst_extents: vec![extent],
dst_length: None,
data_sha256_hash: Some(digest_compressed.as_ref().to_vec()),
src_sha256_hash: None,
};
partition.operations.clear();
partition.operations.push(operation);
Ok(writer)
}
}
impl<W: Write> Write for CompressedPartitionWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.h_uncompressed.update(&buf[..n]);
self.written += n as u64;
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
/// Verify the payload signatures using the specified certificate and check that
/// the digests in `payload_properties.txt` are correct.
pub fn verify_payload(
mut reader: impl Read + Seek,
cert: &Certificate,
properties_raw: &str,
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let header = PayloadHeader::from_reader(&mut reader)?;
reader.rewind()?;
@@ -665,7 +594,7 @@ pub fn verify_payload(
)?;
let buf = writer.into_inner();
util::read_protobuf::<Signatures>(&buf)?
Signatures::decode(buf.as_slice())?
};
// Check the metadata signatures.
@@ -675,7 +604,10 @@ pub fn verify_payload(
{
let actual = reader.stream_position()?;
if header.blob_offset != actual {
return Err(Error::InvalidBlobOffset(header.blob_offset, actual));
return Err(Error::InvalidBlobOffset {
expected: header.blob_offset,
actual,
});
}
}
@@ -697,7 +629,7 @@ pub fn verify_payload(
let expected = header.blob_offset + payload_signatures_offset;
let actual = reader.stream_position()?;
if expected != actual {
return Err(Error::InvalidPayloadSignaturesOffset(expected, actual));
return Err(Error::InvalidPayloadSignaturesOffset { expected, actual });
}
}
@@ -714,7 +646,7 @@ pub fn verify_payload(
)?;
let buf = writer.into_inner();
util::read_protobuf::<Signatures>(&buf)?
Signatures::decode(buf.as_slice())?
};
// Check the payload signatures.
@@ -735,11 +667,11 @@ pub fn verify_payload(
let expected_value = expected_properties.get(&key);
if expected_value != Some(&actual_value) {
return Err(Error::InvalidProperty(
return Err(Error::InvalidProperty {
key,
actual_value,
expected_value.cloned(),
));
expected: expected_value.cloned(),
actual: actual_value,
});
}
}
@@ -753,7 +685,7 @@ pub fn apply_operation(
block_size: u32,
blob_offset: u64,
op: &InstallOperation,
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> Result<()> {
for extent in &op.dst_extents {
let start_block = extent
@@ -765,19 +697,19 @@ pub fn apply_operation(
let out_offset = start_block
.checked_mul(block_size.into())
.ok_or_else(|| Error::IntegerTooLarge("out_offset"))?;
.ok_or_else(|| Error::FieldOutOfBounds("out_offset"))?;
let out_data_length = num_blocks
.checked_mul(block_size.into())
.ok_or_else(|| Error::IntegerTooLarge("out_data_length"))?;
.ok_or_else(|| Error::FieldOutOfBounds("out_data_length"))?;
writer.seek(SeekFrom::Start(out_offset))?;
let mut hasher = Context::new(&ring::digest::SHA256);
match op.type_pb {
match op.r#type() {
// Handle ZERO/DISCARD specially since they don't require access to
// the payload blob.
mod_InstallOperation::Type::ZERO | mod_InstallOperation::Type::DISCARD => {
Type::Zero | Type::Discard => {
stream::copy_n_inspect(
io::repeat(0),
&mut writer,
@@ -795,12 +727,12 @@ pub fn apply_operation(
.ok_or_else(|| Error::MissingField("data_length"))?;
let in_offset = blob_offset
.checked_add(data_offset)
.ok_or_else(|| Error::IntegerTooLarge("in_offset"))?;
.ok_or_else(|| Error::FieldOutOfBounds("in_offset"))?;
reader.seek(SeekFrom::Start(in_offset))?;
match other {
mod_InstallOperation::Type::REPLACE => {
Type::Replace => {
stream::copy_n_inspect(
&mut reader,
&mut writer,
@@ -809,7 +741,7 @@ pub fn apply_operation(
cancel_signal,
)?;
}
mod_InstallOperation::Type::REPLACE_BZ => {
Type::ReplaceBz => {
let mut decoder = BzDecoder::new(&mut writer);
stream::copy_n_inspect(
&mut reader,
@@ -820,7 +752,7 @@ pub fn apply_operation(
)?;
decoder.finish()?;
}
mod_InstallOperation::Type::REPLACE_XZ => {
Type::ReplaceXz => {
let mut decoder = XzDecoder::new(&mut writer);
stream::copy_n_inspect(
&mut reader,
@@ -831,7 +763,7 @@ pub fn apply_operation(
)?;
decoder.finish()?;
}
_ => return Err(Error::UnsupportedOperation(op.type_pb)),
_ => return Err(Error::UnsupportedOperation(op.r#type())),
}
}
}
@@ -839,47 +771,45 @@ pub fn apply_operation(
let expected_digest = op.data_sha256_hash.as_deref();
let digest = hasher.finish();
if expected_digest != Some(digest.as_ref())
&& op.type_pb != mod_InstallOperation::Type::ZERO
{
return Err(Error::MismatchedDigest(
expected_digest.map(hex::encode),
hex::encode(digest.as_ref()),
));
if expected_digest != Some(digest.as_ref()) && op.r#type() != Type::Zero {
return Err(Error::MismatchedDigest {
expected: expected_digest.map(hex::encode),
actual: hex::encode(digest.as_ref()),
});
}
}
Ok(())
}
/// Extract the specified image from the payload into memory. This is done
/// multithreaded and uses rayon's global thread pool. `open_payload` will be
/// called from multiple threads.
pub fn extract_image_to_memory(
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
/// Extract the specified image from the payload. This is done multithreaded and
/// uses rayon's global thread pool. Both the `payload` and `output` streams
/// will be reopened from multiple threads.
pub fn extract_image(
payload: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
header: &PayloadHeader,
partition_name: &str,
cancel_signal: &Arc<AtomicBool>,
) -> Result<SharedCursor> {
cancel_signal: &AtomicBool,
) -> Result<()> {
let partition = header
.manifest
.partitions
.iter()
.find(|p| p.partition_name == partition_name)
.ok_or_else(|| Error::MissingPartition(partition_name.to_owned()))?;
let stream = SharedCursor::default();
partition
.operations
.par_iter()
.map(|op| -> Result<()> {
let reader = open_payload()?;
let writer = stream.clone();
let reader = payload.reopen_boxed()?;
let writer = output.reopen_boxed()?;
apply_operation(
reader,
writer,
header.manifest.block_size,
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
@@ -887,20 +817,18 @@ pub fn extract_image_to_memory(
Ok(())
})
.collect::<Result<_>>()?;
Ok(stream)
.collect::<Result<_>>()
}
/// Extract the specified partition images from the payload into writers. This
/// is done multithreaded and uses rayon's global thread pool. `open_payload`
/// and `open_output` will be called from multiple threads.
pub fn extract_images<'a>(
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
payload: &(dyn ReadSeekReopen + Sync),
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
header: &PayloadHeader,
partition_names: impl IntoIterator<Item = &'a str>,
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> Result<()> {
let mut remaining = partition_names.into_iter().collect::<HashSet<_>>();
// We parallelize at the operation level or else one thread might get stuck
@@ -923,13 +851,13 @@ pub fn extract_images<'a>(
operations
.into_par_iter()
.map(|(name, op)| -> Result<()> {
let reader = open_payload()?;
let reader = payload.reopen_boxed()?;
let writer = open_output(name)?;
apply_operation(
reader,
writer,
header.manifest.block_size,
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
@@ -939,3 +867,290 @@ pub fn extract_images<'a>(
})
.collect()
}
fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8>, Digest)> {
let reader = Cursor::new(raw_data);
let writer = Cursor::new(Vec::new());
let hashing_writer = HashingWriter::new(writer, Context::new(&ring::digest::SHA256));
// AOSP's payload_consumer does not support checking CRC during
// decompression. Also, we intentionally pick the lowest compression level
// since we primarily care about squishing zeros. The non-zero portions of
// boot images are usually already-compressed kernels and ramdisks.
let stream = Stream::new_easy_encoder(0, Check::None)?;
let mut xz_writer = XzEncoder::new_stream(hashing_writer, stream);
stream::copy_n(reader, &mut xz_writer, raw_data.len() as u64, cancel_signal)?;
let hashing_writer = xz_writer.finish()?;
let (writer, context_compressed) = hashing_writer.finish();
let digest_compressed = context_compressed.finish();
let data = writer.into_inner();
Ok((data, digest_compressed))
}
/// Compress the image and return the corresponding information to insert into
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
/// then written in parallel (but in order) to the output. Each chunk will have
/// a corresponding [`InstallOperation`] in the return value. The caller must
/// update [`InstallOperation::data_offset`] in each operation manually because
/// the initial values are relative to 0.
pub fn compress_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
partition_name: &str,
block_size: u32,
cancel_signal: &AtomicBool,
) -> Result<(PartitionInfo, Vec<InstallOperation>)> {
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
const CHUNK_GROUP: u64 = 32;
let file_size = input.reopen_boxed()?.seek(SeekFrom::End(0))?;
let final_chunk_different = file_size % CHUNK_SIZE != 0;
if file_size % u64::from(block_size) != 0 || CHUNK_SIZE % u64::from(block_size) != 0 {
return Err(Error::InvalidPartitionSize {
name: partition_name.to_owned(),
size: file_size,
block_size,
});
}
let chunks_total = util::div_ceil(file_size, CHUNK_SIZE);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut operations = vec![];
// Read the file one group at a time. This allows for some parallelization
// without reading the entire file into memory. This is necessary because we
// need to compute the checksum of the entire file.
while (operations.len() as u64) < chunks_total {
let chunks_done = operations.len() as u64;
let chunks_group = (chunks_total - chunks_done).min(CHUNK_GROUP);
let uncompressed_data_group = (chunks_done..chunks_done + chunks_group)
.into_par_iter()
.map(|chunk| -> Result<(u64, Vec<u8>)> {
let mut reader = input.reopen_boxed()?;
let offset = reader.seek(SeekFrom::Start(chunk * CHUNK_SIZE))?;
let chunk_size = if final_chunk_different && chunk == chunks_total - 1 {
file_size % CHUNK_SIZE
} else {
CHUNK_SIZE
};
let mut data = vec![0u8; chunk_size as usize];
stream::check_cancel(cancel_signal)?;
reader.read_exact(&mut data)?;
Ok((offset, data))
})
.collect::<Result<Vec<_>>>()?;
for (_, data) in &uncompressed_data_group {
context_uncompressed.update(data);
}
let mut compressed_data_group = uncompressed_data_group
.into_par_iter()
.map(
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
let extent = Extent {
start_block: Some(raw_offset / u64::from(block_size)),
num_blocks: Some(raw_data.len() as u64 / u64::from(block_size)),
};
let mut operation = InstallOperation::default();
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(data.len() as u64);
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, operation))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, operation) in &mut compressed_data_group {
operation.data_offset = Some(bytes_compressed);
bytes_compressed += data.len() as u64;
}
let group_operations = compressed_data_group
.into_par_iter()
.map(|(data, operation)| -> Result<InstallOperation> {
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
Ok(operation)
})
.collect::<Result<Vec<_>>>()?;
operations.extend(group_operations.into_iter());
}
let digest_uncompressed = context_uncompressed.finish();
let partition_info = PartitionInfo {
size: Some(file_size),
hash: Some(digest_uncompressed.as_ref().to_vec()),
};
Ok((partition_info, operations))
}
fn extents_sorted(operations: &[InstallOperation]) -> bool {
let mut offset = 0;
for operation in operations {
if operation.dst_extents.is_empty() {
return false;
}
for extent in &operation.dst_extents {
let Some(start) = extent.start_block else {
return false;
};
let Some(size) = extent.num_blocks else {
return false;
};
if start != offset {
return false;
}
let Some(next) = start.checked_add(size) else {
return false;
};
offset = next;
}
}
true
}
/// Compress the modified image and update the specified [`PartitionInfo`] and
/// list of [`InstallOperation`]s. [`InstallOperation`]s that do not match any
/// byte range in `ranges` will not be compressed. The caller must update
/// [`InstallOperation::data_offset`] in each operation manually because the
/// initial values are relative to 0.
///
/// Returns the ranges of indices of `operations` that were updated.
pub fn compress_modified_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
block_size: u32,
partition_info: &mut PartitionInfo,
operations: &mut [InstallOperation],
ranges: &[Range<u64>],
cancel_signal: &AtomicBool,
) -> Result<Vec<Range<usize>>> {
const OPERATION_GROUP: usize = 32;
// Full OTAs created by payload_generator have one extent per operation and
// they're all in order with no gaps. Verify this so we can take advantage
// of this layout.
if !extents_sorted(operations) {
return Err(Error::ExtentsNotInOrder);
}
let groups_total = util::div_ceil(operations.len(), OPERATION_GROUP);
let mut bytes_compressed = 0;
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
let mut modified_operations = vec![];
// Read the file one group at a time. This allows for some parallelization
// without reading the entire file into memory. This is necessary because we
// need to compute the checksum of the entire file.
for group in 0..groups_total {
let operation_start = group * OPERATION_GROUP;
let operation_size = (operations.len() - operation_start).min(OPERATION_GROUP);
let operation_end = operation_start + operation_size;
let uncompressed_data_group = operations[operation_start..operation_end]
.par_iter()
.map(|operation| -> Result<(Vec<u8>, bool)> {
let extents_start = operation.dst_extents[0]
.start_block()
.checked_mul(u64::from(block_size))
.ok_or_else(|| Error::FieldOutOfBounds("extents_start"))?;
let extents_size = operation
.dst_extents
.iter()
.map(|e| e.num_blocks())
.try_fold(0u64, |acc, n| acc.checked_add(n))
.and_then(|n| n.checked_mul(u64::from(block_size)))
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
let extents_end = extents_start
.checked_add(extents_size)
.ok_or_else(|| Error::FieldOutOfBounds("extents_end"))?;
let extents_size = extents_size
.to_usize()
.ok_or_else(|| Error::FieldOutOfBounds("extents_size"))?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(extents_start))?;
let mut data = vec![0u8; extents_size];
stream::check_cancel(cancel_signal)?;
reader.read_exact(&mut data)?;
let was_modified = util::ranges_overlaps(ranges, &(extents_start..extents_end));
Ok((data, was_modified))
})
.collect::<Result<Vec<_>>>()?;
for (data, _) in &uncompressed_data_group {
context_uncompressed.update(data);
}
// Only compress the modified chunks.
let mut compressed_data_group = operations[operation_start..operation_end]
.par_iter_mut()
.enumerate()
.zip(uncompressed_data_group)
.filter(|(_, (_, was_modified))| *was_modified)
.map(
|((i_rel, operation), (raw_data, _))| -> Result<(Vec<u8>, usize, &mut InstallOperation)> {
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(data.len() as u64);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
Ok((data, i_rel + operation_start, operation))
},
)
.collect::<Result<Vec<_>>>()?;
for (data, _, operation) in &mut compressed_data_group {
operation.data_offset = Some(bytes_compressed);
bytes_compressed += data.len() as u64;
}
let modified_group_operations = compressed_data_group
.into_par_iter()
.map(|(data, i, operation)| {
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
writer.write_all(&data)?;
Ok(i..i + 1)
})
.collect::<Result<Vec<_>>>()?;
modified_operations.extend(modified_group_operations);
}
let digest_uncompressed = context_uncompressed.finish();
partition_info.hash = Some(digest_uncompressed.as_ref().to_vec());
Ok(util::merge_overlapping(&modified_operations))
}
+142
View File
@@ -0,0 +1,142 @@
/*
* 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
// implementations for every supported configuration.
#![allow(non_snake_case)]
use gf256::rs::rs;
use phf::phf_map;
#[rs(block = 255, data = 231)]
mod rs255w231 {}
#[rs(block = 255, data = 232)]
mod rs255w232 {}
#[rs(block = 255, data = 233)]
mod rs255w233 {}
#[rs(block = 255, data = 234)]
mod rs255w234 {}
#[rs(block = 255, data = 235)]
mod rs255w235 {}
#[rs(block = 255, data = 236)]
mod rs255w236 {}
#[rs(block = 255, data = 237)]
mod rs255w237 {}
#[rs(block = 255, data = 238)]
mod rs255w238 {}
#[rs(block = 255, data = 239)]
mod rs255w239 {}
#[rs(block = 255, data = 240)]
mod rs255w240 {}
#[rs(block = 255, data = 241)]
mod rs255w241 {}
#[rs(block = 255, data = 242)]
mod rs255w242 {}
#[rs(block = 255, data = 243)]
mod rs255w243 {}
#[rs(block = 255, data = 244)]
mod rs255w244 {}
#[rs(block = 255, data = 245)]
mod rs255w245 {}
#[rs(block = 255, data = 246)]
mod rs255w246 {}
#[rs(block = 255, data = 247)]
mod rs255w247 {}
#[rs(block = 255, data = 248)]
mod rs255w248 {}
#[rs(block = 255, data = 249)]
mod rs255w249 {}
#[rs(block = 255, data = 250)]
mod rs255w250 {}
#[rs(block = 255, data = 251)]
mod rs255w251 {}
#[rs(block = 255, data = 252)]
mod rs255w252 {}
#[rs(block = 255, data = 253)]
mod rs255w253 {}
pub static FN_ENCODE: phf::Map<u8, fn(&mut [u8])> = phf_map! {
231u8 => rs255w231::encode,
232u8 => rs255w232::encode,
233u8 => rs255w233::encode,
234u8 => rs255w234::encode,
235u8 => rs255w235::encode,
236u8 => rs255w236::encode,
237u8 => rs255w237::encode,
238u8 => rs255w238::encode,
239u8 => rs255w239::encode,
240u8 => rs255w240::encode,
241u8 => rs255w241::encode,
242u8 => rs255w242::encode,
243u8 => rs255w243::encode,
244u8 => rs255w244::encode,
245u8 => rs255w245::encode,
246u8 => rs255w246::encode,
247u8 => rs255w247::encode,
248u8 => rs255w248::encode,
249u8 => rs255w249::encode,
250u8 => rs255w250::encode,
251u8 => rs255w251::encode,
252u8 => rs255w252::encode,
253u8 => rs255w253::encode,
};
pub static FN_IS_CORRECT: phf::Map<u8, fn(&[u8]) -> bool> = phf_map! {
231u8 => rs255w231::is_correct,
232u8 => rs255w232::is_correct,
233u8 => rs255w233::is_correct,
234u8 => rs255w234::is_correct,
235u8 => rs255w235::is_correct,
236u8 => rs255w236::is_correct,
237u8 => rs255w237::is_correct,
238u8 => rs255w238::is_correct,
239u8 => rs255w239::is_correct,
240u8 => rs255w240::is_correct,
241u8 => rs255w241::is_correct,
242u8 => rs255w242::is_correct,
243u8 => rs255w243::is_correct,
244u8 => rs255w244::is_correct,
245u8 => rs255w245::is_correct,
246u8 => rs255w246::is_correct,
247u8 => rs255w247::is_correct,
248u8 => rs255w248::is_correct,
249u8 => rs255w249::is_correct,
250u8 => rs255w250::is_correct,
251u8 => rs255w251::is_correct,
252u8 => rs255w252::is_correct,
253u8 => rs255w253::is_correct,
};
// Each one of these has its own error type, but the functions can only fail one
// way (too many corrupt bytes), so just throw away the error and return an
// Option instead.
#[allow(clippy::type_complexity)]
pub static FN_CORRECT_ERRORS: phf::Map<u8, fn(&mut [u8]) -> Option<usize>> = phf_map! {
231u8 => |data: &mut [u8]| rs255w231::correct_errors(data).ok(),
232u8 => |data: &mut [u8]| rs255w232::correct_errors(data).ok(),
233u8 => |data: &mut [u8]| rs255w233::correct_errors(data).ok(),
234u8 => |data: &mut [u8]| rs255w234::correct_errors(data).ok(),
235u8 => |data: &mut [u8]| rs255w235::correct_errors(data).ok(),
236u8 => |data: &mut [u8]| rs255w236::correct_errors(data).ok(),
237u8 => |data: &mut [u8]| rs255w237::correct_errors(data).ok(),
238u8 => |data: &mut [u8]| rs255w238::correct_errors(data).ok(),
239u8 => |data: &mut [u8]| rs255w239::correct_errors(data).ok(),
240u8 => |data: &mut [u8]| rs255w240::correct_errors(data).ok(),
241u8 => |data: &mut [u8]| rs255w241::correct_errors(data).ok(),
242u8 => |data: &mut [u8]| rs255w242::correct_errors(data).ok(),
243u8 => |data: &mut [u8]| rs255w243::correct_errors(data).ok(),
244u8 => |data: &mut [u8]| rs255w244::correct_errors(data).ok(),
245u8 => |data: &mut [u8]| rs255w245::correct_errors(data).ok(),
246u8 => |data: &mut [u8]| rs255w246::correct_errors(data).ok(),
247u8 => |data: &mut [u8]| rs255w247::correct_errors(data).ok(),
248u8 => |data: &mut [u8]| rs255w248::correct_errors(data).ok(),
249u8 => |data: &mut [u8]| rs255w249::correct_errors(data).ok(),
250u8 => |data: &mut [u8]| rs255w250::correct_errors(data).ok(),
251u8 => |data: &mut [u8]| rs255w251::correct_errors(data).ok(),
252u8 => |data: &mut [u8]| rs255w252::correct_errors(data).ok(),
253u8 => |data: &mut [u8]| rs255w253::correct_errors(data).ok(),
};
+3 -1
View File
@@ -13,10 +13,12 @@
// We use pb-rs' nostd mode. See build.rs.
extern crate alloc;
pub mod boot;
pub mod cli;
pub mod crypto;
pub mod escape;
pub mod format;
pub mod octal;
pub mod patch;
pub mod protobuf;
pub mod stream;
pub mod util;
+53
View File
@@ -0,0 +1,53 @@
/*
* 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.
use std::{
fmt::{self, Octal},
marker::PhantomData,
};
use num_traits::{Num, PrimInt};
use serde::{de::Visitor, Deserializer, Serializer};
pub fn serialize<S, T>(data: &T, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: PrimInt + Octal,
{
serializer.serialize_str(&format!("{data:o}"))
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
{
struct OctalStrVisitor<T>(PhantomData<T>);
impl<'de, T> Visitor<'de> for OctalStrVisitor<T>
where
T: PrimInt,
<T as Num>::FromStrRadixErr: fmt::Display,
{
type Value = T;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "a string containing an octal number")
}
fn visit_str<E>(self, data: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
T::from_str_radix(data, 8).map_err(serde::de::Error::custom)
}
}
deserializer.deserialize_str(OctalStrVisitor(PhantomData))
}
+342 -161
View File
@@ -5,15 +5,18 @@
use std::{
cmp::Ordering,
collections::HashMap,
collections::{HashMap, HashSet},
fs::File,
io::{self, BufRead, BufReader, Cursor, Read, Seek, Write},
io::{self, BufRead, BufReader, Cursor, Read, Seek},
num::ParseIntError,
ops::Range,
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Arc},
slice,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator};
use regex::bytes::Regex;
use ring::digest::Context;
use rsa::RsaPrivateKey;
@@ -23,28 +26,28 @@ use xz2::{
stream::{Check, Stream},
write::XzEncoder,
};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
use zip::{result::ZipError, ZipArchive};
use crate::{
crypto,
format::{
avb::{self, AlgorithmType, Descriptor},
avb::{self, AppendedDescriptorMut, Footer, Header},
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntryNew},
cpio::{self, CpioEntry, CpioEntryData},
},
stream::{self, FromReader, HashingWriter, SectionReader, ToWriter},
util::EscapedString,
patch::otacert::{self, OtaCertBuildFlags},
stream::{self, FromReader, HashingWriter, ReadSeek, SectionReader, ToWriter, WriteSeek},
};
#[derive(Debug, Error)]
pub enum Error {
#[error("No compatible boot image found for {0}")]
NoTargets(&'static str),
#[error("Boot image has no vbmeta footer")]
NoFooter,
#[error("No hash descriptor found in vbmeta footer")]
#[error("No hash descriptor found in vbmeta header")]
NoHashDescriptor,
#[error("Found multiple hash descriptors in vbmeta footer")]
MultipleHashDescriptors,
#[error("Validation error: {0}")]
Validation(String),
#[error("Failed to parse Magisk version from line: {0:?}")]
@@ -61,35 +64,64 @@ pub enum Error {
Crypto(#[from] crypto::Error),
#[error("CPIO error")]
Cpio(#[from] cpio::Error),
#[error("OTA certificate error")]
OtaCert(#[from] otacert::Error),
#[error("XZ stream error")]
XzStream(#[from] xz2::stream::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
#[error("I/O error")]
IoError(#[from] io::Error),
Io(#[from] io::Error),
#[error("File I/O error")]
File(PathBuf, #[source] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
fn load_ramdisk(data: &[u8]) -> Result<(Vec<CpioEntryNew>, CompressedFormat)> {
fn load_ramdisk(
data: &[u8],
cancel_signal: &AtomicBool,
) -> Result<(Vec<CpioEntry>, CompressedFormat)> {
let raw_reader = Cursor::new(data);
let mut reader = CompressedReader::new(raw_reader, false)?;
let entries = cpio::load(&mut reader, false)?;
let entries = cpio::load(&mut reader, false, cancel_signal)?;
Ok((entries, reader.format()))
}
fn save_ramdisk(entries: &[CpioEntryNew], format: CompressedFormat) -> Result<Vec<u8>> {
fn save_ramdisk(
entries: &[CpioEntry],
format: CompressedFormat,
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let raw_writer = Cursor::new(vec![]);
let mut writer = CompressedWriter::new(raw_writer, format)?;
cpio::save(&mut writer, entries, false)?;
cpio::save(&mut writer, entries, false, cancel_signal)?;
let raw_writer = writer.finish()?;
Ok(raw_writer.into_inner())
}
pub trait BootImagePatcher {
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()>;
pub struct BootImageInfo {
pub header: Header,
pub footer: Footer,
pub image_size: u64,
pub boot_image: BootImage,
}
pub trait BootImagePatch {
fn patcher_name(&self) -> &'static str;
/// Inspect a set of possible candidate boot images and return a list of
/// image names that can be patched. Both the boot image and the AVB info
/// can be inspected, but during patching, only the boot image is available.
fn find_targets<'a>(
&self,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> Result<Vec<&'a str>>;
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
}
/// Root a boot image with Magisk.
@@ -108,7 +140,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: &[Range<u32>] = &[25102..25207, 25211..26300];
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..26500];
const VER_PREINIT_DEVICE: Range<u32> =
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
@@ -161,7 +193,7 @@ impl MagiskRootPatcher {
}
fn get_version(path: &Path) -> Result<u32> {
let reader = File::open(path)?;
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
let reader = BufReader::new(reader);
let mut zip = ZipArchive::new(reader)?;
let entry = zip.by_name("assets/util_functions.sh")?;
@@ -191,7 +223,7 @@ impl MagiskRootPatcher {
/// entries as `.backup/<path>`.
///
/// Both lists and entries within the lists may be mutated.
fn apply_magisk_backup(old_entries: &mut [CpioEntryNew], new_entries: &mut Vec<CpioEntryNew>) {
fn apply_magisk_backup(old_entries: &mut [CpioEntry], new_entries: &mut Vec<CpioEntry>) {
cpio::sort(old_entries);
cpio::sort(new_entries);
@@ -203,20 +235,20 @@ impl MagiskRootPatcher {
loop {
match (old_iter.peek(), new_iter.peek()) {
(Some(&old), Some(&new)) => match old.name.cmp(&new.name) {
(Some(&old), Some(&new)) => match old.path.cmp(&new.path) {
Ordering::Less => {
to_back_up.push(old);
old_iter.next();
}
Ordering::Equal => {
if old.content != new.content {
if old.data != new.data {
to_back_up.push(old);
}
old_iter.next();
new_iter.next();
}
Ordering::Greater => {
rm_list.extend(&new.name);
rm_list.extend(&new.path);
rm_list.push(b'\0');
new_iter.next();
}
@@ -226,7 +258,7 @@ impl MagiskRootPatcher {
old_iter.next();
}
(None, Some(new)) => {
rm_list.extend(&new.name);
rm_list.extend(&new.path);
rm_list.push(b'\0');
new_iter.next();
}
@@ -234,28 +266,47 @@ impl MagiskRootPatcher {
}
}
// Intentially using 000 permissions to match Magisk.
new_entries.push(CpioEntryNew::new_directory(b".backup"));
new_entries.push(CpioEntry::new_directory(b".backup", 0));
for old_entry in to_back_up {
let mut new_entry = old_entry.clone();
new_entry.name = b".backup/".to_vec();
new_entry.name.extend(&old_entry.name);
new_entry.path = b".backup/".to_vec();
new_entry.path.extend(&old_entry.path);
new_entries.push(new_entry);
}
{
// Intentially using 000 permissions to match Magisk.
let mut entry = CpioEntryNew::new_file(b".backup/.rmlist");
entry.content = rm_list;
new_entries.push(entry);
}
new_entries.push(CpioEntry::new_file(
b".backup/.rmlist",
0,
CpioEntryData::Data(rm_list),
));
}
}
impl BootImagePatcher for MagiskRootPatcher {
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &Arc<AtomicBool>) -> Result<()> {
let zip_reader = File::open(&self.apk_path)?;
impl BootImagePatch for MagiskRootPatcher {
fn patcher_name(&self) -> &'static str {
"MagiskRootPatcher"
}
fn find_targets<'a>(
&self,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> Result<Vec<&'a str>> {
let mut targets = vec![];
if boot_images.contains_key("init_boot") {
targets.push("init_boot");
} else if boot_images.contains_key("boot") {
targets.push("boot");
};
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
let zip_reader =
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
// Load the first ramdisk. If it doesn't exist, we have to generate one
@@ -266,7 +317,7 @@ impl BootImagePatcher for MagiskRootPatcher {
BootImage::VendorV3Through4(b) => b.ramdisks.first(),
};
let (mut entries, ramdisk_format) = match ramdisk {
Some(r) if !r.is_empty() => load_ramdisk(r)?,
Some(r) if !r.is_empty() => load_ramdisk(r, cancel_signal)?,
_ => (vec![], CompressedFormat::Lz4Legacy),
};
@@ -277,13 +328,11 @@ impl BootImagePatcher for MagiskRootPatcher {
(b"overlay.d".as_slice(), 0o750),
(b"overlay.d/sbin".as_slice(), 0o750),
] {
let mut entry = CpioEntryNew::new_directory(path);
entry.mode |= perms;
entries.push(entry);
entries.push(CpioEntry::new_directory(path, perms));
}
// Delete the original init.
entries.retain(|e| e.name != b"init");
entries.retain(|e| e.path != b"init");
// Add magiskinit.
{
@@ -291,10 +340,11 @@ impl BootImagePatcher for MagiskRootPatcher {
let mut data = vec![];
zip_entry.read_to_end(&mut data)?;
let mut entry = CpioEntryNew::new_file(b"init");
entry.mode |= 0o750;
entry.content = data;
entries.push(entry);
entries.push(CpioEntry::new_file(
b"init",
0o750,
CpioEntryData::Data(data),
));
}
// Add xz-compressed magisk32 and magisk64.
@@ -323,10 +373,12 @@ impl BootImagePatcher for MagiskRootPatcher {
stream::copy(reader, &mut writer, cancel_signal)?;
let raw_writer = writer.finish()?;
let mut entry = CpioEntryNew::new_file(target);
entry.mode |= 0o644;
entry.content = raw_writer.into_inner();
entries.push(entry);
entries.push(CpioEntry::new_file(
target,
0o644,
CpioEntryData::Data(raw_writer.into_inner()),
));
}
// Create Magisk .backup directory structure.
@@ -356,17 +408,16 @@ impl BootImagePatcher for MagiskRootPatcher {
magisk_config.push_str(&format!("RANDOMSEED={:#x}\n", self.random_seed));
}
{
// Intentially using 000 permissions to match Magisk.
let mut entry = CpioEntryNew::new_file(b".backup/.magisk");
entry.content = magisk_config.into_bytes();
entries.push(entry);
}
entries.push(CpioEntry::new_file(
b".backup/.magisk",
0,
CpioEntryData::Data(magisk_config.into_bytes()),
));
// Repack ramdisk.
cpio::sort(&mut entries);
cpio::reassign_inodes(&mut entries);
let new_ramdisk = save_ramdisk(&entries, ramdisk_format)?;
cpio::assign_inodes(&mut entries, false)?;
let new_ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
match boot_image {
BootImage::V0Through2(b) => b.ramdisk = new_ramdisk,
@@ -399,13 +450,16 @@ pub struct OtaCertPatcher {
}
impl OtaCertPatcher {
const OTACERTS_PATH: &[u8] = b"system/etc/security/otacerts.zip";
const OTACERTS_PATH: &'static [u8] = b"system/etc/security/otacerts.zip";
pub fn new(cert: Certificate) -> Self {
Self { cert }
}
pub fn get_certificates(boot_image: &BootImage) -> Result<Vec<Certificate>> {
pub fn get_certificates(
boot_image: &BootImage,
cancel_signal: &AtomicBool,
) -> Result<Vec<Certificate>> {
let mut ramdisks = vec![];
match boot_image {
@@ -417,12 +471,19 @@ impl OtaCertPatcher {
let mut certificates = vec![];
for ramdisk in ramdisks {
let (entries, _) = load_ramdisk(ramdisk)?;
let Some(entry) = entries.iter().find(|e| e.name == Self::OTACERTS_PATH) else {
if ramdisk.is_empty() {
continue;
}
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
let Some(entry) = entries.iter().find(|e| e.path == Self::OTACERTS_PATH) else {
continue;
};
let CpioEntryData::Data(data) = &entry.data else {
continue;
};
let mut zip = ZipArchive::new(Cursor::new(&entry.content))?;
let mut zip = ZipArchive::new(Cursor::new(&data))?;
for index in 0..zip.len() {
let zip_entry = zip.by_index(index)?;
@@ -438,62 +499,88 @@ impl OtaCertPatcher {
Ok(certificates)
}
fn patch_ramdisk(&self, data: &mut Vec<u8>) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(data)?;
let Some(entry) = entries.iter_mut().find(|e| e.name == Self::OTACERTS_PATH) else {
fn patch_ramdisk(
&self,
ramdisk: &mut Vec<u8>,
zip: &[u8],
cancel_signal: &AtomicBool,
) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
let Some(entry) = entries.iter_mut().find(|e| e.path == Self::OTACERTS_PATH) else {
return Ok(false);
};
// Create a new otacerts archive. The old certs are ignored since
// flashing a stock OTA will render the device unbootable.
{
let raw_writer = Cursor::new(vec![]);
let mut writer = ZipWriter::new(raw_writer);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
writer.start_file("ota.x509.pem", options)?;
crypto::write_pem_cert(&mut writer, &self.cert)?;
let raw_writer = writer.finish()?;
entry.content = raw_writer.into_inner();
}
entry.data = CpioEntryData::Data(zip.to_vec());
// Repack ramdisk.
*data = save_ramdisk(&entries, ramdisk_format)?;
*ramdisk = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
}
impl BootImagePatcher for OtaCertPatcher {
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
let patched_any = match boot_image {
BootImage::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk)?,
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk)?,
BootImage::VendorV3Through4(b) => {
let mut patched = false;
impl BootImagePatch for OtaCertPatcher {
fn patcher_name(&self) -> &'static str {
"OtaCertPatcher"
}
for ramdisk in &mut b.ramdisks {
if self.patch_ramdisk(ramdisk)? {
patched = true;
break;
}
fn find_targets<'a>(
&self,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> Result<Vec<&'a str>> {
let mut targets = vec![];
'outer: for (name, info) in boot_images {
let ramdisks = match &info.boot_image {
BootImage::V0Through2(b) => slice::from_ref(&b.ramdisk),
BootImage::V3Through4(b) => slice::from_ref(&b.ramdisk),
BootImage::VendorV3Through4(b) => &b.ramdisks,
};
for ramdisk in ramdisks {
if ramdisk.is_empty() {
continue;
}
patched
let (entries, _) = load_ramdisk(ramdisk, cancel_signal)?;
if entries.iter().any(|e| e.path == Self::OTACERTS_PATH) {
targets.push(*name);
continue 'outer;
}
}
}
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
let ramdisks = match boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
BootImage::VendorV3Through4(b) => &mut b.ramdisks,
};
let new_zip = otacert::create_zip(&self.cert, OtaCertBuildFlags::empty())?;
for ramdisk in ramdisks {
if ramdisk.is_empty() {
continue;
}
if self.patch_ramdisk(ramdisk, &new_zip, cancel_signal)? {
return Ok(());
}
}
// Fail hard if otacerts does not exist. We don't want to lock the user
// out of future updates if the OTA certificate mechanism has changed.
if !patched_any {
return Err(Error::Validation(format!(
"No ramdisk contains {}",
EscapedString::new(Self::OTACERTS_PATH),
)));
}
Ok(())
Err(Error::Validation(format!(
"No ramdisk contains {:?}",
Self::OTACERTS_PATH.as_bstr(),
)))
}
}
@@ -506,7 +593,7 @@ impl BootImagePatcher for OtaCertPatcher {
pub struct PrepatchedImagePatcher {
prepatched: PathBuf,
fatal_level: u8,
warning_fn: Box<dyn Fn(&str) + Send>,
warning_fn: Box<dyn Fn(&str) + Send + Sync>,
}
impl PrepatchedImagePatcher {
@@ -514,12 +601,13 @@ impl PrepatchedImagePatcher {
const MAX_LEVEL: u8 = 2;
// We compile without Unicode support so we have to use [0-9] instead of \d.
const VERSION_REGEX: &str = r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
const VERSION_REGEX: &'static str =
r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
pub fn new(
prepatched: &Path,
fatal_level: u8,
warning_fn: impl Fn(&str) + Send + 'static,
warning_fn: impl Fn(&str) + Send + Sync + 'static,
) -> Self {
Self {
prepatched: prepatched.to_owned(),
@@ -528,6 +616,14 @@ impl PrepatchedImagePatcher {
}
}
fn load_prepatched_image(&self) -> Result<BootImage> {
let raw_reader =
File::open(&self.prepatched).map_err(|e| Error::File(self.prepatched.clone(), e))?;
let boot_image = BootImage::from_reader(BufReader::new(raw_reader))?;
Ok(boot_image)
}
fn get_kmi_version(kernel: &[u8]) -> Result<Option<String>> {
let mut decompressed = vec![];
{
@@ -556,13 +652,38 @@ impl PrepatchedImagePatcher {
}
}
impl BootImagePatcher for PrepatchedImagePatcher {
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &Arc<AtomicBool>) -> Result<()> {
let prepatched_image = {
let raw_reader = File::open(&self.prepatched)?;
BootImage::from_reader(BufReader::new(raw_reader))?
impl BootImagePatch for PrepatchedImagePatcher {
fn patcher_name(&self) -> &'static str {
"PrepatchedImagePatcher"
}
fn find_targets<'a>(
&self,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> Result<Vec<&'a str>> {
let prepatched_image = self.load_prepatched_image()?;
let has_kernel = match prepatched_image {
BootImage::V0Through2(b) => !b.kernel.is_empty(),
BootImage::V3Through4(b) => !b.kernel.is_empty(),
BootImage::VendorV3Through4(_) => false,
};
let mut targets = vec![];
if !has_kernel && boot_images.contains_key("init_boot") {
targets.push("init_boot");
} else if boot_images.contains_key("boot") {
targets.push("boot");
};
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
let prepatched_image = self.load_prepatched_image()?;
// Level 0: Warnings that don't affect booting
// Level 1: Warnings that may affect booting
// Level 2: Warnings that are very likely to affect booting
@@ -728,61 +849,121 @@ impl BootImagePatcher for PrepatchedImagePatcher {
}
}
/// Run each patcher against the boot image with the vbmeta footer stripped off
/// and then re-sign the image.
pub fn patch_boot(
mut reader: impl Read + Seek,
writer: impl Write + Seek,
key: &RsaPrivateKey,
patchers: &[Box<dyn BootImagePatcher + Send>],
cancel_signal: &Arc<AtomicBool>,
) -> Result<()> {
let (mut header, footer, image_size) = avb::load_image(&mut reader)?;
let Some(footer) = footer else {
return Err(Error::NoFooter);
};
pub fn load_boot_images<'a>(
names: &[&'a str],
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
) -> Result<HashMap<&'a str, BootImageInfo>> {
names
.par_iter()
.map(|name| {
let mut reader = open_input(name)?;
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
let mut boot_image = BootImage::from_reader(section_reader)?;
let (header, footer, image_size) = avb::load_image(&mut reader)?;
let Some(footer) = footer else {
return Err(Error::NoFooter);
};
for patcher in patchers {
patcher.patch(&mut boot_image, cancel_signal)?;
}
let section_reader = SectionReader::new(reader, 0, footer.original_image_size)?;
let boot_image = BootImage::from_reader(section_reader)?;
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
if let Descriptor::Hash(h) = d {
Some(h)
} else {
None
}
});
let info = BootImageInfo {
header,
footer,
image_size,
boot_image,
};
let Some(descriptor) = descriptor_iter.next() else {
return Err(Error::NoHashDescriptor);
};
// Write new boot image. We reuse the existing salt for the digest.
let mut context = Context::new(&ring::digest::SHA256);
context.update(&descriptor.salt);
let mut hashing_writer = HashingWriter::new(writer, context);
boot_image.to_writer(&mut hashing_writer)?;
let (mut writer, context) = hashing_writer.finish();
header.algorithm_type = AlgorithmType::Sha256Rsa4096;
descriptor.image_size = writer.stream_position()?;
descriptor.hash_algorithm = "sha256".to_owned();
descriptor.root_digest = context.finish().as_ref().to_vec();
if descriptor_iter.next().is_some() {
return Err(Error::MultipleHashDescriptors);
}
if !header.public_key.is_empty() {
header.sign(key)?;
}
avb::write_appended_image(writer, &header, &footer, image_size)?;
Ok(())
Ok((*name, info))
})
.collect()
}
/// Apply applicable patches to the list of specified boot images. For each
/// image, the applicable patchers run in the same order as in `patchers`. All
/// operations run in parallel where possible. Only the patcher execution for a
/// given image is guaranteed to be sequential. The input and output files will
/// be opened from multiple threads, but at most once each.
pub fn patch_boot_images<'a>(
names: &[&'a str],
open_input: impl Fn(&str) -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn(&str) -> io::Result<Box<dyn WriteSeek>> + Sync,
key: &RsaPrivateKey,
patchers: &[Box<dyn BootImagePatch + Sync>],
cancel_signal: &AtomicBool,
) -> Result<HashSet<&'a str>> {
// Preparse all images. Some patchers need to inspect every candidate.
let mut images = load_boot_images(names, open_input)?;
// Find the targets that each patcher wants to patch.
let all_targets = patchers
.par_iter()
.map(|p| {
p.find_targets(&images, cancel_signal).and_then(|targets| {
if targets.is_empty() {
Err(Error::NoTargets(p.patcher_name()))
} else {
Ok(targets)
}
})
})
.collect::<Result<Vec<_>>>()?;
// Regroup data so we can parallelize by target.
let mut groups = HashMap::<&str, (BootImageInfo, Vec<&Box<dyn BootImagePatch + Sync>>)>::new();
for (patcher, targets) in patchers.iter().zip(all_targets.into_iter()) {
for target in targets {
groups
.entry(target)
.or_insert_with(|| (images.remove(target).unwrap(), vec![]))
.1
.push(patcher);
}
}
// Deallocate all untouched images.
drop(images);
// Apply all patches.
groups
.par_iter_mut()
.try_for_each(|(_, (info, patchers))| -> Result<()> {
patchers
.iter()
.try_for_each(|p| p.patch(&mut info.boot_image, cancel_signal))
})?;
// Resign and write new images.
groups
.par_iter_mut()
.map(|(name, (info, _))| {
let AppendedDescriptorMut::Hash(descriptor) = info.header.appended_descriptor_mut()?
else {
return Err(Error::NoHashDescriptor);
};
let writer = open_output(name)?;
// Write new boot image. We reuse the existing salt for the digest.
let mut context = Context::new(&ring::digest::SHA256);
context.update(&descriptor.salt);
let mut hashing_writer = HashingWriter::new(writer, context);
info.boot_image.to_writer(&mut hashing_writer)?;
let (mut writer, context) = hashing_writer.finish();
descriptor.image_size = writer.stream_position()?;
descriptor.hash_algorithm = "sha256".to_owned();
descriptor.root_digest = context.finish().as_ref().to_vec();
if !info.header.public_key.is_empty() {
info.header.set_algo_for_key(key)?;
info.header.sign(key)?;
}
avb::write_appended_image(writer, &info.header, &mut info.footer, info.image_size)?;
Ok(())
})
.collect::<Result<()>>()?;
Ok(groups.keys().cloned().collect())
}
+8
View File
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
pub mod boot;
pub mod otacert;
pub mod system;
+145
View File
@@ -0,0 +1,145 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{borrow::Cow, cmp::Ordering, io::Cursor};
use bitflags::bitflags;
use thiserror::Error;
use x509_cert::{der::asn1::BitString, Certificate};
use zip::{result::ZipError, write::FileOptions, CompressionMethod, ZipWriter};
use crate::{crypto, format::ota};
#[derive(Debug, Error)]
pub enum Error {
#[error("New otacerts.zip is too small to pad to {0} bytes")]
ZipTooSmall(usize),
#[error("New otacerts.zip is too large to fit in {0} bytes")]
ZipTooLarge(usize),
#[error("Crypto error")]
Crypto(#[from] crypto::Error),
#[error("x509 DER error")]
Der(#[from] x509_cert::der::Error),
#[error("Zip error")]
Zip(#[from] ZipError),
}
type Result<T> = std::result::Result<T, Error>;
/// Pad a non-zip64 zip file to the specified size by adding null bytes to the
/// archive comment field.
pub fn pad_zip(data: &mut Vec<u8>, size: usize) -> Result<()> {
match size.cmp(&data.len()) {
Ordering::Equal => Ok(()),
Ordering::Less => Err(Error::ZipTooLarge(size)),
Ordering::Greater => {
let padding = size - data.len();
if data.len() < 22
|| &data[data.len() - 22..][..4] != ota::ZIP_EOCD_MAGIC
|| padding > usize::from(u16::MAX)
{
return Err(Error::ZipTooSmall(size));
}
// Rewrite the comment size and pad with null bytes.
data.pop();
data.pop();
data.extend((padding as u16).to_le_bytes());
data.resize(size, 0);
Ok(())
}
}
}
bitflags! {
/// Android uses X.509 as nothing more than a file format to transport RSA
/// public keys. This is true for both the framework's RecoverySystem and
/// recovery's otautil/verifier.cpp. The only fields that must exist are
/// the public key and the signature algorithm. The rest can be removed with
/// no side effects whatsoever.
#[derive(Debug, Clone, Copy)]
pub struct OtaCertBuildFlags: u8 {
const COMPRESS_DEFLATE = 1 << 0;
const REMOVE_SIGNATURE = 1 << 1;
const REMOVE_EXTENSIONS = 1 << 2;
const REMOVE_ISSUER = 1 << 3;
const REMOVE_SUBJECT = 1 << 4;
}
}
/// Create an `otacerts.zip` file containing the specified certificate.
pub fn create_zip(cert: &Certificate, flags: OtaCertBuildFlags) -> Result<Vec<u8>> {
let raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let compression_method = if flags.contains(OtaCertBuildFlags::COMPRESS_DEFLATE) {
CompressionMethod::Deflated
} else {
CompressionMethod::Stored
};
let options = FileOptions::default().compression_method(compression_method);
writer.start_file("ota.x509.pem", options)?;
let cert = if flags.is_empty() {
Cow::Borrowed(cert)
} else {
let mut modified = cert.clone();
if flags.contains(OtaCertBuildFlags::REMOVE_SIGNATURE) {
modified.signature = BitString::from_bytes(&[])?;
}
if flags.contains(OtaCertBuildFlags::REMOVE_EXTENSIONS) {
if let Some(extensions) = &mut modified.tbs_certificate.extensions {
extensions.clear();
}
}
if flags.contains(OtaCertBuildFlags::REMOVE_ISSUER) {
modified.tbs_certificate.issuer.0.clear();
modified.tbs_certificate.issuer_unique_id = None;
}
if flags.contains(OtaCertBuildFlags::REMOVE_SUBJECT) {
modified.tbs_certificate.subject.0.clear();
modified.tbs_certificate.subject_unique_id = None;
}
Cow::Owned(modified)
};
crypto::write_pem_cert(&mut writer, &cert)?;
let raw_writer = writer.finish()?;
Ok(raw_writer.into_inner())
}
/// Create an `otacerts.zip` file padded to the specified size.
///
/// This will incrementally remove unneeded components from the certificate to
/// meet the size limit if needed.
pub fn create_zip_with_size(cert: &Certificate, size: usize) -> Result<Vec<u8>> {
let mut flags = OtaCertBuildFlags::empty();
for additional_flag in [
OtaCertBuildFlags::empty(),
OtaCertBuildFlags::COMPRESS_DEFLATE,
OtaCertBuildFlags::REMOVE_SIGNATURE,
OtaCertBuildFlags::REMOVE_EXTENSIONS,
OtaCertBuildFlags::REMOVE_ISSUER,
OtaCertBuildFlags::REMOVE_SUBJECT,
] {
flags |= additional_flag;
let mut data = create_zip(cert, flags)?;
if data.len() <= size {
pad_zip(&mut data, size)?;
return Ok(data);
}
}
Err(Error::ZipTooLarge(size))
}
+216
View File
@@ -0,0 +1,216 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
io::{self, Cursor, SeekFrom},
ops::Range,
sync::atomic::AtomicBool,
};
use memchr::memmem;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rsa::RsaPrivateKey;
use thiserror::Error;
use x509_cert::Certificate;
use zip::ZipArchive;
use crate::{
format::{
avb::{self, AppendedDescriptorMut, Footer},
ota,
},
patch::otacert,
stream::{self, ReadSeekReopen, SectionReader, WriteSeekReopen},
util,
};
#[derive(Debug, Error)]
pub enum Error {
#[error("Old otacerts.zip not found in image")]
OldZipNotFound,
#[error("Image has no vbmeta footer")]
NoFooter,
#[error("No hash tree descriptor found in vbmeta header")]
NoHashTreeDescriptor,
#[error("{0:?} field is out of bounds")]
FieldOutOfBounds(&'static str),
#[error("AVB error")]
Avb(#[from] avb::Error),
#[error("OTA certificate error")]
OtaCert(#[from] otacert::Error),
#[error("I/O error")]
Io(#[from] io::Error),
}
type Result<T> = std::result::Result<T, Error>;
/// Find the bounds of a non-zip64 zip starting from the EOCD magic offset.
fn find_zip_bounds(data: &[u8], eocd_offset: usize) -> Option<Range<usize>> {
let eocd = &data[eocd_offset..];
if eocd.len() < 22 {
return None;
}
let cd_size = u32::from_le_bytes(eocd[12..16].try_into().unwrap()) as usize;
let cd_offset = u32::from_le_bytes(eocd[16..20].try_into().unwrap()) as usize;
let comment_size = usize::from(u16::from_le_bytes(eocd[20..22].try_into().unwrap()));
let start = eocd_offset.checked_sub(cd_size)?.checked_sub(cd_offset)?;
let end = eocd_offset.checked_add(22)?.checked_add(comment_size)?;
if end > data.len() {
return None;
}
let reader = SectionReader::new(Cursor::new(data), start as u64, (end - start) as u64).ok()?;
let mut zip_reader = ZipArchive::new(reader).ok()?;
if zip_reader.is_empty() {
// otacerts.zip files contain at least one cert.
return None;
}
for index in 0..zip_reader.len() {
let entry = zip_reader.by_index_raw(index).ok()?;
if !entry.name().ends_with(".x509.pem") {
// otacerts.zip files only contain files named this way.
return None;
}
}
// There's one or more entries and every one is named *.x509.pem.
Some(start..end)
}
/// Replace `otacerts.zip` with a new one containing the new certificate, but
/// padded to the same size. If the new zip is too large, the certificate will
/// be modified to remove unnecessary components until it fits. All operations
/// run in parallel where possible. The input and output must refer to the same
/// file and will be reopened from multiple threads.
///
/// Returns two sorted and non-overlapping lists of byte ranges that were
/// modified. The first list are the byte regions within the filesystem data
/// that contained otacerts.zip. The second list is the list of byte regions
/// outside of the filesyste, like the hash tree, FEC data, and AVB metadata.
///
/// If [`Error::OldZipNotFound`] is returned, the output will not have been
/// modified.
#[allow(clippy::type_complexity)]
pub fn patch_system_image(
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
certificate: &Certificate,
key: &RsaPrivateKey,
cancel_signal: &AtomicBool,
) -> Result<(Vec<Range<u64>>, Vec<Range<u64>>)> {
// This must be a multiple of normal filesystem block sizes (eg. 4 KiB).
// This ensures that the block containing otacerts.zip's data won't cross
// chunk boundaries.
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
let (mut header, footer, image_size) = avb::load_image(input.reopen_boxed()?)?;
let Some(mut footer) = footer else {
return Err(Error::NoFooter);
};
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
};
let num_chunks = util::div_ceil(footer.original_image_size, CHUNK_SIZE);
let modified_ranges = (0..num_chunks)
.into_par_iter()
.map(|chunk| -> Result<Vec<Range<u64>>> {
stream::check_cancel(cancel_signal)?;
let offset = chunk * CHUNK_SIZE;
let size = CHUNK_SIZE.min(footer.original_image_size - offset);
let mut buf = vec![0u8; size as usize];
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let mut writer = output.reopen_boxed()?;
let mut ranges = Vec::<Range<u64>>::new();
for eocd_offset_rel in memmem::find_iter(&buf, ota::ZIP_EOCD_MAGIC) {
let Some(bounds_rel) = find_zip_bounds(&buf, eocd_offset_rel) else {
continue;
};
let zip_size = bounds_rel.end - bounds_rel.start;
let new_zip = otacert::create_zip_with_size(certificate, zip_size)?;
let bounds = offset + bounds_rel.start as u64..offset + bounds_rel.end as u64;
stream::check_cancel(cancel_signal)?;
writer.seek(SeekFrom::Start(bounds.start))?;
writer.write_all(&new_zip)?;
ranges.push(bounds);
}
Ok(ranges)
})
.try_reduce(Vec::new, |mut result, item| {
result.extend(item);
Ok(result)
})?;
if modified_ranges.is_empty() {
return Err(Error::OldZipNotFound);
}
let update_ranges = if descriptor.hash_algorithm == "sha1" {
// Promote to a secure algorithm. SHA1 is allowed for verification only.
// The entire hash tree and FEC data will need to be recomputed.
descriptor.hash_algorithm = "sha256".to_owned();
None
} else {
// Only need to update the hash tree and FEC data corresponding to the
// modified regions.
Some(modified_ranges.as_slice())
};
descriptor.update(input, output, update_ranges, cancel_signal)?;
if !header.public_key.is_empty() {
header.set_algo_for_key(key)?;
header.sign(key)?;
}
let writer = output.reopen_boxed()?;
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
return Err(Error::NoHashTreeDescriptor);
};
// The hash tree, FEC data, and AVB regions will have been modified.
let hash_tree_end = descriptor
.tree_offset
.checked_add(descriptor.tree_size)
.ok_or_else(|| Error::FieldOutOfBounds("hash_tree_end"))?;
let fec_data_end = descriptor
.fec_offset
.checked_add(descriptor.fec_size)
.ok_or_else(|| Error::FieldOutOfBounds("fec_data_end"))?;
let header_end = footer
.vbmeta_offset
.checked_add(footer.vbmeta_size)
.ok_or_else(|| Error::FieldOutOfBounds("avb_end"))?;
let footer_start = image_size - Footer::SIZE as u64;
let other_ranges = util::merge_overlapping(&[
descriptor.tree_offset..hash_tree_end,
descriptor.fec_offset..fec_data_end,
footer.vbmeta_offset..header_end,
footer_start..image_size,
]);
Ok((modified_ranges, other_ranges))
}
+11 -1
View File
@@ -1 +1,11 @@
include!(concat!(env!("OUT_DIR"), "/protobuf/mod.rs"));
pub mod build {
pub mod tools {
pub mod releasetools {
include!(concat!(env!("OUT_DIR"), "/build.tools.releasetools.rs"));
}
}
}
pub mod chromeos_update_engine {
include!(concat!(env!("OUT_DIR"), "/chromeos_update_engine.rs"));
}
+109 -38
View File
@@ -5,17 +5,18 @@
use std::{
fs::File,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, RwLock,
},
};
use bstr::ByteSlice;
use num_traits::ToPrimitive;
use ring::digest::Context;
use crate::util::{self, EscapedString};
use crate::util;
/// A trait for seekable readers. This is only needed because `dyn Read + Seek`
/// is not a valid construct in Rust yet.
@@ -29,6 +30,28 @@ pub trait WriteSeek: Write + Seek {}
impl<W: Write + Seek> WriteSeek for W {}
/// A trait for seekable and reopenable readers.
pub trait ReadSeekReopen: ReadSeek {
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>>;
}
impl<R: ReadSeek + Reopen + 'static> ReadSeekReopen for R {
fn reopen_boxed(&self) -> io::Result<Box<dyn ReadSeek>> {
Ok(Box::new(self.reopen()?))
}
}
/// A trait for seekable and reopenable writers.
pub trait WriteSeekReopen: WriteSeek {
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>>;
}
impl<W: WriteSeek + Reopen + 'static> WriteSeekReopen for W {
fn reopen_boxed(&self) -> io::Result<Box<dyn WriteSeek>> {
Ok(Box::new(self.reopen()?))
}
}
/// Common function for reading a structure from a reader.
pub trait FromReader<R: Read>: Sized {
type Error;
@@ -118,7 +141,7 @@ impl<R: Read> ReadStringExt for R {
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
}
@@ -138,7 +161,7 @@ impl<R: Read> ReadStringExt for R {
String::from_utf8(buf).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid UTF-8: {}: {e}", EscapedString::new(e.as_bytes())),
format!("Invalid UTF-8: {:?}: {e}", e.as_bytes().as_bstr()),
)
})
}
@@ -167,6 +190,25 @@ impl<W: Write> WriteStringExt for W {
}
}
/// Extensions for file-like types to reopen themselves.
pub trait Reopen: Sized {
/// Open a new handle to the same file. The new handle is independently
/// seekable and the file offset is initially set to 0.
fn reopen(&self) -> io::Result<Self>;
}
impl<R: Read + Reopen> Reopen for BufReader<R> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufReader::new(self.get_ref().reopen()?))
}
}
impl<W: Write + Reopen> Reopen for BufWriter<W> {
fn reopen(&self) -> io::Result<Self> {
Ok(BufWriter::new(self.get_ref().reopen()?))
}
}
/// A reader wrapper that implements [`Seek`], but only for reporting the
/// current file position.
pub struct CountingReader<R: Read> {
@@ -324,6 +366,14 @@ impl<R: Read + Seek> SectionReader<R> {
}
}
impl<R: Read + Seek + Reopen> Reopen for SectionReader<R> {
fn reopen(&self) -> io::Result<Self> {
let inner = self.inner.reopen()?;
Self::new(inner, self.start, self.size)
}
}
impl<R: Read + Seek> Read for SectionReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let to_read = self.size.saturating_sub(self.pos).min(buf.len() as u64) as usize;
@@ -398,9 +448,10 @@ impl<W: Write + Seek> Write for HolePunchingWriter<W> {
}
}
/// A file wrapper that uses a userspace file offset. A cloned instances uses
/// the same underlying kernel file descriptor, but a new userspace file offset.
#[derive(Clone)]
/// A file wrapper that uses a userspace file offset. A reopened instance uses
/// the same underlying kernel file descriptor, but a new userspace file offset,
/// initially set to 0.
#[derive(Debug)]
pub struct PSeekFile {
// The lock is needed because flush() takes a `&mut self`.
file: Arc<RwLock<File>>,
@@ -449,6 +500,15 @@ impl PSeekFile {
}
}
impl Reopen for PSeekFile {
fn reopen(&self) -> io::Result<Self> {
Ok(Self {
file: self.file.clone(),
offset: 0,
})
}
}
impl Read for PSeekFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let n = self.read_at(buf)?;
@@ -509,17 +569,26 @@ impl Seek for PSeekFile {
/// readers into different parts of the same [`SharedCursor`] writer and the
/// read operation is significantly more expensive than the write operation (eg.
/// due to decompression).
#[derive(Clone, Default)]
#[derive(Default)]
pub struct SharedCursor {
inner: Arc<Mutex<Cursor<Vec<u8>>>>,
offset: u64,
}
impl SharedCursor {
pub fn clone_rewind(&self) -> Self {
let mut new = self.clone();
new.offset = 0;
new
pub fn new() -> Self {
Self {
..Default::default()
}
}
}
impl Reopen for SharedCursor {
fn reopen(&self) -> io::Result<Self> {
Ok(Self {
inner: self.inner.clone(),
offset: 0,
})
}
}
@@ -560,6 +629,21 @@ impl Seek for SharedCursor {
}
}
/// Returns an I/O error with the [`io::ErrorKind::Interrupted`] type if
/// `cancel_signal` is true. This should be called frequently in I/O loops for
/// cancellation to be responsive.
#[inline]
pub fn check_cancel(cancel_signal: &AtomicBool) -> io::Result<()> {
if cancel_signal.load(Ordering::SeqCst) {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"Received cancel signal",
));
}
Ok(())
}
/// Copy exactly `size` bytes from `reader` to `writer`, invoking `inspect`
/// after every buffer read iteration. If either `reader` or `writer` reaches
/// EOF before `size` bytes are copied, an error is returned. The operation is
@@ -569,17 +653,12 @@ pub fn copy_n_inspect(
mut writer: impl Write,
mut size: u64,
mut inspect: impl FnMut(&[u8]),
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> io::Result<()> {
let mut buf = [0u8; 16384];
while size > 0 {
if cancel_signal.load(Ordering::SeqCst) {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"Received cancel signal",
));
}
check_cancel(cancel_signal)?;
let to_read = size.min(buf.len() as u64) as usize;
reader.read_exact(&mut buf[..to_read])?;
@@ -599,7 +678,7 @@ pub fn copy_n(
reader: impl Read,
writer: impl Write,
size: u64,
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> io::Result<()> {
copy_n_inspect(reader, writer, size, |_| {}, cancel_signal)
}
@@ -610,18 +689,13 @@ pub fn copy_n(
pub fn copy(
mut reader: impl Read,
mut writer: impl Write,
cancel_signal: &Arc<AtomicBool>,
cancel_signal: &AtomicBool,
) -> io::Result<u64> {
let mut buf = [0u8; 16384];
let mut copied = 0;
loop {
if cancel_signal.load(Ordering::SeqCst) {
return Err(io::Error::new(
io::ErrorKind::Interrupted,
"Received cancel signal",
));
}
check_cancel(cancel_signal)?;
let n = reader.read(&mut buf)?;
if n == 0 {
@@ -640,18 +714,15 @@ pub fn copy(
mod tests {
use std::{
io::{self, Cursor, Read, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
sync::atomic::{AtomicBool, Ordering},
};
use ring::digest::Context;
use super::{
CountingReader, CountingWriter, HashingReader, HashingWriter, HolePunchingWriter,
PSeekFile, ReadDiscardExt, ReadStringExt, SectionReader, SharedCursor, WriteStringExt,
WriteZerosExt,
PSeekFile, ReadDiscardExt, ReadStringExt, Reopen, SectionReader, SharedCursor,
WriteStringExt, WriteZerosExt,
};
const FOOBAR_SHA256: [u8; 32] = [
@@ -830,8 +901,8 @@ mod tests {
fn pseek_file() {
let raw_file = tempfile::tempfile().unwrap();
let mut a = PSeekFile::new(raw_file);
let mut b = a.clone();
let mut c = b.clone();
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
@@ -850,8 +921,8 @@ mod tests {
#[test]
fn shared_cursor() {
let mut a = SharedCursor::default();
let mut b = a.clone();
let mut c = b.clone();
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
@@ -869,7 +940,7 @@ mod tests {
#[test]
fn copy() {
let cancel_signal = Arc::new(AtomicBool::new(false));
let cancel_signal = AtomicBool::new(false);
let mut reader = Cursor::new(b"foobar");
let mut writer = Cursor::new([0u8; 6]);
+116 -81
View File
@@ -3,19 +3,19 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::fmt;
use std::{cmp::Ordering, fmt, ops::Range, path::Path};
use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer};
use num_traits::PrimInt;
pub const ZEROS: [u8; 16384] = [0u8; 16384];
/// A small wrapper to format a number as a size in bytes.
#[derive(Clone, Copy)]
pub struct NumBytes(pub usize);
pub struct NumBytes<T: PrimInt>(pub T);
impl fmt::Debug for NumBytes {
impl<T: PrimInt + fmt::Debug> fmt::Debug for NumBytes<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 == 1 {
if self.0 == T::one() {
write!(f, "<{:?} byte>", self.0)
} else {
write!(f, "<{:?} bytes>", self.0)
@@ -23,72 +23,6 @@ impl fmt::Debug for NumBytes {
}
}
/// A wrapper around a byte slice to format it as ASCII with invalid bytes
/// escaped as `\x##`.
#[derive(Clone)]
pub struct EscapedString<T: AsRef<[u8]>> {
inner: T,
quoted: bool,
}
impl<T: AsRef<[u8]>> EscapedString<T> {
pub fn new(inner: T) -> Self {
Self {
inner,
quoted: true,
}
}
pub fn new_unquoted(inner: T) -> Self {
Self {
inner,
quoted: false,
}
}
pub fn into_inner(self) -> T {
self.inner
}
pub fn get_ref(&self) -> &T {
&self.inner
}
pub fn get_mut(&mut self) -> &mut T {
&mut self.inner
}
}
impl<T: AsRef<[u8]>> fmt::Debug for EscapedString<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let escaped: String = self
.inner
.as_ref()
.iter()
.flat_map(|b| b.escape_ascii())
.map(char::from)
.collect();
if self.quoted {
write!(f, "\"")?;
}
write!(f, "{escaped}")?;
if self.quoted {
write!(f, "\"")?;
}
Ok(())
}
}
impl<T: AsRef<[u8]>> fmt::Display for EscapedString<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self, f)
}
}
/// Check if a byte slice is all zeros.
pub fn is_zero(mut buf: &[u8]) -> bool {
while !buf.is_empty() {
@@ -103,16 +37,117 @@ pub fn is_zero(mut buf: &[u8]) -> bool {
true
}
/// Read a protobuf message with no leading size field.
pub fn read_protobuf<'a, M: MessageRead<'a>>(data: &'a [u8]) -> quick_protobuf::Result<M> {
let mut reader = BytesReader::from_bytes(data);
M::from_reader(&mut reader, data)
/// Get the non-empty parent of a path. If the path has no parent in the string,
/// then `.` is returned. This does not perform any filesystem operations.
pub fn parent_path(path: &Path) -> &Path {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
return parent;
}
}
Path::new(".")
}
/// Write a protobuf message with no leading size field.
pub fn write_protobuf<M: MessageWrite>(message: &M) -> quick_protobuf::Result<Vec<u8>> {
let mut buf = Vec::with_capacity(message.get_size());
let mut writer = Writer::new(&mut buf);
message.write_message(&mut writer)?;
Ok(buf)
/// Since Rust's built-in .div_ceil() is still nightly-only.
pub fn div_ceil<T: PrimInt>(dividend: T, divisor: T) -> T {
dividend / divisor
+ if dividend % divisor != T::zero() {
T::one()
} else {
T::zero()
}
}
/// Sort and merge overlapping intervals.
pub fn merge_overlapping<T>(sections: &[Range<T>]) -> Vec<Range<T>>
where
T: Ord + Clone + Copy,
{
let mut sections = sections.to_vec();
sections.sort_by_key(|r| (r.start, r.end));
let mut result = Vec::<Range<T>>::new();
for section in sections {
if section.start >= section.end {
continue;
} else if let Some(last) = result.last_mut() {
if section.start <= last.end {
last.end = last.end.max(section.end);
continue;
}
}
result.push(section);
}
result
}
/// Binary search to determine if the needle overlaps any of the ranges.
pub fn ranges_overlaps<T>(ranges: &[Range<T>], needle: &Range<T>) -> bool
where
T: Ord,
{
if needle.start < needle.end {
ranges
.binary_search_by(|range| {
if range.start > needle.end {
Ordering::Greater
} else if range.end <= needle.start {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
} else {
false
}
}
/// Binary search to determine if any of the ranges contain the needle.
pub fn ranges_contains<T>(ranges: &[Range<T>], needle: &T) -> bool
where
T: Ord,
{
ranges
.binary_search_by(|range| {
if range.start > *needle {
Ordering::Greater
} else if range.end <= *needle {
Ordering::Less
} else {
Ordering::Equal
}
})
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ranges_overlaps() {
assert_eq!(ranges_overlaps(&[0..4], &(0..0)), false);
assert_eq!(ranges_overlaps(&[0..4], &(0..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(1..4)), true);
assert_eq!(ranges_overlaps(&[0..4], &(0..3)), true);
assert_eq!(ranges_overlaps(&[0..4], &(4..5)), false);
assert_eq!(ranges_overlaps(&[5..8], &(5..9)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..8)), true);
assert_eq!(ranges_overlaps(&[5..8], &(4..9)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(4..5)), true);
assert_eq!(ranges_overlaps(&[0..4, 5..8], &(0..9)), true);
}
#[test]
fn test_ranges_contains() {
assert_eq!(ranges_contains(&[0..4], &0), true);
assert_eq!(ranges_contains(&[0..4], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &4), false);
assert_eq!(ranges_contains(&[0..4, 5..8], &6), true);
}
}
+118 -16
View File
@@ -3,13 +3,20 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::io::{self, Cursor, Read, Seek, SeekFrom};
use std::{
io::{Cursor, Read, Seek, SeekFrom, Write},
sync::atomic::AtomicBool,
};
use assert_matches::assert_matches;
use pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
use avbroot::{self, format::avb};
use avbroot::{
self,
format::avb::{self, AppendedDescriptorMut, AppendedDescriptorRef},
stream::{self, SharedCursor},
};
fn get_test_key() -> RsaPrivateKey {
let data = include_str!(concat!(
@@ -35,11 +42,14 @@ fn round_trip_root_image() {
let (mut header, footer, _) = avb::load_image(reader).unwrap();
assert_matches!(footer, None);
let key = get_test_key();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
let key = get_test_key();
header.sign(&key).unwrap();
let mut writer = Cursor::new(Vec::new());
@@ -50,33 +60,125 @@ fn round_trip_root_image() {
}
#[test]
fn round_trip_appended_image() {
fn round_trip_appended_hash_image() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vbmeta_appended.img",
"/tests/data/vbmeta_appended_hash.img",
));
let mut reader = Cursor::new(data);
let cancel_signal = AtomicBool::new(false);
let (mut header, footer, _) = avb::load_image(&mut reader).unwrap();
let footer = footer.unwrap();
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
let mut footer = footer.unwrap();
let key = get_test_key();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Verify the digest.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(_) => panic!("Expected hash descriptor"),
AppendedDescriptorRef::Hash(d) => {
reader.rewind().unwrap();
d.verify(&mut reader, &cancel_signal).unwrap();
}
}
let mut writer = Cursor::new(Vec::new());
// Copy the partition data.
reader.seek(SeekFrom::Start(0)).unwrap();
stream::copy_n(
&mut reader,
&mut writer,
footer.original_image_size,
&cancel_signal,
)
.unwrap();
// Regenerate the digest.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(_) => panic!("Expected hash descriptor"),
AppendedDescriptorMut::Hash(d) => {
d.root_digest.clear();
writer.rewind().unwrap();
d.update(&mut writer, &cancel_signal).unwrap();
}
}
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
let key = get_test_key();
header.sign(&key).unwrap();
let mut writer = Cursor::new(Vec::new());
// Copy the partition data.
let image_size = reader.seek(SeekFrom::End(0)).unwrap();
reader.seek(SeekFrom::Start(0)).unwrap();
io::copy(&mut reader.take(footer.original_image_size), &mut writer).unwrap();
// Write new vbmeta structures.
avb::write_appended_image(&mut writer, &header, &footer, image_size).unwrap();
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let new_data = writer.into_inner();
assert_eq!(data, new_data.as_slice());
}
#[test]
fn round_trip_appended_hash_tree_image() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/vbmeta_appended_hash_tree.img",
));
let mut reader = SharedCursor::default();
reader.write_all(data).unwrap();
let cancel_signal = AtomicBool::new(false);
let (mut header, footer, image_size) = avb::load_image(&mut reader).unwrap();
let mut footer = footer.unwrap();
let key = get_test_key();
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
// Verify the hash tree and FEC data.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(d) => d.verify(&reader, &cancel_signal).unwrap(),
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
}
let mut writer = SharedCursor::default();
// Copy the partition data, excluding the hash tree and FEC data.
reader.seek(SeekFrom::Start(0)).unwrap();
stream::copy_n(
&mut reader,
&mut writer,
footer.original_image_size,
&cancel_signal,
)
.unwrap();
// Regenerate the hash tree and FEC data.
match header.appended_descriptor_mut().unwrap() {
AppendedDescriptorMut::HashTree(d) => {
d.root_digest.clear();
d.tree_offset = 0;
d.tree_size = 0;
d.fec_offset = 0;
d.fec_size = 0;
d.update(&writer, &writer, None, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
// Clear out the signature-related fields and re-sign.
header.hash.clear();
header.signature.clear();
header.public_key.clear();
header.sign(&key).unwrap();
// Write new vbmeta structures.
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
let mut new_data = Vec::new();
writer.rewind().unwrap();
writer.read_to_end(&mut new_data).unwrap();
assert_eq!(data, new_data.as_slice());
}
+50
View File
@@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::io::{self, Cursor};
use avbroot::{
self,
format::cpio::{CpioEntryType, CpioReader, CpioWriter},
util,
};
#[test]
fn round_trip_archive() {
let data = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/data/archive.cpio",
));
assert_ne!(data.len() % 512, 0);
for pad_to_block_size in [false, true] {
println!("Pad to block size: {pad_to_block_size}");
let reader = Cursor::new(data);
let mut cpio_reader = CpioReader::new(reader, false);
let writer = Cursor::new(Vec::new());
let mut cpio_writer = CpioWriter::new(writer, pad_to_block_size);
while let Some(entry) = cpio_reader.next_entry().unwrap() {
cpio_writer.start_entry(&entry).unwrap();
if entry.file_type == CpioEntryType::Regular {
io::copy(&mut cpio_reader, &mut cpio_writer).unwrap();
}
}
let writer = cpio_writer.finish().unwrap();
let new_data = writer.get_ref().as_slice();
if pad_to_block_size {
assert!(new_data.starts_with(data));
assert!(util::is_zero(&new_data[data.len()..]));
assert_eq!(new_data.len() % 512, 0);
} else {
assert_eq!(new_data, data);
}
}
}
Binary file not shown.
File diff suppressed because one or more lines are too long
+37
View File
@@ -3,11 +3,37 @@ vulnerability = "deny"
unmaintained = "deny"
yanked = "deny"
notice = "deny"
ignore = [
# https://rustsec.org/advisories/RUSTSEC-2023-0071
#
# This is a side-channel vulnerability where secrets can be leaked to an
# attacker that is able to measure the timing of a large number of RSA
# operations. As of 2023-12-03, there is no released version of the rsa
# crate that contains a fix.
#
# For avbroot specifically, this vulnerability is not too critical for a
# couple reasons:
#
# 1. avbroot performs RSA signing only at the end of lengthy processes
# that involve a lot of disk I/O. It's very expensive to run avbroot
# the millions of times needed to capture a sufficient amount of timing
# data.
# 2. During a single run of avbroot, it will only perform RSA signing a
# handful of times. To get sufficient measurements, the attacker would
# need to rerun avbroot. If they are able to rerun avbroot, then they
# are also able to just read and steal the private key directly.
#
# avbroot has no network capabilities, so this is not inherently remotely
# exploitable.
"RUSTSEC-2023-0071",
]
[licenses]
include-dev = true
unlicensed = "deny"
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-3-Clause",
"ISC",
"MIT",
@@ -26,15 +52,26 @@ license-files = [
[bans]
multiple-versions = "warn"
multiple-versions-include-dev = true
deny = [
# https://github.com/serde-rs/serde/issues/2538
{ name = "serde_derive", version = ">=1.0.172,<1.0.184" },
]
[bans.build]
executables = "deny"
include-dependencies = true
include-workspace = true
bypass = [
# Copies of unmodified crashwrangler objects for old macOS versions.
{ name = "honggfuzz", allow-globs = ["honggfuzz/third_party/mac/CrashReport_*.o"] },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-git = [
"https://github.com/chenxiaolong/xz2-rs",
"https://github.com/chenxiaolong/zip",
"https://github.com/jongiddy/bzip2-rs",
]
-1
View File
@@ -1 +0,0 @@
/files/
+8 -5
View File
@@ -14,16 +14,19 @@ avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
reqwest = { version = "0.11.20", features = ["stream"] }
ring = "0.16.20"
ring = "0.17.0"
rsa = "0.9.6"
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
tokio = { version = "1.32.0", features = ["signal", "rt-multi-thread", "macros"] }
tokio-stream = "0.1.14"
toml_edit = { version = "0.19.14", features = ["serde"] }
toml_edit = { version = "0.21.0", features = ["serde"] }
topological-sort = "0.2.2"
x509-cert = "0.2.5"
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
git = "https://github.com/chenxiaolong/zip"
rev = "989101f9384b9e94e36e6e9e0f51908fdf98bde6"
default-features = false
[features]
static = ["avbroot/static"]
+12 -50
View File
@@ -1,64 +1,26 @@
# End-to-end tests
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
avbroot's output file is reproducible for a given input file. [`e2e.toml`](./e2e.toml) lists some profiles for generating mock OTA images with unique properties and the expected checksums before and after patching. These tests use pregenerated, hardcoded test keys for signing. **These keys should NEVER be used for any other purpose.**
For each image listed in the config, the test process will:
For each profile listed in the config, the test process will:
1. Download the OTA if it doesn't already exist in `./files/<device>/` (or the workdir specified by `-w`)
2. Verify the OTA checksum
3. Run avbroot against the OTA using `--magisk`
4. Extract the AVB-related partitions from the patched OTA and verify their checksums
5. Verify the patched OTA checksum
1. Generate a mock OTA based on the specification
2. Verify tha original OTA checksum
3. Run avbroot against the OTA using `--magisk` (with a mock Magisk APK)
4. Verify the patched OTA checksum
5. Extract the AVB-related partitions from the patched OTA
6. Run avbroot against the OTA again using `--prepatched`
7. Verify the patched OTA checksum again
For more efficient CI testing, the tests can operate on "stripped" OTAs. A stripped OTA is identical to the full OTA, except that partitions in `payload.bin` unrelated to AVB are zeroed out. This reduces the download size and disk space requirements by a couple orders of magnitude. **A stripped OTA is NOT bootable and should never be flashed on a real device.**
The default profiles shipped with the project mimic how various stock OTAs for Pixel devices are built. The generated mock OTAs have valid signatures and data structures for all components, but without any actual data where possible. For example, most files in the ramdisks are empty files. To ensure the mock OTAs cannot be mistakenly installed on a real device, the OTA metadata lists a fake device name in the preconditions section.
## Running the tests
To test against the device OTA images listed in [`e2e.toml`](./e2e.toml), run:
To test against the profiles listed in [`e2e.toml`](./e2e.toml), run:
```bash
# To test all device OTAs
# To test all profiles
cargo run --release -- test -a
# Or to test against specific device OTAs
cargo run --release -- test -d cheetah -d bluejay
# Or to test against specific profiles
cargo run --release -- test -p pixel_v4_gki -p pixel_v4_non_gki
```
To test against stripped OTAs (smaller download, but not bootable), pass in `--stripped`.
## Downloading a device image
To download a full OTA image, run:
```bash
cargo run --release -- download -d <device>
```
This normally happens automatically when running the `test` subcommand. To download the stripped OTA image instead, pass in `--stripped`.
If the image file does not already exist, then it will be downloaded and the checksums will be validated. If the download is interrupted, it will automatically resume when the command is rerun. If the file is already downloaded, the command is effectively a no-op unless `--revalidate` is passed in to revalidate the image checksums.
## Adding a new device image
To add a new device image to the testing configuration, run:
```bash
cargo run --release -- add -d <device> -u <full OTA URL> -H <expected checksum>
```
If the OS vendor does not provide a SHA-256 checksum, omit `-H` and the program will compute the checksum from the downloaded data.
This process will download the full OTA, strip it, patch the full OTA, patch the stripped OTA, extract the AVB partitions, and write all of the checksums to [`e2e.toml`](./e2e.toml).
The process for updating an existing device config is exactly the same as adding a new one.
## Stripping a full OTA
To convert a full OTA to the stripped form, run:
```bash
cargo run --release -- strip -i <input zip> -o <output zip>
```
This normally happens automatically as a part of adding a new device image.
+135 -103
View File
@@ -1,116 +1,148 @@
[magisk]
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.0/Magisk-v26.0.apk"
"hash" = "9e14d3d3ca1f1a2765f8ca215ebbf35ea5fd2896fb147eea581fcaa3b4e77d25"
# Metadata used when generating OTAs. These values don't affect behavior at all.
[ota_info]
# Make sure generated OTAs aren't flashable on real devices.
device = "avbroot_fake_device"
fingerprint = "avbroot/avbroot_fake_device:14/UQ1A.240101.000/12345678:user/release-keys"
build_number = "UQ1A.240101.000"
incremental_version = "12345678"
android_version = "14"
sdk_version = "34"
security_patch_level = "2024-01-01"
# Google Pixel 7 Pro
# What's unique: init_boot (boot v4) + vendor_boot (vendor v4)
[device.cheetah]
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq2a.230305.008.c1-6ac5ff2e.zip"
sections = [
{ start = 0, end = 151715 },
{ start = 21683150, end = 23179365 },
{ start = 2043499985, end = 2043508325 },
{ start = 2315331822, end = 2333060126 },
{ start = 2344084950, end = 2344090066 },
]
hash.original.full = "6ac5ff2e14dc16755ea4ea30e6dbe25103b889a36a465194ef943bd0d665b91c"
hash.original.stripped = "549522015f0369a3b89385f532ab62235b47c2c39540bd0adaaf6acc81fdda94"
hash.patched.full = "b380720852e2a1e994bcf38064f577bac68b18e799cf8166cb6a7edb8a661cb4"
hash.patched.stripped = "560cdf4d7b25fb5bc650f96ae5a3c7593ac229a364bb39887287cfb734f6b377"
hash.avb_images."init_boot.img" = "fac9305ce22b897fbfb193968d5346f4c70f6c18c3060bb106226f134ce5f433"
hash.avb_images."vbmeta.img" = "0b3d719b751dd43bbec95d02b1bf57b5dae62e42a52502895892a207b773e77d"
hash.avb_images."vbmeta_system.img" = "cf8c77dcf0a4474d49b5bdc2a44bdb3646464d5212fbe12aa5d3c5f531742f4f"
hash.avb_images."vbmeta_vendor.img" = "660d8f61acd95a4f8ad416b4cbe126e9c039706462b4236ad723953c72ac49a8"
hash.avb_images."vendor_boot.img" = "c788d4d8eb7926ad1bfa2a9c000343c3c73cedc580449f5270a711feb620f033"
[profile.pixel_v4_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_gki.partitions.init_boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.ramdisks = ["init"]
[profile.pixel_v4_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "init_boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = ["otacerts"]
[profile.pixel_v4_gki.hashes]
original = "f9477a35e3b60a495e49431c61e3897f11775f453a6a9897ead568357c963618"
patched = "eb045799a514300727357a5ec471f9b04b276991daf4fd72f17f840b2a7dd1b8"
# Google Pixel 6a
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
[device.bluejay]
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq2a.230305.008.e1-915f9087.zip"
sections = [
{ start = 0, end = 140787 },
{ start = 1060207, end = 21612852 },
{ start = 1886150700, end = 1886158844 },
{ start = 2069112987, end = 2092260102 },
{ start = 2098778558, end = 2098783674 },
]
hash.original.full = "915f9087b627b6961be9bb447dc63a7a1083b536753a78715e98641eaeb9c9d1"
hash.original.stripped = "a3ee5b6e39e687665c31790118ab9f47715b0b8285ae9847dbf81307f963db14"
hash.patched.full = "bfa7b26d90bdc889a7a199439e1564b219e5e2dbfddc1657bc1c6b73229be67e"
hash.patched.stripped = "4e56ee4a8554f2b08ffc2f1470ad60b9b63d2b6fb469ed1f7c4b1204bbf8ad7d"
hash.avb_images."boot.img" = "a1a705092e7034d20b83c94d78291418e13c343b1573c1b11e0fc884fc00ae62"
hash.avb_images."vbmeta.img" = "3c123705be57ab142d2b43beef9b123eaad129df17a523b59b1d63b9122d28b0"
hash.avb_images."vbmeta_system.img" = "285b83e4290f3257dc3678f0c3191794830bb2d72fb0969b69fc8f09d7ddff12"
hash.avb_images."vbmeta_vendor.img" = "981f736586b91a9f4c93c4208a0d191a35ff15118c6fa505d755ee7fda8b2477"
hash.avb_images."vendor_boot.img" = "ceffcb4fdb33aa3bb2c70621060acd5af612206b21cc413d5c2d39fee25144ab"
[profile.pixel_v4_non_gki.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v4"
data.kernel = true
[profile.pixel_v4_non_gki.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v4_non_gki.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v4_non_gki.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v4_non_gki.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v4"
data.ramdisks = ["init_and_otacerts", "dlkm"]
[profile.pixel_v4_non_gki.hashes]
original = "021b4510bc244f5f686fbff89eb2058ec9c96a2949c2fe8caa7750a78d593225"
patched = "5d1a36e2eb18d9d905ab397d4eaf2d7e7c94da6e7573afe3d4e4367841171fe3"
# Google Pixel 4a 5G
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
[device.bramble]
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq2a.230305.008.c1-a925dd09.zip"
sections = [
{ start = 0, end = 140531 },
{ start = 496187, end = 11655082 },
{ start = 1650283561, end = 1650287993 },
{ start = 1884739919, end = 1908081011 },
{ start = 1910894027, end = 1910899144 },
]
hash.original.full = "a925dd09c8d613d46cf72677c16f4fadee18bc21734d57047c6ccf31f672507b"
hash.original.stripped = "79322b0b417359e8f072032de676d7e5bd2715a3b3554c48ed5cc9e9a25c6866"
hash.patched.full = "cf79cd60acd3635f5085d8bb411d4a8dc4d7e62440f62a14e7ea12f5a9b7cd8a"
hash.patched.stripped = "b684a78fe08014f1633e74d1f380e137823ba77a47ddf428bc649364f5548b75"
hash.avb_images."boot.img" = "2bbf2c6d2f82d454426b26ac3b4887b26ca0591b458e6cf137207ecbb8f7649d"
hash.avb_images."vbmeta.img" = "ab3b2487671b3fc28e163898621d6c068164d335b6ddd9b94e4fde9471c95d66"
hash.avb_images."vbmeta_system.img" = "2fcd52d7462916a8510bbb07f2f5a14200afe2de97568396fe75e04c5c283152"
hash.avb_images."vendor_boot.img" = "e27f157c4ebf4e958165997a4b87d4de1cfc34f4dea423a153327a27b053cac7"
[profile.pixel_v3.partitions.boot]
avb.signed = true
data.type = "boot"
data.version = "v3"
data.kernel = true
data.ramdisks = ["init"]
[profile.pixel_v3.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v3.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vendor_boot", "vbmeta_system"]
[profile.pixel_v3.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v3.partitions.vendor_boot]
avb.signed = false
data.type = "boot"
data.version = "vendor_v3"
data.ramdisks = ["otacerts"]
[profile.pixel_v3.hashes]
original = "12221a69ff32e137d5f19b61f576fc6b33f0973c4a81da7722c640554ff4bc4e"
patched = "e2049c6eba6990fc5ce30130af470c134a5ccc42947dcdd398f737fbca7ae44a"
# Google Pixel 4a
# What's unique: boot (boot v2)
[device.sunfish]
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq2a.230305.008.c1-174fd16b.zip"
sections = [
{ start = 0, end = 129700 },
{ start = 476996, end = 34035261 },
{ start = 1624751947, end = 1624756371 },
{ start = 1823132563, end = 1823137581 },
]
hash.original.full = "174fd16b47ef994ea8f3cb0f3fb456df2654b0aa1f9ea6fb8e54e5c6319f2601"
hash.original.stripped = "943ce3ae2aac8a0ccd4a7e9d4e38a9c495c39639f2621c6853be4c7a3fa0fc26"
hash.patched.full = "cdfffa731f0aca0ab9eff1e7d7bfee8c4716054d64cf0a47e50f50da6cbdb849"
hash.patched.stripped = "35b720058c460dd28b00f40bed4bcd1d133522d42d00ba048172b3092a7444ee"
hash.avb_images."boot.img" = "22182f2efc7043f35d79e71abb400ce919c6b3b8419405e16469644932367ee6"
hash.avb_images."vbmeta.img" = "4cbe171bb37515f59cd4082cc2d982ca1edfeba9db09dd62c8959dd318423888"
hash.avb_images."vbmeta_system.img" = "7cdb590bfc1056a5a8c7606ff05e99eb344efe108296682698b5cfe83905e0cd"
# OnePlus 10 Pro
# Build NE2215_11_C.26
# What's unique:
# - boot (boot v4) + recovery (boot v4)
# - boot images have VTS signature block filled with all 0s
# - payload.bin uses ZERO blocks
# Build info:
# - Unofficial list of full OTAs: https://forum.xda-developers.com/t/oneplus-10-pro-rom-ota-oxygen-os-repo-of-oxygen-os-builds.4572593/
# - The North American builds are used because they're the only ones hosted on
# a well known domain
# - The build number can be found in <my_manifest>/build.prop since it's not
# obvious from the filename
[device.ossi]
url = "https://android.googleapis.com/packages/ota-api/package/4cacbe5e6a3ab6a6fade68cc40f44d0fa6a2928a.zip"
sections = [
{ start = 0, end = 204048 },
{ start = 19105432, end = 34966775 },
{ start = 2657405750, end = 2657407254 },
{ start = 4984045446, end = 5006377281 },
{ start = 5114504441, end = 5114507197 },
{ start = 5138158449, end = 5138159817 },
{ start = 5140101511, end = 5140105324 },
]
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
hash.patched.full = "8e9cf3159e57d706047325a7b774dc2dc84f0d56baae100ad54c0c723b621469"
hash.patched.stripped = "0b5dcdfdfea742bf6662b286ba260f8572a7b0081bb1129b7274c29214cdfb49"
hash.avb_images."boot.img" = "f5dc3b147c54589be8db00ca15257a3688424a9eb88bd9c2cec82ebf4f6bf859"
hash.avb_images."recovery.img" = "a42c0bf4f023cd24394184a33ee113783a9c89a7cc4c0c582a5f72cc23b72309"
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
[profile.pixel_v2.partitions.boot]
avb.signed = false
data.type = "boot"
data.version = "v2"
data.kernel = true
data.ramdisks = ["init_and_otacerts"]
[profile.pixel_v2.partitions.system]
avb.signed = false
data.type = "dm_verity"
data.content = "system_otacerts"
[profile.pixel_v2.partitions.vbmeta]
avb.signed = true
data.type = "vbmeta"
data.deps = ["boot", "vbmeta_system"]
[profile.pixel_v2.partitions.vbmeta_system]
avb.signed = true
data.type = "vbmeta"
data.deps = ["system"]
[profile.pixel_v2.hashes]
original = "8b38d2d999b5b6e240e894f669e9e2643b3764c108d53bb7b02447da725e7c18"
patched = "16f56e3d02c08bb646d8d0694ce77a6edb02a613bbcff14b10ead4a448c3dc00"
+54
View File
@@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQjwBqCb7mn4vtIbEE
v/daiQICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAMdJ698WAn+aEvKOQ70wGjBIIJ
UIJbh4gk0vu1YP5FAPR6S1jvfSaf+hCmDq1mrK2nUt1sv5vDUFimKAJUYs0lLwuH
1FCB3QkXrn+2Z2wcJoeA1nq0pp39nvGu4EBBTAHgTebwfaqvfNvRE9kAjGjMWpux
BKaXxpSOC1V2JNfcxWi+reKwOweDzde9s5vpvPx7/637GPrLiMOvKWoUSg15jDbF
u8T/hfY2TC0KiRLLT9b/8DZrscjXH1jAM7e3FK9F1PT9DSO9SfEkH3YRGPwkcnrl
JcI6aPD0J3/YtXYrgcTdNNUdJy3Tbq328peXyw1ayumQTHfHahCNaIaKtH2WmKtx
xUcaZnFD+AxPT52Dl41XD69tN6tWSENuf0ruaD6HsRRcALH4X5xqLKsObA/t2b6S
m3bWcbKhuD5MGLcuw0oKbDuEYVJeZvuppsKhr2JsqqPBbnJc8FR59t2IGMYOteFV
HD6oX0YxIZU+Cb3R3xF7gO7cn/NxBJetph9FBhn1yLEqZnU2uWpU0+WFY64J8S/1
yDJsu7Sa9HShZpUQfLC/eRsQQQKaZrUsCHHC9MFFqy9zmsn1lIFUbqAJ6MTe3cf4
QQJNhwNdBzSInBuI8sv5lwk6eidQyyRkaB+M8TrjhIwFRlCBd7XUbPACID+m/1jD
DvzB/iV8GHhDW0gf8jOObfGDA9zsDt9R1fVgb4ehimNPhDV8ILIELqREo9DWTr4Q
RGmjSwiyENi1Pp4PXS12NI0KbdJ5zuj5scwwLogs3Kdt3WMmqCZU6ektDeKuRI0W
ZnnCEiHo7jinMo869ZhyI3BQykuSfRH24JuPX+F3vxHmPoFYDjHG8ZCVWyZEMHPl
wRxok5k8jwe+sMOmRtQZPkcJFSiSYWyUciRED2oV1U5ojDo2LBvmQLjgPwCj/ECo
v9DwRzsrljwI7tpIqBmHbWxcSxdDhdQNiRk0PU0bunNh1ubJkoyj/FdEPHOvexpR
hCGzwDRqGRz5Mk73FI6ybCbBguo7m3ic6duMNhSYQdKTBPAilPjSpEB+a4awyjFv
sn5zVciwgti/CtZJiFDNeGAxr4V18OO8qdDVL8Za/SvqZrr0Y0MJgnWM4RuB6lBl
d+Rwub9ReJIhiAjTPIiJfHibxtDLEtXEfbSdE/a/Yn0xQYUYHKdbYJS0heumSF7D
ihvGedIpW16YjXCb052xCxeN05rDzbm1m7UJlFivSfo/GXgQSH4W9xzG8yLCid8L
PnptMXJFbI53mCJfjKy7ZV7rlx6dXf/uN620qwr/QCxBQfjM4ndA7X3UUAF/riHW
MgaY5+GcBhRhGug1IcIZG/p8QB0t8Lt6Xssu/87uVSsc9pKEymVZwdhy3gU/YZY0
dx6AsYk9E2n05g5AtafsDnik1gVgBb5KtMzW7XKYAA8805Ms4+gqeS9OUrevF6QS
mRFHJpvaNuEYR6Lp6jLfwPH6MK+tZbE42St8XGLbFFgp3ncSe6UBhoGCYN7/hJkw
QXPnK7vlgiO3NEV4d0IdZWRPUHXASYiEkpv2IYmUq8CYyqZuBERVs6eZmLIC7D9V
y6kORASxDl0wEl4Nc0B0iZ0jhMGiNmG9EO2ek+YwlpB+vBm1C/Z+30qpzAnbnh22
HWHn5g9koMa4oEkO7P5Bpp4UifxX/S5f0lAuLotaE6eYmwr9Tw7N9Za/mSLk2CuK
akc/T/ycgQdGb/wWHvCzvm3EsbuAAxFDqhwAoobhkdvSyQek8HYJ2FiiXHgFY9kI
RJ49AHCaiXwHInZwVzJd30NJFn1I/o9dLULwYPNsjfeULr5ZUSe95CYLL4LUxDFr
ecd0YiuoA1Y8gIwIRAaFAs1wdkNZoiEj3vH3LMFM6DMlvwDCB+vOpFwmLRQOT5Vq
DWPZTKNYkmU9ReWY6AM2VhmMZa+GHW7wbPKllPUCA/OmZQM98f1ivPXhADP+IAWt
1zNy+hqhrB/HV820t7m8SEK9GRAj0ARfpV6b9LsUH91TBY4MXKSZW9BDlu51Nwcl
zRZDIW1ZDA4cCI8JWnCALTNntuzcRx+y9pBRwxSOJXv6i08yOxDE7En1iiF79S6T
91jo20SLGeazQm1iun2eOh/49xplktxED5m/4/aAIjwDxXpbGT9VzOZ86P0/z32j
G0PngN173UGkmLDUZlaUavVrR3l85Uw6OxIgNMDaG8gQkbZT+iVLQs4MGiOJqFfc
UqusOXh2E3WUkX+5cWIdv3GGwBvKw6NE09u1+RJMvqHB3NlOruSazDyTVgPC/IkQ
iMSm1rGWdXQozaAur3jXQDYrmM4iV2ydDDcUc4FAgCwnT68073BemHvv9gPzTyex
Wr8N98nmTepypAcgARDI2wlwaGm3fO5kvaxOq4fiqYK7PoTUVsS6OCfafhR7MNVQ
lVpNHaDFS99ygjzjIvd2tQYY/Mwsvhp6YyMcXhtqTk0snlCGNFQh1rnDfEUFCGV7
EaraeuvTk5+ofTd9S8Qm+weYddRkHeZVpEFAOa/cL/cevek7yv60pXDUPrmlWD29
Ydu0VEO7DmVQnLm2gDprIQsZc01QxKRpCuRgwRgcpmAHX+xCFUWTCFL59mHW3F5x
7VCl79NXYaO6Rp7GV6xTgjD5FMe0pP51s8+dUwTCq3U5Y5KqScvvHiPaatb0DuSI
FcMzXRcapGrj5C42U2R7cpLq0xJi5QFapDD+wlEUs0lxZDiqf30Qr197a3KOOsWs
pyoR/Ytry0nyHxy4CFvA5hbWc7ng2ZegykCkBVS6XM2upUbWxbPihb3v5UgDb3zD
wXXwhjs+hwIh6xha7HlfH43HF8ucjLsPadw5EuQfQJ21a4dmaMbSDu1u6aPoKHsQ
zF/DnySx5ZEdjNz9xMvz7g8LoRAWR4dMsuZFOBismy7KmMqFNsw0GNSRcDT5ffZf
MmXbI654Jv4s6UMl2ghyK7xmYiEeSeVH41AF/LaZMl4V5KSNKuOhn764DzB6z0d4
1XQrq3wCpJ3K4hknKZhTbgSAmvk7i8CwpWZfUWAzuBL9jS1Z6RoN/zgsjM5vDMSs
QUEguBjytPKioiuhZh+F+buvoNDKWvYwlJk8tUyNwtFOhWjniend7zEeioVO/kdg
abSrBmjqZkxbCVreNdDekiGwOz1iD1dlreW6lJ6hW0mBnSAafR/lwXLJCE1slTWf
LIbM8DLZMMeKZ9v7M+VMlW0uL36utP2hMnzHnh9f9eYG
-----END ENCRYPTED PRIVATE KEY-----
@@ -0,0 +1 @@
CWokJ23olHitbyjqw6aZoIFhAo3mwzwdEEkv7sjh600hPwR0YNUDxSkNA7ztK5Ii
Binary file not shown.
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE6jCCAtKgAwIBAgIIG/hHrySZ878wDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UE
AwwHYXZicm9vdDAgFw0yMzEyMjYwMzQwNDBaGA8yMDUxMDUxMzAzNDA0MFowEjEQ
MA4GA1UEAwwHYXZicm9vdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
AK9VVEeI2UnjQr3IrVlZddq2lTHOlV8qFBVzMguVJUr128HqIXQRgXbEzHfCOOzC
Nuh2x+cBXO7gztpwjSJFi2P+q4OdEujmgCf8SdwxS1NqWo5TRsB9szPALeQ+FQi8
yir+urv6uJUuDpwwPgjhCDwn1tL426c4HgGsQpO0IWjCzc2SWL+KP4zirk7GIf2R
Fy0TsFeJi5ucHWiMuXCGyiPZ2xV7AZ3AakZnf6rqWEIkOOsBf6UVNxFKuppMBcY6
Lb6YcXB7nn2xFwZ+S5gGdZjkZIlLAoZjRynVRBz2BwlJB6ignH8+MbWkYS6ls9C8
C10SmiGXI/S/DhRR11JCNAC5AD8FO+A6i9Vp19PiVmUv6nTx01+FhRqTeiW5bPhk
PTn9gUAU5FKL4ouhr3ojJd6BczdJ2lWB9lQYmWnwzdYnSAnvXwc3WaSX+ryi7LoJ
sZtlc91ZT2yNiqmPQpH6fcPKfi/BJZhe61hZ7Xv3+OVupbmU+IuUK9iKEw5+aqNt
DiT15PKoyKuAfm0A/TJ1UV/P4FbSavEOrMse2SZvhRL/EvwpRXwaiwPFQL/vZ5GZ
mGCXdmVt0FEH9z3UyYq9+UIhZoQh3V9eSrOgjOh3dnFZN5E/UQzqndykGy+pYjG4
e4ZZQjQR5G3pmAiCIngqnqRUNWpG5DQcZYuXLdor19o5AgMBAAGjQjBAMB0GA1Ud
DgQWBBQgy5IQsP4+QnQRWlMJiFsqwyXAzTAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud
DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAn8pexl/crAWBPJ/nPu9rhRo0
dowLhbamhx/i5RRNxIJl8Lpf78Boj4t9ICuTU4yeLFHfA+IcnthS/wrFADbhSgF5
m4gLBSfRjtiOPJSJqkIqCIV6RIcbfdCJa/V3p1nvu3vuGYEp9eSYACEtv6kLaMFu
61RsEWgR6tNmb1Hssl9XzGdM2yf2vy/Gip+Ugztz9gfTF3Vhdos9VToPOzLNQh6C
dr+uLKnnPHOb9PfAonYs23MAfpBaP8m4sEMkyMgACCUP+qld0S8xpptEA/9Wblxy
zWJIZTNWhyfo7cH030lVbdgMBYtZN/WxYVtD3fp+BZPk5fD3QwM712Ja288anDic
HRwtc0wFwJ9EOOwXT1kYT1GgKrmLTnmAGDktnjKtsHMvl7Rn2Wf+5XTQ3FC99DJU
q/Jhe3BGUvOXS7uPfUSMQrH4q+cwUdPSLGu2TSPyXVUEv5/Z4mH4JD+HQbJrKjdO
NvUlnNJgtL3nIW8iSR1BRkIQn9HAnHtfYELHFBr1XHdn8LCjeWmYStdI3s9jucWb
5uIfij1/50hc6A5zl20/y1h9OGFKdRttjk9XzRR6HPt22zyStjab5Y5SzHOwMCaP
wk/uDc+Aestyqm7BoIqU8XptC+VV92OoNiEouI0UI5zPw9EWLRmTjFAc4NzOc1I7
2AKjtz3xnKC3R3oqtsA=
-----END CERTIFICATE-----
+54
View File
@@ -0,0 +1,54 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIJrTBXBgkqhkiG9w0BBQ0wSjApBgkrBgEEAdpHBAswHAQQ1ucIEwTFlt1U7QDz
w8o/RAICQAACAQgCAQEwHQYJYIZIAWUDBAEqBBAqttk+qw4JJKTbU1zgKChgBIIJ
ULyEdMcpMWYvgN6aefcIns6UJr3bYfq8aX0FvhGXzO1N8Voxg/jKc753Hj3F+h1d
xfJyFzpg3D2jIIR3GRZrXILqc+aXaW3AS8/U7IN2d4gJIrYAbUelBnssdgLg6FSY
MUprWGUC8ShMZpOhAoDD2V8ZHyb9NHJ/xPpbhhBUvDl/jQWurCo3KnWlFiXiGO3I
VyH09LL0T+v4OcU7HjrVHWXokcv+6mkcPvkZZI6MTMC+7UyxNi9WlZs98Lzckpax
asiEHImnDbtGsvYXvjKWn7QXs1CF4SVZ5DdoHMtf1BEHMnXTQ4teSRkp8B02ihr6
LtSoStVXdw1E3+5R6DmW8b+Bxi7duO9URazkH2dHG7ZJvhL86DcvsFNnMW7NZ3PE
N0BYDGTPjSAp8qbkCesbzhYEoDFsTfp6S1vLB+9PL0/S8jJ76MgZJ5f6i69aEz62
Ek7pbydMnrMlMoWmgNEzcywGf1GnNcSQfPUVUyW11HLShNUoD+PktrnPRHhti2cr
VXfWNeaf9lEwnQQYPGEuwxx4+lfK/sL6TV245ziVFAWQ0koJHIF09nIlFKGT9sOa
JrwXWSMg1iT2iua4OLvwsHs7oMMvC+C4ycEMS0NKPEVx3pyAPrsO+rAHnEeUIz/D
ooZEjnSfE9Xlsb5W4qqPuso4H9s/ulFsGz312ZEnt3EDGrgW2BksG42F7G3Qm8nn
wUzSEGAAs/iMO/qbVF6P5/LnoO7h+ds9GCudmBYPKj1o5cwLcyNPjBHbKAXlzvOJ
xyx17aCJ2f7uZzto1ov05lr6N+0/ajRwQn9IIx3R1wFvssETav+fXLavvm0Ul065
iQgpyQnmZeKgzXClwmc9HgoFIMQSzs3lfRgV2coQCvFMgKvP6cVLzMf98NUf7XmM
/fL6aG62KGUVSaat+4mfPT8p8MiuGBz2BmTkEtTWMf+HsgPu3yYKHJ6iAF4JjZ2m
xpDOAd2jACpVVQ+7fxyejSl/VILFuJPL7RKAvIjJzq40X3o/w2Ydih1LgGLiyFHe
I9RXAqCkrA7rbN/TEtZkgpgsLUbbY2b5+rd++DoCIzB4zF6JSE1RTRYkR4jsxyFB
BFwNqgZX/xpA0vQ4WqpYkAc4tsefJEXxt2SsI4C3pVYLYuso+4hY0ZP+n3QXseyV
N6VKpbztWleHXeLI53Bmb5kj4oJk44ouO36A/DtUlEESfvtGrnqRzVG8vMIqhOOU
sX8L+XmvCSnCkV8zDqaI/Wmr/X+4BQMjWJ3C0v3RBXJNCPWaQG9laNoHrcdCxGuR
GCNArOhyxHfjl7jplw2YgNscPPd/GFuUOY3UEG7hWW14/cpGBz5D/PfKnq1QkKw+
e5xvq/+BYGch3K0kOh3aEhbgPZxztIF+JZMcSokNDMPATE7zPpCxAYFCkasA/ccJ
CTeyZqvLGMFYuMwe/rnPbgcxr/1fEEzgAsYkC6JqsIFq2uhEaDPrvmQ9LwAAwBfw
EWhHpSOtMxEwOvzOqGuKDqUy918f5xSXyvKGPA8RC2ZPEEVHChQfyRRcS/9TRqhR
6E0h6frMunWUdBtiDmm3iC6hllWVmwmXJL68aOWokCOf0hBKLl0tGw2QKsaeD3ta
SV8478YUoPn2oNZSQW4QwncQMU/6djjtsL31MF6itZU3ethXbxOQWO3RkybFY1iL
2eZmExFd9IdagxrI8xOxxMq7hYbAF+gB1vgcuzktz3/3L/lBAAl6n+9eAAK3E87U
jhNDC/flJWxpdbNmwGoAUmGMt+oemdtE2gqDpCMgU/TEpuJcPrNYf3cce/CFIYOG
tsYYPzp1QE63418/A7+9nJYTC2Bqqc0nLnARVXKMUpAUuf5xnRNvMod50xMZviJi
aMdGcKnMVAk0esJG68/W3cQdK1efIeIhd7pFYUq1WdzBwfJV3BUYbXLV40qld50U
EuxUPxWVCKD0soPvRl6DqLY7fE4gzEykwzjc2Y6l8oLuQfAzaNX/ItEVX+qvUXKe
myZpKYqAqjOOdAZHLfyyFQzIMHzls83XhTMixlY53ZNMn3rs34LYR0F2k7is6Ysh
qxulqwQ/EcY446WQyTw3XT0IPLIIBXQGpYsuYChn/U2QVSoJxSS1LIeOpMSNJOwk
QnsPtxB6gK3Svt/wdnK0GCZEKwMBx9VAz+M1NKCQQMsv2xWdQ4qm6fOhMEdm/oq5
Pc9pQrq2J9XwBI/3AncKvjxKCfRC6Ob6gEO4cUybbEAZ9r3GCGtdf2+9x/aJm5XP
OeXXRYbwOQtaQp5GgAHE/WoGPWt+lXKLu5mUqrbONS6TLibdAYve9l4cFp3hTvki
159bCm4HrCUj3Etu3x6Nv5SMhXhRdyULrOvx9GA7kco2arXcxSPM79KiyCPMVvyu
e/sRn3X/dS1NfaJ5IlKYLjiJQVySJOmyNhmlMQkLErqjQ1M2ibwybFglrUdJflcd
Bjk3ZbN0r/BqbWWrwVIUVkq6vr0Yuxb1AgEl6HH1QNcX67XLSrKFG5sGkfsImMTd
gvEYKFukgXmqKX3/p9czjJMYlC6sFT0L3e5G3LiEJlGT5jxklEitcAvwTowhbhCy
T0Jf/KTNn2GnisDUK2DZKRbhiAHv3AEkxAIGg6On5SJ0PzFF0MhtxJ7nA3TqzvUN
gN7jAAftaiALPImQf1EDgj3rHu8O8XAC/Nac65Qvn2ZUjMOW+Cba1POal0G76XZx
JunWIDu0BgOXNc7kBRmE+dK9ppEbD5DScUjosAvkZuF94h23Ofzwm9/xBgXPbaV4
CbWBtvRLuKacuLKF3WfQCw/Wi0K70dwDUEAaM1/rJiavGzkYp1z0KPey3hKDzkFL
2ZjkzQdCreXo48Jwfwu4OQFTpkACWLvB0pLbhBXjq5Q9NEJHizRr08YT0gl+5mbJ
Dk+Aw/n2Wy5ZRVbxxklO5brLnu1R8Rq3Szq1ATENrXSHwIN3sUiOEuNu8tSa3If3
Hb5WS1WELgVPzGaUWKsWI9sooTJEnPOXxBq0U0iZpqpnoUw6NenIgIfBWrvG/7LC
5SPksOVuaD6YdC4OF35/Jlrk4uKnCsW5SYa69Kr5RST/8MFGrefb7233iiRAo3La
vr+uvh4NGA9Jf7Pv4yuN2IuCsyAl2jleSDEj6cf/tTz6m9xPT3Nr1/pKrL8mkRxv
HDHq2ivR6QjBpEEoHITMaYndgOvwpCA1qXdsP/DiugCx
-----END ENCRYPTED PRIVATE KEY-----
@@ -0,0 +1 @@
R9iItBaS278KvqlLyil1yUlizNGvWI93SERQ6uAitbzXm4fOc9t3c8dG8r2ThuZz
+14 -126
View File
@@ -3,44 +3,21 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{ffi::OsString, path::PathBuf, str::FromStr};
use std::path::PathBuf;
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
#[derive(Debug, Args)]
pub struct DeviceGroup {
/// Device config name.
pub struct ProfileGroup {
/// OTA profile name.
#[arg(short, long, value_name = "NAME")]
pub device: Vec<String>,
pub profile: Vec<String>,
/// All device configs.
#[arg(short, long, conflicts_with = "device")]
/// All profiles.
#[arg(short, long, conflicts_with = "profile")]
pub all: bool,
}
#[derive(Debug, Args)]
pub struct DownloadGroup {
/// Revalidate hash of existing download.
#[arg(long)]
pub revalidate: bool,
/// Download the stripped OTA instead of the full OTA.
#[arg(long)]
pub stripped: bool,
}
#[derive(Debug, Args)]
pub struct PatchGroup {
/// Delete patched output files on success.
#[arg(long)]
pub delete_on_success: bool,
/// Suffix for patched output files.
#[arg(long = "output-file-suffix", value_parser, default_value = ".patched")]
pub suffix: OsString,
}
#[derive(Debug, Args)]
pub struct ConfigGroup {
/// Path to config file.
@@ -53,112 +30,26 @@ pub struct ConfigGroup {
)]
pub config: PathBuf,
/// Working directory for storing images.
#[arg(
short,
long,
value_name = "DIRECTORY",
value_parser,
default_value = "files"
)]
pub work_dir: PathBuf,
}
/// Convert a full OTA to stripped form.
///
/// A stripped OTA omits byte regions of the OTA that aren't needed for testing
/// avbroot's patching logic (eg. the system partition image). This reduces the
/// size of the test files by about two orders of magnitude.
#[derive(Debug, Parser)]
pub struct StripCli {
/// Path to original OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
pub input: PathBuf,
/// Path to new stripped OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
pub output: PathBuf,
}
#[derive(Debug, Clone)]
pub struct Sha256Arg(pub [u8; 32]);
impl FromStr for Sha256Arg {
type Err = hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut data = [0u8; 32];
hex::decode_to_slice(s, &mut data)?;
Ok(Self(data))
}
}
/// Add a new OTA image to the test config.
///
/// This will download the OTA image, strip it, patch both images, and add the
/// resulting metadata (eg. checksums) to the specified test config file.
#[derive(Debug, Parser)]
pub struct AddCli {
/// URL to the full OTA zip.
#[arg(short, long)]
pub url: String,
/// Device config name.
#[arg(short, long, value_name = "NAME")]
pub device: String,
/// Expected sha256 hash of the full OTA zip.
#[arg(short = 'H', long, value_name = "SHA256_HEX", value_parser)]
pub hash: Option<Sha256Arg>,
#[command(flatten)]
pub patch: PatchGroup,
#[command(flatten)]
pub config: ConfigGroup,
/// Skip verifying OTA and AVB signatures.
/// Working directory.
///
/// OTAs for some devices (eg. ossi) ship with vbmeta partitions containing
/// invalid hashes. These will normally fail during validation.
#[arg(long)]
pub skip_verify: bool,
}
/// Download a device image.
#[derive(Debug, Parser)]
pub struct DownloadCli {
/// Download the Magisk APK.
#[arg(short, long)]
pub magisk: bool,
#[command(flatten)]
pub device: DeviceGroup,
#[command(flatten)]
pub download: DownloadGroup,
#[command(flatten)]
pub config: ConfigGroup,
/// If unset, a temporary directory is used, which will be automatically
/// cleaned up, even if a failure occurs. Custom working directories are
/// not deleted.
#[arg(short, long, value_name = "DIRECTORY", value_parser)]
pub work_dir: Option<PathBuf>,
}
/// Run tests.
#[derive(Debug, Parser)]
pub struct TestCli {
#[command(flatten)]
pub device: DeviceGroup,
#[command(flatten)]
pub download: DownloadGroup,
#[command(flatten)]
pub patch: PatchGroup,
pub profile: ProfileGroup,
#[command(flatten)]
pub config: ConfigGroup,
}
/// List devices in config file.
/// List profiles in config file.
#[derive(Debug, Parser)]
pub struct ListCli {
#[command(flatten)]
@@ -167,9 +58,6 @@ pub struct ListCli {
#[derive(Debug, Subcommand)]
pub enum Command {
Strip(StripCli),
Add(AddCli),
Download(DownloadCli),
Test(TestCli),
List(ListCli),
}
+91 -100
View File
@@ -3,15 +3,11 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{collections::BTreeMap, fs, ops::Range, path::Path};
use std::{collections::BTreeMap, fs, path::Path};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use toml_edit::{
ser::ValueSerializer,
visit_mut::{self, VisitMut},
Array, Document, InlineTable, Item, KeyMut, Table, Value,
};
use toml_edit::Document;
#[derive(Serialize, Deserialize)]
pub struct Sha256Hash(
@@ -23,117 +19,112 @@ pub struct Sha256Hash(
);
#[derive(Serialize, Deserialize)]
pub struct Magisk {
pub url: String,
pub hash: Sha256Hash,
#[serde(deny_unknown_fields)]
pub struct OtaInfo {
pub device: String,
pub fingerprint: String,
pub build_number: String,
pub incremental_version: String,
pub android_version: String,
pub sdk_version: String,
pub security_patch_level: String,
}
#[derive(Serialize, Deserialize)]
pub struct OtaHashes {
pub full: Sha256Hash,
pub stripped: Sha256Hash,
#[serde(deny_unknown_fields)]
pub struct Avb {
pub signed: bool,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RamdiskContent {
Init,
Otacerts,
InitAndOtacerts,
Dlkm,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BootVersion {
V2,
V3,
V4,
VendorV3,
VendorV4,
}
#[derive(Serialize, Deserialize)]
pub struct ImageHashes {
pub original: OtaHashes,
pub patched: OtaHashes,
pub avb_images: BTreeMap<String, Sha256Hash>,
#[serde(deny_unknown_fields)]
pub struct BootData {
pub version: BootVersion,
#[serde(default)]
pub kernel: bool,
#[serde(default)]
pub ramdisks: Vec<RamdiskContent>,
}
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DmVerityContent {
SystemOtacerts,
}
#[derive(Serialize, Deserialize)]
pub struct Device {
pub url: String,
pub sections: Vec<Range<u64>>,
pub hash: ImageHashes,
#[serde(deny_unknown_fields)]
pub struct DmVerityData {
pub content: DmVerityContent,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VbmetaData {
pub deps: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Data {
Boot(BootData),
DmVerity(DmVerityData),
Vbmeta(VbmetaData),
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Hashes {
pub original: Sha256Hash,
pub patched: Sha256Hash,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Partition {
pub avb: Avb,
pub data: Data,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Profile {
pub partitions: BTreeMap<String, Partition>,
pub hashes: Hashes,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub magisk: Magisk,
pub device: BTreeMap<String, Device>,
}
struct ConfigFormatter;
impl VisitMut for ConfigFormatter {
fn visit_table_like_kv_mut(&mut self, key: KeyMut<'_>, node: &mut Item) {
// Convert non-array-of-tables inline tables into regular tables.
if let Item::Value(Value::InlineTable(t)) = node {
let inline_table = std::mem::replace(t, InlineTable::new());
*node = Item::Table(inline_table.into_table());
}
// But for hashes, use dotted notation until TOML 1.1, which allows
// newlines in inline tables, is released.
if key == "hash" || key == "original" || key == "patched" || key == "avb_images" {
if let Some(t) = node.as_table_like_mut() {
t.set_dotted(true);
}
}
visit_mut::visit_table_like_kv_mut(self, key, node);
}
fn visit_table_mut(&mut self, node: &mut Table) {
// Make tables implicit unless they are empty, which may be meaningful.
if !node.is_empty() {
node.set_implicit(true);
}
visit_mut::visit_table_mut(self, node);
}
fn visit_array_mut(&mut self, node: &mut Array) {
visit_mut::visit_array_mut(self, node);
// Put array elements on their own indented lines.
if node.is_empty() {
node.set_trailing("");
node.set_trailing_comma(false);
} else {
for item in node.iter_mut() {
item.decor_mut().set_prefix("\n ");
}
node.set_trailing("\n");
node.set_trailing_comma(true);
}
}
}
/// Add a device to the config file. This leaves all comments intact, except for
/// those contained within the existing device section if it exists.
pub fn add_device(document: &mut Document, name: &str, device: &Device) -> Result<()> {
let device_table = document.entry("device").or_insert_with(|| {
let mut t = toml_edit::Table::new();
t.set_implicit(true);
Item::Table(t)
});
let old_table = device_table.get(name).and_then(|i| i.as_table());
let value = device.serialize(ValueSerializer::new())?;
let Value::InlineTable(inline_table) = value else {
unreachable!("Device did not serialize as an inline table");
};
let mut table = inline_table.into_table();
ConfigFormatter.visit_table_mut(&mut table);
// Keep top-level comment on the table.
if let Some(t) = old_table {
*table.decor_mut() = t.decor().clone();
}
device_table[name] = Item::Table(table);
Ok(())
pub ota_info: OtaInfo,
#[serde(default)]
pub profile: BTreeMap<String, Profile>,
}
pub fn load_config(path: &Path) -> Result<(Config, Document)> {
let contents =
fs::read_to_string(path).with_context(|| anyhow!("Failed to read config: {path:?}"))?;
fs::read_to_string(path).with_context(|| format!("Failed to read config: {path:?}"))?;
let config: Config = toml_edit::de::from_str(&contents)
.with_context(|| anyhow!("Failed to parse config: {path:?}"))?;
.with_context(|| format!("Failed to parse config: {path:?}"))?;
let document: Document = contents.parse().unwrap();
Ok((config, document))
-456
View File
@@ -1,456 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
* SPDX-License-Identifier: GPL-3.0-only
*/
use std::{
cmp,
collections::{HashMap, VecDeque},
fs::{self, OpenOptions},
io::{self, Seek, SeekFrom, Write},
ops::Range,
path::{Path, PathBuf},
time::{Duration, Instant},
};
use anyhow::{anyhow, bail, Context, Result};
use avbroot::stream::PSeekFile;
use serde::{Deserialize, Serialize};
use tokio::{
runtime::Runtime,
signal::ctrl_c,
sync::{mpsc, oneshot},
task::{self, JoinSet},
};
use tokio_stream::StreamExt;
/// Minimum download chunk size per task.
const MIN_CHUNK_SIZE: u64 = 1024 * 1024;
pub trait ProgressDisplay {
fn progress(&mut self, current: u64, total: u64);
fn error(&mut self, msg: &str);
fn finish(&mut self);
}
pub struct BasicProgressDisplay {
current: u64,
total: u64,
interval: Duration,
last_render: Instant,
avg: VecDeque<(Instant, u64)>,
}
// Speed is a simple moving average over 5 seconds.
static AVG_INTERVAL: Duration = Duration::from_millis(100);
static AVG_WINDOW_SIZE: usize = 5000 / AVG_INTERVAL.as_millis() as usize;
impl BasicProgressDisplay {
pub fn new(interval: Duration) -> Self {
Self {
current: 0,
total: 0,
interval,
last_render: Instant::now() - interval,
avg: VecDeque::new(),
}
}
fn clear_line(&self) {
eprint!("\x1b[2K\r");
}
}
impl ProgressDisplay for BasicProgressDisplay {
fn progress(&mut self, current: u64, total: u64) {
self.current = current;
self.total = total;
let now = Instant::now();
if self.avg.is_empty() || (now - self.avg.back().unwrap().0) > AVG_INTERVAL {
if self.avg.len() == AVG_WINDOW_SIZE {
self.avg.pop_front();
}
self.avg.push_back((now, current));
}
if now - self.last_render > self.interval {
let current_mib = current as f64 / 1024.0 / 1024.0;
let total_mib = total as f64 / 1024.0 / 1024.0;
let front = self.avg.front().unwrap();
let back = self.avg.back().unwrap();
let avg_window_mib = (back.1 - front.1) as f64 / 1024.0 / 1024.0;
let avg_window_duration = back.0 - front.0;
let speed_mib_s = if avg_window_duration.is_zero() {
0.0
} else {
avg_window_mib / avg_window_duration.as_secs_f64()
};
self.clear_line();
eprint!("{current_mib:.1} / {total_mib:.1} MiB ({speed_mib_s:.1} MiB/s)");
self.last_render = now;
}
}
fn error(&mut self, msg: &str) {
self.clear_line();
eprintln!("{msg}");
}
fn finish(&mut self) {
self.clear_line();
}
}
#[derive(Debug)]
struct ProgressMessage {
task_id: u64,
bytes: u64,
// Controller replies with new ending offset
resp: oneshot::Sender<u64>,
}
/// Download a contiguous byte range. The number of bytes downloaded per loop
/// iteration will be sent to the specified channel via a `ProgressMessage`. The
/// receiver of the message must reply with the new ending offset for this
/// download via the oneshot channel in the `resp` field. An appropriate error
/// will be returned if the full range (subject to modification) cannot be fully
/// downloaded (eg. premature EOF is an error).
async fn download_range(
task_id: u64,
url: &str,
mut file: PSeekFile,
initial_range: Range<u64>,
channel: mpsc::Sender<ProgressMessage>,
) -> Result<()> {
assert!(initial_range.start < initial_range.end);
let client = reqwest::ClientBuilder::new().build()?;
let response = client
.get(url)
.header(
reqwest::header::RANGE,
format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
)
.send()
.await
.and_then(|r| r.error_for_status())
.with_context(|| anyhow!("Failed to start download for range: {initial_range:?}"))?;
let mut stream = response.bytes_stream();
let mut range = initial_range.clone();
while range.start < range.end {
let data = if let Some(x) = stream.next().await {
x?
} else {
return Err(anyhow!("Unexpected EOF from server"));
};
// This may overlap with another task's write when a range split occurs,
// but the same data will be written anyway, so it's not a huge deal.
task::block_in_place(|| {
file.seek(SeekFrom::Start(range.start))?;
file.write_all(&data)
})
.with_context(|| {
format!(
"Failed to write {} bytes to output file at offset {}",
data.len(),
range.start,
)
})?;
let consumed = cmp::min(range.end - range.start, data.len() as u64);
range.start += consumed;
// Report progress to the controller.
let (tx, rx) = oneshot::channel();
let msg = ProgressMessage {
task_id,
bytes: consumed,
resp: tx,
};
channel.send(msg).await?;
// Get new ending offset from controller.
let new_end = rx.await?;
if new_end != range.end {
debug_assert!(new_end <= range.end);
range.end = new_end;
}
}
Ok(())
}
/// Create download task for a byte range. This just calls [`download_range()`]
/// and returns a tuple containing the task ID and the result.
async fn download_task(
task_id: u64,
url: String,
file: PSeekFile,
initial_range: Range<u64>,
channel: mpsc::Sender<ProgressMessage>,
) -> (u64, Result<()>) {
(
task_id,
download_range(task_id, &url, file, initial_range, channel).await,
)
}
/// Send a HEAD request to get the value of the Content-Length header.
async fn get_content_length(url: &str) -> Result<u64> {
let response = reqwest::Client::new()
.head(url)
.send()
.await
.and_then(|r| r.error_for_status())
.context("Failed to send HEAD request to get Content-Length")?;
response
.headers()
.get("content-length")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.parse().ok())
.ok_or_else(|| anyhow!("HEAD request did not return a valid Content-Length"))
}
/// Download a set of file chunks in parallel. Only unrecoverable errors are
/// returned as an Err. Normal/expected errors and download progress info are
/// reported via `display`. Returns the remaining ranges that need to be
/// downloaded.
async fn download_ranges(
url: &str,
output: &Path,
initial_ranges: Option<&[Range<u64>]>,
display: &mut dyn ProgressDisplay,
max_tasks: usize,
max_errors: u8,
) -> Result<Vec<Range<u64>>> {
let file_size = get_content_length(url).await?;
// Open for writing, but without truncation.
let file = task::block_in_place(|| {
OpenOptions::new()
.write(true)
.create(true)
.open(output)
.map(PSeekFile::new)
.with_context(|| anyhow!("Failed to open for writing: {output:?}"))
})?;
task::block_in_place(|| file.set_len(file_size))
.with_context(|| anyhow!("Failed to set file size: {output:?}"))?;
// Queue of ranges that need to be downloaded.
let mut remaining = VecDeque::from(match initial_ranges {
Some(r) => r.to_vec(),
#[allow(clippy::single_range_in_vec_init)]
None => vec![0..file_size],
});
// Ranges that have failed.
let mut failed = Vec::<Range<u64>>::new();
// Ranges for currently running tasks.
let mut task_ranges = HashMap::<u64, Range<u64>>::new();
// Overall progress.
let mut progress = file_size - remaining.iter().map(|r| r.end - r.start).sum::<u64>();
display.progress(progress, file_size);
let mut tasks = JoinSet::new();
let mut next_task_id = 0;
let mut error_count = 0u8;
// Progress messages from tasks.
let (tx, mut rx) = mpsc::channel(max_tasks);
loop {
// Spawn new tasks.
while tasks.len() < max_tasks {
if remaining.is_empty() && !tasks.is_empty() {
// No more ranges to download. Split another task's range.
let (_, old_range) = task_ranges
.iter_mut()
.max_by_key(|(_, r)| r.end - r.start)
.unwrap();
let size = old_range.end - old_range.start;
if size >= MIN_CHUNK_SIZE {
let new_range = old_range.start + size / 2..old_range.end;
old_range.end = new_range.start;
remaining.push_back(new_range);
}
}
if let Some(task_range) = remaining.pop_front() {
tasks.spawn(download_task(
next_task_id,
url.to_owned(),
file.clone(),
task_range.clone(),
tx.clone(),
));
task_ranges.insert(next_task_id, task_range);
next_task_id += 1;
} else {
// No pending ranges and no running tasks can be split.
break;
}
}
tokio::select! {
// Interrupted by user.
c = ctrl_c() => {
c?;
break;
}
// Received progress notification.
msg = rx.recv() => {
let msg = msg.unwrap();
progress += msg.bytes;
display.progress(progress, file_size);
let task_range = task_ranges.get_mut(&msg.task_id).unwrap();
task_range.start += msg.bytes;
msg.resp.send(task_range.end).unwrap();
}
// Received completion message.
r = tasks.join_next() => {
match r {
// All tasks exited.
None => {
break;
},
// Download task panicked.
Some(Err(e)) => {
return Err(e).context("Unexpected panic in download task");
}
// Task completed successfully.
Some(Ok((task_id, Ok(_)))) => {
task_ranges.remove(&task_id).unwrap();
}
// Task failed.
Some(Ok((task_id, Err(e)))) => {
display.error(&format!("[Task#{task_id}] {e}"));
error_count += 1;
let range = task_ranges.remove(&task_id).unwrap();
if error_count < max_errors {
remaining.push_back(range);
} else {
failed.push(range);
}
}
}
}
}
}
display.finish();
failed.extend(remaining.into_iter());
failed.extend(task_ranges.into_values());
Ok(failed)
}
#[derive(Serialize, Deserialize)]
struct State {
ranges: Vec<Range<u64>>,
}
fn read_state(path: &Path) -> Result<Option<State>> {
let data = match fs::read_to_string(path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => Err(e).with_context(|| anyhow!("Failed to read download state: {path:?}"))?,
};
let state = toml_edit::de::from_str(&data)
.with_context(|| anyhow!("Failed to parse download state: {path:?}"))?;
Ok(Some(state))
}
fn write_state(path: &Path, state: &State) -> Result<()> {
let data = toml_edit::ser::to_string(state).unwrap();
fs::write(path, data).with_context(|| anyhow!("Failed to write download state: {path:?}"))?;
Ok(())
}
fn delete_if_exists(path: &Path) -> Result<()> {
if let Err(e) = fs::remove_file(path) {
if e.kind() != io::ErrorKind::NotFound {
return Err(e).context(format!("Failed to delete file: {path:?}"));
}
}
Ok(())
}
pub fn state_path(path: &Path) -> PathBuf {
let mut s = path.as_os_str().to_owned();
s.push(".state");
PathBuf::from(s)
}
/// Download `url` to `output` with parallel threads.
///
/// If `initial_ranges` is specified, only those sections of the file will be
/// downloaded. The empty regions are left untouched (i.e. filled with zeroes).
/// A `.state` file is written if the download is interrupted. If the state
/// file exists when this function is called, `initial_ranges` is ignored and
/// the ranges from the state file are used to resume the download.
pub fn download(
url: &str,
output: &Path,
initial_ranges: Option<&[Range<u64>]>,
display: &mut dyn ProgressDisplay,
max_tasks: usize,
max_errors: u8,
) -> Result<()> {
let state_path = state_path(output);
let ranges = match read_state(&state_path)? {
Some(r) => Some(r.ranges),
None => initial_ranges.map(|r| r.to_vec()),
};
let runtime = Runtime::new()?;
let remaining = runtime.block_on(download_ranges(
url,
output,
ranges.as_deref(),
display,
max_tasks,
max_errors,
))?;
if remaining.is_empty() {
delete_if_exists(&state_path)?;
} else {
write_state(&state_path, &State { ranges: remaining })?;
bail!("Download was interrupted");
}
Ok(())
}
+1002 -620
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
/hfuzz_target/
HONGGFUZZ.REPORT.TXT
*.honggfuzz.cov
*.fuzz
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "fuzz"
version.workspace = true
license.workspace = true
edition.workspace = true
repository.workspace = true
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
# There's currently no good way to exclude a workspace member for a specific
# platform, so we just compile empty stubs on Windows (which isn't supported by
# honggfuzz).
# https://github.com/rust-lang/cargo/issues/5220
# https://github.com/rust-lang/cargo/issues/6179
[target.'cfg(unix)'.dependencies]
avbroot = { path = "../avbroot" }
honggfuzz = "0.5.55"
+53
View File
@@ -0,0 +1,53 @@
# Fuzzing
While avbroot's parsers are all memory-safe, it is still possible for panics or crashes to occur, for example due to excessive memory allocation, integer overflow, or division by zero. Fuzzing helps to identify these issues by randomizing inputs in a way that tries to increase code coverage.
## Running the fuzzers
1. Install the cargo honggfuzz commands.
```bash
cargo install honggfuzz
```
2. Pick a fuzz target to run. A fuzz target is the name of the source file in [`src/bin/`](./src/bin) without the `.rs` extension.
The list of targets can be queried programmatically with:
```bash
cargo read-manifest | jq -r '.targets[].name'
```
3. Run the fuzzer.
```bash
cargo hfuzz run <fuzz target>
```
This will run forever until it is manually killed. At the top of the screen, a summary section like the following is shown:
```
Iterations : 31,243 [31.24k]
Mode [1/3] : Feedback Driven Dry Run [2486/4085]
Target : hfuzz_target/x86_64-unknown-linux-gnu/release/bootimage
Threads : 8, CPUs: 16, CPU%: 800% [50%/CPU]
Speed : 36,126/sec [avg: 31,243]
Crashes : 53 [unique: 1, blocklist: 0, verified: 0]
Timeouts : 0 [1 sec]
Corpus Size : 1,424, max: 24,576 bytes, init: 4,085 files
Cov Update : 0 days 00 hrs 00 mins 00 secs ago
Coverage : edge: 897/224,621 [0%] pc: 2 cmp: 34,736
```
When a crash occurs, the `Crashes` counter will increment and the input data that triggered the crash will be written to `hfuzz_workspace/<fuzz target>/*.fuzz`. New files are only written for unique crashes.
4. If a crash occurs, run the following command to trigger the crash in a debugger.
```bash
cargo hfuzz run-debug <fuzz target> \
hfuzz_workspace/<fuzz_target>/<input file>.fuzz
```
This defaults to using `rust-lldb`. To use `rust-gdb` instead, set the `HFUZZ_DEBUGGER` environment variable to `rust-gdb`.
Alternatively, just feed the input file to the appropriate avbroot command directly (eg. `avbroot boot info -i hfuzz_workspace/<fuzz_target>/<input file>.fuzz` for boot images).
@@ -0,0 +1 @@
../../../../avbroot/tests/data/vbmeta_appended_hash.img
@@ -0,0 +1 @@
../../../../avbroot/tests/data/vbmeta_appended_hash_tree.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/vbmeta_root.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v0.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v1.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v2.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v3.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v4.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/boot_v4_vts.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/vendor_v3.img
+1
View File
@@ -0,0 +1 @@
../../../../avbroot/tests/data/vendor_v4.img
Binary file not shown.
File diff suppressed because one or more lines are too long
+21
View File
@@ -0,0 +1,21 @@
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
use avbroot::format::avb;
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let reader = Cursor::new(data);
let _ = avb::load_image(reader);
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
+21
View File
@@ -0,0 +1,21 @@
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
use avbroot::{format::bootimage::BootImage, stream::FromReader};
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let reader = Cursor::new(data);
let _ = BootImage::from_reader(reader);
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
+22
View File
@@ -0,0 +1,22 @@
#[cfg(not(windows))]
mod fuzz {
use std::{io::Cursor, sync::atomic::AtomicBool};
use avbroot::format::cpio;
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let cancel_signal = AtomicBool::new(false);
let reader = Cursor::new(data);
let _ = cpio::load(reader, true, &cancel_signal);
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
+36
View File
@@ -0,0 +1,36 @@
#[cfg(not(windows))]
mod fuzz {
use std::{io::Cursor, sync::atomic::AtomicBool};
use avbroot::{
format::fec::FecImage,
stream::{FromReader, SharedCursor, WriteZerosExt},
};
use honggfuzz::fuzz;
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let cancel_signal = AtomicBool::new(false);
let reader = Cursor::new(data);
if let Ok(fec) = FecImage::from_reader(reader) {
let mut input = SharedCursor::new();
// Allow verify() to get further, but don't blow up the host
// with excessive memory usage.
if fec.data_size < 64 * 1024 * 1024 {
input.write_zeros_exact(fec.data_size).unwrap();
}
let _ = fec.verify(&input, &cancel_signal);
}
});
}
}
}
fn main() {
#[cfg(not(windows))]
fuzz::main();
}
-6
View File
@@ -1,6 +0,0 @@
id=com.chiller3.avbroot.clearotacerts
name=clearotacerts
version=v2.0.0
versionCode=131072
author=chenxiaolong
description=Block A/B OTAs by clearing verification certificates
+2 -2
View File
@@ -1,6 +1,6 @@
id=com.chiller3.avbroot.oemunlockonboot
name=oemunlockonboot
version=v2.0.0
versionCode=131072
version=v3.0.0
versionCode=196608
author=chenxiaolong
description=Enable OEM unlocking on every boot
+1 -1
View File
@@ -14,7 +14,7 @@ clap = { version = "4.4.1", features = ["derive"] }
human-sort = "0.2.2"
regex = { version = "1.9.4", default-features = false, features = ["perf", "std"] }
tempfile = "3.8.0"
toml_edit = "0.19.14"
toml_edit = "0.21.0"
walkdir = "2.3.3"
# https://github.com/zip-rs/zip/pull/383
+6 -22
View File
@@ -20,16 +20,11 @@ use crate::WORKSPACE_DIR;
struct LinkRef {
link_type: String,
number: u32,
user: Option<String>,
}
impl fmt::Display for LinkRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{} #{}", self.link_type, self.number)?;
if let Some(u) = &self.user {
write!(f, " @{}", u)?;
}
write!(f, "]")
write!(f, "[{} #{}]", self.link_type, self.number)
}
}
@@ -55,7 +50,7 @@ fn check_brackets(line: &str) -> Result<()> {
fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
let re_standalone_link = Regex::new(r"\[([^\]]+)\]($|[^\(\[])")?;
let re_auto_link = Regex::new(r"^(Issue|PR) #([0-9]+)(?: @([a-zA-Z0-9\-]+))?$")?;
let re_auto_link = Regex::new(r"^(Discussion|Issue|PR) #([0-9]+)?$")?;
let mut links = BTreeMap::<LinkRef, String>::new();
let raw_reader = File::open(path)?;
@@ -85,22 +80,12 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
let link_ref = captures.get(0).unwrap().as_str();
let link_type = captures.get(1).unwrap().as_str();
let number: u32 = captures.get(2).unwrap().as_str().parse()?;
let user = captures.get(3).map(|c| c.as_str());
let link = match link_type {
"Issue" => {
if user.is_some() {
bail!("{link_ref} should not have a username");
}
format!("{base_url}/issues/{number}")
}
"PR" => {
if user.is_none() {
bail!("{link_ref} should have a username");
}
format!("{base_url}/pull/{number}")
}
t => bail!("Unknown link type: {t:?}"),
"Discussion" => format!("{base_url}/discussions/{number}"),
"Issue" => format!("{base_url}/issues/{number}"),
"PR" => format!("{base_url}/pull/{number}"),
t => bail!("Unknown link type in {link_ref:?}: {t:?}"),
};
// #0 is used for examples only.
@@ -109,7 +94,6 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
LinkRef {
link_type: link_type.to_owned(),
number,
user: user.map(|u| u.to_owned()),
},
link,
);
-11
View File
@@ -49,12 +49,6 @@ fn newest_child_by_name(directory: &Path) -> Result<PathBuf> {
.ok_or_else(|| anyhow!("{directory:?} has no children"))
}
fn build_empty_zip(writer: &mut dyn Write) -> Result<()> {
let mut writer = ZipWriter::new_streaming(writer);
writer.finish()?;
Ok(())
}
fn build_dex(writer: &mut dyn Write, sources: &[&Path]) -> Result<()> {
let sdk = env::var_os("ANDROID_HOME")
.map(PathBuf::from)
@@ -183,10 +177,6 @@ pub fn modules_subcommand(cli: &ModulesCli) -> Result<()> {
let (path, mut writer) = start_module(&dist_dir, &common_dir, &module_dir)?;
match module {
Module::ClearOtaCerts => {
writer.start_file("system/etc/security/otacerts.zip", FileOptions::default())?;
build_empty_zip(&mut writer)?;
}
Module::OemUnlockOnBoot => {
writer.start_file("classes.dex", FileOptions::default())?;
build_dex(&mut writer, &[&module_dir.join("Main.java")])?;
@@ -209,7 +199,6 @@ pub fn modules_subcommand(cli: &ModulesCli) -> Result<()> {
#[derive(Clone, Copy, Debug, ValueEnum)]
#[value(rename_all = "lower")]
enum Module {
ClearOtaCerts,
OemUnlockOnBoot,
}
+1 -1
View File
@@ -20,7 +20,7 @@ fn update_cargo_version(version: &str) -> Result<()> {
let data = fs::read_to_string(&path)?;
let mut document: Document = data.parse()?;
document["workspace"]["package"]["version"] = value(version.clone());
document["workspace"]["package"]["version"] = value(version);
fs::write(path, document.to_string())?;