From: Philipp Dunkel <pip@pipobscure.com>
To: linux-bluetooth@vger.kernel.org
Cc: Philipp Dunkel <pip@pipobscure.com>
Subject: [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation
Date: Sat, 18 Jul 2026 21:42:29 +0200 [thread overview]
Message-ID: <20260718194230.35959-2-pip@pipobscure.com> (raw)
In-Reply-To: <20260718194230.35959-1-pip@pipobscure.com>
When bluetoothd connects to a freshly discovered LE peer it probes it as
a GATT client through its built-in profiles (battery, mcp, ...). If one
of those reads a characteristic the peer has protected, the peer answers
with Insufficient Authentication (0x05). change_security() reacts to that
error by raising BT_SECURITY on the ATT socket, and on an unbonded LE
link raising security starts SMP pairing.
The result is a bond that nobody asked for: no user, no agent, and no
application requested it. It logs as a device_bonding_complete() bond
with a (nil) requestor, and the user is shown a pairing prompt for a
device that merely came into range and connected. On phones and laptops
that expose authentication-gated GATT characteristics this fires on
essentially every connection.
The existing opt-out cannot prevent it. change_security() only returns
early when chan->sec_level != BT_ATT_SECURITY_AUTO, but
bt_att_chan_set_security() records sec_level solely for BT_ATT_LOCAL
channels. An L2CAP/LE channel keeps its zero-initialised value (AUTO)
for its entire lifetime, so the guard never triggers and the link
always elevates.
Add bt_att_set_no_auto_sec(), a per-bt_att flag that makes
change_security() refuse to elevate. Set it from gatt_client_init() for
any connection that is neither bonded nor in a requested bonding, so a
speculative profile probe that hits an authentication-protected
characteristic fails on the auth error instead of silently pairing.
Devices being bonded via Pair() are unaffected: device->bonding is set
before the connection is established, so gatt_client_init() takes the
existing elevate-for-bonding branch and never sets the flag. Already
bonded devices are unaffected too: they are elevated proactively at
attach time when their LTK is available, and reconnections see
device_is_bonded() and skip the flag.
Tested against Android (Galaxy Z Flip 4), iOS (iPhone) and macOS
(MacBook): each connects and is probed by the built-in profiles with no
unsolicited pairing prompt, while explicit pairing continues to work.
Assisted-by: Claude:Opus-4.8
AI disclosure: this change was developed with the assistance of an AI
model (Claude Opus 4.8). The author reviewed every line. The fix was
built and exercised on real hardware -- against Android (Galaxy Z Flip
4), iOS (iPhone) and macOS (MacBook) peers -- and its behaviour was
observed directly, not inferred.
---
src/device.c | 13 +++++++++++++
src/shared/att.c | 20 ++++++++++++++++++++
src/shared/att.h | 1 +
3 files changed, 34 insertions(+)
diff --git a/src/device.c b/src/device.c
index 65d84be..821a834 100644
--- a/src/device.c
+++ b/src/device.c
@@ -6301,6 +6301,7 @@ static void gatt_debug(const char *str, void *user_data)
static void gatt_client_init(struct btd_device *device)
{
uint8_t features;
+ bool unbonded;
gatt_client_cleanup(device);
@@ -6319,9 +6320,21 @@ static void gatt_client_init(struct btd_device *device)
if (btd_opts.gatt_channels > 1)
features |= BT_GATT_CHRC_CLI_FEAT_EATT;
+ unbonded = !device_is_bonded(device, device->bdaddr_type);
+
if (!btd_opts.gatt_seclevel && device->bonding) {
DBG("Elevating security level since bonding is in progress");
bt_att_set_security(device->att, BT_ATT_SECURITY_MEDIUM);
+ } else if (!btd_opts.gatt_seclevel && unbonded) {
+ /* No bond exists and none is being requested, so no
+ * user or agent has consented to pairing. Forbid a
+ * reactive security elevation: a speculative GATT
+ * profile probe (battery, MCP, ...) that reads an
+ * auth-protected characteristic must fail rather than
+ * silently initiating SMP bonding nobody asked for.
+ */
+ DBG("Unbonded link: no reactive GATT elevation");
+ bt_att_set_no_auto_sec(device->att, true);
}
device->client = bt_gatt_client_new(device->db, device->att,
diff --git a/src/shared/att.c b/src/shared/att.c
index 3d3c8cf..c42a521 100644
--- a/src/shared/att.c
+++ b/src/shared/att.c
@@ -92,6 +92,10 @@ struct bt_att {
struct sign_info *local_sign;
struct sign_info *remote_sign;
+
+ bool no_auto_sec; /* Never reactively elevate security
+ * (would initiate an unrequested bond)
+ */
};
struct sign_info {
@@ -766,6 +770,14 @@ static bool change_security(struct bt_att_chan *chan, uint8_t ecode)
{
int security;
+ /* A reactive elevation on an unbonded link starts SMP bonding.
+ * When the owner has forbidden auto-elevation (no_auto_sec), a
+ * speculative GATT profile probe that reads an auth-protected
+ * characteristic must fail here rather than silently pair.
+ */
+ if (chan->att->no_auto_sec)
+ return false;
+
if (chan->sec_level != BT_ATT_SECURITY_AUTO)
return false;
@@ -2103,6 +2115,14 @@ bool bt_att_set_security(struct bt_att *att, int level)
return bt_att_chan_set_security(chan, level);
}
+void bt_att_set_no_auto_sec(struct bt_att *att, bool value)
+{
+ if (!att)
+ return;
+
+ att->no_auto_sec = value;
+}
+
void bt_att_set_enc_key_size(struct bt_att *att, uint8_t enc_size)
{
if (!att)
diff --git a/src/shared/att.h b/src/shared/att.h
index ba1f846..3dbfe86 100644
--- a/src/shared/att.h
+++ b/src/shared/att.h
@@ -112,6 +112,7 @@ bool bt_att_unregister_all(struct bt_att *att);
int bt_att_get_security(struct bt_att *att, uint8_t *enc_size);
bool bt_att_set_security(struct bt_att *att, int level);
+void bt_att_set_no_auto_sec(struct bt_att *att, bool value);
void bt_att_set_enc_key_size(struct bt_att *att, uint8_t enc_size);
bool bt_att_set_local_key(struct bt_att *att, uint8_t sign_key[16],
--
2.55.0
next prev parent reply other threads:[~2026-07-18 19:43 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-18 19:42 [PATCH BlueZ 0/2] Don't auto-bond on reactive GATT security elevation Philipp Dunkel
2026-07-18 19:42 ` Philipp Dunkel [this message]
2026-07-18 21:22 ` bluez.test.bot
2026-07-18 19:42 ` [PATCH BlueZ 2/2] unit/test-gatt: cover no-auto-sec on auth error Philipp Dunkel
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260718194230.35959-2-pip@pipobscure.com \
--to=pip@pipobscure.com \
--cc=linux-bluetooth@vger.kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox