Skip to content

fix: whiteListRemove use-after-free — loop continues on invalidated iterator#1171

Closed
sanastasiou wants to merge 2 commits into
h2zero:masterfrom
sanastasiou:fix/whitelistremove-uaf
Closed

fix: whiteListRemove use-after-free — loop continues on invalidated iterator#1171
sanastasiou wants to merge 2 commits into
h2zero:masterfrom
sanastasiou:fix/whitelistremove-uaf

Conversation

@sanastasiou

Copy link
Copy Markdown
Contributor

whiteListRemove iterates m_whiteList and, on a match:

  1. m_whiteList.erase(it) — invalidates it (and all iterators at/after it),
  2. std::vector<NimBLEAddress>(m_whiteList).swap(m_whiteList) — the shrink-to-fit swap frees the ENTIRE buffer it points into,
  3. the loop then continues: ++it and it < m_whiteList.end() operate on a dangling iterator into freed memory — undefined behavior on every removal that finds a match.

When the freed buffer's stale pointer happens to compare "in range" against the new allocation (pure heap-layout luck), *it == address reads freed memory, and a spurious re-match calls m_whiteList.erase(it) with a foreign iterator — the vector's internal element move then writes address-shaped data across unrelated heap memory.

Observed in the field (ESP32-C3, arduino-esp32 / IDF 5.5.4) as two distinct intermittent heap-corruption crashes under whitelist churn, both inside or adjacent to whiteListRemove:

  • assert failed: multi_heap_free multi_heap_poisoning.c:279 (head != NULL) — the vector buffer's heap-block header overwritten with BLE-address bytes (CORRUPT HEAP: Bad head ... Expected 0xabba1234 got 0xbb250000, the 25 bb being the LSB-first wire bytes of a whitelisted peer address);
  • assert failed: xQueueSemaphoreTake queue.c:1713 (pxQueue->uxItemSize == 0) — the NimBLE host mutex clobbered by the same wild write, tripping on the next ble_hs_lock().

Fix: break after the shrink. Since whiteListAdd guards against duplicates (onWhiteList() check before push_back), at most one element can match — breaking out after the removal preserves semantics exactly.

Verification: with this fix, the same churn workload (repeated whitelist rebuilds around pair/disconnect cycles) runs clean under CONFIG_HEAP_POISONING_COMPREHENSIVE plus periodic heap_caps_check_integrity_all() sweeps — previously ~2 crashes per 5 cycles.

Side note (not touched here to keep the diff minimal): &m_whiteList[0] on line 751 is also UB when the vector becomes empty after erasing the last entry — m_whiteList.data() would be the safe spelling.

…rink

whiteListRemove erases the matched element (invalidating the iterator) and
shrink-to-fits m_whiteList (freeing the buffer the iterator points into),
then continues the loop on the dangling iterator. A layout-dependent
spurious re-match then calls erase() with a foreign iterator, and the
vector's internal element move writes across unrelated heap memory.

Observed on ESP32-C3 as intermittent heap corruption under whitelist churn
(multi_heap_free bad-head asserts with address bytes in the corrupted
header; a clobbered NimBLE host mutex tripping xQueueSemaphoreTake).
Verified fixed under CONFIG_HEAP_POISONING_COMPREHENSIVE plus periodic
heap_caps_check_integrity_all() sweeps.

whiteListAdd guards duplicates via onWhiteList(), so at most one element
can match - breaking out after the removal preserves semantics.
After erasing the last whitelist entry, &m_whiteList[0] indexes an empty
vector before ble_gap_wl_set(..., 0). data() is well-defined for empty
vectors; same change applied to the whiteListAdd call site for consistency.
@sanastasiou sanastasiou closed this by deleting the head repository Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant