/*
 * Copyright (C) 2025 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 *
 * You can also choose to distribute this program under the terms of
 * the Unmodified Binary Distribution Licence (as given in the file
 * COPYING.UBDL), provided that you have satisfied its requirements.
 */

FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
FILE_SECBOOT ( PERMITTED );

/** @file
 *
 * EFI protocol opening and closing
 *
 * 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.
 */

#include <assert.h>
#include <errno.h>
#include <ipxe/efi/efi.h>

/**
 * Open (or test) protocol for ephemeral use
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v interface		Protocol interface pointer to fill in (or NULL to test)
 * @ret rc		Return status code
 */
int efi_open_untyped ( EFI_HANDLE handle, EFI_GUID *protocol,
		       void **interface ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;
	unsigned int attributes;
	EFI_STATUS efirc;
	int rc;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );

	/* Open protocol
	 *
	 * We set ControllerHandle to NULL to avoid collisions with
	 * other open types.
	 */
	controller = NULL;
	attributes = ( interface ? EFI_OPEN_PROTOCOL_GET_PROTOCOL :
		       EFI_OPEN_PROTOCOL_TEST_PROTOCOL );
	if ( ( efirc = bs->OpenProtocol ( handle, protocol, interface, agent,
					  controller, attributes ) ) != 0 ) {
		rc = -EEFI ( efirc );
		if ( interface )
			*interface = NULL;
		return rc;
	}

	/* Close protocol immediately
	 *
	 * While it may seem prima facie unsafe to use a protocol
	 * after closing it, UEFI doesn't actually give us any safety
	 * even while the protocol is nominally open.  Opening a
	 * protocol with EFI_OPEN_PROTOCOL_GET_PROTOCOL attributes
	 * does not in any way ensure that the interface pointer
	 * remains valid: there are no locks or notifications
	 * associated with these "opens".
	 *
	 * The only way to obtain a (partially) guaranteed persistent
	 * interface pointer is to open the protocol with the
	 * EFI_OPEN_PROTOCOL_BY_DRIVER attributes.  This is not
	 * possible in the general case, since UEFI permits only a
	 * single image at a time to have the protocol opened with
	 * these attributes.
	 *
	 * We can therefore obtain at best an ephemeral interface
	 * pointer: one that is guaranteed to remain valid only for as
	 * long as we do not relinquish the thread of control.
	 *
	 * (Since UEFI permits calls to UninstallProtocolInterface()
	 * at levels up to and including TPL_NOTIFY, this means that
	 * we technically cannot rely on the pointer remaining valid
	 * unless the caller is itself running at TPL_NOTIFY.  This is
	 * clearly impractical, and large portions of the EDK2
	 * codebase presume that using EFI_OPEN_PROTOCOL_GET_PROTOCOL
	 * is safe at lower TPLs.)
	 *
	 * Closing is not strictly necessary for protocols opened
	 * ephemerally (i.e. using EFI_OPEN_PROTOCOL_GET_PROTOCOL or
	 * EFI_OPEN_PROTOCOL_TEST_PROTOCOL), but avoids polluting the
	 * protocol open information tables with stale data.
	 *
	 * Closing immediately also simplifies the callers' code
	 * paths, since they do not need to worry about closing the
	 * protocol.
	 *
	 * The overall effect is equivalent to using HandleProtocol(),
	 * but without the associated pollution of the protocol open
	 * information tables, and with improved traceability.
	 */
	bs->CloseProtocol ( handle, protocol, agent, controller );

	return 0;
}

/**
 * Open protocol for unsafe persistent use
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v interface		Protocol interface pointer to fill in
 * @ret rc		Return status code
 */
int efi_open_unsafe_untyped ( EFI_HANDLE handle, EFI_GUID *protocol,
			      void **interface ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;
	unsigned int attributes;
	EFI_STATUS efirc;
	int rc;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );
	assert ( interface != NULL );

	/* Open protocol
	 *
	 * We set ControllerHandle equal to AgentHandle to avoid
	 * collisions with other open types.
	 */
	controller = agent;
	attributes = EFI_OPEN_PROTOCOL_GET_PROTOCOL;
	if ( ( efirc = bs->OpenProtocol ( handle, protocol, interface, agent,
					  controller, attributes ) ) != 0 ) {
		rc = -EEFI ( efirc );
		*interface = NULL;
		return rc;
	}

	return 0;
}

/**
 * Close protocol opened for unsafe persistent use
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v child		Child controller handle
 */
void efi_close_unsafe ( EFI_HANDLE handle, EFI_GUID *protocol ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );

	/* Close protocol */
	controller = agent;
	bs->CloseProtocol ( handle, protocol, agent, controller );
}

/**
 * Open protocol for persistent use by a driver
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v interface		Protocol interface pointer to fill in
 * @ret rc		Return status code
 */
int efi_open_by_driver_untyped ( EFI_HANDLE handle, EFI_GUID *protocol,
				 void **interface ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;
	unsigned int attributes;
	EFI_STATUS efirc;
	int rc;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );
	assert ( interface != NULL );

	/* Open protocol
	 *
	 * We set ControllerHandle equal to Handle to avoid collisions
	 * with other open types.
	 */
	controller = handle;
	attributes = ( EFI_OPEN_PROTOCOL_BY_DRIVER |
		       EFI_OPEN_PROTOCOL_EXCLUSIVE );
	if ( ( efirc = bs->OpenProtocol ( handle, protocol, interface, agent,
					  controller, attributes ) ) != 0 ) {
		rc = -EEFI ( efirc );
		*interface = NULL;
		return rc;
	}

	return 0;
}

/**
 * Close protocol opened for persistent use by a driver
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 */
void efi_close_by_driver ( EFI_HANDLE handle, EFI_GUID *protocol ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );

	/* Close protocol */
	controller = handle;
	bs->CloseProtocol ( handle, protocol, agent, controller );
}

/**
 * Open protocol for persistent use by a child controller
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v child		Child controller handle
 * @v interface		Protocol interface pointer to fill in
 * @ret rc		Return status code
 */
int efi_open_by_child_untyped ( EFI_HANDLE handle, EFI_GUID *protocol,
				EFI_HANDLE child, void **interface ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;
	unsigned int attributes;
	EFI_STATUS efirc;
	int rc;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );
	assert ( child != NULL );
	assert ( interface != NULL );

	/* Open protocol
	 *
	 * We set ControllerHandle to a non-NULL value distinct from
	 * both Handle and AgentHandle to avoid collisions with other
	 * open types.
	 */
	controller = child;
	assert ( controller != handle );
	assert ( controller != agent );
	attributes = EFI_OPEN_PROTOCOL_BY_CHILD_CONTROLLER;
	if ( ( efirc = bs->OpenProtocol ( handle, protocol, interface, agent,
					  controller, attributes ) ) != 0 ) {
		rc = -EEFI ( efirc );
		*interface = NULL;
		return rc;
	}

	return 0;
}

/**
 * Close protocol opened for persistent use by a child controller
 *
 * @v handle		EFI handle
 * @v protocol		Protocol GUID
 * @v child		Child controller handle
 */
void efi_close_by_child ( EFI_HANDLE handle, EFI_GUID *protocol,
			  EFI_HANDLE child ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_HANDLE agent = efi_image_handle;
	EFI_HANDLE controller;

	/* Sanity checks */
	assert ( handle != NULL );
	assert ( protocol != NULL );
	assert ( child != NULL );

	/* Close protocol */
	controller = child;
	assert ( controller != handle );
	assert ( controller != agent );
	bs->CloseProtocol ( handle, protocol, agent, controller );
}
