Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save mikroskeem/34054a024addfce249eae526bb50d933 to your computer and use it in GitHub Desktop.

Select an option

Save mikroskeem/34054a024addfce249eae526bb50d933 to your computer and use it in GitHub Desktop.
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