Skip to content

Commit dddbf51

Browse files
uefi: Rework exit_boot_services API
Exiting boot services is a one-way operation, which is why `exit_boot_services` takes `self` rather than a reference. If it succeeds then you get back a `SystemTable<Runtime>`, but even if it fails you can't continue to use boot services except for a very limited subset. Specifically, in UEFI 2.8 and earlier, only `get_memory_map` and `exit_boot_services` are allowed. Starting in UEFI 2.9 other memory allocation functions may also be called. To properly support pre-2.9 firmware then, the right thing to do is to make sure that the initial buffer allocated to hold the memory map has sufficient extra room in case the first attempt to get the memory map and exit boot services fails. We now add room for 8 extra entries if that occurs, matching the behavior of the Linux kernel: https://github.com/torvalds/linux/blob/e544a07438/drivers/firmware/efi/libstub/efistub.h#L173 If something unexpected happens and exiting boot services still fails after two attempts, the system is in an undefined state that we can't reasonably recover from. The best thing we can do then is to simply reset the system, essentially a strong panic.
1 parent ba3343b commit dddbf51

File tree

3 files changed

+104
-63
lines changed

3 files changed

+104
-63
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## uefi - [Unreleased]
44

5+
### Changed
6+
7+
- `SystemTable::exit_boot_services` now takes no parameters and handles
8+
the memory map allocation itself. Errors are now treated as
9+
unrecoverable and will cause the system to reset.
10+
511
## uefi-macros - [Unreleased]
612

713
## uefi-services - [Unreleased]

uefi-test-runner/src/main.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fn efi_main(image: Handle, mut st: SystemTable<Boot>) -> Status {
6161

6262
runtime::test(st.runtime_services());
6363

64-
shutdown(image, st);
64+
shutdown(st);
6565
}
6666

6767
fn check_revision(rev: uefi::table::Revision) {
@@ -152,7 +152,7 @@ fn send_request_to_host(bt: &BootServices, request: HostRequest) {
152152
}
153153
}
154154

