From 5aa918d93b2f5c58e8b99dde9b732fa4cfbe9916 Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Thu, 30 Apr 2026 03:10:59 +0000 Subject: [PATCH 1/3] uki: report unsupported EFI image architectures clearly fastboot.efi is commonly used to boot images cross-compiled on X86 machines, but if the user forgets to force the UKI to be built for aarch64, fastboot.efi will determine that it's not a valid PE image and end up failing to load the image without clear indication of what's wrong. Handle this case in a cleaner way to aid the user in determining the cause of such issues. Signed-off-by: Bjorn Andersson --- src/main.rs | 22 +++++++++++++++++++--- src/peimage.rs | 41 +++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 28f0f15..cdff7f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,7 @@ mod initrd; mod memcardinfo; mod peimage; -use peimage::{handle_peimage, is_peimage}; +use peimage::{expected_pe_machine, handle_peimage, is_peimage, pe_machine}; mod proto; @@ -248,6 +248,21 @@ fn create_empty_rt_properties_table() -> Result { fn handle_boot(usb_device: &ScopedProtocol, payload: &[u8]) -> Result { let (handle, _initrd) = if is_peimage(payload) { (handle_peimage(payload)?, None) + } else if let Some(machine) = pe_machine(payload) { + info!( + "rejecting EFI payload with unsupported machine {:04x}, expected {:04x}", + machine, + expected_pe_machine() + ); + fastboot_respond( + usb_device, + &format!( + "FAILunsupported EFI arch {:04x}, need {:04x}", + machine, + expected_pe_machine() + ), + )?; + return Ok(()); } else if is_bootimg_v0(payload) { let result = handle_bootimg_v0(payload); if let Err(err) = result { @@ -263,8 +278,9 @@ fn handle_boot(usb_device: &ScopedProtocol, payload: &[u8]) -> Res } result.unwrap() } else { - fastboot_respond(usb_device, "FAIL")?; - return Err(uefi::Error::new(Status::INVALID_PARAMETER, ())); + info!("rejecting payload with unsupported format"); + fastboot_respond(usb_device, "FAILunsupported boot image format")?; + return Ok(()); }; create_empty_rt_properties_table()?.install_configuration_table(&EFI_RT_PROPERTIES_TABLE)?; diff --git a/src/peimage.rs b/src/peimage.rs index fcc27b1..d3e492e 100644 --- a/src/peimage.rs +++ b/src/peimage.rs @@ -11,31 +11,44 @@ const PE_ARM64: u16 = 0xaa64; const PE_PLUS: u16 = 0x020b; const PE_SUBSYSTEM_EFI_APP: u16 = 10; -pub(crate) fn is_peimage(payload: &[u8]) -> bool { +pub(crate) fn expected_pe_machine() -> u16 { + PE_ARM64 +} + +pub(crate) fn pe_machine(payload: &[u8]) -> Option { if payload.len() < PE_OFFSET + 4 || payload[0] != b'M' || payload[1] != b'Z' { - return false; + return None; } - let pe_offset: [u8; 4] = payload[PE_OFFSET..PE_OFFSET + 4].try_into().unwrap(); - let pe_offset = u32::from_le_bytes(pe_offset) as usize; + let pe_offset = payload.get(PE_OFFSET..PE_OFFSET + 4)?; + let pe_offset = u32::from_le_bytes(pe_offset.try_into().ok()?) as usize; - if payload.len() < pe_offset + PE_MAGIC.len() + 70 - || payload[pe_offset..pe_offset + PE_MAGIC.len()] != PE_MAGIC - { - return false; - } - if payload[pe_offset..pe_offset + PE_MAGIC.len()] != PE_MAGIC { - return false; + if payload.get(pe_offset..pe_offset + PE_MAGIC.len())? != PE_MAGIC { + return None; } - let coff_hdr = &payload[pe_offset + PE_MAGIC.len()..]; - let machine: [u8; 2] = coff_hdr[0..2].try_into().unwrap(); - let machine = u16::from_le_bytes(machine); + let coff_hdr = payload.get(pe_offset + PE_MAGIC.len()..)?; + let machine = coff_hdr.get(0..2)?; + Some(u16::from_le_bytes(machine.try_into().ok()?)) +} +pub(crate) fn is_peimage(payload: &[u8]) -> bool { + let machine = match pe_machine(payload) { + Some(machine) => machine, + None => return false, + }; if machine != PE_ARM64 { return false; } + let pe_offset: [u8; 4] = payload[PE_OFFSET..PE_OFFSET + 4].try_into().unwrap(); + let pe_offset = u32::from_le_bytes(pe_offset) as usize; + if payload.len() < pe_offset + PE_MAGIC.len() + 70 { + return false; + } + + let coff_hdr = &payload[pe_offset + PE_MAGIC.len()..]; + let opt_hdr_size: [u8; 2] = coff_hdr[16..18].try_into().unwrap(); let opt_hdr_size = u16::from_le_bytes(opt_hdr_size); if opt_hdr_size < 88 { From 69cf4d188b5a6fef56d6c2e1e1c18a89594b672b Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Thu, 30 Apr 2026 03:14:26 +0000 Subject: [PATCH 2/3] fastboot: reuse a persistent transfer buffer for fastboot responses fastboot_respond() has been allocating a new USB transfer buffer for every response and never released it. Allocate one 64-byte IN transfer buffer in main(), pass it through command handlers, and free it at shutdown. This keeps response semantics unchanged while fixing the response-buffer lifetime and leak. Signed-off-by: Bjorn Andersson --- src/main.rs | 90 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index cdff7f5..c0bcf7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,27 +74,31 @@ fn fastboot_open(serial_number: &CStr16) -> Result> Ok(usb_device) } -fn fastboot_respond(usb_device: &ScopedProtocol, response: &str) -> Result { - let buf = usb_device - .allocate_transfer_buffer(64) - .expect("failed to allocate command buffer"); - +fn fastboot_respond( + usb_device: &ScopedProtocol, + response_buffer: *mut u8, + response: &str, +) -> Result { let mut payload = response.as_bytes().to_vec(); let payload_len = payload.len().min(64); payload.push(0); unsafe { - ptr::copy_nonoverlapping(payload.as_ptr(), buf, payload_len); + ptr::copy_nonoverlapping(payload.as_ptr(), response_buffer, payload_len); } usb_device - .send(usb_device::ENDPOINT_IN, payload_len, buf) + .send(usb_device::ENDPOINT_IN, payload_len, response_buffer) .expect("failed to send response"); Ok(()) } -fn handle_download(usb_device: &ScopedProtocol, size: usize) -> Result<&[u8]> { +fn handle_download( + usb_device: &ScopedProtocol, + response_buffer: *mut u8, + size: usize, +) -> Result<&[u8]> { let mut download_remains = size; let target = boot::allocate_pool(MemoryType::BOOT_SERVICES_DATA, size).unwrap(); @@ -107,7 +111,7 @@ fn handle_download(usb_device: &ScopedProtocol, size: usize) -> Re .allocate_transfer_buffer(16 * 1024 * 1024) .expect("failed to allocate command buffer"); - fastboot_respond(usb_device, &format!("DATA{size:08x}"))?; + fastboot_respond(usb_device, response_buffer, &format!("DATA{size:08x}"))?; usb_device .send( @@ -146,7 +150,7 @@ fn handle_download(usb_device: &ScopedProtocol, size: usize) -> Re usb_device.free_transfer_buffer(receive_buffer)?; if offset == target_slice.len() { - fastboot_respond(usb_device, "OKAY")?; + fastboot_respond(usb_device, response_buffer, "OKAY")?; } Ok(target_slice) @@ -245,7 +249,11 @@ fn create_empty_rt_properties_table() -> Result { Ok(buf) } -fn handle_boot(usb_device: &ScopedProtocol, payload: &[u8]) -> Result { +fn handle_boot( + usb_device: &ScopedProtocol, + response_buffer: *mut u8, + payload: &[u8], +) -> Result { let (handle, _initrd) = if is_peimage(payload) { (handle_peimage(payload)?, None) } else if let Some(machine) = pe_machine(payload) { @@ -256,6 +264,7 @@ fn handle_boot(usb_device: &ScopedProtocol, payload: &[u8]) -> Res ); fastboot_respond( usb_device, + response_buffer, &format!( "FAILunsupported EFI arch {:04x}, need {:04x}", machine, @@ -266,32 +275,48 @@ fn handle_boot(usb_device: &ScopedProtocol, payload: &[u8]) -> Res } else if is_bootimg_v0(payload) { let result = handle_bootimg_v0(payload); if let Err(err) = result { - fastboot_respond(usb_device, &format!("FAILfailed: {}", err.data()))?; + fastboot_respond( + usb_device, + response_buffer, + &format!("FAILfailed: {}", err.data()), + )?; return Ok(()); } (result.unwrap(), None) } else if is_bootimg_v2(payload) { let result = handle_bootimg_v2(payload); if let Err(err) = result { - fastboot_respond(usb_device, &format!("FAILfailed: {}", err.data()))?; + fastboot_respond( + usb_device, + response_buffer, + &format!("FAILfailed: {}", err.data()), + )?; return Ok(()); } result.unwrap() } else { info!("rejecting payload with unsupported format"); - fastboot_respond(usb_device, "FAILunsupported boot image format")?; + fastboot_respond( + usb_device, + response_buffer, + "FAILunsupported boot image format", + )?; return Ok(()); }; create_empty_rt_properties_table()?.install_configuration_table(&EFI_RT_PROPERTIES_TABLE)?; - fastboot_respond(usb_device, "OKAY")?; + fastboot_respond(usb_device, response_buffer, "OKAY")?; boot::start_image(handle)?; Ok(()) } -fn handle_getvar(usb_device: &ScopedProtocol, variable: &str) -> Result { +fn handle_getvar( + usb_device: &ScopedProtocol, + response_buffer: *mut u8, + variable: &str, +) -> Result { let response = match variable { "version" => Some("0.4"), "version-bootloader" => Some(env!("BUILD_VERSION")), @@ -303,7 +328,7 @@ fn handle_getvar(usb_device: &ScopedProtocol, variable: &str) -> R None => format!("FAILunknown variable: {variable}"), }; - fastboot_respond(usb_device, &response).expect("Failed to send response"); + fastboot_respond(usb_device, response_buffer, &response).expect("Failed to send response"); Ok(()) } @@ -344,6 +369,9 @@ fn main() -> Status { let command_buffer = usb_device .allocate_transfer_buffer(1024 * 1024) .expect("failed to allocate command buffer"); + let response_buffer = usb_device + .allocate_transfer_buffer(64) + .expect("failed to allocate response buffer"); let mut loaded_data: Option<&[u8]> = None; @@ -364,16 +392,22 @@ fn main() -> Status { let parts = request.split(':').nth(1).unwrap(); let size = usize::from_str_radix(parts, 16).unwrap(); - loaded_data = Some(handle_download(&usb_device, size).unwrap()); + loaded_data = + Some(handle_download(&usb_device, response_buffer, size).unwrap()); } else if request == "boot" { if let Some(payload) = loaded_data { - handle_boot(&usb_device, payload).expect("Failed to handle boot command"); + handle_boot(&usb_device, response_buffer, payload) + .expect("Failed to handle boot command"); } else { - fastboot_respond(&usb_device, "FAILdownload something first") - .expect("Failed to send response"); + fastboot_respond( + &usb_device, + response_buffer, + "FAILdownload something first", + ) + .expect("Failed to send response"); }; } else if request == "reboot" { - let _ = fastboot_respond(&usb_device, "OKAY"); + let _ = fastboot_respond(&usb_device, response_buffer, "OKAY"); let reset_data = cstr16!("RESET_PARAM"); runtime::reset( @@ -382,19 +416,20 @@ fn main() -> Status { Some(reset_data.as_bytes()), ); } else if request == "continue" { - let _ = fastboot_respond(&usb_device, "OKAY"); + let _ = fastboot_respond(&usb_device, response_buffer, "OKAY"); break 'message_loop; } else if request.starts_with("getvar") { let Some(variable) = request.split(':').nth(1) else { - fastboot_respond(&usb_device, "FAILinvalid getvar") + fastboot_respond(&usb_device, response_buffer, "FAILinvalid getvar") .expect("Failed to send response"); continue; }; - handle_getvar(&usb_device, variable).expect("Failed to handle getvar command"); + handle_getvar(&usb_device, response_buffer, variable) + .expect("Failed to handle getvar command"); } else { - fastboot_respond(&usb_device, "FAILunknown command") + fastboot_respond(&usb_device, response_buffer, "FAILunknown command") .expect("Failed to send response"); } @@ -410,6 +445,9 @@ fn main() -> Status { usb_device .free_transfer_buffer(command_buffer) .expect("Failed to free transfer buffer"); + usb_device + .free_transfer_buffer(response_buffer) + .expect("Failed to free response buffer"); Status::SUCCESS } From 1ca91801efe375e9d05ecd4182d62673e1308579 Mon Sep 17 00:00:00 2001 From: Bjorn Andersson Date: Thu, 30 Apr 2026 03:16:06 +0000 Subject: [PATCH 3/3] fastboot: harden fastboot command parsing and boot response handling Handle malformed command payloads more defensively by truncating at NUL, trimming CR/LF, validating UTF-8, and rejecting invalid download/getvar syntax with explicit FAIL responses. Also avoid panicking if boot fails after acknowledging a boot request: send a single OKAY before StartImage, then only log StartImage failures. This preserves fastboot protocol expectations and avoids a second response after OKAY. Signed-off-by: Bjorn Andersson --- src/main.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index c0bcf7e..5dc947b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,6 +94,17 @@ fn fastboot_respond( Ok(()) } +fn parse_fastboot_request(data: &[u8]) -> core::result::Result<&str, ()> { + let request_end = data + .iter() + .position(|&byte| byte == 0) + .unwrap_or(data.len()); + let request = core::str::from_utf8(&data[..request_end]).map_err(|_| ())?; + let request = request.trim_end_matches(|ch| ch == '\r' || ch == '\n'); + + Ok(request) +} + fn handle_download( usb_device: &ScopedProtocol, response_buffer: *mut u8, @@ -255,7 +266,16 @@ fn handle_boot( payload: &[u8], ) -> Result { let (handle, _initrd) = if is_peimage(payload) { - (handle_peimage(payload)?, None) + let result = handle_peimage(payload); + if let Err(err) = result { + fastboot_respond( + usb_device, + response_buffer, + &format!("FAILfailed: {:?}", err.status()), + )?; + return Ok(()); + } + (result.unwrap(), None) } else if let Some(machine) = pe_machine(payload) { info!( "rejecting EFI payload with unsupported machine {:04x}, expected {:04x}", @@ -304,10 +324,30 @@ fn handle_boot( return Ok(()); }; - create_empty_rt_properties_table()?.install_configuration_table(&EFI_RT_PROPERTIES_TABLE)?; + let rt_properties = match create_empty_rt_properties_table() { + Ok(table) => table, + Err(err) => { + fastboot_respond( + usb_device, + response_buffer, + &format!("FAILfailed: {:?}", err.status()), + )?; + return Ok(()); + } + }; + if let Err(err) = rt_properties.install_configuration_table(&EFI_RT_PROPERTIES_TABLE) { + fastboot_respond( + usb_device, + response_buffer, + &format!("FAILfailed: {:?}", err.status()), + )?; + return Ok(()); + } fastboot_respond(usb_device, response_buffer, "OKAY")?; - boot::start_image(handle)?; + if let Err(err) = boot::start_image(handle) { + info!("boot image returned after OKAY: {:?}", err.status()); + } Ok(()) } @@ -386,18 +426,47 @@ fn main() -> Status { .expect("failed to queue command buffer"); } usb_device::EfiUsbDeviceEvent::OutData(data) => { - let request = core::str::from_utf8(data).unwrap(); + let request = match parse_fastboot_request(data) { + Ok(request) => request, + Err(()) => { + fastboot_respond( + &usb_device, + response_buffer, + "FAILmalformed fastboot command", + ) + .expect("Failed to send response"); + usb_device + .send(usb_device::ENDPOINT_OUT, 1024 * 1024, command_buffer) + .expect("failed to queue command buffer"); + continue; + } + }; if request.starts_with("download:") { - let parts = request.split(':').nth(1).unwrap(); - let size = usize::from_str_radix(parts, 16).unwrap(); + let Some(parts) = request.strip_prefix("download:") else { + fastboot_respond(&usb_device, response_buffer, "FAILinvalid download") + .expect("Failed to send response"); + usb_device + .send(usb_device::ENDPOINT_OUT, 1024 * 1024, command_buffer) + .expect("failed to queue command buffer"); + continue; + }; + let Ok(size) = usize::from_str_radix(parts, 16) else { + fastboot_respond(&usb_device, response_buffer, "FAILinvalid download size") + .expect("Failed to send response"); + usb_device + .send(usb_device::ENDPOINT_OUT, 1024 * 1024, command_buffer) + .expect("failed to queue command buffer"); + continue; + }; loaded_data = Some(handle_download(&usb_device, response_buffer, size).unwrap()); } else if request == "boot" { if let Some(payload) = loaded_data { - handle_boot(&usb_device, response_buffer, payload) - .expect("Failed to handle boot command"); + if let Err(err) = handle_boot(&usb_device, response_buffer, payload) { + info!("Failed to handle boot command: {:?}", err.status()); + } } else { fastboot_respond( &usb_device, @@ -419,15 +488,17 @@ fn main() -> Status { let _ = fastboot_respond(&usb_device, response_buffer, "OKAY"); break 'message_loop; - } else if request.starts_with("getvar") { - let Some(variable) = request.split(':').nth(1) else { + } else if request == "getvar" { + fastboot_respond(&usb_device, response_buffer, "FAILinvalid getvar") + .expect("Failed to send response"); + } else if let Some(variable) = request.strip_prefix("getvar:") { + if variable.is_empty() { fastboot_respond(&usb_device, response_buffer, "FAILinvalid getvar") .expect("Failed to send response"); - continue; - }; - - handle_getvar(&usb_device, response_buffer, variable) - .expect("Failed to handle getvar command"); + } else { + handle_getvar(&usb_device, response_buffer, variable) + .expect("Failed to handle getvar command"); + } } else { fastboot_respond(&usb_device, response_buffer, "FAILunknown command") .expect("Failed to send response");