Skip to content

ble_hs_timer_reset() deinits the host timer callout while its event may be queued, host task then calls a NULL ev->fn #1184

Description

@rfordinal

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

  1. 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().
  2. ble_hs_is_enabled() returns true only for _ON, so it is now false.
  3. 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);
  4. 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.
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions