//! Minimal FDT v17 reader for direct children of QEMU's ARM64 `virt` root. //! It is deliberately not a general bus-address translator. const MAGIC: u32 = 0xd00d_feed; const HEADER_LEN: usize = 40; pub const MAX_SIZE: usize = 2 * 1024 * 1024; #[derive(Debug, PartialEq, Eq)] pub enum Error { Invalid, Unsupported, MissingDevice, } #[derive(Debug, PartialEq, Eq)] pub struct Hardware { pub uart_base: u64, pub ram_base: u64, pub ram_size: u64, } #[derive(Debug, PartialEq, Eq)] pub struct InterruptHardware { pub gic_distributor: u64, pub gic_redistributor: u64, pub nonsecure_phys_timer_ppi: u32, } pub const MAX_VIRTIO_MMIO_NODES: usize = 32; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PciEcamNode { pub base: u64, pub size: u64, pub first_bus: u8, pub last_bus: u8, /// CPU physical start and exclusive end of one identity-translated, /// non-prefetchable PCI memory aperture, when the host supplies it. pub memory_aperture: Option<(u64, u64)>, /// Absolute GIC SPI IDs for INTA–INTD on root slot 3, if described. pub slot3_intx: Option<[u32; 4]>, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct VirtioMmioNode { pub base: u64, pub size: u64, } #[derive(Debug, PartialEq, Eq)] pub struct VirtioMmioNodes { entries: [VirtioMmioNode; MAX_VIRTIO_MMIO_NODES], len: usize, } impl VirtioMmioNodes { pub fn as_slice(&self) -> &[VirtioMmioNode] { &self.entries[..self.len] } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct VirtioPageWindow { pub start: u64, pub end: u64, pub pages: usize, } /// K322: a 0x200-byte QEMU virtio slot is a sub-page range, not itself 4 KiB /// aligned. Validate its containing page before a later MMU mapping may use it. pub fn virtio_page_window( nodes: &VirtioMmioNodes, ram_base: u64, ram_size: u64, ) -> Result { const PAGE: u64 = 0x1000; const SLOT: u64 = 0x200; let ram_end = ram_base.checked_add(ram_size).ok_or(Error::Invalid)?; if ram_size == 0 || nodes.as_slice().is_empty() { return Err(Error::Invalid); } let mut first = u64::MAX; let mut last = 0u64; for (index, node) in nodes.as_slice().iter().enumerate() { let end = node.base.checked_add(node.size).ok_or(Error::Invalid)?; let page = node.base & !(PAGE - 1); let page_end = page.checked_add(PAGE).ok_or(Error::Invalid)?; if node.size != SLOT || node.base & (SLOT - 1) != 0 || end > page_end || page < ram_end && ram_base < page_end { return Err(Error::Invalid); } for prior in &nodes.as_slice()[..index] { let prior_end = prior.base.checked_add(prior.size).ok_or(Error::Invalid)?; if node.base < prior_end && prior.base < end { return Err(Error::Invalid); } } first = first.min(page); last = last.max(page_end); } let bytes = last.checked_sub(first).ok_or(Error::Invalid)?; let pages = usize::try_from(bytes / PAGE).map_err(|_| Error::Unsupported)?; // A sparse or multiply aliased range is not a valid contiguous aperture. if pages != 4 || nodes.as_slice().len() != 32 || bytes != nodes.as_slice().len() as u64 * SLOT { return Err(Error::Invalid); } Ok(VirtioPageWindow { start: first, end: last, pages, }) } pub const MAX_RESERVATIONS: usize = 32; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Reservation { pub start: u64, pub size: u64, /// Only `/reserved-memory` nodes carrying `no-map` require a translation hole. /// Entries in the FDT reservation block do not imply this attribute. pub no_map: bool, } #[derive(Debug, PartialEq, Eq)] pub struct Reservations { entries: [Reservation; MAX_RESERVATIONS], len: usize, } impl Reservations { pub fn as_slice(&self) -> &[Reservation] { &self.entries[..self.len] } } fn be32(bytes: &[u8], offset: usize) -> Result { let field = bytes .get(offset..offset.checked_add(4).ok_or(Error::Invalid)?) .ok_or(Error::Invalid)?; Ok(u32::from_be_bytes( field.try_into().map_err(|_| Error::Invalid)?, )) } fn be64(bytes: &[u8], offset: usize) -> Result { let field = bytes .get(offset..offset.checked_add(8).ok_or(Error::Invalid)?) .ok_or(Error::Invalid)?; Ok(u64::from_be_bytes( field.try_into().map_err(|_| Error::Invalid)?, )) } pub fn total_size(header: &[u8]) -> Result { if header.len() < HEADER_LEN || be32(header, 0)? != MAGIC { return Err(Error::Invalid); } let size = be32(header, 4)? as usize; if !(HEADER_LEN..=MAX_SIZE).contains(&size) { return Err(Error::Invalid); } Ok(size) } pub fn reservations(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size { return Err(Error::Invalid); } let blob = &blob[..size]; let start = be32(blob, 16)? as usize; let end = be32(blob, 8)? as usize; if start < HEADER_LEN || start & 7 != 0 || end > size || start >= end { return Err(Error::Invalid); } let mut result = Reservations { entries: [Reservation::default(); MAX_RESERVATIONS], len: 0, }; let mut cursor = start; loop { if cursor.checked_add(16).ok_or(Error::Invalid)? > end { return Err(Error::Invalid); } let address = be64(blob, cursor)?; let length = be64(blob, cursor + 8)?; cursor += 16; if address == 0 && length == 0 { return Ok(result); } if length == 0 || address.checked_add(length).is_none() { return Err(Error::Invalid); } if result.len == MAX_RESERVATIONS { return Err(Error::Unsupported); } let candidate = Reservation { start: address, size: length, no_map: false, }; for prior in result.as_slice() { let prior_end = prior.start + prior.size; let candidate_end = candidate.start + candidate.size; if candidate.start < prior_end && prior.start < candidate_end { return Err(Error::Invalid); } } result.entries[result.len] = candidate; result.len += 1; } } /// Static `reg` ranges below `/reserved-memory` on the root's identity bus. /// Dynamic `size` requests, phandle references, and non-identity `ranges` /// deliberately fail closed. This parser does not traverse a phandle graph. pub fn static_reserved(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size || be32(blob, 20)? < 17 || be32(blob, 24)? > 17 { return Err(Error::Invalid); } let blob = &blob[..size]; let structure = section(blob, be32(blob, 8)? as usize, be32(blob, 36)? as usize)?; let strings = section(blob, be32(blob, 12)? as usize, be32(blob, 32)? as usize)?; let mut result = Reservations { entries: [Reservation::default(); MAX_RESERVATIONS], len: 0, }; let mut cursor = 0usize; let mut depth = 0usize; let mut root_seen = false; let mut root_children_started = false; let mut reserved_seen = false; let mut parent_children_started = false; let mut root_address_cells = 2usize; let mut root_size_cells = 1usize; let mut root_address_cells_seen = false; let mut root_size_cells_seen = false; let mut reserved_parent = false; let mut parent_address_cells = None; let mut parent_size_cells = None; let mut identity_ranges = false; let mut child_reg: Option<&[u8]> = None; let mut dynamic_size = false; let mut disabled = false; let mut child_no_map = false; let mut child_reusable = false; let mut child_phandle = None; let mut child_memory_region = false; loop { let token = be32(structure, cursor)?; cursor += 4; match token { 1 => { if depth == 0 { if root_seen || cursor != 4 { return Err(Error::Invalid); } root_seen = true; } let name = nul_string(structure.get(cursor..).ok_or(Error::Invalid)?)?; cursor = align4(cursor.checked_add(name.len() + 1).ok_or(Error::Invalid)?)?; depth += 1; match depth { 1 if !name.is_empty() => return Err(Error::Invalid), 2 => { root_children_started = true; reserved_parent = name == b"reserved-memory"; if reserved_parent { if reserved_seen { return Err(Error::Invalid); } reserved_seen = true; parent_children_started = false; parent_address_cells = None; parent_size_cells = None; identity_ranges = false; } } 3 if reserved_parent => { parent_children_started = true; child_reg = None; dynamic_size = false; disabled = false; child_no_map = false; child_reusable = false; child_phandle = None; child_memory_region = false; } 4 if reserved_parent => return Err(Error::Unsupported), _ => {} } } 2 => { if depth == 0 { return Err(Error::Invalid); } if depth == 3 && reserved_parent && !disabled { if child_memory_region { // A reserved-memory child must not make this static // parser silently accept an unhandled reference (in // particular a self-phandle cycle). return Err(Error::Unsupported); } if dynamic_size { return Err(Error::Unsupported); } if child_no_map && child_reusable { return Err(Error::Invalid); } let reg_bytes = child_reg.ok_or(Error::Invalid)?; let address_cells = parent_address_cells.ok_or(Error::Unsupported)?; let size_cells = parent_size_cells.ok_or(Error::Unsupported)?; if address_cells != root_address_cells || size_cells != root_size_cells || !identity_ranges { return Err(Error::Unsupported); } let pair_len = (address_cells + size_cells) * 4; if reg_bytes.is_empty() || reg_bytes.len() % pair_len != 0 { return Err(Error::Invalid); } for bytes in reg_bytes.chunks_exact(pair_len) { let (start, length) = reg(bytes, address_cells, size_cells)?; if length == 0 || start.checked_add(length).is_none() { return Err(Error::Invalid); } let end = start + length; for prior in result.as_slice() { if start < prior.start + prior.size && prior.start < end { return Err(Error::Invalid); } } if result.len == MAX_RESERVATIONS { return Err(Error::Unsupported); } result.entries[result.len] = Reservation { start, size: length, no_map: child_no_map, }; result.len += 1; } } if depth == 2 && reserved_parent { if parent_address_cells != Some(root_address_cells) || parent_size_cells != Some(root_size_cells) || !identity_ranges { return Err(Error::Unsupported); } reserved_parent = false; } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let length = be32(structure, cursor)? as usize; let name_offset = be32(structure, cursor + 4)? as usize; cursor += 8; let value = section(structure, cursor, length)?; cursor = align4(cursor.checked_add(length).ok_or(Error::Invalid)?)?; let name = nul_string(strings.get(name_offset..).ok_or(Error::Invalid)?)?; if depth == 1 { if root_children_started { return Err(Error::Invalid); } if name == b"#address-cells" { if root_address_cells_seen || value.len() != 4 { return Err(Error::Invalid); } root_address_cells_seen = true; root_address_cells = be32(value, 0)? as usize; } if name == b"#size-cells" { if root_size_cells_seen || value.len() != 4 { return Err(Error::Invalid); } root_size_cells_seen = true; root_size_cells = be32(value, 0)? as usize; } if !(1..=2).contains(&root_address_cells) || !(1..=2).contains(&root_size_cells) { return Err(Error::Unsupported); } } else if depth == 2 && reserved_parent { if parent_children_started { return Err(Error::Invalid); } match name { b"#address-cells" => { if parent_address_cells.is_some() || value.len() != 4 { return Err(Error::Invalid); } parent_address_cells = Some(be32(value, 0)? as usize) } b"#size-cells" => { if parent_size_cells.is_some() || value.len() != 4 { return Err(Error::Invalid); } parent_size_cells = Some(be32(value, 0)? as usize) } b"ranges" => { if !value.is_empty() { return Err(Error::Unsupported); } identity_ranges = true; } _ => {} } } else if depth == 3 && reserved_parent { match name { b"reg" => { // A second property must not replace an earlier protected range. if child_reg.is_some() { return Err(Error::Invalid); } child_reg = Some(value); } b"size" => dynamic_size = true, b"status" => disabled = value == b"disabled\0", b"no-map" => { if child_no_map || !value.is_empty() { return Err(Error::Invalid); } child_no_map = true; } b"reusable" => { if child_reusable || !value.is_empty() { return Err(Error::Invalid); } child_reusable = true; } b"phandle" => { if child_phandle.is_some() || value.len() != 4 { return Err(Error::Invalid); } let number = be32(value, 0)?; if number == 0 { return Err(Error::Invalid); } child_phandle = Some(number); } b"memory-region" => { if child_memory_region || value.is_empty() || value.len() % 4 != 0 { return Err(Error::Invalid); } child_memory_region = true; } _ => {} } } } 4 => {} 9 if root_seen && depth == 0 && cursor == structure.len() => break, _ => return Err(Error::Invalid), } } Ok(result) } fn section(blob: &[u8], start: usize, len: usize) -> Result<&[u8], Error> { blob.get(start..start.checked_add(len).ok_or(Error::Invalid)?) .ok_or(Error::Invalid) } fn nul_string(bytes: &[u8]) -> Result<&[u8], Error> { let end = bytes .iter() .position(|byte| *byte == 0) .ok_or(Error::Invalid)?; Ok(&bytes[..end]) } fn contains_string(list: &[u8], target: &[u8]) -> bool { list.split(|byte| *byte == 0).any(|item| item == target) } fn cells(bytes: &[u8], count: usize) -> Result { if !(1..=2).contains(&count) || bytes.len() < count * 4 { return Err(Error::Unsupported); } let mut value = 0u64; for index in 0..count { value = (value << 32) | u64::from(be32(bytes, index * 4)?); } Ok(value) } fn reg(bytes: &[u8], address_cells: usize, size_cells: usize) -> Result<(u64, u64), Error> { let pair_len = (address_cells + size_cells) * 4; if bytes.len() < pair_len || bytes.len() % pair_len != 0 { return Err(Error::Invalid); } Ok(( cells(bytes, address_cells)?, cells(&bytes[address_cells * 4..], size_cells)?, )) } fn align4(value: usize) -> Result { value.checked_add(3).map(|n| n & !3).ok_or(Error::Invalid) } /// K321: discover root-bus *transport slots* by exact `compatible`, never by /// the node name. A DTB slot is not proof that a virtio device is attached. pub fn discover_virtio_mmio(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size || be32(blob, 20)? < 17 || be32(blob, 24)? > 17 { return Err(Error::Invalid); } let blob = &blob[..size]; let structure = section(blob, be32(blob, 8)? as usize, be32(blob, 36)? as usize)?; let strings = section(blob, be32(blob, 12)? as usize, be32(blob, 32)? as usize)?; let mut nodes = VirtioMmioNodes { entries: [VirtioMmioNode::default(); MAX_VIRTIO_MMIO_NODES], len: 0, }; let mut cursor = 0usize; let mut depth = 0usize; let mut root_seen = false; let mut root_children_started = false; let mut address_cells = 2usize; let mut size_cells = 1usize; let mut address_cells_seen = false; let mut size_cells_seen = false; let mut compatible = false; let mut disabled = false; let mut node_reg: Option<&[u8]> = None; // K586: properties after a child node and repeated properties are malformed // (DTSpec); other readers would see a different node, so fail closed. let mut node_children_started = false; let mut compatible_seen = false; let mut status_seen = false; loop { let token = be32(structure, cursor)?; cursor += 4; match token { 1 => { if depth == 0 { if root_seen || cursor != 4 { return Err(Error::Invalid); } root_seen = true; } let name = nul_string(structure.get(cursor..).ok_or(Error::Invalid)?)?; cursor = align4(cursor.checked_add(name.len() + 1).ok_or(Error::Invalid)?)?; depth += 1; if depth == 1 && !name.is_empty() { return Err(Error::Invalid); } if depth == 2 { root_children_started = true; compatible = false; disabled = false; node_reg = None; node_children_started = false; compatible_seen = false; status_seen = false; } if depth == 3 { node_children_started = true; } } 2 => { if depth == 0 { return Err(Error::Invalid); } if depth == 2 && compatible && !disabled { let bytes = node_reg.ok_or(Error::Invalid)?; let pair_len = (address_cells + size_cells) * 4; if bytes.len() != pair_len { return Err(Error::Invalid); } let (base, size) = reg(bytes, address_cells, size_cells)?; if size == 0 || base.checked_add(size).is_none() { return Err(Error::Invalid); } if nodes.len == MAX_VIRTIO_MMIO_NODES { return Err(Error::Unsupported); } nodes.entries[nodes.len] = VirtioMmioNode { base, size }; nodes.len += 1; } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let len = be32(structure, cursor)? as usize; let name_offset = be32(structure, cursor + 4)? as usize; cursor += 8; let value = section(structure, cursor, len)?; cursor = align4(cursor.checked_add(len).ok_or(Error::Invalid)?)?; let name = nul_string(strings.get(name_offset..).ok_or(Error::Invalid)?)?; if depth == 1 { if root_children_started { return Err(Error::Invalid); } match name { b"#address-cells" => { if address_cells_seen || value.len() != 4 { return Err(Error::Invalid); } address_cells_seen = true; address_cells = be32(value, 0)? as usize; } b"#size-cells" => { if size_cells_seen || value.len() != 4 { return Err(Error::Invalid); } size_cells_seen = true; size_cells = be32(value, 0)? as usize; } _ => {} } if !(1..=2).contains(&address_cells) || !(1..=2).contains(&size_cells) { return Err(Error::Unsupported); } } else if depth == 2 { if node_children_started { return Err(Error::Invalid); } match name { b"compatible" => { if value.last() != Some(&0) || core::mem::replace(&mut compatible_seen, true) { return Err(Error::Invalid); } compatible = contains_string(value, b"virtio,mmio"); } b"status" => { if core::mem::replace(&mut status_seen, true) { return Err(Error::Invalid); } disabled = value == b"disabled\0" } b"reg" => { if node_reg.replace(value).is_some() { return Err(Error::Invalid); } } _ => {} } } } 4 => {} 9 if root_seen && depth == 0 && cursor == structure.len() => break, _ => return Err(Error::Invalid), } } Ok(nodes) } /// Find one enabled root PCI ECAM host. The returned resource is still /// unmapped; the caller must validate and own a Device mapping before access. pub fn discover_pci_ecam(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size || be32(blob, 20)? < 17 || be32(blob, 24)? > 17 { return Err(Error::Invalid); } let blob = &blob[..size]; let structure = section(blob, be32(blob, 8)? as usize, be32(blob, 36)? as usize)?; let strings = section(blob, be32(blob, 12)? as usize, be32(blob, 32)? as usize)?; let mut cursor = 0usize; let mut depth = 0usize; let mut root_seen = false; let mut root_children_started = false; let mut address_cells = 2usize; let mut size_cells = 1usize; let mut address_cells_seen = false; let mut size_cells_seen = false; let mut compatible = false; let mut disabled = false; let mut node_reg: Option<&[u8]> = None; let mut node_bus_range: Option<&[u8]> = None; let mut node_ranges: Option<&[u8]> = None; let mut node_interrupt_map: Option<&[u8]> = None; let mut node_interrupt_mask: Option<&[u8]> = None; let mut node_address_cells: Option = None; let mut node_size_cells: Option = None; let mut node_children_started = false; let mut compatible_seen = false; let mut status_seen = false; let mut found = None; loop { let token = be32(structure, cursor)?; cursor += 4; match token { 1 => { if depth == 0 { if root_seen || cursor != 4 { return Err(Error::Invalid); } root_seen = true; } let name = nul_string(structure.get(cursor..).ok_or(Error::Invalid)?)?; cursor = align4(cursor.checked_add(name.len() + 1).ok_or(Error::Invalid)?)?; depth += 1; if depth == 1 && !name.is_empty() { return Err(Error::Invalid); } if depth == 2 { root_children_started = true; compatible = false; disabled = false; node_reg = None; node_bus_range = None; node_ranges = None; node_interrupt_map = None; node_interrupt_mask = None; node_address_cells = None; node_size_cells = None; node_children_started = false; compatible_seen = false; status_seen = false; } if depth == 3 { node_children_started = true; } } 2 => { if depth == 0 { return Err(Error::Invalid); } if depth == 2 && compatible && !disabled { if found.is_some() { return Err(Error::Invalid); } let bytes = node_reg.ok_or(Error::Invalid)?; if bytes.len() != (address_cells + size_cells) * 4 { return Err(Error::Invalid); } let (base, size) = reg(bytes, address_cells, size_cells)?; let buses = node_bus_range.ok_or(Error::Invalid)?; if buses.len() != 8 { return Err(Error::Invalid); } let first = be32(buses, 0)?; let last = be32(buses, 4)?; if first > last || last > 255 || size == 0 || base.checked_add(size).is_none() { return Err(Error::Invalid); } found = Some(PciEcamNode { base, size, first_bus: first as u8, last_bus: last as u8, memory_aperture: match node_ranges { Some(ranges) => { if node_address_cells != Some(3) || node_size_cells != Some(2) || address_cells != 2 || ranges.len() % 28 != 0 { return Err(Error::Unsupported); } let mut memory = None; for entry in ranges.chunks_exact(28) { if be32(entry, 0)? != 0x0200_0000 { continue; } if memory.is_some() { return Err(Error::Invalid); } let bus = (u64::from(be32(entry, 4)?) << 32) | u64::from(be32(entry, 8)?); let cpu = (u64::from(be32(entry, 12)?) << 32) | u64::from(be32(entry, 16)?); let length = (u64::from(be32(entry, 20)?) << 32) | u64::from(be32(entry, 24)?); let end = cpu.checked_add(length).ok_or(Error::Invalid)?; if bus != cpu || cpu == 0 || length == 0 || cpu % 4096 != 0 || end % 4096 != 0 { return Err(Error::Invalid); } memory = Some((cpu, end)); } memory } None => None, }, slot3_intx: match (node_interrupt_map, node_interrupt_mask) { (Some(map), Some(mask)) => { if mask.len() != 16 || be32(mask, 0)? != 0x1800 || be32(mask, 4)? != 0 || be32(mask, 8)? != 0 || be32(mask, 12)? != 7 || map.len() % 40 != 0 { return Err(Error::Unsupported); } let mut routes = [0u32; 4]; for entry in map.chunks_exact(40) { let slot = be32(entry, 0)?; let pin = be32(entry, 12)?; if slot & 0x1800 != 0x1800 || !(1..=4).contains(&pin) { continue; } let index = (pin - 1) as usize; if routes[index] != 0 || be32(entry, 20)? != 0 || be32(entry, 24)? != 0 || be32(entry, 28)? != 0 || be32(entry, 36)? != 4 { return Err(Error::Invalid); } routes[index] = be32(entry, 32)? .checked_add(32).ok_or(Error::Invalid)?; } if routes.iter().any(|spi| *spi < 32 || *spi > 1019) { return Err(Error::Invalid); } Some(routes) } (None, None) => None, _ => return Err(Error::Invalid), }, }); } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let len = be32(structure, cursor)? as usize; let name_offset = be32(structure, cursor + 4)? as usize; cursor += 8; let value = section(structure, cursor, len)?; cursor = align4(cursor.checked_add(len).ok_or(Error::Invalid)?)?; let name = nul_string(strings.get(name_offset..).ok_or(Error::Invalid)?)?; if depth == 1 { if root_children_started { return Err(Error::Invalid); } match name { b"#address-cells" => { if address_cells_seen || value.len() != 4 { return Err(Error::Invalid); } address_cells_seen = true; address_cells = be32(value, 0)? as usize; } b"#size-cells" => { if size_cells_seen || value.len() != 4 { return Err(Error::Invalid); } size_cells_seen = true; size_cells = be32(value, 0)? as usize; } _ => {} } if !(1..=2).contains(&address_cells) || !(1..=2).contains(&size_cells) { return Err(Error::Unsupported); } } else if depth == 2 { if node_children_started { return Err(Error::Invalid); } match name { b"compatible" => { if value.last() != Some(&0) || core::mem::replace(&mut compatible_seen, true) { return Err(Error::Invalid); } compatible = contains_string(value, b"pci-host-ecam-generic"); } b"status" => { if core::mem::replace(&mut status_seen, true) { return Err(Error::Invalid); } disabled = value == b"disabled\0"; } b"reg" => { if node_reg.replace(value).is_some() { return Err(Error::Invalid); } } b"bus-range" => { if node_bus_range.replace(value).is_some() { return Err(Error::Invalid); } } b"ranges" => { if node_ranges.replace(value).is_some() { return Err(Error::Invalid); } } b"interrupt-map" => { if node_interrupt_map.replace(value).is_some() { return Err(Error::Invalid); } } b"interrupt-map-mask" => { if node_interrupt_mask.replace(value).is_some() { return Err(Error::Invalid); } } b"#address-cells" => { if node_address_cells.replace(be32(value, 0)? as usize).is_some() || value.len() != 4 { return Err(Error::Invalid); } } b"#size-cells" => { if node_size_cells.replace(be32(value, 0)? as usize).is_some() || value.len() != 4 { return Err(Error::Invalid); } } _ => {} } } } 4 => {} 9 if root_seen && depth == 0 && cursor == structure.len() => break, _ => return Err(Error::Invalid), } } found.ok_or(Error::MissingDevice) } pub fn discover(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size { return Err(Error::Invalid); } if be32(blob, 20)? < 17 || be32(blob, 24)? > 17 { return Err(Error::Unsupported); } let blob = &blob[..size]; if be32(blob, 8)? < HEADER_LEN as u32 || be32(blob, 12)? < HEADER_LEN as u32 { return Err(Error::Invalid); } let structure = section(blob, be32(blob, 8)? as usize, be32(blob, 36)? as usize)?; let strings = section(blob, be32(blob, 12)? as usize, be32(blob, 32)? as usize)?; let mut cursor = 0usize; let mut depth = 0usize; let mut address_cells = 2usize; let mut size_cells = 1usize; let mut address_cells_seen = false; let mut size_cells_seen = false; let mut node_reg: Option<&[u8]> = None; let mut is_memory = false; let mut is_uart = false; let mut disabled = false; let mut ram = None; let mut uart = None; // K586: properties after child nodes and repeated properties are malformed. let mut root_children_started = false; let mut node_children_started = false; let mut seen = [false; 3]; loop { let token = be32(structure, cursor)?; cursor += 4; match token { 1 => { if depth == 0 && cursor != 4 { return Err(Error::Invalid); } let name = nul_string(structure.get(cursor..).ok_or(Error::Invalid)?)?; cursor = align4(cursor.checked_add(name.len() + 1).ok_or(Error::Invalid)?)?; depth += 1; if depth == 1 && !name.is_empty() { return Err(Error::Invalid); } if depth == 2 { root_children_started = true; node_children_started = false; seen = [false; 3]; node_reg = None; is_memory = name.starts_with(b"memory@"); is_uart = false; disabled = false; } if depth == 3 { node_children_started = true; } } 2 => { if depth == 0 { return Err(Error::Invalid); } if depth == 2 && !disabled { if let Some(bytes) = node_reg { if is_memory { // Hardware currently carries one contiguous RAM range. Never // expose only the first bank of a multi-bank guest as all RAM. if ram.is_some() { return Err(Error::Unsupported); } let pair_len = (address_cells + size_cells) * 4; if bytes.len() > pair_len && bytes.len() % pair_len == 0 { return Err(Error::Unsupported); } let (base, size) = reg(bytes, address_cells, size_cells)?; if size == 0 || base.checked_add(size).is_none() { return Err(Error::Invalid); } ram = Some((base, size)); } if is_uart { // A second compatible UART is ambiguous for the single-UART // boot contract; do not silently keep the first one. if uart.is_some() { return Err(Error::Invalid); } let pair_len = (address_cells + size_cells) * 4; if bytes.len() != pair_len { return Err(Error::Invalid); } let (base, size) = reg(bytes, address_cells, size_cells)?; if size < 0x1c || base.checked_add(size).is_none() { return Err(Error::Invalid); } uart = Some(base); } } else if is_memory || is_uart { return Err(Error::Invalid); } } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let len = be32(structure, cursor)? as usize; let name_offset = be32(structure, cursor + 4)? as usize; cursor += 8; let value = section(structure, cursor, len)?; cursor = align4(cursor.checked_add(len).ok_or(Error::Invalid)?)?; let name = nul_string(strings.get(name_offset..).ok_or(Error::Invalid)?)?; if depth == 1 { if root_children_started { return Err(Error::Invalid); } if name == b"#address-cells" { if address_cells_seen || value.len() != 4 { return Err(Error::Invalid); } address_cells_seen = true; address_cells = be32(value, 0)? as usize; } if name == b"#size-cells" { if size_cells_seen || value.len() != 4 { return Err(Error::Invalid); } size_cells_seen = true; size_cells = be32(value, 0)? as usize; } if !(1..=2).contains(&address_cells) || !(1..=2).contains(&size_cells) { return Err(Error::Unsupported); } } else if depth == 2 { if node_children_started { return Err(Error::Invalid); } let slot = match name { b"device_type" => Some(0), b"compatible" => Some(1), b"status" => Some(2), _ => None, }; if let Some(slot) = slot { if core::mem::replace(&mut seen[slot], true) { return Err(Error::Invalid); } } match name { b"reg" => { if node_reg.replace(value).is_some() { return Err(Error::Invalid); } } b"device_type" => is_memory |= value == b"memory\0", b"compatible" => is_uart = contains_string(value, b"arm,pl011"), b"status" => disabled = value == b"disabled\0", _ => {} } } } 4 => {} 9 if depth == 0 && cursor == structure.len() => break, _ => return Err(Error::Invalid), } } let (ram_base, ram_size) = ram.ok_or(Error::MissingDevice)?; Ok(Hardware { uart_base: uart.ok_or(Error::MissingDevice)?, ram_base, ram_size, }) } /// Discover only the QEMU virt GICv3 + ARMv8 timer contract. This does not /// configure the GIC, timer or CPU interrupt mask. pub fn discover_interrupts(blob: &[u8]) -> Result { let size = total_size(blob)?; if blob.len() < size || be32(blob, 20)? < 17 || be32(blob, 24)? > 17 { return Err(Error::Invalid); } let blob = &blob[..size]; let structure = section(blob, be32(blob, 8)? as usize, be32(blob, 36)? as usize)?; let strings = section(blob, be32(blob, 12)? as usize, be32(blob, 32)? as usize)?; let mut cursor = 0; let mut depth = 0; let mut address_cells = 2; let mut size_cells = 1; let mut address_cells_seen = false; let mut size_cells_seen = false; let mut gic_node = false; let mut timer_node = false; let mut compatible: Option<&[u8]> = None; let mut node_reg: Option<&[u8]> = None; let mut interrupts: Option<&[u8]> = None; let mut interrupt_cells = None; let mut controller = false; let mut disabled = false; let mut gic = None; let mut timer = None; // K586: properties after child nodes are malformed; #interrupt-cells is one u32. let mut root_children_started = false; let mut node_children_started = false; let mut status_seen = false; loop { let token = be32(structure, cursor)?; cursor += 4; match token { 1 => { let name = nul_string(structure.get(cursor..).ok_or(Error::Invalid)?)?; cursor = align4(cursor.checked_add(name.len() + 1).ok_or(Error::Invalid)?)?; depth += 1; if depth == 1 && !name.is_empty() { return Err(Error::Invalid); } if depth == 2 { root_children_started = true; node_children_started = false; status_seen = false; gic_node = name.starts_with(b"intc@"); timer_node = name == b"timer"; compatible = None; node_reg = None; interrupts = None; interrupt_cells = None; controller = false; disabled = false; } if depth == 3 { node_children_started = true; } } 2 => { if depth == 0 { return Err(Error::Invalid); } if depth == 2 && !disabled { if gic_node { if gic.is_some() { return Err(Error::Invalid); } if !compatible.is_some_and(|c| contains_string(c, b"arm,gic-v3")) || !controller || interrupt_cells != Some(3) { return Err(Error::Unsupported); } let bytes = node_reg.ok_or(Error::Invalid)?; let pair_len = (address_cells + size_cells) * 4; if bytes.len() != pair_len * 2 { return Err(Error::Unsupported); } let (dist, dist_size) = reg(bytes, address_cells, size_cells)?; let (redist, redist_size) = reg(&bytes[pair_len..], address_cells, size_cells)?; if dist_size < 0x10000 || redist_size < 0x20000 || dist.checked_add(dist_size).is_none() || redist.checked_add(redist_size).is_none() { return Err(Error::Invalid); } gic = Some((dist, redist)); } if timer_node { if timer.is_some() { return Err(Error::Invalid); } if !compatible.is_some_and(|c| contains_string(c, b"arm,armv8-timer")) { return Err(Error::Unsupported); } let bytes = interrupts.ok_or(Error::Invalid)?; if bytes.len() != 4 * 3 * 4 { return Err(Error::Unsupported); } // The second tuple is the non-secure physical timer PPI. let kind = be32(bytes, 12)?; let index = be32(bytes, 16)?; let flags = be32(bytes, 20)?; if kind != 1 || index >= 16 || flags != 4 { return Err(Error::Unsupported); } timer = Some(16 + index); } } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let len = be32(structure, cursor)? as usize; let name_offset = be32(structure, cursor + 4)? as usize; cursor += 8; let value = section(structure, cursor, len)?; cursor = align4(cursor.checked_add(len).ok_or(Error::Invalid)?)?; let name = nul_string(strings.get(name_offset..).ok_or(Error::Invalid)?)?; if depth == 1 { if root_children_started { return Err(Error::Invalid); } if name == b"#address-cells" { if address_cells_seen || value.len() != 4 { return Err(Error::Invalid); } address_cells_seen = true; address_cells = be32(value, 0)? as usize; } if name == b"#size-cells" { if size_cells_seen || value.len() != 4 { return Err(Error::Invalid); } size_cells_seen = true; size_cells = be32(value, 0)? as usize; } if !(1..=2).contains(&address_cells) || !(1..=2).contains(&size_cells) { return Err(Error::Unsupported); } } else if depth == 2 && node_children_started { return Err(Error::Invalid); } else if depth == 2 && (gic_node || timer_node) { match name { b"compatible" => { if compatible.replace(value).is_some() { return Err(Error::Invalid); } } b"reg" => { if node_reg.replace(value).is_some() { return Err(Error::Invalid); } } b"interrupts" => { if interrupts.replace(value).is_some() { return Err(Error::Invalid); } } b"#interrupt-cells" => { if value.len() != 4 || interrupt_cells.replace(be32(value, 0)?).is_some() { return Err(Error::Invalid); } } b"interrupt-controller" => { if controller || !value.is_empty() { return Err(Error::Invalid); } controller = true; } b"status" => { if core::mem::replace(&mut status_seen, true) { return Err(Error::Invalid); } disabled = value == b"disabled\0" } _ => {} } } } 4 => {} 9 if depth == 0 && cursor == structure.len() => break, _ => return Err(Error::Invalid), } } let (gic_distributor, gic_redistributor) = gic.ok_or(Error::MissingDevice)?; Ok(InterruptHardware { gic_distributor, gic_redistributor, nonsecure_phys_timer_ppi: timer.ok_or(Error::MissingDevice)?, }) } #[cfg(test)] mod tests { extern crate std; use super::*; use sha2::{Digest, Sha256}; use std::collections::BTreeSet; use std::vec::Vec; fn word(out: &mut Vec, value: u32) { out.extend_from_slice(&value.to_be_bytes()); } fn padded(out: &mut Vec, bytes: &[u8]) { out.extend_from_slice(bytes); while out.len() % 4 != 0 { out.push(0); } } fn node(out: &mut Vec, name: &[u8]) { word(out, 1); padded(out, name); } fn prop(out: &mut Vec, name_offset: u32, value: &[u8]) { word(out, 3); word(out, value.len() as u32); word(out, name_offset); padded(out, value); } fn fixture() -> Vec { let strings = b"#address-cells\0#size-cells\0reg\0device_type\0compatible\0ranges\0size\0status\0no-map\0reusable\0"; let mut tree = Vec::new(); node(&mut tree, b"\0"); prop(&mut tree, 0, &2u32.to_be_bytes()); prop(&mut tree, 15, &2u32.to_be_bytes()); node(&mut tree, b"memory@40000000\0"); prop( &mut tree, 27, &[0, 0, 0, 0, 0x40, 0, 0, 0, 0, 0, 0, 0, 0x20, 0, 0, 0], ); prop(&mut tree, 31, b"memory\0"); word(&mut tree, 2); node(&mut tree, b"pl011@9000000\0"); prop( &mut tree, 27, &[0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, 0], ); prop(&mut tree, 43, b"arm,pl011\0arm,primecell\0"); word(&mut tree, 2); word(&mut tree, 2); word(&mut tree, 9); let strings_offset = 56 + tree.len(); let mut blob = Vec::new(); for field in [ MAGIC, (strings_offset + strings.len()) as u32, 56, strings_offset as u32, 40, 17, 16, 0, strings.len() as u32, tree.len() as u32, ] { word(&mut blob, field); } blob.extend_from_slice(&[0; 16]); blob.extend_from_slice(&tree); blob.extend_from_slice(strings); blob } #[test] fn p15_k585_dtb_boundary_corpus_v2() { use sha2::{Digest, Sha256}; assert!(discover(&fixture()).is_ok()); let corpus = include_str!("../tests/corpus/dtb-v2.txt"); let mut count = 0; for line in corpus .lines() .filter(|s| !s.is_empty() && !s.starts_with('#')) { let fields: Vec<_> = line.split('|').collect(); assert_eq!(fields.len(), 4); let mut bytes = fixture(); let offset: usize = fields[1].parse().unwrap(); for (index, pair) in fields[2].as_bytes().chunks_exact(2).enumerate() { bytes[offset + index] = u8::from_str_radix(core::str::from_utf8(pair).unwrap(), 16).unwrap(); } let result = discover(&bytes).unwrap_err(); assert_eq!(std::format!("{result:?}"), fields[3]); let hash = Sha256::digest(&bytes) .iter() .map(|b| std::format!("{b:02x}")) .collect::(); std::eprintln!("K585 dtb {} {:?} {}", fields[0], result, hash); count += 1; } assert_eq!(count, 4); } #[test] fn k441_replays_versioned_dtb_corpus() { let corpus = include_str!("../tests/corpus/dtb-v1.txt"); assert!(corpus.starts_with("# TurkOS K441 DTB corpus v1.")); let mut seen = BTreeSet::new(); let mut good = 0; let mut bad = 0; for line in corpus .lines() .filter(|line| !line.starts_with('#') && !line.is_empty()) { let fields: Vec<_> = line.split('|').collect(); assert_eq!(fields.len(), 6, "malformed corpus record: {line}"); let [id, api, operation, offset, bytes, expected]: [&str; 6] = fields.try_into().unwrap(); assert!(seen.insert(id), "duplicate corpus id: {id}"); let mut blob = fixture(); let offset = offset.parse::().expect("numeric mutation offset"); match operation { "write" => { assert!(!bytes.is_empty() && bytes.len() % 2 == 0, "bad hex: {id}"); let end = offset + bytes.len() / 2; assert!(end <= blob.len(), "out-of-bounds mutation: {id}"); for (index, chunk) in bytes.as_bytes().chunks_exact(2).enumerate() { let hex = core::str::from_utf8(chunk).unwrap(); blob[offset + index] = u8::from_str_radix(hex, 16).expect("hex byte"); } } "cut" => { assert_eq!(offset, 0, "cut offset must be zero: {id}"); let length = bytes.parse::().expect("numeric cut length"); assert!(length < blob.len(), "cut must shorten blob: {id}"); blob.truncate(length); } _ => panic!("unknown mutation operation: {id}"), } let observed = match api { "discover" => discover(&blob).map(|_| ()), "static_reserved" => static_reserved(&blob).map(|_| ()), "reservations" => reservations(&blob).map(|_| ()), _ => panic!("unknown parser API: {id}"), }; let expected_result = match expected { "ok" => Ok(()), "Invalid" => Err(Error::Invalid), "Unsupported" => Err(Error::Unsupported), "MissingDevice" => Err(Error::MissingDevice), _ => panic!("unknown expected outcome: {id}"), }; assert_eq!(observed, expected_result, "corpus case {id}"); if observed.is_ok() { good += 1; } else { bad += 1; } let digest = Sha256::digest(&blob) .iter() .map(|byte| std::format!("{byte:02x}")) .collect::(); std::eprintln!("K441 {id} {api} {expected} {digest}"); } assert_eq!(seen.len(), 27, "K441 corpus completeness changed"); assert_eq!((good, bad), (3, 24)); } fn reserved_fixture( reg_value: Option<&[u8]>, dynamic: bool, ranges: Option<&[u8]>, disabled: bool, ) -> Vec { reserved_fixture_with_flags(reg_value, dynamic, ranges, disabled, None, false) } fn reserved_fixture_with_flags( reg_value: Option<&[u8]>, dynamic: bool, ranges: Option<&[u8]>, disabled: bool, no_map: Option<&[u8]>, reusable: bool, ) -> Vec { let mut blob = fixture(); let structure_len = be32(&blob, 36).unwrap() as usize; let insertion = 56 + structure_len - 8; let mut child = Vec::new(); node(&mut child, b"reserved-memory\0"); prop(&mut child, 0, &2u32.to_be_bytes()); prop(&mut child, 15, &2u32.to_be_bytes()); if let Some(value) = ranges { prop(&mut child, 54, value); } node(&mut child, b"pool@40010000\0"); if let Some(value) = reg_value { prop(&mut child, 27, value); } if dynamic { prop(&mut child, 61, &0x1000u64.to_be_bytes()); } if disabled { prop(&mut child, 66, b"disabled\0"); } if let Some(value) = no_map { prop(&mut child, 73, value); } if reusable { prop(&mut child, 80, &[]); } word(&mut child, 2); word(&mut child, 2); let added = child.len() as u32; blob.splice(insertion..insertion, child); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + added; blob[12..16].copy_from_slice(&strings.to_be_bytes()); blob[36..40].copy_from_slice(&((structure_len as u32) + added).to_be_bytes()); blob } fn reserved_phandle_fixture_with_extra( phandle: u32, reference: Option, extra_no_map: bool, ) -> Vec { let mut reg_bytes = Vec::new(); reg_bytes.extend_from_slice(&0x4001_0000u64.to_be_bytes()); reg_bytes.extend_from_slice(&0x1000u64.to_be_bytes()); let mut blob = reserved_fixture_with_flags( Some(®_bytes), false, Some(&[]), false, if extra_no_map { Some(&[]) } else { None }, false, ); let old_strings = be32(&blob, 12).unwrap() as usize; let old_string_len = be32(&blob, 32).unwrap() as usize; let old_structure_len = be32(&blob, 36).unwrap() as usize; // Last four tokens are child end, parent end, root end and FDT_END. let insertion = old_strings - 16; assert_eq!(be32(&blob, insertion).unwrap(), 2); let mut properties = Vec::new(); prop( &mut properties, old_string_len as u32, &phandle.to_be_bytes(), ); if let Some(number) = reference { prop( &mut properties, (old_string_len + b"phandle\0".len()) as u32, &number.to_be_bytes(), ); } let added = properties.len(); blob.splice(insertion..insertion, properties); let names = if reference.is_some() { b"phandle\0memory-region\0".as_slice() } else { b"phandle\0".as_slice() }; blob.extend_from_slice(names); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); blob[12..16].copy_from_slice(&((old_strings + added) as u32).to_be_bytes()); blob[32..36].copy_from_slice(&((old_string_len + names.len()) as u32).to_be_bytes()); blob[36..40].copy_from_slice(&((old_structure_len + added) as u32).to_be_bytes()); blob } fn reserved_phandle_fixture(phandle: u32, reference: Option) -> Vec { reserved_phandle_fixture_with_extra(phandle, reference, false) } #[test] fn k038_k039_minimized_phandle_cycle_corpus_is_bounded_and_replayed() { let corpus = include_str!("../tests/corpus/dtb-phandle-cycle-v1.txt"); assert!(corpus.starts_with("# TurkOS K038/K039 deterministic")); let mut cases = 0; for line in corpus .lines() .filter(|line| !line.starts_with('#') && !line.is_empty()) { let fields: Vec<_> = line.split('|').collect(); assert_eq!(fields.len(), 4); let phandle = fields[1].parse::().unwrap(); let reference = if fields[2] == "-" { None } else { Some(fields[2].parse::().unwrap()) }; let blob = reserved_phandle_fixture(phandle, reference); let observed = static_reserved(&blob).map(|reserved| reserved.as_slice().len()); match fields[3] { "Unsupported" => assert_eq!(observed, Err(Error::Unsupported), "{}", fields[0]), "ok" => assert_eq!(observed, Ok(1), "{}", fields[0]), other => panic!("unknown expected outcome: {other}"), } if fields[0] == "self-cycle" { assert_eq!( phandle, reference.unwrap(), "smallest graph cycle is one self edge" ); } let digest = Sha256::digest(&blob) .iter() .map(|byte| std::format!("{byte:02x}")) .collect::(); std::eprintln!("K038K039 {} {} {digest}", fields[0], fields[3]); cases += 1; } assert_eq!(cases, 3); // K039: remove an irrelevant no-map property from a failing cycle. // The shorter fixed corpus seed retains the exact Unsupported result; // deleting the sole reference instead changes it to a valid node. let inflated = reserved_phandle_fixture_with_extra(1, Some(1), true); let minimized = reserved_phandle_fixture(1, Some(1)); assert!(minimized.len() < inflated.len()); assert_eq!(static_reserved(&inflated), Err(Error::Unsupported)); assert_eq!(static_reserved(&minimized), Err(Error::Unsupported)); assert_eq!( static_reserved(&reserved_phandle_fixture(1, None)).map(|r| r.as_slice().len()), Ok(1) ); std::eprintln!( "K039 shrink BLOATED={} MINIMIZED={} RESULT=Unsupported", inflated.len(), minimized.len() ); } fn irq_fixture(timer_ppi_index: u32, gic_compatible: &[u8]) -> Vec { let mut blob = fixture(); let strings_offset = be32(&blob, 12).unwrap() as usize; let strings_len = be32(&blob, 32).unwrap() as usize; let new_names = b"#interrupt-cells\0interrupt-controller\0interrupts\0"; let cells_name = strings_len as u32; let controller_name = cells_name + 17; let interrupts_name = controller_name + 21; blob.extend_from_slice(new_names); blob[32..36].copy_from_slice(&((strings_len + new_names.len()) as u32).to_be_bytes()); let structure_len = be32(&blob, 36).unwrap() as usize; let insertion = 56 + structure_len - 8; let mut extra = Vec::new(); node(&mut extra, b"intc@8000000\0"); prop(&mut extra, 43, gic_compatible); prop(&mut extra, cells_name, &3u32.to_be_bytes()); prop(&mut extra, controller_name, &[]); let mut ranges = Vec::new(); for (start, size) in [(0x0800_0000u64, 0x10000u64), (0x080a_0000, 0xf60000)] { ranges.extend_from_slice(&start.to_be_bytes()); ranges.extend_from_slice(&size.to_be_bytes()); } prop(&mut extra, 27, &ranges); word(&mut extra, 2); node(&mut extra, b"timer\0"); prop(&mut extra, 43, b"arm,armv8-timer\0"); let mut interrupts = Vec::new(); for index in [13, timer_ppi_index, 11, 10] { for cell in [1, index, 4] { word(&mut interrupts, cell); } } prop(&mut extra, interrupts_name, &interrupts); word(&mut extra, 2); let added = extra.len() as u32; blob.splice(insertion..insertion, extra); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); blob[12..16].copy_from_slice(&((strings_offset as u32) + added).to_be_bytes()); blob[36..40].copy_from_slice(&((structure_len as u32) + added).to_be_bytes()); blob } fn virtio_fixture( name: &[u8], compatible: &[u8], reg_value: Option<&[u8]>, disabled: bool, ) -> Vec { let mut blob = fixture(); let structure_len = be32(&blob, 36).unwrap() as usize; let insertion = 56 + structure_len - 8; let mut child = Vec::new(); node(&mut child, name); prop(&mut child, 43, compatible); if let Some(value) = reg_value { prop(&mut child, 27, value); } if disabled { prop(&mut child, 66, b"disabled\0"); } word(&mut child, 2); let added = child.len() as u32; blob.splice(insertion..insertion, child); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + added; blob[12..16].copy_from_slice(&strings.to_be_bytes()); blob[36..40].copy_from_slice(&((structure_len as u32) + added).to_be_bytes()); blob } #[test] fn k321_discovers_exact_compatible_independent_of_node_name() { let range = [0x0a00_0000u64.to_be_bytes(), 0x200u64.to_be_bytes()].concat(); let blob = virtio_fixture( b"surprising@a000000\0", b"vendor,other\0virtio,mmio\0", Some(&range), false, ); assert_eq!( discover_virtio_mmio(&blob).unwrap().as_slice(), &[VirtioMmioNode { base: 0x0a00_0000, size: 0x200 }] ); } #[test] fn k321_ignores_wrong_compatible_disabled_and_absent_nodes() { let range = [0x0a00_0000u64.to_be_bytes(), 0x200u64.to_be_bytes()].concat(); assert!( discover_virtio_mmio(&fixture()) .unwrap() .as_slice() .is_empty() ); for (compatible, disabled) in [ (b"vendor,other\0".as_slice(), false), (b"virtio,mmio\0".as_slice(), true), ] { let blob = virtio_fixture(b"virtio_mmio@a000000\0", compatible, Some(&range), disabled); assert!(discover_virtio_mmio(&blob).unwrap().as_slice().is_empty()); } } #[test] fn k321_rejects_missing_multiple_or_overflowing_compatible_reg() { let range = [0x0a00_0000u64.to_be_bytes(), 0x200u64.to_be_bytes()].concat(); let multiple = [range.as_slice(), range.as_slice()].concat(); let overflow = [u64::MAX.to_be_bytes(), 0x200u64.to_be_bytes()].concat(); for entry in [None, Some(multiple.as_slice()), Some(overflow.as_slice())] { let blob = virtio_fixture(b"virtio_mmio@a000000\0", b"virtio,mmio\0", entry, false); assert_eq!(discover_virtio_mmio(&blob), Err(Error::Invalid)); } } #[test] fn k321_rejects_malformed_compatible_and_header() { let range = [0x0a00_0000u64.to_be_bytes(), 0x200u64.to_be_bytes()].concat(); let blob = virtio_fixture( b"virtio_mmio@a000000\0", b"virtio,mmio", Some(&range), false, ); assert_eq!(discover_virtio_mmio(&blob), Err(Error::Invalid)); let mut blob = fixture(); blob[8..12].copy_from_slice(&u32::MAX.to_be_bytes()); assert_eq!(discover_virtio_mmio(&blob), Err(Error::Invalid)); } fn k322_nodes() -> VirtioMmioNodes { let mut nodes = VirtioMmioNodes { entries: [VirtioMmioNode::default(); MAX_VIRTIO_MMIO_NODES], len: MAX_VIRTIO_MMIO_NODES, }; for (index, node) in nodes.entries.iter_mut().enumerate() { *node = VirtioMmioNode { base: 0x0a00_0000 + index as u64 * 0x200, size: 0x200, }; } nodes } #[test] fn k322_accepts_eight_subpage_slots_per_device_page() { let window = virtio_page_window(&k322_nodes(), 0x4000_0000, 0x4000_0000).unwrap(); assert_eq!( window, VirtioPageWindow { start: 0x0a00_0000, end: 0x0a00_4000, pages: 4 } ); } #[test] fn k322_rejects_ram_intersection_misalignment_and_slot_overlap() { let nodes = k322_nodes(); assert_eq!( virtio_page_window(&nodes, 0x0a00_1000, 0x1000), Err(Error::Invalid) ); let mut nodes = k322_nodes(); nodes.entries[1].base += 1; assert_eq!( virtio_page_window(&nodes, 0x4000_0000, 0x2000_0000), Err(Error::Invalid) ); let mut nodes = k322_nodes(); nodes.entries[1].base = nodes.entries[0].base; assert_eq!( virtio_page_window(&nodes, 0x4000_0000, 0x2000_0000), Err(Error::Invalid) ); } #[test] fn k322_rejects_sparse_oversized_and_overflowing_windows() { let mut nodes = k322_nodes(); nodes.entries[31].base += 0x200; assert_eq!( virtio_page_window(&nodes, 0x4000_0000, 0x2000_0000), Err(Error::Invalid) ); let mut nodes = k322_nodes(); nodes.entries[0].size = 0x400; assert_eq!( virtio_page_window(&nodes, 0x4000_0000, 0x2000_0000), Err(Error::Invalid) ); let mut nodes = k322_nodes(); nodes.entries[0].base = u64::MAX - 0x1ff; assert_eq!( virtio_page_window(&nodes, 0x4000_0000, 0x2000_0000), Err(Error::Invalid) ); assert_eq!( virtio_page_window(&k322_nodes(), u64::MAX, 2), Err(Error::Invalid) ); } #[test] fn discovers_gicv3_and_nonsecure_physical_timer_without_enabling_irq() { assert_eq!( discover_interrupts(&irq_fixture(14, b"arm,gic-v3\0")), Ok(InterruptHardware { gic_distributor: 0x0800_0000, gic_redistributor: 0x080a_0000, nonsecure_phys_timer_ppi: 30 }) ); } #[test] fn rejects_unknown_gic_and_out_of_range_timer_ppi() { assert_eq!( discover_interrupts(&irq_fixture(14, b"arm,gic-v2\0")), Err(Error::Unsupported) ); assert_eq!( discover_interrupts(&irq_fixture(16, b"arm,gic-v3\0")), Err(Error::Unsupported) ); assert_eq!(discover_interrupts(&fixture()), Err(Error::MissingDevice)); } #[test] fn finds_qemu_profile() { assert_eq!( discover(&fixture()), Ok(Hardware { uart_base: 0x0900_0000, ram_base: 0x4000_0000, ram_size: 0x2000_0000 }) ); } #[test] fn rejects_second_ram_range_in_one_reg() { let mut blob = fixture(); let name = b"memory@40000000\0"; let reg_token = blob.windows(name.len()).position(|w| w == name).unwrap() + name.len(); let reg_offset = reg_token + 12; let second = [0x5000_0000u64.to_be_bytes(), 0x1000_0000u64.to_be_bytes()].concat(); blob.splice(reg_offset + 16..reg_offset + 16, second); // A second bank is not supported by the single-range Hardware contract. let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + 16; blob[12..16].copy_from_slice(&strings.to_be_bytes()); let structure = be32(&blob, 36).unwrap() + 16; blob[36..40].copy_from_slice(&structure.to_be_bytes()); let len_offset = reg_token + 4; blob[len_offset..len_offset + 4].copy_from_slice(&32u32.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Unsupported)); // A malformed trailing bank must be rejected as invalid, not ignored. let mut truncated = fixture(); let reg_token = truncated .windows(name.len()) .position(|w| w == name) .unwrap() + name.len(); let reg_offset = reg_token + 12; truncated.splice(reg_offset + 16..reg_offset + 16, [0, 0, 0, 0]); let total = truncated.len() as u32; truncated[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&truncated, 12).unwrap() + 4; truncated[12..16].copy_from_slice(&strings.to_be_bytes()); let structure = be32(&truncated, 36).unwrap() + 4; truncated[36..40].copy_from_slice(&structure.to_be_bytes()); truncated[reg_token + 4..reg_token + 8].copy_from_slice(&20u32.to_be_bytes()); assert_eq!(discover(&truncated), Err(Error::Invalid)); } #[test] fn rejects_second_root_memory_node() { let mut blob = fixture(); let structure_len = be32(&blob, 36).unwrap() as usize; let insertion = 56 + structure_len - 8; let mut extra = Vec::new(); node(&mut extra, b"memory@50000000\0"); let bank = [0x5000_0000u64.to_be_bytes(), 0x1000_0000u64.to_be_bytes()].concat(); prop(&mut extra, 27, &bank); prop(&mut extra, 31, b"memory\0"); word(&mut extra, 2); let added = extra.len() as u32; blob.splice(insertion..insertion, extra); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + added; blob[12..16].copy_from_slice(&strings.to_be_bytes()); blob[36..40].copy_from_slice(&((structure_len as u32) + added).to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Unsupported)); } #[test] fn rejects_short_and_bad_magic() { assert_eq!(discover(&[]), Err(Error::Invalid)); let mut blob = fixture(); blob[0] = 0; assert_eq!(discover(&blob), Err(Error::Invalid)); } #[test] fn rejects_out_of_bounds_structure() { let mut blob = fixture(); blob[8..12].copy_from_slice(&u32::MAX.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); } #[test] fn rejects_out_of_bounds_strings_and_property_names() { let mut blob = fixture(); blob[12..16].copy_from_slice(&u32::MAX.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); let mut blob = fixture(); blob[32..36].copy_from_slice(&u32::MAX.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); let mut blob = fixture(); let strings_len = be32(&blob, 32).unwrap(); // The first root property begins after the 8-byte root node token/name. // Its name offset is at 56 + 8 + 8 and must remain inside strings. blob[72..76].copy_from_slice(&strings_len.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); let mut blob = fixture(); let strings_start = be32(&blob, 12).unwrap() as usize; let strings_len = be32(&blob, 32).unwrap() as usize; blob[72..76].copy_from_slice(&((strings_len - 1) as u32).to_be_bytes()); blob[strings_start + strings_len - 1] = b'x'; assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); } #[test] fn rejects_truncated_or_misaligned_structure_tokens() { let mut blob = fixture(); let structure_len = be32(&blob, 36).unwrap(); blob[36..40].copy_from_slice(&(structure_len - 1).to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); let mut blob = fixture(); blob[8..12].copy_from_slice(&57u32.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); let mut blob = fixture(); // A declared value one byte longer shifts the next aligned token. blob[68..72].copy_from_slice(&5u32.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); } #[test] fn rejects_duplicate_root_cell_counts() { for name_offset in [0, 15] { let mut blob = fixture(); let mut duplicate = Vec::new(); prop(&mut duplicate, name_offset, &2u32.to_be_bytes()); // Insert before the first child node, after the two root properties. blob.splice(96..96, duplicate); let added = 16u32; let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + added; blob[12..16].copy_from_slice(&strings.to_be_bytes()); let structure = be32(&blob, 36).unwrap() + added; blob[36..40].copy_from_slice(&structure.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); assert_eq!(discover_interrupts(&blob), Err(Error::Invalid)); } } #[test] fn rejects_zero_root_size_cells() { let mut blob = fixture(); // The second root property value is the 32-bit #size-cells count. blob[92..96].copy_from_slice(&0u32.to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Unsupported)); assert_eq!(static_reserved(&blob), Err(Error::Unsupported)); assert_eq!(discover_interrupts(&blob), Err(Error::Unsupported)); } #[test] fn rejects_wrong_or_ambiguous_uart_compatible() { let mut wrong = fixture(); let compatible = b"arm,pl011\0"; let position = wrong .windows(compatible.len()) .position(|bytes| bytes == compatible) .unwrap(); wrong[position + 8] = b'x'; assert_eq!(discover(&wrong), Err(Error::MissingDevice)); let mut duplicate = fixture(); let structure_len = be32(&duplicate, 36).unwrap() as usize; let insertion = 56 + structure_len - 8; let mut second_uart = Vec::new(); node(&mut second_uart, b"pl011@9100000\0"); let reg = [0x0910_0000u64.to_be_bytes(), 0x1000u64.to_be_bytes()].concat(); prop(&mut second_uart, 27, ®); prop(&mut second_uart, 43, b"arm,pl011\0"); word(&mut second_uart, 2); let added = second_uart.len() as u32; duplicate.splice(insertion..insertion, second_uart); let total = duplicate.len() as u32; duplicate[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&duplicate, 12).unwrap() + added; duplicate[12..16].copy_from_slice(&strings.to_be_bytes()); duplicate[36..40].copy_from_slice(&((structure_len as u32) + added).to_be_bytes()); assert_eq!(discover(&duplicate), Err(Error::Invalid)); } #[test] fn rejects_short_overflowing_or_multi_entry_uart_window() { let mut short = fixture(); let reg = [0x0900_0000u64.to_be_bytes(), 0x1000u64.to_be_bytes()].concat(); let start = short .windows(reg.len()) .position(|bytes| bytes == reg) .unwrap(); short[start + 8..start + 16].copy_from_slice(&0x10u64.to_be_bytes()); assert_eq!(discover(&short), Err(Error::Invalid)); let mut overflowing = fixture(); overflowing[start..start + 8].copy_from_slice(&(u64::MAX - 0xff).to_be_bytes()); assert_eq!(discover(&overflowing), Err(Error::Invalid)); let mut multiple = fixture(); let uart_name = b"pl011@9000000\0"; let name_start = multiple .windows(uart_name.len()) .position(|bytes| bytes == uart_name) .unwrap(); let reg_property = name_start + 16; let reg_value = reg_property + 12; multiple.splice(reg_value + 16..reg_value + 16, reg); multiple[reg_property + 4..reg_property + 8].copy_from_slice(&32u32.to_be_bytes()); let total = multiple.len() as u32; multiple[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&multiple, 12).unwrap() + 16; multiple[12..16].copy_from_slice(&strings.to_be_bytes()); let structure = be32(&multiple, 36).unwrap() + 16; multiple[36..40].copy_from_slice(&structure.to_be_bytes()); assert_eq!(discover(&multiple), Err(Error::Invalid)); } #[test] fn rejects_short_gic_distributor_window() { let mut blob = irq_fixture(14, b"arm,gic-v3\0"); let reg = [0x0800_0000u64.to_be_bytes(), 0x10000u64.to_be_bytes()].concat(); let start = blob .windows(reg.len()) .position(|bytes| bytes == reg) .unwrap(); blob[start + 8..start + 16].copy_from_slice(&0xffffu64.to_be_bytes()); assert_eq!(discover_interrupts(&blob), Err(Error::Invalid)); } #[test] fn rejects_short_or_overflowing_gic_redistributor_window() { let mut blob = irq_fixture(14, b"arm,gic-v3\0"); let reg = [0x080a_0000u64.to_be_bytes(), 0xf60000u64.to_be_bytes()].concat(); let start = blob .windows(reg.len()) .position(|bytes| bytes == reg) .unwrap(); blob[start + 8..start + 16].copy_from_slice(&0x1ffffu64.to_be_bytes()); assert_eq!(discover_interrupts(&blob), Err(Error::Invalid)); let mut overflowing = irq_fixture(14, b"arm,gic-v3\0"); overflowing[start..start + 8].copy_from_slice(&(u64::MAX - 0xffff).to_be_bytes()); assert_eq!(discover_interrupts(&overflowing), Err(Error::Invalid)); } #[test] fn rejects_truncated_and_oversized_blob() { let mut blob = fixture(); blob.pop(); assert_eq!(discover(&blob), Err(Error::Invalid)); let mut blob = fixture(); blob[4..8].copy_from_slice(&((MAX_SIZE + 1) as u32).to_be_bytes()); assert_eq!(discover(&blob), Err(Error::Invalid)); } #[test] fn reads_empty_reservation_map() { assert!(reservations(&fixture()).unwrap().as_slice().is_empty()); } #[test] fn reads_reservation_and_rejects_overlap() { let mut blob = fixture(); let first = [0x4000_1000u64.to_be_bytes(), 0x2000u64.to_be_bytes()].concat(); blob.splice(40..40, first); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let structure = 72u32; blob[8..12].copy_from_slice(&structure.to_be_bytes()); let strings = u32::from_be_bytes(blob[12..16].try_into().unwrap()) + 16; blob[12..16].copy_from_slice(&strings.to_be_bytes()); assert_eq!( reservations(&blob).unwrap().as_slice(), &[Reservation { start: 0x4000_1000, size: 0x2000, no_map: false, }] ); let duplicate = [0x4000_2000u64.to_be_bytes(), 0x1000u64.to_be_bytes()].concat(); blob.splice(56..56, duplicate); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); blob[8..12].copy_from_slice(&88u32.to_be_bytes()); blob[12..16].copy_from_slice(&(strings + 16).to_be_bytes()); assert_eq!(reservations(&blob), Err(Error::Invalid)); } #[test] fn rejects_unterminated_reservation_map() { let mut blob = fixture(); blob[40..48].copy_from_slice(&0x4000_1000u64.to_be_bytes()); blob[48..56].copy_from_slice(&0x1000u64.to_be_bytes()); assert_eq!(reservations(&blob), Err(Error::Invalid)); } #[test] fn rejects_unaligned_and_overflowing_reservations() { let mut blob = fixture(); blob[16..20].copy_from_slice(&41u32.to_be_bytes()); assert_eq!(reservations(&blob), Err(Error::Invalid)); let mut blob = fixture(); blob[40..48].copy_from_slice(&(u64::MAX - 0xff).to_be_bytes()); blob[48..56].copy_from_slice(&0x200u64.to_be_bytes()); assert_eq!(reservations(&blob), Err(Error::Invalid)); } #[test] fn reads_static_reserved_memory_node() { let reg = [0, 0, 0, 0, 0x40, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0x10, 0]; let blob = reserved_fixture(Some(®), false, Some(&[]), false); assert!(discover(&blob).is_ok()); assert_eq!( static_reserved(&blob).unwrap().as_slice(), &[Reservation { start: 0x4001_0000, size: 0x1000, no_map: false, }] ); } #[test] fn preserves_no_map_without_promoting_plain_reservations() { let reg = [0x4000_0000u64.to_be_bytes(), 0x4000u64.to_be_bytes()].concat(); let blob = reserved_fixture_with_flags(Some(®), false, Some(&[]), false, Some(&[]), false); assert_eq!( static_reserved(&blob).unwrap().as_slice(), &[Reservation { start: 0x4000_0000, size: 0x4000, no_map: true, }] ); assert!( reservations(&blob) .unwrap() .as_slice() .iter() .all(|item| !item.no_map) ); } #[test] fn rejects_malformed_or_conflicting_no_map() { let reg = [0x4000_0000u64.to_be_bytes(), 0x4000u64.to_be_bytes()].concat(); let malformed = reserved_fixture_with_flags(Some(®), false, Some(&[]), false, Some(&[1]), false); assert_eq!(static_reserved(&malformed), Err(Error::Invalid)); let conflicting = reserved_fixture_with_flags(Some(®), false, Some(&[]), false, Some(&[]), true); assert_eq!(static_reserved(&conflicting), Err(Error::Invalid)); let disabled = reserved_fixture_with_flags(Some(®), false, Some(&[]), true, Some(&[]), false); assert!(static_reserved(&disabled).unwrap().as_slice().is_empty()); } #[test] fn excludes_each_static_range_from_free_pages() { let mut reg = Vec::new(); for (start, size) in [(0x4001_0001u64, 0x1000u64), (0x4002_0000, 0x2000)] { reg.extend_from_slice(&start.to_be_bytes()); reg.extend_from_slice(&size.to_be_bytes()); } let blob = reserved_fixture(Some(®), false, Some(&[]), false); let reserved = static_reserved(&blob).unwrap(); assert_eq!(reserved.as_slice().len(), 2); let mut free = crate::memory::FreeMap::from_ram(0x4000_0000, 0x40000).unwrap(); for region in reserved.as_slice() { free.reserve(region.start, region.size).unwrap(); } assert_eq!(free.total_pages(), 60); for region in free.regions() { assert!(region.end <= 0x4001_0000 || region.start >= 0x4001_2000); assert!(region.end <= 0x4002_0000 || region.start >= 0x4002_2000); } } #[test] fn rejects_missing_malformed_and_overflowing_static_ranges() { let missing = reserved_fixture(None, false, Some(&[]), false); assert_eq!(static_reserved(&missing), Err(Error::Invalid)); let short = reserved_fixture(Some(&[0; 15]), false, Some(&[]), false); assert_eq!(static_reserved(&short), Err(Error::Invalid)); let mut overflow = Vec::new(); overflow.extend_from_slice(&(u64::MAX - 0xff).to_be_bytes()); overflow.extend_from_slice(&0x200u64.to_be_bytes()); let overflow = reserved_fixture(Some(&overflow), false, Some(&[]), false); assert_eq!(static_reserved(&overflow), Err(Error::Invalid)); } #[test] fn rejects_zero_length_and_truncated_second_static_range() { let mut zero = Vec::new(); zero.extend_from_slice(&0x4001_0000u64.to_be_bytes()); zero.extend_from_slice(&0u64.to_be_bytes()); assert_eq!( static_reserved(&reserved_fixture(Some(&zero), false, Some(&[]), false)), Err(Error::Invalid) ); let mut truncated = Vec::new(); for (start, size) in [(0x4001_0000u64, 0x1000u64), (0x4002_0000, 0x2000)] { truncated.extend_from_slice(&start.to_be_bytes()); truncated.extend_from_slice(&size.to_be_bytes()); } truncated.pop(); assert_eq!( static_reserved(&reserved_fixture(Some(&truncated), false, Some(&[]), false)), Err(Error::Invalid) ); } #[test] fn accepts_adjacent_static_ranges_without_merging_them() { let mut reg = Vec::new(); for (start, size) in [(0x4001_0000u64, 0x1000u64), (0x4001_1000, 0x1000)] { reg.extend_from_slice(&start.to_be_bytes()); reg.extend_from_slice(&size.to_be_bytes()); } let blob = reserved_fixture(Some(®), false, Some(&[]), false); assert_eq!( static_reserved(&blob).unwrap().as_slice(), &[ Reservation { start: 0x4001_0000, size: 0x1000, no_map: false, }, Reservation { start: 0x4001_1000, size: 0x1000, no_map: false, }, ] ); } #[test] fn rejects_duplicate_reserved_reg_property() { let first = [0x4001_0000u64.to_be_bytes(), 0x1000u64.to_be_bytes()].concat(); let second = [0x4002_0000u64.to_be_bytes(), 0x1000u64.to_be_bytes()].concat(); let mut blob = reserved_fixture(Some(&first), false, Some(&[]), false); let mut extra = Vec::new(); prop(&mut extra, 27, &second); let insertion = blob .windows(first.len()) .position(|window| window == first) .unwrap() + first.len(); let added = extra.len() as u32; blob.splice(insertion..insertion, extra); let total = blob.len() as u32; blob[4..8].copy_from_slice(&total.to_be_bytes()); let strings = be32(&blob, 12).unwrap() + added; blob[12..16].copy_from_slice(&strings.to_be_bytes()); let structure_len = be32(&blob, 36).unwrap() + added; blob[36..40].copy_from_slice(&structure_len.to_be_bytes()); assert_eq!(be32(&blob, insertion), Ok(3)); assert_eq!(be32(&blob, insertion + 4), Ok(16)); assert!( discover(&blob).is_ok(), "duplicate-reg fixture must remain a valid FDT" ); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); } #[test] fn rejects_overlapping_static_ranges() { let mut reg = Vec::new(); for (start, size) in [(0x4001_0000u64, 0x2000u64), (0x4001_1000, 0x1000)] { reg.extend_from_slice(&start.to_be_bytes()); reg.extend_from_slice(&size.to_be_bytes()); } let blob = reserved_fixture(Some(®), false, Some(&[]), false); assert_eq!(static_reserved(&blob), Err(Error::Invalid)); } #[test] fn rejects_dynamic_or_translated_reserved_memory() { let dynamic = reserved_fixture(None, true, Some(&[]), false); assert_eq!(static_reserved(&dynamic), Err(Error::Unsupported)); let reg = [0, 0, 0, 0, 0x40, 0x01, 0, 0, 0, 0, 0, 0, 0, 0x10, 0]; let translated = reserved_fixture(Some(®), false, Some(&[0, 0, 0, 0]), false); assert_eq!(static_reserved(&translated), Err(Error::Unsupported)); let missing_ranges = reserved_fixture(Some(®), false, None, false); assert_eq!(static_reserved(&missing_ranges), Err(Error::Unsupported)); let mut bad_cells = reserved_fixture(Some(®), false, Some(&[]), false); let parent_address_cells = 56 + be32(&fixture(), 36).unwrap() as usize - 8 + 32; bad_cells[parent_address_cells..parent_address_cells + 4] .copy_from_slice(&3u32.to_be_bytes()); assert_eq!(static_reserved(&bad_cells), Err(Error::Unsupported)); } #[test] fn ignores_disabled_reserved_memory() { let blob = reserved_fixture(None, false, Some(&[]), true); assert!(static_reserved(&blob).unwrap().as_slice().is_empty()); } }