* [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux
@ 2026-10-30 21:29 Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 01/10] tools/test-runner: Default to time travel with " Luiz Augusto von Dentz
` (9 more replies)
0 siblings, 10 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
This allows running test-functional with User Mode Linux (UML) VM hosts,
e.g. on machines without KVM support, together with pytest-bluezenv:
https://github.com/bluez/pytest-bluezenv/pull/8
test-runner runs UML guests on time travel by default, as the kernel
testers benefit from it, with -R to run on real time, which is what
test-functional uses: with traffic between several guests, lock-step
makes the tests slower, see the measurements in the documentation.
The slower timing of UML exposed races, which are fixed by:
- adapter: setting Pairable right after an agent registered failed
with Busy while bondable was being set by bluetoothd itself. This
is fixed in bluetoothd, replying once the pending change completes
when it sets the requested value, rather than retrying in the tests.
- test_bap: transports were acquired or selected before bluetoothctl
had them configured on its endpoint.
- shared/mcp: commands sent before the Media Control Point Opcodes
Supported was read were dropped while the D-Bus call succeeded.
- a2dp, adapter: when the remote reconnects before the disconnection
of the previous link is processed, the new AVDTP connection was taken
for a transport channel of the stale session and refused, or its
service authorization was cancelled by the stale disconnection. A
new test reproduces it by stopping bluetoothd across the reconnection.
With these, make check-functional with 4 workers takes about 100-140 s
under UML (68 s with KVM on the same machine). Remaining intermittent
failures under UML:
- test_bass_past_*_transport_acquire, addressed by the pending "bass:
wait for PAST listener readiness".
- test_hog[sci], reading the HID SCI Information characteristic returns
00, not investigated yet.
Luiz Augusto von Dentz (10):
tools/test-runner: Default to time travel with User Mode Linux
tools/test-runner: Unbind virtio devices before powering off UML
emulator: Answer the time of a guest with a skew
adapter: Don't return Busy when a pending change sets the same value
test: functional: wait for BAP transports to be configured
shared/mcp: Don't drop commands before supported opcodes are read
doc/test-functional: Document User Mode Linux VM hosts
a2dp: Fix refusing the AVDTP connection of a new link
adapter: Don't cancel requests of a new link on stale disconnection
test: Cover A2DP reconnection with a pending disconnection
doc/functional-a2dp.rst | 32 +++++++++
doc/test-functional.rst | 74 +++++++++++++++++++--
doc/test-runner.rst | 23 ++++---
emulator/timetravel.c | 8 ++-
profiles/audio/a2dp.c | 7 ++
profiles/audio/avdtp.c | 24 +++++++
profiles/audio/avdtp.h | 1 +
profiles/audio/mcp.c | 10 +--
src/adapter.c | 123 +++++++++++++++++++++++++++++++++--
src/shared/mcp.c | 8 ++-
test/functional/test_a2dp.py | 36 ++++++++++
test/functional/test_bap.py | 22 ++++---
tools/test-runner.c | 72 +++++++++++++++++---
13 files changed, 396 insertions(+), 44 deletions(-)
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 01/10] tools/test-runner: Default to time travel with User Mode Linux
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 02/10] tools/test-runner: Unbind virtio devices before powering off UML Luiz Augusto von Dentz
` (8 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Run User Mode Linux on simulated time whenever the kernel supports it,
since that is what makes the testers fast and their timing reproducible,
and add -R, --real-time, to run it on real time instead. -T now only
makes the lack of time travel support an error.
Do not start a calendar when a time-travel kernel parameter is given
with -o, so guests can join an external calendar shared with other
guests and controllers, as test-functional does.
Assisted-by: Claude:claude-opus-5-5
---
doc/test-runner.rst | 23 +++++++++++++----------
tools/test-runner.c | 33 +++++++++++++++++++++++++--------
2 files changed, 38 insertions(+), 18 deletions(-)
diff --git a/doc/test-runner.rst b/doc/test-runner.rst
index 02d69b06f54c..220a2cec1c55 100644
--- a/doc/test-runner.rst
+++ b/doc/test-runner.rst
@@ -29,7 +29,8 @@ OPTIONS
:-k/--kernel=<image>: Kernel image (bzImage) or User Mode Linux binary
:-F/--virtiofs[=<path>]: Path to virtiofsd, or no to disable virtio-fs
:-T/--time-travel: Run a User Mode Linux kernel on simulated time, with
- btvirt as time travel calendar
+ btvirt as time travel calendar (default if the kernel supports it)
+:-R/--real-time: Run a User Mode Linux kernel on real time
:-o/--option=<opt>: Additional argument passed to QEMU, or kernel
parameter with User Mode Linux
:-h/--help: Show help options
@@ -152,12 +153,14 @@ Bluetooth controller, or joins an external simulated clock with
``time-travel=ext:<socket>``. See `virtio.rst <virtio.rst>`_. ``-u``,
``-U``, ``-P``, ``-q`` and ``-H`` need QEMU.
-With ``-T``, test-runner starts **btvirt(1)** ``--time`` as time travel
-calendar and runs the guest on its simulated clock: time only advances when
-the guest waits, so it skips idle time and timeouts take no real time, while
-the guest sees the same timings on every run. This needs a kernel with
-``CONFIG_UML_TIME_TRAVEL_SUPPORT``, which test-runner checks; without
-``-T`` the same kernel runs on real time.
+If the kernel has ``CONFIG_UML_TIME_TRAVEL_SUPPORT``, test-runner starts
+**btvirt(1)** ``--time`` as time travel calendar and runs the guest on its
+simulated clock: time only advances when the guest waits, so it skips idle
+time and timeouts take no real time, while the guest sees the same timings
+on every run. ``-R`` runs the guest on real time instead, and ``-T`` fails if
+the kernel has no time travel support. A ``time-travel`` kernel parameter
+given with ``-o``, e.g. to join the calendar of other guests, replaces the
+calendar of test-runner.
Kernel
======
@@ -421,9 +424,9 @@ Running mgmt-tester under User Mode Linux
$ make ARCH=um olddefconfig linux
$ tools/test-runner -k <linux>/linux -- tools/mgmt-tester
-On simulated time, with a kernel that also has
-``CONFIG_UML_TIME_TRAVEL_SUPPORT=y``:
+With a kernel that also has ``CONFIG_UML_TIME_TRAVEL_SUPPORT=y`` this runs
+on simulated time; to run it on real time:
.. code-block::
- $ tools/test-runner -T -k <linux>/linux -- tools/mgmt-tester
+ $ tools/test-runner -R -k <linux>/linux -- tools/mgmt-tester
diff --git a/tools/test-runner.c b/tools/test-runner.c
index 8be4bfb09265..b4947f113607 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -66,7 +66,8 @@ static bool start_dbus_session;
static bool start_daemon = false;
static bool start_monitor = false;
static bool qemu_host_cpu = false;
-static bool time_travel;
+/* Simulated time with User Mode Linux: -1 if supported by the kernel */
+static int time_travel = -1;
static int num_devs = 0;
static int num_emulator = 0;
static const char *device_path = "/tmp/bt-server-bredr";
@@ -1224,9 +1225,14 @@ static int start_uml(void)
strv_append(&argv, "TESTAUDIO='%s'", audio_server ? audio_server : "");
strv_append(&argv, "TESTARGS='%s'", testargs);
- for (i = 0; i < num_extra_opts; ++i)
+ for (i = 0; i < num_extra_opts; ++i) {
strv_append(&argv, "%s", extra_opts[i]);
+ /* Time travel mode given with -o, e.g. an external calendar */
+ if (!strncmp(extra_opts[i], "time-travel", 11))
+ time_travel = 0;
+ }
+
if (time_travel) {
calendar = start_calendar(dir);
if (calendar < 0)
@@ -2164,6 +2170,8 @@ static void usage(void)
"\t-F, --virtiofs[=<path>] Virtiofsd path or 'no'\n"
"\t-T, --time-travel Run User Mode Linux on simulated\n"
"\t time, with btvirt as calendar\n"
+ "\t (default if supported)\n"
+ "\t-R, --real-time Run User Mode Linux on real time\n"
"\t-o, --option <opt> Additional argument passed to QEMU,\n"
"\t or kernel parameter with User\n"
"\t Mode Linux\n"
@@ -2188,6 +2196,7 @@ static const struct option main_options[] = {
{ "option", required_argument, NULL, 'o' },
{ "virtiofs", optional_argument, NULL, 'F' },
{ "time-travel", no_argument, NULL, 'T' },
+ { "real-time", no_argument, NULL, 'R' },
{ "version", no_argument, NULL, 'v' },
{ "help", no_argument, NULL, 'h' },
{ }
@@ -2211,8 +2220,9 @@ int main(int argc, char *argv[])
for (;;) {
int opt;
- opt = getopt_long(argc, argv, "au::bdsl::mq:Hk:A::U:P:o:F::Tvh",
- main_options, NULL);
+ opt = getopt_long(argc, argv,
+ "au::bdsl::mq:Hk:A::U:P:o:F::TRvh",
+ main_options, NULL);
if (opt < 0)
break;
@@ -2267,7 +2277,10 @@ int main(int argc, char *argv[])
extra_opts[num_extra_opts++] = optarg;
break;
case 'T':
- time_travel = true;
+ time_travel = 1;
+ break;
+ case 'R':
+ time_travel = 0;
break;
case 'F':
virtiofs_auto = false;
@@ -2324,17 +2337,21 @@ int main(int argc, char *argv[])
}
if (time_travel && !uml_has_time_travel()) {
- fprintf(stderr, "%s has no time travel support "
+ if (time_travel > 0) {
+ fprintf(stderr, "%s has no time travel support "
"(CONFIG_UML_TIME_TRAVEL_SUPPORT)\n",
kernel_image);
- return EXIT_FAILURE;
+ return EXIT_FAILURE;
+ }
+
+ time_travel = 0;
}
printf("Using User Mode Linux kernel %s\n", kernel_image);
return start_uml();
}
- if (time_travel) {
+ if (time_travel > 0) {
fprintf(stderr, "-T needs a User Mode Linux kernel\n");
return EXIT_FAILURE;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 02/10] tools/test-runner: Unbind virtio devices before powering off UML
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 01/10] tools/test-runner: Default to time travel with " Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 03/10] emulator: Answer the time of a guest with a skew Luiz Augusto von Dentz
` (7 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
User Mode Linux unregisters its virtio driver when powering off, in
atomic context, where removing devices that sleep, like virtio_bt in
hci_unregister_dev(), triggers scheduling while atomic bugs and, with
lock debugging enabled, an endless loop of warnings.
Unbind the virtio-uml devices before powering off, so they are removed
in process context.
Fixes: a7e9a42a791a ("tools/test-runner: Add User Mode Linux support")
Assisted-by: Claude:claude-opus-5-5
---
tools/test-runner.c | 39 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 38 insertions(+), 1 deletion(-)
diff --git a/tools/test-runner.c b/tools/test-runner.c
index b4947f113607..0bca31a46351 100644
--- a/tools/test-runner.c
+++ b/tools/test-runner.c
@@ -232,6 +232,39 @@ static bool is_uml_guest(void)
return uml;
}
+#define VIRTIO_UML_DRIVER "/sys/bus/platform/drivers/virtio-uml"
+
+/* User Mode Linux unbinds its virtio devices when powering off, in atomic
+ * context where their drivers cannot sleep, so unbind them before.
+ */
+static void uml_unbind_virtio(void)
+{
+ struct dirent *d;
+ DIR *dir;
+ int fd;
+
+ dir = opendir(VIRTIO_UML_DRIVER);
+ if (!dir)
+ return;
+
+ fd = open(VIRTIO_UML_DRIVER "/unbind", O_WRONLY | O_CLOEXEC);
+ if (fd < 0) {
+ closedir(dir);
+ return;
+ }
+
+ while ((d = readdir(dir))) {
+ if (strncmp(d->d_name, "virtio-uml.", 11))
+ continue;
+
+ if (write(fd, d->d_name, strlen(d->d_name)) < 0)
+ perror("Failed to unbind virtio device");
+ }
+
+ close(fd);
+ closedir(dir);
+}
+
static bool find_kernel(const char *root, char path[PATH_MAX])
{
struct stat st;
@@ -2213,7 +2246,11 @@ int main(int argc, char *argv[])
sync();
/* Rebooting User Mode Linux starts it again */
- reboot(is_uml_guest() ? RB_POWER_OFF : RB_AUTOBOOT);
+ if (is_uml_guest()) {
+ uml_unbind_virtio();
+ reboot(RB_POWER_OFF);
+ } else
+ reboot(RB_AUTOBOOT);
return EXIT_SUCCESS;
}
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 03/10] emulator: Answer the time of a guest with a skew
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 01/10] tools/test-runner: Default to time travel with " Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 02/10] tools/test-runner: Unbind virtio devices before powering off UML Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value Luiz Augusto von Dentz
` (6 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
With a skew, the current time of the calendar may be ahead of a guest,
so answering it as the time of an interrupt moves the guest past the
events it requested, which then run in its past and make it panic as
time goes backwards.
Answer the time of the guest itself instead.
Fixes: d7ef45305b15 ("emulator: Add time travel calendar to btvirt")
Assisted-by: Claude:claude-opus-5-5
---
emulator/timetravel.c | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/emulator/timetravel.c b/emulator/timetravel.c
index c1938ed23bdc..be70acd3c10d 100644
--- a/emulator/timetravel.c
+++ b/emulator/timetravel.c
@@ -461,8 +461,14 @@ static void handle_msg(struct tt_client *client, struct tt_msg *msg)
break;
case TT_GET:
/* The guest takes it as the time of an interrupt: the current
- * time, which no guest is ahead of.
+ * time, which no guest is ahead of in lock-step. With a skew,
+ * other guests may be ahead of this one, which must stay at
+ * its own time to run the events it requested.
*/
+ if (tt.skew) {
+ send_msg(client, TT_ACK, msg->seq, client->time);
+ return;
+ }
client->time = MAX(client->time, tt.now);
send_msg(client, TT_ACK, msg->seq, client->time);
return;
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 03/10] emulator: Answer the time of a guest with a skew Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:34 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 05/10] test: functional: wait for BAP transports to be configured Luiz Augusto von Dentz
` (5 subsequent siblings)
9 siblings, 1 reply; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Registering an agent sets the adapter bondable, so setting Pairable
right after, e.g. with "pairable on" in bluetoothctl as soon as it
registered its default agent, fails with Busy although the pending
change already does what was requested.
Track the value of pending changes, and when a request sets the same
value, reply to it once the pending change completed with its result,
without sending the command again. Opposing requests still fail with
Busy.
Fixes: 348feb005a12 ("adapter: Return Busy error when setting is pending")
Assisted-by: Claude:claude-opus-5-5
---
src/adapter.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 77 insertions(+), 4 deletions(-)
diff --git a/src/adapter.c b/src/adapter.c
index edd2fdaf4db9..3c0895d1e241 100644
--- a/src/adapter.c
+++ b/src/adapter.c
@@ -280,6 +280,8 @@ struct btd_adapter {
char *short_name; /* controller short name */
uint32_t supported_settings; /* controller supported settings */
uint32_t pending_settings; /* pending controller settings */
+ uint32_t pending_values; /* values of pending settings */
+ struct queue *pending_props; /* requests waiting a pending setting */
uint32_t power_state; /* the power state */
uint32_t current_settings; /* current controller settings */
@@ -745,6 +747,44 @@ struct set_mode_data {
uint8_t value;
};
+struct pending_prop {
+ uint32_t setting;
+ GDBusPendingPropertySet id;
+ uint8_t status;
+};
+
+static bool reply_pending_prop(const void *data, const void *match_data)
+{
+ const struct pending_prop *prop = data;
+ const struct pending_prop *done = match_data;
+
+ if (!(prop->setting & done->setting))
+ return false;
+
+ if (done->status == MGMT_STATUS_SUCCESS)
+ g_dbus_pending_property_success(prop->id);
+ else if (done->status == MGMT_STATUS_RFKILLED)
+ g_dbus_pending_property_error(prop->id,
+ ERROR_INTERFACE ".Blocked",
+ mgmt_errstr(done->status));
+ else
+ g_dbus_pending_property_error(prop->id,
+ ERROR_INTERFACE ".Failed",
+ mgmt_errstr(done->status));
+
+ return true;
+}
+
+/* Reply to the requests that waited for a pending setting change */
+static void reply_pending_props(struct btd_adapter *adapter,
+ uint32_t setting, uint8_t status)
+{
+ struct pending_prop done = { .setting = setting, .status = status };
+
+ queue_remove_all(adapter->pending_props, reply_pending_prop, &done,
+ free);
+}
+
static void set_mode_complete(uint8_t status, uint16_t length,
const void *param, void *user_data)
{
@@ -761,6 +801,7 @@ static void set_mode_complete(uint8_t status, uint16_t length,
if (status != MGMT_STATUS_RFKILLED &&
data->setting & MGMT_SETTING_POWERED)
reset_power_state_target(adapter, data->value);
+ reply_pending_props(adapter, data->setting, status);
return;
}
@@ -770,6 +811,7 @@ static void set_mode_complete(uint8_t status, uint16_t length,
* event handling functions here.
*/
new_settings_callback(adapter->dev_id, length, param, adapter);
+ reply_pending_props(adapter, data->setting, status);
}
static void remove_temporary_devices(struct btd_adapter *adapter)
@@ -785,6 +827,17 @@ static void remove_temporary_devices(struct btd_adapter *adapter)
}
}
+static void set_pending(struct btd_adapter *adapter, uint32_t setting,
+ uint8_t value)
+{
+ adapter->pending_settings |= setting;
+
+ if (value)
+ adapter->pending_values |= setting;
+ else
+ adapter->pending_values &= ~setting;
+}
+
static bool set_mode(struct btd_adapter *adapter, uint16_t opcode,
uint8_t mode)
{
@@ -828,7 +881,7 @@ static bool set_mode(struct btd_adapter *adapter, uint16_t opcode,
if (mgmt_send(adapter->mgmt, opcode,
adapter->dev_id, sizeof(cp), &cp,
set_mode_complete, data, g_free) > 0) {
- adapter->pending_settings |= setting;
+ set_pending(adapter, setting, mode);
return true;
}
g_free(data);
@@ -3149,6 +3202,7 @@ static void property_set_mode_complete(uint8_t status, uint16_t length,
if (status != MGMT_STATUS_RFKILLED &&
data->setting & MGMT_SETTING_POWERED)
reset_power_state_target(adapter, data->value);
+ reply_pending_props(adapter, data->setting, status);
return;
}
@@ -3160,6 +3214,7 @@ static void property_set_mode_complete(uint8_t status, uint16_t length,
* event handling functions here.
*/
new_settings_callback(adapter->dev_id, length, param, adapter);
+ reply_pending_props(adapter, data->setting, status);
}
static void clear_discoverable(struct btd_adapter *adapter)
@@ -3199,9 +3254,23 @@ static void property_set_mode(struct btd_adapter *adapter, uint32_t setting,
dbus_message_iter_get_basic(value, &enable);
+ /* A pending change, e.g. bondable set when an agent registers,
+ * already does what is requested if it sets the same value: reply
+ * once it completed instead of sending the command again.
+ */
if (adapter->pending_settings & setting) {
- g_dbus_pending_property_error(id, ERROR_INTERFACE ".Busy",
- NULL);
+ struct pending_prop *prop;
+
+ if (!!(adapter->pending_values & setting) != !!enable) {
+ g_dbus_pending_property_error(id,
+ ERROR_INTERFACE ".Busy", NULL);
+ return;
+ }
+
+ prop = new0(struct pending_prop, 1);
+ prop->setting = setting;
+ prop->id = id;
+ queue_push_tail(adapter->pending_props, prop);
return;
}
@@ -3289,7 +3358,7 @@ static void property_set_mode(struct btd_adapter *adapter, uint32_t setting,
if (mgmt_send(adapter->mgmt, opcode, adapter->dev_id, len, param,
property_set_mode_complete, data, g_free) > 0) {
- adapter->pending_settings |= setting;
+ set_pending(adapter, setting, mode);
return;
}
@@ -6244,6 +6313,9 @@ static void adapter_free(gpointer user_data)
queue_destroy(adapter->exp_pending, cancel_exp_pending);
+ reply_pending_props(adapter, UINT32_MAX, MGMT_STATUS_FAILED);
+ queue_destroy(adapter->pending_props, NULL);
+
/*
* Unregister all handlers for this specific index since
* the adapter bound to them is no longer valid.
@@ -7311,6 +7383,7 @@ static struct btd_adapter *btd_adapter_new(uint16_t index)
adapter->auths = g_queue_new();
adapter->exps = queue_new();
adapter->exp_pending = queue_new();
+ adapter->pending_props = queue_new();
return btd_adapter_ref(adapter);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 05/10] test: functional: wait for BAP transports to be configured
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (3 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 06/10] shared/mcp: Don't drop commands before supported opcodes are read Luiz Augusto von Dentz
` (4 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
bluetoothctl only selects or acquires a transport its local endpoint was
configured with, which it learns from SetConfiguration, while the
transport object may be reported before. The tests acting on transports
could then fail with "transport endpoint not found" when the call came
later, as happens under User Mode Linux.
Wait for the transport reported under SetConfiguration in the tests
selecting or acquiring it.
Assisted-by: Claude:claude-opus-5-5
---
test/functional/test_bap.py | 22 ++++++++++++++--------
1 file changed, 14 insertions(+), 8 deletions(-)
diff --git a/test/functional/test_bap.py b/test/functional/test_bap.py
index 7cd9466bd5f7..221efd982071 100644
--- a/test/functional/test_bap.py
+++ b/test/functional/test_bap.py
@@ -37,6 +37,11 @@ ControllerMode = le
TRANSPORT_RE = r"Transport (/org/bluez/\S+/fd\d+)"
+# A transport is reported once created, and indented under the endpoint
+# SetConfiguration call once its local endpoint is configured with it,
+# which bluetoothctl requires to select or acquire it
+CONFIGURED = "\t"
+
def dev_addr(host):
return host.bdaddr.upper().replace(":", "_")
@@ -259,14 +264,15 @@ def unicast_hosts(hosts):
yield host0, host1, initiator, acceptor
-def expect_transports(ctl):
+def expect_transports(ctl, configured=False):
"""
The endpoint is configured for stereo, so one stream per location is
created, each with its own CIS in the same CIG.
"""
- _, m = ctl.expect(r"Transport (/org/bluez/\S+/fd0)")
+ prefix = CONFIGURED if configured else ""
+ _, m = ctl.expect(prefix + r"Transport (/org/bluez/\S+/fd0)")
left = m[0].decode("utf-8")
- _, m = ctl.expect(r"Transport (/org/bluez/\S+/fd1)")
+ _, m = ctl.expect(prefix + r"Transport (/org/bluez/\S+/fd1)")
right = m[0].decode("utf-8")
return left, right
@@ -292,7 +298,7 @@ def test_bap_unicast_transport_created(unicast_hosts):
def test_bap_unicast_transport_acquire(unicast_hosts):
host0, host1, initiator, acceptor = unicast_hosts
- left, right = expect_transports(initiator)
+ left, right = expect_transports(initiator, configured=True)
# The CIS are only created once every CIS of the CIG is ready, so
# all the transports need to be acquired
@@ -627,7 +633,7 @@ def test_bap_broadcast_transport_created(hosts, source_script):
def test_bap_broadcast_transport_acquire(hosts, source_script):
source, sink = start_broadcast(hosts, source_script)
- _, m = sink.expect(TRANSPORT_RE)
+ _, m = sink.expect(CONFIGURED + TRANSPORT_RE)
transport = m[0].decode("utf-8")
# Selecting the transport syncs to the BIG and starts acquiring it
@@ -682,7 +688,7 @@ def expect_bis_transport(ctl, host, bis):
Transport a side created for the BIS carrying its own channel,
checking it is the only one it has.
"""
- _, m = ctl.expect(BIS_TRANSPORT.format(bis))
+ _, m = ctl.expect(CONFIGURED + BIS_TRANSPORT.format(bis))
transport = m[0].decode("utf-8")
# Checked over D-Bus rather than with transport.list, as the
@@ -942,8 +948,8 @@ def set_hosts(hosts, secure_connections):
"Pairing successful",
f"DeviceSet /org/bluez/hci0/{SET_PATH}",
f"Device {host2.bdaddr.upper()} Bonded: yes",
- TRANSPORT.format(dev_addr(host1)),
- TRANSPORT.format(dev_addr(host2)),
+ CONFIGURED + TRANSPORT.format(dev_addr(host1)),
+ CONFIGURED + TRANSPORT.format(dev_addr(host2)),
],
)
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 06/10] shared/mcp: Don't drop commands before supported opcodes are read
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (4 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 05/10] test: functional: wait for BAP transports to be configured Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 07/10] doc/test-functional: Document User Mode Linux VM hosts Luiz Augusto von Dentz
` (3 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
The remote player is created once the CCID of the service is read,
before Media Control Point Opcodes Supported is, and until then every
command was dropped. As the player returned the request id as error
code, the D-Bus call succeeded anyway, so e.g. a Play right after the
player appeared was silently lost.
Only refuse opcodes once the supported ones are known, letting the
server reject them otherwise, and report commands that were not sent as
errors.
Fixes: 9027edc4604e ("shared/mcp, mcp: support multiple MCP, and add non-stub MCS server")
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/mcp.c | 10 +++++-----
src/shared/mcp.c | 8 +++++++-
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/profiles/audio/mcp.c b/profiles/audio/mcp.c
index 314bce2439df..7522ca4e6ea3 100644
--- a/profiles/audio/mcp.c
+++ b/profiles/audio/mcp.c
@@ -253,35 +253,35 @@ static int remote_mp_play(struct media_player *mp, void *user_data)
{
struct remote_player *remote = user_data;
- return bt_mcp_play(remote->mcp, remote->ccid);
+ return bt_mcp_play(remote->mcp, remote->ccid) ? 0 : -EIO;
}
static int remote_mp_pause(struct media_player *mp, void *user_data)
{
struct remote_player *remote = user_data;
- return bt_mcp_pause(remote->mcp, remote->ccid);
+ return bt_mcp_pause(remote->mcp, remote->ccid) ? 0 : -EIO;
}
static int remote_mp_stop(struct media_player *mp, void *user_data)
{
struct remote_player *remote = user_data;
- return bt_mcp_stop(remote->mcp, remote->ccid);
+ return bt_mcp_stop(remote->mcp, remote->ccid) ? 0 : -EIO;
}
static int remote_mp_next(struct media_player *mp, void *user_data)
{
struct remote_player *remote = user_data;
- return bt_mcp_next_track(remote->mcp, remote->ccid);
+ return bt_mcp_next_track(remote->mcp, remote->ccid) ? 0 : -EIO;
}
static int remote_mp_previous(struct media_player *mp, void *user_data)
{
struct remote_player *remote = user_data;
- return bt_mcp_previous_track(remote->mcp, remote->ccid);
+ return bt_mcp_previous_track(remote->mcp, remote->ccid) ? 0 : -EIO;
}
static bool remote_mp_set_setting(struct media_player *mp, const char *key,
diff --git a/src/shared/mcp.c b/src/shared/mcp.c
index 4ee14d1567fe..8ddc072dacaa 100644
--- a/src/shared/mcp.c
+++ b/src/shared/mcp.c
@@ -108,6 +108,7 @@ struct bt_mcp_service {
struct bt_mcs_db rdb;
bool ready;
+ bool op_supported_known;
unsigned int notify_id[MAX_ATTR];
unsigned int notify_id_count;
@@ -1256,7 +1257,11 @@ static unsigned int mcp_command(struct bt_mcp *mcp, uint8_t ccid, uint8_t op,
if (!service || !cmd)
return 0;
- if (!(service->rdb.media_cp_op_supported_value & cmd->support))
+ /* The service is added once its CCID is read, so commands may come
+ * before the supported opcodes are known: let the server reject them
+ */
+ if (service->op_supported_known &&
+ !(service->rdb.media_cp_op_supported_value & cmd->support))
return 0;
DBG_SVC(service, "%s %d", cmd->name, arg);
@@ -1686,6 +1691,7 @@ static void update_media_cp_op_supported(bool success, uint8_t att_ecode,
DBG_SVC(service, "Media CP Op Supported: %d", v);
service->rdb.media_cp_op_supported_value = v;
+ service->op_supported_known = true;
}
static void update_add_service(void *data, void *user_data)
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 07/10] doc/test-functional: Document User Mode Linux VM hosts
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (5 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 06/10] shared/mcp: Don't drop commands before supported opcodes are read Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 08/10] a2dp: Fix refusing the AVDTP connection of a new link Luiz Augusto von Dentz
` (2 subsequent siblings)
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Document running the VM hosts as User Mode Linux guests through
test-runner, e.g. on machines without KVM support, with btvirt serving
their controllers and channels over vhost-user, and the kernel
configuration this needs.
The hosts run on real time, as time travel makes these tests slower:
document the measurements and why.
Assisted-by: Claude:claude-opus-5-5
---
doc/test-functional.rst | 74 ++++++++++++++++++++++++++++++++++++++---
1 file changed, 70 insertions(+), 4 deletions(-)
diff --git a/doc/test-functional.rst b/doc/test-functional.rst
index e2fcf34138b3..a50cd441189c 100644
--- a/doc/test-functional.rst
+++ b/doc/test-functional.rst
@@ -90,8 +90,9 @@ The following additional options apply:
``--kernel=<image>``
Kernel image (or built Linux source tree root) to
- use. See **test-runner(1)** and `tester.config` for required
- kernel config.
+ use, either a QEMU ``bzImage`` or a User Mode Linux binary, see
+ `User Mode Linux`_. See **test-runner(1)** and `tester.config`
+ for required kernel config.
If not provided, value from `FUNCTIONAL_TESTING_KERNEL`
environment variable is used. If none, no image is used.
@@ -167,7 +168,9 @@ A test run consists of the Pytest process (the *upper tester*, running on
the developer machine) and one or more *VM hosts*. Each VM host is a
separate **test-runner(1)** instance, i.e. a QEMU virtual machine booting
the given kernel image with the developer machine's root filesystem passed
-through read-only. The VM hosts are wired together by a Bluetooth
+through read-only. With a User Mode Linux kernel the VM hosts are UML
+processes instead, see `User Mode Linux`_. The VM hosts are wired
+together by a Bluetooth
controller: either the emulated **btvirt(1)** controller, or a real USB
controller passed through to the VM::
@@ -249,6 +252,46 @@ Writing test code that runs in the VM, and the available VM-side
facilities, are documented by `pytest-bluezenv
<https://pypi.org/project/pytest-bluezenv/>`__.
+User Mode Linux
+---------------
+
+When ``--kernel`` is a User Mode Linux (UML) binary, or a built kernel tree
+with a ``linux`` binary and no ``bzImage``, **test-runner(1)** runs the VM
+hosts as UML processes instead of QEMU, e.g. on machines without KVM
+support, with the developer machine's root filesystem mounted read-only
+through hostfs. The channels are passed to it with ``-o`` as kernel
+parameters: a single ``btvirt`` process serves every device of the VM hosts
+over vhost-user (see `virtio.rst <virtio.rst>`_), the controllers with
+``-V``, and the control and log channels as virtio consoles with
+``--console``::
+
+ VM host #0 (UML) developer machine VM host #1 (UML)
+ ┌────────────┐ ┌─────────────────┐ ┌────────────┐
+ │ virtio_bt ├─ vhost ────┤ btvirt ├──── vhost ─┤ virtio_bt │
+ │ hvc ├─ vhost ────┤ controllers, ├──── vhost ─┤ hvc │
+ └────────────┘ │ consoles │ └────────────┘
+ └────────┬────────┘
+ │ control, log
+ pytest
+
+The shared directory is mounted with hostfs instead of 9p. There is no
+``tty`` channel, so ``test/test-functional-attach`` is not available, and
+controllers cannot be passed through, so ``--usb`` and ``--pcie`` need
+QEMU.
+
+The VM hosts run on real time, with test-runner ``-R``. Time travel, which
+makes the kernel testers faster, makes the functional tests slower: they
+exchange traffic between two or three guests, and in lock-step only one
+guest runs at a time, each exchange taking a round trip to the calendar.
+Wall clock time of ``make check-functional`` with 4 workers, same kernel
+version and machine, UML with seccomp, cut off at 78 s, and of
+``-k test_bluetoothctl``::
+
+ VM hosts check-functional test_bluetoothctl
+ KVM 68 s, 49 tests run 20.1 s
+ UML 78 s, 41 tests run 20.4 s
+ UML time travel 78 s, 12 tests run 25.8 s
+
REQUIREMENTS
============
@@ -258,7 +301,7 @@ General
The following are needed:
-- QEmu (x86_64)
+- QEmu (x86_64), unless running User Mode Linux VM hosts
- ``dbus-daemon`` available
Recommended:
@@ -317,6 +360,22 @@ configuration enabled:
CONFIG_PTP_1588_CLOCK_KVM=y
CONFIG_PTP_1588_CLOCK_VMCLOCK=y
+A User Mode Linux kernel is built with ``ARCH=um`` from the same config,
+and needs in addition:
+
+.. code-block::
+
+ CONFIG_HOSTFS=y
+ CONFIG_VIRTIO_UML=y
+
+.. code-block::
+
+ make ARCH=um defconfig
+ scripts/kconfig/merge_config.sh -m .config ../bluez/doc/tester.config
+ make ARCH=um olddefconfig linux
+
+The KVM and PTP options above do not apply to User Mode Linux.
+
USB
---
@@ -348,6 +407,13 @@ Run all tests
Test output is logged in ``test-functional.log``.
+Run all tests under User Mode Linux
+-----------------------------------
+
+.. code-block::
+
+ $ test/test-functional --kernel=/pathto/linux
+
Show output during run
----------------------
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 08/10] a2dp: Fix refusing the AVDTP connection of a new link
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (6 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 07/10] doc/test-functional: Document User Mode Linux VM hosts Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 09/10] adapter: Don't cancel requests of a new link on stale disconnection Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 10/10] test: Cover A2DP reconnection with a pending disconnection Luiz Augusto von Dentz
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
When the remote reconnects before the disconnection of the previous
link is processed, the A2DP channel of that link is still registered
and an incoming AVDTP connection on the new link is taken as a
transport channel of its session, and refused.
Process the disconnection of the signalling channel of the existing
session first if it is pending, so the connection is handled as a new
signalling channel.
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/a2dp.c | 7 +++++++
profiles/audio/avdtp.c | 24 ++++++++++++++++++++++++
profiles/audio/avdtp.h | 1 +
3 files changed, 32 insertions(+)
diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index cb1b3a9804e7..3b29ec204d00 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -2796,6 +2796,13 @@ static void confirm_cb(GIOChannel *io, gpointer data)
goto drop;
chan = queue_find(server->channels, match_by_device, device);
+
+ /* The channel of a previous link, if its disconnection was not
+ * processed yet: this is not a transport channel of it.
+ */
+ if (chan && chan->session && avdtp_check_disconnected(chan->session))
+ chan = queue_find(server->channels, match_by_device, device);
+
if (chan) {
struct a2dp_setup *setup;
diff --git a/profiles/audio/avdtp.c b/profiles/audio/avdtp.c
index 56d27b973a17..30f80a0c2fd1 100644
--- a/profiles/audio/avdtp.c
+++ b/profiles/audio/avdtp.c
@@ -18,6 +18,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <errno.h>
+#include <poll.h>
#include <unistd.h>
#include <assert.h>
@@ -1168,6 +1169,29 @@ static void connection_lost(struct avdtp *session, int err)
avdtp_unref(session);
}
+/* Process a disconnection of the signalling channel not dispatched yet,
+ * returning whether it is disconnected.
+ */
+bool avdtp_check_disconnected(struct avdtp *session)
+{
+ struct pollfd pfd;
+
+ if (!session->io)
+ return false;
+
+ pfd.fd = g_io_channel_unix_get_fd(session->io);
+ pfd.events = 0;
+ pfd.revents = 0;
+
+ if (poll(&pfd, 1, 0) <= 0 ||
+ !(pfd.revents & (POLLHUP | POLLERR | POLLNVAL)))
+ return false;
+
+ connection_lost(session, ECONNRESET);
+
+ return true;
+}
+
static bool disconnect_timeout(gpointer user_data)
{
struct avdtp *session = user_data;
diff --git a/profiles/audio/avdtp.h b/profiles/audio/avdtp.h
index 10c8f496b44e..ab5059b055ec 100644
--- a/profiles/audio/avdtp.h
+++ b/profiles/audio/avdtp.h
@@ -205,6 +205,7 @@ typedef void (*avdtp_discover_cb_t) (struct avdtp *session, GSList *seps,
struct avdtp_error *err, void *user_data);
void avdtp_unref(struct avdtp *session);
+bool avdtp_check_disconnected(struct avdtp *session);
struct avdtp *avdtp_ref(struct avdtp *session);
struct avdtp_service_capability *avdtp_service_cap_new(uint8_t category,
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 09/10] adapter: Don't cancel requests of a new link on stale disconnection
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (7 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 08/10] a2dp: Fix refusing the AVDTP connection of a new link Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 10/10] test: Cover A2DP reconnection with a pending disconnection Luiz Augusto von Dentz
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
When the remote reconnects before the disconnection of the previous
link is processed, e.g. if bluetoothd was slow, processing it cancels
the pending service authorizations and authentication of the device,
which then belong to the new link, refusing e.g. its AVDTP connection.
Check with the kernel whether a BR/EDR link to the device exists again,
and if so don't cancel them.
Fixes: 5b6ffe0381e8 ("adapter: Cancel the service authorization when remote is disconnected")
Assisted-by: Claude:claude-opus-5-5
---
src/adapter.c | 42 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 40 insertions(+), 2 deletions(-)
diff --git a/src/adapter.c b/src/adapter.c
index 3c0895d1e241..fd6746258f70 100644
--- a/src/adapter.c
+++ b/src/adapter.c
@@ -7985,6 +7985,36 @@ struct agent *adapter_get_agent(struct btd_adapter *adapter)
return agent_get(NULL);
}
+/* Whether the kernel has a BR/EDR link to the device: its disconnection
+ * may be processed after the device connected again, e.g. when bluetoothd
+ * was slow to process events.
+ */
+static bool link_connected(struct btd_adapter *adapter,
+ struct btd_device *device, uint8_t bdaddr_type)
+{
+ struct hci_conn_info_req *cr;
+ bool connected;
+ int dd;
+
+ if (bdaddr_type != BDADDR_BREDR)
+ return false;
+
+ dd = hci_open_dev(adapter->dev_id);
+ if (dd < 0)
+ return false;
+
+ cr = g_malloc0(sizeof(*cr) + sizeof(struct hci_conn_info));
+ bacpy(&cr->bdaddr, device_get_address(device));
+ cr->type = ACL_LINK;
+
+ connected = !ioctl(dd, HCIGETCONNINFO, cr);
+
+ g_free(cr);
+ hci_close_dev(dd);
+
+ return connected;
+}
+
static void adapter_remove_connection(struct btd_adapter *adapter,
struct btd_device *device,
uint8_t bdaddr_type,
@@ -8005,7 +8035,9 @@ static void adapter_remove_connection(struct btd_adapter *adapter,
device_remove_connection(device, bdaddr_type, &remove_device, reason);
- device_cancel_authentication(device, TRUE);
+ /* Requests of a link that connected meanwhile are not cancelled */
+ if (!link_connected(adapter, device, bdaddr_type))
+ device_cancel_authentication(device, TRUE);
/* If another bearer is still connected */
if (btd_device_bearer_is_connected(device))
@@ -9013,6 +9045,7 @@ static void dev_disconnected(struct btd_adapter *adapter,
uint8_t reason)
{
struct btd_device *device;
+ bool reconnected = false;
char dst[18];
ba2str(&addr->bdaddr, dst);
@@ -9023,6 +9056,11 @@ static void dev_disconnected(struct btd_adapter *adapter,
if (device) {
bool removed;
+ /* The device connected again before this was processed: its
+ * pending requests belong to the new link.
+ */
+ reconnected = link_connected(adapter, device, addr->type);
+
adapter_remove_connection(adapter, device, addr->type, reason,
&removed);
/* No need to continue if device was removed from the adapter,
@@ -9036,7 +9074,7 @@ static void dev_disconnected(struct btd_adapter *adapter,
}
/* device could be still connected to different bearer */
- if (!device || !btd_device_is_connected(device))
+ if (!reconnected && (!device || !btd_device_is_connected(device)))
bonding_attempt_complete(adapter, &addr->bdaddr, addr->type,
MGMT_STATUS_DISCONNECTED);
}
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH BlueZ v1 10/10] test: Cover A2DP reconnection with a pending disconnection
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
` (8 preceding siblings ...)
2026-10-30 21:29 ` [PATCH BlueZ v1 09/10] adapter: Don't cancel requests of a new link on stale disconnection Luiz Augusto von Dentz
@ 2026-10-30 21:29 ` Luiz Augusto von Dentz
9 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:29 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Stop bluetoothd on the sink while the source disconnects and connects
again, so that the disconnection of the previous link and the AVDTP
connection of the new one are pending together when it resumes, which
made the new connection be refused.
Assisted-by: Claude:claude-opus-5-5
---
doc/functional-a2dp.rst | 32 ++++++++++++++++++++++++++++++++
test/functional/test_a2dp.py | 36 ++++++++++++++++++++++++++++++++++++
2 files changed, 68 insertions(+)
diff --git a/doc/functional-a2dp.rst b/doc/functional-a2dp.rst
index f38a458bb7c2..367992159b7b 100644
--- a/doc/functional-a2dp.rst
+++ b/doc/functional-a2dp.rst
@@ -134,3 +134,35 @@ test_a2dp_disconnect_during_setconf[accept|reject]
``auto_config()``. A late ``yes`` used to also leave the stale
transport registered, failing the next connection with
``Resource temporarily unavailable``.
+
+test_a2dp_reconnect_pending_disconnect
+--------------------------------------
+
+:Setup: As above, with the stream configured and the transport created.
+
+:Steps:
+ 1. Peripheral: stop `bluetoothd` with ``SIGSTOP``.
+ 2. Central: ``disconnect <peripheral bdaddr>``.
+ 3. Central: ``connect <peripheral bdaddr>``.
+ 4. Peripheral: once the new link is up, resume `bluetoothd` with
+ ``SIGCONT``.
+
+:Expected:
+ 1. `bluetoothd` on the peripheral stops processing events.
+ 2. ``Disconnection successful`` on the central.
+ 3. The new ACL link is established by the kernels, and the AVDTP
+ connection of the central waits for `bluetoothd` on the
+ peripheral to accept it.
+ 4. ``Connection successful`` on the central, and a transport
+ appears on both hosts.
+
+:Notes: Regression test for `bluetoothd` on the peripheral handling the
+ incoming AVDTP connection of the new link before the disconnection
+ of the previous one. It used to take the connection for a transport
+ channel of the A2DP channel of the previous link, still registered,
+ and refuse it, or, once that channel was gone, the disconnection
+ cancelled the service authorization of the new channel
+ (``Access denied: org.bluez.Error.Canceled``). In both cases the
+ central failed with ``Connection refused``. Without the pause the
+ two events are rarely pending at the same time, mostly under the
+ slower timing of User Mode Linux.
diff --git a/test/functional/test_a2dp.py b/test/functional/test_a2dp.py
index 12348ea05ae2..c07a2b2a1232 100644
--- a/test/functional/test_a2dp.py
+++ b/test/functional/test_a2dp.py
@@ -4,6 +4,9 @@
Tests for A2DP using bluetoothctl in VM instances
"""
+import signal
+import subprocess
+
import pytest
from pytest_bluezenv import Bluetoothd, Pexpect, find_exe, host_config
@@ -200,3 +203,36 @@ def test_a2dp_disconnect_during_setconf(hosts, reply):
sink.send("yes\n")
source.expect("Connection successful")
source.expect(TRANSPORT_RE)
+
+
+def vm_signal_bluetoothd(sig):
+ subprocess.run(["pkill", f"-{sig}", "-x", "bluetoothd"], check=True)
+
+
+@a2dp_host_config
+def test_a2dp_reconnect_pending_disconnect(a2dp_hosts):
+ host0, host1, source, sink = a2dp_hosts
+
+ source.expect(TRANSPORT_RE)
+ sink.expect(TRANSPORT_RE)
+
+ # Stop the sink's bluetoothd so that the disconnection of the link and
+ # the AVDTP connection on the new one are pending together when it
+ # resumes, as when it is slow to process the disconnection
+ host1.call(vm_signal_bluetoothd, signal.SIGSTOP)
+ try:
+ source.send(f"disconnect {host1.bdaddr}\n")
+ source.expect("Disconnection successful")
+
+ source.send(f"connect {host1.bdaddr}\n")
+ source.expect(f"Device {host1.bdaddr.upper()} Connected: yes")
+ finally:
+ host1.call(vm_signal_bluetoothd, signal.SIGCONT)
+
+ # The transport may be reported before or after the reply
+ pending = ["Connection successful", TRANSPORT_RE]
+ while pending:
+ idx, _ = source.expect(pending)
+ pending.pop(idx)
+
+ sink.expect(TRANSPORT_RE)
--
2.56.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* Re: [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value
2026-10-30 21:29 ` [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value Luiz Augusto von Dentz
@ 2026-10-30 21:34 ` Luiz Augusto von Dentz
0 siblings, 0 replies; 12+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-30 21:34 UTC (permalink / raw)
To: linux-bluetooth
Hi @Pauli Virtanen
On Fri, Oct 30, 2026 at 5:29 PM Luiz Augusto von Dentz
<luiz.dentz@gmail.com> wrote:
>
> From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
>
> Registering an agent sets the adapter bondable, so setting Pairable
> right after, e.g. with "pairable on" in bluetoothctl as soon as it
> registered its default agent, fails with Busy although the pending
> change already does what was requested.
>
> Track the value of pending changes, and when a request sets the same
> value, reply to it once the pending change completed with its result,
> without sending the command again. Opposing requests still fail with
> Busy.
>
> Fixes: 348feb005a12 ("adapter: Return Busy error when setting is pending")
> Assisted-by: Claude:claude-opus-5-5
> ---
> src/adapter.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++---
> 1 file changed, 77 insertions(+), 4 deletions(-)
>
> diff --git a/src/adapter.c b/src/adapter.c
> index edd2fdaf4db9..3c0895d1e241 100644
> --- a/src/adapter.c
> +++ b/src/adapter.c
> @@ -280,6 +280,8 @@ struct btd_adapter {
> char *short_name; /* controller short name */
> uint32_t supported_settings; /* controller supported settings */
> uint32_t pending_settings; /* pending controller settings */
> + uint32_t pending_values; /* values of pending settings */
> + struct queue *pending_props; /* requests waiting a pending setting */
> uint32_t power_state; /* the power state */
> uint32_t current_settings; /* current controller settings */
>
> @@ -745,6 +747,44 @@ struct set_mode_data {
> uint8_t value;
> };
>
> +struct pending_prop {
> + uint32_t setting;
> + GDBusPendingPropertySet id;
> + uint8_t status;
> +};
> +
> +static bool reply_pending_prop(const void *data, const void *match_data)
> +{
> + const struct pending_prop *prop = data;
> + const struct pending_prop *done = match_data;
> +
> + if (!(prop->setting & done->setting))
> + return false;
> +
> + if (done->status == MGMT_STATUS_SUCCESS)
> + g_dbus_pending_property_success(prop->id);
> + else if (done->status == MGMT_STATUS_RFKILLED)
> + g_dbus_pending_property_error(prop->id,
> + ERROR_INTERFACE ".Blocked",
> + mgmt_errstr(done->status));
> + else
> + g_dbus_pending_property_error(prop->id,
> + ERROR_INTERFACE ".Failed",
> + mgmt_errstr(done->status));
> +
> + return true;
> +}
> +
> +/* Reply to the requests that waited for a pending setting change */
> +static void reply_pending_props(struct btd_adapter *adapter,
> + uint32_t setting, uint8_t status)
> +{
> + struct pending_prop done = { .setting = setting, .status = status };
> +
> + queue_remove_all(adapter->pending_props, reply_pending_prop, &done,
> + free);
> +}
> +
> static void set_mode_complete(uint8_t status, uint16_t length,
> const void *param, void *user_data)
> {
> @@ -761,6 +801,7 @@ static void set_mode_complete(uint8_t status, uint16_t length,
> if (status != MGMT_STATUS_RFKILLED &&
> data->setting & MGMT_SETTING_POWERED)
> reset_power_state_target(adapter, data->value);
> + reply_pending_props(adapter, data->setting, status);
> return;
> }
>
> @@ -770,6 +811,7 @@ static void set_mode_complete(uint8_t status, uint16_t length,
> * event handling functions here.
> */
> new_settings_callback(adapter->dev_id, length, param, adapter);
> + reply_pending_props(adapter, data->setting, status);
> }
>
> static void remove_temporary_devices(struct btd_adapter *adapter)
> @@ -785,6 +827,17 @@ static void remove_temporary_devices(struct btd_adapter *adapter)
> }
> }
>
> +static void set_pending(struct btd_adapter *adapter, uint32_t setting,
> + uint8_t value)
> +{
> + adapter->pending_settings |= setting;
> +
> + if (value)
> + adapter->pending_values |= setting;
> + else
> + adapter->pending_values &= ~setting;
> +}
> +
> static bool set_mode(struct btd_adapter *adapter, uint16_t opcode,
> uint8_t mode)
> {
> @@ -828,7 +881,7 @@ static bool set_mode(struct btd_adapter *adapter, uint16_t opcode,
> if (mgmt_send(adapter->mgmt, opcode,
> adapter->dev_id, sizeof(cp), &cp,
> set_mode_complete, data, g_free) > 0) {
> - adapter->pending_settings |= setting;
> + set_pending(adapter, setting, mode);
> return true;
> }
> g_free(data);
> @@ -3149,6 +3202,7 @@ static void property_set_mode_complete(uint8_t status, uint16_t length,
> if (status != MGMT_STATUS_RFKILLED &&
> data->setting & MGMT_SETTING_POWERED)
> reset_power_state_target(adapter, data->value);
> + reply_pending_props(adapter, data->setting, status);
> return;
> }
>
> @@ -3160,6 +3214,7 @@ static void property_set_mode_complete(uint8_t status, uint16_t length,
> * event handling functions here.
> */
> new_settings_callback(adapter->dev_id, length, param, adapter);
> + reply_pending_props(adapter, data->setting, status);
> }
>
> static void clear_discoverable(struct btd_adapter *adapter)
> @@ -3199,9 +3254,23 @@ static void property_set_mode(struct btd_adapter *adapter, uint32_t setting,
>
> dbus_message_iter_get_basic(value, &enable);
>
> + /* A pending change, e.g. bondable set when an agent registers,
> + * already does what is requested if it sets the same value: reply
> + * once it completed instead of sending the command again.
> + */
> if (adapter->pending_settings & setting) {
> - g_dbus_pending_property_error(id, ERROR_INTERFACE ".Busy",
> - NULL);
> + struct pending_prop *prop;
> +
> + if (!!(adapter->pending_values & setting) != !!enable) {
> + g_dbus_pending_property_error(id,
> + ERROR_INTERFACE ".Busy", NULL);
> + return;
> + }
> +
> + prop = new0(struct pending_prop, 1);
> + prop->setting = setting;
> + prop->id = id;
> + queue_push_tail(adapter->pending_props, prop);
> return;
> }
>
> @@ -3289,7 +3358,7 @@ static void property_set_mode(struct btd_adapter *adapter, uint32_t setting,
>
> if (mgmt_send(adapter->mgmt, opcode, adapter->dev_id, len, param,
> property_set_mode_complete, data, g_free) > 0) {
> - adapter->pending_settings |= setting;
> + set_pending(adapter, setting, mode);
> return;
> }
>
> @@ -6244,6 +6313,9 @@ static void adapter_free(gpointer user_data)
>
> queue_destroy(adapter->exp_pending, cancel_exp_pending);
>
> + reply_pending_props(adapter, UINT32_MAX, MGMT_STATUS_FAILED);
> + queue_destroy(adapter->pending_props, NULL);
> +
> /*
> * Unregister all handlers for this specific index since
> * the adapter bound to them is no longer valid.
> @@ -7311,6 +7383,7 @@ static struct btd_adapter *btd_adapter_new(uint16_t index)
> adapter->auths = g_queue_new();
> adapter->exps = queue_new();
> adapter->exp_pending = queue_new();
> + adapter->pending_props = queue_new();
>
> return btd_adapter_ref(adapter);
> }
> --
> 2.56.0
This one should address the same problem you had with test:
functional: retry pairable when Busy. Let me know if you still want to
merge that one first or you are fine with the reply waiting the
command to complete.
--
Luiz Augusto von Dentz
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-10-30 21:35 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-30 21:29 [PATCH BlueZ v1 00/10] Run test-functional under User Mode Linux Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 01/10] tools/test-runner: Default to time travel with " Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 02/10] tools/test-runner: Unbind virtio devices before powering off UML Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 03/10] emulator: Answer the time of a guest with a skew Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 04/10] adapter: Don't return Busy when a pending change sets the same value Luiz Augusto von Dentz
2026-10-30 21:34 ` Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 05/10] test: functional: wait for BAP transports to be configured Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 06/10] shared/mcp: Don't drop commands before supported opcodes are read Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 07/10] doc/test-functional: Document User Mode Linux VM hosts Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 08/10] a2dp: Fix refusing the AVDTP connection of a new link Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 09/10] adapter: Don't cancel requests of a new link on stale disconnection Luiz Augusto von Dentz
2026-10-30 21:29 ` [PATCH BlueZ v1 10/10] test: Cover A2DP reconnection with a pending disconnection Luiz Augusto von Dentz
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox