* Use-after-free in avdtp_connect_cb() (profiles/audio/avdtp.c) via unreferenced transport connect callback
@ 2026-08-25 11:36 BUGPWN
2026-08-26 13:21 ` Bastien Nocera
0 siblings, 1 reply; 2+ messages in thread
From: BUGPWN @ 2026-08-25 11:36 UTC (permalink / raw)
To: security; +Cc: linux-bluetooth
Summary
-------
profiles/audio/avdtp.c contains a use-after-free in
avdtp_connect_cb(), reachable when
an AVDTP session is torn down (e.g. the remote peer closes the
signaling channel) while
an asynchronous transport channel connect, started by l2cap_connect(),
is still in
flight. The transport connect's callback is registered without a
reference on the
session object and without a destroy notifier, so it can fire after
the session has
already been freed.
Also submitted via GitHub's private vulnerability reporting.
Confirmed against current git HEAD: commit
efd216fe280ba5dd38d1b6084f64784bf1328731
(2026-07-16).
Vulnerable code
---------------
profiles/audio/avdtp.c:2516-2534, l2cap_connect():
io = bt_io_connect(avdtp_connect_cb, session, NULL, &err, ...);
/*
* no avdtp_ref(session) before this call, and no GDestroyNotify
* (third argument) either */
bt_io_connect() (btio/btio.c) attaches its own glib watch on the
transport L2CAP socket,
entirely independent of avdtp's own reference counting. If session's
refcount drops to
zero while this watch is still armed (avdtp_unref(),
avdtp.c:1215-1245, frees the
session once ref reaches 0), the next time the watch fires, avdtp_connect_cb()
(avdtp.c:2402) dereferences the already-freed session.
Reachability
------------
1. bluetoothd sends AVDTP_OPEN, the peer accepts it: l2cap_connect() starts an
asynchronous transport channel connect, with the callback above registered
unprotected.
2. The peer immediately closes the AVDTP signaling channel.
3. session_cb() (avdtp.c:2258) sees G_IO_HUP, calls connection_lost()
(avdtp.c:1152),
which can drop session's refcount to zero and free it via avdtp_free()
(avdtp.c:1121).
4. The transport connect's glib watch is untouched by any of this (nothing ever
referenced or cancelled it). When it later fires,
avdtp_connect_cb() runs on the
dangling session pointer.
No pairing beyond an already-established AVDTP session is required
(i.e. this happens
after a normal A2DP connection); the remote peer fully controls the
timing of the
signaling channel closure relative to the transport connect.
Proof of concept
-----------------
Reproducing this over real L2CAP sockets and glib timing would require a virtual
controller (vhci) and a fake peer (bthost/btdev) with precise
two-channel timing.
Instead we noted that the bug does not actually
depend on network timing itself, only on the logical order of two
events on the same
session object (its being freed, and the transport callback firing
late), which we can
force directly and deterministically using the real code:
1. We built a real struct avdtp by hand (ref=1, state=CONNECTING,
matching a session
with a transport connect in flight after AVDTP_OPEN). struct avdtp
is private to
avdtp.c, so we reached it (and the static
avdtp_connect_cb()/connection_lost()) by
#include-ing the real, unmodified file, the same technique used for
our bass.c PoC.
2. We called the real, public avdtp_unref() exactly once, the only
entry point a real
caller uses. Because ref drops to 0 while state is CONNECTING, this
one call drives
the entire real teardown chain inside unmodified code: avdtp_unref() ->
connection_lost() -> avdtp_set_state(DISCONNECTED) -> avdtp_unref()
-> avdtp_free().
We never called avdtp_free() or connection_lost() ourselves.
3. session is now genuinely freed heap memory. We then called the real
avdtp_connect_cb()
directly on it, simulating the transport socket's glib watch firing
late, exactly
what l2cap_connect()'s missing ref/destroy-notify fails to prevent
in the real code.
device.h/adapter.h/btio.h/sink.h/source.h/sdp_lib.h dependencies not
on this path were
provided by minimal stubs (none of them are exercised in this
scenario; they only need
to exist for linking).
Built with AddressSanitizer:
gcc -I. -Ilib $(pkg-config --cflags glib-2.0 dbus-1) \
-fsanitize=address -fno-omit-frame-pointer -g -O0 \
-o poc_p1_uaf poc_p1_uaf.c stubs.c \
src/shared/queue.c src/shared/util.c src/shared/timeout-glib.c \
lib/bluetooth/bluetooth.c $(pkg-config --libs glib-2.0)
Result:
==7610==ERROR: AddressSanitizer: heap-use-after-free on address ...
READ of size 8 at ...
#0 in avdtp_connect_cb profiles/audio/avdtp.c:2414
#1 in main poc_p1_uaf.c:98
freed by thread T0 here:
#0 in free ...
#1 in avdtp_free profiles/audio/avdtp.c:1149
#2 in avdtp_unref profiles/audio/avdtp.c:1242
#3 in connection_lost profiles/audio/avdtp.c:1168
#4 in avdtp_unref profiles/audio/avdtp.c:1238
#5 in main poc_p1_uaf.c:84
previously allocated by thread T0 here:
#0 in calloc ...
#1 in g_malloc0 ...
The "freed by" trace confirms, with the real lines from the
repository, exactly the
chain the audit predicted: a single avdtp_unref() call unwinds the whole real
avdtp_unref -> connection_lost -> avdtp_set_state -> avdtp_unref ->
avdtp_free path. The
crash itself confirms avdtp_connect_cb() dereferences session (the "if
(!session->io)"
check, avdtp.c:2414) after that.
We're happy to share the PoC in whatever format
is convenient.
Impact
------
A malicious or misbehaving AVDTP peer (a Bluetooth headset, speaker,
or any A2DP-capable
device bluetoothd connects to) that accepts a transport channel open
request and then
immediately closes the signaling channel can trigger a use-after-free
inside bluetoothd
(a root-owned daemon), with no user interaction beyond an
already-established A2DP
connection.
Reporter
--------
BUG|PWN & Sadoc ADONON
vuln.bugpwn@gmail.com
--
Vuln @ BUG|PWN Team
Email: vuln.bugpwn@gmail.com
Twitter
Linkedin
^ permalink raw reply [flat|nested] 2+ messages in thread* Re: Use-after-free in avdtp_connect_cb() (profiles/audio/avdtp.c) via unreferenced transport connect callback
2026-08-25 11:36 Use-after-free in avdtp_connect_cb() (profiles/audio/avdtp.c) via unreferenced transport connect callback BUGPWN
@ 2026-08-26 13:21 ` Bastien Nocera
0 siblings, 0 replies; 2+ messages in thread
From: Bastien Nocera @ 2026-08-26 13:21 UTC (permalink / raw)
To: BUGPWN, security; +Cc: linux-bluetooth
On Tue, 2026-08-25 at 12:36 +0100, BUGPWN wrote:
> Summary
> -------
>
> profiles/audio/avdtp.c contains a use-after-free in
> avdtp_connect_cb(), reachable when
> an AVDTP session is torn down (e.g. the remote peer closes the
> signaling channel) while
> an asynchronous transport channel connect, started by
> l2cap_connect(),
> is still in
> flight. The transport connect's callback is registered without a
> reference on the
> session object and without a destroy notifier, so it can fire after
> the session has
> already been freed.
>
> Also submitted via GitHub's private vulnerability reporting.
What's the link to that report?
>
<snip>
> Reproducing this over real L2CAP sockets and glib timing would
> require a virtual
> controller (vhci) and a fake peer (bthost/btdev) with precise
> two-channel timing.
Can this even be triggered without using vHCI?
Regards
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-08-26 13:21 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-25 11:36 Use-after-free in avdtp_connect_cb() (profiles/audio/avdtp.c) via unreferenced transport connect callback BUGPWN
2026-08-26 13:21 ` Bastien Nocera
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox