[ipxe-devel] [RFC PATCH 8/8] efi_vmbus: add driver for VMBus devices for OVMF

Roman Kagan rkagan at virtuozzo.com
Mon Jun 11 17:28:23 UTC 2018


Add a UEFI driver for VMBus devices, to be used with the OVMF
implementation of the HyperV VMBus device protocol (note that it's
incompatible with the real HyperV Gen2 UEFI firmware).

Signed-off-by: Roman Kagan <rkagan at virtuozzo.com>
---
 src/include/ipxe/errfile.h    |   1 +
 src/interface/efi/efi_vmbus.c | 600 ++++++++++++++++++++++++++++++++++
 2 files changed, 601 insertions(+)
 create mode 100644 src/interface/efi/efi_vmbus.c

diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h
index 8e989e42..3f38cd7a 100644
--- a/src/include/ipxe/errfile.h
+++ b/src/include/ipxe/errfile.h
@@ -374,6 +374,7 @@ FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
 #define ERRFILE_cert_cmd	      ( ERRFILE_OTHER | 0x004f0000 )
 #define ERRFILE_acpi_settings	      ( ERRFILE_OTHER | 0x00500000 )
 #define ERRFILE_ntlm		      ( ERRFILE_OTHER | 0x00510000 )
+#define ERRFILE_efi_vmbus	      ( ERRFILE_OTHER | 0x00520000 )
 
 /** @} */
 
