[efi] Add ability to extract device path from an EFI load option

An EFI boot option (stored in a BootXXXX variable) comprises an
EFI_LOAD_OPTION structure, which includes some undefined number of EFI
device paths.  (The structure is extremely messy and awkward to parse
in C, but that's par for the course with EFI.)

Add a function to extract the first device path from an EFI load
option, along with wrapper functions to read and extract the first
device path from an EFI boot variable.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
Michael Brown
2025-08-28 15:34:32 +01:00
parent 5bec2604a3
commit c10da8b53c
2 changed files with 204 additions and 0 deletions

View File

@@ -43,6 +43,7 @@ efi_path_prev ( EFI_DEVICE_PATH_PROTOCOL *path,
extern EFI_DEVICE_PATH_PROTOCOL *
efi_path_end ( EFI_DEVICE_PATH_PROTOCOL *path );
extern size_t efi_path_len ( EFI_DEVICE_PATH_PROTOCOL *path );
extern int efi_path_check ( EFI_DEVICE_PATH_PROTOCOL *path, size_t max );
extern void * efi_path_mac ( EFI_DEVICE_PATH_PROTOCOL *path );
extern unsigned int efi_path_vlan ( EFI_DEVICE_PATH_PROTOCOL *path );
extern int efi_path_guid ( EFI_DEVICE_PATH_PROTOCOL *path, union uuid *uuid );
@@ -58,6 +59,10 @@ extern EFI_DEVICE_PATH_PROTOCOL * efi_fcp_path ( struct fcp_description *desc );
extern EFI_DEVICE_PATH_PROTOCOL *
efi_ib_srp_path ( struct ib_srp_device *ib_srp );
extern EFI_DEVICE_PATH_PROTOCOL * efi_usb_path ( struct usb_function *func );
extern EFI_DEVICE_PATH_PROTOCOL * efi_load_path ( EFI_LOAD_OPTION *load,
size_t len );
extern EFI_DEVICE_PATH_PROTOCOL * efi_boot_path ( unsigned int number );
extern EFI_DEVICE_PATH_PROTOCOL * efi_current_boot_path ( void );
extern EFI_DEVICE_PATH_PROTOCOL * efi_describe ( struct interface *interface );
#define efi_describe_TYPE( object_type ) \

View File

@@ -38,6 +38,7 @@ FILE_LICENCE ( GPL2_OR_LATER );
#include <ipxe/dhcp.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_driver.h>
#include <ipxe/efi/efi_strings.h>
#include <ipxe/efi/efi_path.h>
/** @file
@@ -147,6 +148,33 @@ size_t efi_path_len ( EFI_DEVICE_PATH_PROTOCOL *path ) {
return ( ( ( void * ) end ) - ( ( void * ) path ) );
}
/**
* Check that device path is well-formed
*
* @v path Device path, or NULL
* @v max Maximum device path length
* @ret rc Return status code
*/
int efi_path_check ( EFI_DEVICE_PATH_PROTOCOL *path, size_t max ) {
EFI_DEVICE_PATH_PROTOCOL *next;
size_t remaining = max;
size_t len;
/* Check that path terminates within maximum length */
for ( ; ; path = next ) {
if ( remaining < sizeof ( *path ) )
return -EINVAL;
next = efi_path_next ( path );
if ( ! next )
break;
len = ( ( ( void * ) next ) - ( ( void * ) path ) );
if ( remaining < len )
return -EINVAL;
}
return 0;
}
/**
* Get MAC address from device path
*
@@ -668,6 +696,177 @@ EFI_DEVICE_PATH_PROTOCOL * efi_usb_path ( struct usb_function *func ) {
return path;
}
/**
* Get EFI device path from load option
*
* @v load EFI load option
* @v len Length of EFI load option
* @ret path EFI device path, or NULL on error
*
* The caller is responsible for eventually calling free() on the
* allocated device path.
*/
EFI_DEVICE_PATH_PROTOCOL * efi_load_path ( EFI_LOAD_OPTION *load,
size_t len ) {
EFI_DEVICE_PATH_PROTOCOL *path;
EFI_DEVICE_PATH_PROTOCOL *copy;
CHAR16 *wdesc;
size_t path_max;
size_t wmax;
size_t wlen;
/* Check basic structure size */
if ( len < sizeof ( *load ) ) {
DBGC ( load, "EFI load option too short for header:\n" );
DBGC_HDA ( load, 0, load, len );
return NULL;
}
/* Get length of description */
wdesc = ( ( ( void * ) load ) + sizeof ( *load ) );
wmax = ( ( len - sizeof ( *load ) ) / sizeof ( wdesc[0] ) );
wlen = wcsnlen ( wdesc, wmax );
if ( wlen == wmax ) {
DBGC ( load, "EFI load option has unterminated "
"description:\n" );
DBGC_HDA ( load, 0, load, len );
return NULL;
}
/* Get inline device path */
path = ( ( ( void * ) load ) + sizeof ( *load ) +
( wlen * sizeof ( wdesc[0] ) ) + 2 /* wNUL */ );
path_max = ( len - sizeof ( *load ) - ( wlen * sizeof ( wdesc[0] ) )
- 2 /* wNUL */ );
if ( load->FilePathListLength > path_max ) {
DBGC ( load, "EFI load option too short for path(s):\n" );
DBGC_HDA ( load, 0, load, len );
return NULL;
}
/* Check path length */
if ( efi_path_check ( path, path_max ) != 0 ) {
DBGC ( load, "EFI load option has unterminated device "
"path:\n" );
DBGC_HDA ( load, 0, load, len );
return NULL;
}
/* Allocate copy of path */
copy = malloc ( path_max );
if ( ! copy )
return NULL;
memcpy ( copy, path, path_max );
return copy;
}
/**
* Get EFI device path for numbered boot option
*
* @v number Boot option number
* @ret path EFI device path, or NULL on error
*
* The caller is responsible for eventually calling free() on the
* allocated device path.
*/
EFI_DEVICE_PATH_PROTOCOL * efi_boot_path ( unsigned int number ) {
EFI_RUNTIME_SERVICES *rs = efi_systab->RuntimeServices;
EFI_GUID *guid = &efi_global_variable;
CHAR16 wname[ 9 /* "BootXXXX" + wNUL */ ];
EFI_LOAD_OPTION *load;
EFI_DEVICE_PATH *path;
UINT32 attrs;
UINTN size;
EFI_STATUS efirc;
int rc;
/* Construct variable name */
efi_snprintf ( wname, ( sizeof ( wname ) / sizeof ( wname[0] ) ),
"Boot%04X", number );
/* Get variable length */
size = 0;
if ( ( efirc = rs->GetVariable ( wname, guid, &attrs, &size,
NULL ) != EFI_BUFFER_TOO_SMALL ) ) {
rc = -EEFI ( efirc );
DBGC ( rs, "EFI could not get size of %ls: %s\n",
wname, strerror ( rc ) );
goto err_size;
}
/* Allocate temporary buffer for EFI_LOAD_OPTION */
load = malloc ( size );
if ( ! load ) {
rc = -ENOMEM;
goto err_alloc;
}
/* Read variable */
if ( ( efirc = rs->GetVariable ( wname, guid, &attrs, &size,
load ) != 0 ) ) {
rc = -EEFI ( efirc );
DBGC ( rs, "EFI could not read %ls: %s\n",
wname, strerror ( rc ) );
goto err_read;
}
DBGC2 ( rs, "EFI boot option %ls is:\n", wname );
DBGC2_HDA ( rs, 0, load, size );
/* Get device path from load option */
path = efi_load_path ( load, size );
if ( ! path ) {
rc = -EINVAL;
DBGC ( rs, "EFI could not parse %ls: %s\n",
wname, strerror ( rc ) );
goto err_path;
}
/* Free temporary buffer */
free ( load );
return path;
err_path:
err_read:
free ( load );
err_alloc:
err_size:
return NULL;
}
/**
* Get EFI device path for current boot option
*
* @ret path EFI device path, or NULL on error
*
* The caller is responsible for eventually calling free() on the
* allocated device path.
*/
EFI_DEVICE_PATH_PROTOCOL * efi_current_boot_path ( void ) {
EFI_RUNTIME_SERVICES *rs = efi_systab->RuntimeServices;
EFI_GUID *guid = &efi_global_variable;
CHAR16 wname[] = L"BootCurrent";
UINT16 current;
UINT32 attrs;
UINTN size;
EFI_STATUS efirc;
int rc;
/* Read current boot option index */
size = sizeof ( current );
if ( ( efirc = rs->GetVariable ( wname, guid, &attrs, &size,
&current ) != 0 ) ) {
rc = -EEFI ( efirc );
DBGC ( rs, "EFI could not read %ls: %s\n",
wname, strerror ( rc ) );
return NULL;
}
/* Get device path from this boot option */
return efi_boot_path ( current );
}
/**
* Describe object as an EFI device path
*