diff --git a/Cargo.lock b/Cargo.lock index 108fbd0..3072181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,8 @@ dependencies = [ "tracing", "tracing-subscriber", "x509-cert", + "zerocopy", + "zerocopy-derive", "zip", ] @@ -190,6 +192,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" diff --git a/README.extra.md b/README.extra.md index 9c559ea..2049dc9 100644 --- a/README.extra.md +++ b/README.extra.md @@ -253,6 +253,58 @@ avbroot hash-tree verify -i -H This will check if the input file has any corrupted blocks. Currently, the command cannot report which specific blocks are corrupted, only whether the file is valid. +## `avbroot lp` + +This set of commands is for working with LP (logical partition) images. These are the containers for dynamically-allocated partitions, like `system`. All LP images are supported: + +* Empty images: These are the `super_empty.img` images in the factory images for newer Google Pixel devices. They define the layout of the `super` partition, but don't contain any actual data. They also do not contain a backup copy of the metadata. As an optimization, `fastboot` can fill in the actual data during flashing to avoid needing to reboot to fastbootd mode. +* Normal images backed by a single device: These are standalone `super.img` images and are how logical partitions are physically stored on disk in most newer devices. They contain a backup copy of all metadata as well as actual partition data. +* Normal images backed by multiple devices: These are images split across multiple files/partitions and are used on devices where support for LP was retrofitted. For example, the LP setup on newer Android builds for the Google Pixel 3a XL reuse the legacy `system` and `vendor` partitions because there is no `super` partition. These are similar to the single-file LP setups, except that data can be stored across all of the LP images. However, the metadata is only stored on the first LP image. + +### Unpacking an LP image + +```bash +avbroot lp unpack -i [-i ]... +``` + +This subcommand unpacks the LP metadata to `lp.toml` and the partition images to the `lp_images` directory. + +If there are multiple images, they must be specified in order. If the order is not known, run `avbroot lp info` on each of the images. The one that successfully parses is the first image and the `block_devices` field in the output specifies the full ordering. + +An LP image can have multiple slots. If the LP image originated from a factory image or OTA, all slots are likely identical. If the LP image was dumped from a real device that installed OTA updates in the past, the slots may differ. If the slots are not identical, then the `--slot` option is required to specify which slot to unpack. + +When unpacking an empty image, files are still created in the `lp_images` directory. These files are sparse files that don't contain any actual data, but do have the correct file size. They are necessary when packing a new LP image so that avbroot can determine the size of each partition. + +### Packing an LP image + +```bash +avbroot lp pack -o [-o ]... +``` + +This subcommand packs a new LP image from the `lp.toml` file and `lp_images` directory. Any `.img` files in the `lp_images` directory that don't have a corresponding entry in `lp.toml` are silently ignored. + +All metadata slots in the newly packed LP image will be identical. + +The partition images in `lp_images` are required even when packing an empty image. They can be sparse files that contain no data, but must have the proper size. + +### Repacking an LP image + +```bash +avbroot lp repack [-i ] [-i ]... -o [-o ]... +``` + +This subcommand is logically equivalent to `avbroot lp unpack` followed by `avbroot lp pack`, except more efficient. Instead of unpacking and packing all partition images, the raw data is directly copied from the old LP image to the new LP image. + +When `--slot` is specified, this is useful for discarding unwanted metadata slots and the partition data exclusive to them. + +### Showing LP image metadata + +```bash +avbroot lp info -i +``` + +This subcommand shows the LP image metadata, including all metadata slots. If there are multiple images, only the first one is needed because it is the only one that stores the metadata. + ## `avbroot payload` ### Unpacking a payload binary diff --git a/avbroot/Cargo.toml b/avbroot/Cargo.toml index 93d4423..0300007 100644 --- a/avbroot/Cargo.toml +++ b/avbroot/Cargo.toml @@ -11,7 +11,7 @@ publish = false [dependencies] anyhow = "1.0.75" base64 = "0.22.1" -bitflags = "2.4.1" +bitflags = { version = "2.4.1", features = ["serde"] } bstr = "1.6.2" byteorder = "1.4.3" cap-std = "3.0.0" @@ -53,6 +53,8 @@ topological-sort = "0.2.2" tracing = "0.1.40" tracing-subscriber = "0.3.18" x509-cert = { version = "0.2.4", features = ["builder"] } +zerocopy = "0.7.35" +zerocopy-derive = "0.7.35" # There are multiple upstream bugs that cause infinite loops in the Drop # implementation of write::BzDecoder. Unfortunately, the project is no longer @@ -82,3 +84,6 @@ assert_matches = "1.5.0" [features] static = ["bzip2/static", "liblzma/static"] + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] } diff --git a/avbroot/src/cli/args.rs b/avbroot/src/cli/args.rs index c66f830..d991194 100644 --- a/avbroot/src/cli/args.rs +++ b/avbroot/src/cli/args.rs @@ -15,7 +15,7 @@ use clap::{Parser, Subcommand, ValueEnum}; use tracing::{debug, Level}; use tracing_subscriber::fmt::{format::Writer, time::FormatTime}; -use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, ota, payload}; +use crate::cli::{avb, boot, completion, cpio, fec, hashtree, key, lp, ota, payload}; #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] @@ -27,6 +27,7 @@ pub enum Command { Fec(fec::FecCli), HashTree(hashtree::HashTreeCli), Key(key::KeyCli), + Lp(lp::LpCli), Ota(ota::OtaCli), Payload(payload::PayloadCli), /// (Deprecated: Use `avbroot ota patch` instead.) @@ -130,6 +131,7 @@ pub fn main(logging_initialized: &AtomicBool, cancel_signal: &AtomicBool) -> Res Command::Fec(c) => fec::fec_main(&c, cancel_signal), Command::HashTree(c) => hashtree::hash_tree_main(&c, cancel_signal), Command::Key(c) => key::key_main(&c), + Command::Lp(c) => lp::lp_main(&c, cancel_signal), Command::Ota(c) => ota::ota_main(&c, cancel_signal), Command::Payload(c) => payload::payload_main(&c, cancel_signal), // Deprecated aliases. diff --git a/avbroot/src/cli/lp.rs b/avbroot/src/cli/lp.rs new file mode 100644 index 0000000..0705957 --- /dev/null +++ b/avbroot/src/cli/lp.rs @@ -0,0 +1,684 @@ +/* + * SPDX-FileCopyrightText: 2024 Andrew Gunnerson + * SPDX-License-Identifier: GPL-3.0-only + */ + +use std::{ + ffi::OsStr, + fs::{self, File}, + io::{Seek, SeekFrom}, + path::{Path, PathBuf}, + sync::atomic::AtomicBool, +}; + +use anyhow::{bail, Context, Result}; +use cap_std::{ambient_authority, fs::Dir}; +use clap::{CommandFactory, Parser, Subcommand}; +use rayon::iter::{ + IndexedParallelIterator, IntoParallelIterator, IntoParallelRefIterator, ParallelIterator, +}; + +use crate::{ + format::lp::{Extent, ExtentType, ImageType, Metadata, SECTOR_SIZE}, + stream::{self, FromReader, PSeekFile, Reopen, ToWriter}, +}; + +fn open_lp_inputs(paths: &[impl AsRef]) -> Result<(Vec, Metadata)> { + let mut inputs = paths + .iter() + .map(|p| { + let p = p.as_ref(); + + File::open(p) + .map(PSeekFile::new) + .with_context(|| format!("Failed to open LP image for reading: {p:?}")) + }) + .collect::>>()?; + + let metadata = Metadata::from_reader(&mut inputs[0]) + .with_context(|| format!("Failed to parse LP image metadata: {:?}", paths[0].as_ref()))?; + + Ok((inputs, metadata)) +} + +fn open_lp_outputs(paths: &[impl AsRef]) -> Result> { + paths + .iter() + .map(|p| { + let p = p.as_ref(); + + File::create(p) + .map(PSeekFile::new) + .with_context(|| format!("Failed to open LP image for writing: {p:?}")) + }) + .collect::>>() +} + +fn read_info(path: &Path) -> Result { + let data = fs::read_to_string(path) + .with_context(|| format!("Failed to read metadata info TOML: {path:?}"))?; + let info = toml_edit::de::from_str(&data) + .with_context(|| format!("Failed to parse metadata info TOML: {path:?}"))?; + + Ok(info) +} + +fn write_info(path: &Path, metadata: &Metadata) -> Result<()> { + let data = toml_edit::ser::to_string_pretty(metadata) + .with_context(|| format!("Failed to serialize metadata info TOML: {path:?}"))?; + fs::write(path, data) + .with_context(|| format!("Failed to write metadata info TOML: {path:?}"))?; + + Ok(()) +} + +fn display_metadata(cli: &LpCli, metadata: &Metadata) { + if !cli.quiet { + println!("{metadata:#?}"); + } +} + +struct CopyExtent { + device_index: usize, + lp_offset: u64, + out_offset: u64, + size: u64, +} + +/// Split extents into smaller ones for parallelization. +fn split_extents(extents: &[Extent]) -> Vec { + // 64 MiB is the smallest size we'll parallelize by. + const CHUNK_SIZE: u64 = 64 * 1024 * 1024; + + let mut result = vec![]; + let mut out_offset = 0; + + for extent in extents { + let mut remain = extent.num_sectors * u64::from(SECTOR_SIZE); + + match extent.extent_type { + ExtentType::Linear { + start_sector, + block_device_index, + } => { + let mut lp_offset = start_sector * u64::from(SECTOR_SIZE); + + // 64 MiB is the smallest size we'll parallelize by. + let num_chunks = remain.div_ceil(64 * 1024 * 1024); + + for _ in 0..num_chunks { + let chunk_size = CHUNK_SIZE.min(remain); + + result.push(CopyExtent { + device_index: block_device_index, + out_offset, + lp_offset, + size: chunk_size, + }); + + out_offset += chunk_size; + lp_offset += chunk_size; + remain -= chunk_size; + } + } + ExtentType::Zero => out_offset += remain, + } + } + + result +} + +/// Use the CLI-specified slot or automatically select one if all slots are +/// identical. +fn get_slot_number(metadata: &Metadata, cli_slot: Option) -> Result { + match cli_slot { + Some(n) => { + let n = n as usize; + if n >= metadata.slots.len() { + bail!("Slot out of range: {n}"); + } + + Ok(n) + } + None => { + if metadata.slots.windows(2).any(|w| w[0] != w[1]) { + bail!("A slot must be specified because they are not all identical"); + } + + Ok(0) + } + } +} + +/// Remove all slots aside from the specified one and return the old slot count. +fn retain_slot(metadata: &mut Metadata, slot: usize) -> usize { + let slot_count = metadata.slots.len(); + metadata.slots.swap(0, slot); + metadata.slots.truncate(1); + slot_count +} + +/// Duplicate the first slot until the required number of slots is reached. +fn fill_slots(metadata: &mut Metadata) { + let required = match metadata.image_type { + ImageType::Normal => metadata.metadata_slot_count as usize, + ImageType::Empty => 1, + }; + + for _ in metadata.slots.len()..required { + metadata.slots.extend_from_within(0..=0); + } +} + +fn unpack_subcommand(lp_cli: &LpCli, cli: &UnpackCli, cancel_signal: &AtomicBool) -> Result<()> { + let mut inputs = cli + .input + .iter() + .map(|p| { + File::open(p) + .map(PSeekFile::new) + .with_context(|| format!("Failed to open LP image for reading: {p:?}")) + }) + .collect::>>()?; + + let mut metadata = Metadata::from_reader(&mut inputs[0]) + .with_context(|| format!("Failed to read LP image metadata: {:?}", cli.input[0]))?; + + // Display and write only the selected slot. + let slot_number = get_slot_number(&metadata, cli.slot)?; + retain_slot(&mut metadata, slot_number); + display_metadata(lp_cli, &metadata); + write_info(&cli.output_info, &metadata)?; + + let authority = ambient_authority(); + Dir::create_ambient_dir_all(&cli.output_images, authority) + .with_context(|| format!("Failed to create directory: {:?}", cli.output_images))?; + let directory = Dir::open_ambient_dir(&cli.output_images, authority) + .with_context(|| format!("Failed to open directory: {:?}", cli.output_images))?; + + let slot = &metadata.slots[0]; + + if slot.block_devices.len() != inputs.len() { + bail!( + "Need {} input images, but have {}", + slot.block_devices.len(), + inputs.len(), + ); + } + + // Preopen all image output files. + let mut paths = vec![]; + let mut files = vec![]; + + for group in &slot.groups { + let mut group_paths = vec![]; + let mut group_files = vec![]; + + for partition in &group.partitions { + // A partition name with unsafe characters fails during parsing. + let path = format!("{}.img", partition.name); + + let file = directory + .create(&path) + .map(|f| PSeekFile::new(f.into_std())) + .with_context(|| format!("Failed to open for writing: {path:?}"))?; + + file.set_len(partition.size()?) + .with_context(|| format!("Failed to truncate file: {path:?}"))?; + + group_paths.push(path); + group_files.push(file); + } + + paths.push(group_paths); + files.push(group_files); + } + + // For empty images, the outputs have already been truncated and there's no + // data to unpack. + if metadata.image_type == ImageType::Empty { + return Ok(()); + } + + slot.groups + .par_iter() + .enumerate() + // Flatten grouped partitions. + .flat_map(|(g_index, g)| { + g.partitions + .par_iter() + .enumerate() + .map(move |(p_index, p)| (g_index, p_index, p)) + }) + // Flatten extents in all partitions and split them to smaller chunks + // for better parallelism. + .flat_map(|(g_index, p_index, p)| { + split_extents(&p.extents) + .into_par_iter() + .map(move |e| (g_index, p_index, e)) + }) + .map(|(g_index, p_index, extent)| { + // Never fails for PSeekFiles. + let mut reader = inputs[extent.device_index].reopen()?; + let mut writer = files[g_index][p_index].reopen()?; + + let r_path = &cli.input[extent.device_index]; + let w_path = &paths[g_index][p_index]; + + reader + .seek(SeekFrom::Start(extent.lp_offset)) + .with_context(|| format!("Failed to seek file: {r_path:?}"))?; + writer + .seek(SeekFrom::Start(extent.out_offset)) + .with_context(|| format!("Failed to seek file: {w_path:?}"))?; + + stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal) + .with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?; + + Ok(()) + }) + .collect::>()?; + + Ok(()) +} + +fn pack_subcommand(lp_cli: &LpCli, cli: &PackCli, cancel_signal: &AtomicBool) -> Result<()> { + let mut metadata = read_info(&cli.input_info)?; + + if metadata.slots.len() != 1 { + bail!("There must be exactly one metadata slot"); + } + + let slot = &mut metadata.slots[0]; + + let mut outputs = open_lp_outputs(&cli.output)?; + + if slot.block_devices.len() != outputs.len() { + bail!( + "Need {} output images, but have {}", + slot.block_devices.len(), + outputs.len(), + ); + } + + if metadata.image_type == ImageType::Normal { + for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() { + output + .set_len(block_device.size) + .with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?; + } + } + + let authority = ambient_authority(); + let directory = Dir::open_ambient_dir(&cli.input_images, authority) + .with_context(|| format!("Failed to open directory: {:?}", cli.input_images))?; + + for group in &slot.groups { + for partition in &group.partitions { + let name = &partition.name; + + if Path::new(name).file_name() != Some(OsStr::new(name)) { + bail!("Unsafe partition name: {name}"); + } + } + } + + // Preopen all image input files. + let mut paths = vec![]; + let mut files = vec![]; + + for group in &mut slot.groups { + let mut group_paths = vec![]; + let mut group_files = vec![]; + + for partition in &mut group.partitions { + let path = format!("{}.img", partition.name); + + let mut file = directory + .open(&path) + .map(|f| PSeekFile::new(f.into_std())) + .with_context(|| format!("Failed to open for reading: {path:?}"))?; + + let size = file + .seek(SeekFrom::End(0)) + .with_context(|| format!("Failed to seek file: {path:?}"))?; + + if size % u64::from(SECTOR_SIZE) != 0 { + bail!("File size is not {SECTOR_SIZE}B aligned: {size}: {path:?}"); + } + + // This will be filled out properly later during reallocation. + partition.extents.push(Extent { + num_sectors: size / u64::from(SECTOR_SIZE), + extent_type: ExtentType::Linear { + start_sector: 0, + block_device_index: 0, + }, + }); + + group_paths.push(path); + group_files.push(file); + } + + paths.push(group_paths); + files.push(group_files); + } + + // Now that we have all the partition sizes, actually allocate extents for + // them on the block devices. + slot.reallocate_extents() + .context("Failed to allocate extents")?; + + // Display only the selected slot and make the rest identical. + let _ = slot; + display_metadata(lp_cli, &metadata); + fill_slots(&mut metadata); + let slot = &metadata.slots[0]; + + // Write the new metadata. + metadata + .to_writer(&mut outputs[0]) + .with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?; + + // For empty images, there's no data to pack. The inputs were only used for + // computing the extents. + if metadata.image_type == ImageType::Empty { + return Ok(()); + } + + slot.groups + .par_iter() + .enumerate() + // Flatten grouped partitions. + .flat_map(|(g_index, g)| { + g.partitions + .par_iter() + .enumerate() + .map(move |(p_index, p)| (g_index, p_index, p)) + }) + // Flatten extents in all partitions and split them to smaller chunks + // for better parallelism. + .flat_map(|(g_index, p_index, p)| { + split_extents(&p.extents) + .into_par_iter() + .map(move |e| (g_index, p_index, e)) + }) + .map(|(g_index, p_index, extent)| { + // Never fails for PSeekFiles. + let mut reader = files[g_index][p_index].reopen()?; + let mut writer = outputs[extent.device_index].reopen()?; + + let r_path = &paths[g_index][p_index]; + let w_path = &cli.output[extent.device_index]; + + reader + .seek(SeekFrom::Start(extent.out_offset)) + .with_context(|| format!("Failed to seek file: {r_path:?}"))?; + writer + .seek(SeekFrom::Start(extent.lp_offset)) + .with_context(|| format!("Failed to seek file: {w_path:?}"))?; + + stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal) + .with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?; + + Ok(()) + }) + .collect::>()?; + + Ok(()) +} + +fn repack_subcommand(lp_cli: &LpCli, cli: &RepackCli, cancel_signal: &AtomicBool) -> Result<()> { + // Show a clap-style error if the number of inputs and outputs aren't equal. + if cli.input.len() != cli.output.len() { + let (arg_id, actual_len, expected_len) = if cli.input.len() < cli.output.len() { + ("input", cli.input.len(), cli.output.len()) + } else { + ("output", cli.output.len(), cli.input.len()) + }; + + let mut command = RepackCli::command(); + command.build(); + + let arg = command + .get_arguments() + .find(|a| a.get_id() == arg_id) + .expect("argument not found"); + + let mut error = + clap::Error::new(clap::error::ErrorKind::WrongNumberOfValues).with_cmd(&command); + error.insert( + clap::error::ContextKind::InvalidArg, + clap::error::ContextValue::String(arg.to_string()), + ); + error.insert( + clap::error::ContextKind::ActualNumValues, + clap::error::ContextValue::Number(actual_len as isize), + ); + error.insert( + clap::error::ContextKind::ExpectedNumValues, + clap::error::ContextValue::Number(expected_len as isize), + ); + + // We don't show the usage because only Command::_build_subcommand() can + // create an appropriate Command instance for showing the subcommand + // usage and there's no way to call that, directly or indirectly. + + error.exit(); + } + + let (inputs, mut metadata) = open_lp_inputs(&cli.input)?; + let mut outputs = open_lp_outputs(&cli.output)?; + + // Display only the selected slot and make the rest identical. + let slot_number = get_slot_number(&metadata, cli.slot)?; + retain_slot(&mut metadata, slot_number); + display_metadata(lp_cli, &metadata); + fill_slots(&mut metadata); + + let slot = &metadata.slots[0]; + + if slot.block_devices.len() != inputs.len() { + bail!( + "Need {} images, but have {}", + slot.block_devices.len(), + inputs.len(), + ); + } + + // Write the new metadata. + metadata + .to_writer(&mut outputs[0]) + .with_context(|| format!("Failed to write LP image metadata: {:?}", cli.output[0]))?; + + // Explicitly set the file size in case there are dm-zero extents, which are + // ignored below. + if metadata.image_type == ImageType::Normal { + for (i, (block_device, output)) in slot.block_devices.iter().zip(&outputs).enumerate() { + output + .set_len(block_device.size) + .with_context(|| format!("Failed to truncate file: {:?}", cli.output[i]))?; + } + } + + slot.groups + .par_iter() + // Flatten grouped partitions. + .flat_map(|group| &group.partitions) + // Flatten extents in all partitions and split them to smaller chunks + // for better parallelism. + .flat_map(|partition| split_extents(&partition.extents)) + .map(|extent| { + // Never fails for PSeekFiles. + let mut reader = inputs[extent.device_index].reopen()?; + let mut writer = outputs[extent.device_index].reopen()?; + + let r_path = &cli.input[extent.device_index]; + let w_path = &cli.output[extent.device_index]; + + reader + .seek(SeekFrom::Start(extent.lp_offset)) + .with_context(|| format!("Failed to seek file: {r_path:?}"))?; + writer + .seek(SeekFrom::Start(extent.lp_offset)) + .with_context(|| format!("Failed to seek file: {w_path:?}"))?; + + stream::copy_n(&mut reader, &mut writer, extent.size, cancel_signal) + .with_context(|| format!("Failed to copy extent: {r_path:?} -> {w_path:?}"))?; + + Ok(()) + }) + .collect::>()?; + + Ok(()) +} + +fn info_subcommand(lp_cli: &LpCli, cli: &InfoCli) -> Result<()> { + let (_, metadata) = open_lp_inputs(&[&cli.input])?; + + // Unlike the other subcommands, we show all metadata slots here. + display_metadata(lp_cli, &metadata); + + Ok(()) +} + +pub fn lp_main(cli: &LpCli, cancel_signal: &AtomicBool) -> Result<()> { + match &cli.command { + LpCommand::Unpack(c) => unpack_subcommand(cli, c, cancel_signal), + LpCommand::Pack(c) => pack_subcommand(cli, c, cancel_signal), + LpCommand::Repack(c) => repack_subcommand(cli, c, cancel_signal), + LpCommand::Info(c) => info_subcommand(cli, c), + } +} + +/// Unpack an LP image. +/// +/// Each partition is extracted to `.img` in the output images +/// directory. This is the case even when unpacking empty LP images. In this +/// scenario, the partition images are sparse files containing no data. +/// +/// The LP image metadata is written to the info TOML file. +/// +/// If any partition names are unsafe to use in a path, the extraction process +/// will fail and exit. Extracted files are never written outside of the tree +/// directory, even if an external process tries to interfere. +#[derive(Debug, Parser)] +struct UnpackCli { + /// Path to input LP images. + /// + /// If there are multiple images, they must be specified in order. If the + /// order is unknown, run `avbroot lp info` against the `super` image and + /// look at the `block_devices` field. + #[arg(short, long, value_name = "FILE", value_parser, required = true)] + input: Vec, + + /// Path to output info TOML. + #[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")] + output_info: PathBuf, + + /// Path to output images directory. + #[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")] + output_images: PathBuf, + + /// The LP metadata slot to use. + /// + /// This slot is the only slot where data extents are copied from. Any data + /// referenced exclusively by other slots (if any) will be ignored. + /// + /// This option is required if not all slots are identical. + #[arg(short, long)] + slot: Option, +} + +/// Pack an LP image. +/// +/// For normal images, the number of metadata slots written is equal to the +/// `metadata_slot_count` value in the info TOML. Each slot has identical +/// metadata. It is not possible to write multiple slots with different metadata +/// using this tool. For empty images, only a single slot is written, regardless +/// of the value of `metadata_slot_count`, as required by the file format. +/// +/// The new LP image will *only* contain images listed in the info TOML file and +/// they are added in the order listed. Note that the input partition images are +/// required even when packing an empty LP image. They can be sparse files that +/// contain no data, but must have the proper size. +#[derive(Debug, Parser)] +struct PackCli { + /// Path to output LP images. + /// + /// If there are multiple images, they must be specified in the same order + /// as the block device entries are listed in the info TOML. + #[arg(short, long, value_name = "FILE", value_parser, required = true)] + output: Vec, + + /// Path to input info TOML. + #[arg(long, value_name = "FILE", value_parser, default_value = "lp.toml")] + input_info: PathBuf, + + /// Path to input images directory. + #[arg(long, value_name = "DIR", value_parser, default_value = "lp_images")] + input_images: PathBuf, +} + +/// Repack an LP image. +/// +/// This command is equivalent to running `unpack` and `pack`, except without +/// storing the unpacked data to disk. +#[derive(Debug, Parser)] +struct RepackCli { + /// Path to input LP images. + /// + /// If there are multiple images, they must be specified in order. If the + /// order is unknown, run `avbroot lp info` against the `super` image and + /// look at the `block_devices` field. + #[arg(short, long, value_name = "FILE", value_parser, required = true)] + input: Vec, + + /// Path to output LP images. + /// + /// The number of output images must equal the number of input images. + #[arg(short, long, value_name = "FILE", value_parser, required = true)] + output: Vec, + + /// The LP metadata slot to use. + /// + /// This slot is the only slot where data extents are copied to the output + /// images. Any data referenced exclusively by other slots (if any) will be + /// ignored. + /// + /// This option is required if not all slots are identical. + #[arg(short, long)] + slot: Option, +} + +/// Display LP image metadata. +#[derive(Debug, Parser)] +struct InfoCli { + /// Path to input LP image. + /// + /// If there are multiple images, this should refer to the first one, which + /// is usually the `super` image. The other images are not needed when + /// inspecting the metadata because the metadata is only stored in the first + /// image. + #[arg(short, long, value_name = "FILE", value_parser)] + input: PathBuf, +} + +#[derive(Debug, Subcommand)] +enum LpCommand { + Unpack(UnpackCli), + Pack(PackCli), + Repack(RepackCli), + Info(InfoCli), +} + +/// Pack, unpack, and inspect LP images. +#[derive(Debug, Parser)] +pub struct LpCli { + #[command(subcommand)] + command: LpCommand, + + /// Don't print LP metadata information. + #[arg(short, long, global = true)] + quiet: bool, +} diff --git a/avbroot/src/cli/mod.rs b/avbroot/src/cli/mod.rs index 63e872a..0b6371b 100644 --- a/avbroot/src/cli/mod.rs +++ b/avbroot/src/cli/mod.rs @@ -11,5 +11,6 @@ pub mod cpio; pub mod fec; pub mod hashtree; pub mod key; +pub mod lp; pub mod ota; pub mod payload; diff --git a/avbroot/src/cli/payload.rs b/avbroot/src/cli/payload.rs index 0b8293f..73d1bb7 100644 --- a/avbroot/src/cli/payload.rs +++ b/avbroot/src/cli/payload.rs @@ -461,7 +461,7 @@ enum PayloadCommand { Info(InfoCli), } -/// Inspect OTA payloads. +/// Pack, unpack, and inspect OTA payloads. #[derive(Debug, Parser)] pub struct PayloadCli { #[command(subcommand)] diff --git a/avbroot/src/format/lp.rs b/avbroot/src/format/lp.rs new file mode 100644 index 0000000..47406d4 --- /dev/null +++ b/avbroot/src/format/lp.rs @@ -0,0 +1,1855 @@ +/* + * SPDX-FileCopyrightText: 2024 Andrew Gunnerson + * SPDX-License-Identifier: GPL-3.0-only + */ + +use std::{ + cmp::Ordering, + fmt, + io::{self, Read, Seek, Write}, + mem, + num::NonZeroU64, + str::{self, FromStr}, +}; + +use bitflags::bitflags; +use bstr::ByteSlice; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +use zerocopy::{byteorder::little_endian, AsBytes, FromBytes, FromZeroes, Unaligned}; + +use crate::{ + format::padding, + stream::{CountingReader, FromReader, ReadDiscardExt, ToWriter, WriteZerosExt}, + util::{self, is_zero}, +}; + +/// Magic value for [`RawGeometry::magic`]. +const GEOMETRY_MAGIC: u32 = 0x616c4467; + +/// Padded size for storing a [`RawGeometry`]. +const GEOMETRY_SIZE: u32 = 4096; + +/// Magic value for [`RawHeader::magic`]. +const HEADER_MAGIC: u32 = 0x414C5030; + +/// Supported major version. +pub const MAJOR_VERSION: u16 = 10; +/// Minimum supported minor version (inclusive). +pub const MINOR_VERSION_MIN: u16 = 0; +/// Maximum supported minor version (inclusive). +pub const MINOR_VERSION_MAX: u16 = 2; + +/// Minor version required for using [`PartitionAttribute::UPDATED`]. +const VERSION_FOR_UPDATED_ATTR: u16 = 1; +/// Metadata minor version needed for the 256-byte [`RawHeader`] instead of the +/// 128-byte header without [`RawHeader::flags`] and [`RawHeader::reserved`]. +const VERSION_FOR_EXPANDED_HEADER: u16 = 2; + +/// Size of a sector. +pub const SECTOR_SIZE: u32 = 512; + +/// Padding at the beginning of a super image to avoid creating a boot sector. +const PARTITION_RESERVED_BYTES: u32 = 4096; + +/// Maximum allowed size of [`RawGeometry::metadata_max_size`] to prevent the +/// memory usage from blowing up. +const METADATA_MAX_SIZE: u32 = 128 * 1024; + +#[derive(Debug, Error)] +pub enum Error { + #[error("Invalid partition name: {0}")] + PartitionNameInvalid(String), + #[error("Geometry: {0}")] + Geometry(String), + #[error("Descriptor offset #{0}: {1}")] + Descriptor(u32, String), + #[error("Header: {0}")] + Header(String), + #[error("Partition: {0}: {1}")] + Partition(String, String), + #[error("Metadata extent #{0}: {1}")] + Extent(usize, String), + #[error("Partition group: {0}: {1}")] + PartitionGroup(String, String), + #[error("Block device: {0}: {1}")] + BlockDevice(String, String), + #[error("Metadata: {0}")] + Metadata(String), + #[error("Insufficient space on block devices to allocate sectors")] + AllocatorDeviceFull, + #[error("I/O error")] + Io(#[from] io::Error), +} + +type Result = std::result::Result; + +bitflags! { + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct HeaderFlags: u32 { + /// The device uses virtual A/B. + const VIRTUAL_AB_DEVICE = 1 << 0; + /// The device has overlay mounts due to `adb remount`. + const OVERLAYS_ACTIVE = 1 << 1; + + const _ = !0; + } + + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct PartitionAttributes: u32 { + /// The device-mapper block device should be created as read-only. + const READONLY = 1 << 0; + /// The super partition itself needs a slot suffix appended. + const SLOT_SUFFIXED = 1 << 1; + /// The partition was created or modified for an OTA update using + /// snapuserd. + const UPDATED = 1 << 2; + /// The partition should be mapped in device-mapper. + const DISABLED = 1 << 3; + + const _ = !0; + } + + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct PartitionGroupFlags: u32 { + /// Whether the group name needs a slot suffix to be appended. + const SLOT_SUFFIXED = 1 << 0; + + const _ = !0; + } + + #[repr(transparent)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] + pub struct BlockDeviceFlags: u32 { + /// Whether the partition name needs a slot suffix to be appended. + const SLOT_SUFFIXED = 1 << 0; + + const _ = !0; + } +} + +impl PartitionAttributes { + /// Attributes introduced in metadata minor version 0. + pub const MASK_V0: Self = Self::READONLY.union(Self::SLOT_SUFFIXED); + /// Attributes introduced in metadata minor version 1. + pub const MASK_V1: Self = Self::UPDATED.union(Self::DISABLED); + /// All supported attributes. + pub const MASK: Self = Self::MASK_V0.union(Self::MASK_V1); +} + +/// Raw on-disk layout for the metadata geometry. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawGeometry { + /// Magic value. This should be equal to [`GEOMETRY_MAGIC`]. + magic: little_endian::U32, + /// Size of this [`RawGeometry`]. + struct_size: little_endian::U32, + /// SHA-256 checksum of this [`RawGeometry`] when this field is set to all + /// zeros. + checksum: [u8; 32], + /// Maximum size of a single copy of the metadata (header + tables). This + /// must be a multiple of [`SECTOR_SIZE`]. + metadata_max_size: little_endian::U32, + /// Number of metadata slots, excluding the backup copies. + metadata_slot_count: little_endian::U32, + /// Block device block size for the logical partitions. This must be a + /// multiple of [`SECTOR_SIZE`]. + logical_block_size: little_endian::U32, +} + +const _: () = assert!(mem::size_of::() < GEOMETRY_SIZE as usize); + +impl fmt::Debug for RawGeometry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawGeometry") + .field("magic", &format_args!("{:#08x}", self.magic.get())) + .field("struct_size", &self.struct_size.get()) + .field("checksum", &hex::encode(self.checksum)) + .field("metadata_max_size", &self.metadata_max_size.get()) + .field("metadata_slot_count", &self.metadata_slot_count.get()) + .field("logical_block_size", &self.logical_block_size.get()) + .finish() + } +} + +impl RawGeometry { + /// Ensure that all fields are semantically valid and can be used without + /// further checks. + fn validate(&self) -> Result<()> { + if self.magic.get() != GEOMETRY_MAGIC { + return Err(Error::Geometry(format!( + "Invalid magic: {:#08x}", + self.magic.get(), + ))); + } + + if self.struct_size.get() != mem::size_of::() as u32 { + return Err(Error::Geometry(format!( + "Invalid struct size: {}", + self.struct_size.get(), + ))); + } + + #[cfg(not(fuzzing))] + { + let mut copy = *self; + copy.checksum.fill(0); + + let digest = ring::digest::digest(&ring::digest::SHA256, copy.as_bytes()); + if digest.as_ref() != self.checksum { + return Err(Error::Geometry(format!( + "Expected digest {}, but have {}", + hex::encode(self.checksum), + hex::encode(digest), + ))); + } + } + + if self.metadata_max_size.get() == 0 || self.metadata_max_size.get() % SECTOR_SIZE != 0 { + return Err(Error::Geometry(format!( + "Maximum metadata size is not sector-aligned: {}", + self.metadata_max_size.get(), + ))); + } else if self.metadata_max_size.get() > METADATA_MAX_SIZE { + return Err(Error::Geometry(format!( + "Maximum metadata size exceeds limit: {} > {METADATA_MAX_SIZE}", + self.metadata_max_size.get(), + ))); + } else if self.metadata_slot_count.get() == 0 { + return Err(Error::Geometry("No metadata slots defined".into())); + } + + if self.logical_block_size.get() % SECTOR_SIZE != 0 { + return Err(Error::Geometry(format!( + "Logical block size is not sector-aligned: {}", + self.logical_block_size.get(), + ))); + } + + Ok(()) + } +} + +/// Raw on-disk layout for a table descriptor within a [`RawHeader`]. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawTableDescriptor { + /// Offset relative to the end of the [`RawHeader`]. + offset: little_endian::U32, + /// Number of entries in the table. + num_entries: little_endian::U32, + /// Size of each entry. + entry_size: little_endian::U32, +} + +impl fmt::Debug for RawTableDescriptor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawTableDescriptor") + .field("offset", &self.offset.get()) + .field("num_entries", &self.num_entries.get()) + .field("entry_size", &self.entry_size.get()) + .finish() + } +} + +impl RawTableDescriptor { + /// Return a slice of the specified table item type `T` from the tables + /// buffer. `buf` must have size [`RawHeader::tables_size`] and the header + /// containing this descriptor must have already passed + /// [`RawHeader::validate`]. Otherwise, this function may panic. + fn slice_from_buf<'a, T: FromBytes + 'a>(&self, buf: &'a [u8]) -> &'a [T] { + let offset = self.offset.get() as usize; + let entry_size = self.entry_size.get() as usize; + let size = self.num_entries.get() as usize * entry_size; + let buf = &buf[offset..][..size]; + + assert_eq!(mem::size_of::(), entry_size); + + T::slice_from(buf).unwrap() + } + + /// Update all fields to match the slice of items beginning at the specified + /// table offset. Returns the starting offset for the next table. + fn update(&mut self, items: &[T], offset: u32) -> Result { + let entry_size = mem::size_of::() as u32; + let num_entries: u32 = items + .len() + .try_into() + .map_err(|_| Error::Descriptor(offset, "Entry count out of bounds".into()))?; + let next_offset = entry_size + .checked_mul(num_entries) + .and_then(|o| o.checked_add(offset)) + .ok_or_else(|| Error::Descriptor(offset, "Next entry offset out of bounds".into()))?; + + self.offset = offset.into(); + self.entry_size = entry_size.into(); + self.num_entries = num_entries.into(); + + Ok(next_offset) + } +} + +/// Raw on-disk layout for the metadata header. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawHeader { + /// Magic value. This should be equal to [`HEADER_MAGIC`]. + magic: little_endian::U32, + /// Major version. [`MAJOR_VERSION`] is the only version supported. All + /// other versions cannot be parsed. + major_version: little_endian::U16, + /// Minor version. Versions between [`MINOR_VERSION_MIN`] and + /// [`MINOR_VERSION_MAX`] are supported. + minor_version: little_endian::U16, + /// Size of this [`RawHeader`]. + header_size: little_endian::U32, + /// SHA-256 checksum of this [`RawHeader`] when this field is set to all + /// zeros. + header_checksum: [u8; 32], + /// Size of all tables. + tables_size: little_endian::U32, + /// SHA-256 checksum of all tables. + tables_checksum: [u8; 32], + /// Partition table descriptor. + partitions: RawTableDescriptor, + /// Extent table descriptor. + extents: RawTableDescriptor, + /// Updatable group descriptor. + groups: RawTableDescriptor, + /// Block device table descriptor. + block_devices: RawTableDescriptor, + /// [Minor version >=2 only] Header flags. These are informational and do + /// not affect parsing. + flags: little_endian::U32, + /// [Minor version >=2 only] Reserved bytes for future header versions. + reserved: [u8; 124], +} + +impl fmt::Debug for RawHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawHeader") + .field("magic", &format_args!("{:#08x}", self.magic.get())) + .field("major_version", &self.major_version.get()) + .field("minor_version", &self.minor_version.get()) + .field("header_size", &self.header_size.get()) + .field("header_checksum", &hex::encode(self.header_checksum)) + .field("tables_size", &self.tables_size.get()) + .field("tables_checksum", &hex::encode(self.tables_checksum)) + .field("partitions", &self.partitions) + .field("extents", &self.extents) + .field("groups", &self.groups) + .field("block_devices", &self.block_devices) + .field("flags", &HeaderFlags::from_bits_retain(self.flags.get()).0) + .field("reserved", &hex::encode(self.reserved)) + .finish() + } +} + +impl RawHeader { + const SIZE_V1_0: usize = mem::offset_of!(Self, flags); + + fn size_for_version(major_version: u16, minor_version: u16) -> usize { + if major_version == MAJOR_VERSION && minor_version >= VERSION_FOR_EXPANDED_HEADER { + mem::size_of::() + } else { + Self::SIZE_V1_0 + } + } + + fn size(&self) -> usize { + Self::size_for_version(self.major_version.get(), self.minor_version.get()) + } + + fn validate_descriptor( + &self, + descriptor: &RawTableDescriptor, + start_offset: u32, + ) -> Option { + if descriptor.offset.get() != start_offset { + return None; + } + + let size = descriptor + .num_entries + .get() + .checked_mul(descriptor.entry_size.get())?; + let next_offset = start_offset.checked_add(size)?; + + if next_offset > self.tables_size.get() { + return None; + } + + Some(next_offset) + } + + /// Ensure that all fields are semantically valid and can be used without + /// further checks. [`RawGeometry::validate`] must have passed before this + /// function is called. + fn validate(&self, geometry: &RawGeometry) -> Result<()> { + if self.magic.get() != HEADER_MAGIC { + return Err(Error::Header(format!( + "Invalid magic: {:#08x}", + self.magic.get(), + ))); + } + + if self.major_version.get() != MAJOR_VERSION || self.minor_version.get() > MINOR_VERSION_MAX + { + return Err(Error::Header(format!( + "Unsupported version: {}.{}", + self.major_version.get(), + self.minor_version.get(), + ))); + } + + let expected_size = self.size(); + + if self.header_size.get() != expected_size as u32 { + return Err(Error::Header(format!( + "Invalid struct size: {}", + self.header_size.get(), + ))); + } + + if self.minor_version.get() < VERSION_FOR_EXPANDED_HEADER { + // This would be an implementation error. + assert!(self.flags.get() == 0); + assert!(util::is_zero(&self.reserved)); + } + + #[cfg(not(fuzzing))] + { + let mut copy = *self; + copy.header_checksum.fill(0); + + let portion = ©.as_bytes()[..expected_size]; + + let digest = ring::digest::digest(&ring::digest::SHA256, portion); + if digest.as_ref() != self.header_checksum { + return Err(Error::Header(format!( + "Expected header digest {}, but have {}", + hex::encode(self.header_checksum), + hex::encode(digest), + ))); + } + } + + // metadata_max_size is guaranteed to be at least one sector, so the + // subtraction cannot overflow. + if self.tables_size.get() > geometry.metadata_max_size.get() - self.header_size.get() { + return Err(Error::Header("Metadata slot exceeds maximum size".into())); + } + + let mut offset = 0; + + // The tables must be contiguous. + for descriptor in [ + &self.partitions, + &self.extents, + &self.groups, + &self.block_devices, + ] { + offset = self + .validate_descriptor(descriptor, offset) + .ok_or_else(|| Error::Header("Descriptors out of bounds".into()))?; + } + + // There cannot be a gap at the end either. + if offset != self.tables_size.get() { + return Err(Error::Header("Gap after last descriptor".into())); + } + + if self.partitions.entry_size.get() != mem::size_of::() as u32 + || self.extents.entry_size.get() != mem::size_of::() as u32 + || self.groups.entry_size.get() != mem::size_of::() as u32 + || self.block_devices.entry_size.get() != mem::size_of::() as u32 + { + return Err(Error::Header("Invalid descriptor entry sizes".into())); + } + + Ok(()) + } +} + +/// A potentially invalid raw partition name string. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct PartitionName([u8; 36]); + +impl fmt::Debug for PartitionName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (prefix, suffix) = self.split(); + let display = if util::is_zero(suffix) { + prefix + } else { + &self.0 + }; + + fmt::Debug::fmt(&display.as_bstr(), f) + } +} + +impl PartitionName { + fn split(&self) -> (&[u8], &[u8]) { + match self.0.iter().position(|b| *b == 0) { + Some(i) => self.0.split_at(i), + None => (&self.0, &[]), + } + } + + fn validate(&self) -> Result<()> { + let (prefix, suffix) = self.split(); + let mut has_alnum = false; + + for b in prefix { + match b { + b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' => has_alnum = true, + b'_' => {} + _ => return Err(Error::PartitionNameInvalid(format!("{self:?}"))), + } + } + + if has_alnum && is_zero(suffix) { + Ok(()) + } else { + Err(Error::PartitionNameInvalid(format!("{self:?}"))) + } + } + + fn as_str(&self) -> Result<&str> { + self.validate()?; + + // ASCII is always UTF-8. + Ok(str::from_utf8(self.split().0).unwrap()) + } +} + +impl FromStr for PartitionName { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut name = Self([0u8; 36]); + + if s.len() > name.0.len() { + return Err(Error::PartitionNameInvalid(format!("{s:?}"))); + } + + let to_copy = s.len().min(name.0.len()); + name.0[..to_copy].copy_from_slice(&s.as_bytes()[..to_copy]); + + name.validate()?; + + Ok(name) + } +} + +/// Raw on-disk layout for an entry in the logical partitions table. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawPartition { + /// Partition name in ASCII. This must be unique across all partitions. + name: PartitionName, + /// Partition attributes. + attributes: little_endian::U32, + /// Index of the first extent owned by this partition. + first_extent_index: little_endian::U32, + /// Number of extents covered by this partition. + num_extents: little_endian::U32, + /// Index of the group containing this partition. + group_index: little_endian::U32, +} + +impl fmt::Debug for RawPartition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let attributes = PartitionAttributes::from_bits_retain(self.attributes.get()); + + f.debug_struct("RawPartition") + .field("name", &self.name) + .field("attributes", &attributes.0) + .field("first_extent_index", &self.first_extent_index.get()) + .field("num_extents", &self.num_extents.get()) + .field("group_index", &self.group_index.get()) + .finish() + } +} + +impl RawPartition { + /// Ensure that all fields are semantically valid and can be used without + /// further checks. [`RawHeader::validate`] must have passed before this + /// function is called, but [`RawExtent::validate`] and + /// [`RawPartitionGroup::validate`] do not. + fn validate( + &self, + image_type: ImageType, + header: &RawHeader, + extents: &[RawExtent], + groups: &[RawPartitionGroup], + ) -> Result<()> { + self.name.validate()?; + + let mut valid_attributes = PartitionAttributes::MASK_V0; + if header.minor_version.get() >= VERSION_FOR_UPDATED_ATTR { + valid_attributes |= PartitionAttributes::MASK_V1; + } + + let attributes = PartitionAttributes::from_bits_retain(self.attributes.get()); + + if !(attributes - valid_attributes).is_empty() { + return Err(Error::Partition( + format!("{:?}", self.name), + format!("Invalid attributes: {}", attributes.0), + )); + } + + match image_type { + ImageType::Normal => { + if self + .first_extent_index + .get() + .checked_add(self.num_extents.get()) + .map_or(true, |n| n as usize > extents.len()) + { + return Err(Error::Partition( + format!("{:?}", self.name), + "Extent indices out of bounds".into(), + )); + } + } + ImageType::Empty => { + if self.first_extent_index.get() != 0 || self.num_extents.get() != 0 { + return Err(Error::Partition( + format!("{:?}", self.name), + "Extent indices set on empty image".into(), + )); + } + } + } + + if self.group_index.get() as usize >= groups.len() { + return Err(Error::Partition( + format!("{:?}", self.name), + format!("Invalid partition group index: {}", self.group_index.get()), + )); + } + + Ok(()) + } +} + +/// Raw on-disk layout for an entry in the extent table. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawExtent { + /// Number of [`SECTOR_SIZE`]-byte sectors in this extent. + num_sectors: little_endian::U64, + /// device-mapper target type. + target_type: little_endian::U32, + /// For [`TargetType::Linear`], this is the physical partition sector that + /// this extent maps to. For [`TargetType::Zero`], this is always 0. + target_data: little_endian::U64, + /// For [`TargetType::Linear`], this is the index into the block devices + /// table specifying the physical source of this extent. For + /// [`TargetType::Zero`], this is always 0. + target_source: little_endian::U32, +} + +impl fmt::Debug for RawExtent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawExtent") + .field("num_sectors", &self.num_sectors.get()) + .field("target_type", &self.target_type.get()) + .field("target_data", &self.target_data.get()) + .field("target_source", &self.target_source.get()) + .finish() + } +} + +impl RawExtent { + /// dm-linear target. + const TARGET_TYPE_LINEAR: u32 = 0; + /// dm-zero target. + const TARGET_TYPE_ZERO: u32 = 1; + + /// Ensure that all fields are semantically valid and can be used without + /// further checks. [`RawBlockDevice::validate`] does not need to have + /// passed before this function is called. + fn validate(&self, index: usize, block_devices: &[RawBlockDevice]) -> Result<()> { + match self.target_type.get() { + Self::TARGET_TYPE_LINEAR => { + let Some(device) = block_devices.get(self.target_source.get() as usize) else { + return Err(Error::Extent( + index, + format!("Invalid block device index: {}", self.target_source.get()), + )); + }; + + let count = self.num_sectors.get(); + let start = self.target_data.get(); + let end = start.checked_add(count).ok_or_else(|| { + Error::Extent( + index, + format!("End sector out of bounds: {start} + {count}"), + ) + })?; + + if start < device.first_logical_sector.get() { + return Err(Error::Extent( + index, + format!( + "{start} starts before block device's first logical sector {}", + device.first_logical_sector, + ), + )); + } + + let device_sectors = device.size.get() / u64::from(SECTOR_SIZE); + + if end > device_sectors { + return Err(Error::Extent( + index, + format!("{end} ends after block device's sector size {device_sectors}"), + )); + } + } + Self::TARGET_TYPE_ZERO => { + if self.target_data.get() != 0 || self.target_source.get() != 0 { + return Err(Error::Extent( + index, + "Type zero extents cannot have non-zero sector or device".into(), + )); + } + } + n => return Err(Error::Extent(index, format!("Invalid type: {n}"))), + } + + Ok(()) + } +} + +/// Raw on-disk layout for an entry in the partition groups table. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawPartitionGroup { + /// Partition group name in ASCII. This must be unique across all groups. + name: PartitionName, + /// Partition group flags. + flags: little_endian::U32, + /// Maximum size of all partitions in this group. If this is set to 0, then + /// there is no size limit. + maximum_size: little_endian::U64, +} + +impl fmt::Debug for RawPartitionGroup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let flags = PartitionGroupFlags::from_bits_retain(self.flags.get()); + + f.debug_struct("RawPartitionGroup") + .field("name", &self.name) + .field("flags", &flags.0) + .field("maximum_size", &self.maximum_size.get()) + .finish() + } +} + +impl RawPartitionGroup { + /// Ensure that all fields are semantically valid and can be used without + /// further checks. [`RawPartition::validate`] and [`RawExtent::validate`] + /// must have passed for all specified partitions and extents before this + /// function is called. + fn validate( + &self, + index: usize, + partitions: &[RawPartition], + extents: &[RawExtent], + ) -> Result<()> { + if self.maximum_size.get() != 0 { + let mut total_size = 0u64; + + for partition in partitions { + if partition.group_index.get() as usize == index { + let first = partition.first_extent_index.get() as usize; + let count = partition.num_extents.get() as usize; + + for extent in &extents[first..][..count] { + total_size = total_size + .checked_add(extent.num_sectors.get()) + .ok_or_else(|| { + Error::PartitionGroup( + format!("{:?}", self.name), + "Size of group's partitions out of bounds".into(), + ) + })?; + } + } + } + + if total_size > self.maximum_size.get() { + return Err(Error::PartitionGroup( + format!("{:?}", self.name), + format!( + "Total partition size {total_size} exceeds limit {}", + self.maximum_size.get(), + ), + )); + } + } + + self.name.validate() + } +} + +/// Raw on-disk layout for an entry in the block devices table. +#[derive(Clone, Copy, FromZeroes, FromBytes, AsBytes, Unaligned)] +#[repr(packed)] +struct RawBlockDevice { + /// The first [`SECTOR_SIZE`]-byte sector where actual data for the logical + /// partitions can be allocated. + first_logical_sector: little_endian::U64, + /// Alignment for the partition start offset. + alignment: little_endian::U32, + /// Adjustment for when the super partition itself is not aligned. + alignment_offset: little_endian::U32, + /// Block device size. + size: little_endian::U64, + /// Partition name in ASCII. This must be unique across all block devices. + partition_name: PartitionName, + /// Block device flags. + flags: little_endian::U32, +} + +impl fmt::Debug for RawBlockDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let flags = BlockDeviceFlags::from_bits_retain(self.flags.get()); + + f.debug_struct("RawBlockDevice") + .field("first_logical_sector", &self.first_logical_sector.get()) + .field("alignment", &self.alignment.get()) + .field("alignment_offset", &self.alignment_offset.get()) + .field("size", &self.size.get()) + .field("partition_name", &self.partition_name) + .field("flags", &flags.0) + .finish() + } +} + +impl RawBlockDevice { + /// Ensure that all fields are semantically valid and can be used without + /// further checks. + fn validate(&self) -> Result<()> { + if self.alignment.get() == 0 { + return Err(Error::BlockDevice( + format!("{:?}", self.partition_name), + "Alignment is 0".into(), + )); + } else if self.alignment.get() % SECTOR_SIZE != 0 { + return Err(Error::BlockDevice( + format!("{:?}", self.partition_name), + "Partition alignment is not sector-aligned".into(), + )); + } + + let alignment_sectors = u64::from(self.alignment.get() / SECTOR_SIZE); + if self.first_logical_sector.get() % alignment_sectors != 0 { + return Err(Error::BlockDevice( + format!("{:?}", self.partition_name), + "First logical sector is not partition-aligned".into(), + )); + } + + if self.alignment_offset.get() % SECTOR_SIZE != 0 { + return Err(Error::BlockDevice( + format!("{:?}", self.partition_name), + "Alignment offset is not sector-aligned".into(), + )); + } + + if self.size.get() % u64::from(SECTOR_SIZE) != 0 { + return Err(Error::BlockDevice( + format!("{:?}", self.partition_name), + "Size is not sector-aligned".into(), + )); + } + + self.partition_name.validate() + } +} + +/// A wrapper around the on-disk layouts for a single metadata slot. +#[derive(Clone, Debug)] +struct RawMetadataSlot { + header: RawHeader, + partitions: Vec, + extents: Vec, + groups: Vec, + block_devices: Vec, +} + +impl RawMetadataSlot { + /// Ensure that all fields are semantically valid and can be used without + /// further checks. + fn validate(&self, image_type: ImageType, geometry: &RawGeometry) -> Result<()> { + self.header.validate(geometry)?; + + for (len, descriptor, name) in [ + (self.partitions.len(), &self.header.partitions, "partition"), + (self.extents.len(), &self.header.extents, "extent"), + (self.groups.len(), &self.header.groups, "partition group"), + ( + self.block_devices.len(), + &self.header.block_devices, + "block device", + ), + ] { + if len != descriptor.num_entries.get() as usize { + return Err(Error::Header(format!( + "Descriptor entries {} does not match {name} table length {len}", + descriptor.num_entries.get(), + ))); + } + } + + // Although not all of the table validation functions require their + // dependencies to be validated first, we'll validate these tables in + // topological order just to be safe. + + for block_device in &self.block_devices { + block_device.validate()?; + } + + for (i, extent) in self.extents.iter().enumerate() { + extent.validate(i, &self.block_devices)?; + } + + // Ensure that all extents are in increasing order and not overlapping. + let mut iter = self + .extents + .iter() + .filter(|e| e.target_type.get() == RawExtent::TARGET_TYPE_LINEAR) + .enumerate(); + while let (Some((_, a)), Some((i, b))) = (iter.next(), iter.next()) { + match a.target_source.get().cmp(&b.target_source.get()) { + Ordering::Equal => { + if a.target_data.get() + a.num_sectors.get() > b.target_data.get() { + return Err(Error::Extent(i, "Overlaps previous extent".into())); + } + } + Ordering::Greater => { + return Err(Error::Extent( + i, + "Earlier block device index than previous extent".into(), + )); + } + Ordering::Less => {} + } + } + + for partition in &self.partitions { + partition.validate(image_type, &self.header, &self.extents, &self.groups)?; + } + + for (i, group) in self.groups.iter().enumerate() { + group.validate(i, &self.partitions, &self.extents)?; + } + + Ok(()) + } +} + +/// A type for storing the raw metadata of an LP image. This only validates +/// fields when reading and writing the image. No fields are recomputed during +/// writes to guarantee lossless round tripping. +#[derive(Clone, Debug)] +struct RawMetadata { + image_type: ImageType, + geometry: RawGeometry, + slots: Vec, +} + +impl RawMetadata { + /// Read the [`RawGeometry`] at the current offset. + fn read_geometry(mut reader: impl Read) -> Result<(ImageType, RawGeometry)> { + let mut buf = [0u8; GEOMETRY_SIZE as usize]; + reader.read_exact(&mut buf)?; + + let image_type = if util::is_zero(&buf) { + ImageType::Normal + } else { + ImageType::Empty + }; + + let geometry = match image_type { + ImageType::Normal => { + // This is an normal non-empty image, which has extra padding at + // the beginning to avoid having the geometry struct interpreted + // as a boot sector. + + // Read the primary copy of the geometry. + reader.read_exact(&mut buf)?; + + let mut geometry = RawGeometry::ref_from_prefix(&buf).unwrap(); + + match geometry.validate() { + Ok(_) => { + // Skip the backup copy. + reader.read_discard_exact(GEOMETRY_SIZE.into())?; + } + Err(_) => { + // Try to parse the backup copy. + reader.read_exact(&mut buf)?; + + geometry = RawGeometry::ref_from_prefix(&buf).unwrap(); + geometry.validate()?; + } + } + + geometry + } + ImageType::Empty => { + // This is an empty image for use with fastboot. These have no + // extra padding at the beginning of the file nor backup copies + // of the geometry and metadata structs. + let geometry = RawGeometry::ref_from_prefix(&buf).unwrap(); + geometry.validate()?; + + geometry + } + }; + + Ok((image_type, geometry.to_owned())) + } + + /// Read a single [`RawMetadataSlot`], including the header and all tables. + /// This does not read the extra padding between the end of the last table + /// and [`RawGeometry::metadata_max_size`]. + fn read_metadata( + mut reader: impl Read, + image_type: ImageType, + geometry: &RawGeometry, + ) -> Result { + let mut header = RawHeader::new_zeroed(); + + reader.read_exact(&mut header.as_bytes_mut()[..RawHeader::SIZE_V1_0])?; + if header.size() > RawHeader::SIZE_V1_0 { + reader.read_exact(&mut header.as_bytes_mut()[RawHeader::SIZE_V1_0..])?; + } + + // We'll end up validating this again at the end, but this initial + // validation is necessary so ensure everything is in bounds when + // parsing the tables. + header.validate(geometry)?; + + let mut tables_buf = vec![0u8; header.tables_size.get() as usize]; + reader.read_exact(&mut tables_buf)?; + + let partitions = header + .partitions + .slice_from_buf::(&tables_buf); + let extents = header.extents.slice_from_buf::(&tables_buf); + let groups = header + .groups + .slice_from_buf::(&tables_buf); + let block_devices = header + .block_devices + .slice_from_buf::(&tables_buf); + + let slot = RawMetadataSlot { + header, + partitions: partitions.to_vec(), + extents: extents.to_vec(), + groups: groups.to_vec(), + block_devices: block_devices.to_vec(), + }; + + slot.validate(image_type, geometry)?; + + Ok(slot) + } + + /// Ensure that all fields are semantically valid and can be used without + /// further checks. + fn validate(&self) -> Result<()> { + self.geometry.validate()?; + + let expected_slots = match self.image_type { + ImageType::Normal => self.geometry.metadata_slot_count.get() as usize, + ImageType::Empty => 1, + }; + if self.slots.len() != expected_slots { + return Err(Error::Metadata(format!( + "Expected slot count {expected_slots}, but have {}", + self.slots.len(), + ))); + } + + for slot in &self.slots { + #[cfg(not(fuzzing))] + { + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(slot.partitions.as_bytes()); + context.update(slot.extents.as_bytes()); + context.update(slot.groups.as_bytes()); + context.update(slot.block_devices.as_bytes()); + let digest = context.finish(); + + if digest.as_ref() != slot.header.tables_checksum { + return Err(Error::Header(format!( + "Expected tables digest {}, but have {}", + hex::encode(slot.header.tables_checksum), + hex::encode(digest), + ))); + } + } + + slot.validate(self.image_type, &self.geometry)?; + } + + Ok(()) + } +} + +impl FromReader for RawMetadata { + type Error = Error; + + fn from_reader(reader: R) -> Result { + let mut reader = CountingReader::new(reader); + + let (image_type, geometry) = Self::read_geometry(&mut reader)?; + + let (num_copies, num_slots) = match image_type { + // Normal images have a backup copy of everything and contains all + // slots. + ImageType::Normal => (2, geometry.metadata_slot_count.get() as usize), + // Empty images only contain one slot, no matter what the geometry + // says. + ImageType::Empty => (1, 1), + }; + let mut slots = vec![None; num_slots]; + let mut last_err = None; + + for _ in 0..num_copies { + for slot in &mut slots { + let mut to_skip = u64::from(geometry.metadata_max_size.get()); + + if slot.is_none() { + let orig_offset = reader.stream_position()?; + + match Self::read_metadata(&mut reader, image_type, &geometry) { + Ok(m) => *slot = Some(m), + Err(e @ Error::Io(_)) => return Err(e), + Err(e) => last_err = Some(e), + } + + // Skip the remaining padding. + let cur_offset = reader.stream_position()?; + to_skip -= cur_offset - orig_offset; + } + + reader.read_discard(to_skip)?; + } + } + + let slots = slots + .into_iter() + .collect::>>() + .ok_or_else(|| last_err.unwrap())?; + + Ok(Self { + image_type, + geometry, + slots, + }) + } +} + +impl ToWriter for RawMetadata { + type Error = Error; + + fn to_writer(&self, mut writer: W) -> Result<()> { + self.validate()?; + + let geometry = self.geometry.as_bytes(); + let geometry_padding = GEOMETRY_SIZE as usize - geometry.len(); + + match self.image_type { + ImageType::Normal => { + writer.write_zeros_exact(PARTITION_RESERVED_BYTES.into())?; + + for _ in 0..2 { + writer.write_all(geometry)?; + writer.write_zeros_exact(geometry_padding as u64)?; + } + + let metadata_max_size = self.geometry.metadata_max_size.get() as usize; + + for _ in 0..2 { + for slot in &self.slots { + let header_size = RawHeader::size_for_version( + slot.header.major_version.get(), + slot.header.minor_version.get(), + ); + let header = &slot.header.as_bytes()[..header_size]; + let tables_size = slot.header.tables_size.get() as usize; + let metadata_padding = metadata_max_size - header.len() - tables_size; + + writer.write_all(header)?; + writer.write_all(slot.partitions.as_bytes())?; + writer.write_all(slot.extents.as_bytes())?; + writer.write_all(slot.groups.as_bytes())?; + writer.write_all(slot.block_devices.as_bytes())?; + writer.write_zeros_exact(metadata_padding as u64)?; + } + } + } + ImageType::Empty => { + writer.write_all(geometry)?; + writer.write_zeros_exact(geometry_padding as u64)?; + + writer.write_all(self.slots[0].header.as_bytes())?; + writer.write_all(self.slots[0].partitions.as_bytes())?; + writer.write_all(self.slots[0].extents.as_bytes())?; + writer.write_all(self.slots[0].groups.as_bytes())?; + writer.write_all(self.slots[0].block_devices.as_bytes())?; + } + } + + Ok(()) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub enum ImageType { + /// An normal LP image containing actual partition data. This has the final + /// layout for how the image would be stored on disk on a device. There is + /// a [`PARTITION_RESERVED_BYTES`]-byte gap at the beginning of the image, + /// followed by two copies of the geometry structure and two copies of the + /// metadata. + Normal, + /// An empty LP image containing no partition data. This is meant for use + /// with fastboot only (`super_empty.img`). There are no reserved bytes at + /// the beginning of the image and there is only a single copy of the + /// geometry and the metadata. The metadata is not padded to the maximum + /// size specified in the geometry. + Empty, +} + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct Partition { + /// Partition name in ASCII. + pub name: String, + /// Partition attributes. + pub attributes: PartitionAttributes, + /// Extents covered by this partition. + #[serde(skip)] + pub extents: Vec, +} + +impl fmt::Debug for Partition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Partition") + .field("name", &self.name) + .field("attributes", &self.attributes.0) + .field("extents", &self.extents) + .finish() + } +} + +impl Partition { + /// Compute the number of sectors covered by the extents. + pub fn num_sectors(&self) -> Result { + self.extents + .iter() + .try_fold(0u64, |total, e| total.checked_add(e.num_sectors)) + .ok_or_else(|| Error::Partition(self.name.clone(), "Sector count overflow".into())) + } + + /// Compute the number of bytes covered by the extents. + pub fn size(&self) -> Result { + self.num_sectors() + .ok() + .and_then(|n| n.checked_mul(SECTOR_SIZE.into())) + .ok_or_else(|| Error::Partition(self.name.clone(), "Byte count overflow".into())) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExtentType { + Linear { + /// The physical sector that this extent starts at on the block device. + start_sector: u64, + /// The index of the block device that backs this extent. + block_device_index: usize, + }, + Zero, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct Extent { + /// Number of [`SECTOR_SIZE`]-byte sectors in this extent. + pub num_sectors: u64, + /// device-mapper target type. + pub extent_type: ExtentType, +} + +impl fmt::Debug for Extent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Extent") + .field("num_sectors", &self.num_sectors) + .field("extent_type", &self.extent_type) + .finish() + } +} + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct PartitionGroup { + /// Partition group name in ASCII. + pub name: String, + /// Partition group flags. + pub flags: PartitionGroupFlags, + /// Maximum size of all partitions in this group. + pub maximum_size: Option, + /// The partitions in this group. + pub partitions: Vec, +} + +impl fmt::Debug for PartitionGroup { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PartitionGroup") + .field("name", &self.name) + .field("flags", &self.flags.0) + .field("maximum_size", &format_args!("{:?}", self.maximum_size)) + .field("partitions", &self.partitions) + .finish() + } +} + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct BlockDevice { + /// The first [`SECTOR_SIZE`]-byte sector where actual data for the logical + /// partitions can be allocated. + pub first_logical_sector: u64, + /// Alignment for both partition and extent sizes + pub alignment: u32, + /// Alignment offset for when the super partition itself is not aligned. + pub alignment_offset: u32, + /// Block device size. + pub size: u64, + /// Partition name in ASCII. + pub partition_name: String, + /// Block device flags. + pub flags: BlockDeviceFlags, +} + +impl fmt::Debug for BlockDevice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BlockDevice") + .field("first_logical_sector", &self.first_logical_sector) + .field("alignment", &self.alignment) + .field("alignment_offset", &self.alignment_offset) + .field("size", &self.size) + .field("partition_name", &self.partition_name) + .field("flags", &self.flags.0) + .finish() + } +} + +/// Basic allocator that allocates sectors from a list of block devices linearly +/// and respects alignment if requested. +#[derive(Clone, Copy, Debug)] +struct LinearAllocator<'a> { + devices: &'a [BlockDevice], + /// Current device to allocate from. + index: usize, + /// Current device's alignment in sectors. + align: u64, + /// Starting sector for next (potentially unaligned) allocation. + sector: u64, + /// Remaining free sectors. + remain: u64, +} + +impl<'a> LinearAllocator<'a> { + pub fn new(devices: &'a [BlockDevice]) -> Self { + let device = &devices[0]; + let align = u64::from(device.alignment / SECTOR_SIZE); + let sector = device.first_logical_sector; + let remain = device.size / u64::from(SECTOR_SIZE) - sector; + + Self { + devices, + index: 0, + align, + sector, + remain, + } + } + + /// Move to the next [`BlockDevice`]. [`Self::sector`] is guaranteed to be + /// aligned. + fn move_to_next_device(&mut self) -> Result<()> { + if self.index + 1 == self.devices.len() { + return Err(Error::AllocatorDeviceFull); + } + + self.index += 1; + + let device = &self.devices[self.index]; + self.align = u64::from(device.alignment / SECTOR_SIZE); + self.sector = device.first_logical_sector; + self.remain = device.size / u64::from(SECTOR_SIZE) - self.sector; + + Ok(()) + } + + /// Try to allocate some sectors. If this allocation would need to cross a + /// block device boundary, then this will only return the first extent on + /// the current device. Call this function again with the remaining sectors + /// to get the next extent. + pub fn try_allocate(&mut self, req_sectors: u64) -> Result { + if self.remain == 0 { + // We do this here instead of at the end of the function so that + // allocation does not fail if we fill up every device exactly. + self.move_to_next_device()?; + } else { + let padding = padding::calc(self.sector, self.align); + if padding >= self.remain { + // The rest of this device is unusable due to alignment. The + // starting sector of the next device will be aligned and there + // will be at least one free sector available. + self.move_to_next_device()?; + } else { + self.sector += padding; + self.remain -= padding; + } + } + + let extent = Extent { + num_sectors: req_sectors.min(self.remain), + extent_type: ExtentType::Linear { + start_sector: self.sector, + block_device_index: self.index, + }, + }; + + self.sector += extent.num_sectors; + self.remain -= extent.num_sectors; + + Ok(extent) + } +} + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +pub struct MetadataSlot { + /// Major version. [`MAJOR_VERSION`] is the only version supported. All + /// other versions cannot be parsed. + pub major_version: u16, + /// Minor version. Versions between [`MINOR_VERSION_MIN`] and + /// [`MINOR_VERSION_MAX`] are supported. + pub minor_version: u16, + /// List of partition groups. + pub groups: Vec, + /// List of block devices containing data extents. + pub block_devices: Vec, + /// Header flags. These are informational and do not affect parsing. + pub flags: HeaderFlags, +} + +impl fmt::Debug for MetadataSlot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MetadataSlot") + .field("major_version", &self.major_version) + .field("minor_version", &self.minor_version) + .field("groups", &self.groups) + .field("block_devices", &self.block_devices) + .field("flags", &self.flags.0) + .finish() + } +} + +impl MetadataSlot { + /// Recompute the extents for every partition, allocating from each sector + /// device in order as they fill up. Each partition will have a starting + /// sector that is aligned to [`BlockDevice::alignment`]. As long as the + /// partition fits on a single device, it will only have a single extent. + pub fn reallocate_extents(&mut self) -> Result<()> { + { + let raw_slot: RawMetadataSlot = (&*self).try_into()?; + + for raw_block_device in &raw_slot.block_devices { + raw_block_device.validate()?; + } + } + + let mut allocator = LinearAllocator::new(&self.block_devices); + + for group in &mut self.groups { + for partition in &mut group.partitions { + let mut sectors = partition.num_sectors()?; + let mut extents = vec![]; + + while sectors > 0 { + let extent = allocator.try_allocate(sectors)?; + sectors -= extent.num_sectors; + extents.push(extent); + } + + partition.extents = extents; + } + } + + Ok(()) + } +} + +impl TryFrom<&RawMetadataSlot> for MetadataSlot { + type Error = Error; + + fn try_from(raw_slot: &RawMetadataSlot) -> Result { + let mut slot = MetadataSlot { + major_version: raw_slot.header.major_version.get(), + minor_version: raw_slot.header.minor_version.get(), + groups: Vec::with_capacity(raw_slot.groups.len()), + block_devices: Vec::with_capacity(raw_slot.block_devices.len()), + flags: HeaderFlags::from_bits_retain(raw_slot.header.flags.get()), + }; + + for raw_group in &raw_slot.groups { + let group = PartitionGroup { + name: raw_group.name.as_str()?.to_owned(), + flags: PartitionGroupFlags::from_bits_retain(raw_group.flags.get()), + maximum_size: NonZeroU64::new(raw_group.maximum_size.get()), + partitions: Vec::new(), + }; + + slot.groups.push(group); + } + + for raw_partition in &raw_slot.partitions { + let group_index = raw_partition.group_index.get() as usize; + let first_extent = raw_partition.first_extent_index.get() as usize; + let num_extents = raw_partition.num_extents.get() as usize; + + let mut partition = Partition { + name: raw_partition.name.as_str()?.to_owned(), + attributes: PartitionAttributes::from_bits_retain(raw_partition.attributes.get()), + extents: Vec::with_capacity(num_extents), + }; + + for raw_extent in &raw_slot.extents[first_extent..][..num_extents] { + let extent = Extent { + num_sectors: raw_extent.num_sectors.get(), + extent_type: match raw_extent.target_type.get() { + RawExtent::TARGET_TYPE_LINEAR => ExtentType::Linear { + start_sector: raw_extent.target_data.get(), + block_device_index: raw_extent.target_source.get() as usize, + }, + RawExtent::TARGET_TYPE_ZERO => ExtentType::Zero, + _ => unreachable!(), + }, + }; + + partition.extents.push(extent); + } + + slot.groups[group_index].partitions.push(partition); + } + + for raw_block_device in &raw_slot.block_devices { + let block_device = BlockDevice { + first_logical_sector: raw_block_device.first_logical_sector.get(), + alignment: raw_block_device.alignment.get(), + alignment_offset: raw_block_device.alignment_offset.get(), + size: raw_block_device.size.get(), + partition_name: raw_block_device.partition_name.as_str()?.to_owned(), + flags: BlockDeviceFlags::from_bits_retain(raw_block_device.flags.get()), + }; + + slot.block_devices.push(block_device); + } + + Ok(slot) + } +} + +impl TryFrom for MetadataSlot { + type Error = Error; + + fn try_from(raw_slot: RawMetadataSlot) -> Result { + (&raw_slot).try_into() + } +} + +impl TryFrom<&MetadataSlot> for RawMetadataSlot { + type Error = Error; + + fn try_from(slot: &MetadataSlot) -> Result { + let header_size = RawHeader::size_for_version(slot.major_version, slot.minor_version); + + let mut raw_slot = RawMetadataSlot { + header: RawHeader { + magic: HEADER_MAGIC.into(), + major_version: slot.major_version.into(), + minor_version: slot.minor_version.into(), + header_size: (header_size as u32).into(), + header_checksum: Default::default(), + tables_size: 0.into(), + tables_checksum: Default::default(), + partitions: RawTableDescriptor::new_zeroed(), + extents: RawTableDescriptor::new_zeroed(), + groups: RawTableDescriptor::new_zeroed(), + block_devices: RawTableDescriptor::new_zeroed(), + flags: slot.flags.bits().into(), + reserved: [0u8; 124], + }, + partitions: Vec::new(), + extents: Vec::new(), + groups: Vec::with_capacity(slot.groups.len()), + block_devices: Vec::with_capacity(slot.block_devices.len()), + }; + + for group in &slot.groups { + let raw_group = RawPartitionGroup { + name: group.name.parse()?, + flags: group.flags.bits().into(), + maximum_size: group + .maximum_size + .map(|s| s.get()) + .unwrap_or_default() + .into(), + }; + + let group_index: u32 = + raw_slot.groups.len().try_into().map_err(|_| { + Error::PartitionGroup(group.name.clone(), "Index too large".into()) + })?; + + for partition in &group.partitions { + let extent_index: u32 = raw_slot.extents.len().try_into().map_err(|_| { + Error::Partition(partition.name.clone(), "Extent index too large".into()) + })?; + let num_extents: u32 = partition.extents.len().try_into().map_err(|_| { + Error::Partition(partition.name.clone(), "Too many extents".into()) + })?; + + let raw_partition = RawPartition { + name: partition.name.parse()?, + attributes: partition.attributes.bits().into(), + first_extent_index: extent_index.into(), + num_extents: num_extents.into(), + group_index: group_index.into(), + }; + + for extent in &partition.extents { + let (target_type, target_data, target_source) = match extent.extent_type { + ExtentType::Linear { + start_sector, + block_device_index, + } => { + let block_device_index: u32 = + block_device_index.try_into().map_err(|_| { + Error::Extent( + raw_slot.extents.len(), + "Block device index too large".into(), + ) + })?; + + ( + RawExtent::TARGET_TYPE_LINEAR, + start_sector, + block_device_index, + ) + } + ExtentType::Zero => (RawExtent::TARGET_TYPE_ZERO, 0, 0), + }; + + let raw_extent = RawExtent { + num_sectors: extent.num_sectors.into(), + target_type: target_type.into(), + target_data: target_data.into(), + target_source: target_source.into(), + }; + + raw_slot.extents.push(raw_extent); + } + + raw_slot.partitions.push(raw_partition); + } + + raw_slot.groups.push(raw_group); + } + + for block_device in &slot.block_devices { + let raw_block_device = RawBlockDevice { + first_logical_sector: block_device.first_logical_sector.into(), + alignment: block_device.alignment.into(), + alignment_offset: block_device.alignment_offset.into(), + size: block_device.size.into(), + partition_name: block_device.partition_name.parse()?, + flags: block_device.flags.bits().into(), + }; + + raw_slot.block_devices.push(raw_block_device); + } + + let mut offset = 0u32; + + offset = raw_slot + .header + .partitions + .update(&raw_slot.partitions, offset)?; + offset = raw_slot.header.extents.update(&raw_slot.extents, offset)?; + offset = raw_slot.header.groups.update(&raw_slot.groups, offset)?; + offset = raw_slot + .header + .block_devices + .update(&raw_slot.block_devices, offset)?; + + raw_slot.header.tables_size = offset.into(); + + let tables_digest = { + let mut context = ring::digest::Context::new(&ring::digest::SHA256); + context.update(raw_slot.partitions.as_bytes()); + context.update(raw_slot.extents.as_bytes()); + context.update(raw_slot.groups.as_bytes()); + context.update(raw_slot.block_devices.as_bytes()); + context.finish() + }; + raw_slot + .header + .tables_checksum + .copy_from_slice(tables_digest.as_ref()); + + let header_digest = ring::digest::digest( + &ring::digest::SHA256, + &raw_slot.header.as_bytes()[..header_size], + ); + raw_slot + .header + .header_checksum + .copy_from_slice(header_digest.as_ref()); + + Ok(raw_slot) + } +} + +impl TryFrom for RawMetadataSlot { + type Error = Error; + + fn try_from(slot: MetadataSlot) -> Result { + (&slot).try_into() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct Metadata { + /// Image type. + pub image_type: ImageType, + /// Maximum size of a single copy of the metadata (header + tables). This + /// must be a multiple of [`SECTOR_SIZE`]. + pub metadata_max_size: u32, + /// Number of metadata slots. + pub metadata_slot_count: u32, + /// Block device block size for the logical partitions. This must be a + /// multiple of [`SECTOR_SIZE`]. + pub logical_block_size: u32, + /// List of metadata slots. There must only be 1 actual slot when + /// [`Self::image_type`] is [`ImageType::Empty`], regardless of the value of + /// [`Self::metadata_slot_count`]. + pub slots: Vec, +} + +impl TryFrom<&RawMetadata> for Metadata { + type Error = Error; + + fn try_from(raw_metadata: &RawMetadata) -> Result { + let mut metadata = Self { + image_type: raw_metadata.image_type, + metadata_max_size: raw_metadata.geometry.metadata_max_size.get(), + metadata_slot_count: raw_metadata.geometry.metadata_slot_count.get(), + logical_block_size: raw_metadata.geometry.logical_block_size.get(), + slots: Vec::with_capacity(raw_metadata.slots.len()), + }; + + for raw_slot in &raw_metadata.slots { + let slot: MetadataSlot = raw_slot.try_into()?; + + metadata.slots.push(slot); + } + + Ok(metadata) + } +} + +impl TryFrom for Metadata { + type Error = Error; + + fn try_from(raw_metadata: RawMetadata) -> Result { + (&raw_metadata).try_into() + } +} + +impl TryFrom<&Metadata> for RawMetadata { + type Error = Error; + + fn try_from(metadata: &Metadata) -> Result { + // We only do the bare minimum calculations needed here to fill out the + // raw fields. There is no semantic validation. + + let mut raw_metadata = RawMetadata { + image_type: metadata.image_type, + geometry: RawGeometry { + magic: GEOMETRY_MAGIC.into(), + struct_size: (mem::size_of::() as u32).into(), + checksum: Default::default(), + metadata_max_size: metadata.metadata_max_size.into(), + metadata_slot_count: metadata.metadata_slot_count.into(), + logical_block_size: metadata.logical_block_size.into(), + }, + slots: Vec::with_capacity(metadata.slots.len()), + }; + + let geometry_digest = + ring::digest::digest(&ring::digest::SHA256, raw_metadata.geometry.as_bytes()); + raw_metadata + .geometry + .checksum + .copy_from_slice(geometry_digest.as_ref()); + + for slot in &metadata.slots { + let raw_slot: RawMetadataSlot = slot.try_into()?; + + raw_metadata.slots.push(raw_slot); + } + + Ok(raw_metadata) + } +} + +impl TryFrom for RawMetadata { + type Error = Error; + + fn try_from(metadata: Metadata) -> Result { + (&metadata).try_into() + } +} + +impl FromReader for Metadata { + type Error = Error; + + fn from_reader(reader: R) -> Result { + RawMetadata::from_reader(reader)?.try_into() + } +} + +impl ToWriter for Metadata { + type Error = Error; + + fn to_writer(&self, writer: W) -> Result<()> { + let raw_metadata: RawMetadata = self.try_into()?; + raw_metadata.validate()?; + raw_metadata.to_writer(writer) + } +} diff --git a/avbroot/src/format/mod.rs b/avbroot/src/format/mod.rs index 177b8fe..af6ffaa 100644 --- a/avbroot/src/format/mod.rs +++ b/avbroot/src/format/mod.rs @@ -9,6 +9,7 @@ pub mod compression; pub mod cpio; pub mod fec; pub mod hashtree; +pub mod lp; pub mod ota; pub mod padding; pub mod payload; diff --git a/avbroot/tests/lp.rs b/avbroot/tests/lp.rs new file mode 100644 index 0000000..b9696b1 --- /dev/null +++ b/avbroot/tests/lp.rs @@ -0,0 +1,428 @@ +/* + * SPDX-FileCopyrightText: 2024 Andrew Gunnerson + * SPDX-License-Identifier: GPL-3.0-only + */ + +use std::{io::Cursor, num::NonZeroU64}; + +use avbroot::{ + format::lp::{ + BlockDevice, BlockDeviceFlags, Extent, ExtentType, HeaderFlags, ImageType, Metadata, + MetadataSlot, Partition, PartitionAttributes, PartitionGroup, PartitionGroupFlags, + }, + stream::{FromReader, ToWriter}, +}; + +fn round_trip(metadata: &Metadata, sha512: &[u8; 64]) { + let mut writer = Cursor::new(Vec::new()); + metadata.to_writer(&mut writer).unwrap(); + let data = writer.into_inner(); + + assert_eq!( + ring::digest::digest(&ring::digest::SHA512, &data).as_ref(), + sha512, + ); + + let mut reader = Cursor::new(&data); + let new_metadata = Metadata::from_reader(&mut reader).unwrap(); + + assert_eq!(&new_metadata, metadata); +} + +#[test] +fn round_trip_empty_image() { + // Layout from Google Pixel 9 Pro XL stock factory image: + // komodo-ad1a.240530.047-factory-bb04e484.zip -> super_empty.img + let metadata = Metadata { + image_type: ImageType::Empty, + metadata_max_size: 65536, + metadata_slot_count: 3, + logical_block_size: 4096, + slots: vec![MetadataSlot { + major_version: 10, + minor_version: 2, + groups: vec![ + PartitionGroup { + name: "default".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: None, + partitions: vec![], + }, + PartitionGroup { + name: "google_dynamic_partitions_a".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: NonZeroU64::new(8527020032), + partitions: vec![ + Partition { + name: "system_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_dlkm_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_ext_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "product_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_dlkm_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + ], + }, + PartitionGroup { + name: "google_dynamic_partitions_b".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: NonZeroU64::new(8527020032), + partitions: vec![ + Partition { + name: "system_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_dlkm_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_ext_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "product_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_dlkm_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + ], + }, + ], + block_devices: vec![BlockDevice { + first_logical_sector: 2048, + alignment: 1048576, + alignment_offset: 0, + size: 8531214336, + partition_name: "super".into(), + flags: BlockDeviceFlags::empty(), + }], + flags: HeaderFlags::VIRTUAL_AB_DEVICE, + }], + }; + // This is semantically equivalent, but not identical. The Metadata data + // structure only retains the order of partitions within a group, but not + // globally. This checksum is meant to protect against unintended future + // changes. + let sha512 = [ + 0xfa, 0xdf, 0xf2, 0xb6, 0x74, 0xec, 0x78, 0x7d, 0x0f, 0x7d, 0x17, 0x54, 0xcf, 0x1b, 0x53, + 0x13, 0x66, 0x13, 0x5e, 0x8e, 0xcc, 0x84, 0xa2, 0x63, 0xaf, 0x0d, 0x68, 0x96, 0xc6, 0x40, + 0x4e, 0x83, 0xe4, 0xe9, 0xef, 0x61, 0xdc, 0x2a, 0x25, 0x5f, 0xa2, 0x7d, 0x29, 0x0b, 0xb6, + 0x26, 0x93, 0x59, 0xc9, 0xa8, 0x56, 0x3b, 0x3d, 0x3d, 0x15, 0x6b, 0xee, 0x78, 0x56, 0x78, + 0xa1, 0x83, 0x6d, 0x70, + ]; + + round_trip(&metadata, &sha512); +} + +#[test] +fn round_trip_normal_image() { + // Layout from Google Pixel 9 Pro XL GrapheneOS factory image: + // komodo-install-2024082500.zip -> super_1.img + let slot = MetadataSlot { + major_version: 10, + minor_version: 2, + groups: vec![ + PartitionGroup { + name: "default".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: None, + partitions: vec![], + }, + PartitionGroup { + name: "google_dynamic_partitions_a".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: NonZeroU64::new(8527020032), + partitions: vec![ + Partition { + name: "system_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 2465952, + extent_type: ExtentType::Linear { + start_sector: 2048, + block_device_index: 0, + }, + }], + }, + Partition { + name: "system_dlkm_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 23720, + extent_type: ExtentType::Linear { + start_sector: 2469888, + block_device_index: 0, + }, + }], + }, + Partition { + name: "system_ext_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 786144, + extent_type: ExtentType::Linear { + start_sector: 2494464, + block_device_index: 0, + }, + }], + }, + Partition { + name: "product_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 1396432, + extent_type: ExtentType::Linear { + start_sector: 3280896, + block_device_index: 0, + }, + }], + }, + Partition { + name: "vendor_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 1959024, + extent_type: ExtentType::Linear { + start_sector: 4677632, + block_device_index: 0, + }, + }], + }, + Partition { + name: "vendor_dlkm_a".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![Extent { + num_sectors: 55008, + extent_type: ExtentType::Linear { + start_sector: 6637568, + block_device_index: 0, + }, + }], + }, + ], + }, + PartitionGroup { + name: "google_dynamic_partitions_b".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: NonZeroU64::new(8527020032), + partitions: vec![ + Partition { + name: "system_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_dlkm_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "system_ext_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "product_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + Partition { + name: "vendor_dlkm_b".into(), + attributes: PartitionAttributes::READONLY, + extents: vec![], + }, + ], + }, + ], + block_devices: vec![BlockDevice { + first_logical_sector: 2048, + alignment: 1048576, + alignment_offset: 0, + size: 8531214336, + partition_name: "super".into(), + flags: BlockDeviceFlags::empty(), + }], + flags: HeaderFlags::VIRTUAL_AB_DEVICE, + }; + let metadata = Metadata { + image_type: ImageType::Normal, + metadata_max_size: 65536, + metadata_slot_count: 3, + logical_block_size: 4096, + slots: vec![slot; 3], + }; + // This is semantically equivalent, but not identical. The Metadata data + // structure only retains the order of partitions within a group, but not + // globally. This checksum is meant to protect against unintended future + // changes. + let sha512 = [ + 0x3b, 0xad, 0xd4, 0x22, 0xa1, 0x5a, 0xc5, 0xdf, 0x72, 0x7d, 0x92, 0x35, 0x04, 0x8a, 0x75, + 0xd9, 0x33, 0x0d, 0xaa, 0x9e, 0x97, 0xd4, 0x13, 0x28, 0x5e, 0x0f, 0x12, 0x0c, 0xf2, 0xb3, + 0xdc, 0x35, 0x89, 0x65, 0x40, 0xb0, 0x67, 0xb1, 0x54, 0x09, 0x52, 0x3e, 0x78, 0x3d, 0x3f, + 0xa7, 0xf7, 0xa0, 0x77, 0xa8, 0xfc, 0xb7, 0x93, 0x19, 0xcd, 0x43, 0xea, 0x9a, 0x74, 0x65, + 0x54, 0x3c, 0xaa, 0x12, + ]; + + round_trip(&metadata, &sha512); +} + +#[test] +fn round_trip_retrofit_image() { + // Layout from Google Pixel 3a XL stock factory image: + // bonito-ota-sp2a.220505.008-37a410d5.zip -> system.img + let slot = MetadataSlot { + major_version: 10, + minor_version: 0, + groups: vec![ + PartitionGroup { + name: "default".into(), + flags: PartitionGroupFlags::empty(), + maximum_size: None, + partitions: vec![], + }, + PartitionGroup { + name: "google_dynamic_partitions".into(), + flags: PartitionGroupFlags::SLOT_SUFFIXED, + maximum_size: NonZeroU64::new(4068474880), + partitions: vec![ + Partition { + name: "system".into(), + attributes: PartitionAttributes::READONLY + | PartitionAttributes::SLOT_SUFFIXED, + extents: vec![Extent { + num_sectors: 1757416, + extent_type: ExtentType::Linear { + start_sector: 2048, + block_device_index: 0, + }, + }], + }, + Partition { + name: "vendor".into(), + attributes: PartitionAttributes::READONLY + | PartitionAttributes::SLOT_SUFFIXED, + extents: vec![Extent { + num_sectors: 991848, + extent_type: ExtentType::Linear { + start_sector: 1761280, + block_device_index: 0, + }, + }], + }, + Partition { + name: "product".into(), + attributes: PartitionAttributes::READONLY + | PartitionAttributes::SLOT_SUFFIXED, + extents: vec![ + Extent { + num_sectors: 3627008, + extent_type: ExtentType::Linear { + start_sector: 2754560, + block_device_index: 0, + }, + }, + Extent { + num_sectors: 538240, + extent_type: ExtentType::Linear { + start_sector: 2048, + block_device_index: 1, + }, + }, + ], + }, + Partition { + name: "system_ext".into(), + attributes: PartitionAttributes::READONLY + | PartitionAttributes::SLOT_SUFFIXED, + extents: vec![Extent { + num_sectors: 490744, + extent_type: ExtentType::Linear { + start_sector: 540672, + block_device_index: 1, + }, + }], + }, + ], + }, + ], + block_devices: vec![ + BlockDevice { + first_logical_sector: 2048, + alignment: 1048576, + alignment_offset: 0, + size: 3267362816, + partition_name: "system".into(), + flags: BlockDeviceFlags::SLOT_SUFFIXED, + }, + BlockDevice { + first_logical_sector: 2048, + alignment: 1048576, + alignment_offset: 0, + size: 805306368, + partition_name: "vendor".into(), + flags: BlockDeviceFlags::SLOT_SUFFIXED, + }, + ], + flags: HeaderFlags::empty(), + }; + let metadata = Metadata { + image_type: ImageType::Normal, + metadata_max_size: 65536, + metadata_slot_count: 2, + logical_block_size: 4096, + slots: vec![slot; 2], + }; + // First 274432 bytes of system.img. Unlike the other test cases, this is + // identical to the original image because there is only one partition group + // with partitions, so the group-level ordering is the same as the global + // ordering. + let sha512 = [ + 0xb9, 0x97, 0xf5, 0x83, 0x39, 0x37, 0x90, 0x0a, 0xb6, 0x46, 0xdd, 0x27, 0x57, 0xf1, 0xf3, + 0xbd, 0x8f, 0xc4, 0x63, 0x07, 0x6f, 0xf4, 0x19, 0xc0, 0x02, 0x28, 0x48, 0x99, 0x54, 0xbb, + 0xb3, 0xbf, 0x67, 0x95, 0xc4, 0xa7, 0x99, 0xf4, 0xa9, 0xc4, 0xf4, 0x1d, 0xf7, 0x59, 0x28, + 0xeb, 0xbc, 0x85, 0x46, 0xd1, 0x7d, 0x65, 0x0f, 0xbe, 0x21, 0xf6, 0xf2, 0xa2, 0x20, 0x5b, + 0xda, 0xde, 0xfa, 0x50, + ]; + + round_trip(&metadata, &sha512); +} diff --git a/fuzz/src/bin/lp.rs b/fuzz/src/bin/lp.rs new file mode 100644 index 0000000..e237bac --- /dev/null +++ b/fuzz/src/bin/lp.rs @@ -0,0 +1,21 @@ +#[cfg(not(windows))] +mod fuzz { + use std::io::Cursor; + + use avbroot::{format::lp::Metadata, stream::FromReader}; + use honggfuzz::fuzz; + + pub fn main() { + loop { + fuzz!(|data: &[u8]| { + let reader = Cursor::new(data); + let _ = Metadata::from_reader(reader); + }); + } + } +} + +fn main() { + #[cfg(not(windows))] + fuzz::main(); +}