2026-07-31 - research - BlueZ
21 Bugs In The Linux Bluetooth Stack: Patch Watch, Part 1
BlueZ is the userspace Bluetooth stack behind Linux desktops, embedded devices, development boards, automotive systems, audio products, and products that inherit a distribution Bluetooth stack. Columbus-1 audited BlueZ 5.86 for us and found 21 distinct attacker-reachable defects. We reported all 21 upstream. This is the first entry in a patch-watch series: as fixes ship, we will publish the matching findings.
We are not publishing details for bugs users cannot yet patch. Each post covers findings with a fix in a released BlueZ tree: what broke, how it was reachable, what changed upstream, and what the fix does or does not cover. BlueZ 5.87 fixes or directly addresses two of the 21, so those are the two covered here.
Disclosure note: the findings were reported privately to the BlueZ security team and to the linux-distros coordination list more than 45 days before this post. We did not receive an acknowledgment. Fixes have started landing anyway, and this series tracks them. One of the two findings below, the A2DP cache overflow, had already been reported upstream by someone else; that section has the credit details.
Methodology
Columbus-1 started with attack-surface mapping, then moved suspicious paths into targeted sanitizer harnesses. We ranked BlueZ inputs by attacker reachability: passive discovery and advertising data, mesh provisioning, connected GATT and LE-Audio control points, paired media-profile responses, SDP, local cache files, OBEX file handling, and root-run tooling. Candidate bugs were rebuilt into focused harnesses and run under AddressSanitizer or hardened builds until the fault was confirmed.
Every reported finding was rebuilt and re-triggered before it was written up. For most findings, the harness linked the real BlueZ C files unmodified and crashed the actual function. For a few modules with heavy daemon dependencies, the vulnerable function body was copied out verbatim and checked against the source. None of the 21 are static-analysis guesses.
Patch watch scoreboard
| Status | Count | Notes |
|---|---|---|
| Confirmed attacker-reachable findings reported | 21 | all re-triggered under ASan, hardened builds, or source-checked harnesses |
| Fixed or addressed as of BlueZ 5.87 | 2 | detailed in this post |
| Awaiting an upstream fix | 19 | details withheld; each gets a follow-up post when its fix ships |
Fix 1: A2DP cached-endpoint overflow
The first fixed finding is a stack overflow in the A2DP endpoint cache reader, profiles/audio/a2dp.c. BlueZ stores negotiated audio endpoint capabilities per remote device and reloads them on startup. The loader read a cached capability string with an unbounded %s conversion into a 256-byte caps buffer, then decoded pairs of hex characters from it into a 128-byte data buffer. It never proved that the string was even-length or small enough to fit.
That gives two overflows from one untrusted string. The sscanf() call could overflow caps. Even if caps fit, the decoded bytes could overflow data. Both destinations are stack buffers in a daemon that parses this file at startup. Reachability is local: anything that can write to the Bluetooth storage tree can feed this parser directly. Treating that on-disk state as trusted was the mistake. We confirmed the overflow with a faithful reproduction of the loader under ASan, which reported a stack-buffer-overflow write in the decode loop.
The 5.87-era fix landed in two steps. 912f5efb0dd9 widened the serialized cache buffer and capped the sscanf() fields at 512 characters. Its decode-loop guard (i < size && i >= 2) never let the loop run, which broke cache loading without adding the right bound for data. b7d71e506785 (Mikhail Gavrilov, 2026-07-10) replaced that with an up-front length check. The combined result:
diff --git a/profiles/audio/a2dp.c b/profiles/audio/a2dp.c
@@
- char seid[4], value[256];
+ char seid[4], value[9 + 512];
@@
- char caps[256];
+ char caps[513];
@@
- if (sscanf(value, "%02hhx:%02hhx:%02hhx:%s",
+ if (sscanf(value, "%02hhx:%02hhx:%02hhx:%512s",
&type, &codec, &delay_reporting, caps) != 4) {
- if (sscanf(value, "%02hhx:%02hhx:%s",
+ if (sscanf(value, "%02hhx:%02hhx:%512s",
&type, &codec, caps) != 3) {
...
}
}
- for (i = 0, size = strlen(caps); i < size; i += 2) {
+ size = strlen(caps);
+
+ if (size % 2 || size / 2 > (int) sizeof(data)) {
+ warn("Unable to load Endpoint: seid %u", rseid);
+ continue;
+ }
+
+ for (i = 0; i < size; i += 2) {
uint8_t *tmp = data + i / 2;
...
}
This is the right fix shape: the string parser is width-limited, and the later binary decode is bounded against its own destination. The second check matters most. A width limit alone would only move the overflow from caps to data: a 512-character string still decodes to 256 bytes, twice what data holds. When a parse happens in stages, each stage needs its own bound.
Fix 2: ASCS control-point invalid-length handling
The second change is in the LE-Audio path. BlueZ 5.87 changed ASCS ASE Control Point handling in src/shared/bap.c so invalid-length writes are answered through an ASE Control Point notification instead of an ATT-level error. This closes the truncated-control-point case we reported: an undersized write reached handler code before the length was properly accounted for.
diff --git a/src/shared/bap.c b/src/shared/bap.c
@@
if (!len) {
DBG(bap, "invalid len %u < %u sizeof(*hdr)", len,
sizeof(*hdr));
- gatt_db_attribute_write_result(attrib, id,
- BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN);
- return;
+ rsp = ascs_ase_cp_rsp_new(len > 0 ? value[0] : 0x00);
+ ret = BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN;
+ goto respond;
}
@@
-if (ret == BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN)
+if (ret == BT_ATT_ERROR_INVALID_ATTRIBUTE_VALUE_LEN) {
ascs_ase_rsp_add_errno(rsp, 0x00, -ENOMSG);
+ ret = 0;
+}
gatt_db_attribute_notify(attrib, rsp->iov_base, rsp->iov_len, att);
gatt_db_attribute_write_result(attrib, id, ret);
The fix changes the failure path from "reject the ATT write" to "accept the write and report the malformed ASE operation in the control-point response." That is better protocol behavior, and it removes the undersized-write crash pattern we reported. The malformed operation now ends in a response builder instead of reaching a handler working from an unchecked length.
The general rule is simple: a fixed-header length check is not a body length check. This change handles writes too short to contain the operation header. Parsers that pull variable-length fields after that header still need to validate each pull at the point of use.
Fix patterns worth generalizing
- When a parse happens in stages, bound every stage against its own destination. A width limit on a text field does not bound the binary buffer that field decodes into.
- Treat on-disk Bluetooth cache and storage files as untrusted input. Width-limit the parsing and fail closed on oversized or malformed fields instead of continuing with a partially parsed record.
- Validate variable-length protocol bodies as they are consumed, not only the fixed header at dispatch time.
- Keep sanitizer harnesses as regression tests for the parser entry points that have been fixed.
What happens next in this series
Nineteen reported findings do not yet have a fix in a released tree. Their details, including component, root cause, reachability, and sanitizer evidence, stay unpublished until that changes. When a fix lands upstream or in a distro backport, that finding gets its own post with the same structure as the two above.
If you maintain a product that ships BlueZ, the practical advice does not depend on knowing which specific bugs remain outstanding: track upstream releases instead of pinning an old tree, build the daemon with normal hardening (stack protector, FORTIFY, PIE, and a hardened allocator), and disable components you do not need. A Bluetooth stack is a set of parsers and state machines with different trust boundaries: passive radio input, mesh provisioning, connected GATT discovery, paired media metadata, local cache files, and root-run diagnostics. Every unused surface you can switch off is one less surface to patch.
Conclusion
BlueZ 5.86 had more than one serious Bluetooth bug. The 21 reported findings point to a parser-hardening problem across components with very different trust assumptions. Two are now fixed upstream, and both fixes have the right shape. The scoreboard is 2 of 21; the next entry publishes whichever finding is fixed next.