You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ThreadSafety issue in NimBLEAttValue::append(), manifesting in setValue() #446
We hit an issue on a device in production that was causing heap corruption. We tracked the issue down to a race condition in NimBLEAttValue::append()
NimBLEAttValue::setValue() / append() are not safe when the NimBLE host task and an application task update the same characteristic value concurrently. append() computes the new length from m_attr_lenoutside the critical section, then copies to res + m_attr_len using m_attr_len re-read inside it. If the other task's setValue() completes in between, the copy uses the wrong offset and overruns the buffer.
Environment
ESP32-S3, ESP-IDF v5.5.0
esp-nimble-cpp 2.3.4. The code is unchanged in 2.5.0
NimBLE host task on core 0, application task on core 1
deepCopy(), used by the copy that notify() takes (auto value{m_value};), sizes its buffer from source.m_capacity outside the critical section. It then copies source.m_attr_len + 1 bytes from source.m_attr_value inside it. If the source grows in between, the copy overruns the new buffer. If the source's realloc has already freed the old buffer, the copy reads freed memory.
Thanks for the quick turnaround, @h2zero . I reviewed #451 against the crash we had and this fixes the specific issue we hit. We have made some changes in how we use attributes to minimize concurrency issues.
For this PR, some observations:
I'd recommend locking readers that make a copy. getValue<T>() , the conversion operators, operator==, operator[], and the source side of operator+=
data(), c_str() and begin()/end() can't be fixed with a lock, because the pointer outlives it. I'd wish the library didn't hand out a pointer to the data. So maybe document this and tell users to treat the pointer getters as safe only on a copy.
if (pChr->m_value.length() != len + next->om_len) {
rc = BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
break;
}
len += next->om_len;
next = SLIST_NEXT(next, om_next);
}
} else {
rc = BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
are not atomic. A write from another task can land in between and leave a mixed value. Since the mutex is recursive, holding one Lock across the whole build would fix it. Another option is to build into a local NimBLEAttValue and move it in, then give the callback that local copy instead of a pointer into m_value. I'd avoid holding the lock across the user callback, since user code would then run under a lock that every attribute shares.
Nitpick: Lock::init() (NimBLEDevice.cpp#L1020) runs after the host task has started and synced. You might consider creating the mutex before nimble_port_freertos_init() and failing init().
Activity
h2zero commentedon Oct 18, 2026
Thank you @OzerOzdemir, should be resolved by #451, please test it and let me know.
OzerOzdemir commentedon Oct 18, 2026
Thanks for the quick turnaround, @h2zero . I reviewed #451 against the crash we had and this fixes the specific issue we hit. We have made some changes in how we use attributes to minimize concurrency issues.
For this PR, some observations:
getValue<T>(), the conversion operators,operator==,operator[], and the source side ofoperator+=data(),c_str()andbegin()/end()can't be fixed with a lock, because the pointer outlives it. I'd wish the library didn't hand out a pointer to the data. So maybe document this and tell users to treat the pointer getters as safe only on a copy.esp-nimble-cpp/src/NimBLEClient.cpp
Lines 1183 to 1197 in 9c094d9
Lock::init()(NimBLEDevice.cpp#L1020) runs after the host task has started and synced. You might consider creating the mutex before nimble_port_freertos_init() and failing init().h2zero commentedon Oct 19, 2026
@OzerOzdemir agreed, thank you, changes implemented, please let me know if there are any further issues.
OzerOzdemir commentedon Oct 20, 2026
Looks good to me