git: 6192776124c5 - stable/13 - Add a virtio-input device emulation.

From: Corvin Köhne <corvink_at_FreeBSD.org>
Date: Tue, 24 Jan 2023 07:28:00 UTC
The branch stable/13 has been updated by corvink:

URL: https://cgit.FreeBSD.org/src/commit/?id=6192776124c5b79521ca290dfbf5f62560c4a163

commit 6192776124c5b79521ca290dfbf5f62560c4a163
Author:     Corvin Köhne <C.Koehne@beckhoff.com>
AuthorDate: 2021-06-08 08:56:43 +0000
Commit:     Corvin Köhne <corvink@FreeBSD.org>
CommitDate: 2023-01-24 06:42:27 +0000

    Add a virtio-input device emulation.
    
    This will be used to inject keyboard/mouse input events into a guest.
    The command line syntax is:
       -s <slot>,virtio-input,/dev/input/eventX
    
    Reviewed by:    jhb (bhyve), grehan
    Obtained from:  Corvin Köhne <C.Koehne@beckhoff.com>
    MFC after:      3 weeks
    Relnotes:       yes
    Differential Revision:  https://reviews.freebsd.org/D30020
    
    (cherry picked from commit 054accac71e0944ab588c3ab052bea6947df7885)
---
 usr.sbin/bhyve/Makefile           |   1 +
 usr.sbin/bhyve/bhyve.8            |  10 +
 usr.sbin/bhyve/bhyve_config.5     |  11 +
 usr.sbin/bhyve/pci_virtio_input.c | 782 ++++++++++++++++++++++++++++++++++++++
 usr.sbin/bhyve/virtio.c           |  10 +-
 usr.sbin/bhyve/virtio.h           |  16 +
 6 files changed, 828 insertions(+), 2 deletions(-)

diff --git a/usr.sbin/bhyve/Makefile b/usr.sbin/bhyve/Makefile
index 5147bb6b71d9..bd78a3362672 100644
--- a/usr.sbin/bhyve/Makefile
+++ b/usr.sbin/bhyve/Makefile
@@ -51,6 +51,7 @@ SRCS=	\
 	pci_virtio_9p.c		\
 	pci_virtio_block.c	\
 	pci_virtio_console.c	\
+	pci_virtio_input.c	\
 	pci_virtio_net.c	\
 	pci_virtio_rnd.c	\
 	pci_virtio_scsi.c	\
diff --git a/usr.sbin/bhyve/bhyve.8 b/usr.sbin/bhyve/bhyve.8
index d9f31a09b7db..c82bbce74c8c 100644
--- a/usr.sbin/bhyve/bhyve.8
+++ b/usr.sbin/bhyve/bhyve.8
@@ -329,6 +329,8 @@ Virtio RNG interface.
 Virtio console interface, which exposes multiple ports
 to the guest in the form of simple char devices for simple IO
 between the guest and host userspaces.
+.It Cm virtio-input
+Virtio input interface.
 .It Cm ahci
 AHCI controller attached to arbitrary devices.
 .It Cm ahci-cd
@@ -591,6 +593,14 @@ resize at present.
 Emergency write is advertised, but no-op at present.
 .El
 .Pp
+Virtio input device backends:
+.Bl -tag -width 10n
+.It Ar /dev/input/eventX
+Send input events of
+.Ar /dev/input/eventX
+to guest by VirtIO Input Interface.
+.El
+.Pp
 Framebuffer devices backends:
 .Bl -bullet
 .Sm off
diff --git a/usr.sbin/bhyve/bhyve_config.5 b/usr.sbin/bhyve/bhyve_config.5
index f14773313180..666c7c907e36 100644
--- a/usr.sbin/bhyve/bhyve_config.5
+++ b/usr.sbin/bhyve/bhyve_config.5
@@ -275,6 +275,8 @@ VirtIO 9p (VirtFS) interface.
 VirtIO block storage interface.
 .It Li virtio-console
 VirtIO console interface.
+.It Li virtio-input
+VirtIO input interface.
 .It Li virtio-net
 VirtIO network interface.
 .It Li virtio-rnd
