patch/boot: Add support for DSU on dedicated recovery devices

On devices with dedicated recovery partitions, first_stage_ramdisk
exists in the ramdisks, but is unused. The first stage switch root
operation is just skipped. To properly add a public key for DSU on these
devices, the top level /avb directory needs to be used.

Fixes: #536

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
This commit is contained in:
Andrew Gunnerson
2026-01-08 19:21:55 -05:00
parent dc9244f014
commit 9214a832cd
2 changed files with 97 additions and 31 deletions
+2 -2
View File
@@ -2066,10 +2066,10 @@ pub fn verify_subcommand(cli: &VerifyCli, cancel_signal: &AtomicBool) -> Result<
}
}
let boot_images = boot::load_boot_images(&boot_image_names, &Opener(temp_dir.path()))
let (boot_images, layout) = boot::load_boot_images(&boot_image_names, &Opener(temp_dir.path()))
.context("Failed to load all boot images")?;
let targets = OtaCertPatcher::new(ota_cert.clone())
.find_targets(&boot_images, cancel_signal)
.find_targets(layout, &boot_images, cancel_signal)
.context("Failed to find boot image containing otacerts.zip")?;
if targets.is_empty() {
+95 -29
View File
@@ -159,6 +159,12 @@ fn save_ramdisk(
Ok(raw_writer.into_inner())
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct BootLayoutInfo {
pub has_recovery: bool,
}
#[derive(Clone, Eq, PartialEq)]
pub struct BootImageInfo {
pub header: Header,
pub footer: Footer,
@@ -174,11 +180,17 @@ pub trait BootImagePatch {
/// can be inspected, but during patching, only the boot image is available.
fn find_targets<'a>(
&self,
layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>>;
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()>;
fn patch(
&self,
layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()>;
}
/// Root a boot image with Magisk.
@@ -440,6 +452,7 @@ impl BootImagePatch for MagiskRootPatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -454,7 +467,12 @@ impl BootImagePatch for MagiskRootPatcher {
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
// Load the first ramdisk. If it doesn't exist, we have to generate one
// from scratch.
let ramdisk = match boot_image {
@@ -746,6 +764,7 @@ impl BootImagePatch for OtaCertPatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -778,7 +797,12 @@ impl BootImagePatch for OtaCertPatcher {
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
let ramdisks = match boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
@@ -817,31 +841,46 @@ impl DsuPubKeyPatcher {
const FIRST_STAGE_PATH: &'static [u8] = b"first_stage_ramdisk";
const DSU_KEYS_PATH: &'static [u8] = b"first_stage_ramdisk/avb";
const AVBROOT_KEY_PATH: &'static [u8] = b"first_stage_ramdisk/avb/avbroot.avbpubkey";
const RECOVERY_DSU_KEYS_PATH: &'static [u8] = b"avb";
const RECOVERY_AVBROOT_KEY_PATH: &'static [u8] = b"avb/avbroot.avbpubkey";
pub fn new(key: RsaPublicKey) -> Self {
Self { key }
}
fn patch_ramdisk(&self, ramdisk: &mut Vec<u8>, cancel_signal: &AtomicBool) -> Result<bool> {
fn patch_ramdisk(
&self,
layout: BootLayoutInfo,
ramdisk: &mut Vec<u8>,
cancel_signal: &AtomicBool,
) -> Result<bool> {
let (mut entries, ramdisk_format) = load_ramdisk(ramdisk, cancel_signal)?;
if !entries.iter_mut().any(|e| e.path == Self::FIRST_STAGE_PATH) {
return Ok(false);
}
if !entries.iter().any(|e| e.path == Self::DSU_KEYS_PATH) {
entries.push(CpioEntry::new_directory(Self::DSU_KEYS_PATH, 0o755));
let (dsu_keys_path, avbroot_key_path) = if layout.has_recovery {
(
Self::RECOVERY_DSU_KEYS_PATH,
Self::RECOVERY_AVBROOT_KEY_PATH,
)
} else {
(Self::DSU_KEYS_PATH, Self::AVBROOT_KEY_PATH)
};
debug!("DSU key path: {:?}", avbroot_key_path.as_bstr());
if !entries.iter().any(|e| e.path == dsu_keys_path) {
entries.push(CpioEntry::new_directory(dsu_keys_path, 0o755));
}
let binary_key = avb::encode_public_key(&self.key).map_err(Error::AvbEncodeKey)?;
let data = CpioEntryData::Data(binary_key);
if let Some(e) = entries
.iter_mut()
.find(|e| e.path == Self::AVBROOT_KEY_PATH)
{
if let Some(e) = entries.iter_mut().find(|e| e.path == avbroot_key_path) {
e.data = data;
} else {
entries.push(CpioEntry::new_file(Self::AVBROOT_KEY_PATH, 0o644, data));
entries.push(CpioEntry::new_file(avbroot_key_path, 0o644, data));
}
*ramdisk = save_ramdisk(&mut entries, ramdisk_format, cancel_signal)?;
@@ -857,6 +896,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
fn find_targets<'a>(
&self,
layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -870,6 +910,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
BootImage::VendorV3Through4(b) => &b.ramdisks,
};
let dsu_keys_path = if layout.has_recovery {
Self::RECOVERY_DSU_KEYS_PATH
} else {
Self::DSU_KEYS_PATH
};
for ramdisk in ramdisks {
if ramdisk.is_empty() {
continue;
@@ -880,10 +926,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
let mut found = false;
for entry in entries {
if entry.path == Self::DSU_KEYS_PATH {
if entry.path == dsu_keys_path {
dsu_keys_targets.push(name);
found = true;
} else if entry.path == Self::FIRST_STAGE_PATH {
// This exists even on devices with recovery partitions.
// init just doesn't switch root to it.
first_stage_targets.push(name);
found = true;
}
@@ -917,7 +965,12 @@ impl BootImagePatch for DsuPubKeyPatcher {
}
}
fn patch(&self, boot_image: &mut BootImage, cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
layout: BootLayoutInfo,
boot_image: &mut BootImage,
cancel_signal: &AtomicBool,
) -> Result<()> {
let ramdisks = match boot_image {
BootImage::V0Through2(b) => slice::from_mut(&mut b.ramdisk),
BootImage::V3Through4(b) => slice::from_mut(&mut b.ramdisk),
@@ -929,7 +982,7 @@ impl BootImagePatch for DsuPubKeyPatcher {
continue;
}
if self.patch_ramdisk(ramdisk, cancel_signal)? {
if self.patch_ramdisk(layout, ramdisk, cancel_signal)? {
return Ok(());
}
}
@@ -1014,6 +1067,7 @@ impl BootImagePatch for PrepatchedImagePatcher {
fn find_targets<'a>(
&self,
_layout: BootLayoutInfo,
boot_images: &HashMap<&'a str, BootImageInfo>,
_cancel_signal: &AtomicBool,
) -> TargetsResult<Vec<&'a str>> {
@@ -1038,7 +1092,12 @@ impl BootImagePatch for PrepatchedImagePatcher {
Ok(targets)
}
fn patch(&self, boot_image: &mut BootImage, _cancel_signal: &AtomicBool) -> Result<()> {
fn patch(
&self,
_layout: BootLayoutInfo,
boot_image: &mut BootImage,
_cancel_signal: &AtomicBool,
) -> Result<()> {
let prepatched_image = self.load_prepatched_image()?;
// Level 0: Warnings that don't affect booting
@@ -1287,10 +1346,10 @@ pub trait BootImageOpener {
pub fn load_boot_images<'a>(
names: &[&'a str],
opener: &(dyn BootImageOpener + Sync),
) -> TargetsResult<HashMap<&'a str, BootImageInfo>> {
) -> TargetsResult<(HashMap<&'a str, BootImageInfo>, BootLayoutInfo)> {
let parent_span = Span::current();
names
let images = names
.par_iter()
.map(|&name| {
let _span = debug_span!(parent: &parent_span, "image", name).entered();
@@ -1303,7 +1362,13 @@ pub fn load_boot_images<'a>(
Ok((name, info))
})
.collect()
.collect::<TargetsResult<HashMap<_, _>>>()?;
let layout = BootLayoutInfo {
has_recovery: images.contains_key("recovery"),
};
Ok((images, layout))
}
/// Apply applicable patches to the list of specified boot images. For each
@@ -1326,7 +1391,7 @@ pub fn patch_boot_images<'a>(
}
// Preparse all images. Some patchers need to inspect every candidate.
let mut images = load_boot_images(names, opener)?;
let (mut images, layout) = load_boot_images(names, opener)?;
// Find the targets that each patcher wants to patch.
let all_targets = patchers
@@ -1334,14 +1399,15 @@ pub fn patch_boot_images<'a>(
.map(|p| {
let _span =
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
p.find_targets(&images, cancel_signal).and_then(|targets| {
if targets.is_empty() {
Err(TargetsError::NoTargets(p.patcher_name()))
} else {
debug!("Found patcher targets: {targets:?}");
Ok(targets)
}
})
p.find_targets(layout, &images, cancel_signal)
.and_then(|targets| {
if targets.is_empty() {
Err(TargetsError::NoTargets(p.patcher_name()))
} else {
debug!("Found patcher targets: {targets:?}");
Ok(targets)
}
})
})
.collect::<TargetsResult<Vec<_>>>()?;
@@ -1368,8 +1434,8 @@ pub fn patch_boot_images<'a>(
.try_for_each(|(&name, (info, patchers))| -> TargetsResult<()> {
patchers.iter().try_for_each(|p| {
let _span =
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name()).entered();
p.patch(&mut info.boot_image, cancel_signal)
debug_span!(parent: &parent_span, "patcher", name = p.patcher_name(), image = name).entered();
p.patch(layout, &mut info.boot_image, cancel_signal)
.map_err(|e| TargetsError::Patch(name.to_owned(), e))
})
})?;