[PATCH 06/13] virtio: Implement a proper sandbox emulator
Simon Glass
sjg at chromium.org
Wed May 27 18:10:16 CEST 2026
The existing sandbox implementation of virtio only tests the basic API.
It is not able to provide a block device, for example.
Add a new implementation which operaties at a higher level. It makes use
of the existing MMIO driver to perform virtio operations.
This emulator-device should be the parent of a function-specific
emulator. That emulator uses this MMIO transport to communicate with the
controller:
virtio-blk {
compatible = "sandbox,virtio-blk-emul";
mmio {
compatible = "sandbox,virtio-emul";
};
};
A new UCLASS_VIRTIO_EMUL uclass is created for the child devices, which
implement the actual function (block device, random-number generator,
etc.)
Signed-off-by: Simon Glass <sjg at chromium.org>
---
arch/Kconfig | 1 +
configs/tools-only_defconfig | 1 +
drivers/virtio/Kconfig | 8 +
drivers/virtio/Makefile | 1 +
drivers/virtio/sandbox_emul.c | 325 ++++++++++++++++++++++++++++++++++
drivers/virtio/sandbox_emul.h | 160 +++++++++++++++++
include/dm/uclass-id.h | 1 +
7 files changed, 497 insertions(+)
create mode 100644 drivers/virtio/sandbox_emul.c
create mode 100644 drivers/virtio/sandbox_emul.h
diff --git a/arch/Kconfig b/arch/Kconfig
index e28e4c4bce7..86fe2943de9 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -261,6 +261,7 @@ config SANDBOX
imply VIRTIO_MMIO
imply VIRTIO_PCI
imply VIRTIO_SANDBOX
+ imply VIRTIO_SANDBOX_EMUL
# Re-enable this when fully implemented
# imply VIRTIO_BLK
imply VIRTIO_NET
diff --git a/configs/tools-only_defconfig b/configs/tools-only_defconfig
index 3b7eea55f77..aa9bd32e848 100644
--- a/configs/tools-only_defconfig
+++ b/configs/tools-only_defconfig
@@ -39,5 +39,6 @@ CONFIG_TIMER=y
CONFIG_VIRTIO_MMIO=n
CONFIG_VIRTIO_PCI=n
CONFIG_VIRTIO_SANDBOX=n
+CONFIG_VIRTIO_SANDBOX_EMUL=n
CONFIG_GENERATE_ACPI_TABLE=n
CONFIG_TOOLS_MKEFICAPSULE=y
diff --git a/drivers/virtio/Kconfig b/drivers/virtio/Kconfig
index 512ac376f18..858556fe802 100644
--- a/drivers/virtio/Kconfig
+++ b/drivers/virtio/Kconfig
@@ -54,6 +54,14 @@ config VIRTIO_SANDBOX
This driver provides support for Sandbox implementation of virtio
transport driver which is used for testing purpose only.
+config VIRTIO_SANDBOX_EMUL
+ bool "Sandbox MMIO emulator for virtio devices"
+ depends on SANDBOX
+ select VIRTIO
+ help
+ This driver provides an MMIO interface to an emulation of a block
+ device. It is used for testing purpose only.
+
config VIRTIO_NET
bool "virtio net driver"
depends on VIRTIO && NETDEVICES
diff --git a/drivers/virtio/Makefile b/drivers/virtio/Makefile
index 4c63a6c6904..d928c7b0ad2 100644
--- a/drivers/virtio/Makefile
+++ b/drivers/virtio/Makefile
@@ -8,6 +8,7 @@ obj-$(CONFIG_VIRTIO_MMIO) += virtio_mmio.o
obj-$(CONFIG_VIRTIO_PCI) += virtio_pci_modern.o
obj-$(CONFIG_VIRTIO_PCI_LEGACY) += virtio_pci_legacy.o
obj-$(CONFIG_VIRTIO_SANDBOX) += virtio_sandbox.o
+obj-$(CONFIG_VIRTIO_SANDBOX_EMUL) += sandbox_emul.o
obj-$(CONFIG_VIRTIO_NET) += virtio_net.o
obj-$(CONFIG_VIRTIO_BLK) += virtio_blk.o
obj-$(CONFIG_VIRTIO_RNG) += virtio_rng.o
diff --git a/drivers/virtio/sandbox_emul.c b/drivers/virtio/sandbox_emul.c
new file mode 100644
index 00000000000..673575806ac
--- /dev/null
+++ b/drivers/virtio/sandbox_emul.c
@@ -0,0 +1,325 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * VirtIO Sandbox emulator, for testing purpose only. This emulates the QEMU
+ * side of virtio, using the MMIO driver and handling any accesses
+ *
+ * This handles traffic from the virtio_ring
+ *
+ * Copyright 2025 Simon Glass <sjg at chromium.org>
+ */
+
+#define LOG_CATEGORY UCLASS_VIRTIO
+
+#include <dm.h>
+#include <malloc.h>
+#include <virtio.h>
+#include <asm/io.h>
+#include <asm/state.h>
+#include <linux/bitops.h>
+#include <linux/kernel.h>
+#include <linux/sizes.h>
+#include "sandbox_emul.h"
+#include "virtio_types.h"
+#include "virtio_blk.h"
+#include "virtio_internal.h"
+#include "virtio_mmio.h"
+#include "virtio_ring.h"
+
+enum {
+ MMIO_SIZE = 0x200,
+ VENDOR_ID = 0xf003,
+ DEVICE_ID = VIRTIO_ID_BLOCK,
+ DISK_SIZE_MB = 16,
+};
+
+/* Replace the low 32 bits of @v with @val, keeping the high 32 bits */
+static u64 set_low32(u64 v, u32 val)
+{
+ return (v & GENMASK_ULL(63, 32)) | val;
+}
+
+/* Replace the high 32 bits of @v with @val, keeping the low 32 bits */
+static u64 set_high32(u64 v, u32 val)
+{
+ return (v & GENMASK_ULL(31, 0)) | ((u64)val << 32);
+}
+
+void process_queue(struct udevice *emul_dev, struct sandbox_emul_priv *priv,
+ uint32_t queue_idx)
+{
+ struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
+ bool processed_something = false;
+ struct virtio_emul_queue *q;
+ struct vring_avail *avail;
+ struct vring_desc *desc;
+ struct vring_used *used;
+ uint old_used_idx;
+
+ if (queue_idx >= priv->num_queues)
+ return;
+ log_debug("Notified on queue %u\n", queue_idx);
+
+ q = &priv->queues[queue_idx];
+ if (!q->ready)
+ return;
+
+ desc = (struct vring_desc *)q->desc_addr;
+ avail = (struct vring_avail *)q->avail_addr;
+ used = (struct vring_used *)q->used_addr;
+ old_used_idx = used->idx;
+
+ while (q->last_avail_idx != avail->idx) {
+ uint ring_idx = q->last_avail_idx % q->num;
+ uint desc_head_idx = avail->ring[ring_idx];
+ uint used_ring_idx;
+ int len;
+ int ret;
+
+ processed_something = true;
+ log_debug("Found request at avail ring index %u (desc head %u)\n",
+ ring_idx, desc_head_idx);
+
+ ret = ops->process_request(emul_dev, desc, desc_head_idx, &len);
+ if (ret)
+ log_warning("Failed to process request (err=%dE)\n",
+ ret);
+
+ used_ring_idx = used->idx % q->num;
+ used->ring[used_ring_idx].id = desc_head_idx;
+ used->ring[used_ring_idx].len = len;
+ used->idx++;
+ q->last_avail_idx++;
+ }
+
+ if (processed_something) {
+ bool needs_interrupt = true;
+
+ log_debug("finished processing, new used_idx is %d.\n",
+ used->idx);
+ if (priv->driver_features & BIT(VIRTIO_RING_F_EVENT_IDX)) {
+ struct {
+ struct vring_avail *avail;
+ unsigned int num;
+ } vr;
+
+ vr.avail = avail;
+ vr.num = q->num;
+
+ needs_interrupt =
+ vring_need_event(vring_used_event((&vr)),
+ used->idx, old_used_idx);
+ log_debug("EVENT_IDX is enabled; driver wants event "
+ "at %u needs_interrupt %d\n",
+ vring_used_event(&vr), needs_interrupt);
+ }
+
+ if (needs_interrupt) {
+ log_debug("sending VRING interrupt\n");
+ priv->interrupt_status |= VIRTIO_MMIO_INT_VRING;
+ }
+ }
+}
+
+long h_read(void *ctx, const void *addr, enum sandboxio_size_t size)
+{
+ struct udevice *dev = ctx;
+ struct udevice *emul_dev = dev_get_parent(dev);
+ struct sandbox_emul_priv *priv = dev_get_priv(dev);
+ ulong offset = (ulong)addr - (ulong)priv->mmio.base;
+ struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
+ struct virtio_emul_queue *q;
+ u32 val = 0;
+
+ if (offset >= VIRTIO_MMIO_CONFIG) {
+ ulong config_offset = offset - VIRTIO_MMIO_CONFIG;
+ int ret;
+
+ ret = ops->get_config(emul_dev, config_offset, &val, size);
+ if (ret)
+ log_warning("Failed to process request (err=%dE)\n",
+ ret);
+ return val;
+ }
+
+ if (priv->queue_sel >= priv->num_queues) {
+ log_debug("invalid queue_sel %d\n", priv->queue_sel);
+ return 0;
+ }
+ q = &priv->queues[priv->queue_sel];
+
+ switch (offset) {
+ case VIRTIO_MMIO_MAGIC_VALUE:
+ return ('v' | 'i' << 8 | 'r' << 16 | 't' << 24);
+ case VIRTIO_MMIO_VERSION:
+ return 2;
+ case VIRTIO_MMIO_DEVICE_ID:
+ return ops->get_device_id(emul_dev);
+ case VIRTIO_MMIO_VENDOR_ID:
+ return VENDOR_ID;
+ case VIRTIO_MMIO_DEVICE_FEATURES:
+ return !priv->features_sel ?
+ lower_32_bits(priv->features) :
+ upper_32_bits(priv->features);
+ case VIRTIO_MMIO_QUEUE_NUM_MAX:
+ return QUEUE_MAX_SIZE;
+ case VIRTIO_MMIO_QUEUE_READY:
+ return q->ready;
+ case VIRTIO_MMIO_INTERRUPT_STATUS:
+ return priv->interrupt_status;
+ case VIRTIO_MMIO_STATUS:
+ return priv->status;
+ case VIRTIO_MMIO_QUEUE_DESC_LOW:
+ return lower_32_bits(q->desc_addr);
+ case VIRTIO_MMIO_QUEUE_DESC_HIGH:
+ return upper_32_bits(q->desc_addr);
+ case VIRTIO_MMIO_QUEUE_AVAIL_LOW:
+ return lower_32_bits(q->avail_addr);
+ case VIRTIO_MMIO_QUEUE_AVAIL_HIGH:
+ return upper_32_bits(q->avail_addr);
+ case VIRTIO_MMIO_QUEUE_USED_LOW:
+ return lower_32_bits(q->used_addr);
+ case VIRTIO_MMIO_QUEUE_USED_HIGH:
+ return upper_32_bits(q->used_addr);
+ case VIRTIO_MMIO_CONFIG_GENERATION:
+ return priv->config_generation;
+ default:
+ log_debug("unhandled read from offset 0x%lx\n", offset);
+ return 0;
+ }
+}
+
+void h_write(void *ctx, void *addr, unsigned int val,
+ enum sandboxio_size_t size)
+{
+ struct udevice *dev = ctx;
+ struct udevice *emul_dev = dev_get_parent(dev);
+ struct sandbox_emul_priv *priv = dev_get_priv(dev);
+ ulong offset = (ulong)addr - (ulong)priv->mmio.base;
+ struct virtio_emul_queue *q;
+
+ if (offset >= VIRTIO_MMIO_CONFIG)
+ return;
+
+ if (priv->queue_sel >= priv->num_queues &&
+ offset != VIRTIO_MMIO_QUEUE_SEL)
+ return;
+ q = &priv->queues[priv->queue_sel];
+
+ switch (offset) {
+ case VIRTIO_MMIO_DEVICE_FEATURES_SEL:
+ priv->features_sel = val;
+ break;
+ case VIRTIO_MMIO_DRIVER_FEATURES:
+ if (priv->features_sel == 0)
+ priv->driver_features = set_low32(priv->driver_features,
+ val);
+ else
+ priv->driver_features = set_high32(priv->driver_features,
+ val);
+ break;
+ case VIRTIO_MMIO_DRIVER_FEATURES_SEL:
+ priv->features_sel = val;
+ break;
+ case VIRTIO_MMIO_QUEUE_SEL:
+ if (val < priv->num_queues)
+ priv->queue_sel = val;
+ else
+ log_debug("tried to select invalid queue %u\n", val);
+ break;
+ case VIRTIO_MMIO_QUEUE_NUM:
+ q->num = (val > 0 && val <= QUEUE_MAX_SIZE) ? val : 0;
+ break;
+ case VIRTIO_MMIO_QUEUE_READY:
+ q->ready = val & 0x1;
+ break;
+ case VIRTIO_MMIO_QUEUE_NOTIFY:
+ process_queue(emul_dev, priv, val);
+ break;
+ case VIRTIO_MMIO_INTERRUPT_ACK:
+ priv->interrupt_status &= ~val;
+ break;
+ case VIRTIO_MMIO_STATUS:
+ priv->status = val;
+ break;
+ case VIRTIO_MMIO_QUEUE_DESC_LOW:
+ q->desc_addr = set_low32(q->desc_addr, val);
+ break;
+ case VIRTIO_MMIO_QUEUE_DESC_HIGH:
+ q->desc_addr = set_high32(q->desc_addr, val);
+ break;
+ case VIRTIO_MMIO_QUEUE_AVAIL_LOW:
+ q->avail_addr = set_low32(q->avail_addr, val);
+ break;
+ case VIRTIO_MMIO_QUEUE_AVAIL_HIGH:
+ q->avail_addr = set_high32(q->avail_addr, val);
+ break;
+ case VIRTIO_MMIO_QUEUE_USED_LOW:
+ q->used_addr = set_low32(q->used_addr, val);
+ break;
+ case VIRTIO_MMIO_QUEUE_USED_HIGH:
+ q->used_addr = set_high32(q->used_addr, val);
+ break;
+ default:
+ log_debug("unhandled write to offset 0x%lx\n", offset);
+ break;
+ }
+}
+
+static int sandbox_emul_of_to_plat(struct udevice *dev)
+{
+ struct udevice *emul_dev = dev_get_parent(dev);
+ struct virtio_emul_ops *ops = virtio_emul_get_ops(emul_dev);
+ struct sandbox_emul_priv *priv = dev_get_priv(dev);
+ int ret;
+
+ /* set up the MMIO base so that virtio_mmio_probe() can find it */
+ priv->mmio.base = memalign(SZ_4K, MMIO_SIZE);
+ if (!priv->mmio.base)
+ return -ENOMEM;
+
+ ret = sandbox_mmio_add(priv->mmio.base, MMIO_SIZE, h_read, h_write,
+ dev);
+ if (ret) {
+ free(priv->mmio.base);
+ return log_msg_ret("sep", ret);
+ }
+
+ priv->num_queues = MAX_VIRTIO_QUEUES;
+ priv->features = BIT(VIRTIO_F_VERSION_1) |
+ BIT(VIRTIO_RING_F_EVENT_IDX) | ops->get_features(emul_dev);
+
+ log_debug("sandbox virtio emulator, mmio %p\n", priv->mmio.base);
+
+ return 0;
+}
+
+static int sandbox_emul_remove(struct udevice *dev)
+{
+ sandbox_mmio_remove(dev);
+
+ return 0;
+}
+
+static const struct udevice_id virtio_sandbox2_ids[] = {
+ { .compatible = "sandbox,virtio-emul" },
+ { }
+};
+
+U_BOOT_DRIVER(virtio_emul) = {
+ .name = "virtio-emul",
+ .id = UCLASS_VIRTIO,
+ .of_match = virtio_sandbox2_ids,
+ .probe = virtio_mmio_probe,
+ .remove = sandbox_emul_remove,
+ .ops = &virtio_mmio_ops,
+ .of_to_plat = sandbox_emul_of_to_plat,
+ .priv_auto = sizeof(struct sandbox_emul_priv),
+};
+
+UCLASS_DRIVER(virtio_emul) = {
+ .name = "virtio_emul",
+ .id = UCLASS_VIRTIO_EMUL,
+#if CONFIG_IS_ENABLED(OF_REAL)
+ .post_bind = dm_scan_fdt_dev,
+#endif
+};
diff --git a/drivers/virtio/sandbox_emul.h b/drivers/virtio/sandbox_emul.h
new file mode 100644
index 00000000000..e206cac3994
--- /dev/null
+++ b/drivers/virtio/sandbox_emul.h
@@ -0,0 +1,160 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * VirtIO Sandbox emulator, for testing purpose only. This emulates the QEMU
+ * side of virtio, using the MMIO driver and handling any accesses
+ *
+ * This handles traffic from the virtio_ring
+ *
+ * Copyright 2025 Simon Glass <sjg at chromium.org>
+ */
+
+#ifndef __SANDBOX_EMUL_H
+#define __SANDBOX_EMUL_H
+
+#include "virtio_mmio.h"
+#include "virtio_types.h"
+
+enum sandboxio_size_t;
+struct udevice;
+struct vring_desc;
+
+enum {
+ MAX_VIRTIO_QUEUES = 8,
+ QUEUE_MAX_SIZE = 256,
+};
+
+/**
+ * struct virtio_emul_queue - Emulator's state for a single virtqueue
+ *
+ * Mirrors the per-queue configuration the driver programs through the
+ * MMIO transport (VIRTIO_MMIO_QUEUE_*) plus the device-side state needed
+ * to consume the driver's requests.
+ *
+ * @num: Queue size, in descriptors. Driver writes via VIRTIO_MMIO_QUEUE_NUM
+ * and must be <= %QUEUE_MAX_SIZE
+ * @ready: Non-zero once the driver has marked this queue as ready for use
+ * (VIRTIO_MMIO_QUEUE_READY). Reset to zero on device reset
+ * @desc_addr: Guest-physical address of the descriptor table for this queue
+ * @avail_addr: Guest-physical address of the available ring
+ * @used_addr: Guest-physical address of the used ring
+ * @last_avail_idx: Index into the available ring of the next request the
+ * device will consume. Advanced each time process_queue() handles a
+ * descriptor chain
+ */
+struct virtio_emul_queue {
+ __virtio32 num;
+ __virtio32 ready;
+ __virtio64 desc_addr;
+ __virtio64 avail_addr;
+ __virtio64 used_addr;
+ __virtio16 last_avail_idx;
+};
+
+/**
+ * struct sandbox_emul_priv - Private info for the emulator
+ *
+ * Holds the per-device state that backs an emulated virtio MMIO transport.
+ * The MMIO callbacks h_read() / h_write() update these fields in response
+ * to driver accesses to the VIRTIO_MMIO_* register window.
+ *
+ * @mmio: Embedded virtio_mmio_priv used to share book-keeping (the MMIO
+ * base in particular) with the regular virtio-mmio driver
+ * @num_queues: Number of virtqueues this device exposes; queues[] is valid
+ * up to this index
+ * @queue_sel: Driver-selected queue index (VIRTIO_MMIO_QUEUE_SEL); names
+ * which entry of queues[] subsequent QUEUE_* accesses refer to
+ * @status: Device status byte (VIRTIO_MMIO_STATUS), as written by the
+ * driver during feature negotiation and bring-up
+ * @features_sel: Which 32-bit half of the 64-bit feature word the driver
+ * is currently reading or writing (VIRTIO_MMIO_DEVICE_FEATURES_SEL /
+ * DRIVER_FEATURES_SEL); 0 selects bits 0..31, 1 selects bits 32..63
+ * @features: Feature bits the device offers (read by the driver via
+ * VIRTIO_MMIO_DEVICE_FEATURES)
+ * @driver_features: Feature bits the driver has acknowledged (written via
+ * VIRTIO_MMIO_DRIVER_FEATURES); the negotiated set is the intersection
+ * with @features
+ * @interrupt_status: Pending interrupt-cause bits (VIRTIO_MMIO_INT_*),
+ * OR'd in when the device wants to interrupt and cleared by the driver
+ * through VIRTIO_MMIO_INTERRUPT_ACK
+ * @config_generation: Counter exposed via VIRTIO_MMIO_CONFIG_GENERATION;
+ * the driver re-reads the config space whenever this value changes
+ * @queues: Per-virtqueue state; only the first @num_queues entries are in
+ * use
+ */
+struct sandbox_emul_priv {
+ struct virtio_mmio_priv mmio;
+ int num_queues;
+ int queue_sel;
+ u32 status;
+ u64 features_sel;
+ u64 features;
+ u64 driver_features;
+ u32 interrupt_status;
+ u32 config_generation;
+ struct virtio_emul_queue queues[MAX_VIRTIO_QUEUES];
+};
+
+/**
+ * struct virtio_emul_ops - Operations for a virtio device emulator
+ *
+ * Each device-type emulator (block, RNG, ...) provides these callbacks. The
+ * transport layer in sandbox_emul.c invokes them in response to driver
+ * activity on the MMIO window: process_request() when the driver notifies a
+ * queue, and the config / feature / device-id accessors when the driver probes
+ * the device.
+ *
+ * @process_request: Handle one descriptor chain from a virtqueue
+ * @get_config: Read from the device-specific configuration space
+ * @get_features: Return the device-specific feature bits
+ * @get_device_id: Return the virtio device ID for this emulator
+ */
+struct virtio_emul_ops {
+ /**
+ * process_request() - Handles a single request from the driver
+ *
+ * @dev: The emulator device
+ * @descs: Pointer to the virtqueue's descriptor table
+ * @head_idx: The index of the first descriptor in the chain for this
+ * request
+ * @lenp: Returns the total number of bytes written by the device into
+ * the driver's buffers (e.g. for a read request and the status
+ * byte). This is what will be placed in the `len` field of the
+ * used ring element.
+ * @return 0 on success, negative on error.
+ */
+ int (*process_request)(struct udevice *dev, struct vring_desc *descs,
+ u32 head_idx, int *lenp);
+
+ /**
+ * get_config() - Reads from the device-specific configuration space
+ *
+ * @dev: The emulator device
+ * @offset: The byte offset into the configuration space to read from
+ * @buf: The buffer to copy the configuration data into
+ * @size: The number of bytes to read
+ * @return 0 on success, negative on error.
+ */
+ int (*get_config)(struct udevice *dev, ulong offset, void *buf,
+ enum sandboxio_size_t size);
+
+ /**
+ * get_features() - Returns the device-specific feature bits
+ *
+ * @dev: The emulator device
+ * @return A bitmask of the device-specific features to be OR'd with
+ * the transport features.
+ */
+ u64 (*get_features)(struct udevice *dev);
+
+ /**
+ * get_device_id() - Returns the virtio device ID
+ *
+ * @dev: The emulator device
+ * @return The virtio device ID for this emulator
+ */
+ u32 (*get_device_id)(struct udevice *dev);
+};
+
+#define virtio_emul_get_ops(dev) ((struct virtio_emul_ops *)(dev)->driver->ops)
+
+#endif
diff --git a/include/dm/uclass-id.h b/include/dm/uclass-id.h
index 36b5d87c304..b3458065e04 100644
--- a/include/dm/uclass-id.h
+++ b/include/dm/uclass-id.h
@@ -34,6 +34,7 @@ enum uclass_id {
UCLASS_PCI_EMUL_PARENT, /* parent for PCI device emulators */
UCLASS_USB_EMUL, /* sandbox USB bus device emulator */
UCLASS_AXI_EMUL, /* sandbox AXI bus device emulator */
+ UCLASS_VIRTIO_EMUL, /* Emulator for a virtIO transport device */
/* U-Boot uclasses start here - in alphabetical order */
UCLASS_ACPI_PMC, /* (x86) Power-management controller (PMC) */
--
2.43.0
More information about the U-Boot
mailing list