Skip to content

fix(tmux): synchronize controlModeDone reads to fix data race - #193

Closed
tstapler wants to merge 5 commits into
mainfrom
fix/control-mode-done-channel-race
Closed

fix(tmux): synchronize controlModeDone reads to fix data race#193
tstapler wants to merge 5 commits into
mainfrom
fix/control-mode-done-channel-race

Conversation

@tstapler

Copy link
Copy Markdown
Collaborator

Summary

Fixes a genuine Go data race in session/tmux/control_mode.go, confirmed via go test -race and currently failing the Test CI job on every PR (pre-existing on main, newly surfaced now that CI can get past the LFS quota blocker far enough to run -race tests).

Root cause

StopControlMode writes t.controlModeDone = nil (and closes it) while holding t.controlModeSubMu.Lock(). Two reader goroutines — readControlModeOutput and monitorControlModeErrors — each captured t.controlModeDone at their first line with a comment acknowledging the hazard ("capture before StopControlMode can nil it"), but neither actually took the lock. The race detector caught the monitorControlModeErrors instance:

Write at 0x00c00011a828 by goroutine 254:
  (*TmuxSession).StopControlMode()  control_mode.go:167
Previous read at 0x00c00011a828 by goroutine 282:
  (*TmuxSession).monitorControlModeErrors()  control_mode.go:322

readControlModeOutput has the identical unsynchronized-read pattern one function up, so it's fixed too for consistency — the race detector just hadn't happened to schedule that interleaving in this run.

Fix

Wrap each capture in controlModeSubMu.RLock()/RUnlock(), matching the existing RLock-for-reads convention already used elsewhere in this file (sendCMCommand, SendInputViaControlMode). The snapshotted channel value remains safe to select on after unlock — only the read of the shared struct field itself needed synchronization.

Testing

  • go test -race ./session/tmux/... -run TestControlModeSurvivesRestart -count=5 — clean, 5/5, no race warnings (this is the regression test from BUG-042, commit b6e76be7d, that was correctly exposing this pre-existing race)
  • go test -race ./session/tmux/... — full package suite, clean
  • go test ./session/tmux/... (no -race) — clean; one test (TestEnsureServerRunning_NoOp) flaked once under full-suite tmux-server contention but passed 3/3 in isolation and on a full-suite re-run, unrelated to this change
  • make build, make lint — clean

Impact

  • Scope: session/tmux/control_mode.go only, two reader goroutines
  • Breaking changes: none
  • Performance: negligible — RLock is only taken once per goroutine at startup to snapshot a channel pointer, not in any hot path
  • Dependencies: none

Reviewer notes

  • Focus areas: confirm the RLock choice is correct (read-only access to controlModeDone, consistent with other RLock reads in this file) rather than a full Lock()
  • Known limitations: none
  • Follow-up tasks: none identified

🤖 Generated with Claude Code

tstapler and others added 5 commits July 27, 2026 09:53
…nsition bugs, live state much improved

Full backlog-feature-improvement skill re-run: ListStuckBacklogItems down to 4 items
(best reading this doc has recorded), root-caused the 2 stale orphaned_triage items to
host swap exhaustion + a missing remediation wiring (not a new lost-event bug). Found
4 new CRITICAL + 4 new MAJOR instances of the recurring swallowed-status-transition
shape via parallel architecture/code/ux review passes, plus confirmed the pipeline-mode
picker and review-gate PipelineEngine coverage gaps are now closed. Also relocates
BUG-042's doc to docs/bugs/fixed/ — its commit (b6e76be) merged 2026-07-25 but the
doc was never moved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
…nsition bugs, live state much improved

Full backlog-feature-improvement skill re-run: ListStuckBacklogItems down to 4 items
(best reading this doc has recorded), root-caused the 2 stale orphaned_triage items to
host swap exhaustion + a missing remediation wiring (not a new lost-event bug). Found
4 new CRITICAL + 4 new MAJOR instances of the recurring swallowed-status-transition
shape via parallel architecture/code/ux review passes, plus confirmed the pipeline-mode
picker and review-gate PipelineEngine coverage gaps are now closed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
Content edit on top of the rename commit — records the fix commit (b6e76be,
merged 2026-07-25) in the doc's status line to match the docs/bugs/fixed/ convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
…mtg-ai#274/smtg-ai#275 outcomes

The swap-exhaustion claim conflated "swap used" with "active memory pressure" —
free -h's available column (24Gi) at the same moment shows the host almost
certainly wasn't under live pressure at audit time, just carrying a stale swap
footprint from an earlier incident. Also records the three draft PRs opened this
session in response to the audit's routing recommendations.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
readControlModeOutput and monitorControlModeErrors each captured
t.controlModeDone without holding controlModeSubMu, racing against
StopControlMode's synchronized close+nil of the same field under that
lock. Confirmed by `go test -race` in CI (WARNING: DATA RACE at
control_mode.go:167 write vs. :322 read), pre-existing on main and
newly surfaced now that the LFS quota CI blocker is fixed and -race
tests can run again.

Both reader goroutines follow the same "capture doneCh before
StopControlMode can nil it" pattern, so both needed the same fix for
consistency, not just the one flagged by the detector. Wrap each
capture in an RLock/RUnlock, matching the RLock-for-reads convention
already used by sendCMCommand and SendInputViaControlMode elsewhere in
this file. The snapshotted channel remains safe to select on after
unlock — only the read of the struct field itself needed
synchronization.

Verified with `go test -race ./session/tmux/... -run
TestControlModeSurvivesRestart -count=5` (clean, 5/5) and the full
`go test -race ./session/tmux/...` suite (clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3683CH7Fs9zYR2yP3Dpba
@github-actions

Copy link
Copy Markdown

✅ Registry Validation

Registry Validation
===================

Building backend scanner...
Scanning backend features...
Wrote 117 feature files to /tmp/tmp.jkwBA8pTzj/backend
Wrote 15 feature files to /tmp/tmp.jkwBA8pTzj/backend
Wrote 45 feature files to /tmp/tmp.jkwBA8pTzj/backend
Wrote 7 feature files to /tmp/tmp.jkwBA8pTzj/backend
Wrote 11 feature files to /tmp/tmp.jkwBA8pTzj/backend

=== Backend Registry Diff ===
Committed: 179  Generated: 179  Divergence: 0.0%
⚠️  110 feature(s) missing // +api: marker (markerFound: false)

✅ Registry validation passed. Divergence: 0.0%

Test Coverage: 19/179 features have testIds (10.6%)

Divergence > 2% blocks merges. Coverage reporting is advisory only.

@tstapler

Copy link
Copy Markdown
Collaborator Author

Closing — this was accidentally opened against the work repo instead of the personal fork (tstapler/stapler-squad), where the equivalent fix was merged as part of smtg-ai#276.

@tstapler tstapler closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant