1use std::io::{Read, Seek, SeekFrom};
9use thiserror::Error;
10use zerocopy::{
11 FromBytes, Immutable, KnownLayout,
12 little_endian::{U16, U32},
13};
14
15use crate::os_release::OsReleaseInfo;
16
17#[derive(Debug, FromBytes, Immutable, KnownLayout)]
19#[cfg_attr(test, derive(zerocopy::IntoBytes, Default))]
20#[repr(C)]
21struct DosStub {
22 _unused1: [u8; 0x20],
23 _unused2: [u8; 0x1c],
24 pe_offset: U32,
25}
26
27#[derive(Debug, FromBytes, Immutable, KnownLayout)]
28#[cfg_attr(test, derive(zerocopy::IntoBytes, Default))]
29#[repr(C)]
30struct CoffFileHeader {
31 machine: U16,
32 number_of_sections: U16,
33 time_date_stamp: U32,
34 pointer_to_symbol_table: U32,
35 number_of_symbols: U32,
36 size_of_optional_header: U16,
37 characteristics: U16,
38}
39
40#[derive(Debug, FromBytes, Immutable, KnownLayout)]
41#[cfg_attr(test, derive(zerocopy::IntoBytes, Default))]
42#[repr(C)]
43struct PeHeader {
44 pe_magic: [u8; 4], coff_file_header: CoffFileHeader,
46}
47const PE_MAGIC: [u8; 4] = *b"PE\0\0";
48
49#[derive(Debug, FromBytes, Immutable, KnownLayout)]
50#[cfg_attr(test, derive(zerocopy::IntoBytes, Default))]
51#[repr(C)]
52struct SectionHeader {
53 name: [u8; 8],
54 virtual_size: U32,
55 virtual_address: U32,
56 size_of_raw_data: U32,
57 pointer_to_raw_data: U32,
58 pointer_to_relocations: U32,
59 pointer_to_line_numbers: U32,
60 number_of_relocations: U16,
61 number_of_line_numbers: U16,
62 characteristics: U32,
63}
64
65#[derive(Debug, Error)]
67pub enum UkiError {
68 #[error("IO Error")]
70 Io(#[from] std::io::Error),
71 #[error("UKI is not valid EFI executable")]
73 PortableExecutableError,
74 #[error("UKI doesn't contain a '{0}' section")]
76 MissingSection(String),
77 #[error("UKI section '{0}' is not UTF-8")]
79 UnicodeError(String),
80 #[error("No name information found in .osrel section")]
82 NoName,
83}
84
85pub fn get_text_section<'a>(
100 image: &'a [u8],
101 section_name: &'static str,
102) -> Result<&'a str, UkiError> {
103 let bytes = get_section(image, section_name).ok_or(UkiError::PortableExecutableError)??;
104 std::str::from_utf8(bytes).or(Err(UkiError::UnicodeError(section_name.into())))
105}
106
107pub 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())))
117}
118
119pub fn get_section<'a>(
143 image: &'a [u8],
144 section_name: &'static str,
145) -> Option<Result<&'a [u8], UkiError>> {
146 let mut section_key = [0u8; 8];
149 section_key[..section_name.len()].copy_from_slice(section_name.as_bytes());
150
151 let (dos_stub, ..) = DosStub::ref_from_prefix(image).ok()?;
153 let rest = image.get(dos_stub.pe_offset.get() as usize..)?;
154
155 let (pe_header, rest) = PeHeader::ref_from_prefix(rest).ok()?;
157 if pe_header.pe_magic != PE_MAGIC {
158 return None;
159 }
160
161 let rest = rest.get(pe_header.coff_file_header.size_of_optional_header.get() as usize..)?;
163
164 let n_sections = pe_header.coff_file_header.number_of_sections.get() as usize;
166 let (sections, ..) = <[SectionHeader]>::ref_from_prefix_with_elems(rest, n_sections).ok()?;
167
168 for section in sections {
169 if section.name == section_key {
170 let bytes = image
171 .get(section.pointer_to_raw_data.get() as usize..)?
172 .get(..section.virtual_size.get() as usize)?;
173 return Some(Ok(bytes));
174 }
175 }
176
177 Some(Err(UkiError::MissingSection(section_name.into())))
178}
179
180pub 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 let mut section_key = [0u8; 8];
193 section_key[..section_name.len()].copy_from_slice(section_name.as_bytes());
194
195 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 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 image.seek(SeekFrom::Current(
213 pe_header.coff_file_header.size_of_optional_header.get() as i64,
214 ))?;
215
216 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(§ions, 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()))
235}
236
237pub fn get_boot_label(image: &[u8]) -> Result<String, UkiError> {
259 let osrel = get_text_section(image, ".osrel")?;
260 OsReleaseInfo::parse(osrel)
261 .get_boot_label()
262 .ok_or(UkiError::NoName)
263}
264
265pub 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
275pub fn get_cmdline(image: &[u8]) -> Result<&str, UkiError> {
277 get_text_section(image, ".cmdline")
278}
279
280pub fn get_cmdline_buffered<R: Read + Seek>(image: &mut R) -> Result<String, UkiError> {
282 get_text_section_buffered(image, ".cmdline")
283}
284
285#[cfg(test)]
286mod test {
287 use core::mem::size_of;
288
289 use similar_asserts::assert_eq;
290 use zerocopy::IntoBytes;
291
292 use super::*;
293
294 fn data_offset(n_sections: usize) -> usize {
295 size_of::<DosStub>() + size_of::<PeHeader>() + n_sections * size_of::<SectionHeader>()
296 }
297
298 fn peify(optional: &[u8], sections: &[SectionHeader], rest: &[&[u8]]) -> Vec<u8> {
299 let mut output = vec![];
300 output.extend_from_slice(
301 DosStub {
302 pe_offset: U32::new(size_of::<DosStub>() as u32),
303 ..Default::default()
304 }
305 .as_bytes(),
306 );
307 output.extend_from_slice(
308 PeHeader {
309 pe_magic: PE_MAGIC,
310 coff_file_header: CoffFileHeader {
311 number_of_sections: U16::new(sections.len() as u16),
312 size_of_optional_header: U16::new(optional.len() as u16),
313 ..Default::default()
314 },
315 }
316 .as_bytes(),
317 );
318 output.extend_from_slice(optional);
319 for section in sections {
320 output.extend_from_slice(section.as_bytes());
321 }
322 assert_eq!(output.len(), data_offset(sections.len()));
323 for data in rest {
324 output.extend_from_slice(data);
325 }
326
327 output
328 }
329
330 fn ukify(osrel: &[u8]) -> Vec<u8> {
331 let osrel_offset = data_offset(1);
332 peify(
333 b"",
334 &[SectionHeader {
335 name: *b".osrel\0\0",
336 virtual_size: U32::new(osrel.len() as u32),
337 pointer_to_raw_data: U32::new(osrel_offset as u32),
338 ..Default::default()
339 }],
340 &[osrel],
341 )
342 }
343
344 #[test]
345 fn test_simple() {
346 let uki = ukify(
347 br#"
348PRETTY_NAME='prettyOS'
349VERSION_ID="Rocky Racoon"
350VERSION=42
351ID=pretty-os
352"#,
353 );
354
355 assert_eq!(
357 get_boot_label(uki.as_ref()).unwrap(),
358 "prettyOS Rocky Racoon"
359 );
360
361 let mut cursor = std::io::Cursor::new(&uki);
363 assert_eq!(
364 get_boot_label_buffered(&mut cursor).unwrap(),
365 "prettyOS Rocky Racoon"
366 );
367 }
368
369 #[test]
370 fn test_bad_pe() {
371 fn pe_err(img: &[u8]) {
372 assert!(matches!(
373 get_boot_label(img),
374 Err(UkiError::PortableExecutableError)
375 ));
376 }
377 fn no_sec(img: &[u8]) {
378 assert!(matches!(
379 get_boot_label(img),
380 Err(UkiError::MissingSection(s)) if s == ".osrel"
381 ));
382
383 let mut cursor = std::io::Cursor::new(img);
385 assert!(matches!(
386 get_boot_label_buffered(&mut cursor),
387 Err(UkiError::MissingSection(s)) if s == ".osrel"
388 ));
389 }
390
391 pe_err(b"");
392 pe_err(b"This is definitely not an EFI executable, but it's big enough to pass the first step...");
393
394 pe_err(
395 DosStub {
396 pe_offset: U32::new(0),
397 ..Default::default()
398 }
399 .as_bytes(),
400 );
401
402 no_sec(&peify(b"", &[], &[]));
404 no_sec(&peify(
406 b"",
407 &[
408 SectionHeader {
409 name: *b".text\0\0\0",
410 ..Default::default()
411 },
412 SectionHeader {
413 name: *b".rodata\0",
414 ..Default::default()
415 },
416 ],
417 &[],
418 ));
419
420 pe_err(&peify(
422 b"",
423 &[SectionHeader {
424 name: *b".osrel\0\0",
425 pointer_to_raw_data: U32::new(1234567),
426 ..Default::default()
427 }],
428 &[],
429 ));
430 }
431
432 #[test]
433 fn test_section_functions() {
434 let osrel_data = b"PRETTY_NAME='TestOS'\nVERSION_ID=1.0\n";
435 let cmdline_data = b"root=/dev/sda1 quiet";
436
437 let osrel_offset = data_offset(2);
438 let cmdline_offset = osrel_offset + osrel_data.len();
439
440 let uki = peify(
441 b"",
442 &[
443 SectionHeader {
444 name: *b".osrel\0\0",
445 virtual_size: U32::new(osrel_data.len() as u32),
446 pointer_to_raw_data: U32::new(osrel_offset as u32),
447 ..Default::default()
448 },
449 SectionHeader {
450 name: *b".cmdline",
451 virtual_size: U32::new(cmdline_data.len() as u32),
452 pointer_to_raw_data: U32::new(cmdline_offset as u32),
453 ..Default::default()
454 },
455 ],
456 &[osrel_data, cmdline_data],
457 );
458
459 let osrel_section = get_section(&uki, ".osrel").unwrap().unwrap();
461 assert_eq!(osrel_section, osrel_data);
462
463 let cmdline_section = get_section(&uki, ".cmdline").unwrap().unwrap();
464 assert_eq!(cmdline_section, cmdline_data);
465
466 let osrel_text = get_text_section(&uki, ".osrel").unwrap();
467 assert_eq!(osrel_text, "PRETTY_NAME='TestOS'\nVERSION_ID=1.0\n");
468
469 let cmdline_text = get_cmdline(&uki).unwrap();
470 assert_eq!(cmdline_text, "root=/dev/sda1 quiet");
471
472 let mut cursor = std::io::Cursor::new(&uki);
474 let osrel_section_buf = get_section_buffered(&mut cursor, ".osrel").unwrap();
475 assert_eq!(osrel_section_buf, osrel_data);
476
477 cursor.set_position(0);
478 let cmdline_section_buf = get_section_buffered(&mut cursor, ".cmdline").unwrap();
479 assert_eq!(cmdline_section_buf, cmdline_data);
480
481 cursor.set_position(0);
482 let osrel_text_buf = get_text_section_buffered(&mut cursor, ".osrel").unwrap();
483 assert_eq!(osrel_text_buf, "PRETTY_NAME='TestOS'\nVERSION_ID=1.0\n");
484
485 cursor.set_position(0);
486 let cmdline_text_buf = get_cmdline_buffered(&mut cursor).unwrap();
487 assert_eq!(cmdline_text_buf, "root=/dev/sda1 quiet");
488
489 cursor.set_position(0);
491 let missing_result = get_section_buffered(&mut cursor, ".missing");
492 assert!(matches!(missing_result, Err(UkiError::MissingSection(s)) if s == ".missing"));
493 }
494
495 #[test]
496 fn test_invalid_utf8() {
497 let invalid_utf8 = b"\xff\xfe\xfd";
498 let osrel_offset = data_offset(1);
499
500 let uki = peify(
501 b"",
502 &[SectionHeader {
503 name: *b".osrel\0\0",
504 virtual_size: U32::new(invalid_utf8.len() as u32),
505 pointer_to_raw_data: U32::new(osrel_offset as u32),
506 ..Default::default()
507 }],
508 &[invalid_utf8],
509 );
510
511 let result = get_text_section(&uki, ".osrel");
513 assert!(matches!(result, Err(UkiError::UnicodeError(s)) if s == ".osrel"));
514
515 let mut cursor = std::io::Cursor::new(&uki);
517 let result_buf = get_text_section_buffered(&mut cursor, ".osrel");
518 assert!(matches!(result_buf, Err(UkiError::UnicodeError(s)) if s == ".osrel"));
519 }
520}