Skip to main content

lowell_core/formats/
pe.rs

1//! PE/COFF helpers for Unified Kernel Images (UKI)
2//!
3//! Read-only introspection of PE/EFI images (UKIs) with small, ergonomic helpers.
4//! We **own** the file bytes and parse with `goblin` on demand; methods then
5//! slice into `self.data`, so the public API stays lifetime-free.
6//!
7//! ### UKI sections you’ll typically care about
8//! - `.linux`   — kernel image (Image/bzImage)
9//! - `.initrd`  — initramfs blob (often gzip/xz/zstd; can be concatenated cpio)
10//! - `.cmdline` — kernel command line (ASCII/UTF-8, NUL-padded)
11//! - `.osrel`   — os-release contents (text)
12//! - `.sbat`    — SBAT CSV (shim)
13//! - `.sdmagic` — systemd-stub marker
14//!
15//! ### Certificates / Authenticode (Secure Boot)
16//! - Presence is indicated by the **Security** data directory (index 4).
17//! - In PE/COFF, **only** this directory uses a **file offset** (not an RVA).
18//! - `goblin` already parses certificates into `pe.certificates`, so you can
19//!   inspect counts, lengths, types, and get the raw blobs directly.
20//! - We DO NOT verify signatures here; presence ≠ validity.
21
22use anyhow::{Context, Result};
23use goblin::pe::{options::ParseOptions, PE};
24use std::path::Path;
25
26/// An owning wrapper around a PE/EFI image (UKI).
27///
28/// Holds the file bytes, parses with goblin on demand, and returns
29/// borrowed slices tied to `&self`. This avoids lifetimes in the
30/// public API and side-steps self-referential types.
31#[derive(Debug)]
32pub struct PeFile {
33    /// Entire image bytes (owned).
34    data: Box<[u8]>,
35}
36
37impl PeFile {
38    /// Read a PE/EFI image from disk and own its bytes.
39    pub fn from_path(path: &Path) -> Result<Self> {
40        let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?;
41        Ok(Self {
42            data: bytes.into_boxed_slice(),
43        })
44    }
45
46    /// Construct from a caller-provided byte vector.
47    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
48        Ok(Self {
49            data: bytes.into_boxed_slice(),
50        })
51    }
52
53    /// Access the full image buffer (read-only).
54    pub fn image(&self) -> &[u8] {
55        &self.data
56    }
57
58    // ---------- Parsing & basics ----------
59
60    /// Parse PE headers using options appropriate for on-disk binaries.
61    /// (We explicitly enable attribute certificate parsing.)
62    fn parse_pe(&self) -> Result<PE<'_>> {
63        let mut opts = ParseOptions::default();
64        opts.parse_attribute_certificates = true; // ensure certs are parsed
65        PE::parse_with_opts(&self.data, &opts).context("not a valid PE/EFI image")
66    }
67
68    /// Return a human-oriented architecture label and PE32+ flag.
69    ///
70    /// Common results:
71    /// - `("x86_64", true)` for amd64 UKIs
72    /// - `("aarch64", true)` for ARM64 UKIs
73    /// - `("i386", false)` for 32-bit x86
74    pub fn arch_summary(&self) -> Result<(&'static str, bool)> {
75        use goblin::pe::header::*;
76        let pe = self.parse_pe()?;
77        let arch = match pe.header.coff_header.machine {
78            COFF_MACHINE_X86_64 => "x86_64",
79            COFF_MACHINE_ARM64 => "aarch64",
80            COFF_MACHINE_ARM => "arm",
81            COFF_MACHINE_X86 => "i386",
82            _ => "unknown",
83        };
84        Ok((arch, pe.is_64))
85    }
86
87    // ---------- Sections ----------
88    //
89    /// Offset and file size of a named section, if it exists.
90    /// (file_offset, file_size)
91    pub fn section_info(&self, name: &str) -> Result<Option<(usize, usize)>> {
92        let pe = self.parse_pe()?;
93        let sec = pe.sections.iter().find(|t| t.name().ok() == Some(name));
94        if let Some(s) = sec {
95            Ok(Some((
96                s.pointer_to_raw_data as usize,
97                s.size_of_raw_data as usize,
98            )))
99        } else {
100            Ok(None)
101        }
102    }
103
104    /// Borrow raw bytes of a named section (e.g., ".initrd", ".linux", ".cmdline").
105    ///
106    /// Returns `Ok(None)` if the section is missing or coordinates are invalid.
107    pub fn section_bytes(&self, name: &str) -> Result<Option<&[u8]>> {
108        let pe = self.parse_pe()?;
109        let sec = pe.sections.iter().find(|t| t.name().ok() == Some(name));
110        if let Some(s) = sec {
111            let off = usize::try_from(s.pointer_to_raw_data).ok();
112            let sz = usize::try_from(s.size_of_raw_data).ok();
113            if let (Some(off), Some(sz)) = (off, sz) {
114                let end = off.checked_add(sz);
115                return Ok(end.and_then(|e| self.data.get(off..e)));
116            }
117        }
118        Ok(None)
119    }
120
121    /// Read a section as text (trim at first NUL). Ideal for `.cmdline` / `.osrel`.
122    pub fn read_text(&self, name: &str) -> Result<Option<String>> {
123        Ok(self.section_bytes(name)?.map(|b| {
124            let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
125            String::from_utf8_lossy(&b[..end]).to_string()
126        }))
127    }
128
129    // ---------- Certificates (Authenticode) ----------
130
131    /// True if the image contains one or more Attribute Certificates.
132    ///
133    /// Presence indicates a **Certificate Table** exists; it does **not** mean
134    /// the signature is valid. Modifying sections (e.g., `.initrd`) will typically
135    /// invalidate verification in Secure Boot.
136    pub fn is_signed(&self) -> Result<bool> {
137        let pe = self.parse_pe()?;
138        Ok(!pe.certificates.is_empty())
139    }
140
141    /// Lightweight metadata for each attribute certificate: (length, revision, type).
142    ///
143    /// `revision` and `typ` come from the WIN_CERTIFICATE header. The blob itself is
144    /// usually PKCS#7 SignedData (`typ` 0x0002).
145    pub fn certificate_metadata(&self) -> Result<Vec<(u32, u16, u16)>> {
146        let pe = self.parse_pe()?;
147        Ok(pe
148            .certificates
149            .iter()
150            .map(|c| (c.length, c.revision as u16, c.certificate_type as u16))
151            .collect())
152    }
153
154    /// The raw certificate blobs (`&[u8]`) for each attribute certificate.
155    pub fn certificate_blobs(&self) -> Result<Vec<&[u8]>> {
156        let pe = self.parse_pe()?;
157        Ok(pe.certificates.iter().map(|c| c.certificate).collect())
158    }
159}