mirror of
https://github.com/chenxiaolong/avbroot.git
synced 2026-07-03 14:05:11 +02:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 461392c226 | |||
| 077a80f4ce | |||
| 1e43930f3c | |||
| d00e53fb2a | |||
| 0b5f3b30cf | |||
| 10c2a0969f | |||
| 83218a747d | |||
| 6fb7d568cd | |||
| 6cbf690e17 | |||
| e097f98404 | |||
| d5605eabc0 | |||
| d1bcf2da80 | |||
| bbf8e28a28 | |||
| dc93b17888 | |||
| f83a408dc4 | |||
| 798fddc4d5 | |||
| dfed06dcf8 | |||
| 02fd92a640 | |||
| dd4faf72ae | |||
| adbe249edb | |||
| b7028b13a2 | |||
| 8122b0eece | |||
| 0613de4ee4 | |||
| 1a18eb7f85 | |||
| 1e1ca9dcbf | |||
| 6924783f48 | |||
| 98071daa33 | |||
| 8e1adae947 | |||
| d6705f4f00 | |||
| a648df1695 | |||
| a7c872be3e | |||
| cf1ab6ecca | |||
| 7364e8d725 | |||
| 935a86e72c | |||
| a9a6107043 | |||
| 3c1d5a8bb3 | |||
| 26ec8098e5 | |||
| 2a293147b1 | |||
| aea12c8d58 | |||
| 5545b0fe1b | |||
| 9c818fb165 | |||
| d174af8969 | |||
| 2cf11094de | |||
| 6f565969b7 | |||
| da2eb6b717 | |||
| 2f8d0264eb | |||
| ec1fe74900 | |||
| b3b7c7b738 | |||
| 2f1ee1ae4f |
@@ -30,14 +30,14 @@ runs:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
target/output/e2e
|
||||
target/output/e2e.exe
|
||||
|
||||
- name: Downloading device image for ${{ inputs.device }}
|
||||
if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --stripped -d ${{ inputs.device }}
|
||||
run: ../target/output/e2e download --stripped -d ${{ inputs.device }}
|
||||
|
||||
- if: ${{ ! steps.cache-img.outputs.cache-hit }}
|
||||
name: Creating sparse archive from image
|
||||
|
||||
@@ -21,11 +21,11 @@ runs:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
target/output/e2e
|
||||
target/output/e2e.exe
|
||||
|
||||
- name: Downloading Magisk
|
||||
if: ${{ ! steps.cache-magisk.outputs.cache-hit }}
|
||||
shell: sh
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e download --magisk
|
||||
run: ../target/output/e2e download --magisk
|
||||
|
||||
+35
-18
@@ -16,7 +16,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -C strip=symbols
|
||||
RUSTFLAGS: -C strip=symbols -C target-feature=+crt-static
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -40,28 +40,35 @@ jobs:
|
||||
| sed -E "s/^v//g;s/([^-]*-g)/r\1/;s/-/./g" \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Get Rust LLVM target triple
|
||||
- name: Get Rust target triple
|
||||
id: get_target
|
||||
shell: bash
|
||||
env:
|
||||
RUSTC_BOOTSTRAP: '1'
|
||||
run: |
|
||||
echo -n 'name=' >> "${GITHUB_OUTPUT}"
|
||||
rustc -Z unstable-options --print target-spec-json \
|
||||
| jq -r '."llvm-target"' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
rustc -vV | sed -n 's|host: ||p' >> "${GITHUB_OUTPUT}"
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo clippy --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo build --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: Tests
|
||||
run: cargo test --release --workspace --features static
|
||||
shell: bash
|
||||
run: |
|
||||
cargo test --release --workspace --features static \
|
||||
--target ${{ steps.get_target.outputs.name }}
|
||||
|
||||
- name: Archive documentation
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -71,22 +78,32 @@ jobs:
|
||||
LICENSE
|
||||
README.md
|
||||
|
||||
# Due to https://github.com/rust-lang/rust/issues/78210, we have to use
|
||||
# the --target option, which puts all output files in a different path.
|
||||
# Symlink that path to the normal output directory so that we don't need
|
||||
# to specify the Rust triple everywhere.
|
||||
- name: Symlink target directory
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf target/output
|
||||
ln -s ${{ steps.get_target.outputs.name }}/release target/output
|
||||
|
||||
# This is separate so we can have a flat directory structure.
|
||||
- name: Archive executable
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: avbroot-${{ steps.get_version.outputs.version }}-${{ steps.get_target.outputs.name }}
|
||||
path: |
|
||||
target/release/avbroot
|
||||
target/release/avbroot.exe
|
||||
target/output/avbroot
|
||||
target/output/avbroot.exe
|
||||
|
||||
- name: Cache e2e executable
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
target/output/e2e
|
||||
target/output/e2e.exe
|
||||
|
||||
setup:
|
||||
name: Prepare workflow data
|
||||
@@ -108,8 +125,8 @@ jobs:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
target/output/e2e
|
||||
target/output/e2e.exe
|
||||
|
||||
- name: Loading test config
|
||||
id: load-config
|
||||
@@ -117,7 +134,7 @@ jobs:
|
||||
run: |
|
||||
echo 'config-path=e2e/e2e.toml' >> "${GITHUB_OUTPUT}"
|
||||
echo -n 'device-list=' >> "${GITHUB_OUTPUT}"
|
||||
../target/release/e2e list \
|
||||
../target/output/e2e list \
|
||||
| jq -cnR '[inputs | select(length > 0)]' \
|
||||
>> "${GITHUB_OUTPUT}"
|
||||
|
||||
@@ -209,10 +226,10 @@ jobs:
|
||||
key: e2e-${{ github.sha }}-${{ runner.os }}
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
target/release/e2e
|
||||
target/release/e2e.exe
|
||||
target/output/e2e
|
||||
target/output/e2e.exe
|
||||
|
||||
# Finally run tests
|
||||
- name: Run test for ${{ matrix.device }}
|
||||
working-directory: e2e
|
||||
run: ../target/release/e2e test --stripped -d ${{ matrix.device }}
|
||||
run: ../target/output/e2e test --stripped -d ${{ matrix.device }}
|
||||
|
||||
@@ -7,6 +7,31 @@
|
||||
to update the actual links at the bottom of the file.
|
||||
-->
|
||||
|
||||
### Unreleased
|
||||
|
||||
* Add support for AVB 2.0 format 1.3.0 (for Android 15) ([PR #210])
|
||||
* Add new `avbroot key decode-avb` command for converting AVB-encoded public keys to the standard PKCS8-encoded format ([PR #219])
|
||||
* Adjust AVB sanity check to validate `*_dlkm` and `odm` specifically because some devices have an unprotected `odm_ext` partition ([PR #220])
|
||||
* Add new `--otacerts-partition` option to override the autodetected partition for replacing `otacerts.zip` ([Issue #218], [PR #221])
|
||||
* Build precompiled executables as statically linked executables ([Issue #222], [PR #224], [PR #227])
|
||||
* Limit critical partition check to bootloader-verified partitions ([Issue #223], [PR #226])
|
||||
|
||||
Behind-the-scenes changes:
|
||||
|
||||
* Fix lint warnings introduced in Rust 1.74.0 ([PR #211])
|
||||
* Temporarily silence [RUSTSEC-2023-0071](https://rustsec.org/advisories/RUSTSEC-2023-0071) warning in cargo-deny ([PR #214])
|
||||
|
||||
### Version 2.3.3
|
||||
|
||||
* Add support for XZ-compressed ramdisks ([Issue #203], [PR #207])
|
||||
* Merge property and kernel command line AVB descriptors when replacing partitions ([Issue #203], [PR #208])
|
||||
|
||||
### Version 2.3.2
|
||||
|
||||
* Improve error messages when using `--replace` with an image that has the wrong AVB descriptor type ([Issue #201], [PR #202])
|
||||
* Automatically update legacy `dm=` kernel command line descriptor when packing AVB images ([Issue #203], [PR #205])
|
||||
* Automatically promote insecure hash algorithms (eg. sha1) to sha256 when packing AVB images ([Issue #203], [PR #206])
|
||||
|
||||
### Version 2.3.1
|
||||
|
||||
* Mark Magisk 264xx as supported ([PR #199])
|
||||
@@ -104,6 +129,11 @@ 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
|
||||
[Issue #218]: https://github.com/chenxiaolong/avbroot/issues/218
|
||||
[Issue #222]: https://github.com/chenxiaolong/avbroot/issues/222
|
||||
[Issue #223]: https://github.com/chenxiaolong/avbroot/issues/223
|
||||
[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
|
||||
@@ -148,3 +178,17 @@ Behind-the-scenes changes:
|
||||
[PR #196]: https://github.com/chenxiaolong/avbroot/pull/196
|
||||
[PR #197]: https://github.com/chenxiaolong/avbroot/pull/197
|
||||
[PR #199]: https://github.com/chenxiaolong/avbroot/pull/199
|
||||
[PR #202]: https://github.com/chenxiaolong/avbroot/pull/202
|
||||
[PR #205]: https://github.com/chenxiaolong/avbroot/pull/205
|
||||
[PR #206]: https://github.com/chenxiaolong/avbroot/pull/206
|
||||
[PR #207]: https://github.com/chenxiaolong/avbroot/pull/207
|
||||
[PR #208]: https://github.com/chenxiaolong/avbroot/pull/208
|
||||
[PR #210]: https://github.com/chenxiaolong/avbroot/pull/210
|
||||
[PR #211]: https://github.com/chenxiaolong/avbroot/pull/211
|
||||
[PR #214]: https://github.com/chenxiaolong/avbroot/pull/214
|
||||
[PR #219]: https://github.com/chenxiaolong/avbroot/pull/219
|
||||
[PR #220]: https://github.com/chenxiaolong/avbroot/pull/220
|
||||
[PR #221]: https://github.com/chenxiaolong/avbroot/pull/221
|
||||
[PR #224]: https://github.com/chenxiaolong/avbroot/pull/224
|
||||
[PR #226]: https://github.com/chenxiaolong/avbroot/pull/226
|
||||
[PR #227]: https://github.com/chenxiaolong/avbroot/pull/227
|
||||
|
||||
Generated
+4
-4
@@ -121,7 +121,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "avbroot"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_matches",
|
||||
@@ -565,7 +565,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "e2e"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc",
|
||||
@@ -679,7 +679,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fuzz"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
dependencies = [
|
||||
"avbroot",
|
||||
"honggfuzz",
|
||||
@@ -2104,7 +2104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "2.3.1"
|
||||
version = "2.3.3"
|
||||
license = "GPL-3.0-only"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/chenxiaolong/avbroot"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,48 +1,114 @@
|
||||
# avbroot
|
||||
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU.
|
||||
avbroot is a program for patching Android A/B-style OTA images for root access while preserving AVB (Android Verified Boot) using custom signing keys. It is compatible with both Magisk and KernelSU. If desired, it can also just re-sign an OTA without enabling root access.
|
||||
|
||||
Having a good understanding of how AVB and A/B OTAs work is recommended prior to using avbroot. At the very least, please make sure the [warnings and caveats](#warnings-and-caveats) are well-understood to avoid the risk of hard bricking.
|
||||
|
||||
**NOTE:** avbroot 2.0 has been rewritten in Rust and no longer relies on any AOSP code. The CLI is fully backwards compatible, but the old Python implementation can be found in the `python` branch if needed.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Only devices that use modern A/B partitioning are supported. This is the case for most non-Samsung devices launched with Android 10 or newer. To check if a device uses this partitioning scheme, open the OTA zip file and check that:
|
||||
|
||||
* `payload.bin` exists
|
||||
* `META-INF/com/android/metadata` (Android 10-11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
|
||||
|
||||
* The device must support using a custom public key for the bootloader's root of trust. This is normally done via the `fastboot flash avb_custom_key` command. All Pixel devices with unlockable bootloaders since the Pixel 2 support this. Other devices may support it as well, but there's no easy way to check without just trying it.
|
||||
|
||||
* NOTE: Some OnePlus devices have a broken implementation where a custom public key can be set, but the device won't boot despite having proper signatures. Downgrading the bootloader to the version shipped with Android 11 might potentially help. This problem has been reported across multiple OnePlus models ([#186](https://github.com/chenxiaolong/avbroot/issues/186), [#195](https://github.com/chenxiaolong/avbroot/discussions/195), [#212](https://github.com/chenxiaolong/avbroot/issues/212)).
|
||||
|
||||
## Patches
|
||||
|
||||
avbroot applies two patches to the boot images:
|
||||
|
||||
* 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 modern (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning scheme, 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` (Android 11) or `META-INF/com/android/metadata.pb` (Android 12+) exists
|
||||
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
|
||||
@@ -55,44 +121,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 \
|
||||
--key-avb /path/to/avb.key \
|
||||
--key-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 \
|
||||
@@ -100,23 +145,31 @@ 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]** Reboot into Android and run:
|
||||
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'
|
||||
@@ -128,27 +181,35 @@ If you lose your AVB or OTA signing key, you will no longer be able to sign new
|
||||
init: [libfs_avb]Returning avb_handle with status: Success
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
5. That's it!
|
||||
|
||||
## Reverting to stock firmware
|
||||
|
||||
To stop using avbroot and revert to the stock firmware:
|
||||
|
||||
1. Unlock the bootloader. This will trigger a data wipe.
|
||||
1. Reboot into fastboot mode and unlock the bootloader. This will trigger a data wipe.
|
||||
|
||||
2. Erase the custom AVB public key.
|
||||
|
||||
@@ -158,35 +219,34 @@ To stop using avbroot and revert to the stock firmware:
|
||||
|
||||
3. Flash the stock firmware.
|
||||
|
||||
## avbroot Magisk modules
|
||||
4. That's it! There are no other remnants to clean up.
|
||||
|
||||
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:
|
||||
## avbroot modules
|
||||
|
||||
```bash
|
||||
cargo xtask modules -a
|
||||
```
|
||||
avbroot's Magisk/KernelSU modules can be downloaded from the [releases page](https://github.com/chenxiaolong/avbroot/releases).
|
||||
|
||||
This requires Java and the Android SDK to be installed. The `ANDROID_HOME` environment variable should be set to the Android SDK path.
|
||||
### `clearotacerts`: Block OTA Updates from default updater app
|
||||
|
||||
### `clearotacerts`: Blocking A/B OTA Updates
|
||||
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.
|
||||
|
||||
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.
|
||||
Disabling the system updater app is recommended. To do so:
|
||||
|
||||
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.
|
||||
* 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).
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Alternatively, see [Custota](https://github.com/chenxiaolong/Custota) for a custom OTA updater app that pulls from a self-hosted OTA server.
|
||||
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:
|
||||
|
||||
@@ -197,9 +257,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>
|
||||
@@ -229,6 +289,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
|
||||
@@ -271,15 +333,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
|
||||
|
||||
@@ -294,7 +356,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`.
|
||||
@@ -303,7 +365,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 \
|
||||
@@ -358,11 +420,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
|
||||
|
||||
+83
-16
@@ -117,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..26500];
|
||||
const VERS_SUPPORTED: &'static [Range<u32>] = &[25102..25207, 25211..26500];
|
||||
const VER_PREINIT_DEVICE: Range<u32> =
|
||||
25211..Self::VERS_SUPPORTED[Self::VERS_SUPPORTED.len() - 1].end;
|
||||
const VER_RANDOM_SEED: Range<u32> = 25211..26103;
|
||||
@@ -407,7 +407,7 @@ pub struct OtaCertPatcher {
|
||||
}
|
||||
|
||||
impl OtaCertPatcher {
|
||||
const OTACERTS_PATH: &[u8] = b"system/etc/security/otacerts.zip";
|
||||
const OTACERTS_PATH: &'static [u8] = b"system/etc/security/otacerts.zip";
|
||||
|
||||
pub fn new(cert: Certificate) -> Self {
|
||||
Self { cert }
|
||||
@@ -452,25 +452,28 @@ impl OtaCertPatcher {
|
||||
Ok(certificates)
|
||||
}
|
||||
|
||||
/// Create a new otacerts archive. The old certs are ignored since flashing
|
||||
/// a stock OTA will render the device unbootable.
|
||||
fn create_zip(cert: &Certificate) -> Result<Vec<u8>> {
|
||||
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)?;
|
||||
|
||||
crypto::write_pem_cert(&mut writer, cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
|
||||
Ok(raw_writer.into_inner())
|
||||
}
|
||||
|
||||
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::new());
|
||||
let mut writer = ZipWriter::new(raw_writer);
|
||||
let options = FileOptions::default().compression_method(CompressionMethod::Stored);
|
||||
writer.start_file("ota.x509.pem", options)?;
|
||||
|
||||
crypto::write_pem_cert(&mut writer, &self.cert)?;
|
||||
|
||||
let raw_writer = writer.finish()?;
|
||||
entry.data = CpioEntryData::Data(raw_writer.into_inner());
|
||||
}
|
||||
entry.data = CpioEntryData::Data(Self::create_zip(&self.cert)?);
|
||||
|
||||
// Repack ramdisk.
|
||||
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
@@ -511,6 +514,70 @@ impl BootImagePatcher for OtaCertPatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace /init with a wrapper that will bind mount otacerts.zip containing
|
||||
/// the custom OTA certificate into the system partition.
|
||||
pub struct InitWrapperPatcher {
|
||||
cert: Certificate,
|
||||
}
|
||||
|
||||
impl InitWrapperPatcher {
|
||||
pub fn new(cert: Certificate) -> Self {
|
||||
Self { cert }
|
||||
}
|
||||
|
||||
fn patch_ramdisk(&self, data: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
|
||||
let (mut entries, ramdisk_format) = load_ramdisk(data, cancel_signal)?;
|
||||
|
||||
if let Some(entry) = entries.iter_mut().find(|e| e.path == b"init") {
|
||||
entry.path = b"avbroot/init.orig".to_vec();
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
entries.push(CpioEntry::new_directory(b"avbroot", 0o755));
|
||||
entries.push(CpioEntry::new_file(b"avbroot/otacerts.zip", 0o644,
|
||||
CpioEntryData::Data(OtaCertPatcher::create_zip(&self.cert)?)));
|
||||
|
||||
// TODO
|
||||
// How do we pick ABI
|
||||
let init_data = std::fs::read("/home/chenxiaolong/git/github/avbroot/init/libs/arm64-v8a/init").unwrap();
|
||||
entries.push(CpioEntry::new_file(b"init", 0o750, CpioEntryData::Data(init_data)));
|
||||
|
||||
cpio::sort(&mut entries);
|
||||
|
||||
*data = save_ramdisk(&entries, ramdisk_format, cancel_signal)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl BootImagePatcher for InitWrapperPatcher {
|
||||
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, 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, cancel_signal)? {
|
||||
patched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
patched
|
||||
}
|
||||
};
|
||||
|
||||
if !patched_any {
|
||||
return Err(Error::Validation("No ramdisk contains init".to_owned()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the boot image with a prepatched boot image if it is compatible.
|
||||
///
|
||||
/// An image is compatible if all the non-size-related header fields are
|
||||
@@ -528,7 +595,7 @@ impl PrepatchedImagePatcher {
|
||||
const MAX_LEVEL: u8 = 2;
|
||||
|
||||
// We compile without Unicode support so we have to use [0-9] instead of \d.
|
||||
const VERSION_REGEX: &str = r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
|
||||
const VERSION_REGEX: &'static str = r"Linux version ([0-9]+\.[0-9]+).[0-9]+-(android[0-9]+)-([0-9]+)-";
|
||||
|
||||
pub fn new(
|
||||
prepatched: &Path,
|
||||
|
||||
+141
-3
@@ -28,7 +28,7 @@ use crate::{
|
||||
crypto::{self, PassphraseSource},
|
||||
format::avb::{
|
||||
self, AlgorithmType, AppendedDescriptorMut, AppendedDescriptorRef, Descriptor, Footer,
|
||||
Header,
|
||||
HashTreeDescriptor, Header, KernelCmdlineDescriptor,
|
||||
},
|
||||
stream::{self, PSeekFile, Reopen},
|
||||
util,
|
||||
@@ -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(
|
||||
@@ -179,11 +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(&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)
|
||||
@@ -194,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
|
||||
@@ -481,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)
|
||||
@@ -507,11 +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.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)
|
||||
|
||||
@@ -71,6 +71,16 @@ pub fn key_main(cli: &KeyCli) -> Result<()> {
|
||||
fs::write(&c.output, encoded)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
KeyCommand::DecodeAvb(c) => {
|
||||
let encoded = fs::read(&c.key)
|
||||
.with_context(|| format!("Failed to load AVB public key: {:?}", c.key))?;
|
||||
|
||||
let public_key = avb::decode_public_key(&encoded)
|
||||
.context("Failed to decode public key as AVB format")?;
|
||||
|
||||
crypto::write_pem_public_key_file(&c.output, &public_key)
|
||||
.with_context(|| format!("Failed to write public key: {:?}", c.output))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -152,11 +162,24 @@ struct ExtractAvbCli {
|
||||
passphrase: PassphraseGroup,
|
||||
}
|
||||
|
||||
/// Convert an AVB-encoded public key to a PKCS8-encoded public key.
|
||||
#[derive(Debug, Parser)]
|
||||
struct DecodeAvbCli {
|
||||
/// Path to output PKCS8-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
output: PathBuf,
|
||||
|
||||
/// Path to AVB-encoded public key.
|
||||
#[arg(short, long, value_name = "FILE", value_parser)]
|
||||
key: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum KeyCommand {
|
||||
GenerateKey(GenerateKeyCli),
|
||||
GenerateCert(GenerateCertCli),
|
||||
ExtractAvb(ExtractAvbCli),
|
||||
DecodeAvb(DecodeAvbCli),
|
||||
}
|
||||
|
||||
/// Generate and convert keys.
|
||||
|
||||
+184
-71
@@ -28,7 +28,7 @@ use x509_cert::Certificate;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipArchive, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
boot::{self, BootImagePatcher, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher},
|
||||
boot::{self, BootImagePatcher, MagiskRootPatcher, OtaCertPatcher, PrepatchedImagePatcher, InitWrapperPatcher},
|
||||
cli::{self, status, warning},
|
||||
crypto::{self, PassphraseSource},
|
||||
format::{
|
||||
@@ -115,8 +115,8 @@ pub fn get_partitions_by_type(manifest: &DeltaArchiveManifest) -> Result<HashMap
|
||||
/// partitions.
|
||||
pub fn get_required_images(
|
||||
manifest: &DeltaArchiveManifest,
|
||||
boot_partition: &str,
|
||||
with_root: bool,
|
||||
rootpatch_partition: Option<&str>,
|
||||
otacerts_partition: Option<&str>,
|
||||
) -> Result<HashMap<String, String>> {
|
||||
let all_partitions = manifest
|
||||
.partitions
|
||||
@@ -127,18 +127,23 @@ pub fn get_required_images(
|
||||
let mut images = HashMap::new();
|
||||
|
||||
for (k, v) in &by_type {
|
||||
if k == "@otacerts" || k.starts_with("@vbmeta:") {
|
||||
if k == "@otacerts" || k == "@gki_ramdisk" || k.starts_with("@vbmeta:") {
|
||||
images.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if with_root {
|
||||
if by_type.contains_key(boot_partition) {
|
||||
images.insert("@rootpatch".to_owned(), by_type[boot_partition].clone());
|
||||
} else if all_partitions.contains(boot_partition) {
|
||||
images.insert("@rootpatch".to_owned(), boot_partition.to_owned());
|
||||
} else {
|
||||
bail!("Boot partition not found: {boot_partition}");
|
||||
for (k, v) in [
|
||||
("@rootpatch", rootpatch_partition),
|
||||
("@otacerts", otacerts_partition),
|
||||
] {
|
||||
if let Some(name) = v {
|
||||
if by_type.contains_key(name) {
|
||||
images.insert(k.to_owned(), by_type[name].clone());
|
||||
} else if all_partitions.contains(name) {
|
||||
images.insert(k.to_owned(), name.to_owned());
|
||||
} else {
|
||||
bail!("{k} partition not found: {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +214,12 @@ fn patch_boot_images(
|
||||
.push(p);
|
||||
}
|
||||
|
||||
// We want our init wrapper to be the entry point, so do this last.
|
||||
boot_patchers
|
||||
.entry(&required_images["@gki_ramdisk"])
|
||||
.or_default()
|
||||
.push(Box::new(InitWrapperPatcher::new(cert_ota.clone())));
|
||||
|
||||
status!(
|
||||
"Patching boot images: {}",
|
||||
joined(sorted(boot_patchers.keys()))
|
||||
@@ -266,19 +277,12 @@ fn load_vbmeta_images(
|
||||
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.
|
||||
/// Check if a partition is critical to AVB's chain of trust and is meant to be
|
||||
/// validated by the bootloader instead of Android. dm-verity partitions cannot
|
||||
/// be statically checked without causing false positives because the fstab
|
||||
/// might be using the avb_keys=/path/to/pubkey option.
|
||||
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"
|
||||
name.ends_with("boot") || name.starts_with("vbmeta")
|
||||
}
|
||||
|
||||
/// Check that all critical partitions within the payload are protected by a
|
||||
@@ -400,6 +404,128 @@ fn get_vbmeta_patch_order(
|
||||
Ok(order)
|
||||
}
|
||||
|
||||
/// Copy the hash or hashtree descriptor from the child image header into the
|
||||
/// parent image header if the child is unsigned or update the parent's chain
|
||||
/// descriptor if the child is signed. The existing descriptor in the parent
|
||||
/// must have the same type as the child.
|
||||
fn update_security_descriptors(
|
||||
parent_header: &mut Header,
|
||||
child_header: &Header,
|
||||
parent_name: &str,
|
||||
child_name: &str,
|
||||
) -> Result<()> {
|
||||
// This can't fail since the descriptor must have existed for the dependency
|
||||
// to exist.
|
||||
let parent_descriptor = parent_header
|
||||
.descriptors
|
||||
.iter_mut()
|
||||
.find(|d| d.partition_name() == Some(child_name))
|
||||
.unwrap();
|
||||
let parent_type = parent_descriptor.type_name();
|
||||
|
||||
if child_header.public_key.is_empty() {
|
||||
// vbmeta is unsigned. Copy the child's existing descriptor.
|
||||
let Some(child_descriptor) = child_header
|
||||
.descriptors
|
||||
.iter()
|
||||
.find(|d| d.partition_name() == Some(child_name))
|
||||
else {
|
||||
bail!("{child_name} has no descriptor for itself");
|
||||
};
|
||||
let child_type = child_descriptor.type_name();
|
||||
|
||||
match (parent_descriptor, child_descriptor) {
|
||||
(Descriptor::Hash(pd), Descriptor::Hash(cd)) => {
|
||||
*pd = cd.clone();
|
||||
}
|
||||
(Descriptor::HashTree(pd), Descriptor::HashTree(cd)) => {
|
||||
*pd = cd.clone();
|
||||
}
|
||||
_ => {
|
||||
bail!("{child_name} descriptor ({child_type}) does not match entry in {parent_name} ({parent_type})");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// vbmeta is signed; Use a chain descriptor.
|
||||
match parent_descriptor {
|
||||
Descriptor::ChainPartition(pd) => {
|
||||
pd.public_key = child_header.public_key.clone();
|
||||
}
|
||||
_ => {
|
||||
bail!("{child_name} descriptor ({parent_type}) in {parent_name} must be a chain descriptor");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the text before the first equal sign in the kernel command line if it is
|
||||
/// not empty.
|
||||
fn cmdline_prefix(cmdline: &str) -> Option<&str> {
|
||||
let Some((prefix, _)) = cmdline.split_once('=') else {
|
||||
return None;
|
||||
};
|
||||
if prefix.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(prefix)
|
||||
}
|
||||
|
||||
/// Merge property descriptors and kernel command line descriptors from the
|
||||
/// child into the parent. The property descriptors are matched based on the
|
||||
/// entire property key. The kernel command line descriptors are matched based
|
||||
/// on the non-empty text left of the first equal sign (if it exists).
|
||||
///
|
||||
/// This is a no-op if the child is signed because it is expected to be chain
|
||||
/// loaded by the parent.
|
||||
fn update_metadata_descriptors(parent_header: &mut Header, child_header: &Header) {
|
||||
if !child_header.public_key.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for child_descriptor in &child_header.descriptors {
|
||||
match child_descriptor {
|
||||
Descriptor::Property(cd) => {
|
||||
let parent_property = parent_header.descriptors.iter_mut().find_map(|d| match d {
|
||||
Descriptor::Property(p) if p.key == cd.key => Some(p),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(pd) = parent_property {
|
||||
pd.value = cd.value.clone();
|
||||
} else {
|
||||
parent_header
|
||||
.descriptors
|
||||
.push(Descriptor::Property(cd.clone()));
|
||||
}
|
||||
}
|
||||
Descriptor::KernelCmdline(cd) => {
|
||||
let Some(prefix) = cmdline_prefix(&cd.cmdline) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let parent_property = parent_header.descriptors.iter_mut().find_map(|d| match d {
|
||||
Descriptor::KernelCmdline(p) if cmdline_prefix(&p.cmdline) == Some(prefix) => {
|
||||
Some(p)
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(pd) = parent_property {
|
||||
pd.cmdline = cd.cmdline.clone();
|
||||
} else {
|
||||
parent_header
|
||||
.descriptors
|
||||
.push(Descriptor::KernelCmdline(cd.clone()));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update vbmeta headers.
|
||||
///
|
||||
/// * If [`Header::flags`] is non-zero, then an error is returned because the
|
||||
@@ -438,50 +564,12 @@ fn update_vbmeta_headers(
|
||||
}
|
||||
|
||||
for dep in deps.iter() {
|
||||
// This can't fail since the descriptor must have existed for the
|
||||
// dependency to exist.
|
||||
let parent_descriptor = parent_header
|
||||
.descriptors
|
||||
.iter_mut()
|
||||
.find(|d| d.partition_name() == Some(dep))
|
||||
.unwrap();
|
||||
|
||||
let reader = images.get_mut(dep).unwrap();
|
||||
let (header, _, _) = avb::load_image(reader)
|
||||
.with_context(|| format!("Failed to load vbmeta footer from image: {dep}"))?;
|
||||
|
||||
if header.public_key.is_empty() {
|
||||
// vbmeta is unsigned. Use the existing descriptor.
|
||||
let Some(descriptor) = header
|
||||
.descriptors
|
||||
.iter()
|
||||
.find(|d| d.partition_name() == Some(dep))
|
||||
else {
|
||||
bail!("{name} has no descriptor for itself");
|
||||
};
|
||||
|
||||
match (parent_descriptor, descriptor) {
|
||||
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
|
||||
*pd = d.clone();
|
||||
}
|
||||
(Descriptor::HashTree(pd), Descriptor::HashTree(d)) => {
|
||||
*pd = d.clone();
|
||||
}
|
||||
_ => {
|
||||
bail!("{name}'s descriptor for {dep} must match {dep}'s self descriptor");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// vbmeta is signed; Use a chain descriptor.
|
||||
match parent_descriptor {
|
||||
Descriptor::ChainPartition(d) => {
|
||||
d.public_key = header.public_key;
|
||||
}
|
||||
_ => {
|
||||
bail!("{name}'s descriptor for {dep} must be a chain descriptor");
|
||||
}
|
||||
}
|
||||
}
|
||||
update_security_descriptors(parent_header, &header, name, dep)?;
|
||||
update_metadata_descriptors(parent_header, &header);
|
||||
}
|
||||
|
||||
// Only sign and rewrite the image if we need to. Some vbmeta images may
|
||||
@@ -549,7 +637,8 @@ fn patch_ota_payload(
|
||||
payload: &(dyn ReadSeekReopen + Sync),
|
||||
writer: impl Write,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_partition: &str,
|
||||
rootpatch_partition: Option<&str>,
|
||||
otacerts_partition: Option<&str>,
|
||||
root_patcher: Option<Box<dyn BootImagePatcher + Send>>,
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaPrivateKey,
|
||||
@@ -585,8 +674,8 @@ fn patch_ota_payload(
|
||||
// don't need to be modified.
|
||||
let required_images = get_required_images(
|
||||
&header_locked.manifest,
|
||||
boot_partition,
|
||||
root_patcher.is_some(),
|
||||
rootpatch_partition,
|
||||
otacerts_partition,
|
||||
)?;
|
||||
let vbmeta_images = required_images
|
||||
.iter()
|
||||
@@ -726,7 +815,8 @@ fn patch_ota_zip(
|
||||
zip_reader: &mut ZipArchive<impl Read + Seek>,
|
||||
mut zip_writer: &mut ZipWriter<impl Write>,
|
||||
external_images: &HashMap<String, PathBuf>,
|
||||
boot_partition: &str,
|
||||
rootpatch_partition: Option<&str>,
|
||||
otacerts_partition: Option<&str>,
|
||||
mut root_patch: Option<Box<dyn BootImagePatcher + Send>>,
|
||||
clear_vbmeta_flags: bool,
|
||||
key_avb: &RsaPrivateKey,
|
||||
@@ -846,7 +936,8 @@ fn patch_ota_zip(
|
||||
&payload_reader,
|
||||
&mut writer,
|
||||
external_images,
|
||||
boot_partition,
|
||||
rootpatch_partition,
|
||||
otacerts_partition,
|
||||
// There's only one payload in the OTA.
|
||||
root_patch.take(),
|
||||
clear_vbmeta_flags,
|
||||
@@ -1053,7 +1144,8 @@ pub fn patch_subcommand(cli: &PatchCli, cancel_signal: &AtomicBool) -> Result<()
|
||||
&mut zip_reader,
|
||||
&mut zip_writer,
|
||||
&external_images,
|
||||
&cli.boot_partition,
|
||||
Some(&cli.boot_partition),
|
||||
cli.otacerts_partition.as_deref(),
|
||||
root_patcher,
|
||||
cli.clear_vbmeta_flags,
|
||||
&key_avb,
|
||||
@@ -1154,7 +1246,11 @@ pub fn extract_subcommand(cli: &ExtractCli, cancel_signal: &AtomicBool) -> Resul
|
||||
.cloned(),
|
||||
);
|
||||
} else {
|
||||
let images = get_required_images(&header.manifest, &cli.boot_partition, true)?;
|
||||
let images = get_required_images(
|
||||
&header.manifest,
|
||||
Some(&cli.boot_partition),
|
||||
cli.otacerts_partition.as_deref(),
|
||||
)?;
|
||||
|
||||
if cli.boot_only {
|
||||
unique_images.insert(images["@rootpatch"].clone());
|
||||
@@ -1256,7 +1352,8 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
|
||||
status!("Checking ramdisk's otacerts.zip");
|
||||
|
||||
let boot_image = {
|
||||
let partitions_by_type = get_partitions_by_type(&header.manifest)?;
|
||||
let partitions_by_type =
|
||||
get_required_images(&header.manifest, None, cli.otacerts_partition.as_deref())?;
|
||||
let path = format!("{}.img", partitions_by_type["@otacerts"]);
|
||||
let file = temp_dir
|
||||
.open(&path)
|
||||
@@ -1474,6 +1571,14 @@ pub struct PatchCli {
|
||||
help_heading = HEADING_OTHER
|
||||
)]
|
||||
pub boot_partition: String,
|
||||
|
||||
/// OTA certificates partition name.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PARTITION",
|
||||
help_heading = HEADING_OTHER
|
||||
)]
|
||||
pub otacerts_partition: Option<String>,
|
||||
}
|
||||
|
||||
/// Extract partition images from an OTA zip's payload.
|
||||
@@ -1498,6 +1603,10 @@ pub struct ExtractCli {
|
||||
/// Boot partition name.
|
||||
#[arg(long, value_name = "PARTITION", default_value = "@gki_ramdisk")]
|
||||
pub boot_partition: String,
|
||||
|
||||
/// OTA certificates partition name.
|
||||
#[arg(long, value_name = "PARTITION")]
|
||||
pub otacerts_partition: Option<String>,
|
||||
}
|
||||
|
||||
/// Verify signatures of an OTA.
|
||||
@@ -1522,6 +1631,10 @@ pub struct VerifyCli {
|
||||
/// valid, not that they are trusted.
|
||||
#[arg(long, value_name = "FILE", value_parser)]
|
||||
pub public_key_avb: Option<PathBuf>,
|
||||
|
||||
/// OTA certificates partition name.
|
||||
#[arg(long, value_name = "PARTITION")]
|
||||
pub otacerts_partition: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
|
||||
@@ -225,6 +225,23 @@ pub fn write_pem_cert_file(path: &Path, cert: &Certificate) -> Result<()> {
|
||||
write_pem_cert(writer, cert)
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a writer.
|
||||
pub fn write_pem_public_key(mut writer: impl Write, key: &RsaPublicKey) -> Result<()> {
|
||||
let data = key.to_public_key_pem(LineEnding::LF)?;
|
||||
|
||||
writer.write_all(data.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write PEM-encoded PKCS8 public key to a file.
|
||||
pub fn write_pem_public_key_file(path: &Path, key: &RsaPublicKey) -> Result<()> {
|
||||
let file = File::create(path)?;
|
||||
let writer = BufWriter::new(file);
|
||||
|
||||
write_pem_public_key(writer, key)
|
||||
}
|
||||
|
||||
/// Read PEM-encoded PKCS8 private key from a reader.
|
||||
pub fn read_pem_key(mut reader: impl Read, source: &PassphraseSource) -> Result<RsaPrivateKey> {
|
||||
let mut data = String::new();
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub const VERSION_MAJOR: u32 = 1;
|
||||
pub const VERSION_MINOR: u32 = 2;
|
||||
pub const VERSION_MINOR: u32 = 3;
|
||||
pub const VERSION_SUB: u32 = 0;
|
||||
|
||||
pub const FOOTER_VERSION_MAJOR: u32 = 1;
|
||||
@@ -379,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
|
||||
@@ -1053,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;
|
||||
}
|
||||
@@ -1101,8 +1109,13 @@ pub struct ChainPartitionDescriptor {
|
||||
pub partition_name: String,
|
||||
#[serde(with = "hex")]
|
||||
pub public_key: Vec<u8>,
|
||||
pub flags: u32,
|
||||
#[serde(with = "hex")]
|
||||
pub reserved: [u8; 64],
|
||||
pub reserved: [u8; 60],
|
||||
}
|
||||
|
||||
impl ChainPartitionDescriptor {
|
||||
pub const FLAG_DO_NOT_USE_AB: u32 = 1 << 0;
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChainPartitionDescriptor {
|
||||
@@ -1111,6 +1124,7 @@ impl fmt::Debug for ChainPartitionDescriptor {
|
||||
.field("rollback_index_location", &self.rollback_index_location)
|
||||
.field("partition_name", &self.partition_name)
|
||||
.field("public_key", &hex::encode(&self.public_key))
|
||||
.field("flags", &self.flags)
|
||||
.field("reserved", &hex::encode(self.reserved))
|
||||
.finish()
|
||||
}
|
||||
@@ -1134,7 +1148,9 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
|
||||
return Err(Error::FieldOutOfBounds("public_key_len"));
|
||||
}
|
||||
|
||||
let mut reserved = [0u8; 64];
|
||||
let flags = reader.read_u32::<BigEndian>()?;
|
||||
|
||||
let mut reserved = [0u8; 60];
|
||||
reader.read_exact(&mut reserved)?;
|
||||
|
||||
// Not NULL-terminated.
|
||||
@@ -1149,6 +1165,7 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
|
||||
rollback_index_location,
|
||||
partition_name,
|
||||
public_key,
|
||||
flags,
|
||||
reserved,
|
||||
};
|
||||
|
||||
@@ -1169,6 +1186,7 @@ impl<W: Write> ToWriter<W> for ChainPartitionDescriptor {
|
||||
writer.write_u32::<BigEndian>(self.rollback_index_location)?;
|
||||
writer.write_u32::<BigEndian>(self.partition_name.len() as u32)?;
|
||||
writer.write_u32::<BigEndian>(self.public_key.len() as u32)?;
|
||||
writer.write_u32::<BigEndian>(self.flags)?;
|
||||
writer.write_all(&self.reserved)?;
|
||||
writer.write_all(self.partition_name.as_bytes())?;
|
||||
writer.write_all(&self.public_key)?;
|
||||
@@ -1193,6 +1211,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),
|
||||
|
||||
@@ -10,14 +10,22 @@ use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
use lz4_flex::frame::FrameDecoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use xz2::{
|
||||
read::XzDecoder,
|
||||
stream::{Check, Stream},
|
||||
write::XzEncoder,
|
||||
};
|
||||
|
||||
static GZIP_MAGIC: &[u8; 2] = b"\x1f\x8b";
|
||||
static LZ4_LEGACY_MAGIC: &[u8; 4] = b"\x02\x21\x4c\x18";
|
||||
static XZ_MAGIC: &[u8; 6] = b"\xfd\x37\x7a\x58\x5a\x00";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unknown compression format")]
|
||||
UnknownFormat,
|
||||
#[error("XZ stream error")]
|
||||
XzStream(#[from] xz2::stream::Error),
|
||||
#[error("I/O error")]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
@@ -102,25 +110,29 @@ pub enum CompressedFormat {
|
||||
None,
|
||||
Gzip,
|
||||
Lz4Legacy,
|
||||
Xz,
|
||||
}
|
||||
|
||||
pub enum CompressedReader<R: Read> {
|
||||
None(R),
|
||||
Gzip(GzDecoder<R>),
|
||||
Lz4(FrameDecoder<R>),
|
||||
Xz(XzDecoder<R>),
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> CompressedReader<R> {
|
||||
pub fn new(mut reader: R, raw_if_unknown: bool) -> Result<Self> {
|
||||
let mut magic = [0u8; 4];
|
||||
let mut magic = [0u8; 6];
|
||||
reader.read_exact(&mut magic)?;
|
||||
|
||||
reader.rewind()?;
|
||||
|
||||
if &magic[0..2] == GZIP_MAGIC {
|
||||
Ok(Self::Gzip(GzDecoder::new(reader)))
|
||||
} else if &magic == LZ4_LEGACY_MAGIC {
|
||||
} else if &magic[0..4] == LZ4_LEGACY_MAGIC {
|
||||
Ok(Self::Lz4(FrameDecoder::new(reader)))
|
||||
} else if &magic == XZ_MAGIC {
|
||||
Ok(Self::Xz(XzDecoder::new(reader)))
|
||||
} else if raw_if_unknown {
|
||||
Ok(Self::None(reader))
|
||||
} else {
|
||||
@@ -133,6 +145,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +154,7 @@ impl<R: Read + Seek> CompressedReader<R> {
|
||||
Self::None(r) => r,
|
||||
Self::Gzip(r) => r.into_inner(),
|
||||
Self::Lz4(r) => r.into_inner(),
|
||||
Self::Xz(r) => r.into_inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,6 +165,7 @@ impl<R: Read> Read for CompressedReader<R> {
|
||||
Self::None(r) => r.read(buf),
|
||||
Self::Gzip(r) => r.read(buf),
|
||||
Self::Lz4(r) => r.read(buf),
|
||||
Self::Xz(r) => r.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,6 +174,7 @@ pub enum CompressedWriter<W: Write> {
|
||||
None(W),
|
||||
Gzip(GzEncoder<W>),
|
||||
Lz4Legacy(Lz4LegacyEncoder<W>),
|
||||
Xz(XzEncoder<W>),
|
||||
}
|
||||
|
||||
impl<W: Write> CompressedWriter<W> {
|
||||
@@ -169,6 +185,11 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Ok(Self::Gzip(GzEncoder::new(writer, Compression::default())))
|
||||
}
|
||||
CompressedFormat::Lz4Legacy => Ok(Self::Lz4Legacy(Lz4LegacyEncoder::new(writer)?)),
|
||||
CompressedFormat::Xz => {
|
||||
// Some kernels are compiled without support for the default CRC64.
|
||||
let stream = Stream::new_easy_encoder(6, Check::Crc32)?;
|
||||
Ok(Self::Xz(XzEncoder::new_stream(writer, stream)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +198,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(_) => CompressedFormat::None,
|
||||
Self::Gzip(_) => CompressedFormat::Gzip,
|
||||
Self::Lz4Legacy(_) => CompressedFormat::Lz4Legacy,
|
||||
Self::Xz(_) => CompressedFormat::Xz,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +207,7 @@ impl<W: Write> CompressedWriter<W> {
|
||||
Self::None(w) => Ok(w),
|
||||
Self::Gzip(w) => w.finish(),
|
||||
Self::Lz4Legacy(w) => w.finish(),
|
||||
Self::Xz(w) => w.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,6 +218,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.write(buf),
|
||||
Self::Gzip(w) => w.write(buf),
|
||||
Self::Lz4Legacy(w) => w.write(buf),
|
||||
Self::Xz(w) => w.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +227,7 @@ impl<W: Write> Write for CompressedWriter<W> {
|
||||
Self::None(w) => w.flush(),
|
||||
Self::Gzip(w) => w.flush(),
|
||||
Self::Lz4Legacy(w) => w.flush(),
|
||||
Self::Xz(w) => w.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,30 @@ vulnerability = "deny"
|
||||
unmaintained = "deny"
|
||||
yanked = "deny"
|
||||
notice = "deny"
|
||||
ignore = [
|
||||
# https://rustsec.org/advisories/RUSTSEC-2023-0071
|
||||
#
|
||||
# This is a side-channel vulnerability where secrets can be leaked to an
|
||||
# attacker that is able to measure the timing of a large number of RSA
|
||||
# operations. As of 2023-12-03, there is no released version of the rsa
|
||||
# crate that contains a fix.
|
||||
#
|
||||
# For avbroot specifically, this vulnerability is not too critical for a
|
||||
# couple reasons:
|
||||
#
|
||||
# 1. avbroot performs RSA signing only at the end of lengthy processes
|
||||
# that involve a lot of disk I/O. It's very expensive to run avbroot
|
||||
# the millions of times needed to capture a sufficient amount of timing
|
||||
# data.
|
||||
# 2. During a single run of avbroot, it will only perform RSA signing a
|
||||
# handful of times. To get sufficient measurements, the attacker would
|
||||
# need to rerun avbroot. If they are able to rerun avbroot, then they
|
||||
# are also able to just read and steal the private key directly.
|
||||
#
|
||||
# avbroot has no network capabilities, so this is not inherently remotely
|
||||
# exploitable.
|
||||
"RUSTSEC-2023-0071",
|
||||
]
|
||||
|
||||
[licenses]
|
||||
include-dev = true
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ fn download_range(
|
||||
.read_timeout(TIMEOUT)
|
||||
.header(
|
||||
"Range",
|
||||
&format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
format!("bytes={}-{}", initial_range.start, initial_range.end - 1),
|
||||
)
|
||||
.send()
|
||||
.and_then(|r| r.error_for_status())
|
||||
|
||||
+1
-1
@@ -117,7 +117,7 @@ fn strip_image(
|
||||
.context("Failed to load OTA payload header")?;
|
||||
|
||||
let required_images =
|
||||
avbroot::cli::ota::get_required_images(&header.manifest, "@gki_ramdisk", true)?
|
||||
avbroot::cli::ota::get_required_images(&header.manifest, Some("@gki_ramdisk"), None)?
|
||||
.into_values()
|
||||
.collect::<HashSet<_>>();
|
||||
let mut data_holes = vec![];
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/libs/
|
||||
/obj/
|
||||
@@ -0,0 +1,7 @@
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := init
|
||||
LOCAL_SRC_FILES := init.c
|
||||
LOCAL_LDFLAGS := -static
|
||||
include $(BUILD_EXECUTABLE)
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2023 Andrew Gunnerson
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mount.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/sendfile.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef DEBUG_PREFIX
|
||||
#define DEBUG_PREFIX ""
|
||||
#endif
|
||||
|
||||
#define AVBROOT_DIR DEBUG_PREFIX "/avbroot"
|
||||
#define DEV_DIR DEBUG_PREFIX "/dev"
|
||||
#define SAFE_DIR DEBUG_PREFIX "/acct"
|
||||
#define STAGE1_DIR DEBUG_PREFIX "/first_stage_ramdisk"
|
||||
|
||||
#define DEV_KMSG DEV_DIR "/kmsg"
|
||||
#define DEV_NULL DEV_DIR "/null"
|
||||
|
||||
#define INIT DEBUG_PREFIX "/init"
|
||||
#define INIT_ORIG AVBROOT_DIR "/init.orig"
|
||||
|
||||
#define OTACERTS DEBUG_PREFIX "/system/etc/security/otacerts.zip"
|
||||
#define OTACERTS_AVBROOT AVBROOT_DIR "/otacerts.zip"
|
||||
#define OTACERTS_TMPFS SAFE_DIR "/otacerts.zip"
|
||||
|
||||
#define LOG(level, fmt, ...) \
|
||||
fprintf(stderr, "<%d>[%d] " fmt, level, getpid(), ##__VA_ARGS__)
|
||||
#define LOGE(...) LOG(3, __VA_ARGS__)
|
||||
#define LOGI(...) LOG(6, __VA_ARGS__)
|
||||
|
||||
// Best effort attempt to output to the kernel log.
|
||||
static void prepare_output()
|
||||
{
|
||||
mknod(DEV_NULL, S_IFCHR | 0666, makedev(1, 3));
|
||||
mknod(DEV_KMSG, S_IFCHR | 0600, makedev(1, 11));
|
||||
|
||||
int fd = open(DEV_NULL, O_RDWR);
|
||||
if (fd >= 0) {
|
||||
dup2(fd, STDIN_FILENO);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
fd = open(DEV_KMSG, O_WRONLY);
|
||||
if (fd >= 0) {
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
dup2(fd, STDERR_FILENO);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
unlink(DEV_NULL);
|
||||
unlink(DEV_KMSG);
|
||||
|
||||
setlinebuf(stdout);
|
||||
}
|
||||
|
||||
static void auto_close(int *fd)
|
||||
{
|
||||
if (*fd >= 0) {
|
||||
int saved_errno = errno;
|
||||
close(*fd);
|
||||
errno = saved_errno;
|
||||
}
|
||||
}
|
||||
|
||||
static int copy_file(const char *source, const char *target)
|
||||
{
|
||||
__attribute__((cleanup(auto_close))) int fd_source =
|
||||
open(source, O_RDONLY | O_CLOEXEC);
|
||||
if (fd_source < 0) {
|
||||
LOGE("%s: Failed to open file: %s\n", source, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct stat sb;
|
||||
if (fstat(fd_source, &sb) < 0) {
|
||||
LOGE("%s: Failed to stat file: %s\n", source, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
__attribute__((cleanup(auto_close))) int fd_target =
|
||||
open(target, O_CREAT | O_TRUNC | O_WRONLY | O_CLOEXEC,
|
||||
sb.st_mode & ~S_IFMT);
|
||||
if (fd_target < 0) {
|
||||
LOGE("%s: Failed to open file: %s\n", target, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint64_t remain = sb.st_size;
|
||||
|
||||
while (remain > 0) {
|
||||
size_t to_copy = remain > 0x7ffff000 ? 0x7ffff000 : remain;
|
||||
|
||||
ssize_t n = sendfile(fd_target, fd_source, NULL, to_copy);
|
||||
if (n < 0) {
|
||||
LOGE("%s -> %s: Failed to copy data: %s\n",
|
||||
source, target, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
remain -= n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Mount a tmpfs at SAFE_DIR and copy the files we need to it. AOSP init will
|
||||
// preserve mount points when switching roots (first /first_stage_ramdisk and
|
||||
// then the system partition), so we'll be able to access the files during the
|
||||
// stage 1 -> stage 2 transition. The safe directory must be a directory that
|
||||
// exists in the system partition and is unused for stage 1 init.
|
||||
static int prepare_safe_dir()
|
||||
{
|
||||
int flags = MS_NOSUID | MS_NODEV | MS_NOEXEC;
|
||||
|
||||
if (mount("avbroot", SAFE_DIR, "tmpfs", flags, "mode=755") < 0) {
|
||||
LOGE("%s: Failed to mount tmpfs: %s\n", SAFE_DIR, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (copy_file(OTACERTS_AVBROOT, OTACERTS_TMPFS) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (mount(NULL, SAFE_DIR, NULL, MS_REMOUNT | MS_RDONLY | flags, NULL) < 0) {
|
||||
LOGE("%s: Failed to remount read-only: %s\n",
|
||||
SAFE_DIR, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Trace the parent process across execve() calls until otacerts.zip exists.
|
||||
// Then, bind mount the replacement and detach. Only the parent (TID 1) needs to
|
||||
// be traced. All other threads and child processes are irrelevant since we only
|
||||
// care about the transition point between stage 1 and stage 2 init.
|
||||
static int trace_parent()
|
||||
{
|
||||
bool first_group_stop = true;
|
||||
|
||||
pid_t parent_pid = getppid();
|
||||
LOGI("Tracing parent PID: %d\n", parent_pid);
|
||||
|
||||
long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEEXEC;
|
||||
|
||||
if (ptrace(PTRACE_SEIZE, parent_pid, NULL, options) < 0) {
|
||||
LOGE("Failed to trace process: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
int status;
|
||||
if (waitpid(parent_pid, &status, __WALL | __WNOTHREAD) == -1) {
|
||||
if (errno == EINTR) {
|
||||
continue;
|
||||
} else {
|
||||
LOGE("waitpid failed: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef __GLIBC__
|
||||
enum __ptrace_request
|
||||
#else
|
||||
int
|
||||
#endif
|
||||
action = PTRACE_CONT;
|
||||
int forward_signal = 0;
|
||||
|
||||
if (WIFEXITED(status)) {
|
||||
LOGE("%d exited with status %d\n", parent_pid, WEXITSTATUS(status));
|
||||
break;
|
||||
} else if (WIFSIGNALED(status)) {
|
||||
LOGE("%d killed by signal %d\n", parent_pid, WTERMSIG(status));
|
||||
break;
|
||||
} else if (WIFSTOPPED(status)) {
|
||||
int ptrace_event = status >> 16;
|
||||
int signal = WSTOPSIG(status);
|
||||
|
||||
switch (ptrace_event) {
|
||||
case PTRACE_EVENT_EXEC: {
|
||||
LOGI("Tracee is about to exec\n");
|
||||
|
||||
// If /first_stage_ramdisk exists, then init hasn't switched
|
||||
// roots yet. otacerts.zip may still exist on devices that
|
||||
// use shared ramdisks for normal and recovery boot.
|
||||
if ((access(STAGE1_DIR, F_OK) != 0 && errno == ENOENT)
|
||||
&& access(OTACERTS, F_OK) == 0) {
|
||||
LOGI("Conditions satisfied; applying override\n");
|
||||
|
||||
action = PTRACE_DETACH;
|
||||
|
||||
if (mount(OTACERTS_TMPFS, OTACERTS, NULL,
|
||||
MS_BIND | MS_RDONLY, "") < 0) {
|
||||
LOGE("Failed to bind mount %s -> %s: %s\n",
|
||||
OTACERTS_TMPFS, OTACERTS, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (umount2(SAFE_DIR, MNT_DETACH) < 0) {
|
||||
LOGE("Failed to detach mount %s: %s\n",
|
||||
SAFE_DIR, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
LOGE("Conditions not yet satisfied\n");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PTRACE_EVENT_STOP: {
|
||||
if (signal == SIGSTOP || signal == SIGTSTP
|
||||
|| signal == SIGTTIN || signal == SIGTTOU) {
|
||||
if (first_group_stop) {
|
||||
LOGI("Resuming tracee\n");
|
||||
kill(parent_pid, SIGCONT);
|
||||
first_group_stop = false;
|
||||
} else {
|
||||
action = PTRACE_LISTEN;
|
||||
}
|
||||
} else {
|
||||
// We get spurious SIGTRAP signals when SIGCONT'ing a
|
||||
// process. rr seem to be running into this as well:
|
||||
// https://github.com/mozilla/rr/issues/2095
|
||||
// strace handles PTRACE_EVENT_STOP + non-group-stop signal
|
||||
// by restarting the process with PTRACE_SYSCALL:
|
||||
// https://github.com/strace/strace/blob/b1e1eb7731e50900bb4591a3a71b96ab37e106a8/strace.c#L2360
|
||||
// https://github.com/strace/strace/blob/b1e1eb7731e50900bb4591a3a71b96ab37e106a8/strace.c#L2408-L2409
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
if (signal == (SIGTRAP | 0x80)) {
|
||||
// Syscall enter/exit stop
|
||||
} else {
|
||||
// Signal delivery stop
|
||||
forward_signal = signal;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ptrace(action, parent_pid, NULL, forward_signal) < 0) {
|
||||
LOGE("Failed to perform action %d (signal %d): %s\n",
|
||||
action, forward_signal, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (action == PTRACE_DETACH) {
|
||||
LOGI("Detaching tracee\n");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LOGE("Invalid waitpid status: 0x%x\n", status);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int prepare_tracing()
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
LOGE("Failed to fork: %s\n", strerror(errno));
|
||||
return -1;
|
||||
} else if (pid == 0) {
|
||||
int ret = trace_parent();
|
||||
if (ret < 0) {
|
||||
// Make sure parent doesn't hang forever.
|
||||
kill(getppid(), SIGCONT);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
_exit(EXIT_SUCCESS);
|
||||
} else {
|
||||
LOGI("Waiting for tracer to be ready\n");
|
||||
kill(getpid(), SIGSTOP);
|
||||
LOGI("Tracer is ready\n");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[], char *envp[])
|
||||
{
|
||||
(void) argc;
|
||||
|
||||
prepare_output();
|
||||
|
||||
if (rename(INIT_ORIG, INIT) < 0) {
|
||||
LOGE("Failed to restore original init: %s\n", strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
if (prepare_safe_dir() < 0) {
|
||||
LOGE("Failed to set up safe directory %s\n", SAFE_DIR);
|
||||
} else if (prepare_tracing() < 0) {
|
||||
LOGE("Failed to set up tracer child process\n");
|
||||
} else {
|
||||
LOGI("Exec hook is ready\n");
|
||||
}
|
||||
|
||||
LOGI("Executing %s\n", INIT);
|
||||
|
||||
execve(INIT, argv, envp);
|
||||
LOGE("Failed to exec %s: %s\n", INIT, strerror(errno));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.clearotacerts
|
||||
name=clearotacerts
|
||||
version=v2.3.1
|
||||
versionCode=131841
|
||||
version=v2.3.3
|
||||
versionCode=131843
|
||||
author=chenxiaolong
|
||||
description=Block A/B OTAs by clearing verification certificates
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
id=com.chiller3.avbroot.oemunlockonboot
|
||||
name=oemunlockonboot
|
||||
version=v2.3.1
|
||||
versionCode=131841
|
||||
version=v2.3.3
|
||||
versionCode=131843
|
||||
author=chenxiaolong
|
||||
description=Enable OEM unlocking on every boot
|
||||
|
||||
Reference in New Issue
Block a user