Severity: High (stream corruption / runs a command in the wrong sandbox)
1. Concurrent Send on a bidi stream
pkg/sandbox/exec_interactive.go:69-95: the stdin-pump goroutine and the resize goroutine both call bidi.Send. grpc-go forbids concurrent SendMsg on one stream. A resize while typing corrupts frames or trips -race. Fix: a single sender goroutine fed by a channel of *pb.ExecSandboxInteractiveRequest; stdin and resize producers write to that channel. Also: ExecInteractive returns on Exit (:123) leaving the RPC and stdin goroutine alive, and BidiStream has no Close; raw.go:87-96 promises the caller cancels but nothing calls bidiStream.cancel.
2. Positional NAME before -- is silently discarded
commandArgs (internal/cli/sandbox_create.go:357) drops everything before --, and the name then falls back to last_sandbox. sandbox exec mybox -- rm -rf build runs in whatever sandbox was last used. Every sibling command accepts a positional NAME, so this is a natural thing to type. Fix: an Args validator that rejects (or accepts exactly one as NAME) pre-dash positionals.
3. Auto-detected TTY exec hangs
internal/cli/sandbox_exec.go:68 takes the interactive path only when --tty is explicit. With both fds TTYs and no flag, the streaming path sends Tty:true, Cols:0, Rows:0 and no stdin, so exec -- vim hangs. Fix: useTTY && stdinTTY (matching upstream run.rs:1464 semantics).
(Streams never receiving the Unauthenticated retry is a separate gateway-layer defect — see item 24.)
Acceptance
-race test with concurrent stdin + resize on the mock bidi stream.
- CLI test:
exec NAME -- cmd targets NAME (or errors), never last_sandbox.
- Test that
ExecInteractive cancels the RPC and stops the stdin goroutine after Exit (goleak).
Generated by Claude Code
Severity: High (stream corruption / runs a command in the wrong sandbox)
1. Concurrent
Sendon a bidi streampkg/sandbox/exec_interactive.go:69-95: the stdin-pump goroutine and the resize goroutine both callbidi.Send. grpc-go forbids concurrentSendMsgon one stream. A resize while typing corrupts frames or trips-race. Fix: a single sender goroutine fed by a channel of*pb.ExecSandboxInteractiveRequest; stdin and resize producers write to that channel. Also:ExecInteractivereturns onExit(:123) leaving the RPC and stdin goroutine alive, andBidiStreamhas noClose;raw.go:87-96promises the caller cancels but nothing callsbidiStream.cancel.2. Positional NAME before
--is silently discardedcommandArgs(internal/cli/sandbox_create.go:357) drops everything before--, and the name then falls back tolast_sandbox.sandbox exec mybox -- rm -rf buildruns in whatever sandbox was last used. Every sibling command accepts a positional NAME, so this is a natural thing to type. Fix: anArgsvalidator that rejects (or accepts exactly one as NAME) pre-dash positionals.3. Auto-detected TTY exec hangs
internal/cli/sandbox_exec.go:68takes the interactive path only when--ttyis explicit. With both fds TTYs and no flag, the streaming path sendsTty:true, Cols:0, Rows:0and no stdin, soexec -- vimhangs. Fix:useTTY && stdinTTY(matching upstream run.rs:1464 semantics).(Streams never receiving the
Unauthenticatedretry is a separate gateway-layer defect — see item 24.)Acceptance
-racetest with concurrent stdin + resize on the mock bidi stream.exec NAME -- cmdtargets NAME (or errors), neverlast_sandbox.ExecInteractivecancels the RPC and stops the stdin goroutine afterExit(goleak).Generated by Claude Code