diff --git a/src/interface/efi/efi_vmbus.c b/src/interface/efi/efi_vmbus.c
new file mode 100644
index 00000000..c931f09a
--- /dev/null
+++ b/src/interface/efi/efi_vmbus.c
@@ -0,0 +1,600 @@
+/*
+ * VMBus device support for OVMF
+ * Copyright (c) 2018 Virtuozzo International GmbH.
+ *
+ * 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.
+ */
+
+FILE_LICENCE ( GPL2_OR_LATER );
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <ipxe/efi/efi.h>
+#include <ipxe/efi/efi_driver.h>
+#include <ipxe/vmbus.h>
+#include <ipxe/efi/Protocol/HypervVmbusDevice.h>
+
+struct vmbus_efi_device {
+	struct vmbus_device vmdev;
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto;
+};
+
+static int vmbus_efi_establish_gpadl ( struct vmbus_device *vmdev,
+					userptr_t data, size_t len ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+	unsigned int gpadl;
+	int rc;
+
+	efirc = efi_proto->EstablishGpadl(efi_proto,
+					  (void *)data, len, &gpadl);
+	if (EFI_ERROR(efirc)) {
+		rc = -EEFI(efirc);
+		DBGC ( vmdev, "VMBUS %s failed to establish GPADL"
+		       " for channel %d: %s\n",
+		       vmdev->dev.name, vmdev->channel, strerror(rc) );
+		return rc;
+	}
+
+	DBGC ( vmdev, "VMBUS %s GPADL %#08x is [%08lx,%08lx)\n",
+	       vmdev->dev.name, gpadl, data, ( data + len ) );
+	return gpadl;
+}
+
+/**
+ * Tear down GPA descriptor list
+ *
+ * @v vmdev		VMBus device
+ * @v gpadl		GPADL ID
+ * @ret rc		Return status code
+ */
+static int vmbus_efi_gpadl_teardown ( struct vmbus_device *vmdev,
+				       unsigned int gpadl ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+	int rc;
+
+	efirc = efi_proto->TeardownGpadl(efi_proto, gpadl);
+	if (EFI_ERROR(efirc)) {
+		rc = -EEFI(efirc);
+		DBGC ( vmdev, "VMBUS %s failed to teardown GPADL %#08x: %s\n",
+		       vmdev->dev.name, gpadl, strerror(rc) );
+		return rc;
+	}
+
+	DBGC ( vmdev, "VMBUS %s torn down GPADL %#08x\n",
+	       vmdev->dev.name, gpadl );
+	return 0;
+}
+
+/**
+ * Open VMBus channel
+ *
+ * @v vmdev		VMBus device
+ * @v op		Channel operations
+ * @v out_len		Outbound ring buffer length
+ * @v in_len		Inbound ring buffer length
+ * @v mtu		Maximum expected data packet length (including headers)
+ * @ret rc		Return status code
+ *
+ * Both outbound and inbound ring buffer lengths must be a power of
+ * two and a multiple of PAGE_SIZE.  The requirement to be a power of
+ * two is a policy decision taken to simplify the ring buffer indexing
+ * logic.
+ */
+static int vmbus_efi_open ( struct vmbus_device *vmdev,
+			     struct vmbus_channel_operations *op,
+			     size_t out_len, size_t in_len, size_t mtu ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+	int rc;
+
+	(void)mtu;
+
+	efirc = efi_proto->OpenChannel(efi_proto, out_len >> PAGE_SHIFT,
+				       in_len >> PAGE_SHIFT);
+	if (EFI_ERROR(efirc)) {
+		rc = -EEFI(efirc);
+		DBGC ( vmdev, "VMBUS %s failed to open channel %d: %s\n",
+		       vmdev->dev.name, vmdev->channel, strerror(rc) );
+		return rc;
+	}
+
+	/* Store channel parameters */
+	vmdev->op = op;
+
+	DBGC ( vmdev, "VMBUS %s channel %#08x\n",
+	       vmdev->dev.name, vmdev->channel);
+	return 0;
+}
+
+/**
+ * Close VMBus channel
+ *
+ * @v vmdev		VMBus device
+ */
+static void vmbus_efi_close ( struct vmbus_device *vmdev ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+	int rc;
+
+	efirc = efi_proto->CloseChannel(efi_proto);
+	if (EFI_ERROR(efirc)) {
+		rc = -EEFI(efirc);
+		DBGC ( vmdev, "VMBUS %s failed to close channel %#08x: %s\n",
+		       vmdev->dev.name, vmdev->channel, strerror(rc) );
+		return;
+	}
+
+	DBGC ( vmdev, "VMBUS %s closed channel %#08x\n", vmdev->dev.name,
+	       vmdev->channel );
+}
+
+static int vmbus_efi_send_control ( struct vmbus_device *vmdev, uint64_t xid,
+				     const void *data, size_t len ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+
+	efirc = efi_proto->WriteChannel(efi_proto, VMBUS_DATA_INBAND, xid,
+					data, len, NULL, 0);
+	if (EFI_ERROR(efirc)) {
+		return -EEFI(efirc);
+	}
+	return 0;
+}
+
+/**
+ * Send data packet via ring buffer
+ *
+ * @v vmdev		VMBus device
+ * @v xid		Transaction ID
+ * @v data		Data
+ * @v len		Length of data
+ * @v iobuf		I/O buffer
+ * @ret rc		Return status code
+ *
+ * Send data using a VMBUS_DATA_GPA_DIRECT packet.  The caller is
+ * responsible for ensuring that the I/O buffer remains untouched
+ * until the corresponding completion has been received.
+ */
+static int vmbus_efi_send_data ( struct vmbus_device *vmdev, uint64_t xid,
+				  const void *data, size_t len,
+				  struct io_buffer *iobuf ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	size_t dlen = iob_len(iobuf);
+	EFI_STATUS efirc;
+
+	efirc = efi_proto->WriteChannel(efi_proto, VMBUS_DATA_GPA_DIRECT, xid,
+					data, len, iobuf->data, dlen);
+	if (EFI_ERROR(efirc)) {
+		return -EEFI(efirc);
+	}
+	return 0;
+}
+
+/**
+ * Send completion packet via ring buffer
+ *
+ * @v vmdev		VMBus device
+ * @v xid		Transaction ID
+ * @v data		Data
+ * @v len		Length of data
+ * @ret rc		Return status code
+ *
+ * Send data using a VMBUS_COMPLETION packet.
+ */
+static int vmbus_efi_send_completion ( struct vmbus_device *vmdev,
+					uint64_t xid,
+					const void *data, size_t len ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+
+	efirc = efi_proto->WriteChannel(efi_proto, VMBUS_COMPLETION, xid,
+					data, len, NULL, 0);
+	if (EFI_ERROR(efirc)) {
+		return -EEFI(efirc);
+	}
+	return 0;
+}
+
+/**
+ * Send cancellation packet via ring buffer
+ *
+ * @v vmdev		VMBus device
+ * @v xid		Transaction ID
+ * @ret rc		Return status code
+ *
+ * Send data using a VMBUS_CANCELLATION packet.
+ */
+static int vmbus_efi_send_cancellation ( struct vmbus_device *vmdev,
+					  uint64_t xid ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+
+	efirc = efi_proto->WriteChannel(efi_proto, VMBUS_CANCELLATION, xid,
+					NULL, 0, NULL, 0);
+	if (EFI_ERROR(efirc)) {
+		return -EEFI(efirc);
+	}
+	return 0;
+}
+
+static struct vmbus_xfer_pages * vmbus_xfer_pages ( struct vmbus_device *vmdev,
+						    uint16_t pageset ) {
+	struct vmbus_xfer_pages *pages;
+
+	/* Locate page set */
+	list_for_each_entry ( pages, &vmdev->pages, list ) {
+		if ( pages->pageset == pageset )
+			return pages;
+	}
+
+	DBGC ( vmdev, "VMBUS %s unrecognised page set ID %#04x\n",
+	       vmdev->dev.name, le16_to_cpu ( pageset ) );
+	return NULL;
+}
+
+/**
+ * Construct I/O buffer list from transfer pages
+ *
+ * @v vmdev		VMBus device
+ * @v header		Transfer page header
+ * @v list		I/O buffer list to populate
+ * @ret rc		Return status code
+ */
+static int vmbus_xfer_page_iobufs ( struct vmbus_device *vmdev,
+				    uint16_t pageset,
+				    uint32_t num_ranges,
+				    HV_VMBUS_BUF_RANGE *range,
+				    struct list_head *list ) {
+	struct vmbus_xfer_pages *pages;
+	struct io_buffer *iobuf;
+	struct io_buffer *tmp;
+	size_t len;
+	size_t offset;
+	unsigned int i;
+	int rc;
+
+	/* Locate page set */
+	pages = vmbus_xfer_pages ( vmdev, pageset );
+	if ( ! pages ) {
+		rc = -ENOENT;
+		goto err_pages;
+	}
+
+	/* Allocate and populate I/O buffers */
+	for ( i = 0 ; i < num_ranges ; i++ ) {
+
+		/* Parse header */
+		len = range[i].Length;
+		offset = range[i].Offset;
+
+		/* Allocate I/O buffer */
+		iobuf = alloc_iob ( len );
+		if ( ! iobuf ) {
+			DBGC ( vmdev, "VMBUS %s could not allocate %zd-byte "
+			       "I/O buffer\n", vmdev->dev.name, len );
+			rc = -ENOMEM;
+			goto err_alloc;
+		}
+
+		/* Add I/O buffer to list */
+		list_add ( &iobuf->list, list );
+
+		/* Populate I/O buffer */
+		if ( ( rc = pages->op->copy ( pages, iob_put ( iobuf, len ),
+					      offset, len ) ) != 0 ) {
+			DBGC ( vmdev, "VMBUS %s could not populate I/O buffer "
+			       "range [%zd,%zd): %s\n",
+			       vmdev->dev.name, offset, len, strerror ( rc ) );
+			goto err_copy;
+		}
+	}
+
+	return 0;
+
+ err_copy:
+ err_alloc:
+	list_for_each_entry_safe ( iobuf, tmp, list, list ) {
+		list_del ( &iobuf->list );
+		free_iob ( iobuf );
+	}
+ err_pages:
+	return rc;
+}
+
+static int vmbus_efi_poll ( struct vmbus_device *vmdev ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto = efi_vmdev->efi_proto;;
+	EFI_STATUS efirc;
+	struct list_head list;
+	uint16_t type;
+	uint64_t xid;
+	uint8_t data[64];
+	uint32_t len;
+	uint16_t pageset;
+	HV_VMBUS_BUF_RANGE ranges[16];
+	uint32_t num_ranges;
+	int rc;
+
+	len = sizeof(data);
+	num_ranges = ARRAY_SIZE(ranges);
+
+	efirc = efi_proto->ReadChannel(efi_proto, &type, &xid, data, &len,
+				       &pageset, ranges, &num_ranges);
+	// DBGC ( vmdev, "VMBUS %s polling %llx : %d\n", vmdev->dev.name, efirc,
+	//       EEFI(efirc) );
+	if (EFI_ERROR(efirc)) {
+		return -EEFI(efirc);
+	}
+
+	/* Handle packet */
+	switch ( type ) {
+
+	case VMBUS_DATA_INBAND:
+		if ( ( rc = vmdev->op->recv_control ( vmdev, xid, data,
+						      len ) ) != 0 ) {
+			DBGC ( vmdev, "VMBUS %s could not handle control "
+			       "packet: %s\n",
+			       vmdev->dev.name, strerror ( rc ) );
+			return rc;
+		}
+		break;
+
+	case VMBUS_DATA_XFER_PAGES:
+		INIT_LIST_HEAD ( &list );
+		if ( type == VMBUS_DATA_XFER_PAGES ) {
+			rc = vmbus_xfer_page_iobufs ( vmdev, pageset,
+						      num_ranges, ranges,
+						      &list );
+			if ( rc )
+				return rc;
+		}
+
+		if ( ( rc = vmdev->op->recv_data ( vmdev, xid, data, len,
+						   &list ) ) != 0 ) {
+			DBGC ( vmdev, "VMBUS %s could not handle data packet: "
+			       "%s\n", vmdev->dev.name, strerror ( rc ) );
+			return rc;
+		}
+		break;
+
+	case cpu_to_le16 ( VMBUS_COMPLETION ) :
+		if ( ( rc = vmdev->op->recv_completion ( vmdev, xid, data,
+							 len ) ) != 0 ) {
+			DBGC ( vmdev, "VMBUS %s could not handle completion: "
+			       "%s\n", vmdev->dev.name, strerror ( rc ) );
+			return rc;
+		}
+		break;
+
+	case cpu_to_le16 ( VMBUS_CANCELLATION ) :
+		if ( ( rc = vmdev->op->recv_cancellation ( vmdev, xid ) ) != 0){
+			DBGC ( vmdev, "VMBUS %s could not handle cancellation: "
+			       "%s\n", vmdev->dev.name, strerror ( rc ) );
+			return rc;
+		}
+		break;
+
+	default:
+		DBGC ( vmdev, "VMBUS %s unknown packet type %d\n",
+		       vmdev->dev.name, le16_to_cpu ( type ) );
+		return -ENOTSUP;
+	}
+
+	return 0;
+}
+
+/**
+ * Dump channel status (for debugging)
+ *
+ * @v vmdev		VMBus device
+ */
+static void vmbus_efi_dump_channel ( struct vmbus_device *vmdev ) {
+	struct vmbus_efi_device *efi_vmdev =
+		container_of ( vmdev, struct vmbus_efi_device, vmdev );
+
+	DBGC ( efi_vmdev, "VMBUS %s dev %p chan %#08x\n",
+	       vmdev->dev.name, vmdev, vmdev->channel);
+}
+
+static struct vmbus_dev_operations vmbus_efi_dev_operations = {
+	.establish_gpadl = vmbus_efi_establish_gpadl,
+	.gpadl_teardown = vmbus_efi_gpadl_teardown,
+	.open = vmbus_efi_open,
+	.close = vmbus_efi_close,
+	.send_control = vmbus_efi_send_control,
+	.send_data = vmbus_efi_send_data,
+	.send_completion = vmbus_efi_send_completion,
+	.send_cancellation = vmbus_efi_send_cancellation,
+	.poll = vmbus_efi_poll,
+	.dump_channel = vmbus_efi_dump_channel,
+};
+
+static int efivmbus_open ( EFI_HANDLE device, UINT32 attributes,
+			   struct vmbus_efi_device *vmdev ) {
+	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
+	EFI_STATUS efirc;
+	HYPERV_VMBUS_DEVICE_PROTOCOL *efi_proto;
+	struct vmbus_driver *driver;
+	union uuid type;
+	int rc;
+
+	efirc = bs->OpenProtocol ( device, &efi_hyperv_vmbus_device_protocol_guid,
+				   (void **)&efi_proto, efi_image_handle,
+				   device, attributes);
+	if (EFI_ERROR(efirc)) {
+		rc = -EEFI(efirc);
+		DBG("EFIVMBUS %s cannot open VMBus protocol: %s (%lx)\n",
+		    efi_handle_name(device), strerror(rc),
+		    (unsigned long)efirc);
+		return rc;
+	}
+
+	memcpy(&type, &efi_proto->TypeGUID, sizeof(type));
+	driver = vmbus_find_driver ( &type );
+	uuid_mangle(&type);
+	if ( !driver ) {
+		DBGC ( device, "EFIVMBUS type %s has no driver\n",
+		       uuid_ntoa(&type) );
+		rc = -ENOENT;
+		goto close_proto;
+	}
+	DBGC ( device, "EFIVMBUS type %s has driver \"%s\"\n",
+	       uuid_ntoa(&type), driver->name);
+
+	vmdev->vmdev.driver = driver;
+	vmdev->efi_proto = efi_proto;
+	rc = 0;
+
+close_proto:
+	return rc;
+}
+
+static void efivmbus_close ( EFI_HANDLE device ) {
+	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
+
+	bs->CloseProtocol ( device, &efi_hyperv_vmbus_device_protocol_guid,
+			    efi_image_handle, device );
+}
+
+static int efivmbus_supported ( EFI_HANDLE device ) {
+	struct vmbus_efi_device vmdev;
+	int rc;
+
+	/* Get PCI device information */
+	rc = efivmbus_open ( device, EFI_OPEN_PROTOCOL_GET_PROTOCOL, &vmdev );
+	if ( rc != 0 )
+		return rc;
+	efivmbus_close ( device );
+
+	/* Look for a driver */
+	return 0;
+}
+
+/**
+ * Attach driver to device
+ *
+ * @v efidev		EFI device
+ * @ret rc		Return status code
+ */
+static int efivmbus_start ( struct efi_device *efidev ) {
+	EFI_HANDLE device = efidev->device;
+	struct vmbus_efi_device *vmdev;
+	union uuid instance;
+	unsigned int channel;
+	int rc;
+
+	/* Allocate VMBus device */
+	vmdev = zalloc ( sizeof ( *vmdev ) );
+	if ( ! vmdev ) {
+		rc = -ENOMEM;
+		goto err_alloc;
+	}
+
+	rc = efivmbus_open ( device, ( EFI_OPEN_PROTOCOL_BY_DRIVER |
+				       EFI_OPEN_PROTOCOL_EXCLUSIVE ),
+			     vmdev);
+	if ( rc != 0 ) {
+		DBGC ( device, "EFIVMBUS %s could not open VMBus device: %s\n",
+		       efi_handle_name ( device ), strerror ( rc ) );
+		DBGC_EFI_OPENERS ( device, device,
+				   &efi_hyperv_vmbus_device_protocol_guid );
+		goto err_open;
+	}
+
+	memcpy(&instance, &vmdev->efi_proto->InstanceGUID, sizeof(instance));
+	uuid_mangle(&instance);
+	snprintf (vmdev->vmdev.dev.name, sizeof (vmdev->vmdev.dev.name),
+		  "{%s}", uuid_ntoa ( &instance ) );
+	channel = vmdev->efi_proto->Channel;
+	vmdev->vmdev.dev.desc.bus_type = BUS_TYPE_HV;
+	vmdev->vmdev.dev.desc.location = channel;
+	INIT_LIST_HEAD ( &vmdev->vmdev.dev.children );
+	/* Mark VMBus device as a child of the EFI device */
+	vmdev->vmdev.dev.parent = &efidev->dev;
+	list_add ( &vmdev->vmdev.dev.siblings, &efidev->dev.children );
+	memcpy(&vmdev->vmdev.instance, &vmdev->efi_proto->InstanceGUID,
+	       sizeof(vmdev->vmdev.instance));
+	INIT_LIST_HEAD ( &vmdev->vmdev.pages );
+	vmdev->vmdev.channel = channel;
+	vmdev->vmdev.dev.driver_name = vmdev->vmdev.driver->name;
+	vmdev->vmdev.dev_op = &vmbus_efi_dev_operations;
+
+	/* Probe driver */
+	rc = vmdev->vmdev.driver->probe ( &vmdev->vmdev );
+	if ( rc != 0 ) {
+		DBGC ( device, "EFIVMBUS %s could not probe driver "
+		       "\"%s\": %s\n", vmdev->vmdev.dev.name,
+		       vmdev->vmdev.dev.driver_name, strerror ( rc ) );
+		goto err_probe;
+	}
+
+	DBGC ( device, "EFIVMBUS %s using driver \"%s\"\n",
+	       vmdev->vmdev.dev.name,
+	       vmdev->vmdev.driver->name);
+
+	efidev_set_drvdata ( efidev, vmdev );
+	return 0;
+
+ err_probe:
+	list_del ( &vmdev->vmdev.dev.siblings );
+	efivmbus_close ( device );
+ err_open:
+	free ( vmdev );
+ err_alloc:
+	return rc;
+}
+
+/**
+ * Detach driver from device
+ *
+ * @v efidev		EFI device
+  */
+static void efivmbus_stop ( struct efi_device *efidev ) {
+	struct vmbus_device *vmdev = efidev_get_drvdata ( efidev );
+	EFI_HANDLE device = efidev->device;
+
+	list_del ( &vmdev->dev.siblings );
+	efivmbus_close ( device );
+	free ( vmdev );
+}
+
+
+struct efi_driver efivmbus_driver __efi_driver ( EFI_DRIVER_NORMAL ) = {
+	.name = "VMBus",
+	.supported = efivmbus_supported,
+	.start = efivmbus_start,
+	.stop = efivmbus_stop,
+};
-- 
2.17.1




More information about the ipxe-devel mailing list