diff --git a/internal/sandboxrun/bwrap.go b/internal/sandboxrun/bwrap.go index 99bfa1ad..131ef19d 100644 --- a/internal/sandboxrun/bwrap.go +++ b/internal/sandboxrun/bwrap.go @@ -20,7 +20,7 @@ import ( // - system baseline as --ro-bind // - fresh --proc /proc and --dev /dev, --tmpfs /tmp unless granted // - --unshare-pid --unshare-ipc --unshare-uts (NOT --unshare-net) -// - --die-with-parent --new-session +// - --die-with-parent (NOT --new-session, see below) // - protected paths inside granted trees masked with --tmpfs (dirs) // or --ro-bind /dev/null (files), honoring override_deny // @@ -34,7 +34,15 @@ func BuildBwrapArgv(g *Grants, stage2Argv []string) ([]string, error) { argv := []string{ "bwrap", "--die-with-parent", - "--new-session", + // Deliberately NOT --new-session. setsid(2) detaches the inner + // process tree from the controlling terminal, which stops the + // kernel from delivering SIGWINCH on window resize — so an inner + // TUI (e.g. opencode) renders once and never reflows. --new-session + // exists only to block TIOCSTI terminal-input injection; that + // vector is gated off by the kernel on Linux >= 6.2 via the + // dev.tty.legacy_tiocsti sysctl (default 0). We keep the real + // terminal so resize works; filesystem/network/PID isolation is + // unaffected (it comes from the binds, Landlock, and --unshare-*). "--unshare-pid", "--unshare-ipc", "--unshare-uts", diff --git a/internal/sandboxrun/bwrap_test.go b/internal/sandboxrun/bwrap_test.go index 2623ef07..317346de 100644 --- a/internal/sandboxrun/bwrap_test.go +++ b/internal/sandboxrun/bwrap_test.go @@ -35,7 +35,6 @@ func TestBwrapArgvStructure(t *testing.T) { for _, want := range []string{ "bwrap", "--die-with-parent", - "--new-session", "--unshare-pid", "--unshare-ipc", "--unshare-uts", @@ -57,6 +56,11 @@ func TestBwrapArgvStructure(t *testing.T) { if strings.Contains(joined, "--unshare-net") { t.Error("network namespace must NOT be unshared") } + // --new-session detaches the controlling terminal and breaks SIGWINCH + // resize propagation into the inner TUI; it must stay out. + if strings.Contains(joined, "--new-session") { + t.Error("must NOT use --new-session (breaks terminal resize/SIGWINCH)") + } } func TestBwrapNoTmpfsWhenTmpGranted(t *testing.T) {