Linux bluetooth development
 help / color / mirror / Atom feed
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


  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