Skip to content

Commit 6816b82

Browse files
Implement buffered readers for UKI
`bootc status` for UKIs takes upto 250MB of memory as we load the entire UKI into memory just to extract the cmdline. In bootc-dev/bootc#2190 tests for UKI get OOM killed Signed-off-by: Johan-Liebert1 <pragyanpoudyal41999@gmail.com>
1 parent a8790a2 commit 6816b82

1 file changed

Lines changed: 101 additions & 7 deletions

File tree

  • crates/composefs-boot/src

crates/composefs-boot/src/uki.rs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! Specification Type 2 requirements for UKI boot entries, including extraction of boot
66
//! labels from os-release information embedded in the UKI binary.
77
8+
use std::io::{Read, Seek, SeekFrom};
89
use thiserror::Error;
910
use zerocopy::{
1011
FromBytes, Immutable, KnownLayout,
@@ -62,17 +63,20 @@ struct SectionHeader {
6263
}
6364

6465
/// Errors that can occur when parsing UKI files.
65-
#[derive(Debug, Error, PartialEq)]
66+
#[derive(Debug, Error)]
6667
pub enum UkiError {
68+
/// IO Error while reading or seeking
69+
#[error("IO Error")]
70+
Io(#[from] std::io::Error),
6771
/// The file is not a valid Portable Executable (PE/EFI) format
6872
#[error("UKI is not valid EFI executable")]
6973
PortableExecutableError,
7074
/// A required PE section is missing from the UKI
7175
#[error("UKI doesn't contain a '{0}' section")]
72-
MissingSection(&'static str),
76+
MissingSection(String),
7377
/// A PE section contains invalid UTF-8
7478
#[error("UKI section '{0}' is not UTF-8")]
75-
UnicodeError(&'static str),
79+
UnicodeError(String),
7680
/// The .osrel section lacks name information
7781
#[error("No name information found in .osrel section")]
7882
NoName,
@@ -97,7 +101,19 @@ pub fn get_text_section<'a>(
97101
section_name: &'static str,
98102
) -> Result<&'a str, UkiError> {
99103
let bytes = get_section(image, section_name).ok_or(UkiError::PortableExecutableError)??;
100-
std::str::from_utf8(bytes).or(Err(UkiError::UnicodeError(section_name)))
104+
std::str::from_utf8(bytes).or(Err(UkiError::UnicodeError(section_name.into())))
105+
}
106+
107+
/// Buffered version of [`get_text_section`].
108+
///
109+
/// See [`get_text_section`] for details. This version works with any [`Read`] + [`Seek`]
110+
/// source instead of requiring the entire image in memory.
111+
pub fn get_text_section_buffered<'a, R: Read + Seek>(
112+
image: &'a mut R,
113+
section_name: &'a str,
114+
) -> Result<String, UkiError> {
115+
let bytes = get_section_buffered(image, section_name)?;
116+
String::from_utf8(bytes).or(Err(UkiError::UnicodeError(section_name.into())))
101117
}
102118

103119
/// Extracts a raw section from a UKI PE file by name.
@@ -158,7 +174,64 @@ pub fn get_section<'a>(
158174
}
159175
}
160176

