Created
May 31, 2026 03:21
-
-
Save mikroskeem/34054a024addfce249eae526bb50d933 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 | |
| From: Mark Vainomaa <mark@zentria.ee> | |
| Date: Sun, 31 May 2026 01:39:38 +0300 | |
| Subject: [PATCH 1/2] OvmfPkg/VirtioNetDxe: pin device to single virtqueue pair | |
| via VIRTIO_NET_F_MQ | |
| When the host backs virtio-net with vhost-net + an IFF_MULTI_QUEUE tap, the | |
| kernel's tun_select_queue() distributes inbound packets across the tap's fds | |
| by skb hash. cloud-hypervisor (and QEMU) feed each fd into a separate | |
| virtio-net RX virtqueue. Without negotiating VIRTIO_NET_F_MQ, this driver | |
| only polls receiveq1, so any packet steered to RX queue >0 is lost. | |
| Negotiate VIRTIO_NET_F_CTRL_VQ + VIRTIO_NET_F_MQ, allocate the control | |
| virtqueue, and after DRIVER_OK issue VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET 1 so | |
| spec-conformant devices steer all RX onto receiveq1. The driver remains | |
| single-queue; SNP has no use for additional queue pairs. Gated on virtio | |
| 1.0+; older transports differ in config-space layout and are not affected | |
| by the multi-queue tap class of bugs in practice. | |
| Diagnosed against cloud-hypervisor with a 4-queue OVS tap: iPXE never | |
| observed DHCPOFFER even though the offer was visible on the tap. Forcing | |
| num_queues=2 at the VMM layer worked around it; this is the firmware-side | |
| fix. | |
| Signed-off-by: Mark Vainomaa <mark@zentria.ee> | |
| --- | |
| .../Include/IndustryStandard/Virtio095Net.h | 34 ++++ | |
| OvmfPkg/VirtioNetDxe/SnpInitialize.c | 103 ++++++++++- | |
| OvmfPkg/VirtioNetDxe/SnpShutdown.c | 5 + | |
| OvmfPkg/VirtioNetDxe/VirtioNet.h | 26 +++ | |
| OvmfPkg/VirtioNetDxe/VirtioNet.inf | 1 + | |
| OvmfPkg/VirtioNetDxe/VirtioNetCtrl.c | 166 ++++++++++++++++++ | |
| 6 files changed, 333 insertions(+), 2 deletions(-) | |
| create mode 100644 OvmfPkg/VirtioNetDxe/VirtioNetCtrl.c | |
| diff --git a/OvmfPkg/Include/IndustryStandard/Virtio095Net.h b/OvmfPkg/Include/IndustryStandard/Virtio095Net.h | |
| index 372015a0f5..f738193be5 100644 | |
| --- a/OvmfPkg/Include/IndustryStandard/Virtio095Net.h | |
| +++ b/OvmfPkg/Include/IndustryStandard/Virtio095Net.h | |
| @@ -16,10 +16,15 @@ | |
| // | |
| // virtio-0.9.5, Appendix C: Network Device | |
| // | |
| +// MaxVirtqueuePairs is only valid when VIRTIO_NET_F_MQ has been negotiated; | |
| +// otherwise the device need not expose the field and reading it via the | |
| +// transport may be illegal. See virtio 1.2, 5.1.4 Device configuration layout. | |
| +// | |
| #pragma pack(1) | |
| typedef struct { | |
| UINT8 Mac[6]; | |
| UINT16 LinkStatus; | |
| + UINT16 MaxVirtqueuePairs; | |
| } VIRTIO_NET_CONFIG; | |
| #pragma pack() | |
| @@ -53,6 +58,35 @@ typedef struct { | |
| #define VIRTIO_NET_F_CTRL_RX BIT18 // control channel RX mode support | |
| #define VIRTIO_NET_F_CTRL_VLAN BIT19 // control channel VLAN filtering | |
| #define VIRTIO_NET_F_GUEST_ANNOUNCE BIT21 // guest can send gratuitous pkts | |
| +#define VIRTIO_NET_F_MQ BIT22 // device supports multiqueue with auto-receive steering | |
| + | |
| +// | |
| +// Control virtqueue command classes (VIRTIO_NET_CTRL_HDR.Class) | |
| +// | |
| +#define VIRTIO_NET_CTRL_MQ 4 | |
| + | |
| +// | |
| +// VIRTIO_NET_CTRL_MQ commands | |
| +// | |
| +#define VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET 0 | |
| +#define VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN 1 | |
| +#define VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX 0x8000 | |
| + | |
| +// | |
| +// Status byte the device writes back at the end of a control command. | |
| +// | |
| +#define VIRTIO_NET_OK 0 | |
| +#define VIRTIO_NET_ERR 1 | |
| + | |
| +// | |
| +// Control virtqueue command header | |
| +// | |
| +#pragma pack(1) | |
| +typedef struct { | |
| + UINT8 Class; | |
| + UINT8 Command; | |
| +} VIRTIO_NET_CTRL_HDR; | |
| +#pragma pack() | |
| // | |
| // Packet Header | |
| diff --git a/OvmfPkg/VirtioNetDxe/SnpInitialize.c b/OvmfPkg/VirtioNetDxe/SnpInitialize.c | |
| index c77aeea826..ee45e4c235 100644 | |
| --- a/OvmfPkg/VirtioNetDxe/SnpInitialize.c | |
| +++ b/OvmfPkg/VirtioNetDxe/SnpInitialize.c | |
| @@ -557,8 +557,34 @@ VirtioNetInitialize ( | |
| !!(Features & VIRTIO_NET_F_STATUS) | |
| ); | |
| + // | |
| + // VIRTIO_NET_F_MQ is requested purely to pin the device to a single | |
| + // virtqueue pair, so that hosts whose backend (e.g. vhost-net with a | |
| + // multi-queue tap) would otherwise steer RX across multiple virtqueues | |
| + // are forced to deliver everything to receiveq1, where this driver polls. | |
| + // It requires VIRTIO_NET_F_CTRL_VQ as a prerequisite, and the control | |
| + // queue path is only exercised under virtio 1.0+ (gated below). | |
| + // | |
| Features &= VIRTIO_NET_F_MAC | VIRTIO_NET_F_STATUS | VIRTIO_F_VERSION_1 | | |
| - VIRTIO_F_IOMMU_PLATFORM; | |
| + VIRTIO_F_IOMMU_PLATFORM | | |
| + VIRTIO_NET_F_CTRL_VQ | VIRTIO_NET_F_MQ; | |
| + | |
| + // | |
| + // F_MQ depends on F_CTRL_VQ per spec. If only one survived, drop both so we | |
| + // don't leave the device in an undefined "half MQ" state. | |
| + // | |
| + if ((Features & VIRTIO_NET_F_CTRL_VQ) == 0) { | |
| + Features &= ~(UINT64)VIRTIO_NET_F_MQ; | |
| + } | |
| + | |
| + // | |
| + // Restrict the MQ control path to virtio 1.0+. Legacy transports have | |
| + // different config-space sizing rules, and cloud-hypervisor (our primary | |
| + // multi-queue target) only exposes MQ via the modern interface anyway. | |
| + // | |
| + if (Dev->VirtIo->Revision < VIRTIO_SPEC_REVISION (1, 0, 0)) { | |
| + Features &= ~(UINT64)(VIRTIO_NET_F_CTRL_VQ | VIRTIO_NET_F_MQ); | |
| + } | |
| // | |
| // In virtio-1.0, feature negotiation is expected to complete before queue | |
| @@ -571,6 +597,34 @@ VirtioNetInitialize ( | |
| } | |
| } | |
| + // | |
| + // If VIRTIO_NET_F_MQ survived negotiation, read max_virtqueue_pairs from | |
| + // device config (legal only when F_MQ was negotiated, per spec) and derive | |
| + // the control virtqueue index: queues 0..2*N-1 are receiveq/transmitq pairs; | |
| + // queue 2*N is controlq. We won't actually steer traffic across multiple | |
| + // queue pairs -- the control queue exists solely so we can issue | |
| + // VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET 1 and force all RX onto receiveq1. | |
| + // | |
| + Dev->CtrlQueuePresent = FALSE; | |
| + if ((Features & VIRTIO_NET_F_MQ) != 0) { | |
| + UINT16 MaxVqPairs; | |
| + | |
| + Status = VIRTIO_CFG_READ (Dev, MaxVirtqueuePairs, &MaxVqPairs); | |
| + if (EFI_ERROR (Status)) { | |
| + goto DeviceFailed; | |
| + } | |
| + | |
| + if ((MaxVqPairs < VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN) || | |
| + (MaxVqPairs > VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX)) | |
| + { | |
| + Status = EFI_DEVICE_ERROR; | |
| + goto DeviceFailed; | |
| + } | |
| + | |
| + Dev->CtrlQueueIndex = (UINT16)(2 * MaxVqPairs); | |
| + Dev->CtrlQueuePresent = TRUE; | |
| + } | |
| + | |
| // | |
| // step 4b, 4c -- allocate and report virtqueues | |
| // | |
| @@ -594,6 +648,24 @@ VirtioNetInitialize ( | |
| goto ReleaseRxRing; | |
| } | |
| + // | |
| + // Allocate the control virtqueue. Must happen before DRIVER_OK -- on | |
| + // virtio 1.0+ the device's queue-address registers are only writable up | |
| + // to that point. | |
| + // | |
| + if (Dev->CtrlQueuePresent) { | |
| + Status = VirtioNetInitRing ( | |
| + Dev, | |
| + Dev->CtrlQueueIndex, | |
| + &Dev->CtrlRing, | |
| + &Dev->CtrlRingMap | |
| + ); | |
| + if (EFI_ERROR (Status)) { | |
| + Dev->CtrlQueuePresent = FALSE; | |
| + goto ReleaseTxRing; | |
| + } | |
| + } | |
| + | |
| // | |
| // step 5 -- keep only the features we want | |
| // | |
| @@ -611,7 +683,28 @@ VirtioNetInitialize ( | |
| NextDevStat |= VSTAT_DRIVER_OK; | |
| Status = Dev->VirtIo->SetDeviceStatus (Dev->VirtIo, NextDevStat); | |
| if (EFI_ERROR (Status)) { | |
| - goto ReleaseTxRing; | |
| + goto ReleaseCtrlRing; | |
| + } | |
| + | |
| + // | |
| + // Pin the device to a single virtqueue pair. The spec lets the device | |
| + // process control commands only after DRIVER_OK, which is why this lives | |
| + // here and not next to the ring allocation above. | |
| + // | |
| + if (Dev->CtrlQueuePresent) { | |
| + UINT16 Pairs; | |
| + | |
| + Pairs = 1; | |
| + Status = VirtioNetSendCtrlCommand ( | |
| + Dev, | |
| + VIRTIO_NET_CTRL_MQ, | |
| + VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET, | |
| + &Pairs, | |
| + sizeof Pairs | |
| + ); | |
| + if (EFI_ERROR (Status)) { | |
| + goto AbortDevice; | |
| + } | |
| } | |
| Status = VirtioNetInitTx (Dev); | |
| @@ -637,6 +730,12 @@ ReleaseTxAux: | |
| AbortDevice: | |
| Dev->VirtIo->SetDeviceStatus (Dev->VirtIo, 0); | |
| +ReleaseCtrlRing: | |
| + if (Dev->CtrlQueuePresent) { | |
| + VirtioNetUninitRing (Dev, &Dev->CtrlRing, Dev->CtrlRingMap); | |
| + Dev->CtrlQueuePresent = FALSE; | |
| + } | |
| + | |
| ReleaseTxRing: | |
| VirtioNetUninitRing (Dev, &Dev->TxRing, Dev->TxRingMap); | |
| diff --git a/OvmfPkg/VirtioNetDxe/SnpShutdown.c b/OvmfPkg/VirtioNetDxe/SnpShutdown.c | |
| index a589d458ab..b43ef87d1c 100644 | |
| --- a/OvmfPkg/VirtioNetDxe/SnpShutdown.c | |
| +++ b/OvmfPkg/VirtioNetDxe/SnpShutdown.c | |
| @@ -60,6 +60,11 @@ VirtioNetShutdown ( | |
| Dev->VirtIo->SetDeviceStatus (Dev->VirtIo, 0); | |
| VirtioNetShutdownRx (Dev); | |
| VirtioNetShutdownTx (Dev); | |
| + if (Dev->CtrlQueuePresent) { | |
| + VirtioNetUninitRing (Dev, &Dev->CtrlRing, Dev->CtrlRingMap); | |
| + Dev->CtrlQueuePresent = FALSE; | |
| + } | |
| + | |
| VirtioNetUninitRing (Dev, &Dev->TxRing, Dev->TxRingMap); | |
| VirtioNetUninitRing (Dev, &Dev->RxRing, Dev->RxRingMap); | |
| diff --git a/OvmfPkg/VirtioNetDxe/VirtioNet.h b/OvmfPkg/VirtioNetDxe/VirtioNet.h | |
| index 719c575132..f1e8f7513b 100644 | |
| --- a/OvmfPkg/VirtioNetDxe/VirtioNet.h | |
| +++ b/OvmfPkg/VirtioNetDxe/VirtioNet.h | |
| @@ -96,6 +96,18 @@ typedef struct { | |
| VOID *TxSharedReqMap; // VirtioNetInitTx | |
| UINT16 TxLastUsed; // VirtioNetInitTx | |
| ORDERED_COLLECTION *TxBufCollection; // VirtioNetInitTx | |
| + | |
| + // | |
| + // Control virtqueue. Allocated only when both VIRTIO_NET_F_CTRL_VQ and | |
| + // VIRTIO_NET_F_MQ are negotiated; used by VirtioNetInitialize() to pin the | |
| + // device to a single virtqueue pair so well-behaved devices steer all RX | |
| + // to receiveq1, where this driver actually polls. | |
| + // | |
| + BOOLEAN CtrlQueuePresent; // VirtioNetInitialize | |
| + UINT16 CtrlQueueIndex; // VirtioNetInitialize | |
| + VRING CtrlRing; // VirtioNetInitRing | |
| + VOID *CtrlRingMap; // VirtioRingMap and | |
| + // VirtioNetInitRing | |
| } VNET_DEV; | |
| // | |
| @@ -275,6 +287,20 @@ VirtioNetUninitRing ( | |
| IN VOID *RingMap | |
| ); | |
| +// | |
| +// Control virtqueue command submission. Only callable after the device has | |
| +// been switched to VSTAT_DRIVER_OK and Dev->CtrlQueuePresent is TRUE. | |
| +// | |
| +EFI_STATUS | |
| +EFIAPI | |
| +VirtioNetSendCtrlCommand ( | |
| + IN OUT VNET_DEV *Dev, | |
| + IN UINT8 Class, | |
| + IN UINT8 Command, | |
| + IN VOID *Data, | |
| + IN UINTN DataLen | |
| + ); | |
| + | |
| // | |
| // utility functions to map caller-supplied Tx buffer system physical address | |
| // to a device address and vice versa | |
| diff --git a/OvmfPkg/VirtioNetDxe/VirtioNet.inf b/OvmfPkg/VirtioNetDxe/VirtioNet.inf | |
| index ada84ed554..45faf4caf3 100644 | |
| --- a/OvmfPkg/VirtioNetDxe/VirtioNet.inf | |
| +++ b/OvmfPkg/VirtioNetDxe/VirtioNet.inf | |
| @@ -34,6 +34,7 @@ | |
| SnpTransmit.c | |
| SnpUnsupported.c | |
| VirtioNet.h | |
| + VirtioNetCtrl.c | |
| [Packages] | |
| MdePkg/MdePkg.dec | |
| diff --git a/OvmfPkg/VirtioNetDxe/VirtioNetCtrl.c b/OvmfPkg/VirtioNetDxe/VirtioNetCtrl.c | |
| new file mode 100644 | |
| index 0000000000..e7eab05e47 | |
| --- /dev/null | |
| +++ b/OvmfPkg/VirtioNetDxe/VirtioNetCtrl.c | |
| @@ -0,0 +1,166 @@ | |
| +/** @file | |
| + | |
| + Helper to issue commands on the virtio-net control virtqueue. | |
| + | |
| + Used by VirtioNetInitialize() to pin a multiqueue-capable device to a | |
| + single virtqueue pair (see VIRTIO_NET_CTRL_MQ_VQ_PAIRS_SET). The driver | |
| + itself is single-queue; the SET-pairs command exists purely to make | |
| + spec-conformant devices (including any vhost-net backed by a multi-queue | |
| + tap) steer all RX into receiveq1, where this driver actually polls. | |
| + | |
| + Copyright (C) 2026, Zentria O�. | |
| + | |
| + SPDX-License-Identifier: BSD-2-Clause-Patent | |
| + | |
| +**/ | |
| + | |
| +#include <Library/BaseLib.h> | |
| +#include <Library/BaseMemoryLib.h> | |
| +#include <Library/VirtioLib.h> | |
| + | |
| +#include "VirtioNet.h" | |
| + | |
| +/** | |
| + Submit a single command on the control virtqueue and wait for completion. | |
| + | |
| + The buffer laid out for the device is: | |
| + | |
| + [VIRTIO_NET_CTRL_HDR { Class, Command }] [Data (DataLen bytes)] [Status] | |
| + | |
| + The first two regions are device-read; the trailing status byte is | |
| + device-write. We allocate one shared common-buffer mapping for the whole | |
| + thing so the same EFI_PHYSICAL_ADDRESS works in both directions, then | |
| + describe it to the device with three chained descriptors. | |
| + | |
| + @param[in,out] Dev VNET_DEV with CtrlQueuePresent == TRUE and the | |
| + device already in VSTAT_DRIVER_OK state. | |
| + @param[in] Class VIRTIO_NET_CTRL_HDR.Class value. | |
| + @param[in] Command VIRTIO_NET_CTRL_HDR.Command value. | |
| + @param[in] Data Command-specific payload. Must be non-NULL when | |
| + DataLen > 0. | |
| + @param[in] DataLen Size of Data in bytes. Must be > 0; every virtio-net | |
| + control command defined to date carries a payload. | |
| + | |
| + @retval EFI_SUCCESS The device acknowledged the command with | |
| + VIRTIO_NET_OK. | |
| + @retval EFI_DEVICE_ERROR The device returned VIRTIO_NET_ERR. | |
| + @return Status codes from AllocateSharedPages, | |
| + VirtioMapAllBytesInSharedBuffer, or VirtioFlush. | |
| +**/ | |
| +EFI_STATUS | |
| +EFIAPI | |
| +VirtioNetSendCtrlCommand ( | |
| + IN OUT VNET_DEV *Dev, | |
| + IN UINT8 Class, | |
| + IN UINT8 Command, | |
| + IN VOID *Data, | |
| + IN UINTN DataLen | |
| + ) | |
| +{ | |
| + EFI_STATUS Status; | |
| + UINTN BufSize; | |
| + UINTN NumPages; | |
| + VOID *Buffer; | |
| + UINT8 *Bytes; | |
| + volatile UINT8 *StatusByte; | |
| + EFI_PHYSICAL_ADDRESS DeviceAddress; | |
| + VOID *BufferMap; | |
| + DESC_INDICES Indices; | |
| + | |
| + ASSERT (Dev->CtrlQueuePresent); | |
| + ASSERT (Data != NULL); | |
| + ASSERT (DataLen > 0); | |
| + ASSERT (DataLen <= MAX_UINT32); | |
| + | |
| + BufSize = sizeof (VIRTIO_NET_CTRL_HDR) + DataLen + sizeof (UINT8); | |
| + NumPages = EFI_SIZE_TO_PAGES (BufSize); | |
| + | |
| + Status = Dev->VirtIo->AllocateSharedPages (Dev->VirtIo, NumPages, &Buffer); | |
| + if (EFI_ERROR (Status)) { | |
| + return Status; | |
| + } | |
| + | |
| + ZeroMem (Buffer, BufSize); | |
| + Bytes = Buffer; | |
| + Bytes[0] = Class; | |
| + Bytes[1] = Command; | |
| + CopyMem (Bytes + sizeof (VIRTIO_NET_CTRL_HDR), Data, DataLen); | |
| + StatusByte = (volatile UINT8 *)(Bytes + sizeof (VIRTIO_NET_CTRL_HDR) + DataLen); | |
| + // | |
| + // Pre-seed the status byte to ERR so a misbehaving device that forgets to | |
| + // write it doesn't get accidentally treated as success. | |
| + // | |
| + *StatusByte = VIRTIO_NET_ERR; | |
| + | |
| + Status = VirtioMapAllBytesInSharedBuffer ( | |
| + Dev->VirtIo, | |
| + VirtioOperationBusMasterCommonBuffer, | |
| + Buffer, | |
| + BufSize, | |
| + &DeviceAddress, | |
| + &BufferMap | |
| + ); | |
| + if (EFI_ERROR (Status)) { | |
| + goto FreeBuffer; | |
| + } | |
| + | |
| + VirtioPrepare (&Dev->CtrlRing, &Indices); | |
| + | |
| + // | |
| + // desc[0]: header (device-read, chained) | |
| + // | |
| + VirtioAppendDesc ( | |
| + &Dev->CtrlRing, | |
| + DeviceAddress, | |
| + (UINT32)sizeof (VIRTIO_NET_CTRL_HDR), | |
| + VRING_DESC_F_NEXT, | |
| + &Indices | |
| + ); | |
| + | |
| + // | |
| + // desc[1]: payload (device-read, chained). VRING_DESC_F_WRITE intentionally | |
| + // unset -- the device only writes the trailing status byte. | |
| + // | |
| + VirtioAppendDesc ( | |
| + &Dev->CtrlRing, | |
| + DeviceAddress + sizeof (VIRTIO_NET_CTRL_HDR), | |
| + (UINT32)DataLen, | |
| + VRING_DESC_F_NEXT, | |
| + &Indices | |
| + ); | |
| + | |
| + // | |
| + // desc[2]: status (device-write, terminal) | |
| + // | |
| + VirtioAppendDesc ( | |
| + &Dev->CtrlRing, | |
| + DeviceAddress + sizeof (VIRTIO_NET_CTRL_HDR) + DataLen, | |
| + (UINT32)sizeof (UINT8), | |
| + VRING_DESC_F_WRITE, | |
| + &Indices | |
| + ); | |
| + | |
| + Status = VirtioFlush ( | |
| + Dev->VirtIo, | |
| + Dev->CtrlQueueIndex, | |
| + &Dev->CtrlRing, | |
| + &Indices, | |
| + NULL | |
| + ); | |
| + if (EFI_ERROR (Status)) { | |
| + goto UnmapBuffer; | |
| + } | |
| + | |
| + MemoryFence (); | |
| + if (*StatusByte != VIRTIO_NET_OK) { | |
| + Status = EFI_DEVICE_ERROR; | |
| + } | |
| + | |
| +UnmapBuffer: | |
| + Dev->VirtIo->UnmapSharedBuffer (Dev->VirtIo, BufferMap); | |
| + | |
| +FreeBuffer: | |
| + Dev->VirtIo->FreeSharedPages (Dev->VirtIo, NumPages, Buffer); | |
| + | |
| + return Status; | |
| +} | |
| -- | |
| 2.53.0 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment