Skip to content

PTY startup cancellation can leak unregistered UnixLocal and Blaxel resources #4749

Description

@fscfede-beep

Please read this first

Describe the bug

Two PTY startup paths can leak resources on ordinary caller cancellation before registration.

Blaxel

pty_exec_start creates an aiohttp.ClientSession, opens the WebSocket, starts the reader task and sends the command before inserting the entry into _pty_sessions.

Its cleanup handlers are:

except asyncio.TimeoutError:
    if not registered:
        await self._terminate_pty_entry(entry)
    ...
except Exception:
    if not registered:
        await self._terminate_pty_entry(entry)
    ...

asyncio.CancelledError derives from BaseException, so cancellation during ws_connect, command send, or a contended _pty_lock acquisition bypasses both handlers. The entry is not in the registry, so pty_terminate_all() cannot recover it.

Concrete resources at risk are the WebSocket, aiohttp.ClientSession, and reader task.

UnixLocal

Once create_subprocess_exec succeeds, the process entry, pump task(s), and wait task exist. Registration then awaits:

async with self._pty_lock:
    ...
    self._pty_processes[process_id] = entry

There is no cancellation cleanup around this ownership transfer. If the map lock is contended and the caller is cancelled, the process entry and background tasks remain unregistered and unreachable from pty_terminate_all().

The TTY creation path has a related gap: its fd-close handler catches only Exception around create_subprocess_exec, so cancellation during that await can also bypass the PTY fd cleanup.

Deterministic control-flow reproduction

I modeled the exact ownership boundaries without provider/network dependencies:

blaxel_current_cleaned=False
blaxel_reference_cleaned=True
unix_current_cleaned=False
unix_reference_cleaned=True
V46_PTY_STARTUP_CANCELLATION_REPRO=PASS

For Blaxel, cancellation bypasses the Exception handlers. For UnixLocal, cancellation while waiting for a held map lock exits after local resource creation but before registration.

Other backends checked

I checked all seven PTY implementations rather than generalizing the claim:

  • Docker catches BaseException and attempts cleanup of an unregistered entry.
  • Cloudflare has an explicit CancelledError cleanup path.
  • Daytona catches BaseException and shields startup cleanup.
  • E2B has an explicit CancelledError cleanup path.
  • Modal has an explicit CancelledError cleanup path.

So the direct single-cancellation claim here is intentionally limited to UnixLocal and Blaxel.

Expected behavior

Once a process/provider resource exists, either:

  1. registration commits and the session registry owns cleanup; or
  2. cancellation before registration settles cleanup before propagating.

A narrow fix can reuse the cancellation-settled cleanup primitive discussed in #4747.

For Blaxel, add a cancellation/BaseException path that cleanup-settles the unregistered entry before re-raising.

For UnixLocal, track whether registration committed and cleanup-settle the entry if cancellation occurs before insertion. The TTY fd-close handler should also cover BaseException so cancellation cannot bypass descriptor cleanup.

Debug information

  • Repository: openai/openai-agents-python
  • Main SHA: 89c02c828ee8510fe9a84ee6675608193aa13b02
  • Provider integration execution: not required / not claimed
  • Reproduction type: deterministic asyncio ownership/control-flow

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