esphome / esphome/backlog

RAM allocation failures lead to abort instead of graceful recovery

Open
#164 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

`new (std::nothrow)` never returns nullptr on ESP-IDF, the device aborts instead; every site that relies on it for out of memory handling needs to move to `RAMAllocator`, and a ci-custom lint will reject new uses.

On-device result from the AtomU running the PR esphome/esphome#19093 build with ESP-IDF 5.5.5, GCC 14.2, exceptions disabled. Each button asks for 8 MB with about 199 KB free.

- RAMAllocator allocate logged a return of 0x0 and the device stayed up.
- nothrow new[] never logged a result. The API connection reset, and the debug reset reason after reboot read "exception/panic".
- Serial capture during the press shows the abort and the symbolized backtrace:

```
abort() was called at PC 0x4013e61e on core 1
0x4013e61e: __wrap___cxa_allocate_exception cxx_exception_stubs.cpp:184
0x4013dfe1: operator new(unsigned int) libsupc++/new_op.cc:54
0x4013e035: operator new[](unsigned int) libsupc++/new_opv.cc:32
0x4013e045: operator new[](unsigned int, std::nothrow_t const&) libsupc++/new_opvnt.cc:38
0x400dbbbf: the button lambda
```

Why it happens. The nothrow operators in libsupc++ are implemented as a try/catch around the throwing operator new. On failure the throwing one calls the exception allocator. ESP-IDF's cxx component wraps both exception entry points with `--wrap` and points them at abort when `CONFIG_COMPILER_CXX_EXCEPTIONS` is off, which is our sdkconfig. The disassembly of the previously built firmware.elf shows exactly this call chain. Espressif closed https://github.com/espressif/esp-idf/issues/18226 as expected behaviour, and their suggested workaround is to define the two nothrow operators globally to call malloc.

Migration pattern, from `APIBuffer` in esphome/esphome#19093. Owned storage lives in a `RAMUniquePtr` (esphome/esphome#19245), whose deleter frees through the allocator, growth goes through `reallocate` and reports failure to the caller instead of aborting:

```cpp
RAMUniquePtr data_;

bool APIBuffer::grow_(size_t n) {
if (n > MAX_SIZE)
return false;
uint8_t *grown = RAMAllocator().reallocate(this->data_.get(), n);
if (grown == nullptr)
return false;
(void) this->data_.release(); // realloc already freed or reused the old block
this->data_.reset(grown);
this->capacity_ = n;
return true;
}
```

Callers check the `[[nodiscard]]` bool from `reserve` and `resize` and drop the connection on false. For an object with a constructor, allocate raw storage with `RAMAllocator` and construct it with placement new.

| Site | Fix |
| --- | --- |
| `esphome/components/api/api_buffer.cpp` grow path | esphome/esphome#19093 |
| `esphome/components/api/api_overflow_buffer.cpp` data and Entry, two sites | esphome/esphome#19093 |
| `esphome/components/esphome/ota/ota_esphome_noise.cpp` NoiseSession | esphome/esphome#19249, merged |
| `esphome/components/ota/ota_signature_esp_idf.cpp` signature block | esphome/esphome#19251, merged |
| `esphome/components/ethernet/w5500_custom_spi.cpp` SPI context | esphome/esphome#19248, merged |
| `esphome/components/nextion/nextion.cpp` NextionComponentBase, two sites | esphome/esphome#19246, merged |
| `esphome/components/esphome/ota/ota_esphome.cpp` InflateSession, added by esphome/esphome#19037 | esphome/esphome#19037 |
| `esphome/components/esp8266/__init__.py` comment recommending nothrow, next to a `NEW_OOM_ABORT` flag Arduino core 3 no longer reads | esphome/esphome#19247 |
| `esphome/components/esphome/ota/ota_esphome.cpp` auth buffer, throwing `std::make_unique` aborts on OOM the same way | esphome/esphome#19249, merged |
| `esphome/components/wifi/wifi_component_esp_idf.cpp` scan results, `std::vector::reserve` aborts on OOM through the same stubs (esphome/esphome#19243) | esphome/esphome#19253 for FixedVector, esphome/esphome#19254 for the scan results, both merged |
| ci-custom lint rejecting `std::nothrow`, message points at RAMAllocator | esphome/esphome#19244 |
| `RAMAllocator::make_unique` and `RAMUniquePtr` core helper the sites above use | esphome/esphome#19245, merged |
| swap the hand written `FreeDeleter` in `api_buffer.h` for `RAMUniquePtr` | esphome/esphome#19093, on top of the merged esphome/esphome#19245 |

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with the listed allocation sites, especially esphome/components/api/api_overflow_buffer.cpp, esphome/components/esphome/ota/ota_esphome.cpp, and esphome/components/wifi/wifi_component_esp_idf.cpp, then inspect RAMAllocator and RAMUniquePtr from the referenced core helper. Run the ci-custom lint and trace each remaining allocation path; done means all unsafe nothrow or throwing allocation sites are migrated and allocation failure is reported without aborting.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
embedded-iot
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.