Skip to main content

bootfs/
bootfs.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Error, anyhow, bail};
6use mapped_vmo::Mapping;
7use std::fmt::Write as _;
8use std::mem::size_of;
9use std::slice;
10use zbi::zbi_format::{
11    ZBI_CONTAINER_MAGIC, ZBI_FLAGS_CRC32, ZBI_FLAGS_STORAGE_COMPRESSED, ZBI_ITEM_MAGIC,
12    ZBI_ITEM_NO_CRC32,
13};
14use zbi::{ZBI_ALIGNMENT_USIZE, ZbiFlags, ZbiHeader, ZbiType};
15use zerocopy::IntoBytes as _;
16use zx::{Name, VmarFlags, Vmo};
17use zx_libc::sanitizer::Log;
18
19const HEADER_SIZE: u64 = size_of::<ZbiHeader>() as u64;
20
21/// Header and VMO payload offset of an item in a ZBI VMO.
22#[derive(Debug, Clone, Copy)]
23pub struct ZbiItem {
24    /// The validated ZBI item header.
25    pub header: ZbiHeader,
26    /// Byte offset of the item's payload within the ZBI VMO.
27    pub payload_offset: u64,
28}
29
30fn check_item_header(header: &ZbiHeader) -> Result<(), Error> {
31    if header.magic != ZBI_ITEM_MAGIC {
32        bail!("ZBI header has bad magic");
33    }
34    if !header.get_flags().contains(ZbiFlags::VERSION) {
35        bail!("ZBI header has bad version");
36    }
37    // Per <lib/zbi-format/zbi.h>, when ZBI_FLAGS_CRC32 is not set, the crc32 field must contain
38    // the sentinel value ZBI_ITEM_NO_CRC32.
39    if !header.get_flags().contains(ZbiFlags::CRC32) && header.crc32 != ZBI_ITEM_NO_CRC32 {
40        bail!("ZBI header without CRC32 flag must have ZBI_ITEM_NO_CRC32");
41    }
42    Ok(())
43}
44
45/// Iterator over items in a ZBI VMO that reads item headers via `zx_vmo_read`
46/// without mapping the entire ZBI VMO into memory.
47#[derive(Debug)]
48pub struct ZbiItemIterator<'a> {
49    zbi_vmo: &'a Vmo,
50    offset: u64,
51    container_end: u64,
52    failed: bool,
53}
54
55impl<'a> ZbiItemIterator<'a> {
56    /// Validates the ZBI container header in `zbi_vmo` and returns an iterator over its items
57    /// using `zx_vmo_read` rather than mapping the VMO.
58    pub fn new(zbi_vmo: &'a Vmo) -> Result<Self, Error> {
59        let vmo_size = zbi_vmo.get_size()?;
60        if vmo_size < HEADER_SIZE {
61            bail!("ZBI VMO is too small for container header");
62        }
63        let header: ZbiHeader = zbi_vmo.read_to_object(0)?;
64        if header.type_ != ZbiType::Container as u32 {
65            bail!("ZBI container header has bad type: {:#x}", header.type_);
66        }
67        if header.extra != ZBI_CONTAINER_MAGIC {
68            bail!("ZBI container header has bad magic");
69        }
70        if (header.flags & ZBI_FLAGS_CRC32) != 0 {
71            bail!("ZBI container header must not have CRC32 flag");
72        }
73        if (header.length as usize) % ZBI_ALIGNMENT_USIZE != 0 {
74            bail!("ZBI container length is not aligned");
75        }
76        check_item_header(&header)?;
77        let container_end = HEADER_SIZE
78            .checked_add(u64::from(header.length))
79            .ok_or_else(|| anyhow!("ZBI container length overflow"))?;
80        if container_end > vmo_size {
81            bail!("ZBI container is truncated");
82        }
83        Ok(Self { zbi_vmo, offset: HEADER_SIZE, container_end, failed: false })
84    }
85}
86
87impl Iterator for ZbiItemIterator<'_> {
88    type Item = Result<ZbiItem, Error>;
89
90    fn next(&mut self) -> Option<Self::Item> {
91        if self.failed || self.offset == self.container_end {
92            return None;
93        }
94        let step = (|| -> Result<ZbiItem, Error> {
95            let payload_offset = self
96                .offset
97                .checked_add(HEADER_SIZE)
98                .ok_or_else(|| anyhow!("ZBI item header offset overflow"))?;
99            if payload_offset > self.container_end {
100                bail!("ZBI item header is truncated");
101            }
102            let header: ZbiHeader = self.zbi_vmo.read_to_object(self.offset)?;
103            check_item_header(&header)?;
104            let payload_end = payload_offset
105                .checked_add(u64::from(header.length))
106                .ok_or_else(|| anyhow!("ZBI item length overflow"))?;
107            if payload_end > self.container_end {
108                bail!("ZBI item payload is truncated");
109            }
110            let next_offset = payload_end
111                .checked_next_multiple_of(ZBI_ALIGNMENT_USIZE as u64)
112                .ok_or_else(|| anyhow!("ZBI item alignment overflow"))?;
113            if next_offset > self.container_end {
114                bail!("ZBI item alignment padding is truncated");
115            }
116            self.offset = next_offset;
117            Ok(ZbiItem { header, payload_offset })
118        })();
119        if step.is_err() {
120            self.failed = true;
121        }
122        Some(step)
123    }
124}
125
126/// Extracts the BOOTFS VMO from the ZBI VMO, mapping only the pages containing the
127/// `STORAGE_BOOTFS` item payload and decompressing it if needed.
128pub fn get_bootfs_vmo(zbi_vmo: &Vmo, check_crc: bool, log: &mut Log) -> Result<Vmo, Error> {
129    let mut bootfs_item = None;
130    for item in ZbiItemIterator::new(zbi_vmo)? {
131        let item = item?;
132        if item.header.type_ == ZbiType::StorageBootFs as u32 {
133            bootfs_item = Some(item);
134            break;
135        }
136    }
137    let bootfs_item = bootfs_item.ok_or_else(|| anyhow!("StorageBootFs item not found in ZBI"))?;
138
139    let payload_len = bootfs_item.header.length as usize;
140    if payload_len == 0 {
141        bail!("StorageBootFs item has empty payload");
142    }
143    let page_size = u64::from(zx::system_get_page_size());
144    let map_offset = bootfs_item.payload_offset & !(page_size - 1);
145    let offset_in_mapping = (bootfs_item.payload_offset - map_offset) as usize;
146    let map_size = (offset_in_mapping as u64)
147        .checked_add(payload_len as u64)
148        .and_then(|end| end.checked_next_multiple_of(page_size))
149        .ok_or_else(|| anyhow!("BOOTFS mapping size overflow"))?;
150    let mut payload_mapping = Mapping::create_from_vmo_with_offset(
151        zbi_vmo,
152        map_offset,
153        map_size as usize,
154        VmarFlags::PERM_READ | VmarFlags::MAP_RANGE,
155    )?;
156    let (payload_ptr, payload_map_len) = payload_mapping.as_ptr_len();
157    // SAFETY: `zbi_vmo` is not modified while `get_bootfs_vmo` is running, and `payload_mapping`
158    // keeps `payload_ptr..payload_ptr + payload_map_len` mapped read-only for the duration of this
159    // function.
160    let payload = unsafe {
161        &slice::from_raw_parts(payload_ptr.cast_const(), payload_map_len)
162            [offset_in_mapping..offset_in_mapping + payload_len]
163    };
164
165    if check_crc && (bootfs_item.header.flags & ZBI_FLAGS_CRC32) != 0 {
166        writeln!(log, "Checking BOOTFS item CRC32 {:#x}...", bootfs_item.header.crc32)?;
167        let mut header_without_crc = bootfs_item.header;
168        header_without_crc.crc32 = 0;
169        let mut hasher = crc32fast::Hasher::new();
170        hasher.update(header_without_crc.as_bytes());
171        hasher.update(payload);
172        if hasher.finalize() == bootfs_item.header.crc32 {
173            writeln!(log, "BOOTFS payload matches item CRC32.")?;
174        } else {
175            writeln!(log, "*** BOOTFS payload DOES NOT MATCH item CRC32! ***")?;
176            bail!("BOOTFS payload CRC32 mismatch");
177        }
178    }
179
180    let is_compressed = (bootfs_item.header.flags & ZBI_FLAGS_STORAGE_COMPRESSED) != 0;
181    let bootfs_vmo = if is_compressed {
182        let uncompressed_size = bootfs_item.header.extra as usize;
183        if uncompressed_size == 0 {
184            bail!("Compressed BOOTFS item has uncompressed size 0");
185        }
186        let vmo = Vmo::create(uncompressed_size as u64)?;
187        // Pre-commit pages and map with MAP_RANGE so zstd decompression writes into pre-populated
188        // page tables instead of taking individual page faults on each page of the output VMO.
189        vmo.op_range(zx::VmoOp::COMMIT, 0, uncompressed_size as u64)?;
190        let mut mapping = Mapping::create_from_vmo(
191            &vmo,
192            uncompressed_size,
193            VmarFlags::PERM_READ | VmarFlags::PERM_WRITE | VmarFlags::MAP_RANGE,
194        )?;
195        let (ptr, len) = mapping.as_ptr_len();
196        // SAFETY: `vmo` was just created above and is not shared with any other thread or process
197        // during decompression, and `mapping` keeps `ptr..ptr + len` mapped readable and writable
198        // for the duration of this block.
199        let buffer = unsafe { slice::from_raw_parts_mut(ptr, len) };
200
201        zstd_safe::decompress(buffer, payload)
202            .map_err(|e| anyhow!("zstd decompression failed with error code: {}", e))?;
203        vmo
204    } else {
205        let vmo = Vmo::create(payload.len() as u64)?;
206        vmo.write(payload, 0)?;
207        vmo
208    };
209
210    bootfs_vmo.set_name(&Name::new("uncompressed-bootfs")?)?;
211    Ok(bootfs_vmo)
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use zbi::{ZbiContainer, align_buffer};
218
219    fn create_test_zbi_vmo_with_prefix(
220        prefix_items: &[(ZbiType, &[u8])],
221        payload: &[u8],
222        compressed: bool,
223        uncompressed_len_override: Option<u32>,
224        with_crc: bool,
225        corrupt_crc: bool,
226    ) -> Vmo {
227        let mut raw_buf = vec![0u8; 32768];
228        let aligned = align_buffer(&mut raw_buf[..]).unwrap();
229        let size = {
230            let mut container = ZbiContainer::new(&mut aligned[..]).unwrap();
231            for &(ty, data) in prefix_items {
232                container.create_entry_with_payload(ty, 0, ZbiFlags::default(), data).unwrap();
233            }
234            let extra = uncompressed_len_override.unwrap_or(payload.len() as u32);
235            if compressed {
236                let mut compressed_buf = vec![0u8; zstd_safe::compress_bound(payload.len())];
237                let compressed_len = zstd_safe::compress(&mut compressed_buf, payload, 1).unwrap();
238                container
239                    .create_entry_with_payload(
240                        ZbiType::StorageBootFs,
241                        extra,
242                        ZbiFlags::default(),
243                        &compressed_buf[..compressed_len],
244                    )
245                    .unwrap();
246                container.iter_mut().last().unwrap().header.flags |= ZBI_FLAGS_STORAGE_COMPRESSED;
247            } else {
248                container
249                    .create_entry_with_payload(
250                        ZbiType::StorageBootFs,
251                        extra,
252                        ZbiFlags::default(),
253                        payload,
254                    )
255                    .unwrap();
256            }
257            if with_crc {
258                let mut item = container.iter_mut().last().unwrap();
259                item.header.flags |= ZBI_FLAGS_CRC32;
260                let mut header_without_crc = *item.header;
261                header_without_crc.crc32 = 0;
262                let mut hasher = crc32fast::Hasher::new();
263                hasher.update(header_without_crc.as_bytes());
264                hasher.update(item.payload.as_bytes());
265                let crc = hasher.finalize();
266                item.header.crc32 = if corrupt_crc { crc ^ 0xffff_ffff } else { crc };
267            }
268            container.container_size().unwrap()
269        };
270        let zbi_bytes = &aligned[..size];
271        let vmo = Vmo::create(zbi_bytes.len() as u64).unwrap();
272        vmo.write(zbi_bytes, 0).unwrap();
273        vmo
274    }
275
276    fn create_test_zbi_vmo(
277        payload: &[u8],
278        compressed: bool,
279        uncompressed_len_override: Option<u32>,
280        with_crc: bool,
281        corrupt_crc: bool,
282    ) -> Vmo {
283        create_test_zbi_vmo_with_prefix(
284            &[],
285            payload,
286            compressed,
287            uncompressed_len_override,
288            with_crc,
289            corrupt_crc,
290        )
291    }
292
293    #[fuchsia::test]
294    fn test_get_bootfs_vmo_uncompressed() {
295        let payload = b"uncompressed bootfs payload data";
296        let zbi_vmo = create_test_zbi_vmo(payload, false, None, false, false);
297        let mut log = Log::new();
298
299        let bootfs_vmo = get_bootfs_vmo(&zbi_vmo, false, &mut log).unwrap();
300
301        assert_eq!(zbi_vmo.info().unwrap().num_mappings, 0);
302        assert_eq!(zbi_vmo.info().unwrap().num_children, 0);
303        assert_eq!(bootfs_vmo.get_name().unwrap(), "uncompressed-bootfs");
304        let mut read_back = vec![0u8; payload.len()];
305        bootfs_vmo.read(&mut read_back, 0).unwrap();
306        assert_eq!(&read_back, payload);
307    }
308
309    #[fuchsia::test]
310    fn test_get_bootfs_vmo_compressed_with_crc() {
311        let payload = vec![0x5au8; 8192];
312        let zbi_vmo = create_test_zbi_vmo(&payload, true, None, true, false);
313        let mut log = Log::new();
314
315        let bootfs_vmo = get_bootfs_vmo(&zbi_vmo, true, &mut log).unwrap();
316
317        assert_eq!(zbi_vmo.info().unwrap().num_mappings, 0);
318        assert_eq!(zbi_vmo.info().unwrap().num_children, 0);
319        assert_eq!(bootfs_vmo.info().unwrap().num_mappings, 0);
320        assert_eq!(bootfs_vmo.get_name().unwrap(), "uncompressed-bootfs");
321        let mut read_back = vec![0u8; payload.len()];
322        bootfs_vmo.read(&mut read_back, 0).unwrap();
323        assert_eq!(read_back, payload);
324    }
325
326    #[fuchsia::test]
327    fn test_get_bootfs_vmo_after_non_page_aligned_prefix_items() {
328        let dummy_kernel = vec![0xa5u8; 5000];
329        let cmdline = b"userboot.next=bin/test";
330        let payload = vec![0x3cu8; 6000];
331        let zbi_vmo = create_test_zbi_vmo_with_prefix(
332            &[(ZbiType::Discard, &dummy_kernel), (ZbiType::CmdLine, cmdline)],
333            &payload,
334            true,
335            None,
336            true,
337            false,
338        );
339        let mut log = Log::new();
340
341        let items = ZbiItemIterator::new(&zbi_vmo).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
342        assert_eq!(items.len(), 3);
343        assert_eq!(items[1].header.type_, ZbiType::CmdLine as u32);
344        let read_cmdline = zbi_vmo
345            .read_to_vec::<u8>(items[1].payload_offset, items[1].header.length.into())
346            .unwrap();
347        assert_eq!(&read_cmdline, cmdline);
348
349        let bootfs_vmo = get_bootfs_vmo(&zbi_vmo, true, &mut log).unwrap();
350        assert_eq!(zbi_vmo.info().unwrap().num_mappings, 0);
351        assert_eq!(zbi_vmo.info().unwrap().num_children, 0);
352        let mut read_back = vec![0u8; payload.len()];
353        bootfs_vmo.read(&mut read_back, 0).unwrap();
354        assert_eq!(read_back, payload);
355    }
356
357    #[fuchsia::test]
358    fn test_get_bootfs_vmo_crc_mismatch() {
359        let payload = b"bootfs payload with bad crc";
360        let zbi_vmo = create_test_zbi_vmo(payload, false, None, true, true);
361        let mut log = Log::new();
362
363        let res = get_bootfs_vmo(&zbi_vmo, true, &mut log);
364        assert!(res.is_err());
365    }
366
367    #[fuchsia::test]
368    fn test_get_bootfs_vmo_compressed_zero_size() {
369        let payload = b"non-empty";
370        let zbi_vmo = create_test_zbi_vmo(payload, true, Some(0), false, false);
371        let mut log = Log::new();
372
373        let res = get_bootfs_vmo(&zbi_vmo, false, &mut log);
374        assert!(res.is_err());
375    }
376
377    #[fuchsia::test]
378    fn test_zbi_container_header_validation() {
379        let payload = b"bootfs payload";
380        let zbi_vmo = create_test_zbi_vmo(payload, false, None, false, false);
381        let orig_header: ZbiHeader = zbi_vmo.read_to_object(0).unwrap();
382
383        // Container header must not have ZBI_FLAGS_CRC32 set.
384        let mut header_with_crc = orig_header;
385        header_with_crc.flags |= ZBI_FLAGS_CRC32;
386        header_with_crc.crc32 = 0x1234_5678;
387        zbi_vmo.write(header_with_crc.as_bytes(), 0).unwrap();
388        assert!(ZbiItemIterator::new(&zbi_vmo).is_err());
389
390        // Container header length must be a multiple of ZBI_ALIGNMENT.
391        let mut header_unaligned_len = orig_header;
392        header_unaligned_len.length -= 1;
393        zbi_vmo.write(header_unaligned_len.as_bytes(), 0).unwrap();
394        assert!(ZbiItemIterator::new(&zbi_vmo).is_err());
395    }
396}