From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 12/27] emulator: Add vhost-user virtio server to btvirt
Date: Thu, 22 Oct 2026 11:46:45 -0400 [thread overview]
Message-ID: <20261022154701.3959500-13-luiz.dentz@gmail.com> (raw)
In-Reply-To: <20261022154701.3959500-1-luiz.dentz@gmail.com>
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add the -V option, which creates a vhost-user server socket on which
each connection gets a new controller, provided to the guest as a virtio
Bluetooth device. This is how User Mode Linux guests, which can only
attach external devices through vhost-user, get emulated controllers.
Packets for the driver are queued until it posts receive buffers, since
the Linux driver keeps only one posted.
Assisted-by: Claude:claude-opus-5-5
---
Makefile.tools | 3 +-
doc/btvirt.rst | 26 +++-
emulator/main.c | 27 +++-
emulator/virtio.c | 323 ++++++++++++++++++++++++++++++++++++++++++++++
emulator/virtio.h | 20 +++
5 files changed, 391 insertions(+), 8 deletions(-)
create mode 100644 emulator/virtio.c
create mode 100644 emulator/virtio.h
diff --git a/Makefile.tools b/Makefile.tools
index cb6c01e5e0d9..6b2adb2f6556 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -103,7 +103,8 @@ emulator_btvirt_SOURCES = emulator/main.c monitor/bt.h \
emulator/smp.c \
emulator/phy.h emulator/phy.c \
emulator/le.h emulator/le.c \
- emulator/vhost.h emulator/vhost.c
+ emulator/vhost.h emulator/vhost.c \
+ emulator/virtio.h emulator/virtio.c
emulator_btvirt_LDADD = lib/libbluetooth-internal.la src/libshared-mainloop.la
emulator_b1ee_SOURCES = emulator/b1ee.c
diff --git a/doc/btvirt.rst b/doc/btvirt.rst
index 2e016406a4fc..c8e2b0be3cc2 100644
--- a/doc/btvirt.rst
+++ b/doc/btvirt.rst
@@ -32,6 +32,8 @@ and speak HCI. They can be exposed to a Bluetooth stack in several ways:
appear as ``hciN`` on the machine running btvirt;
- as H:4 streams on Unix socket servers (**-s**) or a TCP server (**-t**),
one controller per client connection;
+- as virtio Bluetooth devices on a vhost-user server (**-V**), one
+ controller per connection, for virtual machines;
- as an H:4 stream on a pseudo terminal (**-S**).
All controllers created by one btvirt process share an emulated air
@@ -45,7 +47,8 @@ btvirt runs until it receives **SIGINT** or **SIGTERM**.
OPTIONS
=======
-At least one of **-l**, **-s**, **-t**, **-S** or **-U** must be given.
+At least one of **-l**, **-s**, **-t**, **-V**, **-S** or **-U** must be
+given.
Optional arguments must be attached to the option, as in **-l2**,
**-t45551** or **--server=/tmp/bt**.
@@ -78,6 +81,14 @@ Optional arguments must be attached to the option, as in **-l2**,
attached as an H:4 stream. The server listens on
127.0.0.1 only.
+-V[path], --vhost[=path] Create a vhost-user server socket
+ ``bt-vhost-bredrle`` in the directory *path*
+ (default: ``/tmp``). Each connection gets a new
+ BR/EDR/LE controller, of the version given with
+ **-C**, provided to the guest as a virtio
+ Bluetooth device. See **virtio** for how to
+ attach it to QEMU or User Mode Linux guests.
+
-S, --serial Create a pseudo terminal with a BR/EDR/LE
controller attached as an H:4 stream. The path of
the terminal is printed on startup; attach it
@@ -99,12 +110,12 @@ Optional arguments must be attached to the option, as in **-l2**,
-C version, --core=version Core Specification version of the BR/EDR/LE
controllers created with **-l**, **-s** (on
- ``bt-server-bredrle``) and **-t**. Supported
+ ``bt-server-bredrle``), **-t** and **-V**. Supported
values are ``5.0``, ``5.2``, ``6.0`` and ``6.2``
(default).
--d, --debug Enable debug output for the **-l** and **-s**
- controllers.
+-d, --debug Enable debug output for the **-l**, **-s** and
+ **-V** controllers.
-v, --version Show version.
@@ -133,6 +144,13 @@ Serve controllers to virtual machines, one per connecting VM, the way
$ btvirt --server=/tmp/bt
$ test-runner -u/tmp/bt/bt-server-bredrle -k bzImage -- ...
+Provide controllers to User Mode Linux guests as virtio Bluetooth
+devices, two from the same socket::
+
+ $ btvirt -V/tmp/bt
+ $ linux ... virtio_uml.device=/tmp/bt/bt-vhost-bredrle:40 \
+ virtio_uml.device=/tmp/bt/bt-vhost-bredrle:40
+
Emulate a Bluetooth 5.2 controller over TCP::
$ btvirt -C 5.2 -t
diff --git a/emulator/main.c b/emulator/main.c
index 61c2f0d5c33d..b448b6c00723 100644
--- a/emulator/main.c
+++ b/emulator/main.c
@@ -27,6 +27,7 @@
#include "btdev.h"
#include "server.h"
#include "vhci.h"
+#include "virtio.h"
#include "le.h"
static void signal_callback(int signum, void *user_data)
@@ -49,6 +50,8 @@ static void usage(void)
"\t-S Create local serial port\n"
"\t-s[path=/tmp] Create local server sockets\n"
"\t-t[port=45550] Create a TCP server\n"
+ "\t-V[path=/tmp] Create local vhost-user virtio\n"
+ "\t server socket\n"
"\t-l[num] Number of local controllers\n"
"\t-L Create LE only controller\n"
"\t-U[num] Number of test LE controllers\n"
@@ -56,7 +59,7 @@ static void usage(void)
"\t-A Create AMP controller\n"
"\t-C, --core=<version> Core Specification version of the\n"
"\t BR/EDR/LE controllers created with\n"
- "\t -l, -s and -t:\n"
+ "\t -l, -s, -t and -V:\n"
"\t 5.0, 5.2, 6.0 or 6.2 (default)\n"
"\t-h, --help Show help options\n");
}
@@ -66,6 +69,7 @@ static const struct option main_options[] = {
{ "serial", no_argument, NULL, 'S' },
{ "server", optional_argument, NULL, 's' },
{ "tcp", optional_argument, NULL, 't' },
+ { "vhost", optional_argument, NULL, 'V' },
{ "local", optional_argument, NULL, 'l' },
{ "le", no_argument, NULL, 'L' },
{ "bredr", no_argument, NULL, 'B' },
@@ -126,6 +130,7 @@ int main(int argc, char *argv[])
bool server_enabled = false;
const char *server_path = "/tmp";
uint16_t tcp_port = 0;
+ const char *vhost_path = NULL;
bool serial_enabled = false;
int letest_count = 0;
int vhci_count = 0;
@@ -140,7 +145,7 @@ int main(int argc, char *argv[])
for (;;) {
int opt;
- opt = getopt_long(argc, argv, "dSs::t::l::LBAC:U::vh",
+ opt = getopt_long(argc, argv, "dSs::t::V::l::LBAC:U::vh",
main_options, NULL);
if (opt < 0)
break;
@@ -163,6 +168,9 @@ int main(int argc, char *argv[])
else
tcp_port = 45550;
break;
+ case 'V':
+ vhost_path = optarg ? optarg : "/tmp";
+ break;
case 'l':
if (optarg)
vhci_count = atoi(optarg);
@@ -209,7 +217,7 @@ int main(int argc, char *argv[])
type = bredrle_type;
if (letest_count < 1 && vhci_count < 1 && !server_enabled &&
- !tcp_port && !serial_enabled) {
+ !tcp_port && !serial_enabled && !vhost_path) {
fprintf(stderr, "No emulator specified\n");
return EXIT_FAILURE;
}
@@ -306,5 +314,18 @@ int main(int argc, char *argv[])
fprintf(stderr, "Listening TCP on 127.0.0.1:%d\n", tcp_port);
}
+ if (vhost_path) {
+ struct vtio *vtio;
+ char path[PATH_MAX];
+
+ snprintf(path, sizeof(path), "%s/%s", vhost_path,
+ "bt-vhost-bredrle");
+ vtio = vtio_open(bredrle_type, path);
+ if (!vtio)
+ fprintf(stderr, "Failed to open vhost-user server\n");
+ else if (debug_enabled)
+ vtio_set_debug(vtio, server_debug, "vhost");
+ }
+
return mainloop_run_with_signal(signal_callback, NULL);
}
diff --git a/emulator/virtio.c b/emulator/virtio.c
new file mode 100644
index 000000000000..47889027075c
--- /dev/null
+++ b/emulator/virtio.c
@@ -0,0 +1,323 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/epoll.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include "src/shared/mainloop.h"
+#include "src/shared/queue.h"
+#include "src/shared/util.h"
+
+#include "btdev.h"
+#include "vhost.h"
+#include "virtio.h"
+
+/* Virtio device ID 40, see include/uapi/linux/virtio_bt.h in Linux */
+#define VTIO_F_MSFT_EXT 1
+
+#define VTIO_VQ_TX 0
+#define VTIO_VQ_RX 1
+#define VTIO_NUM_VQS 2
+
+#define VTIO_MSFT_OPCODE 0xfc1e
+
+/* The Linux driver posts receive buffers of this size */
+#define VTIO_MAX_PKT 1000
+
+struct vtio_config {
+ uint8_t type;
+ uint16_t vendor;
+ uint16_t msft_opcode;
+} __attribute__((packed));
+
+struct vtio {
+ enum btdev_type type;
+ int fd;
+ struct queue *clients;
+ vtio_debug_func_t debug_callback;
+ void *debug_data;
+};
+
+struct client {
+ struct vtio *vtio;
+ struct vhost *vu;
+ struct btdev *btdev;
+ /* Packets for the driver, waiting for receive buffers */
+ struct queue *pending;
+};
+
+struct packet {
+ size_t len;
+ uint8_t data[];
+};
+
+static struct vtio_config config;
+
+static void vtio_debug(struct vtio *vtio, const char *format, ...)
+{
+ char str[128];
+ va_list ap;
+
+ if (!vtio || !vtio->debug_callback)
+ return;
+
+ va_start(ap, format);
+ vsnprintf(str, sizeof(str), format, ap);
+ va_end(ap);
+
+ vtio->debug_callback(str, vtio->debug_data);
+}
+
+static void client_free(void *data)
+{
+ struct client *client = data;
+
+ vhost_free(client->vu);
+ btdev_destroy(client->btdev);
+ queue_destroy(client->pending, free);
+ free(client);
+}
+
+static void flush_pending(struct client *client)
+{
+ struct packet *pkt;
+
+ while ((pkt = queue_peek_head(client->pending))) {
+ struct iovec iov = { pkt->data, pkt->len };
+
+ if (!vhost_push(client->vu, VTIO_VQ_RX, &iov, 1))
+ break;
+
+ queue_pop_head(client->pending);
+ free(pkt);
+ }
+}
+
+static void btdev_send(const struct iovec *iov, int iovlen, void *user_data)
+{
+ struct client *client = user_data;
+ struct packet *pkt;
+ size_t len = 0;
+ int i;
+
+ for (i = 0; i < iovlen; i++)
+ len += iov[i].iov_len;
+
+ if (len > VTIO_MAX_PKT) {
+ vtio_debug(client->vtio, "dropping %zu bytes packet", len);
+ return;
+ }
+
+ pkt = malloc(sizeof(*pkt) + len);
+ if (!pkt)
+ return;
+
+ pkt->len = 0;
+ for (i = 0; i < iovlen; i++) {
+ memcpy(pkt->data + pkt->len, iov[i].iov_base, iov[i].iov_len);
+ pkt->len += iov[i].iov_len;
+ }
+
+ /* The driver may have no receive buffer posted, e.g. for back to
+ * back events, so keep the packet until it posts one.
+ */
+ queue_push_tail(client->pending, pkt);
+ flush_pending(client);
+}
+
+static void queue_notify(struct vhost *vu, unsigned int index,
+ void *user_data)
+{
+ struct client *client = user_data;
+ uint8_t buf[VTIO_MAX_PKT + 4];
+ ssize_t len;
+
+ switch (index) {
+ case VTIO_VQ_TX:
+ while ((len = vhost_pop(vu, index, buf, sizeof(buf)))) {
+ if (len > 0)
+ btdev_receive_h4(client->btdev, buf, len);
+ }
+ break;
+ case VTIO_VQ_RX:
+ flush_pending(client);
+ break;
+ }
+}
+
+static void disconnect(struct vhost *vu, void *user_data)
+{
+ struct client *client = user_data;
+
+ vtio_debug(client->vtio, "client disconnected");
+
+ /* vu is being torn down by the mainloop and freed by client_free */
+ if (client->vtio)
+ queue_remove(client->vtio->clients, client);
+
+ client_free(client);
+}
+
+static const struct vhost_ops vhost_ops = {
+ .features = 1ULL << VTIO_F_MSFT_EXT,
+ .num_queues = VTIO_NUM_VQS,
+ .config = &config,
+ .config_len = sizeof(config),
+ .queue_notify = queue_notify,
+ .disconnect = disconnect,
+};
+
+static void vu_debug(const char *str, void *user_data)
+{
+ struct client *client = user_data;
+
+ vtio_debug(client->vtio, "vhost-user: %s", str);
+}
+
+static void accept_callback(int fd, uint32_t events, void *user_data)
+{
+ struct vtio *vtio = user_data;
+ struct client *client;
+ int nfd;
+
+ if (events & (EPOLLERR | EPOLLHUP)) {
+ mainloop_remove_fd(vtio->fd);
+ return;
+ }
+
+ nfd = accept(vtio->fd, NULL, NULL);
+ if (nfd < 0)
+ return;
+
+ client = new0(struct client, 1);
+ client->vtio = vtio;
+ client->pending = queue_new();
+
+ client->btdev = btdev_create(vtio->type, 0x42);
+ if (!client->btdev)
+ goto fail;
+
+ btdev_set_msft_opcode(client->btdev, VTIO_MSFT_OPCODE);
+ btdev_set_send_handler(client->btdev, btdev_send, client);
+
+ client->vu = vhost_new(nfd, &vhost_ops, client);
+ if (!client->vu)
+ goto fail;
+
+ vhost_set_debug(client->vu, vu_debug, client);
+
+ queue_push_tail(vtio->clients, client);
+
+ vtio_debug(vtio, "client connected");
+
+ return;
+
+fail:
+ close(nfd);
+ btdev_destroy(client->btdev);
+ queue_destroy(client->pending, free);
+ free(client);
+}
+
+static void detach_client(void *data)
+{
+ struct client *client = data;
+
+ client->vtio = NULL;
+}
+
+static void vtio_destroy(void *user_data)
+{
+ struct vtio *vtio = user_data;
+
+ queue_destroy(vtio->clients, detach_client);
+ close(vtio->fd);
+ free(vtio);
+}
+
+struct vtio *vtio_open(enum btdev_type type, const char *path)
+{
+ struct vtio *vtio;
+ struct sockaddr_un addr;
+
+ if (strlen(path) >= sizeof(addr.sun_path))
+ return NULL;
+
+ config.msft_opcode = cpu_to_le16(VTIO_MSFT_OPCODE);
+
+ vtio = new0(struct vtio, 1);
+ vtio->type = type;
+ vtio->clients = queue_new();
+
+ vtio->fd = socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if (vtio->fd < 0) {
+ perror("Failed to open vhost-user socket");
+ goto fail;
+ }
+
+ unlink(path);
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ memcpy(addr.sun_path, path, strlen(path));
+
+ if (bind(vtio->fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
+ perror("Failed to bind vhost-user socket");
+ goto fail;
+ }
+
+ if (listen(vtio->fd, 5) < 0) {
+ perror("Failed to listen on vhost-user socket");
+ goto fail;
+ }
+
+ if (mainloop_add_fd(vtio->fd, EPOLLIN, accept_callback, vtio,
+ vtio_destroy) < 0)
+ goto fail;
+
+ return vtio;
+
+fail:
+ if (vtio->fd >= 0)
+ close(vtio->fd);
+ queue_destroy(vtio->clients, NULL);
+ free(vtio);
+ return NULL;
+}
+
+void vtio_close(struct vtio *vtio)
+{
+ if (!vtio)
+ return;
+
+ mainloop_remove_fd(vtio->fd);
+}
+
+bool vtio_set_debug(struct vtio *vtio,
+ vtio_debug_func_t callback, void *user_data)
+{
+ if (!vtio)
+ return false;
+
+ vtio->debug_callback = callback;
+ vtio->debug_data = user_data;
+
+ return true;
+}
diff --git a/emulator/virtio.h b/emulator/virtio.h
new file mode 100644
index 000000000000..1e1b44f0006a
--- /dev/null
+++ b/emulator/virtio.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ *
+ * BlueZ - Bluetooth protocol stack for Linux
+ *
+ * Copyright (C) 2026 Intel Corporation
+ *
+ *
+ */
+
+#include <stdbool.h>
+
+struct vtio;
+
+struct vtio *vtio_open(enum btdev_type type, const char *path);
+void vtio_close(struct vtio *vtio);
+
+typedef void (*vtio_debug_func_t)(const char *str, void *user_data);
+bool vtio_set_debug(struct vtio *vtio,
+ vtio_debug_func_t callback, void *user_data);
--
2.56.0
next prev parent reply other threads:[~2026-10-22 15:47 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 15:46 [PATCH BlueZ v1 00/27] Run the testers under User Mode Linux with time travel Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 01/27] tools/test-runner: Do not report filesystems already mounted Luiz Augusto von Dentz
2026-10-22 20:02 ` Run the testers under User Mode Linux with time travel bluez.test.bot
2026-10-22 15:46 ` [PATCH BlueZ v1 02/27] shared/tester: Time out teardown Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 03/27] mgmt-tester: Wait for LL Privacy setup to complete Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 04/27] iso-tester: Fix leaking socket watches Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 05/27] l2cap-tester: Fix race in LE Client - Close socket 2 Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 06/27] iso-tester: Fix busy waiting in PAST tests Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 07/27] doc: Enable IPv6 in tester.config Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 08/27] 6lowpan-tester: Find the interface of the controller Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 09/27] emulator: Remove -T option from btvirt Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 10/27] doc: Add btvirt documentation Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 11/27] emulator: Add vhost-user backend Luiz Augusto von Dentz
2026-10-22 15:46 ` Luiz Augusto von Dentz [this message]
2026-10-22 15:46 ` [PATCH BlueZ v1 13/27] emulator: Add time travel calendar to btvirt Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 14/27] emulator: Add virtio console and time travel client " Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 15/27] tools/test-runner: Add User Mode Linux support Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 16/27] doc: Add virtio documentation Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 17/27] shared/mainloop: Add idle callbacks Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 18/27] shared/timeout: Run timeouts of 0 once idle Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 19/27] shared/tester: Remove GLib dependency Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 20/27] emulator/hciemu: " Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 21/27] tools: Build testers that do not need GLib with the mainloop Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 22/27] shared/mainloop: Add deferred callbacks Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 23/27] tools: Add socket watches for testers without GLib Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 24/27] tools: Run sco-tester, rfcomm-tester and 6lowpan-tester on the mainloop Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 25/27] tools: Run l2cap-tester and iso-tester " Luiz Augusto von Dentz
2026-10-22 15:46 ` [PATCH BlueZ v1 26/27] doc/virtio: Update measurements of the testers Luiz Augusto von Dentz
2026-10-22 15:47 ` [PATCH BlueZ v1 27/27] doc: Split tester.config for QEMU and User Mode Linux Luiz Augusto von Dentz
2026-10-29 18:40 ` [PATCH BlueZ v1 00/27] Run the testers under User Mode Linux with time travel patchwork-bot+bluetooth
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261022154701.3959500-13-luiz.dentz@gmail.com \
--to=luiz.dentz@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox