virtio: Add config access helpers

At the moment device-specific config access is tailored for a Linux
guest, that performs any access in 8 bits. But config access can have
any size, and modern virtio drivers must use the size of the accessed
field. Add helpers that generalize config accesses.

Signed-off-by: Jean-Philippe Brucker <jean-philippe.brucker@arm.com>
Link: https://lore.kernel.org/r/20220607170239.120084-7-jean-philippe.brucker@arm.com
Signed-off-by: Will Deacon <will@kernel.org>
diff --git a/include/kvm/virtio.h b/include/kvm/virtio.h
index 24179ec..cc4ba1d 100644
--- a/include/kvm/virtio.h
+++ b/include/kvm/virtio.h
@@ -236,6 +236,9 @@
 			   struct virt_queue *vq, size_t nr_descs);
 void virtio_exit_vq(struct kvm *kvm, struct virtio_device *vdev, void *dev,
 		    int num);
+bool virtio_access_config(struct kvm *kvm, struct virtio_device *vdev, void *dev,
+			  unsigned long offset, void *data, size_t size,
+			  bool is_write);
 void virtio_set_guest_features(struct kvm *kvm, struct virtio_device *vdev,
 			       void *dev, u32 features);
 void virtio_notify_status(struct kvm *kvm, struct virtio_device *vdev,
diff --git a/virtio/core.c b/virtio/core.c
index d6f2c68..be0f6f8 100644
--- a/virtio/core.c
+++ b/virtio/core.c
@@ -282,6 +282,44 @@
 		vdev->ops->notify_status(kvm, dev, ext_status);
 }
 
+bool virtio_access_config(struct kvm *kvm, struct virtio_device *vdev,
+			  void *dev, unsigned long offset, void *data,
+			  size_t size, bool is_write)
+{
+	void *in, *out, *config;
+	size_t config_size = vdev->ops->get_config_size(kvm, dev);
+
+	if (WARN_ONCE(offset + size > config_size,
+		      "Config access offset (%lu) is beyond config size (%zu)\n",
+		      offset, config_size))
+		return false;
+
+	config = vdev->ops->get_config(kvm, dev) + offset;
+
+	in = is_write ? data : config;
+	out = is_write ? config : data;
+
+	switch (size) {
+	case 1:
+		*(u8 *)out = *(u8 *)in;
+		break;
+	case 2:
+		*(u16 *)out = *(u16 *)in;
+		break;
+	case 4:
+		*(u32 *)out = *(u32 *)in;
+		break;
+	case 8:
+		*(u64 *)out = *(u64 *)in;
+		break;
+	default:
+		WARN_ONCE(1, "%s: invalid access size\n", __func__);
+		return false;
+	}
+
+	return true;
+}
+
 int virtio_init(struct kvm *kvm, void *dev, struct virtio_device *vdev,
 		struct virtio_ops *ops, enum virtio_trans trans,
 		int device_id, int subsys_id, int class)
diff --git a/virtio/mmio.c b/virtio/mmio.c
index 77289e2..268a439 100644
--- a/virtio/mmio.c
+++ b/virtio/mmio.c
@@ -98,33 +98,6 @@
 	return 0;
 }
 
-static void virtio_mmio_device_specific(struct kvm_cpu *vcpu,
-					u64 addr, u8 *data, u32 len,
-					u8 is_write, struct virtio_device *vdev)
-{
-	struct virtio_mmio *vmmio = vdev->virtio;
-	u8 *config;
-	size_t config_size;
-	u32 i;
-
-	config = vdev->ops->get_config(vmmio->kvm, vmmio->dev);
-	config_size = vdev->ops->get_config_size(vmmio->kvm, vmmio->dev);
-
-	/* Prevent invalid accesses which go beyond the config */
-	if (config_size < addr + len) {
-		WARN_ONCE(1, "Offset (%llu) Length (%u) goes beyond config size (%zu).\n",
-			addr, len, config_size);
-		return;
-	}
-
-	for (i = 0; i < len; i++) {
-		if (is_write)
-			config[addr + i] = *(u8 *)data + i;
-		else
-			data[i] = config[addr + i];
-	}
-}
-
 #define vmmio_selected_vq(vdev, vmmio) \
 	(vdev)->ops->get_vq((vmmio)->kvm, (vmmio)->dev, (vmmio)->hdr.queue_sel)
 
@@ -263,7 +236,8 @@
 
 	if (offset >= VIRTIO_MMIO_CONFIG) {
 		offset -= VIRTIO_MMIO_CONFIG;
-		virtio_mmio_device_specific(vcpu, offset, data, len, is_write, ptr);
+		virtio_access_config(vmmio->kvm, vdev, vmmio->dev, offset, data,
+				     len, is_write);
 		return;
 	}
 
diff --git a/virtio/pci.c b/virtio/pci.c
index 20b1622..85018e7 100644
--- a/virtio/pci.c
+++ b/virtio/pci.c
@@ -135,25 +135,8 @@
 
 		return true;
 	} else if (type == VIRTIO_PCI_O_CONFIG) {
-		u8 cfg;
-		size_t config_size;
-
-		config_size = vdev->ops->get_config_size(kvm, vpci->dev);
-		if (config_offset + size > config_size) {
-			/* Access goes beyond the config size, so return failure. */
-			WARN_ONCE(1, "Config access offset (%u) is beyond config size (%zu)\n",
-				config_offset, config_size);
-			return false;
-		}
-
-		/* TODO: Handle access lengths beyond one byte */
-		if (size != 1) {
-			WARN_ONCE(1, "Size (%u) not supported\n", size);
-			return false;
-		}
-		cfg = vdev->ops->get_config(kvm, vpci->dev)[config_offset];
-		ioport__write8(data, cfg);
-		return true;
+		return virtio_access_config(kvm, vdev, vpci->dev, config_offset,
+					    data, size, false);
 	}
 
 	return false;
@@ -290,24 +273,8 @@
 
 		return true;
 	} else if (type == VIRTIO_PCI_O_CONFIG) {
-		size_t config_size;
-
-		config_size = vdev->ops->get_config_size(kvm, vpci->dev);
-		if (config_offset + size > config_size) {
-			/* Access goes beyond the config size, so return failure. */
-			WARN_ONCE(1, "Config access offset (%u) is beyond config size (%zu)\n",
-				config_offset, config_size);
-			return false;
-		}
-
-		/* TODO: Handle access lengths beyond one byte */
-		if (size != 1) {
-			WARN_ONCE(1, "Size (%u) not supported\n", size);
-			return false;
-		}
-		vdev->ops->get_config(kvm, vpci->dev)[config_offset] = *(u8 *)data;
-
-		return true;
+		return virtio_access_config(kvm, vdev, vpci->dev, config_offset,
+					    data, size, true);
 	}
 
 	return false;