blob: 76d32ac39b21226d653f49a0a842967573a86a19 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
anyhow::{anyhow, Error},
core::mem::size_of,
fidl_fuchsia_boot as fboot,
fuchsia_zbi::{ZbiParser, ZbiParserError, ZbiResult, ZbiType::BootloaderFile},
fuchsia_zircon as zx,
futures::prelude::*,
std::{collections::HashMap, str::from_utf8, sync::Arc},
};
pub struct Items {
zbi_parser: ZbiParser,
bootloader_files: HashMap<String, Vec<u8>>,
}
impl Items {
pub fn new(mut zbi_parser: ZbiParser) -> Result<Arc<Self>, Error> {
// Bootloader files, if they are present in the ZBI, have special layout aware processing
// where this service needs to parse their payload to extract the filename which is the
// key. All other items are just stored unprocessed.
let bootloader_files = match zbi_parser.try_get_item(BootloaderFile.into_raw(), None) {
Ok(result) => {
zbi_parser.release_item(BootloaderFile)?;
Items::parse_bootloader_items(result)?
}
Err(_) => HashMap::new(),
};
Ok(Arc::new(Items { zbi_parser, bootloader_files }))
}
pub fn parse_bootloader_items(
items: Vec<ZbiResult>,
) -> Result<HashMap<String, Vec<u8>>, Error> {
let mut bootloader_result = HashMap::new();
for item in items {
// The layout of a bootloader file has the following format:
// | size of name | name | payload |
// 0 1 size of name length of item
let length = item.bytes.len();
if length < size_of::<u8>() {
return Err(anyhow!(
"Bootloader ZBI item is too small to contain the size of the name"
));
}
// The bootloader items have been split into multiple byte vectors, so this offset
// is into an individual bootloader item starting at index 0.
let mut offset = 0;
let name_length = item.bytes[offset].try_into()?;
offset += size_of::<u8>();
if length < (offset + name_length) {
return Err(anyhow!(
"Bootloader ZBI item is too small to contain the reported length of the name"
));
}
let name = from_utf8(&item.bytes[offset..(offset + name_length)])?.to_owned();
offset = offset
.checked_add(name_length)
.ok_or(anyhow!("Overflow when parsing bootloader ZBI item"))?;
if bootloader_result.contains_key(&name) {
return Err(anyhow!("Bootloader items in ZBI have duplicate filenames: {}", name));
}
bootloader_result.insert(name, item.bytes[offset..].to_vec());
}
Ok(bootloader_result)
}
pub async fn serve(
self: Arc<Self>,
mut stream: fboot::ItemsRequestStream,
) -> Result<(), Error> {
while let Some(request) = stream.try_next().await? {
match request {
fboot::ItemsRequest::Get { type_, extra, responder } => {
match self.zbi_parser.try_get_last_matching_item(type_, extra) {
Ok(result) => {
let vmo = zx::Vmo::create(result.bytes.len().try_into()?)?;
vmo.write(&result.bytes, 0)?;
responder.send(Some(vmo), result.bytes.len().try_into()?)?
}
Err(_) => responder.send(None, 0)?,
}
}
fboot::ItemsRequest::Get2 { type_, extra, responder } => {
let extra = if let Some(extra) = extra { Some((*extra).n) } else { None };
let item_vec = match self.zbi_parser.try_get_item(type_, extra) {
Ok(vec) => vec
.iter()
.map(|result| -> Result<fboot::RetrievedItems, Error> {
let vmo = zx::Vmo::create(result.bytes.len().try_into()?)?;
vmo.write(&result.bytes, 0)?;
Ok(fboot::RetrievedItems {
payload: vmo,
length: result.bytes.len().try_into()?,
extra: result.extra,
})
})
.collect::<Result<Vec<fboot::RetrievedItems>, Error>>()
.map_err(|_| zx::Status::INTERNAL.into_raw()),
Err(err) => {
match err {
ZbiParserError::ItemNotStored { .. } => {
Err(zx::Status::NOT_SUPPORTED.into_raw())
}
_ => {
// Errors such as item not found are not unexpected, and so not
// propagated to the client.
Ok(vec![])
}
}
}
};
responder.send(item_vec)?
}
fboot::ItemsRequest::GetBootloaderFile { filename, responder } => {
match self.bootloader_files.get(&filename) {
Some(bytes) => {
let vmo = zx::Vmo::create(bytes.len().try_into()?)?;
vmo.write(&bytes, 0)?;
responder.send(Some(vmo))?
}
None => responder.send(None)?,
}
}
};
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fuchsia_async as fasync,
fuchsia_zbi::{
zbi_header_t, ZbiType, ZBI_CONTAINER_MAGIC, ZBI_FLAGS_VERSION, ZBI_ITEM_MAGIC,
ZBI_ITEM_NO_CRC32,
},
zerocopy::{byteorder::little_endian::U32, AsBytes},
};
const ZBI_HEADER_SIZE: usize = size_of::<zbi_header_t>();
fn serve_items(zbi_parser: ZbiParser) -> Result<fboot::ItemsProxy, Error> {
let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<fboot::ItemsMarker>()?;
fasync::Task::local(
Items::new(zbi_parser)?
.serve(stream)
.unwrap_or_else(|e| panic!("Error while serving items service: {}", e)),
)
.detach();
Ok(proxy)
}
struct ZbiBuilder {
zbi_bytes: Vec<u8>,
}
impl ZbiBuilder {
fn get_bootloader_item(name: &[u8], payload: &[u8]) -> Vec<u8> {
// The first byte of the bootloader item is the length of the bootloader item name
// in bytes, and following the name is the actual payload. All three sections
// together sum to the length reported by the header.
let mut result = vec![name.len().try_into().unwrap()];
result.extend(name);
result.extend(payload);
result
}
fn simple_header(zbi_type: ZbiType, extra: u32, length: u32) -> zbi_header_t {
zbi_header_t {
zbi_type: U32::new(zbi_type as u32),
length: U32::new(length),
extra: U32::new(extra),
flags: U32::new(ZBI_FLAGS_VERSION),
reserved_0: U32::new(0),
reserved_1: U32::new(0),
magic: U32::new(ZBI_ITEM_MAGIC),
crc32: U32::new(ZBI_ITEM_NO_CRC32),
}
}
fn add_item(mut self, data: &[u8]) -> Self {
self.zbi_bytes.extend(data);
let padding_amount = ZbiParser::align_zbi_item(data.len().try_into().unwrap()).unwrap()
as usize
- data.len();
if padding_amount > 0 {
let padding = vec![0u8; padding_amount];
self.zbi_bytes.extend(padding);
}
self
}
fn add_header(mut self, zbi_type: ZbiType, extra: u32, length: u32) -> Self {
self.zbi_bytes.extend(ZbiBuilder::simple_header(zbi_type, extra, length).as_bytes());
self
}
fn new() -> Self {
Self {
zbi_bytes: ZbiBuilder::simple_header(ZbiType::Container, ZBI_CONTAINER_MAGIC, 0)
.as_bytes()
.to_vec(),
}
}
fn calculate_item_length(mut self) -> Self {
let item_length =
U32::new(u32::try_from(self.zbi_bytes.len() - ZBI_HEADER_SIZE).unwrap());
let item_length_bytes = item_length.as_bytes();
let mut i = 4usize;
for x in item_length_bytes {
self.zbi_bytes[i] = *x;
i += 1;
}
self
}
fn generate(self) -> Result<zx::Vmo, Error> {
let vmo = zx::Vmo::create(self.zbi_bytes.len().try_into()?)?;
vmo.write(&self.zbi_bytes, 0)?;
Ok(vmo)
}
}
#[fuchsia::test]
async fn get2_untracked_item_not_found() {
let zbi_type = ZbiType::Crashlog;
let item = b"abcd";
let zbi = ZbiBuilder::new()
.add_header(zbi_type, 0, item.len().try_into().unwrap())
.add_item(item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi)
.set_store_item(ZbiType::Cmdline)
.parse()
.expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
// Crashlog existed in the ZBI, but the parser configuration is set to only store Cmdline
// items.
let result = item_service
.get2(zbi_type.into_raw(), None)
.await
.expect("failed to query item service");
assert_eq!(zx::Status::from_raw(result.unwrap_err()), zx::Status::NOT_SUPPORTED);
}
#[fuchsia::test]
async fn get2_multiple_items_same_type_different_extras() {
let zbi_type = ZbiType::Crashlog;
let item1 = b"abcd";
let extra1 = 123;
let item2 = b"efgh";
let extra2 = 456;
let zbi = ZbiBuilder::new()
.add_header(zbi_type, extra1, item1.len().try_into().unwrap())
.add_item(item1)
.add_header(zbi_type, extra2, item2.len().try_into().unwrap())
.add_item(item2)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
// Get both items by not specifying the extra.
let result = item_service
.get2(zbi_type.into_raw(), None)
.await
.expect("failed to query item service")
.expect("failed to retrieve items");
assert_eq!(result.len(), 2);
let retrieved =
result.iter().find(|item| item.extra == extra1).expect("failed to find item");
let mut bytes = vec![0; retrieved.length as usize];
retrieved.payload.read(&mut bytes, 0).expect("failed to read bytes");
assert_eq!(bytes, item1);
assert_eq!(retrieved.length, item1.len() as u32);
let retrieved =
result.iter().find(|item| item.extra == extra2).expect("failed to find item");
let mut bytes = vec![0; retrieved.length as usize];
retrieved.payload.read(&mut bytes, 0).expect("failed to read bytes");
assert_eq!(bytes, item2);
assert_eq!(retrieved.length, item2.len() as u32);
// Get a single item by specifying the extra.
let result = item_service
.get2(zbi_type.into_raw(), Some(&fidl_fuchsia_boot::Extra { n: extra2 }))
.await
.expect("failed to query item service")
.expect("failed to retrieve items");
assert!(result.iter().find(|item| item.extra == extra2).is_some());
assert!(result.iter().find(|item| item.extra == extra1).is_none());
}
#[fuchsia::test]
async fn item_not_found_no_matching_extra() {
// The ZBI contains the correct item, but without a matching extra.
let actual_extra = 54321;
let queried_extra = 12345;
let zbi_type = ZbiType::Crashlog;
let item = b"abcd";
let zbi = ZbiBuilder::new()
.add_header(zbi_type, actual_extra, item.len().try_into().unwrap())
.add_item(item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
let (vmo, length) = item_service
.get(zbi_type as u32, queried_extra)
.await
.expect("failed to query item service");
assert!(vmo.is_none());
assert_eq!(length, 0);
}
#[fuchsia::test]
async fn item_not_found_no_matching_zbi_type() {
// The ZBI contains a different item with the matching extra, but no item with both
// fields matching.
let extra = 54321;
let actual_zbi_type = ZbiType::Crashlog;
let queried_zbi_type = ZbiType::KernelDriver;
let item = b"abcd";
let zbi = ZbiBuilder::new()
.add_header(actual_zbi_type, extra, item.len().try_into().unwrap())
.add_item(item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
let (vmo, length) = item_service
.get(queried_zbi_type as u32, extra)
.await
.expect("failed to query item service");
assert!(vmo.is_none());
assert_eq!(length, 0);
}
#[fuchsia::test]
async fn get_non_bootloader_item() {
// Both ZBI type and extra match, returning the item parsed from the ZBI.
let zbi_type = ZbiType::Crashlog;
let extra = 12345;
let item = b"abcd";
let zbi = ZbiBuilder::new()
.add_header(zbi_type, extra, item.len().try_into().unwrap())
.add_item(item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
let (vmo, length) =
item_service.get(zbi_type as u32, extra).await.expect("failed to query item service");
assert!(vmo.is_some());
let mut bytes = vec![0; item.len()];
vmo.unwrap().read(&mut bytes, 0).expect("failed to read bytes");
assert_eq!(bytes, b"abcd");
assert_eq!(length, item.len() as u32);
}
#[fuchsia::test]
async fn badly_formatted_bootloader_file() {
// Take a correct bootloader payload, and cut it in half. The first byte reporting the
// length of the name is now incorrect, making this an invalid entry.
let mut item = ZbiBuilder::get_bootloader_item(b"bootloader_name", b"");
item.resize(item.len() / 2, 0);
let zbi = ZbiBuilder::new()
.add_header(BootloaderFile, 0, item.len().try_into().unwrap())
.add_item(&item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser);
assert!(item_service.is_err());
}
#[fuchsia::test]
async fn duplicate_bootloader_files() {
let item1 = ZbiBuilder::get_bootloader_item(b"file", b"abcd");
let item2 = ZbiBuilder::get_bootloader_item(b"file", b"efgh");
let zbi = ZbiBuilder::new()
.add_header(BootloaderFile, 0, item1.len().try_into().unwrap())
.add_item(&item1)
.add_header(BootloaderFile, 0, item2.len().try_into().unwrap())
.add_item(&item2)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser);
assert!(item_service.is_err());
}
#[fuchsia::test]
async fn no_matching_bootloader_file() {
let actual_name = b"this_is_a_name";
let queried_name = "this_is_NOT_a_name";
let item = ZbiBuilder::get_bootloader_item(actual_name, b"this_is_a_payload");
let zbi = ZbiBuilder::new()
.add_header(BootloaderFile, 0, item.len().try_into().unwrap())
.add_item(&item)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
let vmo = item_service
.get_bootloader_file(queried_name)
.await
.expect("failed to query item service");
assert!(vmo.is_none());
}
#[fuchsia::test]
async fn get_bootloader_files() {
let name1 = b"this_is_a_name";
let payload1 = b"this_is_a_payload";
let item1 = ZbiBuilder::get_bootloader_item(name1, payload1);
let name2 = b"this_is_another_name";
let payload2 = b"this_is_another_payload";
let item2 = ZbiBuilder::get_bootloader_item(name2, payload2);
let zbi = ZbiBuilder::new()
.add_header(BootloaderFile, 0, item1.len().try_into().unwrap())
.add_item(&item1)
.add_header(BootloaderFile, 0, item2.len().try_into().unwrap())
.add_item(&item2)
.calculate_item_length()
.generate()
.expect("failed to create ZBI");
let parser = ZbiParser::new(zbi).parse().expect("failed to parse ZBI");
let item_service = serve_items(parser).expect("failed to serve items");
let response = item_service
.get_bootloader_file(from_utf8(name1).unwrap())
.await
.expect("failed to query item service");
assert!(response.is_some());
let vmo = response.unwrap();
let length = vmo.get_content_size().unwrap().try_into().unwrap();
let mut bytes = vec![0; length];
vmo.read(&mut bytes, 0).expect("failed to read bytes");
assert_eq!(bytes, payload1);
assert_eq!(length, payload1.len());
let response = item_service
.get_bootloader_file(from_utf8(name2).unwrap())
.await
.expect("failed to query item service");
assert!(response.is_some());
let vmo = response.unwrap();
let length = vmo.get_content_size().unwrap().try_into().unwrap();
let mut bytes = vec![0; length];
vmo.read(&mut bytes, 0).expect("failed to read bytes");
assert_eq!(bytes, payload2);
assert_eq!(length, payload2.len());
}
}