@@ -631,6 +633,15 @@ The name of the port exposed to the guest.
 .It Va path Ta path Ta Ta
 The path of a UNIX domain socket providing the host connection for the port.
 .El
+.Ss VirtIO Input Interface Settings
+Each VirtIO Input device contains one input event device.
+All input events of the input event device are send to the guest by VirtIO Input interface.
+VirtIO Input Interfaces support the following variables:
+.Bl -column "Name" "Format" "Default"
+.It Sy Name Ta Sy Format Ta Sy Default Ta Sy Description
+.It Va path Ta path Ta Ta
+The path of the input event device exposed to the guest
+.El
 .Ss VirtIO Network Interface Settings
 In addition to the network backend settings,
 VirtIO network interfaces support the following variables:
diff --git a/usr.sbin/bhyve/pci_virtio_input.c b/usr.sbin/bhyve/pci_virtio_input.c
new file mode 100644
index 000000000000..4517333b1603
--- /dev/null
+++ b/usr.sbin/bhyve/pci_virtio_input.c
@@ -0,0 +1,782 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
+ *
+ * Copyright (c) 2021 Beckhoff Automation GmbH & Co. KG
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer
+ *    in this position and unchanged.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * virtio input device emulation.
+ */
+
+#include <sys/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#include <sys/param.h>
+#ifndef WITHOUT_CAPSICUM
+#include <sys/capsicum.h>
+
+#include <capsicum_helpers.h>
+#endif
+#include <sys/ioctl.h>
+#include <sys/linker_set.h>
+#include <sys/uio.h>
+
+#include <dev/evdev/input.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "bhyverun.h"
+#include "config.h"
+#include "debug.h"
+#include "mevent.h"
+#include "pci_emul.h"
+#include "virtio.h"
+
+#define VTINPUT_RINGSZ 64
+
+#define VTINPUT_MAX_PKT_LEN 10
+
+/*
+ * Queue definitions.
+ */
+#define VTINPUT_EVENTQ 0
+#define VTINPUT_STATUSQ 1
+
+#define VTINPUT_MAXQ 2
+
+static int pci_vtinput_debug;
+#define DPRINTF(params)        \
+	if (pci_vtinput_debug) \
+	PRINTLN params
+#define WPRINTF(params) PRINTLN params
+
+enum vtinput_config_select {
+	VTINPUT_CFG_UNSET = 0x00,
+	VTINPUT_CFG_ID_NAME = 0x01,
+	VTINPUT_CFG_ID_SERIAL = 0x02,
+	VTINPUT_CFG_ID_DEVIDS = 0x03,
+	VTINPUT_CFG_PROP_BITS = 0x10,
+	VTINPUT_CFG_EV_BITS = 0x11,
+	VTINPUT_CFG_ABS_INFO = 0x12
+};
+
+struct vtinput_absinfo {
+	uint32_t min;
+	uint32_t max;
+	uint32_t fuzz;
+	uint32_t flat;
+	uint32_t res;
+} __packed;
+
+struct vtinput_devids {
+	uint16_t bustype;
+	uint16_t vendor;
+	uint16_t product;
+	uint16_t version;
+} __packed;
+
+struct vtinput_config {
+	uint8_t select;
+	uint8_t subsel;
+	uint8_t size;
+	uint8_t reserved[5];
+	union {
+		char string[128];
+		uint8_t bitmap[128];
+		struct vtinput_absinfo abs;
+		struct vtinput_devids ids;
+	} u;
+} __packed;
+
+struct vtinput_event {
+	uint16_t type;
+	uint16_t code;
+	uint32_t value;
+} __packed;
+
+struct vtinput_event_elem {
+	struct vtinput_event event;
+	struct iovec iov;
+	uint16_t idx;
+};
+
+struct vtinput_eventqueue {
+	struct vtinput_event_elem *events;
+	uint32_t size;
+	uint32_t idx;
+};
+
+/*
+ * Per-device softc
+ */
+struct pci_vtinput_softc {
+	struct virtio_softc vsc_vs;
+	struct vqueue_info vsc_queues[VTINPUT_MAXQ];
+	pthread_mutex_t vsc_mtx;
+	const char *vsc_evdev;
+	int vsc_fd;
+	struct vtinput_config vsc_config;
+	int vsc_config_valid;
+	struct mevent *vsc_evp;
+	struct vtinput_eventqueue vsc_eventqueue;
+};
+
+static void pci_vtinput_reset(void *);
+static int pci_vtinput_cfgread(void *, int, int, uint32_t *);
+static int pci_vtinput_cfgwrite(void *, int, int, uint32_t);
+
+static struct virtio_consts vtinput_vi_consts = {
+	"vtinput",		       /* our name */
+	VTINPUT_MAXQ,		       /* we support 1 virtqueue */
+	sizeof(struct vtinput_config), /* config reg size */
+	pci_vtinput_reset,	       /* reset */
+	NULL,			       /* device-wide qnotify -- not used */
+	pci_vtinput_cfgread,	       /* read virtio config */
+	pci_vtinput_cfgwrite,	       /* write virtio config */
+	NULL,			       /* apply negotiated features */
+	0,			       /* our capabilities */
+};
+
+static void
+pci_vtinput_reset(void *vsc)
+{
+	struct pci_vtinput_softc *sc = vsc;
+
+	DPRINTF(("%s: device reset requested", __func__));
+	vi_reset_dev(&sc->vsc_vs);
+}
+
+static void
+pci_vtinput_notify_eventq(void *vsc, struct vqueue_info *vq)
+{
+	DPRINTF(("%s", __func__));
+}
+
+static void
+pci_vtinput_notify_statusq(void *vsc, struct vqueue_info *vq)
+{
+	struct pci_vtinput_softc *sc = vsc;
+
+	while (vq_has_descs(vq)) {
+		/* get descriptor chain */
+		struct iovec iov;
+		struct vi_req req;
+		const int n = vq_getchain(vq, &iov, 1, &req);
+		if (n <= 0) {
+			WPRINTF(("%s: invalid descriptor: %d", __func__, n));
+			return;
+		}
+
+		/* get event */
+		struct vtinput_event event;
+		memcpy(&event, iov.iov_base, sizeof(event));
+
+		/*
+		 * on multi touch devices:
+		 * - host send EV_MSC to guest
+		 * - guest sends EV_MSC back to host
+		 * - host writes EV_MSC to evdev
+		 * - evdev saves EV_MSC in it's event buffer
+		 * - host receives an extra EV_MSC by reading the evdev event
+		 *   buffer
+		 * - frames become larger and larger
+		 * avoid endless loops by ignoring EV_MSC
+		 */
+		if (event.type == EV_MSC) {
+			vq_relchain(vq, req.idx, sizeof(event));
+			continue;
+		}
+
+		/* send event to evdev */
+		struct input_event host_event;
+		host_event.type = event.type;
+		host_event.code = event.code;
+		host_event.value = event.value;
+		if (gettimeofday(&host_event.time, NULL) != 0) {
+			WPRINTF(("%s: failed gettimeofday", __func__));
+		}
+		if (write(sc->vsc_fd, &host_event, sizeof(host_event)) == -1) {
+			WPRINTF(("%s: failed to write host_event", __func__));
+		}
+
+		vq_relchain(vq, req.idx, sizeof(event));
+	}
+	vq_endchains(vq, 1);
+}
+
+static int
+pci_vtinput_get_bitmap(struct pci_vtinput_softc *sc, int cmd, int count)
+{
+	if (count <= 0 || !sc) {
+		return (-1);
+	}
+
+	/* query bitmap */
+	memset(sc->vsc_config.u.bitmap, 0, sizeof(sc->vsc_config.u.bitmap));
+	if (ioctl(sc->vsc_fd, cmd, sc->vsc_config.u.bitmap) < 0) {
+		return (-1);
+	}
+
+	/* get number of set bytes in bitmap */
+	for (int i = count - 1; i >= 0; i--) {
+		if (sc->vsc_config.u.bitmap[i]) {
+			return i + 1;
+		}
+	}
+
+	return (-1);
+}
+
+static int
+pci_vtinput_read_config_id_name(struct pci_vtinput_softc *sc)
+{
+	char name[128];
+	if (ioctl(sc->vsc_fd, EVIOCGNAME(sizeof(name) - 1), name) < 0) {
+		return (1);
+	}
+
+	memcpy(sc->vsc_config.u.string, name, sizeof(name));
+	sc->vsc_config.size = strnlen(name, sizeof(name));
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config_id_serial(struct pci_vtinput_softc *sc)
+{
+	/* serial isn't supported */
+	sc->vsc_config.size = 0;
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config_id_devids(struct pci_vtinput_softc *sc)
+{
+	struct input_id devids;
+	if (ioctl(sc->vsc_fd, EVIOCGID, &devids)) {
+		return (1);
+	}
+
+	sc->vsc_config.u.ids.bustype = devids.bustype;
+	sc->vsc_config.u.ids.vendor = devids.vendor;
+	sc->vsc_config.u.ids.product = devids.product;
+	sc->vsc_config.u.ids.version = devids.version;
+	sc->vsc_config.size = sizeof(struct vtinput_devids);
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config_prop_bits(struct pci_vtinput_softc *sc)
+{
+	/*
+	 * Evdev bitmap countains 1 bit per count. Additionally evdev bitmaps
+	 * are arrays of longs instead of chars. Calculate how many longs are
+	 * required for evdev bitmap. Multiply that with sizeof(long) to get the
+	 * number of elements.
+	 */
+	const int count = howmany(INPUT_PROP_CNT, sizeof(long) * 8) *
+	    sizeof(long);
+	const unsigned int cmd = EVIOCGPROP(count);
+	const int size = pci_vtinput_get_bitmap(sc, cmd, count);
+	if (size <= 0) {
+		return (1);
+	}
+
+	sc->vsc_config.size = size;
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config_ev_bits(struct pci_vtinput_softc *sc, uint8_t type)
+{
+	int count;
+
+	switch (type) {
+	case EV_KEY:
+		count = KEY_CNT;
+		break;
+	case EV_REL:
+		count = REL_CNT;
+		break;
+	case EV_ABS:
+		count = ABS_CNT;
+		break;
+	case EV_MSC:
+		count = MSC_CNT;
+		break;
+	case EV_SW:
+		count = SW_CNT;
+		break;
+	case EV_LED:
+		count = LED_CNT;
+		break;
+	default:
+		return (1);
+	}
+
+	/*
+	 * Evdev bitmap countains 1 bit per count. Additionally evdev bitmaps
+	 * are arrays of longs instead of chars. Calculate how many longs are
+	 * required for evdev bitmap. Multiply that with sizeof(long) to get the
+	 * number of elements.
+	 */
+	count = howmany(count, sizeof(long) * 8) * sizeof(long);
+	const unsigned int cmd = EVIOCGBIT(sc->vsc_config.subsel, count);
+	const int size = pci_vtinput_get_bitmap(sc, cmd, count);
+	if (size <= 0) {
+		return (1);
+	}
+
+	sc->vsc_config.size = size;
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config_abs_info(struct pci_vtinput_softc *sc)
+{
+	/* check if evdev has EV_ABS */
+	if (!pci_vtinput_read_config_ev_bits(sc, EV_ABS)) {
+		return (1);
+	}
+
+	/* get abs information */
+	struct input_absinfo abs;
+	if (ioctl(sc->vsc_fd, EVIOCGABS(sc->vsc_config.subsel), &abs) < 0) {
+		return (1);
+	}
+
+	/* save abs information */
+	sc->vsc_config.u.abs.min = abs.minimum;
+	sc->vsc_config.u.abs.max = abs.maximum;
+	sc->vsc_config.u.abs.fuzz = abs.fuzz;
+	sc->vsc_config.u.abs.flat = abs.flat;
+	sc->vsc_config.u.abs.res = abs.resolution;
+	sc->vsc_config.size = sizeof(struct vtinput_absinfo);
+
+	return (0);
+}
+
+static int
+pci_vtinput_read_config(struct pci_vtinput_softc *sc)
+{
+	switch (sc->vsc_config.select) {
+	case VTINPUT_CFG_UNSET:
+		return (0);
+	case VTINPUT_CFG_ID_NAME:
+		return pci_vtinput_read_config_id_name(sc);
+	case VTINPUT_CFG_ID_SERIAL:
+		return pci_vtinput_read_config_id_serial(sc);
+	case VTINPUT_CFG_ID_DEVIDS:
+		return pci_vtinput_read_config_id_devids(sc);
+	case VTINPUT_CFG_PROP_BITS:
+		return pci_vtinput_read_config_prop_bits(sc);
+	case VTINPUT_CFG_EV_BITS:
+		return pci_vtinput_read_config_ev_bits(
+		    sc, sc->vsc_config.subsel);
+	case VTINPUT_CFG_ABS_INFO:
+		return pci_vtinput_read_config_abs_info(sc);
+	default:
+		return (1);
+	}
+}
+
+static int
+pci_vtinput_cfgread(void *vsc, int offset, int size, uint32_t *retval)
+{
+	struct pci_vtinput_softc *sc = vsc;
+
+	/* check for valid offset and size */
+	if (offset + size > sizeof(struct vtinput_config)) {
+		WPRINTF(("%s: read to invalid offset/size %d/%d", __func__,
+		    offset, size));
+		memset(retval, 0, size);
+		return (0);
+	}
+
+	/* read new config values, if select and subsel changed. */
+	if (!sc->vsc_config_valid) {
+		if (pci_vtinput_read_config(sc) != 0) {
+			DPRINTF(("%s: could not read config %d/%d", __func__,
+			    sc->vsc_config.select, sc->vsc_config.subsel));
+			memset(retval, 0, size);
+			return (0);
+		}
+		sc->vsc_config_valid = 1;
+	}
+
+	uint8_t *ptr = (uint8_t *)&sc->vsc_config;
+	memcpy(retval, ptr + offset, size);
+
+	return (0);
+}
+
+static int
+pci_vtinput_cfgwrite(void *vsc, int offset, int size, uint32_t value)
+{
+	struct pci_vtinput_softc *sc = vsc;
+
+	/* guest can only write to select and subsel fields */
+	if (offset + size > 2) {
+		WPRINTF(("%s: write to readonly reg %d", __func__, offset));
+		return (1);
+	}
+
+	/* copy value into config */
+	uint8_t *ptr = (uint8_t *)&sc->vsc_config;
+	memcpy(ptr + offset, &value, size);
+
+	/* select/subsel changed, query new config on next cfgread */
+	sc->vsc_config_valid = 0;
+
+	return (0);
+}
+
+static int
+vtinput_eventqueue_add_event(
+    struct vtinput_eventqueue *queue, struct input_event *e)
+{
+	/* check if queue is full */
+	if (queue->idx >= queue->size) {
+		/* alloc new elements for queue */
+		const uint32_t newSize = queue->idx;
+		const void *newPtr = realloc(queue->events,
+		    queue->size * sizeof(struct vtinput_event_elem));
+		if (newPtr == NULL) {
+			WPRINTF(("%s: realloc memory for eventqueue failed!",
+			    __func__));
+			return (1);
+		}
+		/* save new size and eventqueue */
+		queue->events = (struct vtinput_event_elem *)newPtr;
+		queue->size = newSize;
+	}
+
+	/* save event */
+	struct vtinput_event *event = &queue->events[queue->idx].event;
+	event->type = e->type;
+	event->code = e->code;
+	event->value = e->value;
+	queue->idx++;
+
+	return (0);
+}
+
+static void
+vtinput_eventqueue_clear(struct vtinput_eventqueue *queue)
+{
+	/* just reset index to clear queue */
+	queue->idx = 0;
+}
+
+static void
+vtinput_eventqueue_send_events(
+    struct vtinput_eventqueue *queue, struct vqueue_info *vq)
+{
+	/*
+	 * First iteration through eventqueue:
+	 *   Get descriptor chains.
+	 */
+	for (uint32_t i = 0; i < queue->idx; ++i) {
+		/* get descriptor */
+		if (!vq_has_descs(vq)) {
+			/*
+			 * We don't have enough descriptors for all events.
+			 * Return chains back to guest.
+			 */
+			vq_retchains(vq, i);
+			WPRINTF((
+			    "%s: not enough available descriptors, dropping %d events",
+			    __func__, queue->idx));
+			goto done;
+		}
+
+		/* get descriptor chain */
+		struct iovec iov;
+		struct vi_req req;
+		const int n = vq_getchain(vq, &iov, 1, &req);
+		if (n <= 0) {
+			WPRINTF(("%s: invalid descriptor: %d", __func__, n));
+			return;
+		}
+		if (n != 1) {
+			WPRINTF(
+			    ("%s: invalid number of descriptors in chain: %d",
+				__func__, n));
+			/* release invalid chain */
+			vq_relchain(vq, req.idx, 0);
+			return;
+		}
+		if (iov.iov_len < sizeof(struct vtinput_event)) {
+			WPRINTF(("%s: invalid descriptor length: %lu", __func__,
+			    iov.iov_len));
+			/* release invalid chain */
+			vq_relchain(vq, req.idx, 0);
+			return;
+		}
+
+		/* save descriptor */
+		queue->events[i].iov = iov;
+		queue->events[i].idx = req.idx;
+	}
+
+	/*
+	 * Second iteration through eventqueue:
+	 *   Send events to guest by releasing chains
+	 */
+	for (uint32_t i = 0; i < queue->idx; ++i) {
+		struct vtinput_event_elem event = queue->events[i];
+		memcpy(event.iov.iov_base, &event.event,
+		    sizeof(struct vtinput_event));
+		vq_relchain(vq, event.idx, sizeof(struct vtinput_event));
+	}
+done:
+	/* clear queue and send interrupt to guest */
+	vtinput_eventqueue_clear(queue);
+	vq_endchains(vq, 1);
+
+	return;
+}
+
+static int
+vtinput_read_event_from_host(int fd, struct input_event *event)
+{
+	const int len = read(fd, event, sizeof(struct input_event));
+	if (len != sizeof(struct input_event)) {
+		if (len == -1 && errno != EAGAIN) {
+			WPRINTF(("%s: event read failed! len = %d, errno = %d",
+			    __func__, len, errno));
+		}
+
+		/* host doesn't have more events for us */
+		return (1);
+	}
+
+	return (0);
+}
+
+static void
+vtinput_read_event(int fd __attribute((unused)),
+    enum ev_type t __attribute__((unused)), void *arg __attribute__((unused)))
+{
+	struct pci_vtinput_softc *sc = arg;
+
+	/* skip if driver isn't ready */
+	if (!(sc->vsc_vs.vs_status & VIRTIO_CONFIG_STATUS_DRIVER_OK))
+		return;
+
+	/* read all events from host */
+	struct input_event event;
+	while (vtinput_read_event_from_host(sc->vsc_fd, &event) == 0) {
+		/* add events to our queue */
+		vtinput_eventqueue_add_event(&sc->vsc_eventqueue, &event);
+
+		/* only send events to guest on EV_SYN or SYN_REPORT */
+		if (event.type != EV_SYN || event.type != SYN_REPORT) {
+			continue;
+		}
+
+		/* send host events to guest */
+		vtinput_eventqueue_send_events(
+		    &sc->vsc_eventqueue, &sc->vsc_queues[VTINPUT_EVENTQ]);
+	}
+}
+
+static int
+pci_vtinput_legacy_config(nvlist_t *nvl, const char *opts)
+{
+	if (opts == NULL)
+		return (-1);
+
+	/*
+	 * parse opts:
+	 *   virtio-input,/dev/input/eventX
+	 */
+	char *cp = strchr(opts, ',');
+	if (cp == NULL) {
+		set_config_value_node(nvl, "path", opts);
+		return (0);
+	}
+	char *path = strndup(opts, cp - opts);
+	set_config_value_node(nvl, "path", path);
+	free(path);
+
+	return (pci_parse_legacy_config(nvl, cp + 1));
+}
+
+static int
+pci_vtinput_init(struct vmctx *ctx, struct pci_devinst *pi, nvlist_t *nvl)
+{
+	struct pci_vtinput_softc *sc;
+
+	/*
+	 * Keep it here.
+	 * Else it's possible to access it uninitialized by jumping to failed.
+	 */
+	pthread_mutexattr_t mtx_attr = NULL;
+
+	sc = calloc(1, sizeof(struct pci_vtinput_softc));
+
+	sc->vsc_evdev = get_config_value_node(nvl, "path");
+	if (sc->vsc_evdev == NULL) {
+		WPRINTF(("%s: missing required path config value", __func__));
+		goto failed;
+	}
+
+	/*
+	 * open evdev by using non blocking I/O:
+	 *   read from /dev/input/eventX would block our thread otherwise
+	 */
+	sc->vsc_fd = open(sc->vsc_evdev, O_RDWR | O_NONBLOCK);
+	if (sc->vsc_fd < 0) {
+		WPRINTF(("%s: failed to open %s", __func__, sc->vsc_evdev));
+		goto failed;
+	}
+
+	/* check if evdev is really a evdev */
+	int evversion;
+	int error = ioctl(sc->vsc_fd, EVIOCGVERSION, &evversion);
+	if (error < 0) {
+		WPRINTF(("%s: %s is no evdev", __func__, sc->vsc_evdev));
+		goto failed;
+	}
+
+	/* gain exclusive access to evdev */
+	error = ioctl(sc->vsc_fd, EVIOCGRAB, 1);
+	if (error < 0) {
+		WPRINTF(("%s: failed to grab %s", __func__, sc->vsc_evdev));
+		goto failed;
+	}
+
+	if (pthread_mutexattr_init(&mtx_attr)) {
+		WPRINTF(("%s: init mutexattr failed", __func__));
+		goto failed;
+	}
+	if (pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_RECURSIVE)) {
+		WPRINTF(("%s: settype mutexattr failed", __func__));
+		goto failed;
+	}
+	if (pthread_mutex_init(&sc->vsc_mtx, &mtx_attr)) {
+		WPRINTF(("%s: init mutex failed", __func__));
+		goto failed;
+	}
+
+	/* init softc */
+	sc->vsc_eventqueue.idx = 0;
+	sc->vsc_eventqueue.size = VTINPUT_MAX_PKT_LEN;
+	sc->vsc_eventqueue.events = calloc(
+	    sc->vsc_eventqueue.size, sizeof(struct vtinput_event_elem));
+	sc->vsc_config_valid = 0;
+	if (sc->vsc_eventqueue.events == NULL) {
+		WPRINTF(("%s: failed to alloc eventqueue", __func__));
+		goto failed;
+	}
+
+	/* register event handler */
+	sc->vsc_evp = mevent_add(sc->vsc_fd, EVF_READ, vtinput_read_event, sc);
+	if (sc->vsc_evp == NULL) {
+		WPRINTF(("%s: could not register mevent", __func__));
+		goto failed;
+	}
+
+#ifndef WITHOUT_CAPSICUM
+	cap_rights_t rights;
+	cap_rights_init(&rights, CAP_EVENT, CAP_IOCTL, CAP_READ, CAP_WRITE);
+	if (caph_rights_limit(sc->vsc_fd, &rights) == -1) {
+		errx(EX_OSERR, "Unable to apply rights for sandbox");
+	}
+#endif
+
+	/* link virtio to softc */
+	vi_softc_linkup(
+	    &sc->vsc_vs, &vtinput_vi_consts, sc, pi, sc->vsc_queues);
+	sc->vsc_vs.vs_mtx = &sc->vsc_mtx;
+
+	/* init virtio queues */
+	sc->vsc_queues[VTINPUT_EVENTQ].vq_qsize = VTINPUT_RINGSZ;
+	sc->vsc_queues[VTINPUT_EVENTQ].vq_notify = pci_vtinput_notify_eventq;
+	sc->vsc_queues[VTINPUT_STATUSQ].vq_qsize = VTINPUT_RINGSZ;
+	sc->vsc_queues[VTINPUT_STATUSQ].vq_notify = pci_vtinput_notify_statusq;
+
+	/* initialize config space */
+	pci_set_cfgdata16(pi, PCIR_DEVICE, VIRTIO_DEV_INPUT);
+	pci_set_cfgdata16(pi, PCIR_VENDOR, VIRTIO_VENDOR);
+	pci_set_cfgdata8(pi, PCIR_CLASS, PCIC_INPUTDEV);
+	pci_set_cfgdata8(pi, PCIR_SUBCLASS, PCIS_INPUTDEV_OTHER);
+	pci_set_cfgdata8(pi, PCIR_REVID, VIRTIO_REV_INPUT);
+	pci_set_cfgdata16(pi, PCIR_SUBDEV_0, VIRTIO_SUBDEV_INPUT);
+	pci_set_cfgdata16(pi, PCIR_SUBVEND_0, VIRTIO_SUBVEN_INPUT);
+
+	/* add MSI-X table BAR */
+	if (vi_intr_init(&sc->vsc_vs, 1, fbsdrun_virtio_msix()))
+		goto failed;
+	/* add virtio register */
+	vi_set_io_bar(&sc->vsc_vs, 0);
+
+	return (0);
+
+failed:
+	if (sc == NULL) {
+		return (-1);
+	}
+
+	if (sc->vsc_evp)
+		mevent_delete(sc->vsc_evp);
+	if (sc->vsc_eventqueue.events)
+		free(sc->vsc_eventqueue.events);
+	if (sc->vsc_mtx)
+		pthread_mutex_destroy(&sc->vsc_mtx);
+	if (mtx_attr)
+		pthread_mutexattr_destroy(&mtx_attr);
+	if (sc->vsc_fd)
+		close(sc->vsc_fd);
+
+	free(sc);
+
+	return (-1);
+}
+
+struct pci_devemu pci_de_vinput = {
+	.pe_emu = "virtio-input",
+	.pe_init = pci_vtinput_init,
+	.pe_legacy_config = pci_vtinput_legacy_config,
+	.pe_barwrite = vi_pci_write,
+	.pe_barread = vi_pci_read,
+};
+PCI_EMUL_SET(pci_de_vinput);
diff --git a/usr.sbin/bhyve/virtio.c b/usr.sbin/bhyve/virtio.c
index b55b36bb5978..90c3471e5d3b 100644
--- a/usr.sbin/bhyve/virtio.c
+++ b/usr.sbin/bhyve/virtio.c
@@ -602,7 +602,10 @@ vi_pci_read(struct vmctx *ctx, int vcpu, struct pci_devinst *pi,
 		max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
 		if (newoff + size > max)
 			goto bad;
-		error = (*vc->vc_cfgread)(DEV_SOFTC(vs), newoff, size, &value);
+		if (vc->vc_cfgread != NULL)
+			error = (*vc->vc_cfgread)(DEV_SOFTC(vs), newoff, size, &value);
+		else
+			error = 0;
 		if (!error)
 			goto done;
 	}
@@ -718,7 +721,10 @@ vi_pci_write(struct vmctx *ctx, int vcpu, struct pci_devinst *pi,
 		max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
 		if (newoff + size > max)
 			goto bad;
-		error = (*vc->vc_cfgwrite)(DEV_SOFTC(vs), newoff, size, value);
+		if (vc->vc_cfgwrite != NULL)
+			error = (*vc->vc_cfgwrite)(DEV_SOFTC(vs), newoff, size, value);
+		else
+			error = 0;
 		if (!error)
 			goto done;
 	}
diff --git a/usr.sbin/bhyve/virtio.h b/usr.sbin/bhyve/virtio.h
index 51c57da8c46d..7b50969e57a9 100644
--- a/usr.sbin/bhyve/virtio.h
+++ b/usr.sbin/bhyve/virtio.h
@@ -171,6 +171,22 @@
 #define	VIRTIO_DEV_SCSI		0x1004
 #define	VIRTIO_DEV_RANDOM	0x1005
 #define	VIRTIO_DEV_9P		0x1009
+#define VIRTIO_DEV_INPUT	0x1052
+
+/*
+ * PCI revision IDs
+ */
+#define VIRTIO_REV_INPUT	1
+
+/*
+ * PCI subvendor IDs
+ */
+#define VIRTIO_SUBVEN_INPUT	0x108E
+
+/*
+ * PCI subdevice IDs
+ */
+#define VIRTIO_SUBDEV_INPUT	0x1100
 
 /* From section 2.3, "Virtqueue Configuration", of the virtio specification */
 static inline int