Compare commits

...

37 Commits

Author SHA1 Message Date
Andrew Gunnerson 461392c226 [BROKEN] Proof of concept implementation of option 5
This adds a wrapper for /init that spawns a child process to ptrace the
parent. When stage 1 execs stage 2, the tracer will bind mount the new
otacerts.zip, detach, and exit.

The mount process works, but stage 2 panics and reboots to the
bootloader for unknown reasons. The conditions that lead to the reboot
don't result in the kernel log being preserved.

Issue: #225

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

Fixes: #223

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

Fixes: #222

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

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

Issue: #218

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

Issue: #218

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

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

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

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

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

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

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

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

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

AOSP changes:

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

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

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

Issue: #203

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

Issue: #203

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
2023-11-15 16:37:00 -05:00
21 changed files with 795 additions and 130 deletions
+3 -3
View File
@@ -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
View File
@@ -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 }}
+33
View File
@@ -7,6 +7,25 @@
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])
@@ -112,6 +131,9 @@ Behind-the-scenes changes:
[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
@@ -159,3 +181,14 @@ Behind-the-scenes changes:
[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
View File
@@ -121,7 +121,7 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "avbroot"
version = "2.3.2"
version = "2.3.3"
dependencies = [
"anyhow",
"assert_matches",
@@ -565,7 +565,7 @@ dependencies = [
[[package]]
name = "e2e"
version = "2.3.2"
version = "2.3.3"
dependencies = [
"anyhow",
"attohttpc",
@@ -679,7 +679,7 @@ dependencies = [
[[package]]
name = "fuzz"
version = "2.3.2"
version = "2.3.3"
dependencies = [
"avbroot",
"honggfuzz",
@@ -2104,7 +2104,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "2.3.2"
version = "2.3.3"
dependencies = [
"anyhow",
"clap",
+1 -1
View File
@@ -4,7 +4,7 @@ members = ["avbroot", "e2e", "fuzz", "xtask"]
resolver = "2"
[workspace.package]
version = "2.3.2"
version = "2.3.3"
license = "GPL-3.0-only"
edition = "2021"
repository = "https://github.com/chenxiaolong/avbroot"
+3 -1
View File
@@ -13,7 +13,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 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.
* 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
+83 -16
View File
@@ -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,
+23
View File
@@ -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 -73
View File
@@ -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,52 +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}"))?;
let pd_type = parent_descriptor.type_name();
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");
};
let d_type = descriptor.type_name();
match (parent_descriptor, descriptor) {
(Descriptor::Hash(pd), Descriptor::Hash(d)) => {
*pd = d.clone();
}
(Descriptor::HashTree(pd), Descriptor::HashTree(d)) => {
*pd = d.clone();
}
_ => {
bail!("{dep} descriptor ({d_type}) does not match entry in {name} ({pd_type})");
}
}
} else {
// vbmeta is signed; Use a chain descriptor.
match parent_descriptor {
Descriptor::ChainPartition(d) => {
d.public_key = header.public_key;
}
_ => {
bail!("{dep} descriptor ({pd_type}) in {name} 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
@@ -551,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,
@@ -587,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()
@@ -728,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,
@@ -848,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,
@@ -1055,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,
@@ -1156,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());
@@ -1258,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)
@@ -1476,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.
@@ -1500,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.
@@ -1524,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)]
+17
View File
@@ -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();
+13 -3
View File
@@ -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;
@@ -1109,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 {
@@ -1119,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()
}
@@ -1142,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.
@@ -1157,6 +1165,7 @@ impl<R: Read> FromReader<R> for ChainPartitionDescriptor {
rollback_index_location,
partition_name,
public_key,
flags,
reserved,
};
@@ -1177,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)?;
+27 -2
View File
@@ -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(),
}
}
}
+24
View File
@@ -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
View File
@@ -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
View File
@@ -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![];
+2
View File
@@ -0,0 +1,2 @@
/libs/
/obj/
+7
View File
@@ -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
View File
@@ -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;
}
+2 -2
View File
@@ -1,6 +1,6 @@
id=com.chiller3.avbroot.clearotacerts
name=clearotacerts
version=v2.3.2
versionCode=131842
version=v2.3.3
versionCode=131843
author=chenxiaolong
description=Block A/B OTAs by clearing verification certificates
+2 -2
View File
@@ -1,6 +1,6 @@
id=com.chiller3.avbroot.oemunlockonboot
name=oemunlockonboot
version=v2.3.2
versionCode=131842
version=v2.3.3
versionCode=131843
author=chenxiaolong
description=Enable OEM unlocking on every boot