From 42d7054f6538a4a5181f5fcd134cf514bce0f726 Mon Sep 17 00:00:00 2001 From: asill-livestream Date: Thu, 4 Jun 2026 06:42:31 +0900 Subject: [PATCH] fix(tui): eliminate TOCTOU race between goal followup and concurrent turn submission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit H1: When a turn finished and a goal followup existed, the finally block set running=False (under history_lock), then the goal followup code re-acquired the lock and set running=True. Between these two lock acquisitions, a notification poller or concurrent prompt.submit could acquire the lock, see running=False, and start a second turn on the same session — leading to interleaved history writes. Fix: instead of releasing and re-acquiring the turn guard, keep running=True across the finally→goal-followup boundary. Only set running=False in the finally when there is NO goal followup to chain. This eliminates the TOCTOU window entirely. All 183 TUI tests pass, including goal command tests. --- tui_gateway/server.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 5ac7ccf5d6378..245394cdc982c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -4574,24 +4574,22 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None: pass _clear_session_context(session_tokens) with session["history_lock"]: - session["running"] = False + if not goal_followup: + # Only release the turn guard when there is no followup + # to chain. When a goal continuation exists we keep + # running=True so no other thread (notification poller, + # concurrent prompt.submit) can steal the turn during the + # gap between this finally and the followup dispatch below. + session["running"] = False session["last_active"] = time.time() _clear_inflight_turn(session) _emit("session.info", sid, _session_info(agent, session)) - # Chain a goal-continuation turn if the judge said so. We do - # this AFTER the finally releases session["running"], so the - # nested _run_prompt_submit doesn't deadlock on the busy - # guard. A real user prompt that races us wins because - # prompt.submit sets running=True under the history_lock and - # we check that guard before re-firing. + # Chain a goal-continuation turn if the judge said so. Because the + # finally block above intentionally kept running=True when a followup + # exists, there is no TOCTOU window — no other thread can sneak in + # and start a concurrent turn on this session. if goal_followup: - with session["history_lock"]: - if session.get("running"): - # User already sent something — their turn wins, - # the judge will re-run on the next turn anyway. - return - session["running"] = True try: _emit("message.start", sid) _run_prompt_submit(rid, sid, session, goal_followup)