mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6c6a9509a | |||
| 5a00995366 | |||
| edf537aad6 | |||
| f006f20209 | |||
| f36c1ca451 | |||
| 29b72961a3 | |||
| e105efb6d3 | |||
| 2a7df104ed | |||
| 8e52a9cf8c | |||
| f9343aa542 | |||
| 0391d4e2c3 | |||
| c18800dc44 | |||
| 2db90f83c2 | |||
| 34915e256f | |||
| 61392eb9d6 | |||
| 029cb4264e | |||
| 98a7fc811d | |||
| f4f4ec8e0d | |||
| 99b316f55e | |||
| 50ee90b61a | |||
| 2e8bd9f9d4 | |||
| 3fa96714c4 | |||
| 7a2530a199 |
@@ -19,6 +19,7 @@ jobs:
|
||||
# https://github.com/rust-lang/rust/issues/78210
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
TARGETS: ${{ join(matrix.artifact.targets, ' ') || matrix.artifact.name }}
|
||||
ANDROID_API: ${{ matrix.artifact.android_api }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -33,6 +34,12 @@ jobs:
|
||||
- aarch64-apple-darwin
|
||||
- x86_64-apple-darwin
|
||||
combine: lipo
|
||||
# ubuntu-latest is not 24.04 yet and 22.04's qemu-user-static segfaults.
|
||||
- os: ubuntu-24.04
|
||||
name: aarch64-linux-android31
|
||||
targets:
|
||||
- aarch64-linux-android
|
||||
android_api: '31'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -40,6 +47,26 @@ jobs:
|
||||
# For git describe
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install qemu-user-static
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get -y update
|
||||
sudo apt-get -y install qemu-user-static
|
||||
|
||||
- name: Set Android temporary directory
|
||||
if: ${{ contains(matrix.artifact.name, 'android') }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "TMPDIR=/tmp" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Install cargo-android
|
||||
shell: bash
|
||||
run: |
|
||||
cargo install \
|
||||
--git https://github.com/chenxiaolong/cargo-android \
|
||||
--tag v0.1.1
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: bash
|
||||
@@ -63,7 +90,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo clippy --release --workspace --features static \
|
||||
cargo android \
|
||||
clippy --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -71,7 +99,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo build --release --workspace --features static \
|
||||
cargo android \
|
||||
build --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -79,7 +108,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo test --release --workspace --features static \
|
||||
cargo android \
|
||||
test --release --workspace --features static \
|
||||
--target "${target}"
|
||||
done
|
||||
|
||||
@@ -87,7 +117,8 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
for target in ${TARGETS}; do
|
||||
cargo run --release -p e2e --features static \
|
||||
cargo android \
|
||||
run --release -p e2e --features static \
|
||||
--target "${target}" \
|
||||
-- test -a -c e2e/e2e.toml
|
||||
done
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Version 3.3.0
|
||||
|
||||
* Recompute CoW size estimate when replacing dynamic partitions ([Issue #306], [PR #307])
|
||||
* Fixes out of space error when flashing a patched OTA that uses `--replace` to replace a dynamic partition (eg. `system`) with a larger or more incompressible image
|
||||
* Add `avbroot payload info` subcommand for inspecting `payload.bin` headers ([PR #309])
|
||||
|
||||
### Version 3.2.3
|
||||
|
||||
* Add prebuilt binary for Android (aarch64) ([PR #304])
|
||||
|
||||
### Version 3.2.2
|
||||
|
||||
* Add new `--recompute-size` option to `avbroot avb pack` to automatically recompute the image size for resizable images ([Discussion #294], [PR #296])
|
||||
* Add new `--output-info` option to `avbroot avb pack` to write a new `avb.toml` file containing computed values ([PR #297])
|
||||
* Add support for upcoming Magisk Canary 27003 ([Issue #301], [PR #268])
|
||||
|
||||
### Version 3.2.1
|
||||
|
||||
* Increase hash tree and FEC size limits to accommodate partition images up to 8 GiB ([Issue #291], [PR #293])
|
||||
@@ -181,6 +197,7 @@ Behind-the-scenes changes:
|
||||
[Discussion #195]: https://github.com/chenxiaolong/avbroot/discussions/195
|
||||
[Discussion #235]: https://github.com/chenxiaolong/avbroot/discussions/235
|
||||
[Discussion #286]: https://github.com/chenxiaolong/avbroot/discussions/286
|
||||
[Discussion #294]: https://github.com/chenxiaolong/avbroot/discussions/294
|
||||
[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
|
||||
@@ -199,6 +216,8 @@ Behind-the-scenes changes:
|
||||
[Issue #278]: https://github.com/chenxiaolong/avbroot/issues/278
|
||||
[Issue #285]: https://github.com/chenxiaolong/avbroot/issues/285
|
||||
[Issue #291]: https://github.com/chenxiaolong/avbroot/issues/291
|
||||
[Issue #301]: https://github.com/chenxiaolong/avbroot/issues/301
|
||||
[Issue #306]: https://github.com/chenxiaolong/avbroot/issues/306
|
||||
[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
|
||||
@@ -277,6 +296,7 @@ Behind-the-scenes changes:
|
||||
[PR #256]: https://github.com/chenxiaolong/avbroot/pull/256
|
||||
[PR #257]: https://github.com/chenxiaolong/avbroot/pull/257
|
||||
[PR #261]: https://github.com/chenxiaolong/avbroot/pull/261
|
||||
[PR #268]: https://github.com/chenxiaolong/avbroot/pull/268
|
||||
[PR #276]: https://github.com/chenxiaolong/avbroot/pull/276
|
||||
[PR #277]: https://github.com/chenxiaolong/avbroot/pull/277
|
||||
[PR #279]: https://github.com/chenxiaolong/avbroot/pull/279
|
||||
@@ -284,3 +304,8 @@ Behind-the-scenes changes:
|
||||
[PR #288]: https://github.com/chenxiaolong/avbroot/pull/288
|
||||
[PR #289]: https://github.com/chenxiaolong/avbroot/pull/289
|
||||
[PR #293]: https://github.com/chenxiaolong/avbroot/pull/293
|
||||
[PR #296]: https://github.com/chenxiaolong/avbroot/pull/296
|
||||
[PR #297]: https://github.com/chenxiaolong/avbroot/pull/297
|
||||
[PR #304]: https://github.com/chenxiaolong/avbroot/pull/304
|
||||
[PR #307]: https://github.com/chenxiaolong/avbroot/pull/307
|
||||
[PR #309]: https://github.com/chenxiaolong/avbroot/pull/309
|
||||
|
||||
Generated
+4
-4
@@ -109,7 +109,7 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "avbroot"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -534,7 +534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"avbroot",
|
||||
@@ -628,7 +628,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fuzz"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"avbroot",
|
||||
"honggfuzz",
|
||||
@@ -2068,7 +2068,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -32,6 +32,10 @@ This subcommand packs a new AVB image from the `avb.toml` file and, for appended
|
||||
|
||||
Note that if the image is an appended image and its hash or hash tree descriptor uses an insecure algorithm, like `sha1`, then it will automatically be promoted to `sha256`.
|
||||
|
||||
By default, for appended vbmeta images, the output image size will match the size of the original image that was unpacked. This size is specified by the `image_size` field in `avb.toml`. If the image is resizable (eg. `system`), then passing in `--recompute-size` will cause the `image_size` field to be ignored and the smallest possible output file that fits the raw image and AVB metadata will be built. This avoids wasting space if `raw.img` shrunk or allows the packing to work at all if `raw.img` grew. **Do not use this option for non-resizable images** (eg. `boot`) or else the device won't be able to boot.
|
||||
|
||||
When packing an image, several of the fields in `avb.toml` may potentially be recomputed. To write a TOML file containing the new values, use `--output-info <output TOML>`. It is safe to overwrite the existing `avb.toml` if desired.
|
||||
|
||||
### Repacking an AVB image
|
||||
|
||||
```bash
|
||||
@@ -248,3 +252,13 @@ avbroot hash-tree verify -i <input data file> -H <input hash tree file>
|
||||
```
|
||||
|
||||
This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid.
|
||||
|
||||
## `avbroot payload`
|
||||
|
||||
### Showing payload header information
|
||||
|
||||
```bash
|
||||
avbroot payload info -i <payload>
|
||||
```
|
||||
|
||||
This subcommand shows all of the payload header fields (which will likely be extremely long).
|
||||
|
||||
@@ -15,9 +15,9 @@ Having a good understanding of how AVB and A/B OTAs work is recommended prior to
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
* 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.
|
||||
|
||||
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
A list of devices known to work can be found in the issue tracker at [#299](https://github.com/chenxiaolong/avbroot/issues/299).
|
||||
|
||||
## Patches
|
||||
|
||||
@@ -440,6 +440,16 @@ Debug builds work too, but they will run significantly slower (in the sha256 com
|
||||
|
||||
By default, the executable links to the system's bzip2 and liblzma libraries, which are the only external libraries avbroot depends on. To compile and statically link these two libraries, pass in `--features static`.
|
||||
|
||||
### Android cross-compilation
|
||||
|
||||
To cross-compile for Android, install [cargo-android](https://github.com/chenxiaolong/cargo-android) and use the `cargo android` wrapper. To make a release build for aarch64, run:
|
||||
|
||||
```bash
|
||||
cargo android build --release --target aarch64-linux-android
|
||||
```
|
||||
|
||||
It is possible to run the tests if the host is running Linux, qemu-user-static is installed, and the executable is built with `RUSTFLAGS=-C target-feature=+crt-static` and `--features static`.
|
||||
|
||||
## Verifying digital signatures
|
||||
|
||||
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/).
|
||||
|
||||
+2
-2
@@ -13,9 +13,9 @@ avbroot – это программа для модификации OTA-обра
|
||||
* наличие файла `payload.bin` (обычно находится в корне архива)
|
||||
* наличие файла `META-INF/com/android/metadata` (Android 10-11) или `META-INF/com/android/metadata.pb` (Android 12+)
|
||||
|
||||
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это делается с помощью команды `fastboot flash avb_custom_key`. Все устройства Pixel с разблокируемым загрузчиком, начиная с Pixel 2, поддерживают эту функцию. Другие устройства тоже могут поддерживать её, но достоверно убедиться в этом можно только проверив лично.
|
||||
* Устройство должно поддерживать установку пользовательского публичного ключа для подтверждения статуса доверия загрузчика. Обычно это производится с помощью команды `fastboot flash avb_custom_key`.
|
||||
|
||||
* ПРИМЕЧАНИЕ: Некоторые девайсы от OnePlus имеют некорректную реализацию, где возможна установка кастомного публичного ключа, но устройство все равно не будет загружаться, несмотря на наличие корректной подписи. Понижение загрузчика до версии, поставляемой вместе с Android 11, потенциально может помочь. Уже сообщалось о наличии этой проблемы на нескольких устройствах OnePlus ([#186,](https://github.com/chenxiaolong/avbroot/issues/186) [#195,](https://github.com/chenxiaolong/avbroot/discussions/195) [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
Список девайсов, на которых проверялась совместимость с указанным выше функционалом, находится здесь: [#299.](https://github.com/chenxiaolong/avbroot/issues/299)
|
||||
|
||||
## Патчи
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,7 @@ use clap::{Parser, Subcommand, ValueEnum};
|
||||
use tracing::{debug, Level};
|
||||
use tracing_subscriber::fmt::{format::Writer, time::FormatTime};
|
||||
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota};
|
||||
use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload};
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -28,6 +28,7 @@ pub enum Command {
|
||||
HashTree(hashtree::HashTreeCli),
|
||||
Key(key::KeyCli),
|
||||
Ota(ota::OtaCli),
|
||||
Payload(payload::PayloadCli),
|
||||
/// (Deprecated: Use `avbroot ota patch` instead.)
|
||||
Patch(ota::PatchCli),
|
||||
/// (Deprecated: Use `avbroot ota extract` instead.)
|
||||
@@ -163,6 +164,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res
|
||||
Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal),
|
||||
Command::Key(c) => key::key_main(&c),
|
||||
Command::Ota(c) => ota::ota_main(&c, cancel_signal),
|
||||
Command::Payload(c) => payload::payload_main(&c),
|
||||
// Deprecated aliases.
|
||||
Command::Patch(c) => ota::patch_subcommand(&c, cancel_signal),
|
||||
Command::Extract(c) => ota::extract_subcommand(&c, cancel_signal),
|
||||
|
||||
+34
-8
@@ -57,16 +57,22 @@ fn read_avb_image(path: &Path) -> Result<(AvbInfo, BufReader<File>)> {
|
||||
Ok((info, reader))
|
||||
}
|
||||
|
||||
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo) -> Result<()> {
|
||||
fn write_avb_image(file: PSeekFile, info: &mut AvbInfo, recompute_size: bool) -> Result<()> {
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
if let Some(f) = &mut info.footer {
|
||||
avb::write_appended_image(&mut writer, &info.header, f, info.image_size)
|
||||
.context("Failed to write appended AVB image")?;
|
||||
info.image_size = if let Some(f) = &mut info.footer {
|
||||
let image_size = if recompute_size {
|
||||
None
|
||||
} else {
|
||||
Some(info.image_size)
|
||||
};
|
||||
|
||||
avb::write_appended_image(&mut writer, &info.header, f, image_size)
|
||||
.context("Failed to write appended AVB image")?
|
||||
} else {
|
||||
avb::write_root_image(&mut writer, &info.header, 4096)
|
||||
.context("Failed to write root AVB image")?;
|
||||
}
|
||||
.context("Failed to write root AVB image")?
|
||||
};
|
||||
|
||||
writer.flush().context("Failed to flush writes")?;
|
||||
|
||||
@@ -625,12 +631,16 @@ fn pack_subcommand(cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
write_avb_image(file, &mut info, cli.recompute_size)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
display_info(&cli.display, &info);
|
||||
|
||||
if let Some(path) = &cli.output_info {
|
||||
write_info(path, &info)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -659,7 +669,7 @@ fn repack_subcommand(cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()>
|
||||
|
||||
sign_or_clear(&mut info, &orig_header, &cli.key)?;
|
||||
|
||||
write_avb_image(file, &mut info)?;
|
||||
write_avb_image(file, &mut info, false)?;
|
||||
|
||||
// We display the info at the very end after both the header and footer are
|
||||
// updated so that incorrect/incomplete information isn't shown.
|
||||
@@ -827,12 +837,28 @@ struct PackCli {
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "avb.toml")]
|
||||
input_info: PathBuf,
|
||||
|
||||
/// Path to output AVB info TOML.
|
||||
///
|
||||
/// If specified, the AVB info containing all recomputed fields will be
|
||||
/// written to this file. This can point to the same file as --input-info.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
output_info: Option<PathBuf>,
|
||||
|
||||
/// Path to input raw image.
|
||||
///
|
||||
/// Appended AVB images require a raw image.
|
||||
#[arg(long, value_name = "FILE", value_parser, default_value = "raw.img")]
|
||||
input_raw: PathBuf,
|
||||
|
||||
/// Recompute image size.
|
||||
///
|
||||
/// By default, it is assumed that the image has a fixed size specified by
|
||||
/// the image_size top-level field in the AVB info TOML. If flag is passed,
|
||||
/// then that field is ignored and the smallest possible output image will
|
||||
/// be created.
|
||||
#[arg(long)]
|
||||
recompute_size: bool,
|
||||
|
||||
#[command(flatten)]
|
||||
key: KeyGroup,
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -12,3 +12,4 @@ pub mod fec;
|
||||
pub mod hashtree;
|
||||
pub mod key;
|
||||
pub mod ota;
|
||||
pub mod payload;
|
||||
|
||||
+31
-3
@@ -674,12 +674,40 @@ fn compress_image(
|
||||
|
||||
info!("Compressing full image: {name}");
|
||||
|
||||
// Otherwise, compress the entire image.
|
||||
let (partition_info, operations) =
|
||||
payload::compress_image(&*file, &writer, name, block_size, cancel_signal)?;
|
||||
// Otherwise, compress the entire image. If VABC is enabled, we need to
|
||||
// update the CoW size estimate or else the CoW block device may run out of
|
||||
// space during flashing.
|
||||
let need_cow = partition.estimate_cow_size.is_some();
|
||||
if need_cow {
|
||||
info!("Needs updated CoW size estimate: {name}");
|
||||
|
||||
// Only CoW v2 + lz4 seems to exist in the wild currently, so that is
|
||||
// all we support.
|
||||
let Some(dpm) = &header.manifest.dynamic_partition_metadata else {
|
||||
bail!("Dynamic partition metadata is missing");
|
||||
};
|
||||
|
||||
if !dpm.vabc_enabled() {
|
||||
bail!("Partition has CoW estimate, but VABC is disabled: {name}");
|
||||
}
|
||||
|
||||
let cow_version = dpm.cow_version();
|
||||
if dpm.cow_version() != 2 {
|
||||
bail!("Unsupported CoW version: {cow_version}");
|
||||
}
|
||||
|
||||
let compression = dpm.vabc_compression_param();
|
||||
if compression != "lz4" {
|
||||
bail!("Unsupported VABC compression: {compression}");
|
||||
}
|
||||
}
|
||||
|
||||
let (partition_info, operations, cow_estimate) =
|
||||
payload::compress_image(&*file, &writer, name, block_size, need_cow, cancel_signal)?;
|
||||
|
||||
partition.new_partition_info = Some(partition_info);
|
||||
partition.operations = operations;
|
||||
partition.estimate_cow_size = cow_estimate;
|
||||
|
||||
*file = writer;
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{fs::File, io::BufReader, path::PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::{format::payload::PayloadHeader, stream::FromReader};
|
||||
|
||||
fn info_subcommand(cli: &InfoCli) -> Result<()> {
|
||||
let mut reader = File::open(&cli.input)
|
||||
.map(BufReader::new)
|
||||
.with_context(|| format!("Failed to open payload: {:?}", cli.input))?;
|
||||
let header = PayloadHeader::from_reader(&mut reader)
|
||||
.with_context(|| format!("Failed to read payload: {:?}", cli.input))?;
|
||||
|
||||
println!("{header:#?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn payload_main(cli: &PayloadCli) -> Result<()> {
|
||||
match &cli.command {
|
||||
PayloadCommand::Info(c) => info_subcommand(c),
|
||||
}
|
||||
}
|
||||
|
||||
/// Display payload information.
|
||||
#[derive(Debug, Parser)]
|
||||
struct InfoCli {
|
||||
/// Path to input payload file.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
input: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum PayloadCommand {
|
||||
Info(InfoCli),
|
||||
}
|
||||
|
||||
/// Inspect OTA payloads.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct PayloadCli {
|
||||
#[command(subcommand)]
|
||||
command: PayloadCommand,
|
||||
}
|
||||
+89
-89
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
use std::{
|
||||
cmp, fmt,
|
||||
fmt,
|
||||
io::{self, Cursor, Read, Seek, SeekFrom, Write},
|
||||
ops::Range,
|
||||
str,
|
||||
@@ -29,8 +29,8 @@ use crate::{
|
||||
padding,
|
||||
},
|
||||
stream::{
|
||||
self, CountingReader, FromReader, ReadDiscardExt, ReadSeekReopen, ReadStringExt, ToWriter,
|
||||
WriteSeekReopen, WriteStringExt, WriteZerosExt,
|
||||
self, CountingReader, CountingWriter, FromReader, ReadDiscardExt, ReadSeekReopen,
|
||||
ReadStringExt, ToWriter, WriteSeekReopen, WriteStringExt, WriteZerosExt,
|
||||
},
|
||||
util,
|
||||
};
|
||||
@@ -124,8 +124,10 @@ pub enum Error {
|
||||
RsaSign(#[source] rsa::Error),
|
||||
#[error("Failed to RSA verify signature")]
|
||||
RsaVerify(#[source] rsa::Error),
|
||||
#[error("{0} byte image size is too small to fit header or footer")]
|
||||
ImageSizeTooSmall(u64),
|
||||
#[error("{0} byte image size is too small to fit header")]
|
||||
TooSmallForHeader(u64),
|
||||
#[error("{0} byte image size is too small to fit footer")]
|
||||
TooSmallForFooter(u64),
|
||||
#[error("Hash tree error")]
|
||||
HashTree(#[from] hashtree::Error),
|
||||
#[error("FEC error")]
|
||||
@@ -1851,97 +1853,95 @@ pub fn load_image(mut reader: impl Read + Seek) -> Result<(Header, Option<Footer
|
||||
Ok((header, footer, image_size))
|
||||
}
|
||||
|
||||
/// Write a vbmeta header to the specified writer. If a footer is specified, it
|
||||
/// will be used as the basis of the newly written footer, with the original
|
||||
/// image size, vbmeta header offset, and vbmeta header size fields updated
|
||||
/// appropriately.
|
||||
///
|
||||
/// The writer must not have an existing vbmeta header or footer.
|
||||
fn write_image_internal(
|
||||
mut writer: impl Write + Seek,
|
||||
header: &Header,
|
||||
footer: Option<&mut Footer>,
|
||||
image_size: Option<u64>,
|
||||
block_size: u64,
|
||||
) -> Result<()> {
|
||||
let eof_image_size = if footer.is_some() {
|
||||
match header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => d
|
||||
.image_size
|
||||
.checked_add(d.tree_size)
|
||||
.and_then(|s| s.checked_add(d.fec_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("eof_image_size"))?,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
writer.seek(SeekFrom::Start(eof_image_size))?;
|
||||
|
||||
// The header must be block-aligned.
|
||||
let vbmeta_offset = if block_size > 0 {
|
||||
let padding_size = padding::write_zeros(&mut writer, block_size)?;
|
||||
eof_image_size
|
||||
.checked_add(padding_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("vbmeta_offset"))?
|
||||
} else {
|
||||
eof_image_size
|
||||
};
|
||||
|
||||
header.to_writer(&mut writer)?;
|
||||
let vbmeta_end = writer.stream_position()?;
|
||||
|
||||
if let Some(s) = image_size {
|
||||
let footer_space = if footer.is_some() {
|
||||
cmp::max(block_size, Footer::SIZE as u64)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if s < footer_space || vbmeta_end > s - footer_space {
|
||||
return Err(Error::ImageSizeTooSmall(s));
|
||||
}
|
||||
}
|
||||
|
||||
if block_size > 0 {
|
||||
padding::write_zeros(&mut writer, block_size)?;
|
||||
}
|
||||
|
||||
if let Some(f) = footer {
|
||||
let footer_offset = image_size.unwrap() - Footer::SIZE as u64;
|
||||
writer.seek(SeekFrom::Start(footer_offset))?;
|
||||
|
||||
let original_image_size = match header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => d.image_size,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
};
|
||||
|
||||
f.original_image_size = original_image_size;
|
||||
f.vbmeta_offset = vbmeta_offset;
|
||||
f.vbmeta_size = vbmeta_end - vbmeta_offset;
|
||||
|
||||
f.to_writer(&mut writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a vbmeta header to the specified writer. This is meant for writing
|
||||
/// vbmeta partition images, not appended vbmeta images. The writer must refer
|
||||
/// to an empty file.
|
||||
pub fn write_root_image(writer: impl Write + Seek, header: &Header, block_size: u64) -> Result<()> {
|
||||
write_image_internal(writer, header, None, None, block_size)
|
||||
/// to an empty file. Returns the size of the new file.
|
||||
pub fn write_root_image(writer: impl Write, header: &Header, block_size: u64) -> Result<u64> {
|
||||
let mut counting_writer = CountingWriter::new(writer);
|
||||
|
||||
header.to_writer(&mut counting_writer)?;
|
||||
padding::write_zeros(&mut counting_writer, block_size)?;
|
||||
|
||||
Ok(counting_writer.stream_position()?)
|
||||
}
|
||||
|
||||
/// Write a vbmeta header and footer to the specified writer. This is meant for
|
||||
/// appending vbmeta data to existing partition data, not writing vbmeta images.
|
||||
/// If `image_size` is specified, then the writer is guaranteed to not grow
|
||||
/// past that size and an error is returned if the header and footer won't fit.
|
||||
/// Otherwise, the writer will grow to the necessary size. Returns the size of
|
||||
/// the new file.
|
||||
pub fn write_appended_image(
|
||||
writer: impl Write + Seek,
|
||||
mut writer: impl Write + Seek,
|
||||
header: &Header,
|
||||
footer: &mut Footer,
|
||||
image_size: u64,
|
||||
) -> Result<()> {
|
||||
image_size: Option<u64>,
|
||||
) -> Result<u64> {
|
||||
// avbtool hardcodes a 4096 block size for appended non-sparse images.
|
||||
write_image_internal(writer, header, Some(footer), Some(image_size), 4096)
|
||||
const BLOCK_SIZE: u64 = 4096;
|
||||
|
||||
// Logical image size, excluding the AVB header and footer.
|
||||
let logical_image_size = match header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => d
|
||||
.image_size
|
||||
.checked_add(d.tree_size)
|
||||
.and_then(|s| s.checked_add(d.fec_size))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("logical_image_size"))?,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
};
|
||||
|
||||
writer.seek(SeekFrom::Start(logical_image_size))?;
|
||||
|
||||
// The header start offset must be block aligned.
|
||||
let header_offset = {
|
||||
let padding_size = padding::write_zeros(&mut writer, BLOCK_SIZE)?;
|
||||
logical_image_size
|
||||
.checked_add(padding_size)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("header_offset"))?
|
||||
};
|
||||
|
||||
// The header lives at the beginning of the empty space.
|
||||
let mut header_buf = Cursor::new(Vec::new());
|
||||
header.to_writer(&mut header_buf)?;
|
||||
let header_size = header_buf.stream_position()?;
|
||||
let header_padding = padding::write_zeros(&mut header_buf, BLOCK_SIZE)?;
|
||||
let header_end_padded = header_offset
|
||||
.checked_add(header_size)
|
||||
.and_then(|s| s.checked_add(header_padding))
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("header_end_padded"))?;
|
||||
|
||||
if let Some(s) = image_size {
|
||||
if header_end_padded > s {
|
||||
return Err(Error::TooSmallForHeader(s));
|
||||
}
|
||||
}
|
||||
|
||||
writer.write_all(&header_buf.into_inner())?;
|
||||
|
||||
// The footer lives in its own separate block at the end of the empty space.
|
||||
let footer_end = if let Some(s) = image_size {
|
||||
if s - header_end_padded < BLOCK_SIZE {
|
||||
return Err(Error::TooSmallForFooter(s));
|
||||
}
|
||||
|
||||
s
|
||||
} else {
|
||||
header_end_padded
|
||||
.checked_add(BLOCK_SIZE)
|
||||
.ok_or_else(|| Error::FieldOutOfBounds("footer_end"))?
|
||||
};
|
||||
|
||||
let footer_offset = footer_end - Footer::SIZE as u64;
|
||||
writer.seek(SeekFrom::Start(footer_offset))?;
|
||||
|
||||
footer.original_image_size = match header.appended_descriptor()? {
|
||||
AppendedDescriptorRef::HashTree(d) => d.image_size,
|
||||
AppendedDescriptorRef::Hash(d) => d.image_size,
|
||||
};
|
||||
footer.vbmeta_offset = header_offset;
|
||||
footer.vbmeta_size = header_size;
|
||||
|
||||
footer.to_writer(&mut writer)?;
|
||||
|
||||
Ok(footer_end)
|
||||
}
|
||||
|
||||
@@ -890,6 +890,21 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
Ok((data, digest_compressed))
|
||||
}
|
||||
|
||||
fn compress_cow_size(mut raw_data: &[u8], block_size: u32) -> u64 {
|
||||
let mut total = 0;
|
||||
|
||||
while !raw_data.is_empty() {
|
||||
let n = raw_data.len().min(block_size as usize);
|
||||
let compressed = lz4_flex::block::compress(&raw_data[..n]);
|
||||
|
||||
total += compressed.len().min(n) as u64;
|
||||
|
||||
raw_data = &raw_data[n..];
|
||||
}
|
||||
|
||||
total
|
||||
}
|
||||
|
||||
/// Compress the image and return the corresponding information to insert into
|
||||
/// the payload manifest's [`PartitionUpdate`] instance. The uncompressed data
|
||||
/// is split into 2 MiB chunks, which are read and compressed in parallel, and
|
||||
@@ -897,13 +912,21 @@ fn compress_chunk(raw_data: &[u8], cancel_signal: &AtomicBool) -> Result<(Vec<u8
|
||||
/// a corresponding [`InstallOperation`] in the return value. The caller must
|
||||
/// update [`InstallOperation::data_offset`] in each operation manually because
|
||||
/// the initial values are relative to 0.
|
||||
///
|
||||
/// If `need_cow_estimate` is true, the VABC CoW v2 + lz4 size estimate will be
|
||||
/// computed. The caller must update [`PartitionUpdate::estimate_cow_size`] with
|
||||
/// this value or else update_engine may fail to flash the partition due to
|
||||
/// running out of space on the CoW block device. CoW v2 + other algorithms and
|
||||
/// also CoW v3 are currently unsupported because there currently are no known
|
||||
/// OTAs that use those configurations.
|
||||
pub fn compress_image(
|
||||
input: &(dyn ReadSeekReopen + Sync),
|
||||
output: &(dyn WriteSeekReopen + Sync),
|
||||
partition_name: &str,
|
||||
block_size: u32,
|
||||
need_cow_estimate: bool,
|
||||
cancel_signal: &AtomicBool,
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>)> {
|
||||
) -> Result<(PartitionInfo, Vec<InstallOperation>, Option<u64>)> {
|
||||
const CHUNK_SIZE: u64 = 2 * 1024 * 1024;
|
||||
const CHUNK_GROUP: u64 = 32;
|
||||
|
||||
@@ -921,6 +944,7 @@ pub fn compress_image(
|
||||
let chunks_total = file_size.div_ceil(CHUNK_SIZE);
|
||||
let mut bytes_compressed = 0;
|
||||
let mut context_uncompressed = Context::new(&ring::digest::SHA256);
|
||||
let mut cow_estimate = 0;
|
||||
let mut operations = vec![];
|
||||
|
||||
// Read the file one group at a time. This allows for some parallelization
|
||||
@@ -957,8 +981,13 @@ pub fn compress_image(
|
||||
let mut compressed_data_group = uncompressed_data_group
|
||||
.into_par_iter()
|
||||
.map(
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation)> {
|
||||
|(raw_offset, raw_data)| -> Result<(Vec<u8>, InstallOperation, u64)> {
|
||||
let (data, digest_compressed) = compress_chunk(&raw_data, cancel_signal)?;
|
||||
let cow_size = if need_cow_estimate {
|
||||
compress_cow_size(&raw_data, block_size)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let extent = Extent {
|
||||
start_block: Some(raw_offset / u64::from(block_size)),
|
||||
@@ -971,19 +1000,20 @@ pub fn compress_image(
|
||||
operation.dst_extents.push(extent);
|
||||
operation.data_sha256_hash = Some(digest_compressed.as_ref().to_vec());
|
||||
|
||||
Ok((data, operation))
|
||||
Ok((data, operation, cow_size))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
for (data, operation) in &mut compressed_data_group {
|
||||
for (data, operation, cow_size) in &mut compressed_data_group {
|
||||
operation.data_offset = Some(bytes_compressed);
|
||||
bytes_compressed += data.len() as u64;
|
||||
cow_estimate += *cow_size;
|
||||
}
|
||||
|
||||
let group_operations = compressed_data_group
|
||||
.into_par_iter()
|
||||
.map(|(data, operation)| -> Result<InstallOperation> {
|
||||
.map(|(data, operation, _)| -> Result<InstallOperation> {
|
||||
let mut writer = output.reopen_boxed()?;
|
||||
writer.seek(SeekFrom::Start(operation.data_offset.unwrap()))?;
|
||||
writer.write_all(&data)?;
|
||||
@@ -1001,7 +1031,16 @@ pub fn compress_image(
|
||||
hash: Some(digest_uncompressed.as_ref().to_vec()),
|
||||
};
|
||||
|
||||
Ok((partition_info, operations))
|
||||
let cow_estimate = if need_cow_estimate {
|
||||
// Because lz4_flex compresses better than official lz4.
|
||||
let fudge = cow_estimate / 100;
|
||||
|
||||
Some(cow_estimate + fudge)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((partition_info, operations, cow_estimate))
|
||||
}
|
||||
|
||||
fn extents_sorted(operations: &[InstallOperation]) -> bool {
|
||||
|
||||
+49
-22
@@ -70,7 +70,11 @@ pub enum Error {
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] liblzma::stream::Error),
|
||||
#[error("Zip error")]
|
||||
Zip(#[from] ZipError),
|
||||
Zip(#[source] ZipError),
|
||||
#[error("Zip error for entry name: {0:?}")]
|
||||
ZipEntryName(String, #[source] ZipError),
|
||||
#[error("Zip error for entry index #{0}")]
|
||||
ZipEntryIndex(usize, #[source] ZipError),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("File I/O error")]
|
||||
@@ -157,6 +161,13 @@ impl MagiskRootPatcher {
|
||||
const VER_XZ_BACKUP: Range<u32> =
|
||||
26403..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
|
||||
const ZIP_LIBMAGISK: &'static str = "lib/arm64-v8a/libmagisk.so";
|
||||
const ZIP_LIBMAGISK32: &'static str = "lib/armeabi-v7a/libmagisk32.so";
|
||||
const ZIP_LIBMAGISK64: &'static str = "lib/arm64-v8a/libmagisk64.so";
|
||||
const ZIP_MAGISKINIT: &'static str = "lib/arm64-v8a/libmagiskinit.so";
|
||||
const ZIP_STUB: &'static str = "assets/stub.apk";
|
||||
const ZIP_UTIL_FUNCTIONS: &'static str = "assets/util_functions.sh";
|
||||
|
||||
pub fn new(
|
||||
path: &Path,
|
||||
preinit_device: Option<&str>,
|
||||
@@ -208,8 +219,10 @@ impl MagiskRootPatcher {
|
||||
fn get_version(path: &Path) -> Result<u32> {
|
||||
let reader = File::open(path).map_err(|e| Error::File(path.to_owned(), e))?;
|
||||
let reader = BufReader::new(reader);
|
||||
let mut zip = ZipArchive::new(reader)?;
|
||||
let entry = zip.by_name("assets/util_functions.sh")?;
|
||||
let mut zip = ZipArchive::new(reader).map_err(Error::Zip)?;
|
||||
let entry = zip
|
||||
.by_name(Self::ZIP_UTIL_FUNCTIONS)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_UTIL_FUNCTIONS.to_owned(), e))?;
|
||||
let mut entry = BufReader::new(entry);
|
||||
let mut line = String::new();
|
||||
|
||||
@@ -384,7 +397,7 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
let zip_reader =
|
||||
File::open(&self.apk_path).map_err(|e| Error::File(self.apk_path.clone(), e))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader))?;
|
||||
let mut zip = ZipArchive::new(BufReader::new(zip_reader)).map_err(Error::Zip)?;
|
||||
|
||||
// Load the first ramdisk. If it doesn't exist, we have to generate one
|
||||
// from scratch.
|
||||
@@ -413,7 +426,9 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
|
||||
// Add magiskinit.
|
||||
{
|
||||
let mut zip_entry = zip.by_name("lib/arm64-v8a/libmagiskinit.so")?;
|
||||
let mut zip_entry = zip
|
||||
.by_name(Self::ZIP_MAGISKINIT)
|
||||
.map_err(|e| Error::ZipEntryName(Self::ZIP_MAGISKINIT.to_owned(), e))?;
|
||||
let mut data = vec![];
|
||||
zip_entry.read_to_end(&mut data)?;
|
||||
|
||||
@@ -424,28 +439,33 @@ impl BootImagePatch for MagiskRootPatcher {
|
||||
));
|
||||
}
|
||||
|
||||
// Add xz-compressed magisk32 and magisk64. We currently unconditionally
|
||||
// include magisk32 because the boot image itself doesn't contain
|
||||
// sufficient information to determine if a device is 64-bit only.
|
||||
let mut xz_files = HashMap::<&str, &[u8]>::new();
|
||||
xz_files.insert(
|
||||
"lib/armeabi-v7a/libmagisk32.so",
|
||||
b"overlay.d/sbin/magisk32.xz",
|
||||
);
|
||||
xz_files.insert(
|
||||
"lib/arm64-v8a/libmagisk64.so",
|
||||
b"overlay.d/sbin/magisk64.xz",
|
||||
);
|
||||
if zip.file_names().any(|n| n == Self::ZIP_LIBMAGISK) {
|
||||
// Newer Magisk versions only include a single binary for the target
|
||||
// ABI in the ramdisk. fb5ee86615ed3df830e8538f8b39b1b133caea34.
|
||||
debug!("Single libmagisk");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK, b"overlay.d/sbin/magisk.xz");
|
||||
} else {
|
||||
// Older Magisk versions include the 64-bit binary and, optionally,
|
||||
// the 32-bit binary if the device supports it. We unconditionally
|
||||
// include the magisk32 because the boot image itself doesn't have
|
||||
// sufficient information to determine if a device is 64-bit only.
|
||||
debug!("Split libmagisk32/libmagisk64");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK32, b"overlay.d/sbin/magisk32.xz");
|
||||
xz_files.insert(Self::ZIP_LIBMAGISK64, b"overlay.d/sbin/magisk64.xz");
|
||||
}
|
||||
|
||||
// Add stub apk, which only exists after Magisk commit
|
||||
// ad0e6511e11ebec65aa9b5b916e1397342850319.
|
||||
if zip.file_names().any(|n| n == "assets/stub.apk") {
|
||||
if zip.file_names().any(|n| n == Self::ZIP_STUB) {
|
||||
debug!("Magisk stub found");
|
||||
xz_files.insert("assets/stub.apk", b"overlay.d/sbin/stub.xz");
|
||||
xz_files.insert(Self::ZIP_STUB, b"overlay.d/sbin/stub.xz");
|
||||
}
|
||||
|
||||
for (source, target) in xz_files {
|
||||
let reader = zip.by_name(source)?;
|
||||
let reader = zip
|
||||
.by_name(source)
|
||||
.map_err(|e| Error::ZipEntryName(source.to_owned(), e))?;
|
||||
let buf = Self::xz_compress(reader, cancel_signal)?;
|
||||
|
||||
entries.push(CpioEntry::new_file(target, 0o644, CpioEntryData::Data(buf)));
|
||||
@@ -564,10 +584,12 @@ impl OtaCertPatcher {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data))?;
|
||||
let mut zip = ZipArchive::new(Cursor::new(&data)).map_err(Error::Zip)?;
|
||||
|
||||
for index in 0..zip.len() {
|
||||
let zip_entry = zip.by_index(index)?;
|
||||
let zip_entry = zip
|
||||
.by_index(index)
|
||||
.map_err(|e| Error::ZipEntryIndex(index, e))?;
|
||||
if !zip_entry.name().ends_with(".x509.pem") {
|
||||
debug!("Skipping invalid entry path: {}", zip_entry.name());
|
||||
continue;
|
||||
@@ -1184,7 +1206,12 @@ pub fn patch_boot_images<'a>(
|
||||
info.header.sign(key)?;
|
||||
}
|
||||
|
||||
avb::write_appended_image(writer, &info.header, &mut info.footer, info.image_size)?;
|
||||
avb::write_appended_image(
|
||||
writer,
|
||||
&info.header,
|
||||
&mut info.footer,
|
||||
Some(info.image_size),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
@@ -207,7 +207,7 @@ pub fn patch_system_image(
|
||||
}
|
||||
|
||||
let writer = output.reopen_boxed()?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, image_size)?;
|
||||
avb::write_appended_image(writer, &header, &mut footer, Some(image_size))?;
|
||||
|
||||
let AppendedDescriptorMut::HashTree(descriptor) = header.appended_descriptor_mut()? else {
|
||||
return Err(Error::NoHashTreeDescriptor);
|
||||
|
||||
+112
-5
@@ -228,7 +228,7 @@ fn round_trip_appended_hash_image() {
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
|
||||
let data = writer.into_inner();
|
||||
|
||||
// Verify checksum of the output.
|
||||
@@ -254,8 +254,8 @@ fn round_trip_appended_hash_image() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image() {
|
||||
let image_size = 28672;
|
||||
fn round_trip_appended_hash_tree_image_fixed_size() {
|
||||
let image_size = 32768;
|
||||
let raw_data: [u8; 8192] = repeat_array(b"foobar");
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
@@ -334,7 +334,114 @@ fn round_trip_appended_hash_tree_image() {
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, image_size).unwrap();
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, Some(image_size)).unwrap();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
|
||||
// Verify checksum of the output.
|
||||
assert_eq!(
|
||||
ring::digest::digest(&ring::digest::SHA512, &data).as_ref(),
|
||||
[
|
||||
0xb5, 0x56, 0x65, 0x81, 0x5a, 0x16, 0x65, 0xa9, 0xa6, 0xc6, 0x9e, 0x41, 0x89, 0x9f,
|
||||
0xe9, 0xbc, 0xea, 0x59, 0x4d, 0x14, 0x8a, 0x9e, 0x2b, 0x13, 0xa0, 0x3a, 0x8e, 0xd4,
|
||||
0x59, 0xcd, 0x74, 0xe7, 0x99, 0xbd, 0xa3, 0x58, 0x4b, 0x84, 0xf2, 0x04, 0xe2, 0x12,
|
||||
0x48, 0xfe, 0x4f, 0x67, 0x1f, 0x2a, 0xaa, 0x22, 0x51, 0x19, 0x83, 0x95, 0xa8, 0x03,
|
||||
0xf5, 0x87, 0x12, 0x05, 0x8e, 0x14, 0xd9, 0xbd
|
||||
],
|
||||
);
|
||||
|
||||
// Parse the generated image.
|
||||
let mut reader = Cursor::new(&data);
|
||||
let (new_header, new_footer, new_image_size) = avb::load_image(&mut reader).unwrap();
|
||||
let new_footer = new_footer.unwrap();
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_appended_hash_tree_image_minimum_size() {
|
||||
let raw_data: [u8; 8192] = repeat_array(b"foobar");
|
||||
let mut header = Header {
|
||||
required_libavb_version_major: 1,
|
||||
required_libavb_version_minor: 0,
|
||||
algorithm_type: AlgorithmType::Sha256Rsa4096,
|
||||
hash: vec![], // autogenerated
|
||||
signature: vec![], // autogenerated
|
||||
public_key: vec![], // autogenerated
|
||||
public_key_metadata: vec![
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
|
||||
0x0e, 0x0f,
|
||||
],
|
||||
descriptors: vec![
|
||||
Descriptor::Property(PropertyDescriptor {
|
||||
key: "foobar".to_owned(),
|
||||
value: b"Invalid UTF-8: \xFF".to_vec(),
|
||||
}),
|
||||
Descriptor::HashTree(HashTreeDescriptor {
|
||||
dm_verity_version: 1,
|
||||
image_size: raw_data.len() as u64,
|
||||
tree_offset: 0, // autogenerated
|
||||
tree_size: 0, // autogenerated
|
||||
data_block_size: 4096,
|
||||
hash_block_size: 4096,
|
||||
fec_num_roots: 2,
|
||||
fec_offset: 0, // autogenerated
|
||||
fec_size: 0, // autogenerated
|
||||
hash_algorithm: "sha256".to_owned(),
|
||||
partition_name: "vbmeta_appended_hash_tree".to_owned(),
|
||||
salt: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef].repeat(4),
|
||||
root_digest: vec![], // autogenerated
|
||||
flags: 0,
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
}),
|
||||
],
|
||||
rollback_index: 1677974400,
|
||||
flags: 0,
|
||||
rollback_index_location: 0,
|
||||
release_string: repeat_str("MaxLength", 48),
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
let mut footer = Footer {
|
||||
version_major: 1,
|
||||
version_minor: 0,
|
||||
original_image_size: 0, // autogenerated
|
||||
vbmeta_offset: 0, // autogenerated
|
||||
vbmeta_size: 0, // autogenerated
|
||||
reserved: repeat_array(&[0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]),
|
||||
};
|
||||
|
||||
let mut writer = SharedCursor::default();
|
||||
let cancel_signal = AtomicBool::new(false);
|
||||
|
||||
// Write the raw partition data.
|
||||
writer.write_all(&raw_data).unwrap();
|
||||
|
||||
// Generate and write the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.update(&writer, &writer, None, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Verify the hash tree and FEC data.
|
||||
match header.appended_descriptor_mut().unwrap() {
|
||||
AppendedDescriptorMut::HashTree(d) => {
|
||||
d.verify(&writer, &cancel_signal).unwrap();
|
||||
}
|
||||
AppendedDescriptorMut::Hash(_) => panic!("Expected hash tree descriptor"),
|
||||
}
|
||||
|
||||
// Sign the header.
|
||||
let key = get_test_key();
|
||||
header.sign(&key).unwrap();
|
||||
assert_eq!(header.verify().unwrap().unwrap(), key.to_public_key());
|
||||
|
||||
// Write vbmeta structures.
|
||||
avb::write_appended_image(&mut writer, &header, &mut footer, None).unwrap();
|
||||
let mut data = Vec::new();
|
||||
writer.rewind().unwrap();
|
||||
writer.read_to_end(&mut data).unwrap();
|
||||
@@ -358,5 +465,5 @@ fn round_trip_appended_hash_tree_image() {
|
||||
|
||||
assert_eq!(new_header, header);
|
||||
assert_eq!(new_footer, footer);
|
||||
assert_eq!(new_image_size, image_size);
|
||||
assert_eq!(new_image_size, 28672);
|
||||
}
|
||||
|
||||
+8
-8
@@ -46,8 +46,8 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v4_gki.hashes]
|
||||
original = "24a0a62cc08b96563f4872aee2fdd4a84d1a977b55c326dd9d4ddd92a1d326ea"
|
||||
patched = "29889670efea78bace221742b19e8ace88b67137fc2f46dcd9dbdf67e4e42267"
|
||||
original = "6b140c378d21eae2fa4fc581bce13a689b21bd32f5fba865698d1fd322f2f8c6"
|
||||
patched = "f00e9745f90754be28ce8355501d876759cd8336451e4c3633908fbb4217b422"
|
||||
|
||||
# Google Pixel 6a
|
||||
# What's unique: boot (boot v4, no ramdisk) + vendor_boot (vendor v4, 2 ramdisks)
|
||||
@@ -80,8 +80,8 @@ data.version = "vendor_v4"
|
||||
data.ramdisks = [["init", "otacerts", "first_stage", "dsu_key_dir"], ["dlkm"]]
|
||||
|
||||
[profile.pixel_v4_non_gki.hashes]
|
||||
original = "0e7d0924a68d46e00abe96abfa0e5f3a98d5d15a32bef7401b91fca9a19748e8"
|
||||
patched = "1a2f53d9ac3a1da75e5e7502110bda437c5a725764f16656c564d42460136279"
|
||||
original = "31963e6f81986c6686111f50e36b89e4d85ee5c02bc8e5ecd560528bc98d6fe7"
|
||||
patched = "43959409034dbb9aa0a605d7c5c0e7885012bbcd63510ec87d8e97015b37a746"
|
||||
|
||||
# Google Pixel 4a 5G
|
||||
# What's unique: boot (boot v3) + vendor_boot (vendor v3)
|
||||
@@ -115,8 +115,8 @@ data.version = "vendor_v3"
|
||||
data.ramdisks = [["otacerts", "first_stage", "dsu_key_dir"]]
|
||||
|
||||
[profile.pixel_v3.hashes]
|
||||
original = "533e6f233cb98c98c945044c2ee81a6069e66baee6f7dbcfbf7523795a11215e"
|
||||
patched = "1eeae9dba0302c2d469bd03c8bccdc0469c171204dbd676a20cb6620d2d11c5c"
|
||||
original = "e684aacb54464098c1b8e3f499efe35dff10ea792e89d71a83404620d0108b3e"
|
||||
patched = "08e03ec327bf5bd841b91ad8d53028c3439a1722b423aaaac6cc0ceee4ef66b1"
|
||||
|
||||
# Google Pixel 4a
|
||||
# What's unique: boot (boot v2)
|
||||
@@ -144,5 +144,5 @@ data.type = "vbmeta"
|
||||
data.deps = ["system"]
|
||||
|
||||
[profile.pixel_v2.hashes]
|
||||
original = "958dfa428abd2901178b90147903d7857ed78d6c016f6cb3af30d024a22a8f9a"
|
||||
patched = "b0d18ef350ca7b6499b7de4b0182f4ac3be7288ad238ce888708643e750f7631"
|
||||
original = "ee9568797d9195985f14753b89949d8ebb08c8863a32eceeeec6e8d94661b1cf"
|
||||
patched = "5e265094d4164cedde8f483911c58860f6008b314dc8e5ed3b44deb53fbb2f96"
|
||||
|
||||
+23
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023-2024 Andrew Gunnerson
|
||||
* SPDX-FileCopyrightText: 2023 Pascal Roeleven
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
@@ -196,7 +196,7 @@ fn append_avb(
|
||||
// Give enough free space for changes from patching.
|
||||
.max(1024 * 1024);
|
||||
|
||||
avb::write_appended_image(file, &header, &mut footer, full_image_size)?;
|
||||
avb::write_appended_image(file, &header, &mut footer, Some(full_image_size))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -593,8 +593,14 @@ fn create_payload(
|
||||
.map(PSeekFile::new)
|
||||
.with_context(|| format!("Failed to create temp file for: {name}"))?;
|
||||
|
||||
let (partition_info, operations) =
|
||||
payload::compress_image(file, &writer, name, 4096, cancel_signal)?;
|
||||
let (partition_info, operations, cow_estimate) = payload::compress_image(
|
||||
file,
|
||||
&writer,
|
||||
name,
|
||||
4096,
|
||||
dynamic_partitions_names.contains(name),
|
||||
cancel_signal,
|
||||
)?;
|
||||
|
||||
compressed.insert(name, writer);
|
||||
|
||||
@@ -617,7 +623,7 @@ fn create_payload(
|
||||
fec_roots: None,
|
||||
version: None,
|
||||
merge_operations: vec![],
|
||||
estimate_cow_size: None,
|
||||
estimate_cow_size: cow_estimate,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -638,7 +644,7 @@ fn create_payload(
|
||||
}],
|
||||
snapshot_enabled: Some(true),
|
||||
vabc_enabled: Some(true),
|
||||
vabc_compression_param: Some("gz".to_owned()),
|
||||
vabc_compression_param: Some("lz4".to_owned()),
|
||||
cow_version: Some(2),
|
||||
vabc_feature_set: None,
|
||||
}),
|
||||
@@ -960,6 +966,7 @@ impl KeySet {
|
||||
fn patch_image(
|
||||
input_file: &Path,
|
||||
output_file: &Path,
|
||||
system_image_file: &Path,
|
||||
extra_args: &[&OsStr],
|
||||
keys: &KeySet,
|
||||
cancel_signal: &AtomicBool,
|
||||
@@ -973,6 +980,9 @@ fn patch_image(
|
||||
input_file.as_os_str(),
|
||||
OsStr::new("--output"),
|
||||
output_file.as_os_str(),
|
||||
OsStr::new("--replace"),
|
||||
OsStr::new("system"),
|
||||
system_image_file.as_os_str(),
|
||||
OsStr::new("--key-avb"),
|
||||
keys.avb_key_file.path().as_os_str(),
|
||||
OsStr::new("--pass-avb-file"),
|
||||
@@ -1119,9 +1129,15 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
.with_context(|| format!("[{name}] Failed to verify original OTA hash"))?;
|
||||
|
||||
// Patch once using Magisk.
|
||||
extract_image(&out_original, &profile_dir, cancel_signal)
|
||||
.with_context(|| format!("[{name}] Failed to extract OTA"))?;
|
||||
|
||||
let system_image = profile_dir.join("system.img");
|
||||
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_magisk,
|
||||
&system_image,
|
||||
&args_magisk,
|
||||
&test_keys,
|
||||
cancel_signal,
|
||||
@@ -1149,6 +1165,7 @@ fn test_subcommand(cli: &TestCli, cancel_signal: &AtomicBool) -> Result<()> {
|
||||
patch_image(
|
||||
&out_original,
|
||||
&out_prepatched,
|
||||
&system_image,
|
||||
&args_prepatched,
|
||||
&test_keys,
|
||||
cancel_signal,
|
||||
|
||||
Reference in New Issue
Block a user