Reporting a field crash on NimBLE-Arduino 2.5.1, ESP32-S3, arduino-esp32 3.3.x, peripheral role (advertising, no connection at the time). We have a coredump and a root cause, but no reliable reproducer.
Symptom
NimBLEDevice::deinit(true) occasionally resets the chip. The crashed task is nimble_host:
Crashed task: 'nimble_host'
exccause 0x14 (InstFetchProhibitedCause)
pc 0x0
a10 -> ble_hs_timer + 8
pc 0x0 with InstFetchProhibited is a call through a NULL function pointer. The faulting instruction is in nimble_port_run():
l32i a8, a10, 4 ; a8 = ev->fn
callx8 a8 ; a8 == 0
a10 is the event that was just dequeued, and it resolves to ble_hs_timer + 8, i.e. the ev member of the host timer callout. So ble_hs_timer.ev.fn was NULL when the host task ran the event.
The task that called deinit() was blocked at the same moment in NimBLEDevice::deinit(true) -> nimble_port_stop() -> ble_hs_stop() -> ble_gap_preempt() -> ble_gap_adv_stop_no_lock(), waiting on the controller semaphore.
Cause
ble_hs_stop_begin() (src/nimble/nimble/host/src/ble_hs_stop.c) sets ble_hs_enabled_state = BLE_HS_ENABLED_STATE_STOPPING and then calls ble_hs_timer_resched().
ble_hs_is_enabled() returns true only for _ON, so it is now false.
ble_hs_timer_reset() (src/nimble/nimble/host/src/ble_hs.c:445-447 on master) therefore takes the branch:
ble_npl_callout_stop(&ble_hs_timer);
ble_npl_callout_deinit(&ble_hs_timer);
npl_freertos_callout_deinit() calls ble_npl_event_deinit(&co->ev) and then memset(co, 0, sizeof(struct ble_npl_callout)), so ev.fn becomes NULL.
- If the timer had already expired, that same
ev is already queued on g_eventq_dflt. The memset does not remove it from the queue. The host task dequeues it and calls ev->fn.
ble_npl_eventq_remove() exists in the FreeRTOS port and would cover step 4, but is not called.
The deinit() call does not exist upstream
Checked 2026-09-01:
| tree |
ble_hs_timer_reset(), not-enabled branch |
| apache/mynewt-nimble master |
ble_npl_callout_stop() only |
| espressif/esp-nimble master |
ble_npl_callout_stop() only |
espressif/esp-nimble @ f566133, e3cbdc0, 70439dd |
stop() and deinit() |
| h2zero/NimBLE-Arduino master |
stop() and deinit() |
The extra deinit() appears in older esp-nimble snapshots and espressif have since removed it, so this looks like a stale sync rather than a deliberate divergence.
Removing it should leak nothing: ble_hs_deinit() in the same file already ends with ble_npl_callout_deinit(&ble_hs_timer), at a point where the host has stopped and the event queue is drained.
Reproducer
We do not have one, and we want to be upfront about that. The window needs the host timer event to be in flight in the same instant the stop begins. This fired once on a device that had done hundreds of init/deinit cycles, and we found it from the coredump rather than from a test.
Happy to send a PR with the one-line removal if you would prefer that to waiting for a core re-sync.
Reporting a field crash on NimBLE-Arduino 2.5.1, ESP32-S3, arduino-esp32 3.3.x, peripheral role (advertising, no connection at the time). We have a coredump and a root cause, but no reliable reproducer.
Symptom
NimBLEDevice::deinit(true)occasionally resets the chip. The crashed task isnimble_host:pc 0x0withInstFetchProhibitedis a call through a NULL function pointer. The faulting instruction is innimble_port_run():a10is the event that was just dequeued, and it resolves toble_hs_timer + 8, i.e. theevmember of the host timer callout. Soble_hs_timer.ev.fnwas NULL when the host task ran the event.The task that called
deinit()was blocked at the same moment inNimBLEDevice::deinit(true)->nimble_port_stop()->ble_hs_stop()->ble_gap_preempt()->ble_gap_adv_stop_no_lock(), waiting on the controller semaphore.Cause
ble_hs_stop_begin()(src/nimble/nimble/host/src/ble_hs_stop.c) setsble_hs_enabled_state = BLE_HS_ENABLED_STATE_STOPPINGand then callsble_hs_timer_resched().ble_hs_is_enabled()returns true only for_ON, so it is now false.ble_hs_timer_reset()(src/nimble/nimble/host/src/ble_hs.c:445-447on master) therefore takes the branch:npl_freertos_callout_deinit()callsble_npl_event_deinit(&co->ev)and thenmemset(co, 0, sizeof(struct ble_npl_callout)), soev.fnbecomes NULL.evis already queued ong_eventq_dflt. The memset does not remove it from the queue. The host task dequeues it and callsev->fn.ble_npl_eventq_remove()exists in the FreeRTOS port and would cover step 4, but is not called.The
deinit()call does not exist upstreamChecked 2026-09-01:
ble_hs_timer_reset(), not-enabled branchble_npl_callout_stop()onlyble_npl_callout_stop()onlyf566133,e3cbdc0,70439ddstop()anddeinit()stop()anddeinit()The extra
deinit()appears in older esp-nimble snapshots and espressif have since removed it, so this looks like a stale sync rather than a deliberate divergence.Removing it should leak nothing:
ble_hs_deinit()in the same file already ends withble_npl_callout_deinit(&ble_hs_timer), at a point where the host has stopped and the event queue is drained.Reproducer
We do not have one, and we want to be upfront about that. The window needs the host timer event to be in flight in the same instant the stop begins. This fired once on a device that had done hundreds of init/deinit cycles, and we found it from the coredump rather than from a test.
Happy to send a PR with the one-line removal if you would prefer that to waiting for a core re-sync.