155-
fn shutdown(image: uefi::Handle, mut st: SystemTable<Boot>) -> ! {
155+
fn shutdown(mut st: SystemTable<Boot>) -> ! {
156156
// Get our text output back.
157157
st.stdout().reset(false).unwrap();
158158

@@ -164,12 +164,7 @@ fn shutdown(image: uefi::Handle, mut st: SystemTable<Boot>) -> ! {
164164
send_request_to_host(st.boot_services(), HostRequest::TestsComplete);
165165

166166
// Exit boot services as a proof that it works :)
167-
let sizes = st.boot_services().memory_map_size();
168-
let max_mmap_size = sizes.map_size + 2 * sizes.entry_size;
169-
let mut mmap_storage = vec![0; max_mmap_size].into_boxed_slice();
170-
let (st, _iter) = st
171-
.exit_boot_services(image, &mut mmap_storage[..])
172-
.expect("Failed to exit boot services");
167+
let (st, _iter) = st.exit_boot_services();
173168

174169
#[cfg(target_arch = "x86_64")]
175170
{

uefi/src/table/system.rs

Lines changed: 95 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ use core::ptr::NonNull;
55
use core::{ptr, slice};
66

77
use crate::proto::console::text;
8-
use crate::{CStr16, Char16, Handle, Result, ResultExt, Status};
8+
use crate::{CStr16, Char16, Handle, Result, Status};
99

10-
use super::boot::{BootServices, MemoryDescriptor, MemoryMapIter};
11-
use super::runtime::RuntimeServices;
10+
use super::boot::{BootServices, MemoryDescriptor, MemoryMapIter, MemoryType};
11+
use super::runtime::{ResetType, RuntimeServices};
1212
use super::{cfg, Header, Revision};
1313

1414
/// Marker trait used to provide different views of the UEFI System Table
@@ -136,7 +136,45 @@ impl SystemTable<Boot> {
136136
unsafe { &*self.table.boot }
137137
}
138138

139-
/// Exit the UEFI boot services
139+
/// Get the size in bytes of the buffer to allocate for storing the memory
140+
/// map in `exit_boot_services`.
141+
///
142+
/// This map contains some extra room to avoid needing to allocate more than
143+
/// once.
144+
///
145+
/// Returns `None` on overflow.
146+
fn memory_map_size_for_exit_boot_services(&self) -> Option<usize> {
147+
// Allocate space for extra entries beyond the current size of the
148+
// memory map. The value of 8 matches the value in the Linux kernel:
149+
// https://github.com/torvalds/linux/blob/e544a07438/drivers/firmware/efi/libstub/efistub.h#L173
150+
let extra_entries = 8;
151+
152+
let memory_map_size = self.boot_services().memory_map_size();
153+
let extra_size = memory_map_size.entry_size.checked_mul(extra_entries)?;
154+
memory_map_size.map_size.checked_add(extra_size)
155+
}
156+
157+
/// Get the current memory map and exit boot services.
158+
unsafe fn get_memory_map_and_exit_boot_services(
159+
&self,
160+
buf: &'static mut [u8],
161+
) -> Result<MemoryMapIter<'static>> {
162+
let boot_services = self.boot_services();
163+
164+
// Get the memory map.
165+
let (memory_map_key, memory_map_iter) = boot_services.memory_map(buf)?;
166+
167+
// Try to exit boot services using the memory map key. Note that after
168+
// the first call to `exit_boot_services`, there are restrictions on
169+
// what boot services functions can be called. In UEFI 2.8 and earlier,
170+
// only `get_memory_map` and `exit_boot_services` are allowed. Starting
171+
// in UEFI 2.9 other memory allocation functions may also be called.
172+
boot_services
173+
.exit_boot_services(boot_services.image_handle(), memory_map_key)
174+
.map(move |()| memory_map_iter)
175+
}
176+
177+
/// Exit the UEFI boot services.
140178
///
141179
/// After this function completes, UEFI hands over control of the hardware
142180
/// to the executing OS loader, which implies that the UEFI boot services
@@ -146,73 +184,75 @@ impl SystemTable<Boot> {
146184
/// `SystemTable<Boot>` view of the System Table and returning a more
147185
/// restricted `SystemTable<Runtime>` view as an output.
148186
///
187+
/// The memory map at the time of exiting boot services is also
188+
/// returned. The map is backed by a [`MemoryType::LOADER_DATA`]
189+
/// allocation. Since the boot services function to free that memory is no
190+
/// longer available after calling `exit_boot_services`, the allocation is
191+
/// live until the program ends. The lifetime of the memory map is therefore
192+
/// `'static`.
193+
///
149194
/// Once boot services are exited, the logger and allocator provided by
150195
/// this crate can no longer be used. The logger should be disabled using
151196
/// the [`Logger::disable`] method, and the allocator should be disabled by
152197
/// calling [`global_allocator::exit_boot_services`]. Note that if the logger and
153198
/// allocator were initialized with [`uefi_services::init`], they will be
154199
/// disabled automatically when `exit_boot_services` is called.
155200
///
156-
/// The handle passed must be the one of the currently executing image,
157-
/// which is received by the entry point of the UEFI application. In
158-
/// addition, the application must provide storage for a memory map, which
159-
/// will be retrieved automatically (as having an up-to-date memory map is a
160-
/// prerequisite for exiting UEFI boot services).
161-
///
162-
/// The storage must be aligned like a `MemoryDescriptor`.
201+
/// # Errors
163202
///
164-
/// The size of the memory map can be estimated by calling
165-
/// `BootServices::memory_map_size()`. But the memory map can grow under the
166-
/// hood between the moment where this size estimate is returned and the
167-
/// moment where boot services are exited, and calling the UEFI memory
168-
/// allocator will not be possible after the first attempt to exit the boot
169-
/// services. Therefore, UEFI applications are advised to allocate storage
170-
/// for the memory map right before exiting boot services, and to allocate a
171-
/// bit more storage than requested by memory_map_size.
203+
/// This function will fail if it is unable to allocate memory for
204+
/// the memory map, if it fails to retrieve the memory map, or if
205+
/// exiting boot services fails (with up to one retry).
172206
///
173-
/// If `exit_boot_services` succeeds, it will return a runtime view of the
174-
/// system table which more accurately reflects the state of the UEFI
175-
/// firmware following exit from boot services, along with a high-level
176-
/// iterator to the UEFI memory map.
207+
/// All errors are treated as unrecoverable because the system is
208+
/// now in an undefined state. Rather than returning control to the
209+
/// caller, the system will be reset.
177210
///
178211
/// [`global_allocator::exit_boot_services`]: crate::global_allocator::exit_boot_services
179212
/// [`Logger::disable`]: crate::logger::Logger::disable
180213
/// [`uefi_services::init`]: https://docs.rs/uefi-services/latest/uefi_services/fn.init.html
181-
pub fn exit_boot_services(
182-
self,
183-
image: Handle,
184-
mmap_buf: &mut [u8],
185-
) -> Result<(SystemTable<Runtime>, MemoryMapIter<'_>)> {
186-
unsafe {
187-
let boot_services = self.boot_services();
188-
189-
loop {
190-
// Fetch a memory map, propagate errors and split the completion
191-
// FIXME: This sad pointer hack works around a current
192-
// limitation of the NLL analysis (see Rust bug 51526).
193-
let mmap_buf = &mut *(mmap_buf as *mut [u8]);
194-
let mmap_comp = boot_services.memory_map(mmap_buf)?;
195-
let (mmap_key, mmap_iter) = mmap_comp;
196-
197-
// Try to exit boot services using this memory map key
198-
let result = boot_services.exit_boot_services(image, mmap_key);
199-
200-
// Did we fail because the memory map was updated concurrently?
201-
if result.status() == Status::INVALID_PARAMETER {
202-
// If so, fetch another memory map and try again
203-
continue;
204-
} else {
205-
// If not, report the outcome of the operation
206-
return result.map(|_| {
207-
let st = SystemTable {
208-
table: self.table,
209-
_marker: PhantomData,
210-
};
211-
(st, mmap_iter)
212-
});
214+
#[must_use]
215+
pub fn exit_boot_services(self) -> (SystemTable<Runtime>, MemoryMapIter<'static>) {
216+
let boot_services = self.boot_services();
217+
218+
// Reboot the device.
219+
let reset = |status| -> ! { self.runtime_services().reset(ResetType::Cold, status, None) };
220+
221+
// Get the size of the buffer to allocate. If that calculation
222+
// overflows treat it as an unrecoverable error.
223+
let buf_size = match self.memory_map_size_for_exit_boot_services() {
224+
Some(buf_size) => buf_size,
225+
None => reset(Status::ABORTED),
226+
};
227+
228+
// Allocate a byte slice to hold the memory map. If the
229+
// allocation fails treat it as an unrecoverable error.
230+
let buf: *mut u8 = match boot_services.allocate_pool(MemoryType::LOADER_DATA, buf_size) {
231+
Ok(buf) => buf,
232+
Err(err) => reset(err.status()),
233+
};
234+
235+
// Calling `exit_boot_services` can fail if the memory map key is not
236+
// current. Retry a second time if that occurs. This matches the
237+
// behavior of the Linux kernel:
238+
// https://github.com/torvalds/linux/blob/e544a0743/drivers/firmware/efi/libstub/efi-stub-helper.c#L375
239+
let mut status = Status::ABORTED;
240+
for _ in 0..2 {
241+
let buf: &mut [u8] = unsafe { slice::from_raw_parts_mut(buf, buf_size) };
242+
match unsafe { self.get_memory_map_and_exit_boot_services(buf) } {
243+
Ok(memory_map) => {
244+
let st = SystemTable {
245+
table: self.table,
246+
_marker: PhantomData,
247+
};
248+
return (st, memory_map);
213249
}
250+
Err(err) => status = err.status(),
214251
}
215252
}
253+
254+
// Failed to exit boot services.
255+
reset(status)
216256
}
217257

218258
/// Clone this boot-time UEFI system table interface

0 commit comments

Comments
 (0)