-
Notifications
You must be signed in to change notification settings - Fork 132
Description
Summary:
There is a memory leak issue in Callback_Watchdog when used as a member in several request-related callback classes (OTA_Handler, Provision_Callback, Attribute_Request_Callback, and RPC_Request_Callback). The leak is caused by the repeated allocation of esp_timer instances on each call to once(...), without corresponding deallocation, especially when objects are created in local scopes and copied.
Detailed Explanation:
In version 0.15.0 of the ThingsBoard SDK, the Callback_Watchdog class creates a new ESP-IDF one-shot timer every time the once(const uint64_t& timeout_microseconds) method is called if no timer is already present.
Each of these timer creations allocates ~60 bytes from the heap, but the created timers are never freed, causing memory leaks when requests are made frequently.
This issue becomes especially problematic when using the callback classes in the following way:
const Attribute_Request_Callback<MAX_SHARED_ATTRIBUTES_AMOUNT_PER_ONCE> logLevelRequestSharedAttrCB(
&processSharedAttributeCB,
SHARED_ATTR_REQUEST_TIMEOUT_MICROSECONDS,
&logLevelRequestTimeout,
REQUESTED_SHARED_ATTRIBUTES
);
In this example, the callback is constructed as a const object in a local scope and passed by reference to Attributes_Request_Subscribe, which stores it inside an array structure by copy.
Since Callback_Watchdog is a member of the callback class and its copy constructor is defaulted, a shallow copy is made — the internal esp_timer* pointer is copied directly.
When the local object goes out of scope, its destructor runs, but the copied object still holds a reference to the same esp_timer*. This leads to repeated allocations for new timers and no corresponding destruction — resulting in a heap leak for every request.
Fix Applied in Fork:
In our fork (BatuhanKaratas/thingsboard-client-sdk), we fixed this issue with the following changes:
Added create_timer(); inside Callback_Watchdog's constructor to ensure a single timer instance is created and reused.
Used Scott Meyers' singleton pattern to avoid frequent construction/destruction of the timer object, ensuring safe reuse and avoiding dangling pointers.
As a result, usage like the following is now safe:
static const Attribute_Request_Callback<MAX_SHARED_ATTRIBUTES_AMOUNT_PER_ONCE> logLevelRequestSharedAttrCB(
&processSharedAttributeCB,
SHARED_ATTR_REQUEST_TIMEOUT_MICROSECONDS,
&logLevelRequestTimeout,
REQUESTED_SHARED_ATTRIBUTES
);
We applied similar static object changes to all other classes that hold a Callback_Watchdog member.
Environment:
ThingsBoard SDK version: 0.15.0
ESP-IDF version: v5.5.0
Platform: ESP32-S3
Tool: VS Code
Suggested Fix:
We recommend:
Refactoring Callback_Watchdog to ensure safe timer creation and reuse
Preventing shallow copies of watchdogs that lead to duplicated allocations
Documenting safe usage patterns (e.g., static callback objects)
Let us know if you'd like us to contribute a pull request with this fix.