[efi] Restructure handling of autoexec.ipxe script

We currently attempt to obtain the autoexec.ipxe script via early use
of the EFI_SIMPLE_FILE_SYSTEM_PROTOCOL or EFI_PXE_BASE_CODE_PROTOCOL
interfaces to obtain an opaque block of memory, which is then
registered as an image at an appropriate point during our startup
sequence.  The early use of these existent interfaces allows us to
obtain the script even if our subsequent actions (e.g. disconnecting
drivers in order to connect up our own) may cause the script to become
inaccessible.

This mirrors the approach used under BIOS, where the autoexec.ipxe
script is provided by the prefix (e.g. as an initrd image when using
the .lkrn build of iPXE) and so must be copied into a normally
allocated image from wherever it happens to previously exist in
memory.

We do not currently have support for downloading an autoexec.ipxe
script if we were ourselves downloaded via UEFI HTTP boot.

There is an EFI_HTTP_PROTOCOL defined within the UEFI specification,
but it is so poorly designed as to be unusable for the simple purpose
of downloading an additional file from the same directory.  It
provides almost nothing more than a very slim wrapper around
EFI_TCP4_PROTOCOL (or EFI_TCP6_PROTOCOL).  It will not handle
redirection, content encoding, retries, or even fundamentals such as
the Content-Length header, leaving all of this up to the caller.

The UEFI HTTP Boot driver will install an EFI_LOAD_FILE_PROTOCOL
instance on the loaded image's device handle.  This looks promising at
first since it provides the LoadFile() API call which is specified to
accept an arbitrary filename parameter.  However, experimentation (and
inspection of the code in EDK2) reveals a multitude of problems that
prevent this from being usable.  Calling LoadFile() will idiotically
restart the entire DHCP process (and potentially pop up a UI requiring
input from the user for e.g. a wireless network password).  The
filename provided to LoadFile() will be ignored.  Any downloaded file
will be rejected unless it happens to match one of the limited set of
types expected by the UEFI HTTP Boot driver.  The list of design
failures and conceptual mismatches is fairly impressive.

Choose to bypass every possible aspect of UEFI HTTP support, and
instead use our own HTTP client and network stack to download the
autoexec.ipxe script over a temporary MNP network device.  Since this
approach works for TFTP as well as HTTP, drop the direct use of
EFI_PXE_BASE_CODE_PROTOCOL.  For consistency and simplicity, also drop
the direct use of EFI_SIMPLE_FILE_SYSTEM_PROTOCOL and rely upon our
existing support to access local files via "file:" URIs.

This approach results in console output during the "iPXE initialising
devices...ok" message that appears while startup is in progress.
Remove the trailing "ok" so that this intermediate output appears at a
sensible location on the screen.  The welcome banner that will be
printed immediately afterwards provides an indication that startup has
completed successfully even absent the explicit "ok".

Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
Michael Brown 2024-03-29 13:03:38 +00:00
parent b940d54235
commit 165995b7e9
4 changed files with 127 additions and 387 deletions

View File

