back

21 Bugs In The Linux Bluetooth Stack: Patch Watch, Part 2

Bluetooth attack paths targeting Linux systems.
BlueZ exposes radio-facing parsers, stateful GATT control points, local cache readers, and root-run tooling.

Five more of the 21 reported BlueZ findings now have upstream fixes. Two of them are the most severe bugs in the set: zero-click stack overflows in the device-name parser, reachable from any Bluetooth radio in range with no pairing and no user interaction. The other three are out-of-bounds reads in AVRCP media-browsing response handlers, reachable from a paired audio/video peer. All five fixes have landed in upstream commits since BlueZ 5.87, though none are in a tagged release yet.

This post follows the same structure as Part 1: what broke, how it was reachable, what changed upstream, and what the fix does or doesn't cover. If you haven't read Part 1, it covers the A2DP cache overflow and the ASCS control-point length fix that shipped in 5.87.

The EIR name overflow covered here now has a public CVE: CVE-2026-80186 (GHSA-68h6-5qgp-3975, CVSS 7.2 High). The advisory credits @sprabhav7 as the reporter. We reported the same bug to the BlueZ security list on July 6, 2026 — Columbus-1 flagged name2utf8() during the 5.86 engagement. Kudos to @sprabhav7 for finding it independently; two teams converging on the same bug says more about how long it sat in the open than about who filed first.

Patch watch scoreboard

StatusCountNotes
Confirmed attacker-reachable findings reported21all re-triggered under ASan, hardened builds, or source-checked harnesses
Fixed or addressed as of BlueZ 5.872detailed in Part 1
Fixed in upstream HEAD since 5.875detailed in this post
Total fixed7
Awaiting an upstream fix14details withheld; each gets a follow-up post when its fix ships

EIR device-name stack overflow

name2utf8() in src/eir.c copies an advertised Bluetooth device name into a 250-byte stack buffer (utf8_name[HCI_MAX_NAME_LENGTH + 2]) using strncpy(utf8_name, name, len), where len comes straight from the EIR field length byte. An EIR Complete Local Name field with field_len = 254 yields data_len = 253, and strncpy writes 253 bytes into the 250-byte buffer. Three bytes past the end.

The strncpy is only the first problem. Because the buffer isn't null-terminated after the overwrite, strtoutf8(), g_strstrip(), and g_strdup() all read past the buffer too. One attacker-controlled field gives a write overflow followed by a chain of read overflows.

This is the textbook zero-click Bluetooth bug. The EIR name is parsed during device discovery from Extended Inquiry Response and LE advertising reports. Any device in radio range can send a crafted name. No pairing, no connection, no user interaction. The parser runs in the privileged bluetoothd daemon.

Under ASan, our harness reported a stack-buffer-overflow write at eir.c:141 with both a 251-byte and 253-byte overrun past the buffer. The larger variant also trips a FORTIFY abort on hardened builds.

The fix landed in two commits from Luiz Augusto von Dentz, a day apart. 381b5d0d2089 (August 19) added a one-line clamp before the strncpy:

