From bddcafaa298665fd128ca2ae0007e2349dcf5950 Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Tue, 26 Aug 2025 11:19:21 +0200 Subject: [PATCH 1/7] nimble/host: Add support for new type of object in the storage. Add support for BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT. This is used for storing values of Client Supported Features for clients with a trusted relationship. --- nimble/host/include/host/ble_store.h | 113 ++++++++++++++- nimble/host/src/ble_store.c | 52 +++++++ nimble/host/src/ble_store_util.c | 9 ++ .../host/store/config/src/ble_store_config.c | 133 ++++++++++++++++++ .../store/config/src/ble_store_config_conf.c | 34 +++++ .../store/config/src/ble_store_config_priv.h | 9 ++ 6 files changed, 347 insertions(+), 3 deletions(-) diff --git a/nimble/host/include/host/ble_store.h b/nimble/host/include/host/ble_store.h index 83b1c4f15f..1a8a4dccbb 100644 --- a/nimble/host/include/host/ble_store.h +++ b/nimble/host/include/host/ble_store.h @@ -29,6 +29,7 @@ #include #include "nimble/ble.h" +#include "host/ble_gatt.h" #ifdef __cplusplus extern "C" { @@ -40,13 +41,16 @@ extern "C" { * @{ */ /** Object type: Our security material. */ -#define BLE_STORE_OBJ_TYPE_OUR_SEC 1 +#define BLE_STORE_OBJ_TYPE_OUR_SEC 1 /** Object type: Peer security material. */ -#define BLE_STORE_OBJ_TYPE_PEER_SEC 2 +#define BLE_STORE_OBJ_TYPE_PEER_SEC 2 /** Object type: Client Characteristic Configuration Descriptor. */ -#define BLE_STORE_OBJ_TYPE_CCCD 3 +#define BLE_STORE_OBJ_TYPE_CCCD 3 + +/** Object type: Peer Client Supported Features. */ +#define BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT 4 /** @} */ @@ -154,6 +158,33 @@ struct ble_store_value_cccd { unsigned value_changed:1; }; +/** + * Used as a key for lookups of stored client supported features of specific + * peer. This struct corresponds to the BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT + * store object type. + */ +struct ble_store_key_cl_sup_feat { + /** + * 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 of specific peer. This struct + * corresponds to the BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT store object type. + */ +struct ble_store_value_cl_sup_feat { + /** The peer address associated with the stored supported features. */ + ble_addr_t peer_addr; + /** Client supported features of a specific peer. */ + uint8_t peer_cl_sup_feat[BLE_GATT_CHR_CLI_SUP_FEAT_SZ]; +}; + /** * 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 +194,8 @@ 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 Peer Client Supported Features store lookpus. */ + struct ble_store_key_cl_sup_feat feat; }; /** @@ -174,6 +207,8 @@ 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. */ + struct ble_store_value_cl_sup_feat feat; }; /** Represents an event associated with the BLE Store. */ @@ -556,6 +591,60 @@ int ble_store_write_cccd(const struct ble_store_value_cccd *value); */ int ble_store_delete_cccd(const struct ble_store_key_cccd *key); +/** + * @brief Reads Client Supported Features value from a storage + * + * This function reads client supported features value from a storage based + * on the provied key and stores the retrieved value in the specified output + * structure. + * + * @param key A pointer to a 'ble_store_key_cl_sup_feat' + * struct representing the key to identify + * Client Supported Features value to be read. + * @param out_value A pointer to a 'ble_store_value_cl_sup_feat' + * struct to store the Client Supported + * Features value read from a storage + * + * @return 0 if the Client Supported Features values was + * successfully read and stored in the + * 'out_value' structure; + * Non-zero on error + */ +int ble_store_read_peer_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key, + struct ble_store_value_cl_sup_feat *out_value); + +/** + * @brief Writes a Client Supported Features value to a storage. + * + * This function writes a Client Supported Features value to a storage based on + * the provided value + * + * @param value A pointer to a 'ble_store_value_cl_sup_feat' + * structure representing the Client Supported + * Features value to be written to a storage. + * + * @return 0 if the value was successfully written to + * a storage; + * Non-zero on error. + */ +int ble_store_write_peer_cl_sup_feat(const struct ble_store_value_cl_sup_feat *value); + +/** + * @brief Deletes a Client Supported Features value from a storage. + * + * This function deletes a Client Supported Features value from a storage based + * on the provided key. + * + * @param key A pointer to a 'ble_store_key_cl_sup_feat' + * structure identifying the Client Supported + * Features value to be deleted from + * a storage. + * + * @return 0 if the Client Supported Features value was + * successfully written to a storage; + * Non-zero on error. + */ +int ble_store_delete_peer_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key); /** * @brief Generates a storage key for a security material entry from its value. @@ -587,6 +676,24 @@ 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); +/** + * @brief Generates a storage key for a Client Supported Features entry from + * its value + * + * This function generates a storage key for a Client Supported Features value + * entry based on the provided value. + * + * @param out_key A pointer to a 'ble_store_key_cl_sup_feat' + * structure where the generated key will be + * stored. + * @param value A pointer to a 'ble_store_value_cl_sup_feat' + * structure containing the Client Supported + * Features value from which the key will be + * generated. + */ +void ble_store_key_from_value_peer_cl_sup_feat( + struct ble_store_key_cl_sup_feat *out_key, + const struct ble_store_value_cl_sup_feat *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..e0070ad2b1 100644 --- a/nimble/host/src/ble_store.c +++ b/nimble/host/src/ble_store.c @@ -249,6 +249,42 @@ ble_store_write_peer_sec(const struct ble_store_value_sec *value_sec) return 0; } +int +ble_store_read_peer_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key, + struct ble_store_value_cl_sup_feat *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_PEER_CL_SUP_FEAT, store_key, store_value); + return rc; +} + +int +ble_store_write_peer_cl_sup_feat(const struct ble_store_value_cl_sup_feat *value) +{ + union ble_store_value *store_value; + int rc; + + store_value = (void *)value; + rc = ble_store_write(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, store_value); + return rc; +} + +int +ble_store_delete_peer_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key) +{ + union ble_store_key *store_key; + int rc; + + store_key = (void *)key; + rc = ble_store_delete(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, store_key); + return rc; +} + int ble_store_read_cccd(const struct ble_store_key_cccd *key, struct ble_store_value_cccd *out_value) @@ -302,6 +338,14 @@ ble_store_key_from_value_sec(struct ble_store_key_sec *out_key, out_key->idx = 0; } +void +ble_store_key_from_value_peer_cl_sup_feat(struct ble_store_key_cl_sup_feat *out_key, + const struct ble_store_value_cl_sup_feat *value) +{ + out_key->peer_addr = value->peer_addr; + out_key->idx = 0; +} + void ble_store_key_from_value(int obj_type, union ble_store_key *out_key, @@ -317,6 +361,9 @@ 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_PEER_CL_SUP_FEAT: + ble_store_key_from_value_peer_cl_sup_feat(&out_key->feat, &value->feat); + default: BLE_HS_DBG_ASSERT(0); break; @@ -346,6 +393,10 @@ ble_store_iterate(int obj_type, key.cccd.peer_addr = *BLE_ADDR_ANY; pidx = &key.cccd.idx; break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + key.feat.peer_addr = *BLE_ADDR_ANY; + pidx = &key.feat.idx; + break; default: BLE_HS_DBG_ASSERT(0); return BLE_HS_EINVAL; @@ -390,6 +441,7 @@ ble_store_clear(void) BLE_STORE_OBJ_TYPE_OUR_SEC, BLE_STORE_OBJ_TYPE_PEER_SEC, BLE_STORE_OBJ_TYPE_CCCD, + BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, }; union ble_store_key key; int obj_type; diff --git a/nimble/host/src/ble_store_util.c b/nimble/host/src/ble_store_util.c index 6dcbca25a1..7c04868331 100644 --- a/nimble/host/src/ble_store_util.c +++ b/nimble/host/src/ble_store_util.c @@ -109,6 +109,14 @@ ble_store_util_delete_peer(const ble_addr_t *peer_id_addr) return rc; } + memset(&key, 0, sizeof key); + key.feat.peer_addr = *peer_id_addr; + + rc = ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, &key); + if (rc != 0) { + return rc; + } + return 0; } @@ -192,6 +200,7 @@ ble_store_util_status_rr(struct ble_store_status_event *event, void *arg) switch (event->overflow.obj_type) { case BLE_STORE_OBJ_TYPE_OUR_SEC: case BLE_STORE_OBJ_TYPE_PEER_SEC: + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: return ble_gap_unpair_oldest_peer(); case BLE_STORE_OBJ_TYPE_CCCD: /* Try unpairing oldest peer except current peer */ diff --git a/nimble/host/store/config/src/ble_store_config.c b/nimble/host/store/config/src/ble_store_config.c index 741c73a5f5..80cd8ac20d 100644 --- a/nimble/host/store/config/src/ble_store_config.c +++ b/nimble/host/store/config/src/ble_store_config.c @@ -47,6 +47,12 @@ struct ble_store_value_cccd int ble_store_config_num_cccds; +#if MYNEWT_VAL(BLE_STORE_MAX_CCCDS) +struct ble_store_value_cl_sup_feat ble_store_config_feats[MYNEWT_VAL(BLE_STORE_MAX_BONDS)]; +#endif + +int ble_store_config_num_feats; + /***************************************************************************** * $sec * *****************************************************************************/ @@ -447,6 +453,120 @@ ble_store_config_write_cccd(const struct ble_store_value_cccd *value_cccd) #endif } +/***************************************************************************** + * $cl_sup_feat * + *****************************************************************************/ + +#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) +static int +ble_store_config_find_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key) +{ + struct ble_store_value_cl_sup_feat *feat; + int skipped; + int i; + + skipped = 0; + for (i = 0; i < ble_store_config_num_feats; i++) { + feat = ble_store_config_feats + i; + + if (ble_addr_cmp(&key->peer_addr, BLE_ADDR_ANY)) { + if (ble_addr_cmp(&feat->peer_addr, &key->peer_addr)) { + continue; + } + } + + if (key->idx > skipped) { + skipped++; + continue; + } + + return i; + } + return -1; +} +#endif + +static int +ble_store_config_delete_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key_feat) +{ +#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) + int idx; + int rc; + + idx = ble_store_config_find_cl_sup_feat(key_feat); + if (idx < 0) { + return BLE_HS_ENOENT; + } + + assert(ble_store_config_num_feats < ARRAY_SIZE(ble_store_config_feats)); + rc = ble_store_config_delete_obj(ble_store_config_feats, + sizeof *ble_store_config_feats, idx, + &ble_store_config_num_feats); + if (rc != 0) { + return rc; + } + + rc = ble_store_config_persist_feats(); + if (rc != 0) { + return rc; + } + return 0; +#else + return BLE_HS_ENOENT; +#endif +} + +static int +ble_store_config_read_cl_sup_feat(const struct ble_store_key_cl_sup_feat *key_feat, + struct ble_store_value_cl_sup_feat *value_feat) +{ +#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) + int idx; + + idx = ble_store_config_find_cl_sup_feat(key_feat); + if (idx == -1) { + return BLE_HS_ENOENT; + } + + *value_feat = ble_store_config_feats[idx]; + return 0; +#else + return BLE_HS_ENOENT; +#endif +} + +static int +ble_store_config_write_cl_sup(const struct ble_store_value_cl_sup_feat *value_feat) +{ +#if MYNEWT_VAL(BLE_STORE_MAX_BONDS) + struct ble_store_key_cl_sup_feat key_feat; + int idx; + int rc; + + ble_store_key_from_value_peer_cl_sup_feat(&key_feat, value_feat); + idx = ble_store_config_find_cl_sup_feat(&key_feat); + if (idx == -1) { + if (ble_store_config_num_feats >= MYNEWT_VAL(BLE_STORE_MAX_BONDS)) { + return BLE_HS_ESTORE_CAP; + } + + idx = ble_store_config_num_feats; + ble_store_config_num_feats++; + } + + ble_store_config_feats[idx] = *value_feat; + + rc = ble_store_config_persist_feats(); + if (rc != 0) { + return rc; + } + + return 0; +#else + return BLE_HS_ENOENT; +#endif +} + /***************************************************************************** * $api * *****************************************************************************/ @@ -489,6 +609,10 @@ 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; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + rc = ble_store_config_read_cl_sup_feat(&key->feat, &value->feat); + return rc; + default: return BLE_HS_ENOTSUP; } @@ -518,6 +642,10 @@ ble_store_config_write(int obj_type, const union ble_store_value *val) rc = ble_store_config_write_cccd(&val->cccd); return rc; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + rc = ble_store_config_write_cl_sup(&val->feat); + return rc; + default: return BLE_HS_ENOTSUP; } @@ -541,6 +669,10 @@ ble_store_config_delete(int obj_type, const union ble_store_key *key) rc = ble_store_config_delete_cccd(&key->cccd); return rc; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + rc = ble_store_config_delete_cl_sup_feat(&key->feat); + return rc; + default: return BLE_HS_ENOTSUP; } @@ -560,6 +692,7 @@ 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; + ble_store_config_num_feats = 0; 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..1677d10cc7 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,12 @@ 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) +#define BLE_STORE_CONFIG_CL_SUP_FEAT_ENCODE_SZ \ + BASE64_ENCODE_SIZE(sizeof(struct ble_store_value_cl_sup_feat)) + +#define BLE_STORE_CONFIG_CL_SUP_FEAT_SET_ENCODE_SZ \ + (MYNEWT_VAL(BLE_STORE_MAX_BONDS) * BLE_STORE_CONFIG_CL_SUP_FEAT_ENCODE_SZ + 1) + static void ble_store_config_serialize_arr(const void *arr, int obj_sz, int num_objs, char *out_buf, int buf_sz) @@ -118,6 +124,11 @@ ble_store_config_conf_set(int argc, char **argv, char *val) sizeof *ble_store_config_cccds, &ble_store_config_num_cccds); return rc; + } else if (strcmp(argv[0], "feat") == 0) { + rc = ble_store_config_deserialize_arr(val, ble_store_config_feats, + sizeof *ble_store_config_feats, + &ble_store_config_num_feats); + return rc; } } return OS_ENOENT; @@ -130,6 +141,7 @@ 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]; + char feat[BLE_STORE_CONFIG_CL_SUP_FEAT_SET_ENCODE_SZ]; } buf; ble_store_config_serialize_arr(ble_store_config_our_secs, @@ -153,6 +165,11 @@ ble_store_config_conf_export(void (*func)(char *name, char *val), sizeof buf.cccd); func("ble_hs/cccd", buf.cccd); + ble_store_config_serialize_arr( + ble_store_config_feats, sizeof *ble_store_config_feats, + ble_store_config_num_feats, buf.feat, sizeof buf.feat); + func("ble_hs/feat", buf.feat); + return 0; } @@ -223,6 +240,23 @@ ble_store_config_persist_cccds(void) return 0; } +int +ble_store_config_persist_feats(void) +{ + char buf[BLE_STORE_CONFIG_CL_SUP_FEAT_SET_ENCODE_SZ]; + int rc; + + ble_store_config_serialize_arr(ble_store_config_feats, + sizeof *ble_store_config_feats, + ble_store_config_num_feats, buf, sizeof buf); + rc = conf_save_one("ble_hs/feat", buf); + if (rc != 0) { + return BLE_HS_ESTORE_FAIL; + } + + return 0; +} + 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..d86d039f11 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,15 @@ extern struct ble_store_value_cccd ble_store_config_cccds[MYNEWT_VAL(BLE_STORE_MAX_CCCDS)]; extern int ble_store_config_num_cccds; +extern struct ble_store_value_cl_sup_feat ble_store_config_feats[MYNEWT_VAL(BLE_STORE_MAX_BONDS)]; +extern int ble_store_config_num_feats; + #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); +int ble_store_config_persist_feats(void); void ble_store_config_conf_init(void); #else @@ -48,6 +52,11 @@ 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; } +static inline int +ble_store_config_persist_feats(void) +{ + return 0; +} static inline void ble_store_config_conf_init(void) { } #endif /* MYNEWT_VAL(BLE_STORE_CONFIG_PERSIST) */ From 2079c25da7151e1d6476fc5b7f0e68ceb09aca3e Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Tue, 26 Aug 2025 11:21:10 +0200 Subject: [PATCH 2/7] nimble/host: maintain Client Supported Features characteristic value This adds support for storing client supported features for clients with a trusted relationship. Fixes Host qualification test case GATT/SR/GAS/BV-04-C --- nimble/host/src/ble_gatts.c | 85 ++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/nimble/host/src/ble_gatts.c b/nimble/host/src/ble_gatts.c index 8d7897bec0..d0999f501e 100644 --- a/nimble/host/src/ble_gatts.c +++ b/nimble/host/src/ble_gatts.c @@ -1717,6 +1717,9 @@ int ble_gatts_peer_cl_sup_feat_update(uint16_t conn_handle, struct os_mbuf *om) { struct ble_hs_conn *conn; + struct ble_store_value_cl_sup_feat store_feat; + struct ble_store_key_cl_sup_feat feat_key; + struct ble_store_value_cl_sup_feat feat_val; uint8_t feat[BLE_GATT_CHR_CLI_SUP_FEAT_SZ] = {}; uint16_t len; int rc = 0; @@ -1744,8 +1747,8 @@ ble_gatts_peer_cl_sup_feat_update(uint16_t conn_handle, struct os_mbuf *om) ble_hs_lock(); conn = ble_hs_conn_find(conn_handle); if (conn == NULL) { - rc = BLE_ATT_ERR_UNLIKELY; - goto done; + ble_hs_unlock(); + return BLE_ATT_ERR_UNLIKELY; } /** @@ -1755,15 +1758,44 @@ ble_gatts_peer_cl_sup_feat_update(uint16_t conn_handle, struct os_mbuf *om) for (i = 0; i < BLE_GATT_CHR_CLI_SUP_FEAT_SZ; i++) { if ((conn->bhc_gatt_svr.peer_cl_sup_feat[i] & feat[i]) != conn->bhc_gatt_svr.peer_cl_sup_feat[i]) { - rc = BLE_ATT_ERR_VALUE_NOT_ALLOWED; - goto done; + ble_hs_unlock(); + return BLE_ATT_ERR_VALUE_NOT_ALLOWED; } } memcpy(conn->bhc_gatt_svr.peer_cl_sup_feat, feat, BLE_GATT_CHR_CLI_SUP_FEAT_SZ); -done: ble_hs_unlock(); + + if (conn->bhc_sec_state.bonded) { + feat_key.peer_addr = conn->bhc_peer_addr; + feat_key.idx = 0; + + rc = ble_store_read_peer_cl_sup_feat(&feat_key, &feat_val); + + if (rc == BLE_HS_ENOENT) { + /* Assume the values were not stored yet */ + store_feat.peer_addr = conn->bhc_peer_addr; + memcpy(store_feat.peer_cl_sup_feat, feat, BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + ble_store_write_peer_cl_sup_feat(&store_feat); + rc = 0; + } else if (rc == 0) { + /* Found cl_sup_feat for this peer, check for disabling already + * enabled features */ + for (i = 0; i < BLE_GATT_CHR_CLI_SUP_FEAT_SZ; i++) { + if ((feat_val.peer_cl_sup_feat[i] & feat[i]) != + feat_val.peer_cl_sup_feat[i]) { + return BLE_ATT_ERR_VALUE_NOT_ALLOWED; + } + } + /* The peer is only adding new features. Add it's cl_sup_feat to + * storage. */ + store_feat.peer_addr = conn->bhc_peer_addr; + memcpy(store_feat.peer_cl_sup_feat, feat, BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + ble_store_write_peer_cl_sup_feat(&store_feat); + } + } + return rc; } @@ -1856,11 +1888,13 @@ ble_gatts_tx_notifications(void) void ble_gatts_bonding_established(uint16_t conn_handle) { + struct ble_store_value_cl_sup_feat feat_value; struct ble_store_value_cccd cccd_value; struct ble_gatts_clt_cfg *clt_cfg; struct ble_gatts_conn *gatt_srv; struct ble_hs_conn *conn; int i; + int rc; ble_hs_lock(); @@ -1873,6 +1907,20 @@ ble_gatts_bonding_established(uint16_t conn_handle) ble_hs_misc_peer_addr_type_to_id(conn->bhc_peer_addr.type); gatt_srv = &conn->bhc_gatt_svr; + feat_value.peer_addr = conn->bhc_peer_addr; + feat_value.peer_addr.type = + ble_hs_misc_peer_addr_type_to_id(conn->bhc_peer_addr.type); + + memcpy(feat_value.peer_cl_sup_feat, gatt_srv->peer_cl_sup_feat, + BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + + ble_hs_unlock(); + rc = ble_store_write_peer_cl_sup_feat(&feat_value); + if (rc != 0) { + BLE_HS_LOG(ERROR, "Failed to persist client supported features, rc=%d\n", rc); + } + ble_hs_lock(); + for (i = 0; i < gatt_srv->num_clt_cfgs; ++i) { clt_cfg = &gatt_srv->clt_cfgs[i]; @@ -1898,6 +1946,7 @@ ble_gatts_bonding_established(uint16_t conn_handle) * Called when bonding has been restored via the encryption procedure. This * function: * o Restores persisted CCCD entries for the connected peer. + * o Restores persisted Client Supported Features for the connected peer. * o Sends all pending notifications to the connected peer. * o Sends up to one pending indication to the connected peer; schedules * any remaining pending indications. @@ -1907,6 +1956,8 @@ ble_gatts_bonding_restored(uint16_t conn_handle) { struct ble_store_value_cccd cccd_value; struct ble_store_key_cccd cccd_key; + struct ble_store_key_cl_sup_feat feat_key; + struct ble_store_value_cl_sup_feat feat_val; struct ble_gatts_clt_cfg *clt_cfg; struct ble_hs_conn *conn; uint8_t att_op; @@ -1924,8 +1975,32 @@ ble_gatts_bonding_restored(uint16_t conn_handle) cccd_key.chr_val_handle = 0; cccd_key.idx = 0; + feat_key.peer_addr = conn->bhc_peer_addr; + feat_key.idx = 0; + ble_hs_unlock(); + rc = ble_store_read_peer_cl_sup_feat(&feat_key, &feat_val); + if (rc != 0) { + /* Entry not found or storage error: + * Set Peer Client Supported Featured to a safe default (0x00). */ + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL) { + memset(conn->bhc_gatt_svr.peer_cl_sup_feat, 0, BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + } + ble_hs_unlock(); + } else { + /* success reading stored Client Supported Features for this peer */ + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + if (conn != NULL) { + memcpy(conn->bhc_gatt_svr.peer_cl_sup_feat, + feat_val.peer_cl_sup_feat, BLE_GATT_CHR_CLI_SUP_FEAT_SZ); + } + ble_hs_unlock(); + } + while (1) { rc = ble_store_read_cccd(&cccd_key, &cccd_value); if (rc != 0) { From 0c705ddc80eb4bf786846dde736e61032cde6b73 Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Wed, 27 Aug 2025 09:37:26 +0200 Subject: [PATCH 3/7] nimble/host: update storage unit tests Extend storage unit tests to cover new object type PEER_CL_SUP_FEAT --- nimble/host/test/src/ble_hs_test_util.c | 12 +++++ nimble/host/test/src/ble_store_test.c | 71 +++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/nimble/host/test/src/ble_hs_test_util.c b/nimble/host/test/src/ble_hs_test_util.c index a31a4ef3ea..7e2c22d8fc 100644 --- a/nimble/host/test/src/ble_hs_test_util.c +++ b/nimble/host/test/src/ble_hs_test_util.c @@ -1923,6 +1923,9 @@ ble_hs_test_util_store_read(int obj_type, const union ble_store_key *key, case BLE_STORE_OBJ_TYPE_CCCD: ble_sm_test_store_key.cccd = key->cccd; break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + ble_sm_test_store_key.feat = key->feat; + break; default: return BLE_HS_ENOTSUP; } @@ -1937,6 +1940,9 @@ ble_hs_test_util_store_read(int obj_type, const union ble_store_key *key, case BLE_STORE_OBJ_TYPE_CCCD: ble_sm_test_store_value.cccd = value->cccd; break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + ble_sm_test_store_value.feat = value->feat; + break; default: rc = BLE_HS_ENOTSUP; break; @@ -1962,6 +1968,9 @@ ble_hs_test_util_store_write(int obj_type, const union ble_store_value *value) case BLE_STORE_OBJ_TYPE_CCCD: ble_sm_test_store_value.cccd = value->cccd; break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + ble_sm_test_store_value.feat = value->feat; + break; default: rc = BLE_HS_ENOTSUP; break; @@ -1985,6 +1994,9 @@ ble_hs_test_util_store_delete(int obj_type, const union ble_store_key *key) case BLE_STORE_OBJ_TYPE_CCCD: ble_sm_test_store_key.cccd = key->cccd; break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + ble_sm_test_store_key.feat = key->feat; + break; default: return BLE_HS_ENOTSUP; } diff --git a/nimble/host/test/src/ble_store_test.c b/nimble/host/test/src/ble_store_test.c index 75d3a490f8..9939bc2e0f 100644 --- a/nimble/host/test/src/ble_store_test.c +++ b/nimble/host/test/src/ble_store_test.c @@ -45,6 +45,11 @@ ble_store_test_util_verify_peer_deleted(const ble_addr_t *addr) rc = ble_store_read(BLE_STORE_OBJ_TYPE_CCCD, &key, &value); TEST_ASSERT(rc == BLE_HS_ENOENT); + memset(&key, 0, sizeof key); + key.feat.peer_addr = *addr; + rc = ble_store_read(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, &key, &value); + TEST_ASSERT(rc == BLE_HS_ENOENT); + rc = ble_store_util_bonded_peers(addrs, &num_addrs, sizeof addrs / sizeof addrs[0]); TEST_ASSERT_FATAL(rc == 0); @@ -198,6 +203,16 @@ TEST_CASE_SELF(ble_store_test_delete_peer) .chr_val_handle = 5, }, }; + struct ble_store_value_cl_sup_feat feats[2] = { + { + .peer_addr = secs[0].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + { + .peer_addr = secs[1].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + }; union ble_store_value value; union ble_store_key key; int count; @@ -218,6 +233,11 @@ TEST_CASE_SELF(ble_store_test_delete_peer) TEST_ASSERT_FATAL(rc == 0); } + for (i = 0; i < sizeof feats / sizeof feats[0]; i++) { + rc = ble_store_write_peer_cl_sup_feat(feats + i); + TEST_ASSERT_FATAL(rc == 0); + } + /* Delete first peer. */ rc = ble_store_util_delete_peer(&secs[0].peer_addr); TEST_ASSERT_FATAL(rc == 0); @@ -254,6 +274,16 @@ TEST_CASE_SELF(ble_store_test_delete_peer) TEST_ASSERT_FATAL(rc == 0); TEST_ASSERT(memcmp(&value.cccd, cccds + 2, sizeof value.cccd) == 0); + ble_store_key_from_value_peer_cl_sup_feat(&key.feat, feats + 1); + + rc = ble_store_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, &count); + TEST_ASSERT_FATAL(rc == 0); + TEST_ASSERT(count == 1); + + rc = ble_store_read_peer_cl_sup_feat(&key.feat, &value.feat); + TEST_ASSERT_FATAL(rc == 0); + TEST_ASSERT(memcmp(&value.feat, feats + 1, sizeof value.feat) == 0); + /* Delete second peer. */ rc = ble_store_util_delete_peer(&secs[1].peer_addr); TEST_ASSERT_FATAL(rc == 0); @@ -294,6 +324,16 @@ TEST_CASE_SELF(ble_store_test_count) .chr_val_handle = 8, }, }; + struct ble_store_value_cl_sup_feat feats[2] = { + { + .peer_addr = secs[0].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + { + .peer_addr = secs[1].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + }; int count; int rc; int i; @@ -314,6 +354,10 @@ TEST_CASE_SELF(ble_store_test_count) TEST_ASSERT_FATAL(rc == 0); TEST_ASSERT(count == 0); + rc = ble_store_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, &count); + TEST_ASSERT_FATAL(rc == 0); + TEST_ASSERT(count == 0); + /* Write some test data. */ for (i = 0; i < 3; i++) { @@ -328,6 +372,10 @@ TEST_CASE_SELF(ble_store_test_count) rc = ble_store_write_cccd(cccds + i); TEST_ASSERT_FATAL(rc == 0); } + for (i = 0; i < 1; i++) { + rc = ble_store_write_peer_cl_sup_feat(feats + i); + TEST_ASSERT_FATAL(rc == 0); + } /*** Verify counts after populating store. */ rc = ble_store_util_count(BLE_STORE_OBJ_TYPE_OUR_SEC, &count); @@ -342,6 +390,10 @@ TEST_CASE_SELF(ble_store_test_count) TEST_ASSERT_FATAL(rc == 0); TEST_ASSERT(count == 1); + rc = ble_store_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT, &count); + TEST_ASSERT_FATAL(rc == 0); + TEST_ASSERT(count == 1); + ble_hs_test_util_assert_mbufs_freed(NULL); } @@ -383,6 +435,16 @@ TEST_CASE_SELF(ble_store_test_clear) .chr_val_handle = 5, }, }; + struct ble_store_value_cl_sup_feat feats[2] = { + { + .peer_addr = secs[0].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + { + .peer_addr = secs[1].peer_addr, + .peer_cl_sup_feat[0] = 0, + }, + }; int rc; int i; @@ -400,6 +462,11 @@ TEST_CASE_SELF(ble_store_test_clear) TEST_ASSERT_FATAL(rc == 0); } + for (i = 0; i < sizeof feats / sizeof feats[0]; i++) { + rc = ble_store_write_peer_cl_sup_feat(feats + i); + TEST_ASSERT_FATAL(rc == 0); + } + /* Sanity check. */ TEST_ASSERT_FATAL( ble_store_test_util_count(BLE_STORE_OBJ_TYPE_OUR_SEC) == 2); @@ -407,6 +474,8 @@ TEST_CASE_SELF(ble_store_test_clear) ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_SEC) == 2); TEST_ASSERT_FATAL( ble_store_test_util_count(BLE_STORE_OBJ_TYPE_CCCD) == 3); + TEST_ASSERT_FATAL( + ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT) == 2); /* Ensure store is empty after clear gets called. */ rc = ble_store_clear(); @@ -414,6 +483,7 @@ TEST_CASE_SELF(ble_store_test_clear) TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_OUR_SEC) == 0); TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_SEC) == 0); TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_CCCD) == 0); + TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT) == 0); /* Ensure second clear succeeds with no effect. */ rc = ble_store_clear(); @@ -421,6 +491,7 @@ TEST_CASE_SELF(ble_store_test_clear) TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_OUR_SEC) == 0); TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_SEC) == 0); TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_CCCD) == 0); + TEST_ASSERT(ble_store_test_util_count(BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT) == 0); ble_hs_test_util_assert_mbufs_freed(NULL); } From b6840f3a1c82397e02bafdea88dc410ab831ac47 Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Wed, 3 Sep 2025 10:23:31 +0200 Subject: [PATCH 4/7] nimble/host: fix code style Fix code style in ble_store.h --- nimble/host/include/host/ble_store.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nimble/host/include/host/ble_store.h b/nimble/host/include/host/ble_store.h index 1a8a4dccbb..6191abf356 100644 --- a/nimble/host/include/host/ble_store.h +++ b/nimble/host/include/host/ble_store.h @@ -41,13 +41,13 @@ extern "C" { * @{ */ /** Object type: Our security material. */ -#define BLE_STORE_OBJ_TYPE_OUR_SEC 1 +#define BLE_STORE_OBJ_TYPE_OUR_SEC 1 /** Object type: Peer security material. */ -#define BLE_STORE_OBJ_TYPE_PEER_SEC 2 +#define BLE_STORE_OBJ_TYPE_PEER_SEC 2 /** Object type: Client Characteristic Configuration Descriptor. */ -#define BLE_STORE_OBJ_TYPE_CCCD 3 +#define BLE_STORE_OBJ_TYPE_CCCD 3 /** Object type: Peer Client Supported Features. */ #define BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT 4 From f50348c7ab39d67677bfd7c99a1eeb51516bf4ed Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Fri, 24 Apr 2026 14:54:31 +0200 Subject: [PATCH 5/7] nimble/host: move BLE_GATT_CHR_CLI_SUP_FEAT_SZ to public header. Moves 'BLE_GATT_CHR_CLI_SUP_FEAT_SZ' from the private header to the public ble_gatt.h header. --- nimble/host/include/host/ble_gatt.h | 8 ++++++++ nimble/host/src/ble_gatt_priv.h | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nimble/host/include/host/ble_gatt.h b/nimble/host/include/host/ble_gatt.h index e74ba9e1a1..fd5bd2719f 100644 --- a/nimble/host/include/host/ble_gatt.h +++ b/nimble/host/include/host/ble_gatt.h @@ -201,6 +201,14 @@ struct ble_hs_cfg; /** @} */ +/** + * @brief Size of the GATT Client Supported Features characteristic. + * + */ +#define BLE_GATT_CHR_CLI_SUP_FEAT_SZ 1 + +/** @} */ + /*** @client. */ /** Represents a GATT error. */ struct ble_gatt_error { diff --git a/nimble/host/src/ble_gatt_priv.h b/nimble/host/src/ble_gatt_priv.h index 9a622788b8..d4b6f79f2f 100644 --- a/nimble/host/src/ble_gatt_priv.h +++ b/nimble/host/src/ble_gatt_priv.h @@ -88,7 +88,6 @@ extern STATS_SECT_DECL(ble_gatts_stats) ble_gatts_stats; #define BLE_GATT_CHR_DECL_SZ_16 5 #define BLE_GATT_CHR_DECL_SZ_128 19 -#define BLE_GATT_CHR_CLI_SUP_FEAT_SZ 1 /** * For now only 3 bits in first octet are defined * From 836dcfc8cfaa078394f616b605c1312859c0f209 Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Wed, 27 May 2026 12:06:25 +0200 Subject: [PATCH 6/7] nimble/host: Add unit tests for maintaining Client Supported Features Introduce a comprehensive unit test suite to validate the persistance, restoration, and specification compliance of the Client Supported Features characteristic values across peer bonding lifecycles. --- .../test/src/ble_gatts_cl_sup_feat_test.c | 335 ++++++++++++++++++ nimble/host/test/src/ble_hs_test.c | 1 + nimble/host/test/src/ble_hs_test.h | 1 + 3 files changed, 337 insertions(+) create mode 100644 nimble/host/test/src/ble_gatts_cl_sup_feat_test.c diff --git a/nimble/host/test/src/ble_gatts_cl_sup_feat_test.c b/nimble/host/test/src/ble_gatts_cl_sup_feat_test.c new file mode 100644 index 0000000000..55a0052ae6 --- /dev/null +++ b/nimble/host/test/src/ble_gatts_cl_sup_feat_test.c @@ -0,0 +1,335 @@ +/* + * 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 +#include +#include "testutil/testutil.h" +#include "nimble/ble.h" +#include "host/ble_uuid.h" +#include "ble_hs_test.h" +#include "ble_hs_test_util.h" +#include "../src/ble_gatt_priv.h" + +#define BLE_GATTS_TEST_CL_SUP_FEAT_CHR_UUID 0x1111 +#define BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ 1 + +/* Client supported features bit positions*/ +#define BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_CACHING_BIT 0x00 +#define BLE_GATT_TEST_CLI_SUP_FEAT_EATT_BIT 0x01 +#define BLE_GATT_TEST_CLI_SUP_FEAT_MULT_NTF_BIT 0x02 + +/* --- Client Supported Features masks --- */ +#define BLE_GATT_TEST_CLI_SUP_FEAT_NONE_MASK 0x00 +#define BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_CACHING_MASK 0x01 +#define BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MASK 0x02 +#define BLE_GATT_TEST_CLI_SUP_FEAT_MULT_NTF_MASK 0x04 +#define BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_EATT_MASK 0x03 +#define BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_MULT_NTF_MASK 0x05 +#define BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MULT_NTF_MASK 0x06 +#define BLE_GATT_TEST_CLI_SUP_FEAT_ALL_MASK 0x07 + +static uint8_t ble_gatts_cl_sup_feat_test_peer_addr[6] = { 1, 2, 3, 4, 5, 6 }; + +static int +ble_gatts_cl_sup_feat_test_misc_access(uint16_t conn_handle, uint16_t attr_handle, + struct ble_gatt_access_ctxt *ctxt, void *arg) +{ + uint8_t supported_feat; + int rc; + + TEST_ASSERT(conn_handle != 0xffff); + + if (ctxt->op == BLE_GATT_ACCESS_OP_READ_CHR) { + rc = ble_gatts_peer_cl_sup_feat_get(conn_handle, &supported_feat, 1); + if (rc == 0) { + rc = os_mbuf_append(ctxt->om, &supported_feat, 1); + } + return rc; + } + + if (ctxt->op == BLE_GATT_ACCESS_OP_WRITE_CHR) { + rc = ble_gatts_peer_cl_sup_feat_update(conn_handle, ctxt->om); + return rc; + } + + return BLE_ATT_ERR_UNLIKELY; +} + +static const struct ble_gatt_svc_def ble_gatts_cl_sup_feat_test_svcs[] = { + { + .type = BLE_GATT_SVC_TYPE_PRIMARY, + .uuid = BLE_UUID16_DECLARE(0x1234), + .characteristics = + (struct ble_gatt_chr_def[]){ + { + .uuid = BLE_UUID16_DECLARE(BLE_GATTS_TEST_CL_SUP_FEAT_CHR_UUID), + .access_cb = ble_gatts_cl_sup_feat_test_misc_access, + .flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE, + }, + { 0 } }, + }, + { 0 } +}; + +static uint16_t ble_gatts_cl_sup_feat_test_chr_def_handle; +static uint16_t ble_gatts_cl_sup_feat_test_chr_val_handle; +static uint8_t ble_gatts_cl_sup_feat_test_chr_val[BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ] = { 0 }; + +static void +ble_gatts_notify_test_misc_reg_cb(struct ble_gatt_register_ctxt *ctxt, void *arg) +{ + uint16_t uuid16; + + if (ctxt->op == BLE_GATT_REGISTER_OP_CHR) { + uuid16 = ble_uuid_u16(ctxt->chr.chr_def->uuid); + switch (uuid16) { + case BLE_GATTS_TEST_CL_SUP_FEAT_CHR_UUID: + ble_gatts_cl_sup_feat_test_chr_def_handle = ctxt->chr.def_handle; + ble_gatts_cl_sup_feat_test_chr_val_handle = ctxt->chr.val_handle; + break; + + default: + TEST_ASSERT_FATAL(0); + break; + } + } +} + +static void +ble_gatts_cl_sup_feat_test_misc_init(uint16_t *out_conn_handle, int bonding) +{ + ble_hs_test_util_init(); + + ble_hs_test_util_reg_svcs(ble_gatts_cl_sup_feat_test_svcs, + ble_gatts_notify_test_misc_reg_cb, NULL); + TEST_ASSERT_FATAL(ble_gatts_cl_sup_feat_test_chr_def_handle != 0); + + ble_hs_test_util_create_conn(1, ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + *out_conn_handle = 1; +} + +static void +ble_gatts_cl_sup_feat_test_perform_bonding(uint16_t conn_handle) +{ + struct ble_hs_conn *conn; + + ble_hs_lock(); + conn = ble_hs_conn_find(conn_handle); + TEST_ASSERT_FATAL(conn != NULL); + conn->bhc_sec_state.encrypted = 1; + conn->bhc_sec_state.authenticated = 1; + conn->bhc_sec_state.bonded = 1; + ble_hs_unlock(); +} + +TEST_CASE_SELF(ble_gatts_cl_sup_feat_test_bond_then_write) +{ + uint16_t conn_handle = 1; + uint16_t conn_handle_2 = 2; + uint8_t out_feat = 0; + int rc; + + ble_gatts_cl_sup_feat_test_misc_init(&conn_handle, 0); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle); + ble_gatts_bonding_established(conn_handle); + + /* Peer with trusted relationship enables EATT in Client Supported + * Features Characteristic */ + ble_gatts_cl_sup_feat_test_chr_val[0] = BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + ble_hs_test_util_conn_disconnect(conn_handle); + ble_hs_test_util_create_conn(conn_handle_2, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle_2); + ble_gatts_bonding_restored(conn_handle_2); + + /* Verify Client Supported Features value is persisted for this peer after + * reconnection.*/ + rc = ble_gatts_peer_cl_sup_feat_get(conn_handle_2, &out_feat, 1); + + TEST_ASSERT(rc == 0); + TEST_ASSERT(out_feat == BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MASK); +} + +TEST_CASE_SELF(ble_gatts_cl_sup_feat_test_write_then_bond) +{ + uint16_t conn_handle = 1; + uint16_t new_conn_handle = 2; + uint8_t out_feat = 0; + int rc; + + ble_gatts_cl_sup_feat_test_misc_init(&conn_handle, 0); + + /* Peer enables Robust Caching in Client Supported Features Characteristic + * prior to bonding*/ + ble_gatts_cl_sup_feat_test_chr_val[0] = + BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_CACHING_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + /* Bonding should result in maintaining last known Client Supported + * Features value for this peer */ + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle); + ble_gatts_bonding_established(conn_handle); + + ble_hs_test_util_conn_disconnect(conn_handle); + ble_hs_test_util_create_conn(new_conn_handle, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + ble_gatts_cl_sup_feat_test_perform_bonding(new_conn_handle); + ble_gatts_bonding_restored(new_conn_handle); + + /* Verify Client Supported Features value is persisted for this peer after + * reconnection.*/ + rc = ble_gatts_peer_cl_sup_feat_get(new_conn_handle, &out_feat, 1); + + TEST_ASSERT(rc == 0); + TEST_ASSERT(out_feat == BLE_GATT_TEST_CLI_SUP_FEAT_ROBUST_CACHING_MASK); +} + +TEST_CASE_SELF(ble_gatts_cl_sup_feat_test_unbonded_no_persist) +{ + uint16_t conn_handle = 1; + uint16_t new_conn_handle = 2; + uint8_t out_feat = 0xFF; /* Initialize to dummy value */ + int rc; + + ble_gatts_cl_sup_feat_test_misc_init(&conn_handle, 0); + + /* Peer enables EATT in Client Supported Features Characteristic */ + ble_gatts_cl_sup_feat_test_chr_val[0] = BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + /* Disconnect (there was no bonding) */ + ble_hs_test_util_conn_disconnect(conn_handle); + + ble_hs_test_util_create_conn(new_conn_handle, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + /* Verify Client Supported Features value wasn't persisted */ + rc = ble_gatts_peer_cl_sup_feat_get(new_conn_handle, &out_feat, 1); + + TEST_ASSERT(rc == 0); + TEST_ASSERT(out_feat == 0x00); +} + +TEST_CASE_SELF(ble_gatts_cl_sup_feat_test_enable_new_feature_across_sessions) +{ + uint16_t conn_handle = 1; + uint16_t conn_handle_2 = 2; + uint16_t conn_handle_3 = 3; + uint8_t out_feat = 0; + int rc; + + ble_gatts_cl_sup_feat_test_misc_init(&conn_handle, 0); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle); + ble_gatts_bonding_established(conn_handle); + + /* Peer with trusted relationship enables EATT and MULT NTF in Client + * Supported Features Characteristic */ + ble_gatts_cl_sup_feat_test_chr_val[0] = + BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MULT_NTF_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + ble_hs_test_util_conn_disconnect(conn_handle); + ble_hs_test_util_create_conn(conn_handle_2, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle_2); + ble_gatts_bonding_restored(conn_handle_2); + + /* After rebonding, peer enables all features in Client Supported Features + * Characteristic */ + ble_gatts_cl_sup_feat_test_chr_val[0] = BLE_GATT_TEST_CLI_SUP_FEAT_ALL_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle_2, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + ble_hs_test_util_conn_disconnect(conn_handle_2); + ble_hs_test_util_create_conn(conn_handle_3, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle_3); + ble_gatts_bonding_restored(conn_handle_3); + + /* Verify Client Supported Features value is persisted for this peer after + * reconnection i.e. all features should be enabled. */ + rc = ble_gatts_peer_cl_sup_feat_get(conn_handle_3, &out_feat, 1); + TEST_ASSERT(rc == 0); + TEST_ASSERT(out_feat == BLE_GATT_TEST_CLI_SUP_FEAT_ALL_MASK); +} + +TEST_CASE_SELF(ble_gatts_cl_sup_feat_test_reject_disabling_already_enabled) +{ + uint16_t conn_handle = 1; + uint16_t new_conn_handle = 2; + uint8_t out_feat = 0; + int rc; + + ble_gatts_cl_sup_feat_test_misc_init(&conn_handle, 0); + + ble_gatts_cl_sup_feat_test_perform_bonding(conn_handle); + ble_gatts_bonding_established(conn_handle); + + /* Peer with trusted relationship enables EATT and MULT NTF in Client + * Supported Features Characteristic */ + ble_gatts_cl_sup_feat_test_chr_val[0] = + BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MULT_NTF_MASK; + ble_hs_test_util_rx_att_write_req( + conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + ble_hs_test_util_conn_disconnect(conn_handle); + ble_hs_test_util_create_conn(new_conn_handle, + ble_gatts_cl_sup_feat_test_peer_addr, NULL, NULL); + + ble_gatts_cl_sup_feat_test_perform_bonding(new_conn_handle); + ble_gatts_bonding_restored(new_conn_handle); + + /* Attempt to disable EATT feature */ + ble_gatts_cl_sup_feat_test_chr_val[0] = BLE_GATT_TEST_CLI_SUP_FEAT_MULT_NTF_BIT; + ble_hs_test_util_rx_att_write_req( + new_conn_handle, ble_gatts_cl_sup_feat_test_chr_val_handle, + ble_gatts_cl_sup_feat_test_chr_val, BLE_GATT_TEST_CL_SUP_FEAT_CHR_SZ); + + /* Verify original features from storage weren't overwritten */ + rc = ble_gatts_peer_cl_sup_feat_get(new_conn_handle, &out_feat, 1); + TEST_ASSERT(rc == 0); + TEST_ASSERT(out_feat == BLE_GATT_TEST_CLI_SUP_FEAT_EATT_MULT_NTF_MASK); +} + +TEST_SUITE(ble_gatts_cl_sup_feat_suite) +{ + ble_gatts_cl_sup_feat_test_bond_then_write(); + ble_gatts_cl_sup_feat_test_write_then_bond(); + ble_gatts_cl_sup_feat_test_unbonded_no_persist(); + ble_gatts_cl_sup_feat_test_enable_new_feature_across_sessions(); + ble_gatts_cl_sup_feat_test_reject_disabling_already_enabled(); +} diff --git a/nimble/host/test/src/ble_hs_test.c b/nimble/host/test/src/ble_hs_test.c index adf9942382..c4aceb9084 100644 --- a/nimble/host/test/src/ble_hs_test.c +++ b/nimble/host/test/src/ble_hs_test.c @@ -76,6 +76,7 @@ main(int argc, char **argv) ble_sm_sc_test_suite(); ble_store_suite(); ble_uuid_test_suite(); + ble_gatts_cl_sup_feat_suite(); return tu_any_failed; } diff --git a/nimble/host/test/src/ble_hs_test.h b/nimble/host/test/src/ble_hs_test.h index 3fb1454eef..5fb4c11f8a 100644 --- a/nimble/host/test/src/ble_hs_test.h +++ b/nimble/host/test/src/ble_hs_test.h @@ -58,5 +58,6 @@ TEST_SUITE_DECL(ble_sm_lgcy_test_suite); TEST_SUITE_DECL(ble_sm_sc_test_suite); TEST_SUITE_DECL(ble_store_suite); TEST_SUITE_DECL(ble_uuid_test_suite); +TEST_SUITE_DECL(ble_gatts_cl_sup_feat_suite); #endif From 83161073416faa38d6f086ceb5b79c412931358d Mon Sep 17 00:00:00 2001 From: Piotr Narajowski Date: Wed, 1 Jul 2026 17:24:45 +0200 Subject: [PATCH 7/7] apps/btshell: update keystore commmands Update keystore commands to handle Client Supported Features storage type object. --- apps/btshell/src/cmd.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/apps/btshell/src/cmd.c b/apps/btshell/src/cmd.c index 3380de7547..4d6f70bff7 100644 --- a/apps/btshell/src/cmd.c +++ b/apps/btshell/src/cmd.c @@ -2267,6 +2267,7 @@ static const struct parse_arg_kv_pair cmd_keystore_entry_type[] = { { "msec", BLE_STORE_OBJ_TYPE_PEER_SEC }, { "ssec", BLE_STORE_OBJ_TYPE_OUR_SEC }, { "cccd", BLE_STORE_OBJ_TYPE_CCCD }, + { "csf", BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT }, { NULL } }; @@ -2388,6 +2389,9 @@ cmd_keystore_add(int argc, char **argv) case BLE_STORE_OBJ_TYPE_CCCD: rc = ble_store_write_cccd(&value.cccd); break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + rc = ble_store_write_peer_cl_sup_feat(&value.feat); + break; default: rc = ble_store_write(obj_type, &value); } @@ -2396,7 +2400,7 @@ cmd_keystore_add(int argc, char **argv) #if MYNEWT_VAL(SHELL_CMD_HELP) static const struct shell_param keystore_add_params[] = { - {"type", "entry type, usage: ="}, + {"type", "entry type, usage: ="}, {"addr_type", "usage: ="}, {"addr", "usage: ="}, {"ediv", "usage: ="}, @@ -2441,7 +2445,7 @@ cmd_keystore_del(int argc, char **argv) #if MYNEWT_VAL(SHELL_CMD_HELP) static const struct shell_param keystore_del_params[] = { - {"type", "entry type, usage: ="}, + {"type", "entry type, usage: ="}, {"addr_type", "usage: ="}, {"addr", "usage: ="}, {"ediv", "usage: ="}, @@ -2504,6 +2508,14 @@ cmd_keystore_iterator(int obj_type, console_printf(" flags: 0x%02x\n", val->cccd.flags); console_printf(" changed: %d\n", val->cccd.value_changed); break; + case BLE_STORE_OBJ_TYPE_PEER_CL_SUP_FEAT: + + console_printf("Key: "); + console_printf("addr_type=%u ", val->feat.peer_addr.type); + print_addr(val->feat.peer_addr.val); + console_printf("\n"); + console_printf("Enabled features: 0x%02x\n", val->feat.peer_cl_sup_feat[0]); + break; } return 0; } @@ -2531,7 +2543,7 @@ cmd_keystore_show(int argc, char **argv) #if MYNEWT_VAL(SHELL_CMD_HELP) static const struct shell_param keystore_show_params[] = { - {"type", "entry type, usage: ="}, + {"type", "entry type, usage: ="}, {NULL, NULL} };