Skip to content

Commit f83c279

Browse files
committed
Serialize stdio-capturing HTTP handlers to prevent cross-request leaks
1 parent 12dfaf4 commit f83c279

1 file changed

Lines changed: 13 additions & 0 deletions

File tree

src/repl/serve.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
// ServerManager manages the background web server
3131
type ServerManager struct {
3232
mu sync.RWMutex
33+
captureMu sync.Mutex // serializes handlers that hijack process stdio and REPL config
3334
status ServerStatus
3435
server *http.Server
3536
config *llm.Config
@@ -263,6 +264,13 @@ func (sm *ServerManager) executeInputWithCapture(input string, stream bool, syst
263264
return "", fmt.Errorf("REPL not available")
264265
}
265266

267+
// This handler mutates process-global stdio and REPL config to thread
268+
// per-request overrides into sendToAI. Serialize against any other
269+
// capturing handler so concurrent requests cannot steal each other's
270+
// output or restore the wrong config values.
271+
sm.captureMu.Lock()
272+
defer sm.captureMu.Unlock()
273+
266274
// Optionally override streaming and system prompt for this call
267275
oldStream := sm.repl.configOptions.Get("llm.stream")
268276
oldSystem := sm.repl.configOptions.Get("llm.systemprompt")
@@ -386,6 +394,11 @@ func (sm *ServerManager) GetStatusString() string {
386394

387395
// executeCommandWithCapture executes a REPL command and captures its output
388396
func (sm *ServerManager) executeCommandWithCapture(command string) (string, error) {
397+
// Shares process-global stdio with executeInputWithCapture, so it must
398+
// take the same lock to avoid two concurrent requests racing on os.Stdout.
399+
sm.captureMu.Lock()
400+
defer sm.captureMu.Unlock()
401+
389402
// Create pipes to capture both stdout and stderr
390403
oldStdout := os.Stdout
391404
oldStderr := os.Stderr

0 commit comments

Comments
 (0)