* [PATCH BlueZ 0/2] Don't auto-bond on reactive GATT security elevation
@ 2026-07-18 19:42 Philipp Dunkel
2026-07-18 19:42 ` [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation Philipp Dunkel
2026-07-18 19:42 ` [PATCH BlueZ 2/2] unit/test-gatt: cover no-auto-sec on auth error Philipp Dunkel
0 siblings, 2 replies; 4+ messages in thread
From: Philipp Dunkel @ 2026-07-18 19:42 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Philipp Dunkel
bluetoothd probes every freshly connected LE peer as a GATT client
through its built-in profiles (battery, mcp, ...). When such a probe
reads a characteristic the peer has protected, the peer answers with
Insufficient Authentication (0x05), and change_security() reacts by
raising BT_SECURITY on the ATT socket. On an unbonded LE link that
starts SMP pairing, so the user is shown a pairing prompt for a device
that merely came into range -- a bond nobody requested, logged as a
device_bonding_complete() bond with a (nil) requestor.
The existing opt-out does not help: change_security() only bails when
chan->sec_level != BT_ATT_SECURITY_AUTO, but sec_level is recorded for
BT_ATT_LOCAL channels only, so an L2CAP/LE channel stays at AUTO for its
whole lifetime and always elevates.
Patch 1 adds bt_att_set_no_auto_sec(), a per-bt_att flag that makes
change_security() refuse to elevate, and sets it from gatt_client_init()
for any link that is neither bonded nor in a requested bonding.
In-progress Pair() and already-bonded devices keep their existing
elevation paths untouched.
Patch 2 adds a unit test: it arms AUTO security and then forbids
elevation, asserting the auth error is delivered to the caller with no
retry on the wire. Without patch 1 the client retries and the test
aborts on the unexpected Read Request.
Tested against Android (Galaxy Z Flip 4), iOS (iPhone) and macOS
(MacBook): each connects and is probed with no unsolicited pairing
prompt, while explicit pairing continues to work.
Both patches were developed with the assistance of an AI model
(Claude Opus 4.8), disclosed per-patch with an Assisted-by: trailer.
Every line was reviewed by me and the result was built, run and tested
on the hardware above -- behaviour observed, not inferred.
Philipp Dunkel (2):
shared/att: don't auto-bond on reactive elevation
unit/test-gatt: cover no-auto-sec on auth error
src/device.c | 13 +++++++++++++
src/shared/att.c | 20 ++++++++++++++++++++
src/shared/att.h | 1 +
unit/test-gatt.c | 28 ++++++++++++++++++++++++++++
4 files changed, 62 insertions(+)
--
2.55.0
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation
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
2026-07-18 21:22 ` Don't auto-bond on reactive GATT security elevation bluez.test.bot
2026-07-18 19:42 ` [PATCH BlueZ 2/2] unit/test-gatt: cover no-auto-sec on auth error Philipp Dunkel
1 sibling, 1 reply; 4+ messages in thread
From: Philipp Dunkel @ 2026-07-18 19:42 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Philipp Dunkel
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
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH BlueZ 2/2] unit/test-gatt: cover no-auto-sec on auth error
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 ` [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation Philipp Dunkel
@ 2026-07-18 19:42 ` Philipp Dunkel
1 sibling, 0 replies; 4+ messages in thread
From: Philipp Dunkel @ 2026-07-18 19:42 UTC (permalink / raw)
To: linux-bluetooth; +Cc: Philipp Dunkel
Add a regression test for bt_att_set_no_auto_sec(). The client reads a
characteristic and the peer answers with Insufficient Authentication
(0x05). Security is armed to BT_ATT_SECURITY_AUTO so a reactive elevation
would normally fire and retry the read, but no_auto_sec is set, so the
error must instead be delivered to the caller with no second request on
the wire.
This is the /auto variant (which does retry after elevating) with the
elevation forbidden. With the flag honoured the read fails once with
0x05; without it the client retries and the test aborts on the
unexpected Read Request, guarding the fix against regression.
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 test was
run against the built tree: it passes with patch 1 applied and was
confirmed to fail (client retries, harness aborts) without it.
---
unit/test-gatt.c | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/unit/test-gatt.c b/unit/test-gatt.c
index 535baaf..3929bb9 100644
--- a/unit/test-gatt.c
+++ b/unit/test-gatt.c
@@ -755,6 +755,22 @@ static void test_read(struct context *context)
test_read_cb, context, NULL));
}
+static void test_read_no_auto_sec(struct context *context)
+{
+ const struct test_step *step = context->data->step;
+
+ /* Arm reactive elevation (AUTO), then forbid it. An Insufficient
+ * Authentication error must be delivered to the caller instead of
+ * silently elevating security, which on a real unbonded link would
+ * start SMP bonding that nobody requested.
+ */
+ g_assert(bt_att_set_security(context->att, BT_ATT_SECURITY_AUTO));
+ bt_att_set_no_auto_sec(context->att, true);
+
+ g_assert(bt_gatt_client_read_value(context->client, step->handle,
+ test_read_cb, context, NULL));
+}
+
static const uint8_t read_data_1[] = {0x01, 0x02, 0x03};
static const struct test_step test_read_1 = {
@@ -795,6 +811,12 @@ static const struct test_step test_read_6 = {
.expected_att_ecode = 0x0c,
};
+static const struct test_step test_read_no_auto_sec_1 = {
+ .handle = 0x0003,
+ .func = test_read_no_auto_sec,
+ .expected_att_ecode = 0x05,
+};
+
static const struct test_step test_read_7 = {
.handle = 0x0004,
.func = test_read,
@@ -2803,6 +2825,12 @@ int main(int argc, char *argv[])
raw_pdu(0x0a, 0x03, 0x00),
raw_pdu(0x0b, 0x01, 0x02, 0x03));
+ define_test_client("/TP/GAR/CL/BI-04-C/no-auto-sec", test_client,
+ service_db_1, &test_read_no_auto_sec_1,
+ SERVICE_DATA_1_PDUS,
+ raw_pdu(0x0a, 0x03, 0x00),
+ raw_pdu(0x01, 0x0a, 0x03, 0x00, 0x05));
+
define_test_client("/TP/GAR/CL/BI-05-C", test_client, service_db_1,
&test_read_6,
SERVICE_DATA_1_PDUS,
--
2.55.0
^ permalink raw reply related [flat|nested] 4+ messages in thread
* RE: Don't auto-bond on reactive GATT security elevation
2026-07-18 19:42 ` [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation Philipp Dunkel
@ 2026-07-18 21:22 ` bluez.test.bot
0 siblings, 0 replies; 4+ messages in thread
From: bluez.test.bot @ 2026-07-18 21:22 UTC (permalink / raw)
To: linux-bluetooth, pip
[-- Attachment #1: Type: text/plain, Size: 2823 bytes --]
This is automated email and please do not reply to this email!
Dear submitter,
Thank you for submitting the patches to the linux bluetooth mailing list.
This is a CI test results with your patch series:
PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1130150
---Test result---
Test Summary:
CheckPatch FAIL 0.71 seconds
GitLint PASS 0.46 seconds
BuildEll PASS 18.36 seconds
BluezMake PASS 653.95 seconds
MakeCheck PASS 18.62 seconds
MakeDistcheck PASS 147.18 seconds
CheckValgrind PASS 211.82 seconds
CheckSmatch PASS 248.24 seconds
bluezmakeextell PASS 91.53 seconds
IncrementalBuild PASS 1156.64 seconds
ScanBuild PASS 818.66 seconds
Details
##############################
Test: CheckPatch - FAIL
Desc: Run checkpatch.pl script
Output:
[BlueZ,1/2] shared/att: don't auto-bond on reactive elevation
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#107:
Assisted-by: Claude:Opus-4.8
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'Claude:Opus-4.8'
#107:
Assisted-by: Claude:Opus-4.8
/github/workspace/src/patch/14695485.patch total: 1 errors, 1 warnings, 73 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14695485.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
[BlueZ,2/2] unit/test-gatt: cover no-auto-sec on auth error
WARNING:BAD_SIGN_OFF: Non-standard signature: Assisted-by:
#81:
Assisted-by: Claude:Opus-4.8
ERROR:BAD_SIGN_OFF: Unrecognized email address: 'Claude:Opus-4.8'
#81:
Assisted-by: Claude:Opus-4.8
/github/workspace/src/patch/14695486.patch total: 1 errors, 1 warnings, 46 lines checked
NOTE: For some of the reported defects, checkpatch may be able to
mechanically convert to the typical style using --fix or --fix-inplace.
/github/workspace/src/patch/14695486.patch has style problems, please review.
NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO
NOTE: If any of the errors are false positives, please report
them to the maintainer, see CHECKPATCH in MAINTAINERS.
https://github.com/bluez/bluez/pull/2330
---
Regards,
Linux Bluetooth
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-07-18 21:22 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH BlueZ 1/2] shared/att: don't auto-bond on reactive elevation Philipp Dunkel
2026-07-18 21:22 ` Don't auto-bond on reactive GATT security elevation bluez.test.bot
2026-07-18 19:42 ` [PATCH BlueZ 2/2] unit/test-gatt: cover no-auto-sec on auth error Philipp Dunkel
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox