Compare commits

..

75 Commits

Author SHA1 Message Date
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
46 changed files with 2642 additions and 1222 deletions
+3 -2
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:
+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
+65
View File
@@ -7,6 +7,48 @@
to update the actual links at the bottom of the file.
-->
### 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.
@@ -60,6 +102,7 @@ Behind-the-scenes changes:
* 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. -->
[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
@@ -67,6 +110,8 @@ Behind-the-scenes changes:
[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
[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
@@ -94,3 +139,23 @@ Behind-the-scenes changes:
[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
Generated
+366 -287
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "2.1.1"
version = "2.3.2"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+45 -25
View File
@@ -30,6 +30,8 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
* To force an image to be signed, use `--key <path> --force`.
* To force an image to be unsigned, use `--force` without specifying `--key`.
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
### Repacking an AVB image
```bash
@@ -68,7 +70,7 @@ By default, this command will not write to any file and fails if an image is cor
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
@@ -94,6 +96,48 @@ 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 cpio`
### Unpacking a cpio archive
```bash
avbroot cpio unpack -i <input cpio archive>
```
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 cpio repack -i <input cpio archive> -o <output cpio archive>
```
This is almost equivalent to running `avbroot cpio unpack` followed by `avbroot cpio pack`, except inode numbers will not be reassigned.
### Showing information about a cpio archive
```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.
@@ -150,27 +194,3 @@ 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 ramdisk`
### Dumping a cpio archive
```bash
avbroot ramdisk dump -i <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`.
### Repacking a cpio archive
```bash
avbroot ramdisk 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:
* files are sorted by name
* inodes are reassigned, starting from 300000
* there is no excess padding at the end of the file
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.
+164 -77
View File
@@ -1,49 +1,112 @@
# 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 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.
## Patches
avbroot applies two patches to the boot 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.
## 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 \
--boot-partition @gki_kernel
```
* 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,44 +119,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.
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.
## Usage
## Initial setup
1. Make sure the caveats listed above are understood. It is possible to hard brick by doing the wrong thing!
1. Reboot into fastboot mode and unlock the bootloader if it isn't already unlocked. This will trigger a data wipe.
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.
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.
3. Follow the steps to [generate signing keys](#generating-keys).
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 \
@@ -101,73 +143,108 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
--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.
If you are using KernelSU, also add `--boot-partition @gki_kernel` to the command.
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.
At least in CalyxOS, the Updater app does not respect the `Automatic system updates` setting and may enter an infinite loop downloading the OTA update and restarting the download when signature verification fails. If this happens on your ROM, you can try to either remove network access from the Updater app or disable the Updater app altogether (if your ROM allows you to do so). In CalyxOS, it is possible to go to `Settings > Apps > See all apps`, open the three-dot menu, `Show system`, then find the `System updater` app and disable it.
4. That's it! There are no other remnants to clean up.
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
## avbroot modules
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
### `clearotacerts`: Block OTA Updates from default updater app
Unpatched OTA updates are already blocked when booted into recovery mode because the original OTA certificate has been replaced with the custom certificate. However, this doesn't prevent the Android's system updater app from attempting to install an unpatched OTA update.
Disabling the system updater app is recommended. 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).
As an extra safety measure, flashing the `clearotacerts` module will intentionally make OTAs fail to install while booted into Android. It does so by overriding `/system/etc/security/otacerts.zip` with an empty zip containing no certificates so that even if an OTA is downloaded, signature verification will fail. This may cause some custom OS' system updater app to get stuck in an infinite loop downloading an OTA update and then retrying when signature verification fails, so make sure the system updater app is disabled.
As an alternative to this module, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that installs updates from a self-hosted OTA server.
### `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:
@@ -178,9 +255,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>
@@ -189,7 +266,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
```
@@ -210,6 +287,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
@@ -252,15 +331,15 @@ 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.
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, 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.
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.
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
@@ -275,7 +354,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`.
@@ -284,7 +363,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 \
@@ -339,11 +418,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
+5 -5
View File
@@ -29,7 +29,7 @@ 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"] }
@@ -37,7 +37,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"] }
@@ -45,7 +45,7 @@ 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.20.1", features = ["serde"] }
topological-sort = "0.2.2"
x509-cert = { version = "0.2.4", features = ["builder"] }
@@ -77,8 +77,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();
}
+68 -58
View File
@@ -32,7 +32,7 @@ use crate::{
avb::{self, Descriptor},
bootimage::{self, BootImage, BootImageExt, RamdiskMeta},
compression::{self, CompressedFormat, CompressedReader, CompressedWriter},
cpio::{self, CpioEntryNew},
cpio::{self, CpioEntry, CpioEntryData},
},
stream::{self, FromReader, HashingWriter, SectionReader, ToWriter},
};
@@ -73,18 +73,25 @@ pub enum 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())
@@ -110,7 +117,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..26400];
const VERS_SUPPORTED: &[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;
@@ -193,7 +200,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);
@@ -205,20 +212,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();
}
@@ -228,7 +235,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();
}
@@ -236,22 +243,20 @@ 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),
));
}
}
@@ -269,7 +274,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),
};
@@ -280,13 +285,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.
{
@@ -294,10 +297,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.
@@ -326,10 +330,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.
@@ -359,17 +365,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,
@@ -408,7 +413,10 @@ impl OtaCertPatcher {
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 {
@@ -420,12 +428,15 @@ 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 {
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)?;
@@ -441,16 +452,16 @@ 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, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(data, 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 raw_writer = Cursor::new(Vec::new());
let mut writer = ZipWriter::new(raw_writer);
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
writer.start_file("ota.x509.pem", options)?;
@@ -458,26 +469,26 @@ impl OtaCertPatcher {
crypto::write_pem_cert(&mut writer, &self.cert)?;
let raw_writer = writer.finish()?;
entry.content = raw_writer.into_inner();
entry.data = CpioEntryData::Data(raw_writer.into_inner());
}
// Repack ramdisk.
*data = save_ramdisk(&entries, ramdisk_format)?;
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
Ok(true)
}
}
impl BootImagePatcher for OtaCertPatcher {
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &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::V0Through2(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
BootImage::V3Through4(b) => self.patch_ramdisk(&mut b.ramdisk, cancel_signal)?,
BootImage::VendorV3Through4(b) => {
let mut patched = false;
for ramdisk in &mut b.ramdisks {
if self.patch_ramdisk(ramdisk)? {
if self.patch_ramdisk(ramdisk, cancel_signal)? {
patched = true;
break;
}
@@ -753,8 +764,6 @@ pub fn patch_boot(
patcher.patch(&mut boot_image, cancel_signal)?;
}
header.set_algo_for_key(key)?;
let mut descriptor_iter = header.descriptors.iter_mut().filter_map(|d| {
if let Descriptor::Hash(h) = d {
Some(h)
@@ -783,6 +792,7 @@ pub fn patch_boot(
}
if !header.public_key.is_empty() {
header.set_algo_for_key(key)?;
header.sign(key)?;
}
+3 -3
View File
@@ -8,7 +8,7 @@ use std::sync::atomic::AtomicBool;
use anyhow::Result;
use clap::{Parser, Subcommand};
use crate::cli::{avb, boot, completion, fec, key, ota, ramdisk};
use crate::cli::{avb, boot, completion, cpio, fec, key, ota};
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Subcommand)]
@@ -16,10 +16,10 @@ pub enum Command {
Avb(avb::AvbCli),
Boot(boot::BootCli),
Completion(completion::CompletionCli),
Cpio(cpio::CpioCli),
Fec(fec::FecCli),
Key(key::KeyCli),
Ota(ota::OtaCli),
Ramdisk(ramdisk::RamdiskCli),
/// (Deprecated: Use `avbroot ota patch` instead.)
Patch(ota::PatchCli),
/// (Deprecated: Use `avbroot ota extract` instead.)
@@ -42,10 +42,10 @@ pub fn main(cancel_signal: &AtomicBool) -> Result<()> {
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::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),
+155 -30
View File
@@ -28,13 +28,13 @@ use crate::{
crypto::{self, PassphraseSource},
format::avb::{
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
Header,
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
},
stream::{self, PSeekFile},
stream::{self, PSeekFile, Reopen},
util,
};
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct AvbInfo {
header: Header,
footer: Option<Footer>,
@@ -92,6 +92,20 @@ fn write_info(path: &Path, info: &AvbInfo) -> Result<()> {
Ok(())
}
/// Packing with insecure algorithms is intentionally not supported, so promote
/// to a secure algorithm if needed.
fn promote_insecure_hash_algorithm(algorithm: &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(
@@ -145,7 +159,7 @@ fn write_raw_and_verify(
let raw_file = write_raw(path, reader, copy_size, cancel_signal)?;
let result = verify_and_repair(None, raw_file.reopen(), descriptor, true, cancel_signal);
let result = verify_and_repair(None, raw_file.reopen()?, descriptor, true, cancel_signal);
// Chop off the old hash tree and FEC data.
raw_file.set_len(f.original_image_size)?;
@@ -179,15 +193,13 @@ fn write_raw_and_update(
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(
|| Ok(Box::new(raw_file.reopen())),
|| Ok(Box::new(raw_file.reopen())),
cancel_signal,
)
.context("Failed to update hash tree descriptor")?;
d.update(&raw_file, &raw_file, 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)
@@ -198,6 +210,121 @@ fn write_raw_and_update(
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
@@ -384,7 +511,7 @@ fn verify_and_repair(
AppendedDescriptorRef::HashTree(d) => {
status!("Verifying hash tree descriptor{suffix}");
match d.verify(|| Ok(Box::new(file.reopen())), cancel_signal) {
match d.verify(&file, cancel_signal) {
Err(
e @ avb::Error::InvalidRootDigest { .. }
| e @ avb::Error::InvalidHashTree { .. },
@@ -392,17 +519,12 @@ fn verify_and_repair(
warning!("Failed to verify hash tree descriptor{suffix}: {e}");
warning!("Attempting to repair using FEC data{suffix}");
d.repair(
|| Ok(Box::new(file.reopen())),
|| Ok(Box::new(file.reopen())),
cancel_signal,
)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.repair(&file, &file, cancel_signal)
.with_context(|| format!("Failed to repair data{suffix}"))?;
d.verify(|| Ok(Box::new(file.reopen())), cancel_signal)
.map(|_| {
status!("Successfully repaired data{suffix}");
})
d.verify(&file, cancel_signal).map(|_| {
status!("Successfully repaired data{suffix}");
})
}
ret => ret,
}
@@ -490,7 +612,11 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
format!("Failed to open raw image for reading: {:?}", cli.input_raw)
})?;
write_raw_and_update(&cli.output, &mut reader, &mut info, cancel_signal)?
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)
@@ -516,15 +642,14 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
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.
// There could have been errors in the original FEC data itself.
if let AppendedDescriptorMut::HashTree(d) = info.header.appended_descriptor_mut()? {
d.update(
|| Ok(Box::new(file.reopen())),
|| Ok(Box::new(file.reopen())),
cancel_signal,
)?;
d.hash_algorithm = promote_insecure_hash_algorithm(&d.hash_algorithm).to_owned();
d.update(&file, &file, cancel_signal)?;
}
update_dm_verity_cmdline(&mut info)?;
file
} else {
File::create(&cli.output)
@@ -664,7 +789,7 @@ struct UnpackCli {
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
output_raw: PathBuf,
// Ignore invalid digests or FEC data.
/// Ignore invalid digests or FEC data.
#[arg(long)]
ignore_invalid: bool,
@@ -782,7 +907,7 @@ enum AvbCommand {
Verify(VerifyCli),
}
/// Show information about AVB-protected images.
/// Pack, unpack, and inspect AVB-protected images.
#[derive(Debug, Parser)]
pub struct AvbCli {
#[command(subcommand)]
+13 -9
View File
@@ -13,7 +13,7 @@ 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},
};
@@ -300,12 +300,16 @@ pub fn magisk_info_subcommand(cli: &MagiskInfoCli) -> Result<()> {
let reader = Cursor::new(ramdisk);
let reader = CompressedReader::new(reader, true)
.with_context(|| format!("Failed to load ramdisk #{i}"))?;
let entries = cpio::load(reader, false)
.with_context(|| format!("Failed to load ramdisk #{i} cpio"))?;
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(());
}
}
}
@@ -330,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.
@@ -390,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.
@@ -479,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,
}
+4 -8
View File
@@ -53,7 +53,7 @@ fn write_fec(path: &Path, fec: &FecImage) -> Result<()> {
fn generate_subcommand(cli: &GenerateCli, cancel_signal: &AtomicBool) -> Result<()> {
let input = open_input(&cli.input, false)?;
let fec = FecImage::generate(|| Ok(Box::new(input.reopen())), cli.parity, cancel_signal)
let fec = FecImage::generate(&input, cli.parity, cancel_signal)
.context("Failed to generate FEC data")?;
write_fec(&cli.fec, &fec)?;
@@ -65,7 +65,7 @@ fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<()>
let input = open_input(&cli.input, false)?;
let fec = read_fec(&cli.fec)?;
fec.verify(|| Ok(Box::new(input.reopen())), cancel_signal)
fec.verify(&input, cancel_signal)
.context("Failed to verify data")?;
Ok(())
@@ -78,12 +78,8 @@ fn repair_subcommand(cli: &RepairCli, cancel_signal: &AtomicBool) -> Result<()>
// 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(
|| Ok(Box::new(input.reopen())),
|| Ok(Box::new(input.reopen())),
cancel_signal,
)
.context("Failed to repair file")?;
fec.repair(&input, &input, cancel_signal)
.context("Failed to repair file")?;
Ok(())
}
+1 -1
View File
@@ -7,10 +7,10 @@ pub mod args;
pub mod avb;
pub mod boot;
pub mod completion;
pub mod cpio;
pub mod fec;
pub mod key;
pub mod ota;
pub mod ramdisk;
macro_rules! status {
($($arg:tt)*) => {
+304 -132
View File
@@ -9,7 +9,7 @@ use std::{
ffi::{OsStr, OsString},
fmt::Display,
fs::{self, File},
io::{self, BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
sync::{atomic::AtomicBool, Mutex},
time::Instant,
@@ -43,8 +43,8 @@ use crate::{
build::tools::releasetools::OtaMetadata, chromeos_update_engine::DeltaArchiveManifest,
},
stream::{
self, CountingWriter, FromReader, HolePunchingWriter, PSeekFile, ReadSeek, SectionReader,
ToWriter,
self, CountingWriter, FromReader, HolePunchingWriter, PSeekFile, ReadSeek, ReadSeekReopen,
Reopen, SectionReader, ToWriter,
},
util,
};
@@ -59,12 +59,19 @@ static PARTITION_PRIORITIES: phf::Map<&'static str, &[&'static str]> = phf_map!
};
fn joined(into_iter: impl IntoIterator<Item = impl Display>) -> String {
let items = into_iter
.into_iter()
.map(|i| i.to_string())
.collect::<Vec<_>>();
use std::fmt::Write;
items.join(", ")
let mut result = String::new();
for (i, item) in into_iter.into_iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
write!(result, "{item}").expect("Failed to allocate");
}
result
}
fn sorted<T: Ord>(iter: impl Iterator<Item = T>) -> Vec<T> {
@@ -142,7 +149,7 @@ pub fn get_required_images(
/// in `external_images`, the real file on the filesystem is opened. Otherwise,
/// the image is extracted from the payload.
fn open_input_streams(
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
payload: &(dyn ReadSeekReopen + Sync),
required_images: &HashMap<String, String>,
external_images: &HashMap<String, PathBuf>,
header: &PayloadHeader,
@@ -167,9 +174,8 @@ fn open_input_streams(
} else {
status!("Extracting from original payload: {name}");
let stream =
payload::extract_image_to_memory(&open_payload, header, name, cancel_signal)
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
let stream = payload::extract_image_to_memory(payload, header, name, cancel_signal)
.with_context(|| format!("Failed to extract from original payload: {name}"))?;
input_streams.insert(name.clone(), Box::new(stream));
}
}
@@ -236,16 +242,14 @@ fn patch_boot_images(
Ok(())
}
/// From the set of input images (modified partitions + all vbmeta partitions)
/// and determine the order to patch the vbmeta images so that it can be done in
/// a single pass.
fn get_vbmeta_patch_order(
/// Load the specified vbmeta image headers. If an image has a vbmeta footer,
/// then an error is returned because the vbmeta patching logic only ever writes
/// root vbmeta images.
fn load_vbmeta_images(
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
vbmeta_images: &HashSet<String>,
) -> Result<Vec<(String, Header, HashSet<String>)>> {
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
let mut headers = HashMap::<&str, Header>::new();
let mut missing = images.keys().cloned().collect::<BTreeSet<_>>();
) -> Result<HashMap<String, Header>> {
let mut result = HashMap::new();
for name in vbmeta_images {
let reader = images.get_mut(name).unwrap();
@@ -253,9 +257,83 @@ fn get_vbmeta_patch_order(
.with_context(|| format!("Failed to load vbmeta image: {name}"))?;
if let Some(f) = footer {
warning!("{name} is a vbmeta partition, but has a footer: {f:?}");
bail!("{name} is a vbmeta partition, but has a footer: {f:?}");
}
result.insert(name.clone(), header);
}
Ok(result)
}
/// Check if a partition is critical to AVB's chain of trust. This is not
/// foolproof and uses a heuristic based on AOSP's boot process. OEM-specific
/// partitions may be equally important, but it's infeasible to list them all.
fn is_critical_to_avb(name: &str) -> bool {
name.ends_with("boot")
|| name.starts_with("odm")
|| name.starts_with("system")
|| name.starts_with("vbmeta")
|| name.starts_with("vendor")
|| name == "dtbo"
|| name == "product"
|| name == "pvmfw"
|| name == "recovery"
}
/// Check that all critical partitions within the payload are protected by a
/// vbmeta image in `vbmeta_headers`.
fn ensure_partitions_protected(
manifest: &DeltaArchiveManifest,
vbmeta_headers: &HashMap<String, Header>,
) -> Result<()> {
let critical_partitions = manifest
.partitions
.iter()
.map(|p| &p.partition_name)
.filter(|n| is_critical_to_avb(n))
.cloned()
.collect::<BTreeSet<_>>();
// vbmeta partitions first.
let mut avb_partitions = vbmeta_headers.keys().cloned().collect::<BTreeSet<_>>();
// Then, everything referred to by the descriptors.
for header in vbmeta_headers.values() {
let partition_names = header
.descriptors
.iter()
.filter_map(|d| d.partition_name())
.map(|n| n.to_owned());
avb_partitions.extend(partition_names);
}
let missing = critical_partitions
.difference(&avb_partitions)
.collect::<Vec<_>>();
if !missing.is_empty() {
bail!(
"Found critical partitions that are not protected by AVB: {}",
joined(missing),
);
}
Ok(())
}
/// From the set of input images (modified partitions + all vbmeta partitions),
/// determine the order to patch the vbmeta images so that it can be done in a
/// single pass.
fn get_vbmeta_patch_order(
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
vbmeta_headers: &HashMap<String, Header>,
) -> Result<Vec<(String, HashSet<String>)>> {
let mut dep_graph = HashMap::<&str, HashSet<String>>::new();
let mut missing = images.keys().cloned().collect::<BTreeSet<_>>();
for (name, header) in vbmeta_headers {
dep_graph.insert(name, HashSet::new());
missing.remove(name);
@@ -273,34 +351,31 @@ fn get_vbmeta_patch_order(
missing.remove(partition_name);
}
}
headers.insert(name, header);
}
if !missing.is_empty() {
warning!("Partitions aren't protected by AVB: {:?}", joined(missing));
}
// Prune vbmeta images we don't need.
loop {
let unneeded = dep_graph
.iter()
.find(|(_, d)| d.is_empty())
.map(|(&n, _)| n.to_owned());
match unneeded {
Some(name) => {
dep_graph.remove(name.as_str());
headers.remove(name.as_str());
// Ensure that there's only a single root of trust. Otherwise, there could
// be eg. a `vbmeta_unused` containing all the relevant descriptors, but is
// never loaded by the bootloader.
let mut roots = BTreeSet::new();
for deps in dep_graph.values_mut() {
deps.remove(name.as_str());
}
}
None => break,
for name in vbmeta_headers.keys() {
if !dep_graph.values().any(|d| d.contains(name)) {
roots.insert(name.as_str());
}
}
// Compute the patching order. This only includes vbmeta images.
// For zero roots, let TopologicalSort report the cycle.
if roots.len() > 1 {
bail!("Found multiple root vbmeta images: {}", joined(roots));
}
// Compute the patching order. This only includes vbmeta images. All vbmeta
// images are included (even those that have no dependencies) so that
// update_vbmeta_headers() can check and update the flags field if needed.
let mut topo = TopologicalSort::<String>::new();
let mut order = vec![];
@@ -313,13 +388,9 @@ fn get_vbmeta_patch_order(
while !topo.is_empty() {
match topo.pop() {
Some(item) => {
// Only include vbmeta images that we need to modify.
if headers.contains_key(item.as_str()) {
order.push((
item.clone(),
headers.remove(item.as_str()).unwrap(),
dep_graph.remove(item.as_str()).unwrap(),
));
// Only include vbmeta images.
if dep_graph.contains_key(item.as_str()) {
order.push((item.clone(), dep_graph.remove(item.as_str()).unwrap()));
}
}
None => bail!("vbmeta dependency graph has cycle: {topo:?}"),
@@ -329,26 +400,43 @@ fn get_vbmeta_patch_order(
Ok(order)
}
/// Update vbmeta descriptors based on the footers from the specified images and
/// then re-sign the vbmeta images.
fn update_vbmeta_descriptors(
/// Update vbmeta headers.
///
/// * If [`Header::flags`] is non-zero, then an error is returned because the
/// value renders AVB useless. If `clear_vbmeta_flags` is set to true, then
/// the value is set to 0 instead.
/// * [`Header::descriptors`] is updated for each dependency listed in `order`.
/// * [`Header::algorithm_type`] is updated with an algorithm type that matches
/// `key`. This is not a factor when determining if a header is changed.
///
/// If changes were made to a vbmeta header, then the image in `images` will be
/// replaced with a new in-memory reader containing the new image. Otherwise,
/// the image is removed from `images` entirely to avoid needing to repack it.
fn update_vbmeta_headers(
images: &mut HashMap<String, Box<dyn ReadSeek + Send>>,
order: &mut [(String, Header, HashSet<String>)],
headers: &mut HashMap<String, Header>,
order: &mut [(String, HashSet<String>)],
clear_vbmeta_flags: bool,
key: &RsaPrivateKey,
block_size: u64,
) -> Result<()> {
for (name, parent_header, deps) in order {
let mut unchanged = HashSet::new();
for (name, deps) in order {
let parent_header = headers.get_mut(name).unwrap();
let orig_parent_header = parent_header.clone();
if parent_header.flags != 0 {
if clear_vbmeta_flags {
parent_header.flags = 0;
} else {
bail!("{name} header flags disable AVB {:#x}", parent_header.flags);
bail!(
"Verified boot is disabled by {name}'s header flags: {:#x}",
parent_header.flags,
);
}
}
parent_header.set_algo_for_key(key)?;
for dep in deps.iter() {
// This can't fail since the descriptor must have existed for the
// dependency to exist.
@@ -361,6 +449,7 @@ fn update_vbmeta_descriptors(
let reader = images.get_mut(dep).unwrap();
let (header, _, _) = avb::load_image(reader)
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
let pd_type = parent_descriptor.type_name();
if header.public_key.is_empty() {
// vbmeta is unsigned. Use the existing descriptor.
@@ -371,6 +460,7 @@ fn update_vbmeta_descriptors(
else {
bail!("{name} has no descriptor for itself");
};
let d_type = descriptor.type_name();
match (parent_descriptor, descriptor) {
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
@@ -380,7 +470,7 @@ fn update_vbmeta_descriptors(
*pd = d.clone();
}
_ => {
bail!("{name}'s descriptor for {dep} must match {dep}'s self descriptor");
bail!("{dep} descriptor ({d_type}) does not match entry in {name} ({pd_type})");
}
}
} else {
@@ -390,25 +480,38 @@ fn update_vbmeta_descriptors(
d.public_key = header.public_key;
}
_ => {
bail!("{name}'s descriptor for {dep} must be a chain descriptor");
bail!("{dep} descriptor ({pd_type}) in {name} must be a chain descriptor");
}
}
}
}
parent_header
.sign(key)
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
// Only sign and rewrite the image if we need to. Some vbmeta images may
// have no dependencies and are only being processed to ensure that the
// flags are set to a sane value.
if parent_header != &orig_parent_header {
parent_header.set_algo_for_key(key)?;
parent_header
.sign(key)
.with_context(|| format!("Failed to sign vbmeta header for image: {name}"))?;
let mut writer = Cursor::new(Vec::new());
parent_header
.to_writer(&mut writer)
.with_context(|| format!("Failed to write vbmeta image: {name}"))?;
let mut writer = Cursor::new(Vec::new());
parent_header
.to_writer(&mut writer)
.with_context(|| format!("Failed to write vbmeta image: {name}"))?;
padding::write_zeros(&mut writer, block_size)
.with_context(|| format!("Failed to write vbmeta padding: {name}"))?;
padding::write_zeros(&mut writer, block_size)
.with_context(|| format!("Failed to write vbmeta padding: {name}"))?;
*images.get_mut(name).unwrap() = Box::new(writer);
*images.get_mut(name).unwrap() = Box::new(writer);
} else {
unchanged.insert(name.as_str());
}
}
// No need to package a replacement image if it's unchanged.
for name in unchanged {
images.remove(name);
}
Ok(())
@@ -445,7 +548,7 @@ fn compress_image(
#[allow(clippy::too_many_arguments)]
fn patch_ota_payload(
open_payload: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
payload: &(dyn ReadSeekReopen + Sync),
writer: impl Write,
external_images: &HashMap<String, PathBuf>,
boot_partition: &str,
@@ -456,8 +559,8 @@ fn patch_ota_payload(
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(String, u64)> {
let header =
PayloadHeader::from_reader(open_payload()?).context("Failed to load OTA payload header")?;
let header = PayloadHeader::from_reader(payload.reopen_boxed()?)
.context("Failed to load OTA payload header")?;
if !header.is_full_ota() {
bail!("Payload is a delta OTA, not a full OTA");
}
@@ -499,7 +602,7 @@ fn patch_ota_payload(
// from the old payload). The values will be replaced later if the images
// need to be patched (eg. boot or vbmeta image).
let mut input_streams = open_input_streams(
&open_payload,
payload,
&required_images,
external_images,
&header_locked,
@@ -515,11 +618,15 @@ fn patch_ota_payload(
cancel_signal,
)?;
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_streams, &vbmeta_images)?;
let mut vbmeta_headers = load_vbmeta_images(&mut input_streams, &vbmeta_images)?;
ensure_partitions_protected(&header_locked.manifest, &vbmeta_headers)?;
let mut vbmeta_order = get_vbmeta_patch_order(&mut input_streams, &vbmeta_headers)?;
status!(
"Patching vbmeta images: {}",
joined(vbmeta_order.iter().map(|(n, _, _)| n)),
joined(vbmeta_order.iter().map(|(n, _)| n)),
);
// Get rid of input readers for vbmeta partitions we don't need to modify.
@@ -530,12 +637,13 @@ fn patch_ota_payload(
}
}
update_vbmeta_descriptors(
update_vbmeta_headers(
&mut input_streams,
&mut vbmeta_headers,
&mut vbmeta_order,
clear_vbmeta_flags,
key_avb,
header_locked.manifest.block_size.into(),
header_locked.manifest.block_size().into(),
)?;
status!(
@@ -543,7 +651,7 @@ fn patch_ota_payload(
joined(sorted(input_streams.keys())),
);
let block_size = header_locked.manifest.block_size;
let block_size = header_locked.manifest.block_size();
drop(header_locked);
input_streams
@@ -559,7 +667,7 @@ fn patch_ota_payload(
let header_locked = header.lock().unwrap();
let mut payload_writer = PayloadWriter::new(writer, header_locked.clone(), key_ota.clone())
.context("Failed to write payload header")?;
let mut orig_payload_reader = open_payload().context("Failed to open payload")?;
let mut orig_payload_reader = payload.reopen_boxed().context("Failed to open payload")?;
while payload_writer
.begin_next_operation()
@@ -628,12 +736,7 @@ fn patch_ota_zip(
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(OtaMetadata, u64)> {
let mut missing = BTreeSet::from([
ota::PATH_METADATA_PB,
ota::PATH_OTACERT,
ota::PATH_PAYLOAD,
ota::PATH_PROPERTIES,
]);
let mut missing = BTreeSet::from([ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES]);
// Keep in sorted order for reproducibility and to guarantee that the
// payload is processed before its properties file.
@@ -648,9 +751,15 @@ fn patch_ota_zip(
if !missing.is_empty() {
bail!("Missing entries in OTA zip: {:?}", joined(missing));
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
bail!(
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
ota::PATH_METADATA,
ota::PATH_METADATA_PB,
)
}
let mut metadata_pb_raw = None;
let mut metadata = None;
let mut properties = None;
let mut payload_metadata_size = None;
let mut entries = vec![];
@@ -671,19 +780,33 @@ fn patch_ota_zip(
.compression_method(CompressionMethod::Stored)
.large_file(use_zip64);
// Processed at the end after all other entries are written.
match path.as_str() {
// Convert legacy metadata from Android 11 to the modern protobuf
// structure. Note that although we can read legacy-only OTAs, we
// always produce both the legacy and protobuf representations in
// the output.
ota::PATH_METADATA => {
// Ignore because the plain-text legacy metadata file is
// regenerated from the new protobuf metadata.
let mut buf = String::new();
reader
.read_to_string(&mut buf)
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
metadata = Some(
ota::parse_legacy_metadata(&buf)
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
);
continue;
}
// This takes precedence due to sorted iteration order.
ota::PATH_METADATA_PB => {
// Processed at the end after all other entries are written.
let mut buf = vec![];
reader
.read_to_end(&mut buf)
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
metadata_pb_raw = Some(buf);
metadata = Some(
ota::parse_protobuf_metadata(&buf)
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
);
continue;
}
_ => {}
@@ -713,19 +836,16 @@ fn patch_ota_zip(
bail!("{path} is not stored uncompressed");
}
let payload_offset = reader.data_start();
let payload_size = reader.size();
// The zip library doesn't provide us with a seekable reader, so
// we make our own from the underlying file.
let payload_reader = SectionReader::new(
BufReader::new(raw_reader.reopen()?),
reader.data_start(),
reader.size(),
)?;
let (p, m) = patch_ota_payload(
|| {
// The zip library doesn't provide us with a seekable
// reader, so we make our own from the underlying file.
Ok(Box::new(SectionReader::new(
BufReader::new(raw_reader.reopen()),
payload_offset,
payload_size,
)?))
},
&payload_reader,
&mut writer,
external_images,
boot_partition,
@@ -778,7 +898,7 @@ fn patch_ota_zip(
zip_writer,
// Offset where next entry would begin.
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
&metadata_pb_raw.unwrap(),
&metadata.unwrap(),
payload_metadata_size.unwrap(),
)
.context("Failed to write new OTA metadata")?;
@@ -816,18 +936,18 @@ fn extract_ota_zip(
})
.collect::<Result<HashMap<_, _>>>()?;
let payload_reader = SectionReader::new(
BufReader::new(raw_reader.reopen()?),
payload_offset,
payload_size,
)?;
// Extract the images. Each time we're asked to open a new file, we just
// clone the relevant PSeekFile. We only ever have one actual kernel file
// descriptor for each file.
payload::extract_images(
|| {
Ok(Box::new(SectionReader::new(
BufReader::new(raw_reader.reopen()),
payload_offset,
payload_size,
)?))
},
|name| Ok(Box::new(BufWriter::new(output_files[name].reopen()))),
&payload_reader,
|name| Ok(Box::new(BufWriter::new(output_files[name].reopen()?))),
header,
images.iter().map(|n| n.as_str()),
cancel_signal,
@@ -913,7 +1033,7 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
let raw_reader = File::open(&cli.input)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
// Open the output file for reading too, so we can verify offsets later.
@@ -1002,7 +1122,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
let raw_reader = File::open(&cli.input)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for reading: {:?}", cli.input))?;
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()))
let mut zip = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
.with_context(|| format!("Failed to read zip: {:?}", cli.input))?;
let payload_entry = zip
.by_name(ota::PATH_PAYLOAD)
@@ -1012,7 +1132,7 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
// Open the payload data directly.
let mut payload_reader = SectionReader::new(
BufReader::new(raw_reader.reopen()),
BufReader::new(raw_reader.reopen()?),
payload_offset,
payload_size,
)
@@ -1147,7 +1267,7 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
.with_context(|| format!("Failed to read boot image: {path:?}"))?
};
let ramdisk_certs = OtaCertPatcher::get_certificates(&boot_image)
let ramdisk_certs = OtaCertPatcher::get_certificates(&boot_image, cancel_signal)
.context("Failed to read ramdisk's otacerts.zip")?;
if !ramdisk_certs.contains(&ota_cert) {
bail!("Ramdisk's otacerts.zip does not contain OTA certificate");
@@ -1195,19 +1315,25 @@ pub fn ota_main(cli: &OtaCli, cancel_signal: &AtomicBool) -> Result<()> {
// inside a group: https://github.com/clap-rs/clap/issues/4707. Even if that
// were fixed, the former option's error message is much more user friendly.
const HEADING_PATH: &str = "Path options";
const HEADING_KEY: &str = "Key options";
const HEADING_MAGISK: &str = "Magisk patch options";
const HEADING_PREPATCHED: &str = "Prepatched boot image options";
const HEADING_OTHER: &str = "Other patch options";
#[derive(Debug, Args)]
#[group(required = true, multiple = false)]
pub struct RootGroup {
/// Path to Magisk APK.
#[arg(long, value_name = "FILE", value_parser)]
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_MAGISK)]
pub magisk: Option<PathBuf>,
/// Path to prepatched boot image.
#[arg(long, value_name = "FILE", value_parser)]
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_PREPATCHED)]
pub prepatched: Option<PathBuf>,
/// Skip applying root patch.
#[arg(long)]
#[arg(long, help_heading = HEADING_OTHER)]
pub rootless: bool,
}
@@ -1215,23 +1341,35 @@ pub struct RootGroup {
#[derive(Debug, Parser)]
pub struct PatchCli {
/// Patch to original OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
pub input: PathBuf,
/// Path to new OTA zip.
#[arg(short, long, value_name = "FILE", value_parser)]
#[arg(short, long, value_name = "FILE", value_parser, help_heading = HEADING_PATH)]
pub output: Option<PathBuf>,
/// Private key for signing vbmeta images.
#[arg(long, alias = "privkey-avb", value_name = "FILE", value_parser)]
#[arg(
long,
alias = "privkey-avb",
value_name = "FILE",
value_parser,
help_heading = HEADING_KEY
)]
pub key_avb: PathBuf,
/// Private key for signing the OTA.
#[arg(long, alias = "privkey-ota", value_name = "FILE", value_parser)]
#[arg(
long,
alias = "privkey-ota",
value_name = "FILE",
value_parser,
help_heading = HEADING_KEY
)]
pub key_ota: PathBuf,
/// Certificate for OTA signing key.
#[arg(long, value_name = "FILE", value_parser)]
#[arg(long, value_name = "FILE", value_parser, help_heading = HEADING_KEY)]
pub cert_ota: PathBuf,
/// Environment variable containing AVB private key passphrase.
@@ -1240,7 +1378,8 @@ pub struct PatchCli {
alias = "passphrase-avb-env-var",
value_name = "ENV_VAR",
value_parser,
group = "pass_avb"
group = "pass_avb",
help_heading = HEADING_KEY
)]
pub pass_avb_env_var: Option<OsString>,
@@ -1250,7 +1389,8 @@ pub struct PatchCli {
alias = "passphrase-avb-file",
value_name = "FILE",
value_parser,
group = "pass_avb"
group = "pass_avb",
help_heading = HEADING_KEY
)]
pub pass_avb_file: Option<PathBuf>,
@@ -1260,7 +1400,8 @@ pub struct PatchCli {
alias = "passphrase-ota-env-var",
value_name = "ENV_VAR",
value_parser,
group = "pass_ota"
group = "pass_ota",
help_heading = HEADING_KEY
)]
pub pass_ota_env_var: Option<OsString>,
@@ -1270,39 +1411,70 @@ pub struct PatchCli {
alias = "passphrase-ota-file",
value_name = "FILE",
value_parser,
group = "pass_ota"
group = "pass_ota",
help_heading = HEADING_KEY
)]
pub pass_ota_file: Option<PathBuf>,
/// Use partition image from a file instead of the original payload.
#[arg(long, value_names = ["PARTITION", "FILE"], value_parser = value_parser!(OsString), num_args = 2)]
#[arg(
long,
value_names = ["PARTITION", "FILE"],
value_parser = value_parser!(OsString),
num_args = 2,
help_heading = HEADING_PATH,
)]
pub replace: Vec<OsString>,
#[command(flatten)]
pub root: RootGroup,
/// Magisk preinit block device.
#[arg(long, value_name = "PARTITION", conflicts_with_all = ["prepatched", "rootless"])]
/// Magisk preinit block device (version >=25211 only).
#[arg(
long,
value_name = "PARTITION",
conflicts_with_all = ["prepatched", "rootless"],
help_heading = HEADING_MAGISK
)]
pub magisk_preinit_device: Option<String>,
/// Magisk random seed.
#[arg(long, value_name = "NUMBER", conflicts_with_all = ["prepatched", "rootless"])]
/// Magisk random seed (version >=25211, <26103 only).
#[arg(
long,
value_name = "NUMBER",
conflicts_with_all = ["prepatched", "rootless"],
help_heading = HEADING_MAGISK
)]
pub magisk_random_seed: Option<u64>,
/// Ignore Magisk compatibility/version warnings.
#[arg(long, conflicts_with_all = ["prepatched", "rootless"])]
#[arg(
long,
conflicts_with_all = ["prepatched", "rootless"],
help_heading = HEADING_MAGISK
)]
pub ignore_magisk_warnings: bool,
/// Ignore compatibility issues with prepatched boot images.
#[arg(long, action = ArgAction::Count, conflicts_with_all = ["magisk", "rootless"])]
#[arg(
long,
action = ArgAction::Count,
conflicts_with_all = ["magisk", "rootless"],
help_heading = HEADING_PREPATCHED
)]
pub ignore_prepatched_compat: u8,
/// Forcibly clear vbmeta flags if they disable AVB.
#[arg(long)]
#[arg(long, help_heading = HEADING_OTHER)]
pub clear_vbmeta_flags: bool,
/// Boot partition name.
#[arg(long, value_name = "PARTITION", default_value = "@gki_ramdisk")]
#[arg(
long,
value_name = "PARTITION",
default_value = "@gki_ramdisk",
help_heading = HEADING_OTHER
)]
pub boot_partition: String,
}
-150
View File
@@ -1,150 +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},
};
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!("{}", data[..512].escape_ascii());
println!("{BINARY_END_TRUNCATED}");
} else {
println!("{}", data.escape_ascii());
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,
}
+48 -27
View File
@@ -28,8 +28,8 @@ use crate::{
padding,
},
stream::{
self, CountingReader, FromReader, ReadDiscardExt, ReadSeek, ReadStringExt, ToWriter,
WriteSeek, WriteStringExt, WriteZerosExt,
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
WriteSeekReopen, WriteStringExt, WriteZerosExt,
},
util,
};
@@ -120,8 +120,9 @@ pub enum Error {
type Result<T> = std::result::Result<T, Error>;
fn ring_algorithm(name: &str) -> Result<&'static Algorithm> {
fn ring_algorithm(name: &str, for_verify: bool) -> Result<&'static Algorithm> {
match name {
"sha1" if for_verify => Ok(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY),
"sha256" => Ok(&ring::digest::SHA256),
"sha512" => Ok(&ring::digest::SHA512),
a => Err(Error::UnsupportedHashAlgorithm(a.to_owned())),
@@ -378,6 +379,9 @@ impl fmt::Debug for HashTreeDescriptor {
}
impl HashTreeDescriptor {
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
pub const FLAG_CHECK_AT_MOST_ONCE: u32 = 1 << 1;
/// 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
@@ -393,7 +397,7 @@ impl HashTreeDescriptor {
cancel_signal: &AtomicBool,
) -> io::Result<Vec<u8>> {
// Each digest must be a power of 2.
let digest_padding = algorithm.output_len.next_power_of_two() - algorithm.output_len;
let digest_padding = algorithm.output_len().next_power_of_two() - algorithm.output_len();
let mut buf = vec![0u8; block_size as usize];
let mut result = vec![];
@@ -427,7 +431,7 @@ impl HashTreeDescriptor {
///
/// NOTE: The result is **not** padded to the block size.
fn hash_one_level_parallel(
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
block_size: u32,
algorithm: &'static Algorithm,
@@ -449,7 +453,7 @@ impl HashTreeDescriptor {
let start = c * chunk_size;
let size = chunk_size.min(image_size - start);
let mut reader = open_input()?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(start))?;
Self::hash_one_level(reader, size, block_size, algorithm, salt, cancel_signal)
@@ -461,7 +465,7 @@ impl HashTreeDescriptor {
/// Calculate the hash tree for the given input in parallel.
fn calculate_hash_tree(
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
image_size: u64,
block_size: u32,
algorithm: &'static Algorithm,
@@ -470,7 +474,7 @@ impl HashTreeDescriptor {
) -> io::Result<(Vec<u8>, Vec<u8>)> {
// Small files are hashed directly, exactly like a hash descriptor.
if image_size <= u64::from(block_size) {
let mut reader = open_input()?;
let mut reader = input.reopen_boxed()?;
let mut buf = vec![0u8; block_size as usize];
reader.read_exact(&mut buf)?;
@@ -500,7 +504,7 @@ impl HashTreeDescriptor {
} else {
// Initially read from file.
Self::hash_one_level_parallel(
&open_input,
input,
level_size,
block_size,
algorithm,
@@ -590,13 +594,13 @@ impl HashTreeDescriptor {
/// original state by truncating it to [`Self::image_size`].
pub fn update(
&mut self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let algorithm = ring_algorithm(&self.hash_algorithm)?;
let algorithm = ring_algorithm(&self.hash_algorithm, false)?;
let (root_digest, hash_tree) = Self::calculate_hash_tree(
&open_input,
input,
self.image_size,
self.data_block_size,
algorithm,
@@ -610,7 +614,7 @@ impl HashTreeDescriptor {
let tree_size = hash_tree.len() as u64;
let mut writer = open_output()?;
let mut writer = output.reopen_boxed()?;
writer.seek(SeekFrom::Start(self.image_size))?;
writer
.write_all(&hash_tree)
@@ -633,7 +637,7 @@ impl HashTreeDescriptor {
// The FEC covers the hash tree as well.
let fec = Fec::new(self.image_size + tree_size, self.data_block_size, parity)?;
let fec_data = fec.generate(open_input, cancel_signal)?;
let fec_data = fec.generate(input, cancel_signal)?;
let fec_size = fec_data
.len()
.to_u64()
@@ -660,19 +664,19 @@ impl HashTreeDescriptor {
/// handles to the same file.
pub fn verify(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
self.check_offsets()?;
let algorithm = ring_algorithm(&self.hash_algorithm)?;
let algorithm = ring_algorithm(&self.hash_algorithm, true)?;
if self.tree_size > HASH_TREE_MAX_SIZE {
return Err(Error::FieldOutOfBounds("tree_size"));
}
let (actual_root_digest, actual_hash_tree) = Self::calculate_hash_tree(
&open_input,
input,
self.image_size,
self.data_block_size,
algorithm,
@@ -687,7 +691,7 @@ impl HashTreeDescriptor {
});
}
let mut reader = open_input()?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(self.tree_offset))?;
let mut hash_tree = vec![0u8; self.tree_size as usize];
@@ -716,7 +720,7 @@ impl HashTreeDescriptor {
.read_exact(&mut fec_data)
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
fec.verify(open_input, &fec_data, cancel_signal)?;
fec.verify(input, &fec_data, cancel_signal)?;
}
Ok(())
@@ -732,8 +736,8 @@ impl HashTreeDescriptor {
/// actually valid.
pub fn repair(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
self.check_offsets()?;
@@ -743,7 +747,7 @@ impl HashTreeDescriptor {
return Err(Error::FecMissing);
}
let mut reader = open_input()?;
let mut reader = input.reopen_boxed()?;
reader.seek(SeekFrom::Start(self.fec_offset))?;
let (fec, fec_size) = self.get_fec()?;
@@ -754,7 +758,7 @@ impl HashTreeDescriptor {
.read_exact(&mut fec_data)
.map_err(|e| Error::ReadFieldError("fec_data", e))?;
fec.repair(open_input, open_output, &fec_data, cancel_signal)?;
fec.repair(input, output, &fec_data, cancel_signal)?;
Ok(())
}
@@ -911,9 +915,10 @@ impl HashDescriptor {
fn calculate(
&self,
reader: impl Read,
for_verify: bool,
cancel_signal: &AtomicBool,
) -> Result<ring::digest::Digest> {
let algorithm = ring_algorithm(&self.hash_algorithm)?;
let algorithm = ring_algorithm(&self.hash_algorithm, for_verify)?;
let mut context = Context::new(algorithm);
context.update(&self.salt);
@@ -930,14 +935,14 @@ impl HashDescriptor {
/// Update the root hash from the input reader's contents.
pub fn update(&mut self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
let digest = self.calculate(reader, cancel_signal)?;
let digest = self.calculate(reader, false, cancel_signal)?;
self.root_digest = digest.as_ref().to_vec();
Ok(())
}
/// Verify the root hash against the input reader.
pub fn verify(&self, reader: impl Read, cancel_signal: &AtomicBool) -> Result<()> {
let digest = self.calculate(reader, cancel_signal)?;
let digest = self.calculate(reader, true, cancel_signal)?;
if self.root_digest != digest.as_ref() {
return Err(Error::InvalidRootDigest {
@@ -1051,6 +1056,11 @@ pub struct KernelCmdlineDescriptor {
pub cmdline: String,
}
impl KernelCmdlineDescriptor {
pub const FLAG_USE_ONLY_IF_HASHTREE_NOT_DISABLED: u32 = 1 << 0;
pub const FLAG_USE_ONLY_IF_HASHTREE_DISABLED: u32 = 1 << 1;
}
impl DescriptorTag for KernelCmdlineDescriptor {
const TAG: u64 = 3;
}
@@ -1191,6 +1201,17 @@ pub enum Descriptor {
}
impl Descriptor {
pub fn type_name(&self) -> &'static str {
match self {
Self::Property(_) => "Property",
Self::HashTree(_) => "HashTree",
Self::Hash(_) => "Hash",
Self::KernelCmdline(_) => "KernelCmdline",
Self::ChainPartition(_) => "ChainPartition",
Self::Unknown { .. } => "Unknown",
}
}
pub fn partition_name(&self) -> Option<&str> {
match self {
Self::HashTree(d) => Some(&d.partition_name),
+1
View File
@@ -1255,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),
+2 -1
View File
@@ -8,6 +8,7 @@ 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;
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
@@ -96,7 +97,7 @@ 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,
+544 -164
View File
@@ -4,18 +4,25 @@
*/
use std::{
borrow::Cow,
collections::{HashMap, HashSet},
fmt,
io::{self, Read, Write},
io::{self, Cursor, Read, Write},
ops::Range,
sync::atomic::AtomicBool,
};
use bstr::ByteSlice;
use num_traits::ToPrimitive;
use num_traits::{ToPrimitive, Zero};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
escape,
format::padding,
stream::{CountingReader, CountingWriter, FromReader, ToWriter, WriteZerosExt},
octal,
stream::{
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt,
},
util::NumBytes,
};
@@ -35,12 +42,9 @@ const C_ISCTG: u32 = 0o110000;
const IO_BLOCK_SIZE: u64 = 512;
/// The threshold when parsing an entry's filename where memory allocation
/// switches from allocating the exact size to resizing as necessary.
const REALLOC_NAME_THRESHOLD: usize = 1024;
/// The threshold when parsing an entry's contents where memory allocation
/// switches from allocating the exact size to resizing as necessary.
const REALLOC_DATA_THRESHOLD: usize = 1024 * 1024;
/// 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 {
@@ -48,6 +52,10 @@ pub enum Error {
UnknownMagic([u8; 6]),
#[error("Hard links are not supported: {:?}", .0.as_bstr())]
HardLinksNotSupported(Vec<u8>),
#[error("Entry of type {0} should not have data: {:?}", .1.as_bstr())]
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")]
@@ -94,56 +102,199 @@ fn write_int(mut writer: impl Write, mut value: u32) -> io::Result<()> {
}
/// Read a chunk of bytes from the reader. If `size` is less than
/// `realloc_thresh`, then the buffer is allocated with the exact size.
/// Otherwise, the buffer starts with a capacity of `realloc_thresh` and grows
/// as necessary. This avoids allocating excessive memory when the entry
/// [`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(mut reader: impl Read, size: usize, realloc_thresh: usize) -> io::Result<Vec<u8>> {
if size < realloc_thresh {
let mut buf = vec![0u8; size];
reader.read_exact(&mut buf)?;
Ok(buf)
} else {
let mut buf = Vec::with_capacity(realloc_thresh);
let mut offset = 0;
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);
while offset < size {
let n = (size - offset).min(16384);
buf.resize(offset + n, 0);
reader.read_exact(&mut buf[offset..][..n])?;
offset += n;
stream::copy_n(reader, &mut cursor, size as u64, cancel_signal)?;
Ok(cursor.into_inner())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum CpioEntryType {
Pipe,
Char,
Directory,
Block,
Regular,
Symlink,
Socket,
Reserved,
Unknown(u16),
}
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),
}
}
Ok(buf)
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(),
}
}
}
fn file_type(mode: u32) -> u32 {
mode & 0o170000
}
#[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>,
}
impl fmt::Debug for CpioEntryNew {
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)
@@ -152,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", &self.name.as_bstr())
.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: {:?}", self.name.as_bstr())?;
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> {
@@ -242,45 +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 = read_data(
let mut path = read_data(
&mut reader,
namesize.to_usize().unwrap(),
REALLOC_NAME_THRESHOLD,
path_size.to_usize().unwrap(),
&AtomicBool::new(false),
)?;
if name.last() != Some(&b'\0') {
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 content = read_data(
&mut reader,
filesize.to_usize().unwrap(),
REALLOC_DATA_THRESHOLD,
)?;
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,
@@ -289,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(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);
@@ -367,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(())
}
+22 -31
View File
@@ -19,7 +19,7 @@ use thiserror::Error;
use crate::{
format::verityrs,
stream::{self, FromReader, ReadSeek, ToWriter, WriteSeek, WriteZerosExt},
stream::{self, FromReader, ReadSeekReopen, ToWriter, WriteSeekReopen, WriteZerosExt},
util::NumBytes,
};
@@ -419,7 +419,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn generate(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<Vec<u8>> {
let fec_size = self.fec_size();
@@ -430,7 +430,7 @@ impl Fec {
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = open_input()?;
let reader = input.reopen_boxed()?;
self.generate_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
@@ -445,7 +445,7 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn verify(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<()> {
@@ -463,7 +463,7 @@ impl Fec {
.map(|(round, buf)| -> Result<()> {
stream::check_cancel(cancel_signal)?;
let reader = open_input()?;
let reader = input.reopen_boxed()?;
self.verify_one_round(reader, round as u64, buf)
})
.collect::<Result<()>>()?;
@@ -484,8 +484,8 @@ impl Fec {
/// This function is multithreaded and uses rayon's global thread pool.
pub fn repair(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
output: &(dyn WriteSeekReopen + Sync),
fec: &[u8],
cancel_signal: &AtomicBool,
) -> Result<u64> {
@@ -504,8 +504,8 @@ impl Fec {
.map(|(round, buf)| -> Result<u64> {
stream::check_cancel(cancel_signal)?;
let reader = open_input()?;
let writer = open_output()?;
let reader = input.reopen_boxed()?;
let writer = output.reopen_boxed()?;
self.repair_one_round(reader, writer, round as u64, buf)
})
.collect::<Result<Vec<u64>>>()?
@@ -542,16 +542,16 @@ impl FecImage {
/// Generate FEC data for a file. `parity` is the number of parity bytes per
/// 255-byte Reed-Solomon codeword.
pub fn generate(
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
parity: u8,
cancel_signal: &AtomicBool,
) -> Result<Self> {
let data_size = {
let mut file = open_input()?;
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(open_input, cancel_signal)?;
let fec_data = fec.generate(input, cancel_signal)?;
Ok(Self {
fec: fec_data,
@@ -564,11 +564,11 @@ impl FecImage {
/// [`Self::repair()`] if performing a repair is not necessary.
pub fn verify(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
input: &(dyn ReadSeekReopen + Sync),
cancel_signal: &AtomicBool,
) -> Result<()> {
let fec = Fec::new(self.data_size, FEC_BLOCK_SIZE as u32, self.parity)?;
fec.verify(open_input, &self.fec, cancel_signal)
fec.verify(input, &self.fec, cancel_signal)
}
/// Repair a file using this instance's FEC data. The maximum correctable
@@ -588,12 +588,12 @@ impl FecImage {
/// that multiple threads will always read and write disjoint file offsets.
pub fn repair(
&self,
open_input: impl Fn() -> io::Result<Box<dyn ReadSeek>> + Sync,
open_output: impl Fn() -> io::Result<Box<dyn WriteSeek>> + Sync,
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(open_input, open_output, &self.fec, cancel_signal)
fec.repair(input, output, &self.fec, cancel_signal)
}
/// Build one instance of the FEC header. The caller is responsible for
@@ -764,18 +764,15 @@ mod tests {
let num_codewords = fec.rounds as usize * block_size as usize;
// Generate FEC data.
let fec_data = fec
.generate(|| Ok(Box::new(file.reopen())), &cancel_signal)
.unwrap();
let fec_data = fec.generate(&file, &cancel_signal).unwrap();
// Verify that there are no errors.
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal)
.unwrap();
fec.verify(&file, &fec_data, &cancel_signal).unwrap();
// Verify that errors are detected.
corrupt_byte(&mut file, 0);
assert_matches!(
fec.verify(|| Ok(Box::new(file.reopen())), &fec_data, &cancel_signal,),
fec.verify(&file, &fec_data, &cancel_signal),
Err(Error::HasErrors)
);
@@ -785,13 +782,7 @@ mod tests {
}
// Verify that all the single-byte errors can be fixed.
fec.repair(
|| Ok(Box::new(file.reopen())),
|| Ok(Box::new(file.reopen())),
&fec_data,
&cancel_signal,
)
.unwrap();
fec.repair(&file, &file, &fec_data, &cancel_signal).unwrap();
let repaired_digest = {
let mut buf = Vec::new();
@@ -826,7 +817,7 @@ mod tests {
file.write_all(&buf).unwrap();
}
let image = FecImage::generate(|| Ok(Box::new(file.reopen())), 2, &cancel_signal).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();
+123 -27
View File
@@ -13,6 +13,7 @@ use std::{
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";
@@ -44,6 +44,8 @@ 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,6 +66,10 @@ pub enum Error {
UnsupportedDigestAlgorithm(ObjectIdentifier),
#[error("Unsupported signature algorithm: {0}")]
UnsupportedSignatureAlgorithm(ObjectIdentifier),
#[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")]
@@ -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();
@@ -523,7 +619,7 @@ pub fn parse_zip_ota_info(
let mut entry = zip.by_name(PATH_METADATA_PB)?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf)?;
util::read_protobuf::<OtaMetadata>(&buf)?
OtaMetadata::decode(buf.as_slice())?
};
let certificate = {
+36 -44
View File
@@ -14,7 +14,7 @@ use base64::Engine;
use byteorder::{BigEndian, ReadBytesExt};
use bzip2::write::BzDecoder;
use num_traits::ToPrimitive;
use quick_protobuf::MessageWrite;
use prost::Message;
use rayon::prelude::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
use ring::digest::{Context, Digest};
use rsa::{traits::PublicKeyParts, Pkcs1v15Sign, RsaPrivateKey};
@@ -30,20 +30,19 @@ 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, CountingWriter, FromReader, HashingWriter, ReadDiscardExt,
ReadSeekReopen, Reopen, SharedCursor, WriteSeek,
},
util,
};
const OTA_MAGIC: &[u8; 4] = b"CrAU";
const OTA_HEADER_SIZE: usize = OTA_MAGIC.len() + 8 + 8 + 4;
const MANIFEST_MAX_SIZE: usize = 1024 * 1024;
const MANIFEST_MAX_SIZE: usize = 4 * 1024 * 1024;
#[derive(Debug, Error)]
pub enum Error {
@@ -68,7 +67,7 @@ pub enum Error {
actual: String,
},
#[error("Unsupported partition operation: {0:?}")]
UnsupportedOperation(mod_InstallOperation::Type),
UnsupportedOperation(Type),
#[error("Expected sha256 {expected:?}, but have {actual:?}")]
MismatchedDigest {
expected: Option<String>,
@@ -90,8 +89,8 @@ pub enum Error {
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")]
@@ -153,7 +152,7 @@ impl<R: Read> FromReader<R> for PayloadHeader {
let mut manifest_raw = vec![0u8; manifest_size];
reader.read_exact(&mut manifest_raw)?;
let manifest: DeltaArchiveManifest = util::read_protobuf(&manifest_raw)?;
let manifest = DeltaArchiveManifest::decode(manifest_raw.as_slice())?;
// Skip manifest signatures.
reader.read_discard_exact(metadata_signature_size.into())?;
@@ -184,6 +183,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 {
@@ -329,14 +329,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);
@@ -364,7 +364,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 {
@@ -392,7 +392,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.
@@ -600,18 +600,12 @@ impl<W: Write> CompressedPartitionWriter<W> {
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,
};
// data_offset must be manually updated by the caller.
let mut operation = InstallOperation::default();
operation.set_type(Type::ReplaceXz);
operation.data_length = Some(size_compressed);
operation.dst_extents.push(extent);
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
partition.operations.clear();
partition.operations.push(operation);
@@ -685,7 +679,7 @@ pub fn verify_payload(
)?;
let buf = writer.into_inner();
util::read_protobuf::<Signatures>(&buf)?
Signatures::decode(buf.as_slice())?
};
// Check the metadata signatures.
@@ -737,7 +731,7 @@ pub fn verify_payload(
)?;
let buf = writer.into_inner();
util::read_protobuf::<Signatures>(&buf)?
Signatures::decode(buf.as_slice())?
};
// Check the payload signatures.
@@ -797,10 +791,10 @@ pub fn apply_operation(
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,
@@ -823,7 +817,7 @@ pub fn apply_operation(
reader.seek(SeekFrom::Start(in_offset))?;
match other {
mod_InstallOperation::Type::REPLACE => {
Type::Replace => {
stream::copy_n_inspect(
&mut reader,
&mut writer,
@@ -832,7 +826,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,
@@ -843,7 +837,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,
@@ -854,7 +848,7 @@ pub fn apply_operation(
)?;
decoder.finish()?;
}
_ => return Err(Error::UnsupportedOperation(op.type_pb)),
_ => return Err(Error::UnsupportedOperation(op.r#type())),
}
}
}
@@ -862,9 +856,7 @@ 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
{
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()),
@@ -879,7 +871,7 @@ pub fn apply_operation(
/// 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,
payload: &(dyn ReadSeekReopen + Sync),
header: &PayloadHeader,
partition_name: &str,
cancel_signal: &AtomicBool,
@@ -896,13 +888,13 @@ pub fn extract_image_to_memory(
.operations
.par_iter()
.map(|op| -> Result<()> {
let reader = open_payload()?;
let writer = stream.reopen();
let reader = payload.reopen_boxed()?;
let writer = stream.reopen()?;
apply_operation(
reader,
writer,
header.manifest.block_size,
header.manifest.block_size(),
header.blob_offset,
op,
cancel_signal,
@@ -919,7 +911,7 @@ pub fn extract_image_to_memory(
/// 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>,
@@ -946,13 +938,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,
+1
View File
@@ -18,6 +18,7 @@ pub mod cli;
pub mod crypto;
pub mod escape;
pub mod format;
pub mod octal;
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))
}
+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"));
}
+70 -17
View File
@@ -5,7 +5,7 @@
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,
@@ -30,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;
@@ -168,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> {
@@ -325,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;
@@ -417,13 +466,6 @@ impl PSeekFile {
}
}
pub fn reopen(&self) -> Self {
Self {
file: self.file.clone(),
offset: 0,
}
}
pub fn set_len(&self, size: u64) -> io::Result<()> {
let file_locked = self.file.read().unwrap();
file_locked.set_len(size)
@@ -458,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)?;
@@ -530,12 +581,14 @@ impl SharedCursor {
..Default::default()
}
}
}
pub fn reopen(&self) -> Self {
Self {
impl Reopen for SharedCursor {
fn reopen(&self) -> io::Result<Self> {
Ok(Self {
inner: self.inner.clone(),
offset: 0,
}
})
}
}
@@ -668,8 +721,8 @@ mod tests {
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] = [
@@ -848,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.reopen();
let mut c = b.reopen();
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
@@ -868,8 +921,8 @@ mod tests {
#[test]
fn shared_cursor() {
let mut a = SharedCursor::default();
let mut b = a.reopen();
let mut c = b.reopen();
let mut b = a.reopen().unwrap();
let mut c = b.reopen().unwrap();
b.write_all(b"foobar").unwrap();
c.write_all(b"hello").unwrap();
+4 -18
View File
@@ -5,17 +5,17 @@
use std::{fmt, 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)
@@ -37,20 +37,6 @@ 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)
}
/// 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)
}
/// 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 {
+2 -10
View File
@@ -138,10 +138,7 @@ fn round_trip_appended_hash_tree_image() {
// Verify the hash tree and FEC data.
match header.appended_descriptor().unwrap() {
AppendedDescriptorRef::HashTree(d) => {
d.verify(|| Ok(Box::new(reader.reopen())), &cancel_signal)
.unwrap();
}
AppendedDescriptorRef::HashTree(d) => d.verify(&reader, &cancel_signal).unwrap(),
AppendedDescriptorRef::Hash(_) => panic!("Expected hash tree descriptor"),
}
@@ -166,12 +163,7 @@ fn round_trip_appended_hash_tree_image() {
d.fec_offset = 0;
d.fec_size = 0;
d.update(
|| Ok(Box::new(writer.reopen())),
|| Ok(Box::new(writer.reopen())),
&cancel_signal,
)
.unwrap();
d.update(&writer, &writer, &cancel_signal).unwrap();
}
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
}
+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.
+2 -2
View File
@@ -15,10 +15,10 @@ avbroot = { path = "../avbroot" }
clap = { version = "4.4.1", features = ["derive"] }
ctrlc = "3.4.0"
hex = { version = "0.4.3", features = ["serde"] }
ring = "0.16.20"
ring = "0.17.0"
serde = { version = "1.0.188", features = ["derive"] }
tempfile = "3.8.0"
toml_edit = { version = "0.19.14", features = ["serde"] }
toml_edit = { version = "0.20.1", features = ["serde"] }
# https://github.com/zip-rs/zip/pull/383
[dependencies.zip]
+62 -62
View File
@@ -1,86 +1,86 @@
[magisk]
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.0/Magisk-v26.0.apk"
"hash" = "9e14d3d3ca1f1a2765f8ca215ebbf35ea5fd2896fb147eea581fcaa3b4e77d25"
"url" = "https://github.com/topjohnwu/Magisk/releases/download/v26.3/Magisk.v26.3.apk"
"hash" = "30ff6ec0709412adfcd0b735c0eb1f61cd9d589af4bdef4cf03c09b986b5acce"
# 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"
url = "https://dl.google.com/dl/android/aosp/cheetah-ota-tq3a.230901.001-6b881553.zip"
sections = [
{ start = 0, end = 151715 },
{ start = 21683150, end = 23179365 },
{ start = 2043499985, end = 2043508325 },
{ start = 2315331822, end = 2333060126 },
{ start = 2344084950, end = 2344090066 },
{ start = 0, end = 152523 },
{ start = 21750700, end = 23251455 },
{ start = 2044485458, end = 2044493782 },
{ start = 2316042420, end = 2333812318 },
{ start = 2344911202, end = 2344916318 },
]
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"
hash.original.full = "6b881553f012d582080642d660e1cf5c9e6fe41e9f1c6ab12ae87fab7894e307"
hash.original.stripped = "9befd7887a125ebd8e9ae0555469dababe6bc04b0aa41aa2562036782a6d87e0"
hash.patched.full = "91e15447ade648c10bce599e75569ce55edc18798b11a704586acf3b51ae7971"
hash.patched.stripped = "84b069ac7f20115ac0b19d7e319bb86d00b9f3f64e0c7a51612f99d6cae297bb"
hash.avb_images."init_boot.img" = "3bedb41be98c46241f11219021dfbb799a6d5c89e6e00d45a66f9a5b42e7dfbc"
hash.avb_images."vbmeta.img" = "e8e6e898ca73807edb43af0a0e86d4a94b14256def89581970287a9b1bf7a3ee"
hash.avb_images."vbmeta_system.img" = "dbb63e08f26f46ccda501d99058d513ff71e3d6302c14d587442b666ff08862a"
hash.avb_images."vbmeta_vendor.img" = "6ffa0a10e72c3371653be80de1380832b4d7f8bbf38a2bd861d44a4097a57117"
hash.avb_images."vendor_boot.img" = "dd58e8d46dd26198edf14f72d17a3315ff4c2aeb65b98dbd06369ca2a6e365a3"
# 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"
url = "https://dl.google.com/dl/android/aosp/bluejay-ota-tq3a.230901.001-1f1f0abe.zip"
sections = [
{ start = 0, end = 140787 },
{ start = 1060207, end = 21612852 },
{ start = 1886150700, end = 1886158844 },
{ start = 2069112987, end = 2092260102 },
{ start = 2098778558, end = 2098783674 },
{ start = 0, end = 142587 },
{ start = 1062619, end = 21678441 },
{ start = 1920893927, end = 1920902079 },
{ start = 2103596124, end = 2126776858 },
{ start = 2133324030, end = 2133329146 },
]
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"
hash.original.full = "1f1f0abe67a6f6f47287be6dafec2c12628de6a715b82ca7beddaf67ad22aca5"
hash.original.stripped = "38b15f5efdc7e056bc799859ba72ef9a73e93c61292c59f85fb4b9c31acc5f82"
hash.patched.full = "65f7e29591fb48ad9c7c3233df4d76bb6986aef96b94c06b73213477bc7486d8"
hash.patched.stripped = "cf77b5e307ce4d2a62e742cd3a300964de6c693880cbfa90dcd0bfec66bd1976"
hash.avb_images."boot.img" = "a19cb4d4fcc7f3e7d3046c3d19e2f243fb02513ca848ff92ad70d1ada55c4e65"
hash.avb_images."vbmeta.img" = "2d817e35f7b6cdc2edce58ef249a966fc677085b70a4aced4c829320aeef0be2"
hash.avb_images."vbmeta_system.img" = "98a050f0d53a016fbb78147b1b4a9bca3fde615aa4da34bf62c2e07a395104b5"
hash.avb_images."vbmeta_vendor.img" = "fac530f47f237e76f3c7c3cdfe96308170dd8e8f0b227d81114a489c69ba763c"
hash.avb_images."vendor_boot.img" = "b3c596360f38cd0d6212341571acf3c2d977921f8bf163d7da27978a353da9f7"
# 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"
url = "https://dl.google.com/dl/android/aosp/bramble-ota-tq3a.230901.001-6d107ffa.zip"
sections = [
{ start = 0, end = 140531 },
{ start = 496187, end = 11655082 },
{ start = 1650283561, end = 1650287993 },
{ start = 1884739919, end = 1908081011 },
{ start = 1910894027, end = 1910899144 },
{ start = 0, end = 142225 },
{ start = 497945, end = 11647087 },
{ start = 1683478307, end = 1683482723 },
{ start = 1917281683, end = 1940628772 },
{ start = 1943442548, end = 1943447665 },
]
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"
hash.original.full = "6d107ffac1cd3da2c972112acc75957ed725e5c13d57ca724d9bcca5404fcebd"
hash.original.stripped = "5b889bdab3bb12ddcd3c243a56e1c58bedada8831069f49d56fe5098fb141e35"
hash.patched.full = "8725e03798539070d7075a07c80fb1403652a2b446d5c460d636a32b7368c9a2"
hash.patched.stripped = "c4b5cfa84dc8c15f3ac9661d768062546fa36ca82cdeb6dd943347be0422903c"
hash.avb_images."boot.img" = "8e7278a2e8ae44ffc5475717eb0e1aa56bfb7650aef34375b0fe92f790835f95"
hash.avb_images."vbmeta.img" = "b036132b867f52a86eef79716261f35b1eb50e843dc4fe42f72cce67b24ae2db"
hash.avb_images."vbmeta_system.img" = "9a7c6fd654e7a92aeffbdbd55ea0d87eee36f4c235e1b505423ad8a13a751a00"
hash.avb_images."vendor_boot.img" = "6e83d22371af4a26aef2c64cc4235f83b03978b91aef69c34eec1985e9942139"
# 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"
url = "https://dl.google.com/dl/android/aosp/sunfish-ota-tq3a.230805.001-01fd34b2.zip"
sections = [
{ start = 0, end = 129700 },
{ start = 476996, end = 34035261 },
{ start = 1624751947, end = 1624756371 },
{ start = 1823132563, end = 1823137581 },
{ start = 0, end = 131325 },
{ start = 478553, end = 34014760 },
{ start = 1658403115, end = 1658407531 },
{ start = 1855778567, end = 1855783585 },
]
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"
hash.original.full = "01fd34b206152a3559039161c9874ab03df37da4268b86a9e0be899de5fc0af7"
hash.original.stripped = "cc311b5bd46e06cfdefbade794d33aa9bc3ceda4ad4f38bfe9f0dfc17033d207"
hash.patched.full = "f2ac798b31a94dc251ca4ce370ebfb3073170d4a34431829df9ed0742149ffe7"
hash.patched.stripped = "4387a5ba30c925f56c67eeaf6512757d5c1e07dfd2a8a99be14e06db8ba2dde7"
hash.avb_images."boot.img" = "506a955080b6cfa2039ef85923e8a4e717ef6c1dc478538599c7ba26ee21e525"
hash.avb_images."vbmeta.img" = "3679c7224e3e3e0793b4d1a031e116098460a6b4c5f3f88d5a3a0a4c65b21582"
hash.avb_images."vbmeta_system.img" = "1d3efa00fd1d44a594c7317072468fa95c23d83d2759d6d6e757783ceeabc594"
# OnePlus 10 Pro
# Build NE2215_11_C.26
@@ -107,10 +107,10 @@ sections = [
]
hash.original.full = "929f892fbd70699cf7f118a119aac1ae1b86351e1ada17715666fa4401e63472"
hash.original.stripped = "4eabaf79b6c2b5df305e3ecdc2b9570c0dd27350b4e8d6434584000c4989ff3d"
hash.patched.full = "3453b2eda97ca1ba7367fa9d0dd37b21a5065e2e57cc3bbe9a6f135349d42d0b"
hash.patched.stripped = "1a41e435bdbf4302761e719e218e8682145644c0fe276739c3265f392adbd74d"
hash.avb_images."boot.img" = "f5dc3b147c54589be8db00ca15257a3688424a9eb88bd9c2cec82ebf4f6bf859"
hash.avb_images."recovery.img" = "a42c0bf4f023cd24394184a33ee113783a9c89a7cc4c0c582a5f72cc23b72309"
hash.patched.full = "ec576b7430e5a2788e89f8bbe37ee81b116386d9b101865d3b75eb9c51a0db4c"
hash.patched.stripped = "ddd0cf1b42564f8c2980320c14f8e0eb125412eb7a13ae8fb52d35424546c235"
hash.avb_images."boot.img" = "480d7cf519326fcaa5106ecfbbeb907309068635f7f6a25e5e4571d525c4926a"
hash.avb_images."recovery.img" = "eeb0f67e2084174fced510f4b59a82022559ffbe1c20d2c0ea4757fcd989a3af"
hash.avb_images."vbmeta.img" = "c022cf79da301a8430af5c49704944c490707fa0306031fe3ea22c39ce4734f6"
hash.avb_images."vbmeta_system.img" = "749616b7f04487c05e9e363ad2071a0ab3bae29d497daf1f1a7695f7c8cfa82a"
hash.avb_images."vbmeta_vendor.img" = "a6037fce745384425fb12745b8568386b84fb57ca6f94f6e47bcf754de341ae4"
+3 -2
View File
@@ -18,7 +18,7 @@ use std::{
};
use anyhow::{anyhow, bail, Context, Result};
use avbroot::stream::PSeekFile;
use avbroot::stream::{PSeekFile, Reopen};
use serde::{Deserialize, Serialize};
/// Minimum download chunk size per task.
@@ -309,7 +309,8 @@ fn download_ranges(
}
if let Some(thread_range) = remaining.pop_front() {
let file_cloned = file.reopen();
// PSeekFile's reopen can't fail.
let file_cloned = file.reopen().unwrap();
let thread_range_cloned = thread_range.clone();
let tx_cloned = tx.clone();
+6 -7
View File
@@ -26,7 +26,8 @@ use anyhow::{anyhow, bail, Context, Result};
use avbroot::{
cli::ota::{ExtractCli, PatchCli, VerifyCli},
format::{ota, payload::PayloadHeader},
stream::{self, FromReader, HashingReader, PSeekFile, SectionReader},
protobuf::chromeos_update_engine::install_operation::Type,
stream::{self, FromReader, HashingReader, PSeekFile, Reopen, SectionReader},
};
use clap::Parser;
use tempfile::TempDir;
@@ -97,7 +98,7 @@ fn strip_image(
let mut raw_reader = File::open(input)
.map(PSeekFile::new)
.with_context(|| format!("Failed to open for reading: {input:?}"))?;
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()))
let mut zip_reader = ZipArchive::new(BufReader::new(raw_reader.reopen()?))
.with_context(|| format!("Failed to read zip: {input:?}"))?;
let payload_entry = zip_reader
.by_name(ota::PATH_PAYLOAD)
@@ -107,7 +108,7 @@ fn strip_image(
// Open the payload data directly.
let mut payload_reader = SectionReader::new(
BufReader::new(raw_reader.reopen()),
BufReader::new(raw_reader.reopen()?),
payload_offset,
payload_size,
)?;
@@ -121,13 +122,11 @@ fn strip_image(
.collect::<HashSet<_>>();
let mut data_holes = vec![];
use avbroot::protobuf::chromeos_update_engine::mod_InstallOperation::Type;
for p in &header.manifest.partitions {
if !required_images.contains(&p.partition_name) {
for op in &p.operations {
match op.type_pb {
Type::ZERO | Type::DISCARD => continue,
match op.r#type() {
Type::Zero | Type::Discard => continue,
_ => {
let start = payload_offset
+ header.blob_offset
+3 -2
View File
@@ -1,6 +1,6 @@
#[cfg(not(windows))]
mod fuzz {
use std::io::Cursor;
use std::{io::Cursor, sync::atomic::AtomicBool};
use avbroot::format::cpio;
use honggfuzz::fuzz;
@@ -8,8 +8,9 @@ mod fuzz {
pub fn main() {
loop {
fuzz!(|data: &[u8]| {
let cancel_signal = AtomicBool::new(false);
let reader = Cursor::new(data);
let _ = cpio::load(reader, true);
let _ = cpio::load(reader, true, &cancel_signal);
});
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ mod fuzz {
input.write_zeros_exact(fec.data_size).unwrap();
}
let _ = fec.verify(|| Ok(Box::new(input.reopen())), &cancel_signal);
let _ = fec.verify(&input, &cancel_signal);
}
});
}
+2 -2
View File
@@ -1,6 +1,6 @@
id=com.chiller3.avbroot.clearotacerts
name=clearotacerts
version=v2.1.1
versionCode=131329
version=v2.3.2
versionCode=131842
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.1.1
versionCode=131329
version=v2.3.2
versionCode=131842
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.20.1"
walkdir = "2.3.3"
# https://github.com/zip-rs/zip/pull/383
+2 -1
View File
@@ -50,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]+)?$")?;
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)?;
@@ -82,6 +82,7 @@ fn update_changelog_links(path: &Path, base_url: &str) -> Result<()> {
let number: u32 = captures.get(2).unwrap().as_str().parse()?;
let link = match link_type {
"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:?}"),
+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())?;