@ -32,9 +32,8 @@ __asmcall int main ( void ) {
initialise();
/* Some devices take an unreasonably long time to initialise */
printf ( "%s initialising devices...", product_short_name );
printf ( "%s initialising devices...\n", product_short_name );
startup();
printf ( "ok\n" );
/* Attempt to boot */
if ( ( rc = ipxe ( NULL ) ) != 0 )

View File

@ -9,9 +9,6 @@
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#include <ipxe/efi/efi.h>
extern int efi_autoexec_load ( EFI_HANDLE device,
EFI_DEVICE_PATH_PROTOCOL *path );
extern int efi_autoexec_load ( void );
#endif /* _IPXE_EFI_AUTOEXEC_H */

View File

@ -24,16 +24,16 @@
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <ipxe/timer.h>
#include <ipxe/image.h>
#include <ipxe/init.h>
#include <ipxe/in.h>
#include <ipxe/netdevice.h>
#include <ipxe/efi/efi.h>
#include <ipxe/efi/efi_utils.h>
#include <ipxe/efi/efi_autoexec.h>
#include <ipxe/efi/Protocol/PxeBaseCode.h>
#include <ipxe/efi/Protocol/SimpleFileSystem.h>
#include <ipxe/efi/Guid/FileInfo.h>
#include <ipxe/efi/mnpnet.h>
#include <usr/imgmgmt.h>
#include <usr/sync.h>
/** @file
*
@ -41,413 +41,160 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
*
*/
/** Autoexec script filename */
static wchar_t efi_autoexec_wname[] = L"autoexec.ipxe";
/** Timeout for autoexec script downloads */
#define EFI_AUTOEXEC_TIMEOUT ( 2 * TICKS_PER_SEC )
/** Autoexec script image name */
static char efi_autoexec_name[] = "autoexec.ipxe";
#define EFI_AUTOEXEC_NAME "autoexec.ipxe"
/** Autoexec script (if any) */
static void *efi_autoexec;
/** Autoexec script length */
static size_t efi_autoexec_len;
/** An EFI autoexec script loader */
struct efi_autoexec_loader {
/** Required protocol GUID */
EFI_GUID *protocol;
/**
* Load autoexec script
*
* @v handle Handle on which protocol was found
* @v image Image to fill in
* @ret rc Return status code
*/
int ( * load ) ( EFI_HANDLE handle, struct image **image );
};
/**
* Load autoexec script from path within filesystem
* Load autoexec script from filesystem
*
* @v device Device handle
* @v path Relative path to image, or NULL to load from root
* @v handle Simple filesystem protocol handle
* @v image Image to fill in
* @ret rc Return status code
*/
static int efi_autoexec_filesystem ( EFI_HANDLE device,
EFI_DEVICE_PATH_PROTOCOL *path ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
union {
void *interface;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL *fs;
} u;
struct {
EFI_FILE_INFO info;
CHAR16 name[ sizeof ( efi_autoexec_wname ) /
sizeof ( efi_autoexec_wname[0] ) ];
} info;
FILEPATH_DEVICE_PATH *filepath;
EFI_FILE_PROTOCOL *root;
EFI_FILE_PROTOCOL *file;
UINTN size;
VOID *data;
unsigned int dirlen;
size_t len;
CHAR16 *wname;
EFI_STATUS efirc;
static int efi_autoexec_filesystem ( EFI_HANDLE handle, struct image **image ) {
EFI_HANDLE device = efi_loaded_image->DeviceHandle;
int rc;
/* Identify directory */
if ( path ) {
/* Check relative device path is a file path */
if ( ! ( ( path->Type == MEDIA_DEVICE_PATH ) &&
( path->SubType == MEDIA_FILEPATH_DP ) ) ) {
DBGC ( device, "EFI %s image path ",
efi_handle_name ( device ) );
DBGC ( device, " \"%s\" is not a file path\n",
efi_devpath_text ( path ) );
rc = -ENOTTY;
goto err_not_filepath;
}
filepath = container_of ( path, FILEPATH_DEVICE_PATH, Header );
/* Find length of containing directory */
dirlen = ( ( ( ( path->Length[1] << 8 ) | path->Length[0] )
- offsetof ( typeof ( *filepath ), PathName ) )
/ sizeof ( filepath->PathName[0] ) );
for ( ; dirlen ; dirlen-- ) {
if ( filepath->PathName[ dirlen - 1 ] == L'\\' )
break;
}
} else {
/* Use root directory */
filepath = NULL;
dirlen = 0;
/* Check that we were loaded from a filesystem */
if ( handle != device ) {
DBGC ( device, "EFI %s is not the file system handle\n",
efi_handle_name ( device ) );
return -ENOTTY;
}
/* Allocate filename */
len = ( ( dirlen * sizeof ( wname[0] ) ) + sizeof ( efi_autoexec_wname ) );
wname = malloc ( len );
if ( ! wname ) {
rc = -ENOMEM;
goto err_wname;
}
memcpy ( wname, filepath->PathName, ( dirlen * sizeof ( wname[0] ) ) );
memcpy ( &wname[dirlen], efi_autoexec_wname, sizeof ( efi_autoexec_wname ) );
/* Try loading from loaded image directory, if supported */
if ( ( rc = imgacquire ( "file:" EFI_AUTOEXEC_NAME,
EFI_AUTOEXEC_TIMEOUT, image ) ) == 0 )
return 0;
/* Open simple file system protocol */
if ( ( efirc = bs->OpenProtocol ( device,
&efi_simple_file_system_protocol_guid,
&u.interface, efi_image_handle,
device,
EFI_OPEN_PROTOCOL_GET_PROTOCOL ))!=0){
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s has no filesystem instance: %s\n",
/* Try loading from root directory, if supported */
if ( ( rc = imgacquire ( "file:/" EFI_AUTOEXEC_NAME,
EFI_AUTOEXEC_TIMEOUT, image ) ) == 0 )
return 0;
return rc;
}
/**
* Load autoexec script via temporary network device
*
* @v handle Managed network protocol service binding handle
* @v image Image to fill in
* @ret rc Return status code
*/
static int efi_autoexec_network ( EFI_HANDLE handle, struct image **image ) {
EFI_HANDLE device = efi_loaded_image->DeviceHandle;
struct net_device *netdev;
int rc;
/* Create temporary network device */
if ( ( rc = mnptemp_create ( handle, &netdev ) ) != 0 ) {
DBGC ( device, "EFI %s could not create net device: %s\n",
efi_handle_name ( device ), strerror ( rc ) );
goto err_filesystem;
goto err_create;
}
/* Open root directory */
if ( ( efirc = u.fs->OpenVolume ( u.fs, &root ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not open volume: %s\n",
/* Open network device */
if ( ( rc = netdev_open ( netdev ) ) != 0 ) {
DBGC ( device, "EFI %s could not open net device: %s\n",
efi_handle_name ( device ), strerror ( rc ) );
goto err_volume;
}
/* Open autoexec script */
if ( ( efirc = root->Open ( root, &file, wname,
EFI_FILE_MODE_READ, 0 ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s has no %ls: %s\n",
efi_handle_name ( device ), wname, strerror ( rc ) );
goto err_open;
}
/* Get file information */
size = sizeof ( info );
if ( ( efirc = file->GetInfo ( file, &efi_file_info_id, &size,
&info ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not get %ls info: %s\n",
efi_handle_name ( device ), wname, strerror ( rc ) );
goto err_getinfo;
}
size = info.info.FileSize;
/* Ignore zero-length files */
if ( ! size ) {
rc = -EINVAL;
DBGC ( device, "EFI %s has zero-length %ls\n",
efi_handle_name ( device ), wname );
goto err_empty;
/* Attempt download */
rc = imgacquire ( EFI_AUTOEXEC_NAME, EFI_AUTOEXEC_TIMEOUT, image );
if ( rc != 0 ) {
DBGC ( device, "EFI %s could not download %s: %s\n",
efi_handle_name ( device ), EFI_AUTOEXEC_NAME,
strerror ( rc ) );
}
/* Allocate temporary copy */
if ( ( efirc = bs->AllocatePool ( EfiBootServicesData, size,
&data ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not allocate %ls: %s\n",
efi_handle_name ( device ), wname, strerror ( rc ) );
goto err_alloc;
}
/* Ensure network exchanges have completed */
sync ( EFI_AUTOEXEC_TIMEOUT );
/* Read file */
if ( ( efirc = file->Read ( file, &size, data ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not read %ls: %s\n",
efi_handle_name ( device ), wname, strerror ( rc ) );
goto err_read;
}
/* Record autoexec script */
efi_autoexec = data;
efi_autoexec_len = size;
data = NULL;
DBGC ( device, "EFI %s found %ls\n", efi_handle_name ( device ), wname );
/* Success */
rc = 0;
err_read:
if ( data )
bs->FreePool ( data );
err_alloc:
err_empty:
err_getinfo:
file->Close ( file );
err_open:
root->Close ( root );
err_volume:
bs->CloseProtocol ( device, &efi_simple_file_system_protocol_guid,
efi_image_handle, device );
err_filesystem:
free ( wname );
err_wname:
err_not_filepath:
mnptemp_destroy ( netdev );
err_create:
return rc;
}
/**
* Load autoexec script from TFTP server
*
* @v device Device handle
* @ret rc Return status code
*/
static int efi_autoexec_tftp ( EFI_HANDLE device ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
union {
void *interface;
EFI_PXE_BASE_CODE_PROTOCOL *pxe;
} u;
EFI_PXE_BASE_CODE_MODE *mode;
EFI_PXE_BASE_CODE_PACKET *packet;
union {
struct in_addr in;
EFI_IP_ADDRESS ip;
} server;
size_t filename_max;
char *filename;
char *sep;
UINT64 size;
VOID *data;
EFI_STATUS efirc;
int rc;
/* Open PXE base code protocol */
if ( ( efirc = bs->OpenProtocol ( device,
&efi_pxe_base_code_protocol_guid,
&u.interface, efi_image_handle,
device,
EFI_OPEN_PROTOCOL_GET_PROTOCOL ))!=0){
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s has no PXE base code instance: %s\n",
efi_handle_name ( device ), strerror ( rc ) );
goto err_pxe;
}
/* Do not attempt to parse DHCPv6 packets */
mode = u.pxe->Mode;
if ( mode->UsingIpv6 ) {
rc = -ENOTSUP;
DBGC ( device, "EFI %s has IPv6 PXE base code\n",
efi_handle_name ( device ) );
goto err_ipv6;
}
/* Identify relevant reply packet */
if ( mode->PxeReplyReceived &&
mode->PxeReply.Dhcpv4.BootpBootFile[0] ) {
/* Use boot filename if present in PXE reply */
DBGC ( device, "EFI %s using PXE reply filename\n",
efi_handle_name ( device ) );
packet = &mode->PxeReply;
} else if ( mode->DhcpAckReceived &&
mode->DhcpAck.Dhcpv4.BootpBootFile[0] ) {
/* Otherwise, use boot filename if present in DHCPACK */
DBGC ( device, "EFI %s using DHCPACK filename\n",
efi_handle_name ( device ) );
packet = &mode->DhcpAck;
} else if ( mode->ProxyOfferReceived &&
mode->ProxyOffer.Dhcpv4.BootpBootFile[0] ) {
/* Otherwise, use boot filename if present in ProxyDHCPOFFER */
DBGC ( device, "EFI %s using ProxyDHCPOFFER filename\n",
efi_handle_name ( device ) );
packet = &mode->ProxyOffer;
} else {
/* No boot filename available */
rc = -ENOENT;
DBGC ( device, "EFI %s has no PXE boot filename\n",
efi_handle_name ( device ) );
goto err_packet;
}
/* Allocate filename */
filename_max = ( sizeof ( packet->Dhcpv4.BootpBootFile )
+ ( sizeof ( efi_autoexec_name ) - 1 /* NUL */ )
+ 1 /* NUL */ );
filename = zalloc ( filename_max );
if ( ! filename ) {
rc = -ENOMEM;
goto err_filename;
}
/* Extract next-server address and boot filename */
memset ( &server, 0, sizeof ( server ) );
memcpy ( &server.in, packet->Dhcpv4.BootpSiAddr,
sizeof ( server.in ) );
memcpy ( filename, packet->Dhcpv4.BootpBootFile,
sizeof ( packet->Dhcpv4.BootpBootFile ) );
/* Update filename to autoexec script name */
sep = strrchr ( filename, '/' );
if ( ! sep )
sep = strrchr ( filename, '\\' );
if ( ! sep )
sep = ( filename - 1 );
strcpy ( ( sep + 1 ), efi_autoexec_name );
/* Get file size */
if ( ( efirc = u.pxe->Mtftp ( u.pxe,
EFI_PXE_BASE_CODE_TFTP_GET_FILE_SIZE,
NULL, FALSE, &size, NULL, &server.ip,
( ( UINT8 * ) filename ), NULL,
FALSE ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not get size of %s:%s: %s\n",
efi_handle_name ( device ), inet_ntoa ( server.in ),
filename, strerror ( rc ) );
goto err_size;
}
/* Ignore zero-length files */
if ( ! size ) {
rc = -EINVAL;
DBGC ( device, "EFI %s has zero-length %s:%s\n",
efi_handle_name ( device ), inet_ntoa ( server.in ),
filename );
goto err_empty;
}
/* Allocate temporary copy */
if ( ( efirc = bs->AllocatePool ( EfiBootServicesData, size,
&data ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not allocate %s:%s: %s\n",
efi_handle_name ( device ), inet_ntoa ( server.in ),
filename, strerror ( rc ) );
goto err_alloc;
}
/* Download file */
if ( ( efirc = u.pxe->Mtftp ( u.pxe, EFI_PXE_BASE_CODE_TFTP_READ_FILE,
data, FALSE, &size, NULL, &server.ip,
( ( UINT8 * ) filename ), NULL,
FALSE ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( device, "EFI %s could not download %s:%s: %s\n",
efi_handle_name ( device ), inet_ntoa ( server.in ),
filename, strerror ( rc ) );
goto err_download;
}
/* Record autoexec script */
efi_autoexec = data;
efi_autoexec_len = size;
data = NULL;
DBGC ( device, "EFI %s found %s:%s\n", efi_handle_name ( device ),
inet_ntoa ( server.in ), filename );
/* Success */
rc = 0;
err_download:
if ( data )
bs->FreePool ( data );
err_alloc:
err_empty:
err_size:
free ( filename );
err_filename:
err_packet:
err_ipv6:
bs->CloseProtocol ( device, &efi_pxe_base_code_protocol_guid,
efi_image_handle, device );
err_pxe:
return rc;
}
/** Autoexec script loaders */
static struct efi_autoexec_loader efi_autoexec_loaders[] = {
{
.protocol = &efi_simple_file_system_protocol_guid,
.load = efi_autoexec_filesystem,
},
{
.protocol = &efi_managed_network_service_binding_protocol_guid,
.load = efi_autoexec_network,
},
};
/**
* Load autoexec script
*
* @v device Device handle
* @v path Image path within device handle
* @ret rc Return status code
*/
int efi_autoexec_load ( EFI_HANDLE device,
EFI_DEVICE_PATH_PROTOCOL *path ) {
int efi_autoexec_load ( void ) {
EFI_HANDLE device = efi_loaded_image->DeviceHandle;
EFI_HANDLE handle;
struct efi_autoexec_loader *loader;
struct image *image;
unsigned int i;
int rc;
/* Sanity check */
assert ( efi_autoexec == NULL );
assert ( efi_autoexec_len == 0 );
/* Use first applicable loader */
for ( i = 0 ; i < ( sizeof ( efi_autoexec_loaders ) /
sizeof ( efi_autoexec_loaders[0] ) ) ; i ++ ) {
/* Try loading from file system loaded image directory, if supported */
if ( ( rc = efi_autoexec_filesystem ( device, path ) ) == 0 )
return 0;
/* Locate required protocol for this loader */
loader = &efi_autoexec_loaders[i];
if ( ( rc = efi_locate_device ( device, loader->protocol,
&handle, 0 ) ) != 0 ) {
DBGC ( device, "EFI %s found no %s: %s\n",
efi_handle_name ( device ),
efi_guid_ntoa ( loader->protocol ),
strerror ( rc ) );
continue;
}
DBGC ( device, "EFI %s found %s on ",
efi_handle_name ( device ),
efi_guid_ntoa ( loader->protocol ) );
DBGC ( device, "%s\n", efi_handle_name ( handle ) );
/* Try loading from file system root directory, if supported */
if ( ( rc = efi_autoexec_filesystem ( device, NULL ) ) == 0 )
return 0;
/* Try loading */
if ( ( rc = loader->load ( handle, &image ) ) != 0 )
return rc;
/* Try loading via TFTP, if supported */
if ( ( rc = efi_autoexec_tftp ( device ) ) == 0 )
/* Discard zero-length images */
if ( ! image->len ) {
DBGC ( device, "EFI %s discarding zero-length %s\n",
efi_handle_name ( device ), image->name );
unregister_image ( image );
return -ENOENT;
}
DBGC ( device, "EFI %s loaded %s (%zd bytes)\n",
efi_handle_name ( device ), image->name, image->len );
return 0;
}
return -ENOENT;
}
/**
* Register autoexec script
*
*/
static void efi_autoexec_startup ( void ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
EFI_HANDLE device = efi_loaded_image->DeviceHandle;
struct image *image;
/* Do nothing if we have no autoexec script */
if ( ! efi_autoexec )
return;
/* Create autoexec image */
image = image_memory ( efi_autoexec_name,
virt_to_user ( efi_autoexec ),
efi_autoexec_len );
if ( ! image ) {
DBGC ( device, "EFI %s could not create %s\n",
efi_handle_name ( device ), efi_autoexec_name );
return;
}
DBGC ( device, "EFI %s registered %s\n",
efi_handle_name ( device ), efi_autoexec_name );
/* Free temporary copy */
bs->FreePool ( efi_autoexec );
efi_autoexec = NULL;
}
/** Autoexec script startup function */
struct startup_fn efi_autoexec_startup_fn __startup_fn ( STARTUP_NORMAL ) = {
.name = "efi_autoexec",
.startup = efi_autoexec_startup,
};

View File

@ -81,25 +81,19 @@ EFI_STATUS EFIAPI _efi_start ( EFI_HANDLE image_handle,
static void efi_init_application ( void ) {
EFI_HANDLE device = efi_loaded_image->DeviceHandle;
EFI_DEVICE_PATH_PROTOCOL *devpath = efi_loaded_image_path;
EFI_DEVICE_PATH_PROTOCOL *filepath = efi_loaded_image->FilePath;
struct uri *uri;
/* Set current working URI from device path, if present */
uri = efi_path_uri ( devpath );
if ( uri )
churi ( uri );
uri_put ( uri );
/* Identify autoboot device, if any */
efi_set_autoboot_ll_addr ( device, devpath );
/* Store cached DHCP packet, if any */
efi_cachedhcp_record ( device, devpath );
/* Load autoexec script, if any */
efi_autoexec_load ( device, filepath );
/* Drop temporary reference to URI */
uri_put ( uri );
}
/** EFI application initialisation function */
@ -114,6 +108,9 @@ struct init_fn efi_init_application_fn __init_fn ( INIT_NORMAL ) = {
*/
static int efi_probe ( struct root_device *rootdev __unused ) {
/* Try loading autoexec script */
efi_autoexec_load();
/* Remove any vetoed drivers */
efi_veto();