linux_bootloader/
pe_loader.rs

1use core::ffi::c_void;
2
3use alloc::vec::Vec;
4use goblin::pe::PE;
5use uefi::{
6    prelude::BootServices,
7    proto::loaded_image::LoadedImage,
8    table::{
9        boot::{AllocateType, MemoryType},
10        Boot, SystemTable,
11    },
12    CStr16, Handle, Status,
13};
14
15/// UEFI mandates 4 KiB pages.
16const UEFI_PAGE_BITS: usize = 12;
17const UEFI_PAGE_MASK: usize = (1 << UEFI_PAGE_BITS) - 1;
18
19#[cfg(target_arch = "aarch64")]
20fn make_instruction_cache_coherent(memory: &[u8]) {
21    use core::arch::asm;
22    // Minimum cache line size is 16 bits per the CSSIDR_EL0 format.
23    // For simplicity, we issue flushes in this stride unconditionally.
24    const CACHE_LINE_SIZE: usize = 16;
25
26    // The start address gets rounded down, while the end address gets rounded up.
27    // This guarantees we flush precisely every cache line touching the passed slice.
28    let start_address = memory.as_ptr() as usize & CACHE_LINE_SIZE.wrapping_neg();
29    let end_address = ((memory.as_ptr() as usize + memory.len() - 1) | (CACHE_LINE_SIZE - 1)) + 1;
30
31    // Compare the ARM Architecture Reference Manual, B2.4.4.
32
33    // Make the writes to every address in the range visible at PoU.
34    for address in (start_address..end_address).step_by(CACHE_LINE_SIZE) {
35        unsafe {
36            // SAFETY: The addressed cache line overlaps `memory`, so it must be mapped.
37            asm!("dc cvau, {address}", address = in(reg) address);
38        }
39    }
40    unsafe {
41        // SAFETY: Barriers are always safe to execute.
42        asm!("dsb ish");
43    }
44
45    // Force reloading the written instructions.
46    for address in (start_address..end_address).step_by(4) {
47        unsafe {
48            // SAFETY: The addressed cache line overlaps `memory`, so it must be mapped.
49            asm!("ic ivau, {address}", address = in(reg) address);
50        }
51    }
52    unsafe {
53        // SAFETY: Barriers are always safe to execute.
54        asm! {
55            "dsb ish",
56            "isb",
57        }
58    }
59}
60
61#[cfg(target_arch = "x86")]
62fn make_instruction_cache_coherent(_memory: &[u8]) {
63    // x86 has coherent instruction cache for legacy compatibility reasons
64}
65
66#[cfg(target_arch = "x86_64")]
67fn make_instruction_cache_coherent(_memory: &[u8]) {
68    // x86_64 mandates coherent instruction cache
69}
70
71pub struct Image {
72    image: &'static mut [u8],
73    entry: extern "efiapi" fn(Handle, SystemTable<Boot>) -> Status,
74}
75
76/// Converts a length in bytes to the number of required pages.
77fn bytes_to_pages(bytes: usize) -> usize {
78    bytes
79        .checked_add(UEFI_PAGE_MASK)
80        .map(|rounded_up| rounded_up >> UEFI_PAGE_BITS)
81        .unwrap_or(1 << (usize::try_from(usize::BITS).unwrap() - UEFI_PAGE_BITS))
82}
83
84impl Image {
85    /// Loads and relocates a PE file.
86    ///
87    /// The image must be handed to [`start`] later. If this does not
88    /// happen, the memory allocated for the unpacked PE binary will
89    /// leak.
90    pub fn load(boot_services: &BootServices, file_data: &[u8]) -> uefi::Result<Image> {
91        let pe = PE::parse(file_data).map_err(|_| Status::LOAD_ERROR)?;
92
93        // Allocate all memory the image will need in virtual memory.
94        // We follow shim here and allocate as EfiLoaderCode.
95        let image = {
96            let section_lengths = pe
97                .sections
98                .iter()
99                .map(|section| {
100                    section
101                        .virtual_address
102                        .checked_add(section.virtual_size)
103                        .ok_or(Status::LOAD_ERROR)
104                })
105                .collect::<Result<Vec<u32>, uefi::Status>>()?;
106
107            let length = usize::try_from(section_lengths.into_iter().max().unwrap_or(0)).unwrap();
108
109            let base = boot_services.allocate_pages(
110                AllocateType::AnyPages,
111                MemoryType::LOADER_CODE,
112                bytes_to_pages(length),
113            )? as *mut u8;
114
115            unsafe {
116                core::ptr::write_bytes(base, 0, length);
117                core::slice::from_raw_parts_mut(base, length)
118            }
119        };
120
121        // Populate all sections in virtual memory.
122        for section in &pe.sections {
123            let copy_size =
124                usize::try_from(u32::min(section.virtual_size, section.size_of_raw_data)).unwrap();
125            let raw_start = usize::try_from(section.pointer_to_raw_data).unwrap();
126            let raw_end = raw_start.checked_add(copy_size).ok_or(Status::LOAD_ERROR)?;
127            let virt_start = usize::try_from(section.virtual_address).unwrap();
128            let virt_end = virt_start
129                .checked_add(copy_size)
130                .ok_or(Status::LOAD_ERROR)?;
131
132            if virt_end > image.len() || raw_end > file_data.len() {
133                return Err(Status::LOAD_ERROR.into());
134            }
135            image[virt_start..virt_end].copy_from_slice(&file_data[raw_start..raw_end]);
136        }
137
138        // Image base relocations are not supported.
139        if pe
140            .header
141            .optional_header
142            .and_then(|h| *h.data_directories.get_base_relocation_table())
143            .is_some()
144        {
145            return Err(Status::INCOMPATIBLE_VERSION.into());
146        }
147
148        // On some platforms, the instruction cache is not coherent with the data cache.
149        // We don't want to execute stale icache contents instead of the code we just loaded.
150        // Platform-specific flushes need to be performed to prevent this from happening.
151        make_instruction_cache_coherent(image);
152
153        if pe.entry >= image.len() {
154            return Err(Status::LOAD_ERROR.into());
155        }
156        let entry = unsafe { core::mem::transmute(&image[pe.entry]) };
157
158        Ok(Image { image, entry })
159    }
160
161    /// Starts a trusted loaded PE file.
162    /// The caller is responsible for verifying that it trusts the PE file to uphold the invariants detailed below.
163    /// If the entry point returns, the image memory is subsequently deallocated.
164    ///
165    /// # Safety
166    /// The image is assumed to be trusted. This means:
167    /// * The PE file it was loaded from must have been a completely valid EFI application of the correct architecture.
168    /// * If the entry point returns, it must leave the system in a state that allows our stub to continue.
169    ///   In particular:
170    ///   * Only memory it either has allocated, or that belongs to the image, should have been altered.
171    ///   * Memory it has not allocated should not have been freed.
172    ///   * Boot services must not have been exited.
173    pub unsafe fn start(
174        self,
175        handle: Handle,
176        system_table: &SystemTable<Boot>,
177        load_options: &CStr16,
178    ) -> Status {
179        let mut loaded_image = system_table
180            .boot_services()
181            .open_protocol_exclusive::<LoadedImage>(handle)
182            .expect("Failed to open the LoadedImage protocol");
183
184        let (our_data, our_size) = loaded_image.info();
185        let our_load_options = loaded_image
186            .load_options_as_bytes()
187            .map(|options| options.as_ptr_range());
188
189        // It seems to be impossible to allocate custom image handles.
190        // Hence, we reuse our own for the kernel.
191        // The shim does the same thing.
192        unsafe {
193            loaded_image.set_image(
194                self.image.as_ptr() as *const c_void,
195                self.image.len().try_into().unwrap(),
196            );
197            loaded_image.set_load_options(
198                load_options.as_ptr() as *const u8,
199                u32::try_from(load_options.num_bytes()).unwrap(),
200            );
201        }
202
203        let status = (self.entry)(handle, unsafe { system_table.unsafe_clone() });
204
205        // If the kernel has exited boot services, it must not return any more, and has full control over the entire machine.
206        // If the kernel entry point returned, deallocate its image, and restore our loaded image handle.
207        // If it calls Exit(), that call returns directly to systemd-boot. This unfortunately causes a resource leak.
208        system_table
209            .boot_services()
210            .free_pages(self.image.as_ptr() as u64, bytes_to_pages(self.image.len()))
211            .expect("Double free attempted");
212
213        unsafe {
214            loaded_image.set_image(our_data, our_size);
215            match our_load_options {
216                Some(options) => loaded_image.set_load_options(
217                    options.start,
218                    options.end.offset_from(options.start).try_into().unwrap(),
219                ),
220                None => loaded_image.set_load_options(core::ptr::null(), 0),
221            }
222        }
223
224        status
225    }
226}