From 5a80bb0faed766aacfdc6bd476ba2ac086578dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Ani=C4=87=20Bani=C4=87?= Date: Wed, 1 Jul 2026 14:39:47 +0200 Subject: [PATCH 1/2] Allow application to reject reads/writes with an ATT error The GATT access handler discarded the outcome of the characteristic write callback and always returned 0 (success) to the peer, so an application had no way to reject a write with an ATT error even under write-with-response. Add opt-in status-returning callbacks onReadStatus/onWriteStatus to NimBLECharacteristicCallbacks that default to invoking the existing void onRead/onWrite and returning 0, preserving current behavior for all existing overrides. Thread an int return up through the internal readEvent/writeEvent chain (NimBLELocalValueAttribute, NimBLECharacteristic, NimBLEDescriptor) so handleGattEvent forwards a non-zero BLE_ATT_ERR_* code to the host, which turns it into an ATT error response. A rejection reaches the peer only for write-with-response; a write command is unacknowledged so the code is dropped by the stack, by spec. --- src/NimBLECharacteristic.cpp | 8 ++++---- src/NimBLECharacteristic.h | 33 +++++++++++++++++++++++++++++++-- src/NimBLEDescriptor.cpp | 7 +++++-- src/NimBLEDescriptor.h | 4 ++-- src/NimBLELocalValueAttribute.h | 6 ++++-- src/NimBLEServer.cpp | 8 +++++--- 6 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/NimBLECharacteristic.cpp b/src/NimBLECharacteristic.cpp index 343d4071..c9e39b51 100644 --- a/src/NimBLECharacteristic.cpp +++ b/src/NimBLECharacteristic.cpp @@ -422,8 +422,8 @@ void NimBLECharacteristic::updatePeerStatus(const NimBLEConnInfo& peerInfo) cons * @brief Handle a read event from a client. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ -void NimBLECharacteristic::readEvent(NimBLEConnInfo& connInfo) { - m_pCallbacks->onRead(this, connInfo); +int NimBLECharacteristic::readEvent(NimBLEConnInfo& connInfo) { + return m_pCallbacks->onReadStatus(this, connInfo); } // readEvent /** @@ -432,9 +432,9 @@ void NimBLECharacteristic::readEvent(NimBLEConnInfo& connInfo) { * @param [in] len The length of the data written by the client. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ -void NimBLECharacteristic::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { +int NimBLECharacteristic::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { setValue(val, len); - m_pCallbacks->onWrite(this, connInfo); + return m_pCallbacks->onWriteStatus(this, connInfo); } // writeEvent /** diff --git a/src/NimBLECharacteristic.h b/src/NimBLECharacteristic.h index e3c70b63..2f5f72f2 100644 --- a/src/NimBLECharacteristic.h +++ b/src/NimBLECharacteristic.h @@ -271,8 +271,8 @@ class NimBLECharacteristic : public NimBLELocalValueAttribute { friend class NimBLEService; void setService(NimBLEService* pService); - void readEvent(NimBLEConnInfo& connInfo) override; - void writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) override; + int readEvent(NimBLEConnInfo& connInfo) override; + int writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) override; bool sendValue(const uint8_t* value, size_t length, bool is_notification = true, @@ -319,6 +319,35 @@ class NimBLECharacteristicCallbacks { virtual ~NimBLECharacteristicCallbacks() {} virtual void onRead(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo); virtual void onWrite(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo); + + /** + * @brief Read request callback that can reject the read with an ATT error. + * @param [in] pCharacteristic The characteristic that is the source of the event. + * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. + * @return 0 to accept the read, or a BLE_ATT_ERR_* code to reject it. + * @details Defaults to calling onRead() and accepting. Override this instead of onRead() + * when you need to reject a read from the application. + */ + virtual int onReadStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) { + onRead(pCharacteristic, connInfo); + return 0; + } + + /** + * @brief Write request callback that can reject the write with an ATT error. + * @param [in] pCharacteristic The characteristic that is the source of the event. + * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. + * @return 0 to accept the write, or a BLE_ATT_ERR_* code to reject it. + * @details Defaults to calling onWrite() and accepting. Override this instead of onWrite() + * when you need to reject a write from the application. A rejection only reaches the peer + * when it used write-with-response; a write-without-response (ATT Write Command) is + * unacknowledged, so the code is dropped by the stack. + */ + virtual int onWriteStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo) { + onWrite(pCharacteristic, connInfo); + return 0; + } + virtual void onStatus(NimBLECharacteristic* pCharacteristic, int code); // deprecated virtual void onStatus(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, int code); virtual void onSubscribe(NimBLECharacteristic* pCharacteristic, NimBLEConnInfo& connInfo, uint16_t subValue); diff --git a/src/NimBLEDescriptor.cpp b/src/NimBLEDescriptor.cpp index 8f1bb41e..a7d0dc58 100644 --- a/src/NimBLEDescriptor.cpp +++ b/src/NimBLEDescriptor.cpp @@ -120,13 +120,16 @@ std::string NimBLEDescriptor::toString() const { return res; } // toString -void NimBLEDescriptor::readEvent(NimBLEConnInfo& connInfo) { +int NimBLEDescriptor::readEvent(NimBLEConnInfo& connInfo) { + // Descriptor callbacks have no status-returning variant; always accept. m_pCallbacks->onRead(this, connInfo); + return 0; } // readEvent -void NimBLEDescriptor::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { +int NimBLEDescriptor::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { setValue(val, len); m_pCallbacks->onWrite(this, connInfo); + return 0; } // writeEvent /** diff --git a/src/NimBLEDescriptor.h b/src/NimBLEDescriptor.h index b6e9b8f2..cf403aad 100644 --- a/src/NimBLEDescriptor.h +++ b/src/NimBLEDescriptor.h @@ -49,8 +49,8 @@ class NimBLEDescriptor : public NimBLELocalValueAttribute { friend class NimBLEService; void setCharacteristic(NimBLECharacteristic* pChar); - void readEvent(NimBLEConnInfo& connInfo) override; - void writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) override; + int readEvent(NimBLEConnInfo& connInfo) override; + int writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) override; NimBLEDescriptorCallbacks* m_pCallbacks{nullptr}; NimBLECharacteristic* m_pCharacteristic{nullptr}; diff --git a/src/NimBLELocalValueAttribute.h b/src/NimBLELocalValueAttribute.h index 5fd37e55..131a3ece 100644 --- a/src/NimBLELocalValueAttribute.h +++ b/src/NimBLELocalValueAttribute.h @@ -112,8 +112,9 @@ class NimBLELocalValueAttribute : public NimBLELocalAttribute, public NimBLEValu * @brief Callback function to support a read request. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. * @details This function is called by NimBLEServer when a read request is received. + * @return 0 on success, or a BLE_ATT_ERR_* code to reject the read. */ - virtual void readEvent(NimBLEConnInfo& connInfo) = 0; + virtual int readEvent(NimBLEConnInfo& connInfo) = 0; /** * @brief Callback function to support a write request. @@ -121,8 +122,9 @@ class NimBLELocalValueAttribute : public NimBLELocalAttribute, public NimBLEValu * @param [in] len The length of the value. * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. * @details This function is called by NimBLEServer when a write request is received. + * @return 0 on success, or a BLE_ATT_ERR_* code to reject the write. */ - virtual void writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) = 0; + virtual int writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) = 0; /** * @brief Get a pointer to value of the attribute. diff --git a/src/NimBLEServer.cpp b/src/NimBLEServer.cpp index 1cab58ed..29131664 100644 --- a/src/NimBLEServer.cpp +++ b/src/NimBLEServer.cpp @@ -742,7 +742,10 @@ int NimBLEServer::handleGattEvent(uint16_t connHandle, uint16_t attrHandle, ble_ // Don't call readEvent if the buffer len is 0 (this is a follow up to a previous read), // or if this is an internal read (handle is NONE) if (ctxt->om->om_len > 0 && connHandle != BLE_HS_CONN_HANDLE_NONE) { - pAtt->readEvent(peerInfo); + int appRc = pAtt->readEvent(peerInfo); + if (appRc != 0) { + return appRc; // application rejected the read + } } ble_npl_hw_enter_critical(); @@ -773,8 +776,7 @@ int NimBLEServer::handleGattEvent(uint16_t connHandle, uint16_t attrHandle, ble_ next = SLIST_NEXT(next, om_next); } - pAtt->writeEvent(buf, len, peerInfo); - return 0; + return pAtt->writeEvent(buf, len, peerInfo); } default: From 470d62d2f46a6c57ce4085b94761a6ad6f49c1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roberto=20Ani=C4=87=20Bani=C4=87?= Date: Wed, 1 Jul 2026 16:15:09 +0200 Subject: [PATCH 2/2] Roll back the characteristic value when a write is rejected writeEvent() committed setValue() before invoking the callback, so a callback returning a BLE_ATT_ERR_* left the rejected payload as the stored value: the peer saw an error while local state had accepted the write (observable on any readable characteristic or via getValue()). Snapshot the value before the tentative commit and restore it when onWriteStatus() rejects. The commit-before-callback ordering is kept so getValue() still reflects the new value inside the callback (the default onWriteStatus calls the legacy onWrite). NimBLEAttValue copy/assign are deep copies, so the snapshot/restore is self-contained. --- src/NimBLECharacteristic.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/NimBLECharacteristic.cpp b/src/NimBLECharacteristic.cpp index c9e39b51..e8a09861 100644 --- a/src/NimBLECharacteristic.cpp +++ b/src/NimBLECharacteristic.cpp @@ -433,8 +433,16 @@ int NimBLECharacteristic::readEvent(NimBLEConnInfo& connInfo) { * @param [in] connInfo A reference to a NimBLEConnInfo instance containing the peer info. */ int NimBLECharacteristic::writeEvent(const uint8_t* val, uint16_t len, NimBLEConnInfo& connInfo) { + // Commit before the callback so getValue() reflects the new value inside it + // (onWriteStatus defaults to calling the legacy onWrite), but snapshot first + // so a rejected write can be rolled back and never becomes observable state. + NimBLEAttValue previous = m_value; setValue(val, len); - return m_pCallbacks->onWriteStatus(this, connInfo); + const int rc = m_pCallbacks->onWriteStatus(this, connInfo); + if (rc != 0) { + m_value = previous; + } + return rc; } // writeEvent /**