Skip to content

Host._send_command does not release command_semaphore on timeout, causing subsequent commands to hang #910

@istemon

Description

@istemon

When Host._send_command times out waiting for an HCI response, the command_semaphore is never released. Every subsequent call to _send_command then blocks indefinitely at await self.command_semaphore.acquire(), making the host permanently unresponsive with no error or exception.

The release in the finally block is gated on response is not None. On a timeout, response remains None, so the semaphore is never released:

response = None
try:
    ...
    response = await asyncio.wait_for(self.pending_response, timeout=response_timeout)
    return response
finally:
    if (
        response is not None          # <-- None on timeout
        and response.num_hci_command_packets
        and self.command_semaphore.locked()
    ):
        self.command_semaphore.release()  # <-- never reached

I'm wondering if this is intentional, to perhaps indicate that the controller is unresponsive/pending? The controller I'm using definitely isn't the most reliable (Raspberry Pi 3B+), but I'm not sure how to detect / recover from this, other than wrapping all bumble calls in a timeout.

There was about 3 minutes between when I issued the command that timed out (!!! Command HCI_LE_CLEAR_FILTER_ACCEPT_LIST_COMMAND timed out) to when I tried issuing the next command.

I haven't been able to capture any proper logs of this happening on real hardware, but this script reproduces what I've seen (reproduction code generated with Claude).

import asyncio

from bumble import hci
from bumble.controller import Controller
from bumble.device import Device
from bumble.host import Host
from bumble.link import LocalLink
from bumble.transport.common import AsyncPipeSink

def make_device() -> Device:
    link = LocalLink()
    controller = Controller("test-controller", link=link)
    device = Device(host=Host())
    device.host.set_packet_sink(controller)
    controller.host = AsyncPipeSink(device.host)
    return device

async def main():
    device = make_device()
    await device.power_on()

    print("Step 1: Send a command with a near-zero timeout to force a timeout...")
    try:
        await device.host._send_command(
            hci.HCI_LE_Clear_Filter_Accept_List_Command(),
            response_timeout=0.00001,
        )
    except TimeoutError:
        print("  -> Timed out as expected.")

    print("Step 2: Send a normal command — hangs forever if semaphore was leaked...")
    try:
        await asyncio.wait_for(
            device.host._send_command(
                hci.HCI_LE_Clear_Filter_Accept_List_Command(),
            ),
            timeout=3.0,
        )
        print("  -> Completed (bug is fixed).")
    except asyncio.TimeoutError:
        print(
            "  -> HUNG for 3 seconds and timed out: semaphore was leaked. Bug confirmed."
        )

asyncio.run(main())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions