Linux bluetooth development
 help / color / mirror / Atom feed
From: Luiz Augusto von Dentz <luiz.dentz@gmail.com>
To: linux-bluetooth@vger.kernel.org
Subject: [PATCH BlueZ v1 13/27] emulator: Add time travel calendar to btvirt
Date: Thu, 22 Oct 2026 11:46:46 -0400	[thread overview]
Message-ID: <20261022154701.3959500-14-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 --time option, which makes btvirt the time travel calendar of
User Mode Linux guests started with time-travel=ext, so they share a
simulated clock with btvirt without an external time controller. Time
only advances when every guest waits, and the timers of the emulated
controllers run on the simulated clock. A guest runs exactly at the
time it requested, and GET answers with the current time, which a guest
takes as the time of an interrupt.

The calendar only uses messages. UML uses the file descriptors of the
START acknowledgment without checking how many it received, so send a
shared memory it rejects and /dev/null.

In lock-step, send FREE_UNTIL with the next event of any other client
or timer before RUN, so a guest does not ask before each of its own
events, about four round trips each, and lower it when an earlier event
shows up. A guest sends UPDATE before each virtio notification, but does
not wait for it to be handled, so take its time from UPDATE and stop it
there before acknowledging: it may not run ahead of what the
notification causes, such as an interrupt from the device.

Add tt_set_skew() to let time advance up to a skew beyond the earliest
running guest, and a hold callback for pacing: while held, time advances
no faster than real time, and no FREE_UNTIL is given so pacing can stop
a guest.

Assisted-by: Claude:claude-opus-5-5
---
 Makefile.tools        |   4 +-
 doc/btvirt.rst        |  22 +-
 emulator/main.c       |  27 +-
 emulator/timeout.c    | 102 ++++++
 emulator/timetravel.c | 743 ++++++++++++++++++++++++++++++++++++++++++
 emulator/timetravel.h |  55 ++++
 emulator/virtio.c     |  33 ++
 7 files changed, 981 insertions(+), 5 deletions(-)
 create mode 100644 emulator/timeout.c
 create mode 100644 emulator/timetravel.c
 create mode 100644 emulator/timetravel.h

diff --git a/Makefile.tools b/Makefile.tools
index 6b2adb2f6556..bc92bc02b83d 100644
--- a/Makefile.tools
+++ b/Makefile.tools
@@ -104,7 +104,9 @@ emulator_btvirt_SOURCES = emulator/main.c monitor/bt.h \
 				emulator/phy.h emulator/phy.c \
 				emulator/le.h emulator/le.c \
 				emulator/vhost.h emulator/vhost.c \
-				emulator/virtio.h emulator/virtio.c
+				emulator/virtio.h emulator/virtio.c \
+				emulator/timetravel.h emulator/timetravel.c \
+				emulator/timeout.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 c8e2b0be3cc2..16ef55dbb7a0 100644
--- a/doc/btvirt.rst
+++ b/doc/btvirt.rst
@@ -47,8 +47,8 @@ btvirt runs until it receives **SIGINT** or **SIGTERM**.
 OPTIONS
 =======
 
-At least one of **-l**, **-s**, **-t**, **-V**, **-S** or **-U** must be
-given.
+At least one of **-l**, **-s**, **-t**, **-V**, **-S**, **-U** or
+**--time** must be given.
 Optional arguments must be attached to the option, as in **-l2**,
 **-t45551** or **--server=/tmp/bt**.
 
@@ -89,6 +89,17 @@ Optional arguments must be attached to the option, as in **-l2**,
                             Bluetooth device. See **virtio** for how to
                             attach it to QEMU or User Mode Linux guests.
 
+--time[=path]               Create a time travel calendar socket ``bt-time``
+                            in the directory *path* (default: ``/tmp``) for
+                            User Mode Linux guests started with
+                            ``time-travel=ext:<path>/bt-time``. The guests
+                            and btvirt then share a simulated clock: time
+                            only advances when every guest waits, and the
+                            timers of the emulated controllers run on it.
+                            Notifications of **-V** controllers are
+                            exchanged in-band, so they are ordered with
+                            the guests' clocks.
+
 -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
@@ -151,6 +162,13 @@ devices, two from the same socket::
     $ linux ... virtio_uml.device=/tmp/bt/bt-vhost-bredrle:40 \
                 virtio_uml.device=/tmp/bt/bt-vhost-bredrle:40
 
