[efi] Create safe wrappers for OpenProtocol() and CloseProtocol()

The UEFI model for opening and closing protocols is broken by design
and cannot be repaired.

Calling OpenProtocol() to obtain a protocol interface pointer does
not, in general, provide any guarantees about the lifetime of that
pointer.  It is theoretically possible that the pointer has already
become invalid by the time that OpenProtocol() returns the pointer to
its caller.  (This can happen when a USB device is physically removed,
for example.)

Various UEFI design flaws make it occasionally necessary to hold on to
a protocol interface pointer despite the total lack of guarantees that
the pointer will remain valid.

The UEFI driver model overloads the semantics of OpenProtocol() to
accommodate the use cases of recording a driver attachment (which is
modelled as opening a protocol with EFI_OPEN_PROTOCOL_BY_DRIVER
attributes) and recording the existence of a related child controller
(which is modelled as opening a protocol with
EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER attributes).

The parameters defined for CloseProtocol() are not sufficient to allow
the implementation to precisely identify the matching call to
OpenProtocol().  While the UEFI model appears to allow for matched
open and close pairs, this is merely an illusion.  Calling
CloseProtocol() will delete *all* matching records in the protocol
open information tables.

Since the parameters defined for CloseProtocol() do not include the
attributes passed to OpenProtocol(), this means that a matched
open/close pair using EFI_OPEN_PROTOCOL_GET_PROTOCOL can inadvertently
end up deleting the record that defines a driver attachment or the
existence of a child controller.  This in turn can cause some very
unexpected side effects, such as allowing other UEFI drivers to start
controlling hardware to which iPXE believes it has exclusive access.
This rarely ends well.

To prevent this kind of inadvertent deletion, we establish a
convention for four different types of protocol opening:

- ephemeral opens: always opened with ControllerHandle = NULL

- unsafe opens: always opened with ControllerHandle = AgentHandle

- by-driver opens: always opened with ControllerHandle = Handle

- by-child opens: always opened with ControllerHandle != Handle

This convention ensures that the four types of open never overlap
within the set of parameters defined for CloseProtocol(), and so a
close of one type cannot inadvertently delete the record corresponding
to a different type.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
Michael Brown
2025-03-23 17:21:36 +00:00
parent 48d1680127
commit 358db15612
3 changed files with 371 additions and 0 deletions

View File

@@ -389,5 +389,17 @@ extern EFI_STATUS efi_init ( EFI_HANDLE image_handle,
EFI_SYSTEM_TABLE *systab );
extern void efi_raise_tpl ( struct efi_saved_tpl *tpl );
extern void efi_restore_tpl ( struct efi_saved_tpl *tpl );
extern int efi_open ( EFI_HANDLE handle, EFI_GUID *protocol,
void **interface );
extern int efi_open_unsafe ( EFI_HANDLE handle, EFI_GUID *protocol,
void **interface );
extern void efi_close_unsafe ( EFI_HANDLE handle, EFI_GUID *protocol );
extern int efi_open_by_driver ( EFI_HANDLE handle, EFI_GUID *protocol,
void **interface );
extern void efi_close_by_driver ( EFI_HANDLE handle, EFI_GUID *protocol );
extern int efi_open_by_child ( EFI_HANDLE handle, EFI_GUID *protocol,
EFI_HANDLE child, void **interface );
extern void efi_close_by_child ( EFI_HANDLE handle, EFI_GUID *protocol,
EFI_HANDLE child );
#endif /* _IPXE_EFI_H */

View File

@@ -84,6 +84,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#define ERRFILE_efi_mp ( ERRFILE_CORE | 0x002c0000 )
#define ERRFILE_efi_service ( ERRFILE_CORE | 0x002d0000 )
#define ERRFILE_null_smbios ( ERRFILE_CORE | 0x002e0000 )
#define ERRFILE_efi_open ( ERRFILE_CORE | 0x002f0000 )
#define ERRFILE_eisa ( ERRFILE_DRIVER | 0x00000000 )
#define ERRFILE_isa ( ERRFILE_DRIVER | 0x00010000 )