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

Open Beginner friendly
#910 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
75/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
python
Domain
embedded-iot

Research direction

Start at Host._send_command and run the reproduction script from the issue to observe the timeout and the subsequent command hang. Trace the finally-block semaphore handling, then add or update coverage for a timed-out command followed by another command. Done means the second command no longer blocks indefinitely after the first timeout.

Written by the indexing model from the issue text.

Description

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())
Dominant language
Python
Stars
556
Forks
138
Avg merge
3d 15h
Merged PRs (30d)
9

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/bumble

All issues in google/bumble

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.