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
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 ` [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