161-
Some(Err(UkiError::MissingSection(section_name)))
177+
Some(Err(UkiError::MissingSection(section_name.into())))
178+
}
179+
180+
/// Buffered version of [`get_section`].
181+
///
182+
/// See [`get_section`] for details. This version works with any [`Read`] + [`Seek`]
183+
/// source and returns owned data instead of borrowed slices.
184+
pub fn get_section_buffered<R: Read + Seek>(
185+
image: &mut R,
186+
section_name: &str,
187+
) -> Result<Vec<u8>, UkiError> {
188+
use std::io::Error as IOError;
189+
190+
// Turn the section_name ".osrel" into a section_key b".osrel\0\0".
191+
// This will panic if section_name.len() > 8, which is what we want.
192+
let mut section_key = [0u8; 8];
193+
section_key[..section_name.len()].copy_from_slice(section_name.as_bytes());
194+
195+
// Skip the DOS stub
196+
let mut buf: Vec<u8> = vec![0; std::mem::size_of::<DosStub>()];
197+
image.read_exact(&mut buf)?;
198+
let dos_stub =
199+
DosStub::ref_from_bytes(&buf).map_err(|e| UkiError::Io(IOError::other(e.to_string())))?;
200+
image.seek(SeekFrom::Start(dos_stub.pe_offset.get() as u64))?;
201+
202+
// Get the PE header
203+
let mut buf: Vec<u8> = vec![0; std::mem::size_of::<PeHeader>()];
204+
image.read_exact(&mut buf)?;
205+
let pe_header =
206+
PeHeader::ref_from_bytes(&buf).map_err(|e| UkiError::Io(IOError::other(e.to_string())))?;
207+
if pe_header.pe_magic != PE_MAGIC {
208+
return Err(UkiError::PortableExecutableError);
209+
}
210+
211+
// Skip the optional header
212+
image.seek(SeekFrom::Current(
213+
pe_header.coff_file_header.size_of_optional_header.get() as i64,
214+
))?;
215+
216+
// Try to load the section headers
217+
let n_sections = pe_header.coff_file_header.number_of_sections.get() as usize;
218+
let mut sections = vec![0; std::mem::size_of::<SectionHeader>() * n_sections];
219+
image.read_exact(&mut sections)?;
220+
let sections = <[SectionHeader]>::ref_from_bytes_with_elems(&sections, n_sections)
221+
.map_err(|e| UkiError::Io(IOError::other(e.to_string())))?;
222+
223+
for section in sections {
224+
if section.name != section_key {
225+
continue;
226+
}
227+
228+
let mut buffer = vec![0; section.virtual_size.get() as usize];
229+
image.seek(SeekFrom::Start(section.pointer_to_raw_data.get() as u64))?;
230+
image.read_exact(&mut buffer)?;
231+
return Ok(buffer);
232+
}
233+
234+
Err(UkiError::MissingSection(section_name.to_string()))
162235
}
163236

164237
/// Gets an appropriate label for display in the boot menu for the given UKI image, according to
@@ -189,11 +262,26 @@ pub fn get_boot_label(image: &[u8]) -> Result<String, UkiError> {
189262
.ok_or(UkiError::NoName)
190263
}
191264

265+
/// Buffered version of [`get_boot_label`].
266+
///
267+
/// See [`get_boot_label`] for details. This version works with any [`Read`] + [`Seek`] source.
268+
pub fn get_boot_label_buffered<R: Read + Seek>(image: &mut R) -> Result<String, UkiError> {
269+
let osrel = get_text_section_buffered(image, ".osrel")?;
270+
OsReleaseInfo::parse(&osrel)
271+
.get_boot_label()
272+
.ok_or(UkiError::NoName)
273+
}
274+
192275
/// Gets the contents of the .cmdline section of a UKI.
193276
pub fn get_cmdline(image: &[u8]) -> Result<&str, UkiError> {
194277
get_text_section(image, ".cmdline")
195278
}
196279

280+
/// Buffered version of [`get_cmdline`]. See [`get_cmdline`] for details.
281+
pub fn get_cmdline_buffered<R: Read + Seek>(image: &mut R) -> Result<String, UkiError> {
282+
get_text_section_buffered(image, ".cmdline")
283+
}
284+
197285
#[cfg(test)]
198286
mod test {
199287
use core::mem::size_of;
@@ -273,10 +361,16 @@ ID=pretty-os
273361
#[test]
274362
fn test_bad_pe() {
275363
fn pe_err(img: &[u8]) {
276-
assert_eq!(get_boot_label(img), Err(UkiError::PortableExecutableError));
364+
assert!(matches!(
365+
get_boot_label(img),
366+
Err(UkiError::PortableExecutableError)
367+
));
277368
}
278369
fn no_sec(img: &[u8]) {
279-
assert_eq!(get_boot_label(img), Err(UkiError::MissingSection(".osrel")));
370+
assert!(matches!(
371+
get_boot_label(img),
372+
Err(UkiError::MissingSection(s)) if s == ".osrel"
373+
));
280374
}
281375

282376
pe_err(b"");

0 commit comments

Comments
 (0)