Skip to content

sim_time: reusing a SimTimeRuntime across scheduled() runs leaves stale driver state and hangs the second run #512

Description

@proboscis

Split out of #507 (minor-findings item 11) on demand; reproduced during the 2026-07-10 audit fix campaign (verified with a faulthandler watchdog dump).

Symptom

Reuse one sim_time_handler (same SimTimeRuntime) across two scheduled() runs, where run 1 ends while a WaitUntil is still parked. Run 1 leaves _driver_running=True and a stale TimeQueue entry. Run 2's first Delay/WaitUntil then never spawns a clock driver (the runtime believes one is running) and the run hangs forever in _drain_one_external (scheduler.py:351 blocking external_queue.get()).

Why this is likely in practice

Handler caching via Hy defhandler + lazy-var (the common composition pattern downstream) makes accidental cross-run reuse of one runtime instance easy.

Related latent hazard (same family)

Cross-run promise-ID collision: both runs' fresh_id sequences start at 0, and promises[pid] completion is unguarded, so a stale TimeQueue entry from run 1 can complete the WRONG promise in run 2.

Fix directions (either)

  • Make sim_time_handler() construct a fresh SimTimeRuntime per scheduled() run (bind runtime state to the run, not the handler closure), or
  • Fail fast: detect a second run reusing a runtime whose driver state/TimeQueue is non-empty and raise with a clear message.

Found by the systematic concurrency audit (#507); reproduced on the fix-time-contract branch (post-#499 fix), so it is orthogonal to the shipped PRs (#508-#511).

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