* [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration
@ 2026-10-07 19:52 Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 2/3] a2dp: Fix crash on NULL session in auto_config Luiz Augusto von Dentz
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-07 19:52 UTC (permalink / raw)
To: linux-bluetooth
From: Eduardo Alves <eduardoalves8006@gmail.com>
When the MediaEndpoint1 rejects SetConfiguration, auto_config() calls
setconf_cb() with an error and avdtp frees the avdtp_stream, but the
a2dp_stream wrapping it is left on sep->streams with a dangling stream
pointer. The next stream configured on the same session finds it in
a2dp_config() and reads the freed avdtp_stream:
ERROR: AddressSanitizer: heap-use-after-free
READ of size 4
#0 avdtp_stream_get_state profiles/audio/avdtp.c:3952
#1 a2dp_config profiles/audio/a2dp.c:3282
#2 select_complete profiles/audio/source.c:200
#3 finalize_select profiles/audio/a2dp.c:486
freed by thread T0 here:
#1 stream_free profiles/audio/avdtp.c:747
#2 setconf_cb profiles/audio/avdtp.c:1504
#3 auto_config profiles/audio/a2dp.c:752
#4 endpoint_setconf_cb profiles/audio/a2dp.c:768
Fix it by destroying the a2dp_stream when the configuration is
rejected, which also drops the session reference it holds.
This can be reproduced with test-functional using two VMs over btvirt,
where the A2DP Sink endpoint replies to SetConfiguration with an error
and the sink then connects to the source on the same session.
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/a2dp.c | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index 3c991de6acb7..08a9e1c96e4f 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -748,9 +748,17 @@ static gboolean auto_config(gpointer data)
}
done:
- if (setup->setconf_cb)
+ if (setup->setconf_cb) {
+ /* Rejecting the configuration frees the avdtp_stream */
+ if (setup->err)
+ a2dp_stream_destroy(setup->sep, setup->stream);
+
setup->setconf_cb(setup->session, setup->stream, setup->err);
+ if (setup->err)
+ setup->stream = NULL;
+ }
+
finalize_config(setup);
setup_error_set(setup, NULL);
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH BlueZ v2 2/3] a2dp: Fix crash on NULL session in auto_config
2026-10-07 19:52 [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration Luiz Augusto von Dentz
@ 2026-10-07 19:52 ` Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 3/3] test: Cover A2DP disconnection during SetConfiguration Luiz Augusto von Dentz
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-07 19:52 UTC (permalink / raw)
To: linux-bluetooth
From: Eduardo Alves <eduardoalves8006@gmail.com>
If the AVDTP channel is disconnected while bluetoothd is still waiting
for the MediaEndpoint1 to reply to SetConfiguration, channel_free()
sets setup->session to NULL but the setup stays alive since the pending
endpoint request holds a reference to it. Once the endpoint replies, or
the call times out and the request is canceled, endpoint_setconf_cb()
calls auto_config() which passes the NULL session to avdtp_get_device()
and then dereferences the resulting NULL device:
bluetoothd[820]: profiles/audio/media.c:endpoint_reply() Endpoint
replied with an error: org.freedesktop.DBus.Error.NoReply
kernel: bluetoothd[820]: segfault at 1a0 ip 00005633dd741323
sp 00007ffe51923650 error 4 in bluetoothd
#0 auto_config (data=0x56340361a120) at profiles/audio/a2dp.c:723
#1 endpoint_setconf_cb (setup=...) at profiles/audio/a2dp.c:768
#2 media_endpoint_cancel (request=...) at profiles/audio/media.c:208
#3 media_endpoint_cancel_all () at profiles/audio/media.c:216
#4 clear_endpoint () at profiles/audio/media.c:379
#5 endpoint_reply (user_data=...) at profiles/audio/media.c:407
(gdb) p *setup
$1 = {chan = 0x0, session = 0x0, ...,
setconf_cb = 0x5633dd74c780 <setconf_cb>, ..., ref = 2}
Commit 14750a2e48f4 ("audio/a2dp: Fix Access session device only when
its valid") fixed the same crash, but commit 77932f2dac1a
("profiles/audio: add nullity checks") moved avdtp_get_device() back
ahead of the checks. Just reordering is not enough though: the
a2dp_stream is still on sep->streams, so the aborted check passes and
setconf_cb() would then be called with a NULL session as well.
Fix it by making finalize_all() abort the pending Set Configuration
while the session is still valid: clearing the endpoint configuration
sends ClearConfiguration, removes the MediaTransport created for the
request and cancels the request, so auto_config() rejects the
configuration and frees the pending avdtp_stream and a2dp_stream, both
previously leaked together with the session reference held by the
latter. If no request is pending, the configuration is rejected
directly. auto_config() bails out when setup->session is NULL, as it
may still run from idle, and setconf_cb is cleared once called so it
cannot be called twice.
Removing a transport now cancels the endpoint requests pending for it,
so a late reply is ignored instead of leaving the transport registered,
which made the next connection fail with "Resource temporarily
unavailable".
Assisted-by: Claude:claude-opus-5-5
---
profiles/audio/a2dp.c | 132 +++++++++++++++++++++++++++--------------
profiles/audio/media.c | 20 ++++++-
2 files changed, 106 insertions(+), 46 deletions(-)
diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
index 08a9e1c96e4f..37f6d4ae12ac 100644
--- a/profiles/audio/a2dp.c
+++ b/profiles/audio/a2dp.c
@@ -507,41 +507,6 @@ static void finalize_discover(struct a2dp_setup *s)
}
}
-static gboolean finalize_all(gpointer data)
-{
- struct a2dp_setup *s = data;
- struct avdtp_stream *stream = s->err ? NULL : s->stream;
- GSList *l;
-
- for (l = s->cb; l != NULL; ) {
- struct a2dp_setup_cb *cb = l->data;
-
- l = l->next;
-
- if (cb->discover_cb) {
- cb->discover_cb(s->session, s->seps,
- error_to_errno(s->err), cb->user_data);
- } else if (cb->select_cb) {
- cb->select_cb(s->session, s->sep, s->caps,
- error_to_errno(s->err), cb->user_data);
- } else if (cb->suspend_cb) {
- cb->suspend_cb(s->session,
- error_to_errno(s->err), cb->user_data);
- } else if (cb->resume_cb) {
- cb->resume_cb(s->session,
- error_to_errno(s->err), cb->user_data);
- } else if (cb->config_cb) {
- cb->config_cb(s->session, s->sep, stream,
- error_to_errno(s->err), cb->user_data);
- } else
- warn("setup_cb doesn't have any callback function");
-
- setup_cb_free(cb);
- }
-
- return FALSE;
-}
-
static struct a2dp_setup *find_setup_by_session(struct avdtp *session)
{
GSList *l;
@@ -711,6 +676,86 @@ static void stream_state_changed(struct avdtp_stream *stream,
sep->endpoint->clear_configuration(sep, dev, sep->user_data);
}
+static void setup_setconf_reply(struct a2dp_setup *setup,
+ struct avdtp_error *err)
+{
+ avdtp_set_configuration_cb cb = setup->setconf_cb;
+
+ if (!cb)
+ return;
+
+ setup->setconf_cb = NULL;
+
+ /* Rejecting the configuration frees the avdtp_stream */
+ if (err)
+ a2dp_stream_destroy(setup->sep, setup->stream);
+
+ cb(setup->session, setup->stream, err);
+
+ if (err)
+ setup->stream = NULL;
+}
+
+/* Reject a pending Set Configuration while setup->session is still valid */
+static void setup_abort_setconf(struct a2dp_setup *setup)
+{
+ struct a2dp_sep *sep = setup->sep;
+ struct avdtp_error err;
+
+ if (!setup->setconf_cb)
+ return;
+
+ /* Clearing the endpoint configuration cancels its pending request
+ * which rejects the configuration via auto_config().
+ */
+ if (sep->endpoint && sep->endpoint->clear_configuration)
+ sep->endpoint->clear_configuration(sep,
+ avdtp_get_device(setup->session),
+ sep->user_data);
+
+ /* Reject it if it was not pending on the endpoint */
+ avdtp_error_init(&err, AVDTP_MEDIA_CODEC,
+ AVDTP_UNSUPPORTED_CONFIGURATION);
+ setup_setconf_reply(setup, &err);
+}
+
+static gboolean finalize_all(gpointer data)
+{
+ struct a2dp_setup *s = data;
+ struct avdtp_stream *stream = s->err ? NULL : s->stream;
+ GSList *l;
+
+ for (l = s->cb; l != NULL; ) {
+ struct a2dp_setup_cb *cb = l->data;
+
+ l = l->next;
+
+ if (cb->discover_cb) {
+ cb->discover_cb(s->session, s->seps,
+ error_to_errno(s->err), cb->user_data);
+ } else if (cb->select_cb) {
+ cb->select_cb(s->session, s->sep, s->caps,
+ error_to_errno(s->err), cb->user_data);
+ } else if (cb->suspend_cb) {
+ cb->suspend_cb(s->session,
+ error_to_errno(s->err), cb->user_data);
+ } else if (cb->resume_cb) {
+ cb->resume_cb(s->session,
+ error_to_errno(s->err), cb->user_data);
+ } else if (cb->config_cb) {
+ cb->config_cb(s->session, s->sep, stream,
+ error_to_errno(s->err), cb->user_data);
+ } else
+ warn("setup_cb doesn't have any callback function");
+
+ setup_cb_free(cb);
+ }
+
+ setup_abort_setconf(s);
+
+ return FALSE;
+}
+
static gboolean auto_config(gpointer data)
{
struct a2dp_setup *setup = data;
@@ -718,6 +763,12 @@ static gboolean auto_config(gpointer data)
struct btd_service *service;
struct a2dp_stream *stream;
+ /* Check if the channel has been disconnected, in which case
+ * channel_free() has already rejected the configuration.
+ */
+ if (!setup->session)
+ goto done;
+
dev = avdtp_get_device(setup->session);
if (setup->sep->type == AVDTP_SEP_TYPE_SOURCE)
@@ -748,16 +799,7 @@ static gboolean auto_config(gpointer data)
}
done:
- if (setup->setconf_cb) {
- /* Rejecting the configuration frees the avdtp_stream */
- if (setup->err)
- a2dp_stream_destroy(setup->sep, setup->stream);
-
- setup->setconf_cb(setup->session, setup->stream, setup->err);
-
- if (setup->err)
- setup->stream = NULL;
- }
+ setup_setconf_reply(setup, setup->err);
finalize_config(setup);
diff --git a/profiles/audio/media.c b/profiles/audio/media.c
index e8418280e60b..34ff251c7ef2 100644
--- a/profiles/audio/media.c
+++ b/profiles/audio/media.c
@@ -359,12 +359,26 @@ static struct media_adapter *find_adapter(struct btd_device *device)
return NULL;
}
+static int request_transport_cmp(gconstpointer data, gconstpointer user_data)
+{
+ const struct endpoint_request *request = data;
+
+ return request->transport == user_data ? 0 : -1;
+}
+
static void endpoint_remove_transport(struct media_endpoint *endpoint,
struct media_transport *transport)
{
+ GSList *l;
+
if (!endpoint || !transport)
return;
+ /* Cancel pending requests for the transport */
+ while ((l = g_slist_find_custom(endpoint->requests, transport,
+ request_transport_cmp)))
+ media_endpoint_cancel(l->data);
+
endpoint->transports = g_slist_remove(endpoint->transports, transport);
media_transport_destroy(transport);
}
@@ -431,7 +445,11 @@ static void endpoint_reply(DBusPendingCall *call, void *user_data)
if (dbus_message_is_method_call(request->msg,
MEDIA_ENDPOINT_INTERFACE,
"SetConfiguration")) {
- endpoint_remove_transport(endpoint, request->transport);
+ struct media_transport *transport = request->transport;
+
+ /* Detach so the request is not canceled */
+ request->transport = NULL;
+ endpoint_remove_transport(endpoint, transport);
error_code = a2dp_parse_config_error(err.name);
ret = &error_code;
size = 1;
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH BlueZ v2 3/3] test: Cover A2DP disconnection during SetConfiguration
2026-10-07 19:52 [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 2/3] a2dp: Fix crash on NULL session in auto_config Luiz Augusto von Dentz
@ 2026-10-07 19:52 ` Luiz Augusto von Dentz
2026-10-08 0:07 ` [BlueZ,v2,1/3] a2dp: Fix UAF after rejected SetConfiguration bluez.test.bot
2026-10-08 15:00 ` [PATCH BlueZ v2 1/3] " patchwork-bot+bluetooth
3 siblings, 0 replies; 5+ messages in thread
From: Luiz Augusto von Dentz @ 2026-10-07 19:52 UTC (permalink / raw)
To: linux-bluetooth
From: Luiz Augusto von Dentz <luiz.von.dentz@intel.com>
Add test_a2dp_disconnect_during_setconf, where the source disconnects
while the sink endpoint has not replied to SetConfiguration yet, and
the late reply, either accepting or rejecting, must be ignored and the
stream configured again on reconnection.
Assisted-by: Claude:claude-opus-5-5
---
doc/functional-a2dp.rst | 37 +++++++++++++++++++++++
test/functional/test_a2dp.py | 58 ++++++++++++++++++++++++++++++++++++
2 files changed, 95 insertions(+)
diff --git a/doc/functional-a2dp.rst b/doc/functional-a2dp.rst
index 03b18a7201c2..f38a458bb7c2 100644
--- a/doc/functional-a2dp.rst
+++ b/doc/functional-a2dp.rst
@@ -97,3 +97,40 @@ test_a2dp_transport_acquire
:Notes: Acquiring on the source side starts the stream, so the
peripheral does not have to acquire its own transport.
+
+test_a2dp_disconnect_during_setconf[accept|reject]
+--------------------------------------------------
+
+:Setup: As above, except the peripheral registers its A2DP Sink
+ endpoint manually with ``Auto Accept: no``, so that `bluetoothd`
+ is left waiting for the reply to
+ ``org.bluez.MediaEndpoint1.SetConfiguration``.
+
+:Steps:
+ 1. Pair and trust as above.
+ 2. Central: ``connect <peripheral bdaddr>``.
+ 3. Peripheral: wait for the ``Accept (yes/no):`` prompt, without
+ answering it.
+ 4. Central: ``disconnect <peripheral bdaddr>``.
+ 5. Peripheral: answer the pending prompt with ``yes``
+ (``accept``) or ``no`` (``reject``).
+ 6. Central: ``connect <peripheral bdaddr>`` again, and the
+ peripheral answers ``yes`` to the new prompt.
+
+:Expected:
+ 1. ``Pairing successful`` and ``trust succeeded`` on both hosts.
+ 2. The peripheral receives ``Endpoint: SetConfiguration``.
+ 3. The prompt is shown.
+ 4. ``Disconnection successful`` on the central, and the transport
+ created on the peripheral for the pending configuration is
+ removed (``[DEL] Transport``).
+ 5. The late reply is ignored.
+ 6. ``Connection successful``, and a transport appears on both
+ hosts.
+
+:Notes: Regression test for a crash of `bluetoothd` on the peripheral:
+ the setup of the pending configuration outlived the AVDTP session
+ and the late reply dereferenced a NULL session in
+ ``auto_config()``. A late ``yes`` used to also leave the stale
+ transport registered, failing the next connection with
+ ``Resource temporarily unavailable``.
diff --git a/test/functional/test_a2dp.py b/test/functional/test_a2dp.py
index 1ab712cd0da1..12348ea05ae2 100644
--- a/test/functional/test_a2dp.py
+++ b/test/functional/test_a2dp.py
@@ -142,3 +142,61 @@ def test_a2dp_transport_acquire(a2dp_hosts):
source.expect(r"Acquire successful: fd \d+ MTU \d+:\d+")
source.expect(f"Transport {transport} State: active")
+
+
+def start_bluetoothctl_manual_sink(host):
+ """
+ Start bluetoothctl registering an A2DP Sink endpoint that does not
+ auto accept, so that SetConfiguration is left pending until the
+ Accept prompt is answered.
+ """
+ exe = find_exe("client", "bluetoothctl")
+ ctl = host.pexpect.spawn([exe])
+
+ ctl.send("power on\n")
+ ctl.expect("Changing power on succeeded")
+
+ ctl.send(f"endpoint.register {A2DP_SINK_UUID} 0x00\n")
+ ctl.expect(r"Auto Accept \(yes/no\):")
+ ctl.send("no\n")
+ ctl.expect(r"Max Transports \(auto/value\):")
+ ctl.send("a\n")
+ ctl.expect("Endpoint /local/endpoint/ep0 registered")
+ return ctl
+
+
+@pytest.mark.parametrize("reply", ["accept", "reject"])
+@a2dp_host_config
+def test_a2dp_disconnect_during_setconf(hosts, reply):
+ host0, host1 = hosts
+
+ source = start_bluetoothctl(host0, "a2dp-source-sbc.bt")
+ sink = start_bluetoothctl_manual_sink(host1)
+
+ pair(host0, source, host1, sink)
+
+ # Leave SetConfiguration pending on the sink
+ source.send(f"connect {host1.bdaddr}\n")
+ sink.expect("Endpoint: SetConfiguration")
+ _, m = sink.expect(TRANSPORT_RE)
+ transport = m[0].decode("utf-8")
+ sink.expect(r"Accept \(yes/no\):")
+
+ source.send(f"disconnect {host1.bdaddr}\n")
+ source.expect("Disconnection successful")
+
+ # The pending configuration is cleared on disconnection
+ # [DEL] is colored, so match around the escape sequences
+ sink.expect(rf"DEL\S*\] Transport {transport}")
+
+ # Late reply must be ignored
+ sink.send("yes\n" if reply == "accept" else "no\n")
+
+ # bluetoothd is still alive and the stream can be configured again
+ source.send(f"connect {host1.bdaddr}\n")
+ sink.expect("Endpoint: SetConfiguration")
+ sink.expect(TRANSPORT_RE)
+ sink.expect(r"Accept \(yes/no\):")
+ sink.send("yes\n")
+ source.expect("Connection successful")
+ source.expect(TRANSPORT_RE)
--
2.55.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* RE: [BlueZ,v2,1/3] a2dp: Fix UAF after rejected SetConfiguration
2026-10-07 19:52 [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 2/3] a2dp: Fix crash on NULL session in auto_config Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 3/3] test: Cover A2DP disconnection during SetConfiguration Luiz Augusto von Dentz
@ 2026-10-08 0:07 ` bluez.test.bot
2026-10-08 15:00 ` [PATCH BlueZ v2 1/3] " patchwork-bot+bluetooth
3 siblings, 0 replies; 5+ messages in thread
From: bluez.test.bot @ 2026-10-08 0:07 UTC (permalink / raw)
To: linux-bluetooth, luiz.dentz
[-- Attachment #1: Type: text/plain, Size: 1019 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/series/1181105/
---Test result---
Test Summary:
CheckPatch PASS 1.62 seconds
GitLint PASS 0.69 seconds
BuildEll PASS 13.67 seconds
BluezMake PASS 213.68 seconds
MakeCheck PASS 13.06 seconds
MakeDistcheck PASS 94.41 seconds
CheckValgrind PASS 142.41 seconds
CheckSmatch PASS 180.59 seconds
bluezmakeextell PASS 66.42 seconds
TestFunctional PASS 813.30 seconds
IncrementalBuild PASS 220.31 seconds
ScanBuild PASS 594.43 seconds
https://github.com/bluez/bluez/pull/2633
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration
2026-10-07 19:52 [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration Luiz Augusto von Dentz
` (2 preceding siblings ...)
2026-10-08 0:07 ` [BlueZ,v2,1/3] a2dp: Fix UAF after rejected SetConfiguration bluez.test.bot
@ 2026-10-08 15:00 ` patchwork-bot+bluetooth
3 siblings, 0 replies; 5+ messages in thread
From: patchwork-bot+bluetooth @ 2026-10-08 15:00 UTC (permalink / raw)
To: Luiz Augusto von Dentz; +Cc: linux-bluetooth
Hello:
This series was applied to bluetooth/bluez.git (master)
by Luiz Augusto von Dentz <luiz.von.dentz@intel.com>:
On Wed, 7 Oct 2026 15:52:04 -0400 you wrote:
> From: Eduardo Alves <eduardoalves8006@gmail.com>
>
> When the MediaEndpoint1 rejects SetConfiguration, auto_config() calls
> setconf_cb() with an error and avdtp frees the avdtp_stream, but the
> a2dp_stream wrapping it is left on sep->streams with a dangling stream
> pointer. The next stream configured on the same session finds it in
> a2dp_config() and reads the freed avdtp_stream:
>
> [...]
Here is the summary with links:
- [BlueZ,v2,1/3] a2dp: Fix UAF after rejected SetConfiguration
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=ef54133c69e9
- [BlueZ,v2,2/3] a2dp: Fix crash on NULL session in auto_config
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=79aedd33f8fa
- [BlueZ,v2,3/3] test: Cover A2DP disconnection during SetConfiguration
https://git.kernel.org/pub/scm/bluetooth/bluez.git/?id=ab5df7ba3019
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-08 15:00 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-07 19:52 [PATCH BlueZ v2 1/3] a2dp: Fix UAF after rejected SetConfiguration Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 2/3] a2dp: Fix crash on NULL session in auto_config Luiz Augusto von Dentz
2026-10-07 19:52 ` [PATCH BlueZ v2 3/3] test: Cover A2DP disconnection during SetConfiguration Luiz Augusto von Dentz
2026-10-08 0:07 ` [BlueZ,v2,1/3] a2dp: Fix UAF after rejected SetConfiguration bluez.test.bot
2026-10-08 15:00 ` [PATCH BlueZ v2 1/3] " patchwork-bot+bluetooth
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.