The author of ring recently announced that the library is no longer
being maintained and fixes for security issues may be significantly
delayed. Big thanks to Brian Smith for creating and maintaining the
library for so long!
This commit replaces ring with aws-lc, a cryptography library maintained
by Amazon AWS. It seems to be well-regarded and is used by high-profile
projects like rustls. It is also API-compatible with ring, so it is
effectively a drop-in replacement.
Unfortunately, we still cannot switch back to the RustCrypto SHA1 and
SHA2 implementations because they are still significantly slower than
ring and aws-lc on systems that do not support the SHA-NI extensions.
https://rustsec.org/advisories/RUSTSEC-2025-0007
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
* There is now a stable release of bzip2-rs with the fix for both the C
and Rust versions of bzip2 being compiled.
* The zlib-rs deflate implementation is faster than the default
miniz_oxide. Changing this requires updating the checksums in the e2e
tests due to slight differences in compression levels between the two
implementations.
* Temporarily silence RUSTSEC-2025-0007 to avoid blocking CI. The ring
library is no longer maintained.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Implicit error propagation was originally used because it was convenient
and made it easy to just bubble up errors via the ? operator without
thinking. However, there have been too many situations where this
resulted in error messages that were completely useless in
troubleshooting the problem. "I/O error", even with a specific reason
attached, is useless where there are potentially hundreds of operations
where I/O can fail.
I no longer think implicit error propagation is a good idea, so this
commit removes every single use of #[from] in every custom error type.
There are now many more error variants, allowing more context to be
attached to the underlying errors.
Previously, it was easy to encounter error messages like:
Caused by:
0: Failed to patch payload: payload.bin
1: Failed to patch boot images: boot, init_boot, vendor_boot
2: Boot image error
3: I/O error
4: failed to fill whole buffer
This is a terrible error message because it doesn't mention which of the
3 boot images failed to parse, nor does it mention during which I/O
operation it encountered EOF. With this commit, this sort of information
is now included. For example, if the boot image happened to be truncated
in the middle of the ramdisk, the error message would now be:
Caused by:
0: Failed to patch payload: payload.bin
1: Failed to patch boot images: boot, init_boot, vendor_boot
2: Failed to load boot image: init_boot
3: Failed to read boot image data: Boot::V3::ramdisk
4: failed to fill whole buffer
Changes:
* Remove all uses of #[from] from thiserror-derived error types.
* Errors during parsing and serialization of RSA private keys, RSA
public keys, and X509 certificates now include the file path.
* Removed unnecessary uses of BufReader and BufWriter when reading and
writing RSA private keys, RSA public keys, and X509 certificates,
since they need to be fully read into memory anyway.
* Use ReadFixedSizeExt instead of read_exact() where possible.
* Use &'static str instead of String in error fields where all possible
values are known at compile-time to avoid unnecessary heap allocation.
* Use ok_or() instead of ok_or_else() to construct errors when the error
variant uses known data and does not require heap allocation.
* Using DebugString instead of String in error variants that store a
preformatted debug string.
While working on this commit, an unrelated bug was found and fixed:
* Fix vendor v4 boot images that were truncated within the padding
following the bootconfig section being treated as valid.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Neither of the two new fields need any special handling in avbroot. This
just allows us to preserve the values from the original OTA.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
On older devices, like the Pixel 4a, where `boot` is used for both
Android and recovery mode, the image will be patched by OtaCertPatcher
and PrepatchedImagePatcher. OtaCertPatcher was always set to run first,
so when PrepatchedImagePatcher used the user-supplied image as-is, prior
modifications got wiped out. This made is so users could no longer flash
further patched OTAs.
This is an unfortunate regression that was introduced in avbroot 2.0.0.
The e2e tests never caught this issue because the --prepatched test was
being fed the boot image previously patched by --magisk. That already
had valid certs so the result of OtaCertPatcher's modifications being
lost were not visible. This commit also fixes the e2e tests so that this
type of issue will be caught in the future.
Fixes: #356
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit adds a new `--zip-mode` parameter to `avbroot ota patch` to
control whether the patched OTA zip is written with data descriptors or
not. By default, the `streaming` mode is used, which matches the current
behavior where the zip is hashed for signing as it is being written. The
new `seekable` mode fully writes the zip before rereading it to hash the
contents.
The new mode is useful for devices with broken zip parsers that fail to
properly handle data descriptors.
All of the end-to-end tests have been duplicated to test both modes.
Adding the seekable mode necessitated a couple other changes:
* BufWriter is no longer used. Type erasure is very painful in Rust, so
we need to keep the writer types the same for both the streaming and
seekable modes. BufWriter is unusable in the seekable mode because we
need to be able to read back what was written, which isn't supported.
* HolePunchingWriter has been removed. It was a simple way to produce
sparse files by seeking whenever a write buffer consists fully of
zeros. When combined with BufWriter, there was previously never a
situation where this was undesirable. However, with the new seekable
mode and the zip library's pattern of writing one field at a time, the
final 2 zero bytes (representing an empty archive comment) is never
written and the file size is not increased either.
Removing this is not a big deal since we no longer use stripped OTAs
for the end-to-end tests. Those were really the only OTAs that
benefitted from sparse files. A real OTA has very few zero bytes due
to compression.
Issue: #328
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Older devices, like the Pixel 4a 5G (bramble) use gzip instead of lz4.
This commit also reworks the CoW size estimate calculation to add the
same constant headroom that AOSP's delta_generator adds. Previously,
avbroot was already adding an additional 1% to account for differences
in compression ratios across compression library implementations. This
papered over the issue for large partitions, but small partitions could
still have a CoW size estimate that's too small. Adding the constant
headroom prevents ENOSPC when flashing those partitions.
Fixes: #332
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Some devices have "full" OTAs where the payload is missing the recovery
partition. These subcommands make it possible to manually add back the
missing image. Given the strict requirements for how the OTA zip is laid
out and signed, users can't just replace payload.bin in a zip and call
it a day, but it's sufficient for feeding a modified input to
`avbroot ota patch`.
Issue: #328
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
By default, the helper program is invoked in a way that is compatible
with avbtool's --signing_helper. However, the arguments have been
extended slightly to allow passing in the passphrase file or environment
variable for non-interactive use.
Fixes: #310
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
lz4_flex appears to compress system images better than the original lz4
implementation used in libsnapshot_cow, so the estimates are too low.
Fixes: #306
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Otherwise, if the partition size increases or the data becomes more
incompressible, update_engine might fail to flash the partition due to
the CoW block device running out of space.
Since all known VABC-enabled OTAs in the wild currently use CoW v2 with
lz4 compression, this is the only configuration we support. CoW v3 also
exists in AOSP's libsnapshot_cow, but is much more complicated to
implement and is not yet used, even in the Android 15 beta OTAs.
Fixes: #306
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Previously, for resizable images, the user had to guess an appropriate
size for the final image that could fit all the AVB metadata. This
commit adds a new `--recompute-size` option to the `avb pack` subcommand
to generate a minimally sized image.
Fixes: #294
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This allows the user to boot GSIs signed by the same key. The option is
disabled by default because some Android builds disable DSU support by
removing all keys to reduce the attack surface. We don't want to
reenable DSU support on these builds unless the user asks for it.
Closes: #286
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Instead of println'ing everything, this commit switches the code base to
using the tracing library. There are now proper log levels and multiple
logging output formats. A bunch of new debug and trace-level messages
have also been added to help with future troubleshooting.
By default, the output is kept nice and short. Spans won't be shown
unless the log level is set to debug or lower.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Upstream Magisk now xz-compresses files in modifies in the ramdisk. This
commit also implements the same in avbroot's MagiskRootPatcher.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit replaces the previous approach of patching real OTAs with
patching mock OTAs. The motivation for this change is to make it
possible to test the system partition otacerts.zip patching without
needing to download huge files. Adding the system image to the stripped
OTAs would increase the file size by an order of magnitude.
The mock OTAs are generated from a set of profiles defined in e2e.toml.
The four included profiles are meant to mimic the OTAs used for testing
before:
* pixel_v4_gki ~= cheetah
* pixel_v4_non_gki ~= bluejay
* pixel_v3 ~= bramble
* pixel_v2 ~= sunfish
There is no equivalent profile for ossi because newer OnePlus devices no
longer support custom signing keys properly.
The mock OTAs are perfectly valid, structure and signature-wise. They
just don't include any real partition data where possible. They are
initially signed with a different set of keys to ensure that the changes
made by the patching process are actually visible.
With how small the mock OTAs are, testing every profile only takes about
two seconds. Thus, the Github Actions workflow was adjusted to just run
e2e in the same job as the build.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Previously, overriding otacerts.zip in the system partition required the
user to flash a Magisk/KernelSU module that would bind mount over the
file during boot. While this worked well enough, it's insufficient for
unrooted setups, which has become more important since unrooting is the
only safe way to use the new OEM repair mode feature. With the stock
otacerts.zip, the OEM's default OTA updater app could run and install an
OS upgrade that's not signed by the user's key.
With this commit, the raw otacerts.zip bytes in the system partition are
directly replaced with a new zip that contains the user's certificate.
This method was inspired by @pascallj's comment in #216 suggesting
intentionally corrupting the otacerts.zip data in the filesystem.
Because avbroot does not have filesystem parsers for ext4/f2fs/erofs, we
rely on a heuristic-based search on the raw filesystem image. The file
is always smaller than one block (which is at least 4096 bytes on all
known devices), so the file data is stored contiguously on disk and in
the case of erofs, won't be compressed. None of the three filesystems
are copy-on-write and thus, have no filesystem-level data checksums. For
the dm-verity layer one level up, avbroot already knows how to recompute
the hash tree and FEC data.
To ensure that there are no false positives, any match that the search
finds must correctly parse as a valid zip and every entry within the zip
must have a filename that ends in .x509.pem. This matches what
update_engine expects from a proper otacerts.zip file.
Since the new approach is doing a raw search and replace, the old and
new files must have the same size. When the new zip is smaller, null
bytes are added to the zip archive comment field to pad to the correct
size. When the new zip is larger, avbroot will attempt the following to
try and make the file size smaller:
1. Enable zip deflate compression
2. Strip the X.509 signature from the certificate
3. Clear out the issuer RDN sequence from the certificate
4. Clear out the subject RDN sequence from the certificate
The latter three changes work because Android never performs any PKI
operations with the certificate. There is no CA certificate chain. The
X.509 certificate file is nothing more than a way to transport an RSA
public key.
avbroot requires the user's key to be RSA 4096. If the original zip had
the same key size, then none of these shrinking methods are needed. If
it contained an RSA 2048 key, then the first two modifications are
usually sufficient. The latter two modifications should only be needed
if the user picked a really long subject value when generating the
certificate.
With these new changes, the OTA patching time will approximately double
on a system with an SSD and modern CPU. This is dominated by the time it
takes to XZ-compress the system partition image. The compression is
already parallelized and scales linearly with the number of cores.
There's likely not much more that can be done to further speed this up.
Finally, these new changes are currently excluded from the e2e tests
because including the system partition in the stripped OTAs would
increase the file size by an order of magnitude. This could potentially
be solved in the future by generating our own small OTAs to use for
testing instead of running against real device OTAs.
Fixes: #225
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
During patching, all boot images are now extracted and the individual
patchers can inspect them to determine which ones need modifications.
This replaces the previous mechanism of detecting which boot images to
patch based on the name alone.
With this new method, the --boot-partition and --otacerts-partitions
options are no longer needed. The former option is kept (but ignored
with a warning message) for backwards compatibility, but the latter is
completely removed because it never made it to a stable release.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
FEC data can be updated efficiently with "round" granularity when the
regions where the input file was modified are known.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Previously, all replacement partition images (those that have been
patched or `--replace`d) were compressed as a whole, which would be very
slow for larger images. Instead, we'll split the images into 2 MiB
chunks and compress them in parallel. This more closely matches what
AOSP's payload_generator does and scales linearly with the number of CPU
cores. The compression is less efficient, but the file size generally
only increases by 10s of KiB.
This commit also reworks the implementation so that patched images are
stored in temp files instead of in memory again. In hindsight, doing
everything in memory only made things more complex and causes the memory
usage to blow up when doing things like `--replace system <path>`.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
The autodetection logic for `@otacerts` is based on the presence of the
`recovery`, `vendor_boot`, and `boot` partitions (in that order). Some
devices have `vendor_boot`, but put `system/etc/security/otacerts.zip`
inside `boot`.
With the way things are written now, we don't have the ability to
inspect the actual partition images for the autodetection. It is based
on the name only. So, for now, we'll just allow the user to override the
autodetected partition similar to what we already do with the
`--boot-partition` option.
Issue: #218
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
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>
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>
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>
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>
This supports all of the `format::avb` functionality, including
repairing dm-verity images. When packing images, all offsets, sizes,
digests, hash trees, FEC data, signatures, etc. are automatically
recomputed. The goal is that the user can edit any partition image
without needing to think about AVB at all.
A new `--repair` option has also been added to the `avb verify` command
to automatically attempt to all dm-verity images.
As a side effect of the changes, RSA2048 keys are now supported.
This commit also removes the `Clone` implementation from the `PSeekFile`
and `SharedCursor` types. These types use the same underlying file or
memory buffer when cloned, allowing parallel threads to read and write
files using the normal `Read`/`Write` APIs. The intention is that
cloning one of these instances would behave as if a new file handle to
the same file was opened. However, the file offset was also copied
instead of being set to 0, which is confusing. A new `reopen()` method
has been added that explicitly sets the initial offset to 0.
Closes: #148
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
There's no reason all these functions need to care about the ownership
of the cancel signal.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
We don't use async for anything else, so switching to a synchronous HTTP
library lets us get rid of the entire async ecosystem and removes 47
packages from e2e's dependency tree.
The only downside is that ^C worst case takes 5 seconds (TCP connect and
read timeouts) or whatever the OS's DNS lookup timeout is. That's good
enough for a test suite.
Performance wise, e2e still easily saturates a gigabit internet
connection.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This fixes `data_offset` being set for `ZERO` and `DISCARD` operations,
which prevents some images (eg. `ossi`) from being flashed due to
update_engine's strict field validation.
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Why?
----
It was always my intention to write avbroot in a compiled language.
Python was a stop-gap solution since it was possible to use the various
tools and parsers from AOSP to make the initial prototyping and
implementation easier. However, doing so required a whole lot of hacks
since nearly all of the Python modules we use were intended to be used
as executables, not libraries, and they were definitely not meant to be
used outside of AOSP's code base.
Although the dependencies on AOSP code have been reduced over time,
working on the Python code is still frustrating. The majority of the
modules we use from both the standard library and external dependencies
are lacking type annotations. All of the Python language servers and
type checker tools I've used choked on them. There have been serveral
avbroot bugs in the past that wouldn't have happened with any
statically typed language.
The catalyst for me working on this recently was dealing with some
python-protobuf versions that wouldn't work with AOSP's pregenerated
protobuf bindings. When parsing protobuf messages, it would fail
with obscure runtime type errors. I need my projects to not feel
frustrating or else I'll just get burnt out.
Hence, the Rust rewrite. With fewer hacks this time! avbroot no longer
has any dependencies on external tools like openssl. I'll be providing
precompiled binaries for the three major desktop OS's, built by GitHub
Actions. avbroot will also be versioned now, starting at 2.0.0.
Whats new?
----------
* A new `avbroot ota verify` subcommand has been added to check that all
OTA and AVB related components have been properly hashed and signed.
This works for all OTA images, including stock ones.
* A couple new `avbroot avb` subcommands have been added for dumping
vbmeta header/footer information and verifying AVB signatures. These
are roughly equivalent to avbtool's `info_image` and `verify_image`
subcommands, though avbroot is about an order of magnitude faster than
the latter.
* A new set of `avbroot boot` subcommands have been added for packing
and unpacking boot images. It supports Android v0-v4 images and vendor
v3-v4 images. Repacking is lossless even when using deprecated fields,
like the boot image v4 VTS signature.
* A new `avbroot ramdisk` subcommand has been added for inspecting
the CPIO structure of ramdisks.
* A new set of `avbroot key` subcommands have been added for generating
signing keys so that it's no longer necessary to install openssl and
avbtool (though of course, keys generated by other tools remain fully
compatible).
* Since avbroot has a ton of CLI options, a new `avbroot completion`
subcommand has been added for generating tab-completion configs for
various shells (eg. bash, zsh, fish, powershell).
What was removed?
-----------------
Nothing :) The `patch` and `extract` subcommands have been moved under
`avbroot ota` and the `magisk-info` subcommand has been moved under
`avbroot boot`, but there are compatibility shims in place to keep all
the old commands working.
The command-line interface will remain backwards compatible for as long
as possible, even with new major releases. The Rust API, however, has no
backwards compatibility guarantees. I currently don't intend for
avbroot's "library" components to be used anywhere outside of Custota
and avbroot itself.
Performance
-----------
Due to having better access to low-level APIs (especially `pread` and
`pwrite`), nearly everything that can be multithreaded in avbroot is now
multithreaded. In addition, during the patching operation, everything
is done entirely in memory without temp files and the maximum memory
usage is still about 100MB lower than with the Python implementation.
The new implementation is bottlenecked by how fast a single CPU core can
calculate 3 SHA256 hashes of overlapping regions spanning the majority
of the OTA file. About 90% of the CPU time is spent calculating SHA256
hashes and another 5% or so performing XZ-compression.
Some numbers:
* Patching should take roughly 40%-70% of the time it took before.
* Extracting with `--all` should take roughly 10%-30% of the time it
took before.
Folks with x86_64 CPUs supporting SHA-NI extensions (eg. Intel 11th gen
and newer) should see even bigger improvements.
Reproducibility
---------------
The new implementation's output files are bit-for-bit identical when the
inputs are the same. However, they do not exactly match what the Python
implementation produced.
* The zip entries, aside from `metadata` and `metadata.pb`, are written
in sorted order.
* All zip entries are stored without compression.
* All zip entries are stored without additional metadata (eg.
modification timestamp).
* The OTA certificate, both in the OTA zip and in the recovery ramdisk's
`otacerts.zip`, goes through deserialization + serialization before
being written. Text in the certificate file before the header and
after the footer will be stripped out.
* The protobuf structures (payload header and OTA metadata) are
serialized differently. Protobuf has more than one way to encode the
same messages "on the wire". The Rust quick_protobuf library
serializes messages a bit differently than python-protobuf, but the
outputs are mutually compatible.
* XZ compression of modified partition images in the payload is now done
at compression level 0 instead of 6. This reduces the patching time by
several seconds at the cost of a couple MiB increase in file size.
* Ramdisks are now compressed with standard LZ4 instead of LZ4HC (high
compression mode). For our use case, the difference is <100 KiB, but
using standard LZ4 allows us to use a pure-Rust LZ4 library and makes
the compression step much faster.
* Older ramdisks compressed with gzip are slightly different due to a
different gzip implementation being used (flate2 vs. zlib). The two
implementations structure the gzip frames slightly differently, but
the output is identical when decompressed.
* Magisk's config file in the ramdisk (`.backup/.magisk`) will have the
`SHA1` field set to all zeros. This allows avbroot to keep track of
less information during patching for better performance. The field is
only used for Magisk's uninstall feature, which can't ever be used in
a locked bootloader setup anyway.
Misc
----
While working on the new `avbroot ota verify` subcommand, I found that
the `ossi` stock image (OnePlus 10 Pro) used in avbroot's tests has an
invalid vbmeta hash for the `odm` partition. I thought it was an avbroot
bug, but AOSP's avbtool reports the same invalid hash too. If that image
actually boots, then I'm not sure AVB can be trusted on those devices...
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>