From 5fbef7528eb7d95308b5c821423fa5b27f2b1bb8 Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 14:16:33 +0200 Subject: [PATCH 1/6] nimble/host: add Database Out Of Sync ATT error code Add the ATT error code 0x12 (Core spec v5.4, Vol 3, Part F, 3.4.1.1), sent by a server to a change-unaware client as part of the GATT robust caching flow. Signed-off-by: Gerard Marull-Paretas --- nimble/host/include/host/ble_att.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nimble/host/include/host/ble_att.h b/nimble/host/include/host/ble_att.h index 8323c9d764..1c7ccc4ef8 100644 --- a/nimble/host/include/host/ble_att.h +++ b/nimble/host/include/host/ble_att.h @@ -110,6 +110,9 @@ struct os_mbuf; /**Insufficient Resources to complete the request. */ #define BLE_ATT_ERR_INSUFFICIENT_RES 0x11 +/** The server requests the client to rediscover the database. */ +#define BLE_ATT_ERR_DATABASE_OUT_OF_SYNC 0x12 + /**Requested value is not allowed. */ #define BLE_ATT_ERR_VALUE_NOT_ALLOWED 0x13 From 7dd86b4cb487bc9ffbe5f0682abadee2aa3ecff3 Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 14:16:51 +0200 Subject: [PATCH 2/6] nimble/host: add GATT database hash calculation Add ble_gatts_calculate_hash(), which computes the GATT database hash as defined in Core spec v5.4, Vol 3, Part G, 7.3.1: an AES-CMAC (with an all-zero key) over the concatenation of the client-visible attribute database, in ascending handle order. Service declarations, includes, characteristic declarations and extended properties descriptors contribute handle, type and value; the 0x2901-0x2905 descriptors contribute handle and type only; all other attributes are excluded. Attributes hidden via ble_gatts_svc_set_visibility() are skipped, as they are not part of the client-visible database. The stream is fed incrementally into the Mbed TLS CMAC API, avoiding a heap allocation for a flattened copy of the database. Per-attribute values are read through ble_att_svr_read_local(), so declaration values come from the same access callbacks that serve regular ATT reads. The CMAC output is byte-swapped since the hash characteristic value is transmitted in little-endian byte order (matching PTS expectations and other stacks, e.g. Zephyr). The feature is gated behind the new BLE_GATT_CACHING syscfg setting, disabled by default. Signed-off-by: Gerard Marull-Paretas --- nimble/host/include/host/ble_gatt.h | 24 ++++ nimble/host/pkg.yml | 3 + nimble/host/src/ble_gatts_db_hash.c | 160 +++++++++++++++++++++++++ nimble/host/syscfg.yml | 8 ++ porting/nimble/include/syscfg/syscfg.h | 4 + 5 files changed, 199 insertions(+) create mode 100644 nimble/host/src/ble_gatts_db_hash.c diff --git a/nimble/host/include/host/ble_gatt.h b/nimble/host/include/host/ble_gatt.h index e74ba9e1a1..c18fcd309a 100644 --- a/nimble/host/include/host/ble_gatt.h +++ b/nimble/host/include/host/ble_gatt.h @@ -68,6 +68,18 @@ struct ble_hs_cfg; /** GATT Characteristic Extended Porperties descriptor 16-bit UUID. */ #define BLE_GATT_DSC_EXT_PROP_UUID16 0x2900 +/** GATT Characteristic User Description descriptor 16-bit UUID. */ +#define BLE_GATT_DSC_USER_DESC_UUID16 0x2901 + +/** GATT Server Characteristic Configuration descriptor 16-bit UUID. */ +#define BLE_GATT_DSC_SRV_CFG_UUID16 0x2903 + +/** GATT Characteristic Presentation Format descriptor 16-bit UUID. */ +#define BLE_GATT_DSC_CHR_FMT_UUID16 0x2904 + +/** GATT Characteristic Aggregate Format descriptor 16-bit UUID. */ +#define BLE_GATT_DSC_AGG_FMT_UUID16 0x2905 + /** @} */ /** @@ -1139,6 +1151,18 @@ typedef void (*ble_gatt_svc_foreach_fn)(const struct ble_gatt_svc_def *svc, */ void ble_gatts_show_local(void); +/** + * Calculates the database hash of the local GATT database (AES-CMAC over + * the attribute database; Core spec Vol 3, Part G, 7.3.1). Only available + * if the BLE_GATT_CACHING syscfg setting is enabled. + * + * @param out_hash_key Buffer to fill with the 16-byte database + * hash, in little-endian byte order. + * + * @return 0 on success; nonzero on failure. + */ +int ble_gatts_calculate_hash(uint8_t *out_hash_key); + /** * Resets the GATT server to its initial state. On success, this function * removes all supported services, characteristics, and descriptors. This diff --git a/nimble/host/pkg.yml b/nimble/host/pkg.yml index 2bb5707914..f231b79dce 100644 --- a/nimble/host/pkg.yml +++ b/nimble/host/pkg.yml @@ -41,6 +41,9 @@ pkg.deps.BLE_SM_LEGACY: pkg.deps.BLE_SM_SC: - "@apache-mynewt-core/crypto/mbedtls" +pkg.deps.BLE_GATT_CACHING: + - "@apache-mynewt-core/crypto/mbedtls" + pkg.deps.BLE_MESH: - nimble/host/mesh diff --git a/nimble/host/src/ble_gatts_db_hash.c b/nimble/host/src/ble_gatts_db_hash.c new file mode 100644 index 0000000000..f06a22d0a5 --- /dev/null +++ b/nimble/host/src/ble_gatts_db_hash.c @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +#include "syscfg/syscfg.h" + +#if MYNEWT_VAL(BLE_GATT_CACHING) + +#include +#include "os/endian.h" +#include "host/ble_gatt.h" +#include "ble_hs_priv.h" +#include +#include + +/* The longest attribute value contributing to the hash is a characteristic + * declaration: properties (1) + value handle (2) + 128-bit UUID (16). + */ +#define BLE_GATTS_DB_HASH_MAX_VAL_SZ 19 + +static int +ble_gatts_db_hash_update_val(mbedtls_cipher_context_t *ctx, uint16_t handle) +{ + struct os_mbuf *om; + uint8_t buf[BLE_GATTS_DB_HASH_MAX_VAL_SZ]; + uint16_t len; + int rc; + + rc = ble_att_svr_read_local(handle, &om); + if (rc != 0) { + return rc; + } + + len = OS_MBUF_PKTLEN(om); + if (len > sizeof buf) { + rc = BLE_HS_EINVAL; + goto done; + } + + rc = os_mbuf_copydata(om, 0, len, buf); + BLE_HS_DBG_ASSERT(rc == 0); + + if (mbedtls_cipher_cmac_update(ctx, buf, len) != 0) { + rc = BLE_HS_EUNKNOWN; + goto done; + } + + rc = 0; + +done: + os_mbuf_free_chain(om); + return rc; +} + +/** + * Calculates the hash of the local attribute database, as defined in + * Core spec v5.4, Vol 3, Part G, 7.3.1. + */ +int +ble_gatts_calculate_hash(uint8_t *out_hash_key) +{ + mbedtls_cipher_context_t ctx; + struct ble_att_svr_entry *entry; + uint8_t key[16]; + uint8_t buf[4]; + uint16_t uuid16; + uint16_t handle; + int with_val; + int rc; + + mbedtls_cipher_init(&ctx); + + memset(key, 0, sizeof key); + if (mbedtls_cipher_setup(&ctx, mbedtls_cipher_info_from_type( + MBEDTLS_CIPHER_AES_128_ECB)) != 0 || + mbedtls_cipher_cmac_starts(&ctx, key, 128) != 0) { + rc = BLE_HS_EUNKNOWN; + goto done; + } + + for (handle = 1; handle != 0 && handle <= ble_att_svr_prev_handle(); + handle++) { + entry = ble_att_svr_find_by_handle(handle); + if (entry == NULL) { + /* Hidden attribute; not part of the client-visible database. */ + continue; + } + + if (entry->ha_uuid->type != BLE_UUID_TYPE_16) { + continue; + } + uuid16 = BLE_UUID16(entry->ha_uuid)->value; + + switch (uuid16) { + case BLE_ATT_UUID_PRIMARY_SERVICE: + case BLE_ATT_UUID_SECONDARY_SERVICE: + case BLE_ATT_UUID_INCLUDE: + case BLE_ATT_UUID_CHARACTERISTIC: + case BLE_GATT_DSC_EXT_PROP_UUID16: + with_val = 1; + break; + + case BLE_GATT_DSC_USER_DESC_UUID16: + case BLE_GATT_DSC_CLT_CFG_UUID16: + case BLE_GATT_DSC_SRV_CFG_UUID16: + case BLE_GATT_DSC_CHR_FMT_UUID16: + case BLE_GATT_DSC_AGG_FMT_UUID16: + with_val = 0; + break; + + default: + continue; + } + + put_le16(buf, handle); + put_le16(buf + 2, uuid16); + if (mbedtls_cipher_cmac_update(&ctx, buf, sizeof buf) != 0) { + rc = BLE_HS_EUNKNOWN; + goto done; + } + + if (with_val) { + rc = ble_gatts_db_hash_update_val(&ctx, handle); + if (rc != 0) { + goto done; + } + } + } + + if (mbedtls_cipher_cmac_finish(&ctx, out_hash_key) != 0) { + rc = BLE_HS_EUNKNOWN; + goto done; + } + + /* CMAC output is big-endian; the hash is transmitted little-endian. */ + swap_in_place(out_hash_key, 16); + + rc = 0; + +done: + mbedtls_cipher_free(&ctx); + return rc; +} + +#endif /* MYNEWT_VAL(BLE_GATT_CACHING) */ diff --git a/nimble/host/syscfg.yml b/nimble/host/syscfg.yml index 36bfb63896..936531ca81 100644 --- a/nimble/host/syscfg.yml +++ b/nimble/host/syscfg.yml @@ -317,6 +317,14 @@ syscfg.defs: due to memory exhaustion. (0/1) Units are milliseconds. (0/1) value: 1000 + # GATT caching options + BLE_GATT_CACHING: + description: > + Enables server support for GATT caching: exposes the Database + Hash characteristic in the GATT service so clients can detect + attribute database changes. + value: 0 + # Enhanced ATT bearer options BLE_EATT_CHAN_NUM: description: > diff --git a/porting/nimble/include/syscfg/syscfg.h b/porting/nimble/include/syscfg/syscfg.h index cc614f8376..09bd81d72c 100644 --- a/porting/nimble/include/syscfg/syscfg.h +++ b/porting/nimble/include/syscfg/syscfg.h @@ -863,6 +863,10 @@ #define MYNEWT_VAL_BLE_GAP_MAX_PENDING_CONN_PARAM_UPDATE (1) #endif +#ifndef MYNEWT_VAL_BLE_GATT_CACHING +#define MYNEWT_VAL_BLE_GATT_CACHING (0) +#endif + #ifndef MYNEWT_VAL_BLE_GATT_DISC_ALL_CHRS #define MYNEWT_VAL_BLE_GATT_DISC_ALL_CHRS (MYNEWT_VAL_BLE_ROLE_CENTRAL) #endif From 89d4e645a26ca36c204b8aeea9ac94973e13ce64 Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 14:17:03 +0200 Subject: [PATCH 3/6] nimble/host/services/gatt: add Database Hash characteristic Expose the Database Hash characteristic (0x2B2A) in the GATT service when BLE_GATT_CACHING is enabled, backed by ble_gatts_calculate_hash(). The hash is computed on each read; the database is static after ble_gatts_start(), and reads only happen when a caching client revalidates its cache, typically once per reconnection. This lets clients that cache the attribute database (notably iOS and Android) detect database changes across firmware updates deterministically, instead of relying only on Service Changed indications. Signed-off-by: Gerard Marull-Paretas --- .../gatt/include/services/gatt/ble_svc_gatt.h | 1 + nimble/host/services/gatt/src/ble_svc_gatt.c | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h b/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h index eefd412f57..82cdb8b741 100644 --- a/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h +++ b/nimble/host/services/gatt/include/services/gatt/ble_svc_gatt.h @@ -29,6 +29,7 @@ extern "C" { struct ble_hs_cfg; #define BLE_SVC_GATT_CHR_SERVICE_CHANGED_UUID16 0x2a05 +#define BLE_SVC_GATT_CHR_DATABASE_HASH_UUID16 0x2b2a #define BLE_SVC_GATT_CHR_SERVER_SUPPORTED_FEAT_UUID16 0x2b3a #define BLE_SVC_GATT_CHR_CLIENT_SUPPORTED_FEAT_UUID16 0x2b29 diff --git a/nimble/host/services/gatt/src/ble_svc_gatt.c b/nimble/host/services/gatt/src/ble_svc_gatt.c index 331d6bedd8..15b26bdd11 100644 --- a/nimble/host/services/gatt/src/ble_svc_gatt.c +++ b/nimble/host/services/gatt/src/ble_svc_gatt.c @@ -51,6 +51,12 @@ static int ble_svc_gatt_cl_sup_feat_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg); +#if MYNEWT_VAL(BLE_GATT_CACHING) +static int +ble_svc_gatt_db_hash_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg); +#endif + static const struct ble_gatt_svc_def ble_svc_gatt_defs[] = { { /*** Service: GATT */ @@ -73,6 +79,13 @@ static const struct ble_gatt_svc_def ble_svc_gatt_defs[] = { .access_cb = ble_svc_gatt_cl_sup_feat_access, .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, }, +#if MYNEWT_VAL(BLE_GATT_CACHING) + { + .uuid = BLE_UUID16_DECLARE(BLE_SVC_GATT_CHR_DATABASE_HASH_UUID16), + .access_cb = ble_svc_gatt_db_hash_access, + .flags = BLE_GATT_CHR_F_READ, + }, +#endif { 0, /* No more characteristics in this service. */ } @@ -122,6 +135,32 @@ ble_svc_gatt_cl_sup_feat_access(uint16_t conn_handle, uint16_t attr_handle, return 0; } +#if MYNEWT_VAL(BLE_GATT_CACHING) +static int +ble_svc_gatt_db_hash_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint8_t db_hash[16]; + int rc; + + if (ctxt->op != BLE_GATT_ACCESS_OP_READ_CHR) { + return BLE_ATT_ERR_WRITE_NOT_PERMITTED; + } + + rc = ble_gatts_calculate_hash(db_hash); + if (rc != 0) { + return BLE_ATT_ERR_UNLIKELY; + } + + rc = os_mbuf_append(ctxt->om, db_hash, sizeof(db_hash)); + if (rc != 0) { + return BLE_ATT_ERR_INSUFFICIENT_RES; + } + + return 0; +} +#endif + static int ble_svc_gatt_access(uint16_t conn_handle, uint16_t attr_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) From 5f01f9a0c7ed58a379f271f4334b437ac7433c4d Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 15:11:06 +0200 Subject: [PATCH 4/6] nimble/host: add CSFC and database hash store object types Add two store object types used by server-side GATT caching: - BLE_STORE_OBJ_TYPE_CSFC persists, per bonded peer, the Client Supported Features value written by the peer along with its change-aware state, both of which must survive reconnections for bonded clients (Vol 3, Part G, 2.5.2.1). - BLE_STORE_OBJ_TYPE_DB_HASH persists the local database hash, so the host can detect across reboots (e.g. firmware updates) that the database changed and mark bonded peers change-unaware. ble_store_util_delete_peer() now also removes the peer's CSFC record, tolerating stores that do not handle the new object type. Signed-off-by: Gerard Marull-Paretas --- nimble/host/include/host/ble_store.h | 121 +++++++++++++++++++++++++++ nimble/host/src/ble_store.c | 83 ++++++++++++++++++ nimble/host/src/ble_store_util.c | 9 ++ 3 files changed, 213 insertions(+) diff --git a/nimble/host/include/host/ble_store.h b/nimble/host/include/host/ble_store.h index 83b1c4f15f..f396cedde3 100644 --- a/nimble/host/include/host/ble_store.h +++ b/nimble/host/include/host/ble_store.h @@ -48,6 +48,12 @@ extern "C" { /** Object type: Client Characteristic Configuration Descriptor. */ #define BLE_STORE_OBJ_TYPE_CCCD 3 +/** Object type: Client Supported Features characteristic value. */ +#define BLE_STORE_OBJ_TYPE_CSFC 4 + +/** Object type: local GATT database hash. */ +#define BLE_STORE_OBJ_TYPE_DB_HASH 5 + /** @} */ /** @@ -154,6 +160,57 @@ struct ble_store_value_cccd { unsigned value_changed:1; }; +/** Size of the stored Client Supported Features value, in bytes. */ +#define BLE_STORE_CSFC_SZ 1 + +/** + * Used as a key for lookups of stored Client Supported Features characteristic + * (CSFC) values. This struct corresponds to the BLE_STORE_OBJ_TYPE_CSFC store + * object type. + */ +struct ble_store_key_csfc { + /** + * Key by peer identity address; + * peer_addr=BLE_ADDR_NONE means don't key off peer. + */ + ble_addr_t peer_addr; + + /** Number of results to skip; 0 means retrieve the first match. */ + uint8_t idx; +}; + +/** + * Represents a stored Client Supported Features characteristic (CSFC) value + * along with the associated GATT caching state. This struct corresponds to + * the BLE_STORE_OBJ_TYPE_CSFC store object type. + */ +struct ble_store_value_csfc { + /** The peer address associated with the stored value. */ + ble_addr_t peer_addr; + /** The Client Supported Features bits written by the peer. */ + uint8_t csfc[BLE_STORE_CSFC_SZ]; + /** Flag indicating whether the peer is change-aware. */ + unsigned change_aware:1; +}; + +/** + * Used as a key for lookups of the stored local GATT database hash. This + * struct corresponds to the BLE_STORE_OBJ_TYPE_DB_HASH store object type. + */ +struct ble_store_key_db_hash { + /** Number of results to skip; 0 means retrieve the first match. */ + uint8_t idx; +}; + +/** + * Represents the stored local GATT database hash. This struct corresponds to + * the BLE_STORE_OBJ_TYPE_DB_HASH store object type. + */ +struct ble_store_value_db_hash { + /** The database hash, in little-endian byte order. */ + uint8_t hash[16]; +}; + /** * Used as a key for store lookups. This union must be accompanied by an * object type code to indicate which field is valid. @@ -163,6 +220,10 @@ union ble_store_key { struct ble_store_key_sec sec; /** Key for Client Characteristic Configuration Descriptor store lookups. */ struct ble_store_key_cccd cccd; + /** Key for Client Supported Features store lookups. */ + struct ble_store_key_csfc csfc; + /** Key for local GATT database hash store lookups. */ + struct ble_store_key_db_hash db_hash; }; /** @@ -174,6 +235,10 @@ union ble_store_value { struct ble_store_value_sec sec; /** Stored Client Characteristic Configuration Descriptor. */ struct ble_store_value_cccd cccd; + /** Stored Client Supported Features value. */ + struct ble_store_value_csfc csfc; + /** Stored local GATT database hash. */ + struct ble_store_value_db_hash db_hash; }; /** Represents an event associated with the BLE Store. */ @@ -556,6 +621,53 @@ int ble_store_write_cccd(const struct ble_store_value_cccd *value); */ int ble_store_delete_cccd(const struct ble_store_key_cccd *key); +/** + * Reads a Client Supported Features characteristic (CSFC) value from storage. + * + * @param key The key identifying the CSFC value to read. + * @param out_value On success, filled with the stored value. + * + * @return 0 on success; nonzero on error. + */ +int ble_store_read_csfc(const struct ble_store_key_csfc *key, + struct ble_store_value_csfc *out_value); + +/** + * Writes a Client Supported Features characteristic (CSFC) value to storage. + * + * @param value The value to persist. + * + * @return 0 on success; nonzero on error. + */ +int ble_store_write_csfc(const struct ble_store_value_csfc *value); + +/** + * Deletes a Client Supported Features characteristic (CSFC) value from + * storage. + * + * @param key The key identifying the CSFC value to delete. + * + * @return 0 on success; nonzero on error. + */ +int ble_store_delete_csfc(const struct ble_store_key_csfc *key); + +/** + * Reads the stored local GATT database hash. + * + * @param out_value On success, filled with the stored hash. + * + * @return 0 on success; nonzero on error. + */ +int ble_store_read_db_hash(struct ble_store_value_db_hash *out_value); + +/** + * Writes the local GATT database hash to storage. + * + * @param value The hash to persist. + * + * @return 0 on success; nonzero on error. + */ +int ble_store_write_db_hash(const struct ble_store_value_db_hash *value); /** * @brief Generates a storage key for a security material entry from its value. @@ -587,6 +699,15 @@ void ble_store_key_from_value_sec(struct ble_store_key_sec *out_key, void ble_store_key_from_value_cccd(struct ble_store_key_cccd *out_key, const struct ble_store_value_cccd *value); +/** + * Generates a storage key for a Client Supported Features entry from its + * value. + * + * @param out_key On return, the generated key. + * @param value The value to generate a key from. + */ +void ble_store_key_from_value_csfc(struct ble_store_key_csfc *out_key, + const struct ble_store_value_csfc *value); /** * @brief Generates a storage key from a value based on the object type. diff --git a/nimble/host/src/ble_store.c b/nimble/host/src/ble_store.c index 79b2f7b95e..72e202bb9e 100644 --- a/nimble/host/src/ble_store.c +++ b/nimble/host/src/ble_store.c @@ -294,6 +294,74 @@ ble_store_key_from_value_cccd(struct ble_store_key_cccd *out_key, out_key->idx = 0; } +int +ble_store_read_csfc(const struct ble_store_key_csfc *key, + struct ble_store_value_csfc *out_value) +{ + union ble_store_value *store_value; + union ble_store_key *store_key; + int rc; + + store_key = (void *)key; + store_value = (void *)out_value; + rc = ble_store_read(BLE_STORE_OBJ_TYPE_CSFC, store_key, store_value); + return rc; +} + +int +ble_store_write_csfc(const struct ble_store_value_csfc *value) +{ + union ble_store_value *store_value; + int rc; + + store_value = (void *)value; + rc = ble_store_write(BLE_STORE_OBJ_TYPE_CSFC, store_value); + return rc; +} + +int +ble_store_delete_csfc(const struct ble_store_key_csfc *key) +{ + union ble_store_key *store_key; + int rc; + + store_key = (void *)key; + rc = ble_store_delete(BLE_STORE_OBJ_TYPE_CSFC, store_key); + return rc; +} + +void +ble_store_key_from_value_csfc(struct ble_store_key_csfc *out_key, + const struct ble_store_value_csfc *value) +{ + out_key->peer_addr = value->peer_addr; + out_key->idx = 0; +} + +int +ble_store_read_db_hash(struct ble_store_value_db_hash *out_value) +{ + union ble_store_value *store_value; + union ble_store_key store_key; + int rc; + + store_key.db_hash.idx = 0; + store_value = (void *)out_value; + rc = ble_store_read(BLE_STORE_OBJ_TYPE_DB_HASH, &store_key, store_value); + return rc; +} + +int +ble_store_write_db_hash(const struct ble_store_value_db_hash *value) +{ + union ble_store_value *store_value; + int rc; + + store_value = (void *)value; + rc = ble_store_write(BLE_STORE_OBJ_TYPE_DB_HASH, store_value); + return rc; +} + void ble_store_key_from_value_sec(struct ble_store_key_sec *out_key, const struct ble_store_value_sec *value) @@ -317,6 +385,14 @@ ble_store_key_from_value(int obj_type, ble_store_key_from_value_cccd(&out_key->cccd, &value->cccd); break; + case BLE_STORE_OBJ_TYPE_CSFC: + ble_store_key_from_value_csfc(&out_key->csfc, &value->csfc); + break; + + case BLE_STORE_OBJ_TYPE_DB_HASH: + out_key->db_hash.idx = 0; + break; + default: BLE_HS_DBG_ASSERT(0); break; @@ -346,6 +422,13 @@ ble_store_iterate(int obj_type, key.cccd.peer_addr = *BLE_ADDR_ANY; pidx = &key.cccd.idx; break; + case BLE_STORE_OBJ_TYPE_CSFC: + key.csfc.peer_addr = *BLE_ADDR_ANY; + pidx = &key.csfc.idx; + break; + case BLE_STORE_OBJ_TYPE_DB_HASH: + pidx = &key.db_hash.idx; + break; default: BLE_HS_DBG_ASSERT(0); return BLE_HS_EINVAL; diff --git a/nimble/host/src/ble_store_util.c b/nimble/host/src/ble_store_util.c index 6dcbca25a1..e460784100 100644 --- a/nimble/host/src/ble_store_util.c +++ b/nimble/host/src/ble_store_util.c @@ -109,6 +109,15 @@ ble_store_util_delete_peer(const ble_addr_t *peer_id_addr) return rc; } + memset(&key, 0, sizeof key); + key.csfc.peer_addr = *peer_id_addr; + + /* Tolerate stores that do not handle the CSFC object type. */ + rc = ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CSFC, &key); + if (rc != 0 && rc != BLE_HS_ENOTSUP) { + return rc; + } + return 0; } From fc09949ec08ce727fbc43e12a2a6c0d92d3920a2 Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 15:11:17 +0200 Subject: [PATCH 5/6] nimble/host/store: handle GATT caching records in config store Store and load the CSFC and database hash records in the config store backend when BLE_GATT_CACHING is enabled. They are persisted under the ble_hs/csfc and ble_hs/db_hash settings, following the existing CCCD pattern; CSFC records are capped at BLE_STORE_MAX_BONDS. Signed-off-by: Gerard Marull-Paretas --- .../host/store/config/src/ble_store_config.c | 197 ++++++++++++++++++ .../store/config/src/ble_store_config_conf.c | 87 ++++++++ .../store/config/src/ble_store_config_priv.h | 17 ++ 3 files changed, 301 insertions(+) diff --git a/nimble/host/store/config/src/ble_store_config.c b/nimble/host/store/config/src/ble_store_config.c index 741c73a5f5..d851e0e1d2 100644 --- a/nimble/host/store/config/src/ble_store_config.c +++ b/nimble/host/store/config/src/ble_store_config.c @@ -47,6 +47,15 @@ struct ble_store_value_cccd int ble_store_config_num_cccds; +#if MYNEWT_VAL(BLE_GATT_CACHING) +struct ble_store_value_csfc + ble_store_config_csfcs[MYNEWT_VAL(BLE_STORE_MAX_BONDS)]; +int ble_store_config_num_csfcs; + +struct ble_store_value_db_hash ble_store_config_db_hash; +int ble_store_config_db_hash_present; +#endif + /***************************************************************************** * $sec * *****************************************************************************/ @@ -447,6 +456,160 @@ ble_store_config_write_cccd(const struct ble_store_value_cccd *value_cccd) #endif } +/***************************************************************************** + * $csfc * + *****************************************************************************/ + +#if MYNEWT_VAL(BLE_GATT_CACHING) +static int +ble_store_config_find_csfc(const struct ble_store_key_csfc *key) +{ + struct ble_store_value_csfc *csfc; + int skipped; + int i; + + skipped = 0; + for (i = 0; i < ble_store_config_num_csfcs; i++) { + csfc = ble_store_config_csfcs + i; + + if (ble_addr_cmp(&key->peer_addr, BLE_ADDR_ANY)) { + if (ble_addr_cmp(&csfc->peer_addr, &key->peer_addr)) { + continue; + } + } + + if (key->idx > skipped) { + skipped++; + continue; + } + + return i; + } + return -1; +} + +static int +ble_store_config_delete_csfc(const struct ble_store_key_csfc *key_csfc) +{ + int idx; + int rc; + + idx = ble_store_config_find_csfc(key_csfc); + if (idx < 0) { + return BLE_HS_ENOENT; + } + + rc = ble_store_config_delete_obj(ble_store_config_csfcs, + sizeof *ble_store_config_csfcs, + idx, + &ble_store_config_num_csfcs); + if (rc != 0) { + return rc; + } + + rc = ble_store_config_persist_csfcs(); + if (rc != 0) { + return rc; + } + return 0; +} + +static int +ble_store_config_read_csfc(const struct ble_store_key_csfc *key_csfc, + struct ble_store_value_csfc *value_csfc) +{ + int idx; + + idx = ble_store_config_find_csfc(key_csfc); + if (idx == -1) { + return BLE_HS_ENOENT; + } + + *value_csfc = ble_store_config_csfcs[idx]; + return 0; +} + +static int +ble_store_config_write_csfc(const struct ble_store_value_csfc *value_csfc) +{ + struct ble_store_key_csfc key_csfc; + int idx; + int rc; + + ble_store_key_from_value_csfc(&key_csfc, value_csfc); + idx = ble_store_config_find_csfc(&key_csfc); + if (idx == -1) { + if (ble_store_config_num_csfcs >= MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { + BLE_HS_LOG(DEBUG, "error persisting csfc; too many entries (%d)\n", + ble_store_config_num_csfcs); + return BLE_HS_ESTORE_CAP; + } + + idx = ble_store_config_num_csfcs; + ble_store_config_num_csfcs++; + } + + ble_store_config_csfcs[idx] = *value_csfc; + + rc = ble_store_config_persist_csfcs(); + if (rc != 0) { + return rc; + } + + return 0; +} + +/***************************************************************************** + * $db hash * + *****************************************************************************/ + +static int +ble_store_config_read_db_hash(struct ble_store_value_db_hash *value_db_hash) +{ + if (!ble_store_config_db_hash_present) { + return BLE_HS_ENOENT; + } + + *value_db_hash = ble_store_config_db_hash; + return 0; +} + +static int +ble_store_config_write_db_hash(const struct ble_store_value_db_hash *value_db_hash) +{ + int rc; + + ble_store_config_db_hash = *value_db_hash; + ble_store_config_db_hash_present = 1; + + rc = ble_store_config_persist_db_hash(); + if (rc != 0) { + return rc; + } + + return 0; +} + +static int +ble_store_config_delete_db_hash(void) +{ + int rc; + + if (!ble_store_config_db_hash_present) { + return BLE_HS_ENOENT; + } + + ble_store_config_db_hash_present = 0; + + rc = ble_store_config_persist_db_hash(); + if (rc != 0) { + return rc; + } + + return 0; +} +#endif /* MYNEWT_VAL(BLE_GATT_CACHING) */ + /***************************************************************************** * $api * *****************************************************************************/ @@ -489,6 +652,16 @@ ble_store_config_read(int obj_type, const union ble_store_key *key, rc = ble_store_config_read_cccd(&key->cccd, &value->cccd); return rc; +#if MYNEWT_VAL(BLE_GATT_CACHING) + case BLE_STORE_OBJ_TYPE_CSFC: + rc = ble_store_config_read_csfc(&key->csfc, &value->csfc); + return rc; + + case BLE_STORE_OBJ_TYPE_DB_HASH: + rc = ble_store_config_read_db_hash(&value->db_hash); + return rc; +#endif + default: return BLE_HS_ENOTSUP; } @@ -518,6 +691,16 @@ ble_store_config_write(int obj_type, const union ble_store_value *val) rc = ble_store_config_write_cccd(&val->cccd); return rc; +#if MYNEWT_VAL(BLE_GATT_CACHING) + case BLE_STORE_OBJ_TYPE_CSFC: + rc = ble_store_config_write_csfc(&val->csfc); + return rc; + + case BLE_STORE_OBJ_TYPE_DB_HASH: + rc = ble_store_config_write_db_hash(&val->db_hash); + return rc; +#endif + default: return BLE_HS_ENOTSUP; } @@ -541,6 +724,16 @@ ble_store_config_delete(int obj_type, const union ble_store_key *key) rc = ble_store_config_delete_cccd(&key->cccd); return rc; +#if MYNEWT_VAL(BLE_GATT_CACHING) + case BLE_STORE_OBJ_TYPE_CSFC: + rc = ble_store_config_delete_csfc(&key->csfc); + return rc; + + case BLE_STORE_OBJ_TYPE_DB_HASH: + rc = ble_store_config_delete_db_hash(); + return rc; +#endif + default: return BLE_HS_ENOTSUP; } @@ -560,6 +753,10 @@ ble_store_config_init(void) ble_store_config_num_our_secs = 0; ble_store_config_num_peer_secs = 0; ble_store_config_num_cccds = 0; +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_store_config_num_csfcs = 0; + ble_store_config_db_hash_present = 0; +#endif ble_store_config_conf_init(); } diff --git a/nimble/host/store/config/src/ble_store_config_conf.c b/nimble/host/store/config/src/ble_store_config_conf.c index 4090b6020b..33f18e5c62 100644 --- a/nimble/host/store/config/src/ble_store_config_conf.c +++ b/nimble/host/store/config/src/ble_store_config_conf.c @@ -57,6 +57,17 @@ static struct conf_handler ble_store_config_conf_handler = { #define BLE_STORE_CONFIG_CCCD_SET_ENCODE_SZ \ (MYNEWT_VAL(BLE_STORE_MAX_CCCDS) * BLE_STORE_CONFIG_CCCD_ENCODE_SZ + 1) +#if MYNEWT_VAL(BLE_GATT_CACHING) +#define BLE_STORE_CONFIG_CSFC_ENCODE_SZ \ + BASE64_ENCODE_SIZE(sizeof (struct ble_store_value_csfc)) + +#define BLE_STORE_CONFIG_CSFC_SET_ENCODE_SZ \ + (MYNEWT_VAL(BLE_STORE_MAX_BONDS) * BLE_STORE_CONFIG_CSFC_ENCODE_SZ + 1) + +#define BLE_STORE_CONFIG_DB_HASH_ENCODE_SZ \ + (BASE64_ENCODE_SIZE(sizeof (struct ble_store_value_db_hash)) + 1) +#endif + static void ble_store_config_serialize_arr(const void *arr, int obj_sz, int num_objs, char *out_buf, int buf_sz) @@ -118,6 +129,22 @@ ble_store_config_conf_set(int argc, char **argv, char *val) sizeof *ble_store_config_cccds, &ble_store_config_num_cccds); return rc; +#if MYNEWT_VAL(BLE_GATT_CACHING) + } else if (strcmp(argv[0], "csfc") == 0) { + rc = ble_store_config_deserialize_arr( + val, + ble_store_config_csfcs, + sizeof *ble_store_config_csfcs, + &ble_store_config_num_csfcs); + return rc; + } else if (strcmp(argv[0], "db_hash") == 0) { + rc = ble_store_config_deserialize_arr( + val, + &ble_store_config_db_hash, + sizeof ble_store_config_db_hash, + &ble_store_config_db_hash_present); + return rc; +#endif } } return OS_ENOENT; @@ -130,6 +157,10 @@ ble_store_config_conf_export(void (*func)(char *name, char *val), union { char sec[BLE_STORE_CONFIG_SEC_SET_ENCODE_SZ]; char cccd[BLE_STORE_CONFIG_CCCD_SET_ENCODE_SZ]; +#if MYNEWT_VAL(BLE_GATT_CACHING) + char csfc[BLE_STORE_CONFIG_CSFC_SET_ENCODE_SZ]; + char db_hash[BLE_STORE_CONFIG_DB_HASH_ENCODE_SZ]; +#endif } buf; ble_store_config_serialize_arr(ble_store_config_our_secs, @@ -153,6 +184,22 @@ ble_store_config_conf_export(void (*func)(char *name, char *val), sizeof buf.cccd); func("ble_hs/cccd", buf.cccd); +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_store_config_serialize_arr(ble_store_config_csfcs, + sizeof *ble_store_config_csfcs, + ble_store_config_num_csfcs, + buf.csfc, + sizeof buf.csfc); + func("ble_hs/csfc", buf.csfc); + + ble_store_config_serialize_arr(&ble_store_config_db_hash, + sizeof ble_store_config_db_hash, + ble_store_config_db_hash_present, + buf.db_hash, + sizeof buf.db_hash); + func("ble_hs/db_hash", buf.db_hash); +#endif + return 0; } @@ -223,6 +270,46 @@ ble_store_config_persist_cccds(void) return 0; } +#if MYNEWT_VAL(BLE_GATT_CACHING) +int +ble_store_config_persist_csfcs(void) +{ + char buf[BLE_STORE_CONFIG_CSFC_SET_ENCODE_SZ]; + int rc; + + ble_store_config_serialize_arr(ble_store_config_csfcs, + sizeof *ble_store_config_csfcs, + ble_store_config_num_csfcs, + buf, + sizeof buf); + rc = conf_save_one("ble_hs/csfc", buf); + if (rc != 0) { + return BLE_HS_ESTORE_FAIL; + } + + return 0; +} + +int +ble_store_config_persist_db_hash(void) +{ + char buf[BLE_STORE_CONFIG_DB_HASH_ENCODE_SZ]; + int rc; + + ble_store_config_serialize_arr(&ble_store_config_db_hash, + sizeof ble_store_config_db_hash, + ble_store_config_db_hash_present, + buf, + sizeof buf); + rc = conf_save_one("ble_hs/db_hash", buf); + if (rc != 0) { + return BLE_HS_ESTORE_FAIL; + } + + return 0; +} +#endif /* MYNEWT_VAL(BLE_GATT_CACHING) */ + void ble_store_config_conf_init(void) { diff --git a/nimble/host/store/config/src/ble_store_config_priv.h b/nimble/host/store/config/src/ble_store_config_priv.h index bae90e9765..c7876fb8ab 100644 --- a/nimble/host/store/config/src/ble_store_config_priv.h +++ b/nimble/host/store/config/src/ble_store_config_priv.h @@ -36,11 +36,24 @@ extern struct ble_store_value_cccd ble_store_config_cccds[MYNEWT_VAL(BLE_STORE_MAX_CCCDS)]; extern int ble_store_config_num_cccds; +#if MYNEWT_VAL(BLE_GATT_CACHING) +extern struct ble_store_value_csfc + ble_store_config_csfcs[MYNEWT_VAL(BLE_STORE_MAX_BONDS)]; +extern int ble_store_config_num_csfcs; + +extern struct ble_store_value_db_hash ble_store_config_db_hash; +extern int ble_store_config_db_hash_present; +#endif + #if MYNEWT_VAL(BLE_STORE_CONFIG_PERSIST) int ble_store_config_persist_our_secs(void); int ble_store_config_persist_peer_secs(void); int ble_store_config_persist_cccds(void); +#if MYNEWT_VAL(BLE_GATT_CACHING) +int ble_store_config_persist_csfcs(void); +int ble_store_config_persist_db_hash(void); +#endif void ble_store_config_conf_init(void); #else @@ -48,6 +61,10 @@ void ble_store_config_conf_init(void); static inline int ble_store_config_persist_our_secs(void) { return 0; } static inline int ble_store_config_persist_peer_secs(void) { return 0; } static inline int ble_store_config_persist_cccds(void) { return 0; } +#if MYNEWT_VAL(BLE_GATT_CACHING) +static inline int ble_store_config_persist_csfcs(void) { return 0; } +static inline int ble_store_config_persist_db_hash(void) { return 0; } +#endif static inline void ble_store_config_conf_init(void) { } #endif /* MYNEWT_VAL(BLE_STORE_CONFIG_PERSIST) */ From 53e266cea1d00aaf584532865877836d1eb60719 Mon Sep 17 00:00:00 2001 From: Gerard Marull-Paretas Date: Fri, 10 Jul 2026 15:11:38 +0200 Subject: [PATCH 6/6] nimble/host: add server-side GATT robust caching Implement the server side of GATT robust caching (Vol 3, Part G, 2.5.2.1) on top of the Database Hash characteristic, enabled by the BLE_GATT_CACHING syscfg setting. The structure mirrors Zephyr's PTS-validated implementation. Each connection tracks a change-aware state in ble_gatts_conn. A change-unaware client that enabled Robust Caching in its Client Supported Features receives an ATT Database Out Of Sync error (0x12) once on the fixed bearer, and its commands are ignored; the error is gated centrally in ble_att_rx_extended() before dispatch. Read By Type requests over the full handle range, or for Include / Characteristic declarations, are exempt so the client can read the Database Hash and rediscover. A client becomes change-aware again when it reads the Database Hash and sends another request, confirms a Service Changed indication (only when using just the fixed ATT bearer, per spec), or sends another request after the error (fixed bearer only). Writing Client Supported Features also marks the client change-aware. Per-bearer tracking of the error on enhanced bearers is not maintained: change-unaware clients get the error on every EATT request until they revalidate via the hash. For bonded peers, the features and change-aware state persist through the CSFC store record: stored when bonding is established and on every state transition, restored when encryption with a bonded peer is re-established. State is not restored before encryption; before that, a reconnected bonded peer is served normally. On ble_gatts_start() the database hash is compared against the persisted copy and, on mismatch (e.g. after a firmware update), all persisted peers are marked change-unaware. Service visibility changes mark all peers, connected and persisted, change-unaware at runtime. Signed-off-by: Gerard Marull-Paretas --- nimble/host/include/host/ble_gatt.h | 3 + nimble/host/services/gatt/src/ble_svc_gatt.c | 2 + nimble/host/src/ble_att.c | 18 + nimble/host/src/ble_eatt.c | 6 + nimble/host/src/ble_eatt_priv.h | 3 + nimble/host/src/ble_gatt_priv.h | 31 ++ nimble/host/src/ble_gatts.c | 33 ++ nimble/host/src/ble_gatts_caching.c | 468 +++++++++++++++++++ 8 files changed, 564 insertions(+) create mode 100644 nimble/host/src/ble_gatts_caching.c diff --git a/nimble/host/include/host/ble_gatt.h b/nimble/host/include/host/ble_gatt.h index c18fcd309a..a534346b61 100644 --- a/nimble/host/include/host/ble_gatt.h +++ b/nimble/host/include/host/ble_gatt.h @@ -62,6 +62,9 @@ struct ble_hs_cfg; /** GATT service 16-bit UUID. */ #define BLE_GATT_SVC_UUID16 0x1801 +/** GATT Service Changed characteristic 16-bit UUID. */ +#define BLE_GATT_CHR_SVC_CHANGED_UUID16 0x2a05 + /** GATT Client Characteristic Configuration descriptor 16-bit UUID. */ #define BLE_GATT_DSC_CLT_CFG_UUID16 0x2902 diff --git a/nimble/host/services/gatt/src/ble_svc_gatt.c b/nimble/host/services/gatt/src/ble_svc_gatt.c index 15b26bdd11..713c94761f 100644 --- a/nimble/host/services/gatt/src/ble_svc_gatt.c +++ b/nimble/host/services/gatt/src/ble_svc_gatt.c @@ -157,6 +157,8 @@ ble_svc_gatt_db_hash_access(uint16_t conn_handle, uint16_t attr_handle, return BLE_ATT_ERR_INSUFFICIENT_RES; } + ble_gatts_caching_hash_read(conn_handle); + return 0; } #endif diff --git a/nimble/host/src/ble_att.c b/nimble/host/src/ble_att.c index 8bde29b2ac..050ed9e6bd 100644 --- a/nimble/host/src/ble_att.c +++ b/nimble/host/src/ble_att.c @@ -552,6 +552,24 @@ ble_att_rx_extended(uint16_t conn_handle, uint16_t cid, struct os_mbuf **om) /* Strip L2CAP ATT header from the front of the mbuf. */ os_mbuf_adj(*om, 1); +#if MYNEWT_VAL(BLE_GATT_CACHING) + switch (ble_gatts_caching_rx_gate(conn_handle, cid, op, *om)) { + case BLE_GATTS_CACHING_GATE_PASS: + break; + + case BLE_GATTS_CACHING_GATE_ERROR: + /* Reuse the request buffer for the error response. */ + os_mbuf_adj(*om, OS_MBUF_PKTLEN(*om)); + ble_att_svr_tx_error_rsp(conn_handle, cid, *om, op, 0, + BLE_ATT_ERR_DATABASE_OUT_OF_SYNC); + *om = NULL; + return 0; + + case BLE_GATTS_CACHING_GATE_DROP: + return 0; + } +#endif + rc = entry->bde_fn(conn_handle, cid, om); if (rc != 0) { if (rc == BLE_HS_ENOTSUP) { diff --git a/nimble/host/src/ble_eatt.c b/nimble/host/src/ble_eatt.c index 047a087641..cf138dfb68 100644 --- a/nimble/host/src/ble_eatt.c +++ b/nimble/host/src/ble_eatt.c @@ -105,6 +105,12 @@ ble_eatt_find_by_conn_handle(uint16_t conn_handle) return NULL; } +bool +ble_eatt_has_chan(uint16_t conn_handle) +{ + return ble_eatt_find_by_conn_handle(conn_handle) != NULL; +} + static struct ble_eatt * ble_eatt_find_by_conn_handle_and_busy_op(uint16_t conn_handle, uint8_t op) { diff --git a/nimble/host/src/ble_eatt_priv.h b/nimble/host/src/ble_eatt_priv.h index d3c9877672..0ea745b65d 100644 --- a/nimble/host/src/ble_eatt_priv.h +++ b/nimble/host/src/ble_eatt_priv.h @@ -17,6 +17,8 @@ * under the License. */ +#include + #include "syscfg/syscfg.h" #include "os/os_mbuf.h" #include "host/ble_l2cap.h" @@ -36,6 +38,7 @@ void ble_eatt_init(ble_eatt_att_rx_fn att_rx_fn); uint16_t ble_eatt_get_available_chan_cid(uint16_t conn_handle, uint8_t op); void ble_eatt_release_chan(uint16_t conn_handle, uint8_t op); int ble_eatt_tx(uint16_t conn_handle, uint16_t cid, struct os_mbuf *txom); +bool ble_eatt_has_chan(uint16_t conn_handle); #else static inline void ble_eatt_init(ble_eatt_att_rx_fn att_rx_fn) diff --git a/nimble/host/src/ble_gatt_priv.h b/nimble/host/src/ble_gatt_priv.h index 9a622788b8..e871d1ffc3 100644 --- a/nimble/host/src/ble_gatt_priv.h +++ b/nimble/host/src/ble_gatt_priv.h @@ -95,8 +95,16 @@ extern STATS_SECT_DECL(ble_gatts_stats) ble_gatts_stats; */ #define BLE_GATT_CHR_CLI_SUP_FEAT_MASK 7 +/* Robust Caching bit in the first Client Supported Features octet. */ +#define BLE_GATT_CHR_CLI_SUP_FEAT_ROBUST_CACHING 0x01 + typedef uint8_t ble_gatts_conn_flags; +/* GATT caching state flags (Vol 3, Part G, 2.5.2.1 Robust Caching). */ +#define BLE_GATTS_CONN_F_CHANGE_AWARE 0x01 +#define BLE_GATTS_CONN_F_DB_HASH_READ 0x02 +#define BLE_GATTS_CONN_F_OUT_OF_SYNC_SENT 0x04 + struct ble_gatts_conn { struct ble_gatts_clt_cfg *clt_cfgs; int num_clt_cfgs; @@ -110,8 +118,31 @@ struct ble_gatts_conn { * (Vol. 3, Part G, 7.2) */ uint8_t peer_cl_sup_feat[BLE_GATT_CHR_CLI_SUP_FEAT_SZ]; + +#if MYNEWT_VAL(BLE_GATT_CACHING) + /** GATT caching state; BLE_GATTS_CONN_F_[...] flags. */ + uint8_t chg_aware_flags; +#endif }; +#if MYNEWT_VAL(BLE_GATT_CACHING) +/* Results of ble_gatts_caching_rx_gate(). */ +#define BLE_GATTS_CACHING_GATE_PASS 0 +#define BLE_GATTS_CACHING_GATE_ERROR 1 +#define BLE_GATTS_CACHING_GATE_DROP 2 + +int ble_gatts_caching_rx_gate(uint16_t conn_handle, uint16_t cid, uint8_t op, + struct os_mbuf *om); +void ble_gatts_caching_hash_read(uint16_t conn_handle); +void ble_gatts_caching_cl_sup_feat_updated(uint16_t conn_handle); +void ble_gatts_caching_indicate_ack(uint16_t conn_handle, + uint16_t chr_val_handle); +void ble_gatts_caching_bonding_established(uint16_t conn_handle); +void ble_gatts_caching_bonding_restored(uint16_t conn_handle); +void ble_gatts_caching_start(void); +void ble_gatts_caching_db_changed(void); +#endif + /*** @client. */ int ble_gattc_locked_by_cur_task(void); diff --git a/nimble/host/src/ble_gatts.c b/nimble/host/src/ble_gatts.c index ac5576f6dc..f97e49213e 100644 --- a/nimble/host/src/ble_gatts.c +++ b/nimble/host/src/ble_gatts.c @@ -1395,6 +1395,13 @@ ble_gatts_start(void) } ble_hs_unlock(); + +#if MYNEWT_VAL(BLE_GATT_CACHING) + if (rc == 0) { + ble_gatts_caching_start(); + } +#endif + return rc; } @@ -1423,6 +1430,10 @@ ble_gatts_conn_init(struct ble_gatts_conn *gatts_conn) gatts_conn->num_clt_cfgs = 0; } +#if MYNEWT_VAL(BLE_GATT_CACHING) + gatts_conn->chg_aware_flags = 0; +#endif + return 0; } @@ -1600,6 +1611,10 @@ ble_gatts_rx_indicate_ack(uint16_t conn_handle, uint16_t chr_val_handle) } } +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_gatts_caching_indicate_ack(conn_handle, chr_val_handle); +#endif + return 0; } @@ -1782,6 +1797,13 @@ ble_gatts_peer_cl_sup_feat_update(uint16_t conn_handle, struct os_mbuf *om) done: ble_hs_unlock(); + +#if MYNEWT_VAL(BLE_GATT_CACHING) + if (rc == 0) { + ble_gatts_caching_cl_sup_feat_updated(conn_handle); + } +#endif + return rc; } @@ -1910,6 +1932,10 @@ ble_gatts_bonding_established(uint16_t conn_handle) } ble_hs_unlock(); + +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_gatts_caching_bonding_established(conn_handle); +#endif } /** @@ -1930,6 +1956,10 @@ ble_gatts_bonding_restored(uint16_t conn_handle) uint8_t att_op; int rc; +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_gatts_caching_bonding_restored(conn_handle); +#endif + ble_hs_lock(); conn = ble_hs_conn_find(conn_handle); @@ -2199,6 +2229,9 @@ ble_gatts_svc_set_visibility(uint16_t handle, int visible) } else { ble_att_svr_hide_range(entry->handle, entry->end_group_handle); } +#if MYNEWT_VAL(BLE_GATT_CACHING) + ble_gatts_caching_db_changed(); +#endif return 0; } } diff --git a/nimble/host/src/ble_gatts_caching.c b/nimble/host/src/ble_gatts_caching.c new file mode 100644 index 0000000000..dad9b02809 --- /dev/null +++ b/nimble/host/src/ble_gatts_caching.c @@ -0,0 +1,468 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Server-side GATT robust caching (Vol 3, Part G, 2.5.2.1). + * + * Tracks a change-aware / change-unaware state for each client. A + * change-unaware client that enabled Robust Caching in its Client Supported + * Features receives a Database Out Of Sync error (0x12) on its first ATT + * request after the database changed, and becomes change-aware again by + * reading the Database Hash characteristic, confirming a Service Changed + * indication, or sending another request after the error. For bonded peers + * the Client Supported Features value and the change-aware state persist via + * the BLE_STORE_OBJ_TYPE_CSFC store record. + */ + +#include "syscfg/syscfg.h" + +#if MYNEWT_VAL(BLE_GATT_CACHING) + +#include +#include +#include "os/endian.h" +#include "host/ble_store.h" +#include "ble_hs_priv.h" +#if MYNEWT_VAL(BLE_EATT_CHAN_NUM) > 0 +#include "ble_eatt_priv.h" +#endif + +/* The stored CSFC record must be able to hold the full RAM value. */ +_Static_assert(BLE_STORE_CSFC_SZ >= BLE_GATT_CHR_CLI_SUP_FEAT_SZ, + "CSFC store record too small"); + +static bool +ble_gatts_caching_fixed_chan_only(uint16_t conn_handle) +{ +#if MYNEWT_VAL(BLE_EATT_CHAN_NUM) > 0 + return !ble_eatt_has_chan(conn_handle); +#else + (void)conn_handle; + return true; +#endif +} + +static bool +ble_gatts_caching_robust_unaware(const struct ble_gatts_conn *gatt_srv) +{ + return (gatt_srv->peer_cl_sup_feat[0] & + BLE_GATT_CHR_CLI_SUP_FEAT_ROBUST_CACHING) && + !(gatt_srv->chg_aware_flags & BLE_GATTS_CONN_F_CHANGE_AWARE); +} + +/** + * Persists the peer's Client Supported Features and change-aware state. + * Only bonded peers are persisted. + */ +static void +ble_gatts_caching_persist(uint16_t conn_handle) +{ + struct ble_store_value_csfc value; + struct ble_hs_conn *conn; + + memset(&value, 0, sizeof value); + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn == NULL || !conn->bhc_sec_state.bonded) { + ble_hs_unlock(); + return; + } + + value.peer_addr = conn->bhc_peer_addr; + value.peer_addr.type = + ble_hs_misc_peer_addr_type_to_id(conn->bhc_peer_addr.type); + memcpy(value.csfc, conn->bhc_gatt_svr.peer_cl_sup_feat, + BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + value.change_aware = !!(conn->bhc_gatt_svr.chg_aware_flags & + BLE_GATTS_CONN_F_CHANGE_AWARE); + ble_hs_unlock(); + + ble_store_write_csfc(&value); +} + +/** + * Checks the Read By Type Request exceptions: a change-unaware client is + * still served when reading over the full handle range (e.g. the Database + * Hash characteristic) or when reading Include / Characteristic declarations. + * + * @param om The request payload, with the opcode stripped. + */ +static bool +ble_gatts_caching_read_type_exception(struct os_mbuf *om) +{ + uint8_t buf[4]; + uint16_t pktlen; + uint16_t uuid16; + + pktlen = OS_MBUF_PKTLEN(om); + if (pktlen != BLE_ATT_READ_TYPE_REQ_SZ_16 - 1 && + pktlen != BLE_ATT_READ_TYPE_REQ_SZ_128 - 1) { + /* Malformed; let the request handler reject it. */ + return true; + } + + if (os_mbuf_copydata(om, 0, 4, buf) != 0) { + return true; + } + + if (get_le16(buf) == 0x0001 && get_le16(buf + 2) == 0xffff) { + return true; + } + + if (pktlen == BLE_ATT_READ_TYPE_REQ_SZ_16 - 1) { + if (os_mbuf_copydata(om, 4, 2, buf) != 0) { + return true; + } + + uuid16 = get_le16(buf); + if (uuid16 == BLE_ATT_UUID_INCLUDE || + uuid16 == BLE_ATT_UUID_CHARACTERISTIC) { + return true; + } + } + + return false; +} + +/** + * Gates an incoming ATT PDU on the peer's change-aware state. Called before + * the PDU is dispatched to its handler; the opcode has been stripped from om. + */ +int +ble_gatts_caching_rx_gate(uint16_t conn_handle, uint16_t cid, uint8_t op, + struct os_mbuf *om) +{ + struct ble_gatts_conn *gatt_srv; + struct ble_hs_conn *conn; + bool persist; + bool is_req; + int res; + + switch (op) { + case BLE_ATT_OP_READ_TYPE_REQ: + case BLE_ATT_OP_READ_REQ: + case BLE_ATT_OP_READ_BLOB_REQ: + case BLE_ATT_OP_READ_MULT_REQ: + case BLE_ATT_OP_READ_MULT_VAR_REQ: + case BLE_ATT_OP_WRITE_REQ: + case BLE_ATT_OP_PREP_WRITE_REQ: + is_req = true; + break; + + case BLE_ATT_OP_WRITE_CMD: + is_req = false; + break; + + default: + return BLE_GATTS_CACHING_GATE_PASS; + } + + res = BLE_GATTS_CACHING_GATE_PASS; + persist = false; + + ble_hs_lock(); + + conn = ble_hs_conn_find(conn_handle); + if (conn == NULL) { + goto done; + } + gatt_srv = &conn->bhc_gatt_svr; + + if (!ble_gatts_caching_robust_unaware(gatt_srv)) { + goto done; + } + + /* A change-unaware client's commands are ignored. */ + if (!is_req) { + res = BLE_GATTS_CACHING_GATE_DROP; + goto done; + } + + /* The client becomes change-aware on the request following a Database + * Hash read or, when using only the fixed ATT bearer, following a + * Database Out Of Sync error. + */ + if ((gatt_srv->chg_aware_flags & BLE_GATTS_CONN_F_DB_HASH_READ) || + ((gatt_srv->chg_aware_flags & BLE_GATTS_CONN_F_OUT_OF_SYNC_SENT) && + ble_gatts_caching_fixed_chan_only(conn_handle))) { + gatt_srv->chg_aware_flags = BLE_GATTS_CONN_F_CHANGE_AWARE; + persist = conn->bhc_sec_state.bonded; + goto done; + } + + if (op == BLE_ATT_OP_READ_TYPE_REQ && + ble_gatts_caching_read_type_exception(om)) { + goto done; + } + + if (cid == BLE_L2CAP_CID_ATT) { + if (!(gatt_srv->chg_aware_flags & BLE_GATTS_CONN_F_OUT_OF_SYNC_SENT)) { + gatt_srv->chg_aware_flags |= BLE_GATTS_CONN_F_OUT_OF_SYNC_SENT; + res = BLE_GATTS_CACHING_GATE_ERROR; + } else { + res = BLE_GATTS_CACHING_GATE_DROP; + } + } else { + /* Enhanced bearer; the error-then-request transition does not apply + * (Vol 3, Part G, 2.5.2.1), so respond with the error every time. + */ + res = BLE_GATTS_CACHING_GATE_ERROR; + } + +done: + ble_hs_unlock(); + + if (persist) { + ble_gatts_caching_persist(conn_handle); + } + + return res; +} + +/** + * Called when a peer reads the Database Hash characteristic. The + * change-aware transition completes on the peer's next ATT request. + */ +void +ble_gatts_caching_hash_read(uint16_t conn_handle) +{ + struct ble_hs_conn *conn; + + if (conn_handle == BLE_HS_CONN_HANDLE_NONE) { + return; + } + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL && + ble_gatts_caching_robust_unaware(&conn->bhc_gatt_svr)) { + conn->bhc_gatt_svr.chg_aware_flags |= BLE_GATTS_CONN_F_DB_HASH_READ; + } + ble_hs_unlock(); +} + +/** + * Called after a peer successfully wrote its Client Supported Features. A + * client writing its supported features is considered change-aware. + */ +void +ble_gatts_caching_cl_sup_feat_updated(uint16_t conn_handle) +{ + struct ble_hs_conn *conn; + bool persist; + + persist = false; + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL) { + conn->bhc_gatt_svr.chg_aware_flags = BLE_GATTS_CONN_F_CHANGE_AWARE; + persist = conn->bhc_sec_state.bonded; + } + ble_hs_unlock(); + + if (persist) { + ble_gatts_caching_persist(conn_handle); + } +} + +/** + * Called when a peer confirms an indication. Confirming a Service Changed + * indication makes a client using only the fixed ATT bearer change-aware. + */ +void +ble_gatts_caching_indicate_ack(uint16_t conn_handle, uint16_t chr_val_handle) +{ + struct ble_att_svr_entry *entry; + struct ble_hs_conn *conn; + bool persist; + + entry = ble_att_svr_find_by_handle(chr_val_handle); + if (entry == NULL || entry->ha_uuid->type != BLE_UUID_TYPE_16 || + BLE_UUID16(entry->ha_uuid)->value != + BLE_GATT_CHR_SVC_CHANGED_UUID16) { + return; + } + + if (!ble_gatts_caching_fixed_chan_only(conn_handle)) { + return; + } + + persist = false; + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL && + ble_gatts_caching_robust_unaware(&conn->bhc_gatt_svr)) { + conn->bhc_gatt_svr.chg_aware_flags = BLE_GATTS_CONN_F_CHANGE_AWARE; + persist = conn->bhc_sec_state.bonded; + } + ble_hs_unlock(); + + if (persist) { + ble_gatts_caching_persist(conn_handle); + } +} + +void +ble_gatts_caching_bonding_established(uint16_t conn_handle) +{ + struct ble_hs_conn *conn; + bool persist; + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + persist = conn != NULL && conn->bhc_gatt_svr.peer_cl_sup_feat[0] != 0; + ble_hs_unlock(); + + if (persist) { + ble_gatts_caching_persist(conn_handle); + } +} + +void +ble_gatts_caching_bonding_restored(uint16_t conn_handle) +{ + struct ble_store_value_csfc value; + struct ble_store_key_csfc key; + struct ble_hs_conn *conn; + int rc; + + memset(&key, 0, sizeof key); + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn == NULL) { + ble_hs_unlock(); + return; + } + key.peer_addr = conn->bhc_peer_addr; + key.peer_addr.type = + ble_hs_misc_peer_addr_type_to_id(conn->bhc_peer_addr.type); + ble_hs_unlock(); + + rc = ble_store_read_csfc(&key, &value); + if (rc != 0) { + return; + } + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL) { + if (conn->bhc_gatt_svr.peer_cl_sup_feat[0] == 0) { + /* The peer has not written its features on this connection; + * adopt the persisted change-aware state. + */ + if (value.change_aware) { + conn->bhc_gatt_svr.chg_aware_flags = + BLE_GATTS_CONN_F_CHANGE_AWARE; + } else { + conn->bhc_gatt_svr.chg_aware_flags = 0; + } + } + conn->bhc_gatt_svr.peer_cl_sup_feat[0] |= value.csfc[0]; + } + ble_hs_unlock(); +} + +/** + * Marks every persisted peer as change-unaware. + */ +static void +ble_gatts_caching_set_unaware_persisted(void) +{ + struct ble_store_value_csfc value; + struct ble_store_key_csfc key; + int rc; + + memset(&key, 0, sizeof key); + key.peer_addr = *BLE_ADDR_ANY; + + for (key.idx = 0; ; key.idx++) { + rc = ble_store_read_csfc(&key, &value); + if (rc != 0) { + break; + } + + if (value.change_aware) { + value.change_aware = 0; + ble_store_write_csfc(&value); + } + } +} + +/** + * Marks every peer, connected or persisted, as change-unaware. Called when + * the database changes at runtime (e.g. service visibility change). + */ +void +ble_gatts_caching_db_changed(void) +{ + struct ble_hs_conn *conn; + int i; + + for (i = 0; ; i++) { + ble_hs_lock(); + conn = ble_hs_conn_find_by_idx(i); + if (conn != NULL) { + conn->bhc_gatt_svr.chg_aware_flags = 0; + } + ble_hs_unlock(); + + if (conn == NULL) { + break; + } + } + + ble_gatts_caching_set_unaware_persisted(); +} + +/** + * Compares the database hash against the persisted one. If the database + * changed (e.g. across a firmware update), every persisted peer becomes + * change-unaware and the new hash is persisted. Called when the GATT server + * starts. + */ +void +ble_gatts_caching_start(void) +{ + struct ble_store_value_db_hash stored; + struct ble_store_value_db_hash cur; + int rc; + + rc = ble_gatts_calculate_hash(cur.hash); + if (rc != 0) { + return; + } + + rc = ble_store_read_db_hash(&stored); + if (rc == 0 && memcmp(stored.hash, cur.hash, sizeof cur.hash) == 0) { + return; + } + + if (rc == 0) { + /* The database changed since the hash was last stored. */ + ble_gatts_caching_set_unaware_persisted(); + } + + ble_store_write_db_hash(&cur); +} + +#endif /* MYNEWT_VAL(BLE_GATT_CACHING) */