diff --git a/src/eir.c b/src/eir.c
@@ -137,6 +137,8 @@ static char *name2utf8(const uint8_t *name, uint8_t len)
 {
     char utf8_name[HCI_MAX_NAME_LENGTH + 2];

+    len = MIN(len, HCI_MAX_NAME_LENGTH);
+
     memset(utf8_name, 0, sizeof(utf8_name));
     strncpy(utf8_name, (char *) name, len);

That stops the immediate overflow. But name2utf8() existed in five separate files: src/eir.c, src/shared/ad.c, monitor/att.c, profiles/audio/mcp.c, and profiles/gap/gas.c. Each copy had drifted. One replaced every non-ASCII byte with a space as soon as a single bad byte appeared, mangling the valid prefix of the name. The others truncated at the first ill-formed sequence and threw away everything after it. None of them agreed.

8c81ab108b09 (August 20) removed all five copies and replaced them with a shared str2utf8() in src/shared/util.c. The new helper heap-allocates len * 3 + 1 bytes (worst case: every byte replaced by the 3-byte U+FFFD sequence), so no stack buffer exists to overflow. The EIR name path becomes eir->name = str2utf8(data, data_len). The commit drops around 120 lines and gives every caller the same behavior; ill-formed sequences are replaced individually with U+FFFD instead of truncating or mangling the rest of the name. Unit tests for the new helper landed in 74c56dff2 and f0e40c5b3.

The clamp alone would have been enough for this call site. The refactor is the fix that matters, because it eliminates the class. Five independent copies of the same function, each quietly wrong in its own way, is exactly the kind of state that produces a CVE every time someone touches the code or a new caller appears.

Advertising-data name stack overflow

ad_replace_name() in src/shared/ad.c is the sibling. The strncpy here was already clamped to MIN(iov->iov_len, HCI_MAX_NAME_LENGTH), so the copy itself was bounded. But strisutf8() and strtoutf8() received the unclamped iov->iov_len. With a 253-byte advertising name, the copy writes 248 bytes into the 250-byte buffer (safe), but the UTF-8 validator reads 253 bytes from it: three bytes past the end.

Same reachability as the EIR name overflow. Advertising data is parsed during passive discovery, before any connection.

784203160e2f (August 20) introduced a single len variable and used it for all three operations:

diff --git a/src/shared/ad.c b/src/shared/ad.c
@@ -276,15 +276,15 @@
 static bool ad_replace_name(struct bt_ad *ad, struct iovec *iov)
 {
     char utf8_name[HCI_MAX_NAME_LENGTH + 2];
+    size_t len = MIN(iov->iov_len, (size_t) HCI_MAX_NAME_LENGTH);

     memset(utf8_name, 0, sizeof(utf8_name));
-    strncpy(utf8_name, (const char *)iov->iov_base,
-            MIN(iov->iov_len, HCI_MAX_NAME_LENGTH));
+    strncpy(utf8_name, (const char *)iov->iov_base, len);

-    if (strisutf8(utf8_name, iov->iov_len))
+    if (strisutf8(utf8_name, len))
         goto done;

-    strtoutf8(utf8_name, iov->iov_len);
+    strtoutf8(utf8_name, len);

Then 8c81ab108b09 (the same str2utf8 refactor from the EIR fix) replaced the entire function body with the shared helper, eliminating the stack buffer.

The pattern here is worth isolating: the copy was bounded, but the validation was not. A partial fix that clamps the write but passes the original length to the functions that follow is a recurring mistake in this codebase. The length used for the copy and the length used for everything else have to be the same variable, or something will read past whatever the copy put down.

AVRCP browsing-response OOB reads

Three out-of-bounds reads in AVRCP response parsing, grouped here because they share the same root cause and the same architectural fix. All three are in profiles/audio/avrcp.c; all three involve raw pointer arithmetic on attacker-controlled AVRCP browsing-channel responses without checking that enough bytes remain.

avrcp_parse_attribute_list() (around line 2465 in 5.86). This function iterates attribute TLV entries in a GetElementAttributes response. Each iteration reads a 32-bit attribute ID, a 16-bit charset, and a 16-bit value length at fixed offsets from a moving pointer, then copies len bytes of value data. None of these reads checked whether enough bytes remained in the PDU. An attacker-controlled count field drives the loop; a response that declares more attributes than it actually contains sends the reads past the end of the buffer.

parse_media_element() and parse_media_folder() (around lines 2596 and 2640). Both read a 16-bit namesize from the PDU, then memcpy that many bytes into a local buffer without verifying that namesize fits within the remaining data. A response can claim a name of 65,535 bytes and the memcpy will read that far past the receive buffer.

avrcp_get_media_player_list_rsp() (around line 3740). A loop reads operands[i] and then get_be16(&operands[i]) without checking that i + 2 <= operand_count. When the PDU is one byte short, get_be16 reads one byte past the buffer.

All three require a paired A/V connection. The remote peer sends a crafted AVRCP browsing-channel response (GetElementAttributes, GetFolderItems with media elements or folders, or the media player list). The local device is acting as AVRCP controller, which is the typical role for a laptop or phone browsing a media player.

The fix came in three stages. bd8989620ed6 (August 14, Bastien Nocera) extracted a parse_media_name() helper that clamps namesize to both the remaining PDU length and NAME_MAX_LEN - 1 before copying. This closed the immediate GetFolderItems overflow.

f0ddbc7aa (September 1, Luiz Augusto von Dentz) rewrote avrcp_parse_attribute_list() and its callers to pass a struct iovec instead of a raw pointer, using the util_iov_pull_* helpers for every field read. It also added params_len validation against the number of bytes actually received, which had never been checked for response callbacks (they don't go through handle_vendordep_pdu()).

52728deef506 (September 1, Luiz Augusto von Dentz) moved all AVRCP response parsing into a new profiles/audio/avrcp-parse.c. Every field read now goes through util_iov_pull_u8(), util_iov_pull_be16(), util_iov_pull_be32(), util_iov_pull_be64(), or util_iov_pull_mem(). Each of these checks iov->iov_len before reading and returns false or NULL on insufficient data. The parsers early-return on any short read. The contrast with the old code is stark:

// Before (5.86) — no bounds checking
 for (i = 0; count > 0; count--) {
     uint32_t id = get_be32(&operands[i]);       // unchecked
     i += 4;
     uint16_t charset = get_be16(&operands[i]);  // unchecked
     i += 2;
     uint16_t len = get_be16(&operands[i]);      // unchecked
     i += 2;
     // use operands[i..i+len] without checking
     i += len;
 }

// After (upstream HEAD) — every pull is bounded
+for (; count > 0; count--) {
+    struct avrcp_attribute attr;
+    if (!util_iov_pull_be32(iov, &attr.id) ||
+            !util_iov_pull_be16(iov, &attr.charset) ||
+            !util_iov_pull_be16(iov, &attr.len))
+        return;
+    attr.value = util_iov_pull_mem(iov, attr.len);
+    if (!attr.value)
+        return;
+    func(&attr, user_data);
+}

The commit message for the refactor states the motivation directly: the old parsing code was interleaved with media_player and D-Bus glue, which made it impossible to reach from unit tests. None of the three preceding security fixes could be covered by a regression test. The new avrcp-parse.c depends on nothing but util_iov and log.h, so the parsers are testable in isolation. Unit tests for the new code landed in c1e0079c9.

Fix patterns worth generalizing

What happens next in this series

Fourteen reported findings don't yet have an upstream fix. Their details stay unpublished until that changes. When a fix lands, that finding gets its own post with the same structure as the ones above.

The two most severe bugs in the set are now patched. The zero-click, pre-pairing EIR and advertising-data name overflows are fixed both by the intermediate clamps and by the architectural str2utf8 refactor that eliminates stack-buffer name parsing entirely. If you're running a BlueZ-based product and can only cherry-pick one commit, 8c81ab108b09 is the one: it removes the vulnerable pattern from all five files at once.

The remaining unfixed bugs span mesh provisioning, SDP, GATT discovery, and the broadcast audio scan service. As before: track upstream, build with normal hardening (stack protector, FORTIFY, PIE, hardened allocator), and disable components you don't need.

Conclusion

The scoreboard is 7 of 21. The fixes covered here are the right kind: the name-parser refactor killed five independent copies of a broken function and replaced them with a tested, heap-allocating helper; the AVRCP refactor replaced unchecked pointer arithmetic with bounded pulls and moved the parsers into a file that can actually be tested. Both fixes addressed the class, not just the instance. Fourteen findings remain open; the next entry publishes whichever one is fixed next.