Skip to content

Commit

Permalink
ota: Add support for legacy OTA metadata
Browse files Browse the repository at this point in the history
Android 11 OTAs use the same `payload.bin` format, but lack the
`metadata.pb` protobuf representation of the OTA metadata. This commit
adds support for parsing the legacy plain-text `metadata` format. Like
before, the output files will still contain both the legacy and
protobuf representations.

Note that the legacy format allowed OEMs to specify arbitrary key/value
pairs. These will be discarded during patching because they cannot be
represented in the protobuf format, which is used in avbroot's internal
representation.

Issue: #195

Signed-off-by: Andrew Gunnerson <[email protected]>
  • Loading branch information
chenxiaolong committed Oct 27, 2023
1 parent f96a288 commit 409867a
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 36 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ avbroot applies two patches to the boot images:

## Warnings and Caveats

* The device must use (non-legacy-SAR) A/B partitioning. This is the case on newer Pixel and OnePlus devices. To check if a device uses this partitioning sceme, open the OTA zip file and check that:
* 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:

* `payload.bin` exists
* `META-INF/com/android/metadata.pb` exists
* `META-INF/com/android/metadata` contains the line: `ota-type=AB`
* `META-INF/com/android/metadata` (Android 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.

Expand Down
39 changes: 27 additions & 12 deletions avbroot/src/cli/ota.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,12 +734,7 @@ fn patch_ota_zip(
cert_ota: &Certificate,
cancel_signal: &AtomicBool,
) -> Result<(OtaMetadata, u64)> {
let mut missing = BTreeSet::from([
ota::PATH_METADATA_PB,
ota::PATH_OTACERT,
ota::PATH_PAYLOAD,
ota::PATH_PROPERTIES,
]);
let mut missing = BTreeSet::from([ota::PATH_OTACERT, ota::PATH_PAYLOAD, ota::PATH_PROPERTIES]);

// Keep in sorted order for reproducibility and to guarantee that the
// payload is processed before its properties file.
Expand All @@ -754,9 +749,15 @@ fn patch_ota_zip(

if !missing.is_empty() {
bail!("Missing entries in OTA zip: {:?}", joined(missing));
} else if !paths.contains(ota::PATH_METADATA) && !paths.contains(ota::PATH_METADATA_PB) {
bail!(
"Neither legacy nor protobuf OTA metadata files exist: {:?}, {:?}",
ota::PATH_METADATA,
ota::PATH_METADATA_PB,
)
}

let mut metadata_pb_raw = None;
let mut metadata = None;
let mut properties = None;
let mut payload_metadata_size = None;
let mut entries = vec![];
Expand All @@ -777,19 +778,33 @@ fn patch_ota_zip(
.compression_method(CompressionMethod::Stored)
.large_file(use_zip64);

// Processed at the end after all other entries are written.
match path.as_str() {
// Convert legacy metadata from Android 11 to the modern protobuf
// structure. Note that although we can read legacy-only OTAs, we
// always produce both the legacy and protobuf representations in
// the output.
ota::PATH_METADATA => {
// Ignore because the plain-text legacy metadata file is
// regenerated from the new protobuf metadata.
let mut buf = String::new();
reader
.read_to_string(&mut buf)
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
metadata = Some(
ota::parse_legacy_metadata(&buf)
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
);
continue;
}
// This takes precedence due to sorted iteration order.
ota::PATH_METADATA_PB => {
// Processed at the end after all other entries are written.
let mut buf = vec![];
reader
.read_to_end(&mut buf)
.with_context(|| format!("Failed to read OTA metadata: {path}"))?;
metadata_pb_raw = Some(buf);
metadata = Some(
ota::parse_protobuf_metadata(&buf)
.with_context(|| format!("Failed to parse OTA metadata: {path}"))?,
);
continue;
}
_ => {}
Expand Down Expand Up @@ -881,7 +896,7 @@ fn patch_ota_zip(
zip_writer,
// Offset where next entry would begin.
entries.last().map(|e| e.offset + e.size).unwrap() + data_descriptor_size,
&metadata_pb_raw.unwrap(),
&metadata.unwrap(),
payload_metadata_size.unwrap(),
)
.context("Failed to write new OTA metadata")?;
Expand Down
138 changes: 117 additions & 21 deletions avbroot/src/format/ota.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const ZIP_EOCD_MAGIC: &[u8; 4] = b"PK\x05\x06";

const COMMENT_MESSAGE: &[u8] = b"signed by avbroot\0";

const LEGACY_SEP: &str = "|";

#[derive(Debug, Error)]
pub enum Error {
#[error("Cannot find OTA signature footer magic")]
Expand All @@ -64,6 +66,10 @@ pub enum Error {
UnsupportedDigestAlgorithm(ObjectIdentifier),
#[error("Unsupported signature algorithm: {0}")]
UnsupportedSignatureAlgorithm(ObjectIdentifier),
#[error("Invalid legacy metadata line: {0:?}")]
InvalidLegacyMetadataLine(String),
#[error("Unsupported legacy metadata field: {key:?} = {value:?}")]
UnsupportedLegacyMetadataField { key: String, value: String },
#[error("Expected entry offsets {expected:?}, but have {actual:?}")]
MismatchedPropertyFiles { expected: String, actual: String },
#[error("Property files {0:?} exceed {1} byte reserved space")]
Expand Down Expand Up @@ -92,21 +98,111 @@ pub enum Error {

type Result<T> = std::result::Result<T, Error>;

pub fn parse_protobuf_metadata(data: &[u8]) -> Result<OtaMetadata> {
Ok(OtaMetadata::decode(data)?)
}

/// Synthesize protobuf structure from legacy plain-text metadata.
pub fn parse_legacy_metadata(data: &str) -> Result<OtaMetadata> {
let mut metadata = OtaMetadata::default();

for line in data.split('\n') {
if line.is_empty() {
continue;
}

let (key, value) = line
.split_once('=')
.ok_or_else(|| Error::InvalidLegacyMetadataLine(line.to_owned()))?;
let unsupported = || Error::UnsupportedLegacyMetadataField {
key: key.to_owned(),
value: value.to_owned(),
};
// Booleans are represented by the presence or absence of `<key>=yes`.
let parse_yes = || match value {
"yes" => Ok(true),
_ => Err(unsupported()),
};
let parse_list = || {
value
.split(LEGACY_SEP)
.map(|s| s.to_owned())
.collect::<Vec<_>>()
};

match key {
"ota-type" => {
match OtaType::from_str_name(value).ok_or_else(unsupported)? {
t @ (OtaType::Ab | OtaType::Block) => metadata.set_type(t),
// Not allowed by AOSP in the legacy format.
_ => return Err(unsupported()),
}
}
"ota-wipe" => metadata.wipe = parse_yes()?,
"ota-retrofit-dynamic-partitions" => {
metadata.retrofit_dynamic_partitions = parse_yes()?
}
"ota-downgrade" => metadata.downgrade = parse_yes()?,
"ota-required-cache" => {
metadata.required_cache = value.parse().map_err(|_| unsupported())?;
}
"post-build" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build = parse_list();
}
"post-build-incremental" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
}
"post-sdk-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.sdk_level = value.to_owned();
}
"post-security-patch-level" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.security_patch_level = value.to_owned();
}
"post-timestamp" => {
let p = metadata.postcondition.get_or_insert_with(Default::default);
p.timestamp = value.parse().map_err(|_| unsupported())?;
}
"pre-device" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.device = parse_list();
}
"pre-build" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build = parse_list();
}
"pre-build-incremental" => {
let p = metadata.precondition.get_or_insert_with(Default::default);
p.build_incremental = value.to_owned();
}
"spl-downgrade" => metadata.spl_downgrade = parse_yes()?,
k if k.ends_with("-property-files") => {
metadata
.property_files
.insert(key.to_owned(), value.to_owned());
}
_ => {
// Ignore. Some OEMs insert values that aren't defined in AOSP.
}
}
}

Ok(metadata)
}

/// Generate the legacy plain-text and modern protobuf serializations of the
/// given metadata instance.
fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
const SEP: &str = "|";
use std::fmt::Write;

let mut pairs = BTreeMap::<String, String>::new();

match metadata.r#type() {
OtaType::Ab => {
pairs.insert("ota-type".to_owned(), "AB".to_owned());
}
OtaType::Block => {
pairs.insert("ota-type".to_owned(), "BLOCK".to_owned());
}
_ => {}
// Other types are not allowed by AOSP in the legacy format.
if let t @ (OtaType::Ab | OtaType::Block) = metadata.r#type() {
pairs.insert("ota-type".to_owned(), t.as_str_name().to_owned());
}
if metadata.wipe {
pairs.insert("ota-wipe".to_owned(), "yes".to_owned());
Expand All @@ -127,7 +223,7 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
);

if let Some(p) = &metadata.postcondition {
pairs.insert("post-build".to_owned(), p.build.join(SEP));
pairs.insert("post-build".to_owned(), p.build.join(LEGACY_SEP));
pairs.insert(
"post-build-incremental".to_owned(),
p.build_incremental.clone(),
Expand All @@ -141,9 +237,9 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {
}

if let Some(p) = &metadata.precondition {
pairs.insert("pre-device".to_owned(), p.device.join(SEP));
pairs.insert("pre-device".to_owned(), p.device.join(LEGACY_SEP));
if !p.build.is_empty() {
pairs.insert("pre-build".to_owned(), p.build.join(SEP));
pairs.insert("pre-build".to_owned(), p.build.join(LEGACY_SEP));
pairs.insert(
"pre-build-incremental".to_owned(),
p.build_incremental.clone(),
Expand All @@ -157,10 +253,10 @@ fn serialize_metadata(metadata: &OtaMetadata) -> Result<(String, Vec<u8>)> {

pairs.extend(metadata.property_files.clone());

let legacy_metadata = pairs
.into_iter()
.map(|(k, v)| format!("{k}={v}\n"))
.collect::<String>();
let legacy_metadata = pairs.into_iter().fold(String::new(), |mut output, (k, v)| {
let _ = writeln!(output, "{k}={v}");
output
});
let modern_metadata = metadata.encode_to_vec();

Ok((legacy_metadata, modern_metadata))
Expand Down Expand Up @@ -286,20 +382,20 @@ fn add_payload_metadata_entry(
/// Add metadata files to the output OTA zip. `zip_entries` is the list of
/// [`ZipEntry`] already written to `zip_writer`. `next_offset` is the current
/// file offset (where the next zip entry's local header begins).
/// `metadata_pb_raw` is the serialized OTA metadata protobuf message from the
/// original OTA. `payload_metadata_size` is the size of the new payload's
/// metadata and metadata signature regions.
/// `metadata` is the OTA metadata protobuf message from the original OTA.
/// `payload_metadata_size` is the size of the new payload's metadata and
/// metadata signature regions.
///
/// The zip file's backing file position MUST BE set to where the central
/// directory would start.
pub fn add_metadata(
zip_entries: &[ZipEntry],
zip_writer: &mut ZipWriter<impl Write>,
next_offset: u64,
metadata_pb_raw: &[u8],
metadata: &OtaMetadata,
payload_metadata_size: u64,
) -> Result<OtaMetadata> {
let mut metadata = OtaMetadata::decode(metadata_pb_raw)?;
let mut metadata = metadata.clone();
let options = FileOptions::default().compression_method(CompressionMethod::Stored);

let mut zip_entries = zip_entries.to_owned();
Expand Down

0 comments on commit 409867a

Please sign in to comment.