+Run the same guests on a simulated clock shared with btvirt::
+
+    $ btvirt --time=/tmp/bt -V/tmp/bt
+    $ linux ... time-travel=ext:/tmp/bt/bt-time \
+                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 b448b6c00723..a3b9456bb642 100644
--- a/emulator/main.c
+++ b/emulator/main.c
@@ -28,6 +28,7 @@
 #include "server.h"
 #include "vhci.h"
 #include "virtio.h"
+#include "timetravel.h"
 #include "le.h"
 
 static void signal_callback(int signum, void *user_data)
@@ -52,6 +53,8 @@ static void usage(void)
 		"\t-t[port=45550]        Create a TCP server\n"
 		"\t-V[path=/tmp]         Create local vhost-user virtio\n"
 		"\t                      server socket\n"
+		"\t--time[=path=/tmp]    Create time travel calendar socket\n"
+		"\t                      for User Mode Linux guests\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"
@@ -70,6 +73,7 @@ static const struct option main_options[] = {
 	{ "server",  optional_argument, NULL, 's' },
 	{ "tcp",     optional_argument, NULL, 't' },
 	{ "vhost",   optional_argument, NULL, 'V' },
+	{ "time",    optional_argument, NULL, 'I' },
 	{ "local",   optional_argument, NULL, 'l' },
 	{ "le",      no_argument,       NULL, 'L' },
 	{ "bredr",   no_argument,       NULL, 'B' },
@@ -131,6 +135,7 @@ int main(int argc, char *argv[])
 	const char *server_path = "/tmp";
 	uint16_t tcp_port = 0;
 	const char *vhost_path = NULL;
+	const char *time_path = NULL;
 	bool serial_enabled = false;
 	int letest_count = 0;
 	int vhci_count = 0;
@@ -145,7 +150,7 @@ int main(int argc, char *argv[])
 	for (;;) {
 		int opt;
 
-		opt = getopt_long(argc, argv, "dSs::t::V::l::LBAC:U::vh",
+		opt = getopt_long(argc, argv, "dSs::t::V::I::l::LBAC:U::vh",
 						main_options, NULL);
 		if (opt < 0)
 			break;
@@ -168,6 +173,9 @@ int main(int argc, char *argv[])
 			else
 				tcp_port = 45550;
 			break;
+		case 'I':
+			time_path = optarg ? optarg : "/tmp";
+			break;
 		case 'V':
 			vhost_path = optarg ? optarg : "/tmp";
 			break;
@@ -217,13 +225,28 @@ int main(int argc, char *argv[])
 		type = bredrle_type;
 
 	if (letest_count < 1 && vhci_count < 1 && !server_enabled &&
-				!tcp_port && !serial_enabled && !vhost_path) {
+				!tcp_port && !serial_enabled && !vhost_path &&
+				!time_path) {
 		fprintf(stderr, "No emulator specified\n");
 		return EXIT_FAILURE;
 	}
 
 	printf("Bluetooth emulator ver %s\n", VERSION);
 
+	/* Before any controller, so their timers use the simulated clock */
+	if (time_path) {
+		char path[PATH_MAX];
+
+		snprintf(path, sizeof(path), "%s/%s", time_path, "bt-time");
+		if (!tt_open(path)) {
+			fprintf(stderr, "Failed to open time travel socket\n");
+			return EXIT_FAILURE;
+		}
+
+		if (debug_enabled)
+			tt_set_debug(server_debug, "time");
+	}
+
 	for (i = 0; i < letest_count; i++) {
 		struct bt_le *le;
 
diff --git a/emulator/timeout.c b/emulator/timeout.c
new file mode 100644
index 000000000000..772cbfa4d4ce
--- /dev/null
+++ b/emulator/timeout.c
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2026  Intel Corporation
+ *
+ *
+ */
+
+/* Timeouts for btvirt: on the simulated clock of the time travel calendar
+ * when it is enabled, otherwise on the mainloop like
+ * src/shared/timeout-mainloop.c.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdlib.h>
+
+#include "src/shared/mainloop.h"
+#include "src/shared/util.h"
+#include "src/shared/timeout.h"
+
+#include "timetravel.h"
+
+/* Calendar timer IDs are kept apart from the mainloop ones */
+#define TT_TIMEOUT_ID	0x80000000U
+
+struct timeout_data {
+	int id;
+	timeout_func_t func;
+	timeout_destroy_func_t destroy;
+	unsigned int timeout;
+	void *user_data;
+};
+
+static void timeout_callback(int id, void *user_data)
+{
+	struct timeout_data *data = user_data;
+
+	if (data->func(data->user_data) &&
+			!mainloop_modify_timeout(data->id, data->timeout))
+		return;
+
+	mainloop_remove_timeout(data->id);
+}
+
+static void timeout_destroy(void *user_data)
+{
+	struct timeout_data *data = user_data;
+
+	if (data->destroy)
+		data->destroy(data->user_data);
+
+	free(data);
+}
+
+unsigned int timeout_add(unsigned int timeout, timeout_func_t func,
+			void *user_data, timeout_destroy_func_t destroy)
+{
+	struct timeout_data *data;
+
+	if (tt_enabled())
+		return TT_TIMEOUT_ID | tt_timeout_add(timeout, func, user_data,
+								destroy);
+
+	data = new0(struct timeout_data, 1);
+	data->func = func;
+	data->user_data = user_data;
+	data->timeout = timeout;
+	data->destroy = destroy;
+
+	data->id = mainloop_add_timeout(timeout, timeout_callback, data,
+							timeout_destroy);
+	if (data->id < 0) {
+		free(data);
+		return 0;
+	}
+
+	return (unsigned int) data->id;
+}
+
+void timeout_remove(unsigned int id)
+{
+	if (!id)
+		return;
+
+	if (id & TT_TIMEOUT_ID) {
+		tt_timeout_remove(id & ~TT_TIMEOUT_ID);
+		return;
+	}
+
+	mainloop_remove_timeout((int) id);
+}
+
+unsigned int timeout_add_seconds(unsigned int timeout, timeout_func_t func,
+			void *user_data, timeout_destroy_func_t destroy)
+{
+	return timeout_add(timeout * 1000, func, user_data, destroy);
+}
diff --git a/emulator/timetravel.c b/emulator/timetravel.c
new file mode 100644
index 000000000000..c1938ed23bdc
--- /dev/null
+++ b/emulator/timetravel.c
@@ -0,0 +1,743 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2026  Intel Corporation
+ *
+ *
+ */
+
+#define _GNU_SOURCE
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <unistd.h>
+#include <sys/epoll.h>
+#include <sys/mman.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 "timetravel.h"
+
+/* Protocol of include/uapi/linux/um_timetravel.h in Linux */
+#define TT_ACK		0
+#define TT_START	1
+#define TT_REQUEST	2
+#define TT_WAIT		3
+#define TT_GET		4
+#define TT_UPDATE	5
+#define TT_RUN		6
+#define TT_FREE_UNTIL	7
+#define TT_GET_TOD	8
+#define TT_BROADCAST	9
+
+#define TT_SHM_SIZE	4096
+
+struct tt_msg {
+	uint32_t op;
+	uint32_t seq;
+	uint64_t time;
+} __attribute__((packed));
+
+struct tt_client {
+	int fd;
+	uint16_t id;
+	bool waiting;
+	bool has_request;
+	uint64_t request;
+	/* Time the client runs at, as far as known */
+	uint64_t time;
+	/* Time the running client may run until without asking, 0 if none */
+	uint64_t free_until;
+};
+
+struct tt_flush {
+	tt_flush_func_t func;
+	void *user_data;
+};
+
+struct tt_timer {
+	unsigned int id;
+	uint64_t time;
+	uint64_t interval;
+	tt_timeout_func_t func;
+	tt_destroy_func_t destroy;
+	void *user_data;
+};
+
+static struct {
+	int fd;
+	struct queue *clients;
+	struct queue *timers;
+	uint64_t now;
+	uint64_t skew;
+	uint64_t tod;
+	uint16_t next_id;
+	unsigned int next_timer;
+	bool scheduling;
+	struct queue *flushes;
+	tt_hold_func_t hold;
+	void *hold_data;
+	/* While held, time advances no faster than real time from here */
+	bool held;
+	uint64_t held_sim;
+	uint64_t held_wall;
+	int pace_id;
+	tt_debug_func_t debug;
+	void *debug_data;
+} tt = { .fd = -1 };
+
+static void tt_debug(const char *format, ...)
+{
+	char str[128];
+	va_list ap;
+
+	if (!tt.debug)
+		return;
+
+	va_start(ap, format);
+	vsnprintf(str, sizeof(str), format, ap);
+	va_end(ap);
+
+	tt.debug(str, tt.debug_data);
+}
+
+static void send_msg(struct tt_client *client, uint32_t op, uint32_t seq,
+							uint64_t time)
+{
+	struct tt_msg msg = { .op = op, .seq = seq, .time = time };
+
+	if (write(client->fd, &msg, sizeof(msg)) < 0)
+		tt_debug("client %u: write failed", client->id);
+}
+
+/* The ACK to START may carry a shared memory and a log file descriptor.
+ * This calendar only uses messages, but UML uses the descriptors it
+ * receives without checking how many it got, so send a shared memory it
+ * rejects (version 0) and /dev/null.
+ */
+static void send_start_ack(struct tt_client *client, uint32_t seq)
+{
+	struct tt_msg msg = { .op = TT_ACK, .seq = seq, .time = client->id };
+	char control[CMSG_SPACE(sizeof(int) * 2)];
+	struct iovec iov = { &msg, sizeof(msg) };
+	struct msghdr mh = {
+		.msg_iov = &iov,
+		.msg_iovlen = 1,
+		.msg_control = control,
+		.msg_controllen = sizeof(control),
+	};
+	struct cmsghdr *cmsg;
+	int fds[2];
+
+	fds[0] = memfd_create("bt-time-shm", MFD_CLOEXEC);
+	fds[1] = open("/dev/null", O_WRONLY | O_CLOEXEC);
+	if (fds[0] < 0 || fds[1] < 0 || ftruncate(fds[0], TT_SHM_SIZE) < 0) {
+		tt_debug("client %u: no file descriptors for START",
+								client->id);
+		send_msg(client, TT_ACK, seq, client->id);
+		goto done;
+	}
+
+	memset(control, 0, sizeof(control));
+	cmsg = CMSG_FIRSTHDR(&mh);
+	cmsg->cmsg_level = SOL_SOCKET;
+	cmsg->cmsg_type = SCM_RIGHTS;
+	cmsg->cmsg_len = CMSG_LEN(sizeof(fds));
+	memcpy(CMSG_DATA(cmsg), fds, sizeof(fds));
+
+	if (sendmsg(client->fd, &mh, 0) < 0)
+		tt_debug("client %u: sendmsg failed", client->id);
+
+done:
+	if (fds[0] >= 0)
+		close(fds[0]);
+	if (fds[1] >= 0)
+		close(fds[1]);
+}
+
+static void find_running(void *data, void *user_data)
+{
+	struct tt_client *client = data;
+	uint64_t *min = user_data;
+
+	if (!client->waiting && client->time < *min)
+		*min = client->time;
+}
+
+static void find_next_client(void *data, void *user_data)
+{
+	struct tt_client *client = data;
+	struct tt_client **next = user_data;
+
+	if (!client->waiting || !client->has_request)
+		return;
+
+	if (!*next || client->request < (*next)->request)
+		*next = client;
+}
+
+static void find_next_timer(void *data, void *user_data)
+{
+	struct tt_timer *timer = data;
+	struct tt_timer **next = user_data;
+
+	if (!*next || timer->time < (*next)->time)
+		*next = timer;
+}
+
+static void timer_free(void *data)
+{
+	struct tt_timer *timer = data;
+
+	if (timer->destroy)
+		timer->destroy(timer->user_data);
+
+	free(timer);
+}
+
+static void fire_timer(struct tt_timer *timer)
+{
+	if (timer->func(timer->user_data)) {
+		/* The callback may have removed the timer */
+		if (queue_find(tt.timers, NULL, timer)) {
+			timer->time = tt.now + timer->interval;
+			return;
+		}
+	}
+
+	if (queue_remove(tt.timers, timer))
+		timer_free(timer);
+}
+
+static void run_flush(void *data, void *user_data)
+{
+	struct tt_flush *flush = data;
+
+	flush->func(flush->user_data);
+}
+
+static void schedule(void);
+
+struct free_limit {
+	struct tt_client *client;
+	uint64_t time;
+};
+
+static void find_free_limit(void *data, void *user_data)
+{
+	struct tt_client *client = data;
+	struct free_limit *limit = user_data;
+
+	if (client == limit->client)
+		return;
+
+	/* A client that has not waited yet may request any time */
+	if (!client->waiting)
+		limit->time = MIN(limit->time, tt.now);
+	else if (client->has_request)
+		limit->time = MIN(limit->time, client->request);
+}
+
+static void find_timer_limit(void *data, void *user_data)
+{
+	struct tt_timer *timer = data;
+	uint64_t *limit = user_data;
+
+	*limit = MIN(*limit, timer->time);
+}
+
+/* How far a client may run without asking: up to the earliest event of any
+ * other client or timer. Only in lock-step, and not while time may be
+ * paced, as pacing must be able to stop the client.
+ */
+static uint64_t free_until(struct tt_client *client)
+{
+	struct free_limit limit = { client, UINT64_MAX };
+
+	if (tt.skew || tt.hold)
+		return 0;
+
+	queue_foreach(tt.clients, find_free_limit, &limit);
+	queue_foreach(tt.timers, find_timer_limit, &limit.time);
+
+	return limit.time;
+}
+
+/* Lower the limit of a running client when an earlier event appeared. This
+ * happens in response to a client, e.g. while it waits for its virtio
+ * notification to be acknowledged, so it receives the new limit before it
+ * runs further.
+ */
+static void update_free_until(void *data, void *user_data)
+{
+	struct tt_client *client = data;
+	uint64_t limit;
+
+	if (client->waiting || !client->free_until)
+		return;
+
+	limit = free_until(client);
+	if (limit >= client->free_until)
+		return;
+
+	client->free_until = limit;
+	send_msg(client, TT_FREE_UNTIL, 0, limit);
+}
+
+static uint64_t wall_now(void)
+{
+	struct timespec ts;
+
+	clock_gettime(CLOCK_MONOTONIC, &ts);
+
+	return (uint64_t) ts.tv_sec * 1000000000ULL + ts.tv_nsec;
+}
+
+static void pace_callback(int id, void *user_data)
+{
+	mainloop_remove_timeout(id);
+	tt.pace_id = 0;
+	schedule();
+}
+
+/* Limit of time while held: real time elapsed since the hold started */
+static uint64_t hold_limit(void)
+{
+	bool hold = tt.hold && tt.hold(tt.hold_data);
+
+	if (!hold) {
+		tt.held = false;
+		return UINT64_MAX;
+	}
+
+	if (!tt.held) {
+		tt.held = true;
+		tt.held_sim = tt.now;
+		tt.held_wall = wall_now();
+	}
+
+	return tt.held_sim + (wall_now() - tt.held_wall);
+}
+
+/* Schedule again when real time catches up with the next event */
+static void pace(uint64_t next, uint64_t limit)
+{
+	unsigned int msec = (next - limit) / 1000000ULL + 1;
+
+	if (tt.pace_id > 0)
+		return;
+
+	tt.pace_id = mainloop_add_timeout(MIN(msec, 100U), pace_callback,
+								NULL, NULL);
+}
+
+/* Run whichever is due first, a timer of this process or the client with
+ * the earliest request. In lock-step (no skew) time only advances when
+ * every client waits. With a skew, time may advance up to that much beyond
+ * the earliest running client, so clients run their own events in parallel
+ * while others compute, which takes no simulated time.
+ */
+static void schedule(void)
+{
+	if (tt.scheduling)
+		return;
+
+	tt.scheduling = true;
+
+	while (1) {
+		struct tt_client *client = NULL;
+		struct tt_timer *timer = NULL;
+		uint64_t running = UINT64_MAX;
+		uint64_t limit = UINT64_MAX;
+		uint64_t hold;
+		uint64_t next;
+
+		queue_foreach(tt.flushes, run_flush, NULL);
+
+		if (queue_isempty(tt.clients))
+			break;
+
+		queue_foreach(tt.clients, find_running, &running);
+		if (running != UINT64_MAX) {
+			if (!tt.skew) {
+				queue_foreach(tt.clients, update_free_until,
+									NULL);
+				break;
+			}
+			limit = running + tt.skew;
+		}
+
+		hold = hold_limit();
+
+		queue_foreach(tt.clients, find_next_client, &client);
+		queue_foreach(tt.timers, find_next_timer, &timer);
+
+		if (timer && (!client || timer->time <= client->request)) {
+			next = timer->time;
+			client = NULL;
+		} else if (client) {
+			next = client->request;
+			timer = NULL;
+		} else {
+			break;
+		}
+
+		if (next > limit)
+			break;
+
+		if (next > hold) {
+			pace(next, hold);
+			break;
+		}
+
+		if (next > tt.now)
+			tt.now = next;
+
+		if (timer) {
+			fire_timer(timer);
+			continue;
+		}
+
+		client->waiting = false;
+		client->has_request = false;
+		client->time = next;
+
+		/* Let the client run until the next event of another one,
+		 * instead of asking for each of its own events.
+		 */
+		client->free_until = free_until(client);
+		if (client->free_until)
+			send_msg(client, TT_FREE_UNTIL, 0, client->free_until);
+
+		send_msg(client, TT_RUN, 0, next);
+	}
+
+	tt.scheduling = false;
+}
+
+void tt_schedule(void)
+{
+	if (tt_enabled())
+		schedule();
+}
+
+static void handle_msg(struct tt_client *client, struct tt_msg *msg)
+{
+	switch (msg->op) {
+	case TT_ACK:
+		/* ACK of a RUN */
+		return;
+	case TT_START:
+		client->id = tt.next_id++;
+		client->time = tt.now;
+		tt_debug("client %u started", client->id);
+		send_start_ack(client, msg->seq);
+		return;
+	case TT_REQUEST:
+		client->has_request = true;
+		/* The guest must run exactly at the time of its event */
+		client->request = msg->time;
+		send_msg(client, TT_ACK, msg->seq, 0);
+		break;
+	case TT_WAIT:
+		client->waiting = true;
+		client->free_until = 0;
+		send_msg(client, TT_ACK, msg->seq, 0);
+		break;
+	case TT_GET:
+		/* The guest takes it as the time of an interrupt: the current
+		 * time, which no guest is ahead of.
+		 */
+		client->time = MAX(client->time, tt.now);
+		send_msg(client, TT_ACK, msg->seq, client->time);
+		return;
+	case TT_UPDATE:
+		if (msg->time > client->time)
+			client->time = msg->time;
+		/* In lock-step the running client sets the time, which may be
+		 * ahead of the calendar when it runs until a FREE_UNTIL.
+		 */
+		if (!tt.skew && !client->waiting && client->time > tt.now)
+			tt.now = client->time;
+		/* The guest sends UPDATE before a virtio notification, which
+		 * it does not wait for, and runs on meanwhile. Stop it at this
+		 * time before acknowledging, so it cannot run ahead of what
+		 * the notification causes, e.g. the device's interrupt.
+		 */
+		if (client->free_until && client->free_until > client->time) {
+			client->free_until = client->time;
+			send_msg(client, TT_FREE_UNTIL, 0, client->time);
+		}
+		send_msg(client, TT_ACK, msg->seq, 0);
+		break;
+	case TT_GET_TOD:
+		send_msg(client, TT_ACK, msg->seq, tt.tod + tt.now);
+		return;
+	default:
+		tt_debug("client %u: unsupported op %u", client->id, msg->op);
+		send_msg(client, TT_ACK, msg->seq, 0);
+		return;
+	}
+}
+
+/* Returns 1 if a message was handled, 0 if none is pending and -1 if the
+ * connection failed.
+ */
+static int read_msg(struct tt_client *client)
+{
+	struct tt_msg msg;
+	ssize_t len;
+
+	len = recv(client->fd, &msg, sizeof(msg), MSG_DONTWAIT);
+	if (len < 0 && (errno == EAGAIN || errno == EINTR))
+		return 0;
+
+	if (len != sizeof(msg)) {
+		tt_debug("client %u: read %zd", client->id, len);
+		return -1;
+	}
+
+	handle_msg(client, &msg);
+
+	return 1;
+}
+
+static void client_destroy(void *data)
+{
+	struct tt_client *client = data;
+
+	tt_debug("client %u disconnected", client->id);
+
+	queue_remove(tt.clients, client);
+	close(client->fd);
+	free(client);
+
+	/* Clients and back-ends are destroyed in any order on shutdown, so
+	 * only schedule while the calendar is open.
+	 */
+	if (tt.fd >= 0)
+		schedule();
+}
+
+static void client_callback(int fd, uint32_t events, void *user_data)
+{
+	struct tt_client *client = user_data;
+
+	int ret;
+
+	if (events & (EPOLLERR | EPOLLHUP)) {
+		mainloop_remove_fd(client->fd);
+		return;
+	}
+
+	/* Process everything that is pending before scheduling, messages
+	 * may also have been processed already by tt_process().
+	 */
+	while ((ret = read_msg(client)) > 0)
+		;
+
+	if (ret < 0) {
+		mainloop_remove_fd(client->fd);
+		return;
+	}
+
+	schedule();
+}
+
+static void process_client(void *data, void *user_data)
+{
+	struct tt_client *client = data;
+
+	while (read_msg(client) > 0)
+		;
+}
+
+void tt_process(void)
+{
+	queue_foreach(tt.clients, process_client, NULL);
+}
+
+static void accept_callback(int fd, uint32_t events, void *user_data)
+{
+	struct tt_client *client;
+	int nfd;
+
+	if (events & (EPOLLERR | EPOLLHUP)) {
+		mainloop_remove_fd(fd);
+		return;
+	}
+
+	nfd = accept(fd, NULL, NULL);
+	if (nfd < 0)
+		return;
+
+	client = new0(struct tt_client, 1);
+	client->fd = nfd;
+
+	if (mainloop_add_fd(nfd, EPOLLIN, client_callback, client,
+						client_destroy) < 0) {
+		close(nfd);
+		free(client);
+		return;
+	}
+
+	queue_push_tail(tt.clients, client);
+}
+
+static void tt_destroy(void *user_data)
+{
+	close(tt.fd);
+	tt.fd = -1;
+}
+
+bool tt_open(const char *path)
+{
+	struct sockaddr_un addr;
+	struct timespec ts;
+
+	if (tt.fd >= 0 || strlen(path) >= sizeof(addr.sun_path))
+		return false;
+
+	tt.fd = socket(PF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+	if (tt.fd < 0)
+		return false;
+
+	unlink(path);
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sun_family = AF_UNIX;
+	memcpy(addr.sun_path, path, strlen(path));
+
+	if (bind(tt.fd, (struct sockaddr *) &addr, sizeof(addr)) < 0 ||
+						listen(tt.fd, 5) < 0)
+		goto fail;
+
+	if (mainloop_add_fd(tt.fd, EPOLLIN, accept_callback, NULL,
+							tt_destroy) < 0)
+		goto fail;
+
+	clock_gettime(CLOCK_REALTIME, &ts);
+	tt.tod = (uint64_t) ts.tv_sec * 1000000000ULL + ts.tv_nsec;
+	tt.clients = queue_new();
+	tt.timers = queue_new();
+	tt.next_id = 1;
+
+	return true;
+
+fail:
+	close(tt.fd);
+	tt.fd = -1;
+	return false;
+}
+
+void tt_close(void)
+{
+	if (tt.fd >= 0)
+		mainloop_remove_fd(tt.fd);
+}
+
+bool tt_enabled(void)
+{
+	return tt.fd >= 0;
+}
+
+uint64_t tt_now(void)
+{
+	return tt.now;
+}
+
+void tt_set_skew(unsigned int msec)
+{
+	tt.skew = (uint64_t) msec * 1000000ULL;
+}
+
+void tt_set_hold(tt_hold_func_t func, void *user_data)
+{
+	tt.hold = func;
+	tt.hold_data = user_data;
+}
+
+static bool match_flush(const void *data, const void *match_data)
+{
+	const struct tt_flush *flush = data;
+	const struct tt_flush *match = match_data;
+
+	return flush->func == match->func &&
+				flush->user_data == match->user_data;
+}
+
+void tt_remove_flush(tt_flush_func_t func, void *user_data)
+{
+	struct tt_flush match = { .func = func, .user_data = user_data };
+
+	free(queue_remove_if(tt.flushes, match_flush, &match));
+}
+
+void tt_add_flush(tt_flush_func_t func, void *user_data)
+{
+	struct tt_flush *flush;
+
+	if (!tt.flushes)
+		tt.flushes = queue_new();
+
+	flush = new0(struct tt_flush, 1);
+	flush->func = func;
+	flush->user_data = user_data;
+
+	queue_push_tail(tt.flushes, flush);
+}
+
+unsigned int tt_timeout_add(unsigned int msec, tt_timeout_func_t func,
+				void *user_data, tt_destroy_func_t destroy)
+{
+	struct tt_timer *timer;
+
+	timer = new0(struct tt_timer, 1);
+	timer->id = ++tt.next_timer;
+	timer->interval = (uint64_t) msec * 1000000ULL;
+	timer->time = tt.now + timer->interval;
+	timer->func = func;
+	timer->user_data = user_data;
+	timer->destroy = destroy;
+
+	queue_push_tail(tt.timers, timer);
+
+	return timer->id;
+}
+
+static bool match_timer(const void *data, const void *match_data)
+{
+	const struct tt_timer *timer = data;
+
+	return timer->id == PTR_TO_UINT(match_data);
+}
+
+void tt_timeout_remove(unsigned int id)
+{
+	struct tt_timer *timer;
+
+	timer = queue_remove_if(tt.timers, match_timer, UINT_TO_PTR(id));
+	if (timer)
+		timer_free(timer);
+}
+
+void tt_set_debug(tt_debug_func_t callback, void *user_data)
+{
+	tt.debug = callback;
+	tt.debug_data = user_data;
+}
diff --git a/emulator/timetravel.h b/emulator/timetravel.h
new file mode 100644
index 000000000000..45b9d4ef69f2
--- /dev/null
+++ b/emulator/timetravel.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ *
+ *  BlueZ - Bluetooth protocol stack for Linux
+ *
+ *  Copyright (C) 2026  Intel Corporation
+ *
+ *
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/* Time travel calendar for User Mode Linux (time-travel=ext) guests */
+
+bool tt_open(const char *path);
+void tt_close(void);
+bool tt_enabled(void);
+
+/* Let time advance up to msec beyond the earliest running guest, so guests
+ * run in parallel; 0 (default) is lock-step.
+ */
+void tt_set_skew(unsigned int msec);
+
+/* Current simulated time in nanoseconds */
+uint64_t tt_now(void);
+
+/* Called before advancing time, to process pending device input */
+typedef void (*tt_flush_func_t)(void *user_data);
+void tt_add_flush(tt_flush_func_t func, void *user_data);
+void tt_remove_flush(tt_flush_func_t func, void *user_data);
+
+/* While the hold function returns true, time advances no faster than real
+ * time. Used while a peer outside the simulation, e.g. a test driver, has
+ * not asked anything of the guests, so they don't run ahead of it.
+ */
+typedef bool (*tt_hold_func_t)(void *user_data);
+void tt_set_hold(tt_hold_func_t func, void *user_data);
+
+/* Advance time if every guest waits, e.g. after processing device input */
+void tt_schedule(void);
+
+/* Process pending calendar messages without advancing time, used while
+ * waiting for a guest that may itself be waiting for the calendar.
+ */
+void tt_process(void);
+
+typedef bool (*tt_timeout_func_t)(void *user_data);
+typedef void (*tt_destroy_func_t)(void *user_data);
+unsigned int tt_timeout_add(unsigned int msec, tt_timeout_func_t func,
+				void *user_data, tt_destroy_func_t destroy);
+void tt_timeout_remove(unsigned int id);
+
+typedef void (*tt_debug_func_t)(const char *str, void *user_data);
+void tt_set_debug(tt_debug_func_t callback, void *user_data);
diff --git a/emulator/virtio.c b/emulator/virtio.c
index 47889027075c..34c8f5d3f93c 100644
--- a/emulator/virtio.c
+++ b/emulator/virtio.c
@@ -27,6 +27,7 @@
 
 #include "btdev.h"
 #include "vhost.h"
+#include "timetravel.h"
 #include "virtio.h"
 
 /* Virtio device ID 40, see include/uapi/linux/virtio_bt.h in Linux */
@@ -160,6 +161,34 @@ static void queue_notify(struct vhost *vu, unsigned int index,
 		flush_pending(client);
 		break;
 	}
+
+	/* The guest may wait for the calendar now */
+	tt_schedule();
+}
+
+/* The guest acknowledges in-band notifications only after its calendar
+ * requests are answered, so keep serving the calendar meanwhile.
+ */
+static void vhost_wait(struct vhost *vu, void *user_data)
+{
+	tt_process();
+}
+
+static void flush_client(void *data, void *user_data)
+{
+	struct client *client = data;
+
+	vhost_process(client->vu);
+}
+
+/* Process guest notifications sent before the guest waited, so time does
+ * not advance past them.
+ */
+static void flush(void *user_data)
+{
+	struct vtio *vtio = user_data;
+
+	queue_foreach(vtio->clients, flush_client, NULL);
 }
 
 static void disconnect(struct vhost *vu, void *user_data)
@@ -182,6 +211,7 @@ static const struct vhost_ops vhost_ops = {
 	.config_len = sizeof(config),
 	.queue_notify = queue_notify,
 	.disconnect = disconnect,
+	.wait = vhost_wait,
 };
 
 static void vu_debug(const char *str, void *user_data)
@@ -247,6 +277,7 @@ static void vtio_destroy(void *user_data)
 {
 	struct vtio *vtio = user_data;
 
+	tt_remove_flush(flush, vtio);
 	queue_destroy(vtio->clients, detach_client);
 	close(vtio->fd);
 	free(vtio);
@@ -292,6 +323,8 @@ struct vtio *vtio_open(enum btdev_type type, const char *path)
 							vtio_destroy) < 0)
 		goto fail;
 
+	tt_add_flush(flush, vtio);
+
 	return vtio;
 
 fail:
-- 
2.56.0


  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 ` [PATCH BlueZ v1 12/27] emulator: Add vhost-user virtio server to btvirt Luiz Augusto von Dentz
2026-10-22 15:46 ` Luiz Augusto von Dentz [this message]
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-14-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