Skip to main content

fuchsia_bootfs/
lib.rs

1// Copyright 2019 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
5pub mod bootfs;
6
7use bootfs::{
8    ZBI_BOOTFS_MAGIC, ZBI_BOOTFS_MAX_NAME_LEN, ZBI_BOOTFS_PAGE_SIZE, zbi_bootfs_dirent_t,
9    zbi_bootfs_header_t,
10};
11use std::ffi::CStr;
12use std::mem::size_of;
13use std::str::Utf8Error;
14use thiserror::Error;
15use zerocopy::Ref;
16
17const ZBI_BOOTFS_HEADER_SIZE: usize = size_of::<zbi_bootfs_header_t>();
18
19pub fn zbi_bootfs_page_align(size: u32) -> u32 {
20    size.wrapping_add(ZBI_BOOTFS_PAGE_SIZE - 1) & !(ZBI_BOOTFS_PAGE_SIZE - 1)
21}
22
23pub fn zbi_bootfs_is_aligned(size: u32) -> bool {
24    size % ZBI_BOOTFS_PAGE_SIZE == 0
25}
26
27#[derive(Debug, Error, Eq, PartialEq)]
28pub enum BootfsParserError {
29    #[error("Invalid magic for bootfs payload")]
30    BadMagic,
31
32    #[error("Directory entry {} exceeds available dirsize of {}", entry_index, dirsize)]
33    DirEntryTooBig { entry_index: u32, dirsize: u32 },
34
35    #[error("Failed to read payload: {}", status)]
36    FailedToReadPayload { status: zx::Status },
37
38    #[error("Failed to parse bootfs header")]
39    FailedToParseHeader,
40
41    #[error("Failed to parse directory entry")]
42    FailedToParseDirEntry,
43
44    #[error("Failed to read name as UTF-8: {}", cause)]
45    InvalidNameFormat {
46        #[source]
47        cause: Utf8Error,
48    },
49
50    #[error("Failed to find null terminated string for name: {}", cause)]
51    InvalidNameString {
52        #[source]
53        cause: std::ffi::FromBytesWithNulError,
54    },
55
56    #[error(
57        "name_len must be between 1 and {}, found {} for directory entry {}",
58        max_name_len,
59        name_len,
60        entry_index
61    )]
62    InvalidNameLength { name_len: u32, max_name_len: u32, entry_index: u32 },
63
64    #[error("Data offset {} for directory entry {} is not page-aligned", offset, entry_index)]
65    UnalignedDataOffset { offset: u32, entry_index: u32 },
66}
67
68fn parse_dirent_name(name_bytes: &[u8]) -> Result<&str, BootfsParserError> {
69    // Name is stored as an array reference to a block of characters.
70    // Characters must be UTF-8 encoded.
71    // Valid names are terminated with NUL.
72    // We should fail if either of the above conditions are not met.
73    match CStr::from_bytes_with_nul(name_bytes) {
74        Ok(bytes) => bytes.to_str().map_err(|cause| BootfsParserError::InvalidNameFormat { cause }),
75        Err(cause) => Err(BootfsParserError::InvalidNameString { cause }),
76    }
77}
78
79/// Parser for bootfs-formatted structures.
80#[derive(Debug)]
81pub struct BootfsParser {
82    dir_bytes: Vec<u8>,
83}
84
85impl BootfsParser {
86    /// Creates a BootfsParser from an existing VMO.
87    ///
88    /// If `vmo` contains invalid header data, BootfsParserError is returned.
89    pub fn create_from_vmo(vmo: &zx::Vmo) -> Result<BootfsParser, BootfsParserError> {
90        let header: zbi_bootfs_header_t = vmo
91            .read_to_object(0)
92            .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
93        if header.magic.get() == ZBI_BOOTFS_MAGIC {
94            let dirsize = header.dirsize.get() as u64;
95            let vmo_size = vmo
96                .get_size()
97                .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
98            if dirsize > vmo_size.saturating_sub(ZBI_BOOTFS_HEADER_SIZE as u64) {
99                return Err(BootfsParserError::FailedToParseHeader);
100            }
101
102            // Read the entire bootfs directory region in a single VMO read so entry iteration can
103            // parse directory entries from memory without per-entry syscalls or allocations.
104            let dir_bytes = vmo
105                .read_to_vec::<u8>(ZBI_BOOTFS_HEADER_SIZE as u64, dirsize)
106                .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
107            Ok(Self { dir_bytes })
108        } else {
109            Err(BootfsParserError::BadMagic)
110        }
111    }
112
113    pub fn iter<'a>(
114        &'a self,
115        vmo: &'a zx::Vmo,
116    ) -> impl Iterator<Item = Result<BootfsEntry, BootfsParserError>> + 'a {
117        BootfsParserIterator::new(self, vmo)
118    }
119
120    pub fn zero_copy_iter(
121        &self,
122    ) -> impl Iterator<Item = Result<ZeroCopyBootfsEntry<'_>, BootfsParserError>> + '_ {
123        ZeroCopyBootfsParserIterator::new(self)
124    }
125}
126
127#[derive(Debug)]
128pub struct ZeroCopyBootfsEntry<'a> {
129    pub name: &'a str,
130    pub offset: u64,
131    pub size: u64,
132}
133
134#[derive(Debug)]
135struct ZeroCopyBootfsParserIterator<'parser> {
136    dir_bytes: &'parser [u8],
137    entry_index: u32,
138}
139
140impl<'parser> ZeroCopyBootfsParserIterator<'parser> {
141    pub fn new(parser: &'parser BootfsParser) -> Self {
142        Self { dir_bytes: &parser.dir_bytes, entry_index: 0 }
143    }
144
145    fn next_entry(&mut self) -> Result<ZeroCopyBootfsEntry<'parser>, BootfsParserError> {
146        let dirsize = self.dir_bytes.len() as u32;
147        let (dirent, rest) = Ref::<_, zbi_bootfs_dirent_t>::from_prefix(self.dir_bytes)
148            .map_err(|_| BootfsParserError::FailedToParseDirEntry)?;
149
150        let name_len = dirent.name_len.get();
151        if name_len < 1 || name_len > ZBI_BOOTFS_MAX_NAME_LEN {
152            return Err(BootfsParserError::InvalidNameLength {
153                entry_index: self.entry_index,
154                max_name_len: ZBI_BOOTFS_MAX_NAME_LEN,
155                name_len,
156            });
157        }
158
159        // Each directory entry has a variable size of [16, 268] bytes that must be a multiple of 4
160        // bytes. Since zbi_bootfs_dirent_t is 12 bytes (already 4-byte aligned), aligning the entry
161        // to 4 bytes is equivalent to aligning name_len to a multiple of 4.
162        static_assertions::const_assert_eq!(size_of::<zbi_bootfs_dirent_t>(), 12);
163        let padded_name_len = name_len.next_multiple_of(4) as usize;
164        let Some((name_and_padding, rest)) = rest.split_at_checked(padded_name_len) else {
165            return Err(BootfsParserError::DirEntryTooBig {
166                dirsize,
167                entry_index: self.entry_index,
168            });
169        };
170
171        let data_off = dirent.data_off.get();
172        if !zbi_bootfs_is_aligned(data_off) {
173            return Err(BootfsParserError::UnalignedDataOffset {
174                offset: data_off,
175                entry_index: self.entry_index,
176            });
177        }
178
179        let name = parse_dirent_name(&name_and_padding[..(name_len as usize)])?;
180        let offset: u64 = data_off.into();
181        let size: u64 = dirent.data_len.get().into();
182
183        self.dir_bytes = rest;
184        self.entry_index += 1;
185
186        Ok(ZeroCopyBootfsEntry { name, offset, size })
187    }
188}
189
190impl<'parser> Iterator for ZeroCopyBootfsParserIterator<'parser> {
191    type Item = Result<ZeroCopyBootfsEntry<'parser>, BootfsParserError>;
192
193    fn next(&mut self) -> Option<Self::Item> {
194        if self.dir_bytes.is_empty() {
195            return None;
196        }
197
198        let result = self.next_entry();
199        if result.is_err() {
200            self.dir_bytes = &[];
201        }
202        Some(result)
203    }
204}
205
206#[derive(Debug)]
207pub struct BootfsEntry {
208    pub name: String,
209    pub offset: u64,
210    pub size: u64,
211    pub payload: Vec<u8>,
212}
213
214#[derive(Debug)]
215struct BootfsParserIterator<'a> {
216    vmo: &'a zx::Vmo,
217    iter: ZeroCopyBootfsParserIterator<'a>,
218}
219
220impl<'a> BootfsParserIterator<'a> {
221    pub fn new(parser: &'a BootfsParser, vmo: &'a zx::Vmo) -> Self {
222        Self { vmo, iter: ZeroCopyBootfsParserIterator::new(parser) }
223    }
224}
225
226impl Iterator for BootfsParserIterator<'_> {
227    type Item = Result<BootfsEntry, BootfsParserError>;
228
229    fn next(&mut self) -> Option<Self::Item> {
230        let entry = self.iter.next()?;
231        let result = entry.and_then(|zero_copy_entry| {
232            let payload = self
233                .vmo
234                .read_to_vec::<u8>(zero_copy_entry.offset, zero_copy_entry.size)
235                .map_err(|status| BootfsParserError::FailedToReadPayload { status })?;
236            Ok(BootfsEntry {
237                name: zero_copy_entry.name.to_string(),
238                offset: zero_copy_entry.offset,
239                size: zero_copy_entry.size,
240                payload,
241            })
242        });
243        if result.is_err() {
244            self.iter.dir_bytes = &[];
245        }
246        Some(result)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use anyhow::Error;
254    use std::collections::HashMap;
255    use std::fs::File;
256    use std::io::prelude::*;
257    use std::sync::LazyLock;
258
259    const ZBI_BOOTFS_DIRENT_SIZE: usize = size_of::<zbi_bootfs_dirent_t>();
260    static GOLDEN_DIR: &str = "/pkg/data/golden/";
261    static BASIC_BOOTFS_UNCOMPRESSED_FILE: &str = "/pkg/data/basic.bootfs.uncompressed";
262
263    fn read_file_into_hashmap(dir: &str, filename: &str, map: &mut HashMap<String, Vec<u8>>) {
264        let mut file_buffer = Vec::new();
265        let path = format!("{}{}", dir, filename);
266
267        File::open(&path)
268            .unwrap_or_else(|e| panic!("Failed to open file {}: {:?}", &path, e))
269            .read_to_end(&mut file_buffer)
270            .unwrap_or_else(|e| panic!("Failed to read file {}: {:?}", &path, e));
271        map.insert(filename.to_string(), file_buffer);
272    }
273
274    static GOLDEN_FILES: LazyLock<HashMap<String, Vec<u8>>> = LazyLock::new(|| {
275        let mut m = HashMap::new();
276        read_file_into_hashmap(GOLDEN_DIR, "dir/empty", &mut m);
277        read_file_into_hashmap(GOLDEN_DIR, "dir/lorem.txt", &mut m);
278        read_file_into_hashmap(GOLDEN_DIR, "dir/simple-copy.txt", &mut m);
279        read_file_into_hashmap(GOLDEN_DIR, "empty", &mut m);
280        read_file_into_hashmap(GOLDEN_DIR, "random.dat", &mut m);
281        read_file_into_hashmap(GOLDEN_DIR, "simple.txt", &mut m);
282        m
283    });
284
285    fn read_file_to_vmo(path: &str) -> Result<zx::Vmo, Error> {
286        let mut file_buffer = Vec::new();
287        File::open(path)?.read_to_end(&mut file_buffer)?;
288
289        let vmo = zx::Vmo::create(file_buffer.len() as u64)?;
290        vmo.write(&file_buffer, 0)?;
291        Ok(vmo)
292    }
293
294    #[test]
295    fn dirent_from_raw_fails_on_bad_cstring() {
296        // The last byte should be NUL...but it's not for this test.
297        let name_bytes = b"oka";
298        match parse_dirent_name(name_bytes).unwrap_err() {
299            BootfsParserError::InvalidNameString { cause: _cause } => (),
300            _ => panic!("parse_dirent_name did not fail with correct error"),
301        }
302    }
303
304    #[test]
305    fn dirent_from_raw_fails_on_non_utf8_string() {
306        // This is an invalid UTF-8 sequence.
307        let name_bytes = [0xC3, 0x28, b'\0'];
308
309        // Assert that it actually IS an invalid UTF-8 string.
310        assert_eq!(true, String::from_utf8(name_bytes.to_vec()).is_err());
311
312        match parse_dirent_name(&name_bytes).unwrap_err() {
313            BootfsParserError::InvalidNameFormat { cause: _cause } => (),
314            _ => panic!("parse_dirent_name did not fail with correct error"),
315        }
316    }
317
318    #[test]
319    fn create_bootfs_parser() {
320        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
321        assert_eq!(true, BootfsParser::create_from_vmo(&vmo).is_ok());
322    }
323
324    #[test]
325    fn process_basic_bootfs() {
326        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
327        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to read bootfs file");
328
329        let mut files = Box::new(HashMap::new());
330
331        parser.iter(&vmo).for_each(|result| {
332            let result = result.expect("Failed to process bootfs payload");
333            let BootfsEntry { name, payload, .. } = result;
334            files.insert(name, payload);
335        });
336
337        assert_eq!(*GOLDEN_FILES, *files);
338    }
339
340    #[test]
341    fn process_bootfs_zero_copy() {
342        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
343
344        let mut files = Box::new(HashMap::new());
345
346        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to read bootfs file");
347        parser.zero_copy_iter().for_each(|result| {
348            let result = result.expect("Failed to process bootfs payload");
349            let ZeroCopyBootfsEntry { name, offset, size } = result;
350
351            let buffer_size = usize::try_from(size).unwrap();
352            let mut bytes = vec![0; buffer_size];
353            vmo.read(&mut bytes, offset).expect("Failed to read data from the vmo");
354
355            files.insert(name.to_string(), bytes);
356        });
357
358        assert_eq!(*GOLDEN_FILES, *files);
359    }
360
361    #[test]
362    fn process_bootfs_with_invalid_header() {
363        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
364        let new_header = [0; ZBI_BOOTFS_HEADER_SIZE];
365
366        // Wipe the header of a known good bootfs payload.
367        vmo.write(&new_header, 0).expect("Failed to wipe bootfs header");
368
369        match BootfsParser::create_from_vmo(&vmo).unwrap_err() {
370            BootfsParserError::BadMagic => (),
371            _ => panic!("BootfsParser::create_from_vmo did not fail with correct error"),
372        }
373    }
374
375    #[test]
376    fn process_bootfs_with_invalid_direntry() {
377        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
378        let new_header = [0; ZBI_BOOTFS_DIRENT_SIZE];
379
380        // Wipe the first direntry of a known good bootfs payload.
381        // The first direntry starts immediately after the header.
382        vmo.write(&new_header, ZBI_BOOTFS_HEADER_SIZE as u64).expect("Failed to wipe direntry");
383
384        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
385        parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
386            BootfsParserError::InvalidNameLength { entry_index, max_name_len, name_len } => {
387                assert_eq!(0, entry_index);
388                assert_eq!(ZBI_BOOTFS_MAX_NAME_LEN, max_name_len);
389                assert_eq!(0, name_len);
390            }
391            _ => panic!("parser did not fail with correct error"),
392        });
393    }
394
395    #[test]
396    fn process_bootfs_undersized_dirsize() {
397        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
398        let new_header = [(ZBI_BOOTFS_DIRENT_SIZE + 1) as u8, 0, 0, 0];
399
400        // Change dirsize to ZBI_BOOTFS_DIRENT_SIZE+1.
401        // It is the second u32 value in the zbi_bootfs_header_t struct.
402        vmo.write(&new_header, size_of::<u32>() as u64).expect("Failed to change dirsize");
403
404        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
405        parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
406            BootfsParserError::DirEntryTooBig { entry_index, dirsize } => {
407                assert_eq!(0, entry_index);
408                assert_eq!(ZBI_BOOTFS_DIRENT_SIZE + 1, dirsize as usize);
409            }
410            _ => panic!("parser did not fail with correct error"),
411        });
412    }
413
414    #[test]
415    fn process_bootfs_oversized_dirsize_fails() {
416        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
417        let vmo_size = vmo.get_size().unwrap();
418
419        // Change dirsize to exceed the VMO capacity for the directory table (VMO size minus
420        // header).
421        let oversized_dirsize = (vmo_size - ZBI_BOOTFS_HEADER_SIZE as u64 + 1) as u32;
422        vmo.write(&oversized_dirsize.to_le_bytes(), size_of::<u32>() as u64)
423            .expect("Failed to change dirsize");
424
425        match BootfsParser::create_from_vmo(&vmo).unwrap_err() {
426            BootfsParserError::FailedToParseHeader => (),
427            err => panic!("Expected FailedToParseHeader, got: {:?}", err),
428        }
429    }
430
431    #[test]
432    fn process_bootfs_truncated_direntry_fails() {
433        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
434
435        // Change dirsize to fewer than ZBI_BOOTFS_DIRENT_SIZE bytes remaining.
436        let truncated_dirsize = (ZBI_BOOTFS_DIRENT_SIZE - 1) as u32;
437        vmo.write(&truncated_dirsize.to_le_bytes(), size_of::<u32>() as u64)
438            .expect("Failed to change dirsize");
439
440        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
441        let mut iter = parser.iter(&vmo);
442        match iter.next() {
443            Some(Err(BootfsParserError::FailedToParseDirEntry)) => (),
444            err => panic!("Expected FailedToParseDirEntry, got: {:?}", err),
445        }
446        assert!(iter.next().is_none());
447
448        let mut zero_copy_iter = parser.zero_copy_iter();
449        match zero_copy_iter.next() {
450            Some(Err(BootfsParserError::FailedToParseDirEntry)) => (),
451            err => panic!("Expected FailedToParseDirEntry, got: {:?}", err),
452        }
453        assert!(zero_copy_iter.next().is_none());
454    }
455
456    #[test]
457    fn process_bootfs_truncated_entry_with_unaligned_offset_fails_with_dir_entry_too_big() {
458        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
459
460        // Change dirsize to ZBI_BOOTFS_DIRENT_SIZE + 1 (entry header plus 1 byte, truncated name).
461        let undersized_dirsize = (ZBI_BOOTFS_DIRENT_SIZE + 1) as u32;
462        vmo.write(&undersized_dirsize.to_le_bytes(), size_of::<u32>() as u64)
463            .expect("Failed to change dirsize");
464
465        // Also change data_off of the first direntry to an unaligned offset.
466        let unaligned_offset = ZBI_BOOTFS_PAGE_SIZE + 1;
467        let data_off_pos = (ZBI_BOOTFS_HEADER_SIZE + 2 * size_of::<u32>()) as u64;
468        vmo.write(&unaligned_offset.to_le_bytes(), data_off_pos)
469            .expect("Failed to change data_off");
470
471        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
472        let mut iter = parser.iter(&vmo);
473        match iter.next() {
474            Some(Err(BootfsParserError::DirEntryTooBig { entry_index, dirsize })) => {
475                assert_eq!(0, entry_index);
476                assert_eq!(undersized_dirsize as usize, dirsize as usize);
477            }
478            err => panic!("Expected DirEntryTooBig, got: {:?}", err),
479        }
480        assert!(iter.next().is_none());
481
482        let mut zero_copy_iter = parser.zero_copy_iter();
483        match zero_copy_iter.next() {
484            Some(Err(BootfsParserError::DirEntryTooBig { entry_index, dirsize })) => {
485                assert_eq!(0, entry_index);
486                assert_eq!(undersized_dirsize as usize, dirsize as usize);
487            }
488            err => panic!("Expected DirEntryTooBig, got: {:?}", err),
489        }
490        assert!(zero_copy_iter.next().is_none());
491    }
492
493    #[test]
494    fn process_bootfs_unaligned_data_off_fails() {
495        let vmo = read_file_to_vmo(BASIC_BOOTFS_UNCOMPRESSED_FILE).unwrap();
496
497        // Change data_off of the first direntry (the third u32 in zbi_bootfs_dirent_t) to an
498        // unaligned offset.
499        let unaligned_offset = ZBI_BOOTFS_PAGE_SIZE + 1;
500        let data_off_pos = (ZBI_BOOTFS_HEADER_SIZE + 2 * size_of::<u32>()) as u64;
501        vmo.write(&unaligned_offset.to_le_bytes(), data_off_pos)
502            .expect("Failed to change data_off");
503
504        let parser = BootfsParser::create_from_vmo(&vmo).expect("Failed to create BootfsParser");
505        parser.iter(&vmo).for_each(|result| match result.unwrap_err() {
506            BootfsParserError::UnalignedDataOffset { offset, entry_index } => {
507                assert_eq!(unaligned_offset, offset);
508                assert_eq!(0, entry_index);
509            }
510            err => panic!("Expected UnalignedDataOffset, got: {:?}", err),
511        });
512    }
513}