//! Bounded FDT v17 traversal and root identity-bus device discovery. //! Caller-owned scratch contains only offsets into the supplied blob. No //! addresses are mapped, resources acquired or platform state published here. use crate::ram::{RamBanks, Region}; pub const MAX_BLOB: usize = 2 * 1024 * 1024; pub const MAX_NODES: usize = 128; pub const MAX_DEPTH: usize = 16; pub const MAX_PROPERTIES: usize = 1024; pub const MAX_EDGES: usize = 256; pub const MAX_RESERVATIONS: usize = 32; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Error { Invalid, BlobLimit, NodeLimit, DepthLimit, PropertyLimit, ReferenceLimit, InvalidPhandle, MissingPhandle, PhandleCycle, Unsupported, MissingDevice, AmbiguousDevice, Ram, ReservationLimit, } impl Error { pub const fn tag(self) -> &'static str { match self { Self::Invalid => "dtb-invalid", Self::BlobLimit => "dtb-blob-limit", Self::NodeLimit => "dtb-node-limit", Self::DepthLimit => "dtb-depth-limit", Self::PropertyLimit => "dtb-property-limit", Self::ReferenceLimit => "dtb-reference-limit", Self::InvalidPhandle => "dtb-invalid-phandle", Self::MissingPhandle => "dtb-missing-phandle", Self::PhandleCycle => "dtb-phandle-cycle", Self::Unsupported => "dtb-unsupported", Self::MissingDevice => "dtb-missing-device", Self::AmbiguousDevice => "dtb-ambiguous-device", Self::Ram => "dtb-ram", Self::ReservationLimit => "dtb-reservation-limit", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Limits { pub blob: usize, pub nodes: usize, pub depth: usize, pub properties: usize, pub references: usize, } impl Limits { pub const DEFAULT: Self = Self { blob: MAX_BLOB, nodes: MAX_NODES, depth: MAX_DEPTH, properties: MAX_PROPERTIES, references: MAX_EDGES, }; fn validate(self) -> Result<(), Error> { if self.blob < 40 || self.blob > MAX_BLOB || self.nodes == 0 || self.nodes > MAX_NODES || self.depth == 0 || self.depth > MAX_DEPTH || self.properties > MAX_PROPERTIES || self.references > MAX_EDGES { return Err(Error::Invalid); } Ok(()) } } #[derive(Clone, Copy, Default)] struct Node { parent: u16, depth: u8, name_start: u32, name_len: u32, phandle: u32, children_started: bool, } #[derive(Clone, Copy, Default)] struct Property { node: u16, name_offset: u32, start: u32, len: u32, } #[derive(Clone, Copy, Default)] struct Edge { from: u16, to: u16, } /// Keep this roughly 22-KiB scratch in BSS in the guest, not on the initial /// 16-KiB stack. Failure can modify scratch, but never publishes a platform. pub struct Workspace { nodes: [Node; MAX_NODES], properties: [Property; MAX_PROPERTIES], edges: [Edge; MAX_EDGES], nodes_len: usize, properties_len: usize, edges_len: usize, strings_start: usize, strings_end: usize, } impl Workspace { pub const fn new() -> Self { Self { nodes: [Node { parent: 0, depth: 0, name_start: 0, name_len: 0, phandle: 0, children_started: false, }; MAX_NODES], properties: [Property { node: 0, name_offset: 0, start: 0, len: 0, }; MAX_PROPERTIES], edges: [Edge { from: 0, to: 0 }; MAX_EDGES], nodes_len: 0, properties_len: 0, edges_len: 0, strings_start: 0, strings_end: 0, } } fn value<'a>(&self, blob: &'a [u8], property: Property) -> Result<&'a [u8], Error> { range(blob, property.start as usize, property.len as usize) } fn property<'a>( &self, blob: &'a [u8], node: usize, name: &[u8], ) -> Result, Error> { for property in &self.properties[..self.properties_len] { if property.node as usize == node && self.property_name(blob, *property)? == name { return Ok(Some((*property, self.value(blob, *property)?))); } } Ok(None) } fn property_name<'a>(&self, blob: &'a [u8], property: Property) -> Result<&'a [u8], Error> { let strings = blob .get(self.strings_start..self.strings_end) .ok_or(Error::Invalid)?; nul(strings .get(property.name_offset as usize..) .ok_or(Error::Invalid)?) } fn handle_node(&self, handle: u32) -> Result { if handle == 0 || handle == u32::MAX { return Err(Error::InvalidPhandle); } self.nodes[..self.nodes_len] .iter() .position(|node| node.phandle == handle) .ok_or(Error::MissingPhandle) } fn references(&mut self, blob: &[u8], limits: Limits) -> Result<(), Error> { for index in 0..self.properties_len { let property = self.properties[index]; let name = self.property_name(blob, property)?; let cells = match name { b"memory-region" | b"interrupt-parent" | b"msi-parent" => Some(None), b"clocks" => Some(Some(b"#clock-cells".as_slice())), b"resets" => Some(Some(b"#reset-cells".as_slice())), b"iommus" => Some(Some(b"#iommu-cells".as_slice())), b"interrupts-extended" => Some(Some(b"#interrupt-cells".as_slice())), _ => None, }; let Some(cells) = cells else { continue }; let value = self.value(blob, property)?; if value.is_empty() || value.len() % 4 != 0 || name == b"interrupt-parent" && value.len() != 4 { return Err(Error::InvalidPhandle); } let mut cursor = 0; while cursor < value.len() { let target = self.handle_node(be32(value, cursor)?)?; cursor += 4; if let Some(cell_name) = cells { let (_, count) = self .property(blob, target, cell_name)? .ok_or(Error::InvalidPhandle)?; if count.len() != 4 { return Err(Error::InvalidPhandle); } let count = be32(count, 0)? as usize; if count > 16 { return Err(Error::InvalidPhandle); } cursor = cursor.checked_add(count * 4).ok_or(Error::InvalidPhandle)?; if cursor > value.len() { return Err(Error::InvalidPhandle); } } if self.edges_len == limits.references { return Err(Error::ReferenceLimit); } self.edges[self.edges_len] = Edge { from: property.node, to: target as u16, }; self.edges_len += 1; } } // Kahn's bounded topological walk detects self and longer reference // cycles without recursive calls or potentially unbounded following. let mut incoming = [0u16; MAX_NODES]; for edge in &self.edges[..self.edges_len] { incoming[edge.to as usize] += 1; } let mut queue = [0u16; MAX_NODES]; let mut len = 0; for (index, degree) in incoming[..self.nodes_len].iter().enumerate() { if *degree == 0 { queue[len] = index as u16; len += 1; } } let mut cursor = 0; while cursor < len { let node = queue[cursor]; cursor += 1; for edge in &self.edges[..self.edges_len] { if edge.from == node { incoming[edge.to as usize] -= 1; if incoming[edge.to as usize] == 0 { queue[len] = edge.to; len += 1; } } } } if len != self.nodes_len { return Err(Error::PhandleCycle); } Ok(()) } } impl Default for Workspace { fn default() -> Self { Self::new() } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TreeInfo { pub blob_bytes: usize, pub nodes: usize, pub depth: usize, pub properties: usize, pub references: usize, } fn be32(blob: &[u8], at: usize) -> Result { Ok(u32::from_be_bytes( range(blob, at, 4)?.try_into().map_err(|_| Error::Invalid)?, )) } fn be64(blob: &[u8], at: usize) -> Result { Ok(u64::from_be_bytes( range(blob, at, 8)?.try_into().map_err(|_| Error::Invalid)?, )) } fn range(blob: &[u8], at: usize, len: usize) -> Result<&[u8], Error> { blob.get(at..at.checked_add(len).ok_or(Error::Invalid)?) .ok_or(Error::Invalid) } fn nul(bytes: &[u8]) -> Result<&[u8], Error> { Ok(&bytes[..bytes .iter() .position(|byte| *byte == 0) .ok_or(Error::Invalid)?]) } fn align4(value: usize) -> Result { value .checked_add(3) .map(|value| value & !3) .ok_or(Error::Invalid) } fn overlaps(a: usize, a_end: usize, b: usize, b_end: usize) -> bool { a < b_end && b < a_end } pub fn inspect(blob: &[u8], limits: Limits, scratch: &mut Workspace) -> Result { limits.validate()?; if blob.len() < 40 || be32(blob, 0)? != 0xd00d_feed { return Err(Error::Invalid); } let total = be32(blob, 4)? as usize; if false { return Err(Error::BlobLimit); } if total < 40 || total > blob.len() || be32(blob, 20)? != 17 || be32(blob, 24)? > 17 { return Err(Error::Invalid); } let blob = &blob[..total]; let structure_start = be32(blob, 8)? as usize; let strings_start = be32(blob, 12)? as usize; let reserve_start = be32(blob, 16)? as usize; let structure_len = be32(blob, 36)? as usize; let strings_len = be32(blob, 32)? as usize; range(blob, structure_start, structure_len)?; range(blob, strings_start, strings_len)?; if structure_start < 40 || !structure_start.is_multiple_of(4) || !structure_len.is_multiple_of(4) || strings_start < 40 || reserve_start < 40 || !reserve_start.is_multiple_of(8) || overlaps( structure_start, structure_start + structure_len, strings_start, strings_start + strings_len, ) { return Err(Error::Invalid); } let mut reserve_end = reserve_start; let mut reservation_count = 0; loop { let start = be64(blob, reserve_end)?; let bytes = be64(blob, reserve_end + 8)?; reserve_end += 16; if start == 0 && bytes == 0 { break; } if bytes == 0 || start.checked_add(bytes).is_none() { return Err(Error::Invalid); } reservation_count += 1; if reservation_count > MAX_RESERVATIONS { return Err(Error::ReservationLimit); } } if overlaps( reserve_start, reserve_end, structure_start, structure_start + structure_len, ) || overlaps( reserve_start, reserve_end, strings_start, strings_start + strings_len, ) { return Err(Error::Invalid); } scratch.nodes_len = 0; scratch.properties_len = 0; scratch.edges_len = 0; scratch.strings_start = strings_start; scratch.strings_end = strings_start + strings_len; let mut stack = [0usize; MAX_DEPTH]; let mut depth = 0; let mut maximum_depth = 0; let mut cursor = structure_start; let structure_end = structure_start + structure_len; loop { if cursor >= structure_end { return Err(Error::Invalid); } let token = be32(blob, cursor)?; cursor += 4; match token { 1 => { if depth == limits.depth { return Err(Error::DepthLimit); } if scratch.nodes_len == limits.nodes { return Err(Error::NodeLimit); } let name = nul(blob.get(cursor..structure_end).ok_or(Error::Invalid)?)?; if depth == 0 && (!name.is_empty() || scratch.nodes_len != 0) { return Err(Error::Invalid); } let index = scratch.nodes_len; let parent = if depth == 0 { u16::MAX } else { stack[depth - 1] as u16 }; if depth > 0 { scratch.nodes[parent as usize].children_started = true; } scratch.nodes[index] = Node { parent, depth: (depth + 1) as u8, name_start: cursor as u32, name_len: name.len() as u32, phandle: 0, children_started: false, }; scratch.nodes_len += 1; stack[depth] = index; depth += 1; maximum_depth = maximum_depth.max(depth); cursor = align4(cursor + name.len() + 1)?; } 2 => { if depth == 0 { return Err(Error::Invalid); } depth -= 1; } 3 => { if depth == 0 { return Err(Error::Invalid); } let length = be32(blob, cursor)? as usize; let offset = be32(blob, cursor + 4)?; cursor += 8; if cursor.checked_add(length).ok_or(Error::Invalid)? > structure_end { return Err(Error::Invalid); } let property = Property { node: stack[depth - 1] as u16, name_offset: offset, start: cursor as u32, len: length as u32, }; let node = property.node as usize; if scratch.nodes[node].children_started { return Err(Error::Invalid); } let name = scratch.property_name(blob, property)?; if name.is_empty() || scratch.property(blob, node, name)?.is_some() { return Err(Error::Invalid); } if scratch.properties_len == limits.properties { return Err(Error::PropertyLimit); } if name == b"phandle" || name == b"linux,phandle" { if length != 4 { return Err(Error::InvalidPhandle); } let handle = be32(blob, cursor)?; if handle == 0 || handle == u32::MAX { return Err(Error::InvalidPhandle); } if scratch.nodes[node].phandle != 0 && scratch.nodes[node].phandle != handle || scratch.nodes[..scratch.nodes_len] .iter() .enumerate() .any(|(i, n)| i != node && n.phandle == handle) { return Err(Error::InvalidPhandle); } scratch.nodes[node].phandle = handle; } scratch.properties[scratch.properties_len] = property; scratch.properties_len += 1; cursor = align4(cursor + length)?; } 4 => {} 9 if depth == 0 && scratch.nodes_len != 0 && cursor == structure_end => break, _ => return Err(Error::Invalid), } if cursor > structure_end { return Err(Error::Invalid); } } scratch.references(blob, limits)?; Ok(TreeInfo { blob_bytes: total, nodes: scratch.nodes_len, depth: maximum_depth, properties: scratch.properties_len, references: scratch.edges_len, }) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DeviceRecord { pub region: Region, pub node: u16, pub reg_property_offset: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Devices { pub uart: DeviceRecord, pub gic_distributor: DeviceRecord, pub gic_redistributor: DeviceRecord, pub timer_ppi: u32, pub timer_node: u16, pub timer_property_offset: u32, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Platform { pub tree: TreeInfo, pub devices: Devices, pub banks: RamBanks, reservations: [Region; MAX_RESERVATIONS], reservations_len: usize, } impl Platform { pub fn reservations(&self) -> &[Region] { &self.reservations[..self.reservations_len] } } fn string_list_has(list: &[u8], target: &[u8]) -> Result { if list.last() != Some(&0) { return Err(Error::Invalid); } Ok(list.split(|byte| *byte == 0).any(|value| value == target)) } fn cell_count(blob: &[u8], scratch: &Workspace, node: usize, name: &[u8]) -> Result { let (_, value) = scratch .property(blob, node, name)? .ok_or(Error::Unsupported)?; if value.len() != 4 { return Err(Error::Invalid); } let count = be32(value, 0)? as usize; if !(1..=2).contains(&count) { return Err(Error::Unsupported); } Ok(count) } fn cells(bytes: &[u8], count: usize) -> Result { match count { 1 => Ok(be32(bytes, 0)? as u64), 2 => be64(bytes, 0), _ => Err(Error::Unsupported), } } fn regions( bytes: &[u8], address_cells: usize, size_cells: usize, mut consume: impl FnMut(Region) -> Result<(), Error>, ) -> Result<(), Error> { let stride = (address_cells + size_cells) * 4; if bytes.is_empty() || !bytes.len().is_multiple_of(stride) { return Err(Error::Invalid); } for pair in bytes.chunks_exact(stride) { let start = cells(pair, address_cells)?; let size = cells(&pair[address_cells * 4..], size_cells)?; let end = start .checked_add(size) .filter(|end| *end > start) .ok_or(Error::Invalid)?; consume(Region { start, end })?; } Ok(()) } pub fn discover(blob: &[u8], limits: Limits, scratch: &mut Workspace) -> Result { let tree = inspect(blob, limits, scratch)?; let address_cells = cell_count(blob, scratch, 0, b"#address-cells")?; let size_cells = cell_count(blob, scratch, 0, b"#size-cells")?; let mut uart = None; let mut gic = None; let mut timer = None; let mut banks = RamBanks::empty(); let mut reservations = [Region::EMPTY; MAX_RESERVATIONS]; let mut reservations_len = 0; let mut add_reservation = |region| { if reservations_len == MAX_RESERVATIONS { return Err(Error::ReservationLimit); } reservations[reservations_len] = region; reservations_len += 1; Ok(()) }; let mut cursor = be32(blob, 16)? as usize; loop { let start = be64(blob, cursor)?; let size = be64(blob, cursor + 8)?; cursor += 16; if start == 0 && size == 0 { break; } add_reservation(Region { start, end: start.checked_add(size).ok_or(Error::Invalid)?, })?; } for node_index in 1..scratch.nodes_len { let node = scratch.nodes[node_index]; let mut ancestor = node_index; let mut enabled = true; loop { if scratch .property(blob, ancestor, b"status")? .is_some_and(|(_, value)| value != b"okay\0" && value != b"ok\0") { enabled = false; } let parent = scratch.nodes[ancestor].parent; if parent == u16::MAX { break; } ancestor = parent as usize; } if !enabled { continue; } if node.depth == 3 { let parent = scratch.nodes[node.parent as usize]; if range(blob, parent.name_start as usize, parent.name_len as usize)? == b"reserved-memory" { if scratch .property(blob, node.parent as usize, b"ranges")? .is_none_or(|(_, value)| !value.is_empty()) || cell_count(blob, scratch, node.parent as usize, b"#address-cells")? != address_cells || cell_count(blob, scratch, node.parent as usize, b"#size-cells")? != size_cells || scratch.property(blob, node_index, b"size")?.is_some() || scratch .property(blob, node_index, b"memory-region")? .is_some() { return Err(Error::Unsupported); } let (_, value) = scratch .property(blob, node_index, b"reg")? .ok_or(Error::Invalid)?; regions(value, address_cells, size_cells, &mut add_reservation)?; } } if node.depth != 2 { continue; } let compatible = scratch .property(blob, node_index, b"compatible")? .map(|(_, value)| value); let name = range(blob, node.name_start as usize, node.name_len as usize)?; let is_memory = scratch .property(blob, node_index, b"device_type")? .is_some_and(|(_, v)| v == b"memory\0") || name.starts_with(b"memory@"); let is_uart = compatible .map(|value| string_list_has(value, b"arm,pl011")) .transpose()? .unwrap_or(false); let is_gic = compatible .map(|value| string_list_has(value, b"arm,gic-v3")) .transpose()? .unwrap_or(false); let is_timer = compatible .map(|value| string_list_has(value, b"arm,armv8-timer")) .transpose()? .unwrap_or(false); if is_memory || is_uart || is_gic { let (property, values) = scratch .property(blob, node_index, b"reg")? .ok_or(Error::MissingDevice)?; let mut records = [Region::EMPTY; 2]; let mut len = 0; regions(values, address_cells, size_cells, |region| { if is_memory { banks.insert(region).map_err(|_| Error::Ram)?; } else { if len == 2 { return Err(Error::Unsupported); } records[len] = region; len += 1; } Ok(()) })?; let record = |region| DeviceRecord { region, node: node_index as u16, reg_property_offset: property.start, }; if is_uart { if uart.is_some() { return Err(Error::AmbiguousDevice); } if len != 1 || records[0].bytes() < 0x1000 { return Err(Error::Unsupported); } uart = Some(record(records[0])); } if is_gic { if gic.is_some() { return Err(Error::AmbiguousDevice); } if len != 2 || records[0].bytes() < 0x10000 || records[1].bytes() < 0x20000 || scratch .property(blob, node_index, b"interrupt-controller")? .is_none_or(|(_, v)| !v.is_empty()) || scratch .property(blob, node_index, b"#interrupt-cells")? .is_none_or(|(_, v)| v != 3u32.to_be_bytes()) || records[0].overlaps(records[1]) { return Err(Error::Unsupported); } let distributor = record(records[0]); let mut redistributor = record(records[1]); redistributor.reg_property_offset += ((address_cells + size_cells) * 4) as u32; gic = Some((distributor, redistributor)); } } if is_timer { if timer.is_some() { return Err(Error::AmbiguousDevice); } let (property, value) = scratch .property(blob, node_index, b"interrupts")? .ok_or(Error::MissingDevice)?; if value.len() != 48 || be32(value, 12)? != 1 || be32(value, 16)? >= 16 || be32(value, 20)? != 4 { return Err(Error::Unsupported); } timer = Some((16 + be32(value, 16)?, node_index as u16, property.start)); } } if banks.as_slice().is_empty() { return Err(Error::Ram); } let uart = uart.ok_or(Error::MissingDevice)?; let (gic_distributor, gic_redistributor) = gic.ok_or(Error::MissingDevice)?; let (timer_ppi, timer_node, timer_property_offset) = timer.ok_or(Error::MissingDevice)?; for device in [uart, gic_distributor, gic_redistributor] { if banks .as_slice() .iter() .any(|bank| bank.overlaps(device.region)) { return Err(Error::Unsupported); } } if uart.region.overlaps(gic_distributor.region) || uart.region.overlaps(gic_redistributor.region) { return Err(Error::Unsupported); } Ok(Platform { tree, devices: Devices { uart, gic_distributor, gic_redistributor, timer_ppi, timer_node, timer_property_offset, }, banks, reservations, reservations_len, }) } #[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] pub struct PlatformSlot { published: Option, } impl PlatformSlot { pub const fn new() -> Self { Self { published: None } } pub const fn published(&self) -> Option<&Platform> { self.published.as_ref() } pub fn admit( &mut self, blob: &[u8], limits: Limits, scratch: &mut Workspace, ) -> Result<(), Error> { if self.published.is_some() { return Err(Error::AmbiguousDevice); } let platform = discover(blob, limits, scratch)?; self.published = Some(platform); Ok(()) } } /// Read a scalar fixture control from the already bounded /chosen node. The /// caller must use scratch from inspection of this same immutable blob. pub fn chosen_u32(blob: &[u8], scratch: &Workspace, name: &[u8]) -> Result, Error> { for (index, node) in scratch.nodes[..scratch.nodes_len].iter().enumerate() { if node.depth == 2 && range(blob, node.name_start as usize, node.name_len as usize)? == b"chosen" { if let Some((_, value)) = scratch.property(blob, index, name)? { if value.len() != 4 { return Err(Error::Invalid); } return Ok(Some(be32(value, 0)?)); } return Ok(None); } } Ok(None) }