From 45261a2d6da77a848610d766e6a171e553557657 Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:07:24 -0400 Subject: [PATCH 1/3] fix(tools): block MSYS and WSL shells under the Windows sandbox Git-for-Windows bash.exe and sh.exe are MSYS binaries: the MSYS runtime secures its signal pipe and shared-memory sections with explicit DACLs granting only the user, Administrators, and SYSTEM (sigproc.cc sigproc_init -> sec_user_nih), so under the WRITE_RESTRICTED token the second access check can never pass and startup dies with "couldn't create signal pipe" or "CreateFileMapping .1", Win32 error 5. The System32 WSL bash launcher fails equivalently: the restricted token cannot connect to the WSL service (Bash/Service/CreateInstance/ E_ACCESSDENIED). Every executable a bare bash can resolve to fails, so block bash and sh preflight like the MSYS coreutils from #476, mention the indirect spawn paths (git hooks, git/gh credential helpers) in the shared suggestion, detect the WSL launcher's UTF-16LE service-denied stderr, and document the root cause in the runner's KNOWN LIMITATION comment beside the Schannel note. --- .../sandbox/windows_command_runner_windows.go | 18 +++++++ internal/tools/shell_runtime.go | 36 ++++++++++--- internal/tools/shell_runtime_test.go | 50 ++++++++++++++++++- 3 files changed, 95 insertions(+), 9 deletions(-) diff --git a/internal/sandbox/windows_command_runner_windows.go b/internal/sandbox/windows_command_runner_windows.go index 6fefc4966..516187849 100644 --- a/internal/sandbox/windows_command_runner_windows.go +++ b/internal/sandbox/windows_command_runner_windows.go @@ -50,6 +50,24 @@ func runWindowsSandboxCommand(config WindowsSandboxCommandConfig, stderr io.Writ // mitigation is to run TLS in a broker process, not the sandboxed one) and // has no clean in-token fix. Workarounds: the degraded path (no restricted // token) or the in-process web_fetch tool. + // + // KNOWN LIMITATION: MSYS2/Cygwin binaries (Git for Windows bash.exe, + // sh.exe, and the usr\bin coreutils) cannot initialize under this token at + // all, whether invoked directly or spawned internally by an otherwise + // native command (git hooks, git/gh credential helpers). The MSYS runtime + // secures its signal pipe and shared-memory sections with explicit DACLs + // granting only the user, Administrators, and SYSTEM (msys2-runtime + // sigproc.cc sigproc_init -> sec_user_nih -> __sec_user), and a + // WRITE_RESTRICTED write check must ALSO match one of the token's + // restricted SIDs (logon SID, Everyone, capability SIDs). None of the + // granted SIDs can be added to the restricted list without collapsing the + // write jail (each has write access nearly everywhere), so MSYS startup + // dies with "couldn't create signal pipe" or "CreateFileMapping .1", + // Win32 error 5, and exit status 0xC0000142. The System32 WSL bash + // launcher fails equivalently (the restricted token cannot connect to the + // WSL service: Bash/Service/CreateInstance/E_ACCESSDENIED). Like Schannel, + // this has no in-token fix; preflight blocking and output hints live in + // internal/tools/shell_runtime.go. tokenSIDs := windowsRuntimeTokenSIDs(capabilitySIDs, offlineSID, config.PermissionProfile.Network.Mode) token, err := createWindowsRestrictedTokenForCapabilitySIDs(tokenSIDs) if err != nil { diff --git a/internal/tools/shell_runtime.go b/internal/tools/shell_runtime.go index 11e30e4fd..645d88be9 100644 --- a/internal/tools/shell_runtime.go +++ b/internal/tools/shell_runtime.go @@ -20,21 +20,29 @@ type shellIssue struct { const windowsMsysSandboxKind = "windows_msys_sandbox" -const windowsMsysSandboxSuggestion = "MSYS/Cygwin coreutils from Git for Windows cannot run under Zero's write-restricted Windows sandbox. Prefer Zero native tools (grep, read_file with offset/limit, list_directory, glob), cmd.exe findstr, or PowerShell Select-Object -First/-Last. If host-level execution is truly required, rerun with sandbox_permissions: \"require_escalated\" and a narrow justification." +const windowsMsysSandboxSuggestion = "MSYS/Cygwin coreutils and shells (bash, sh) from Git for Windows cannot run under Zero's write-restricted Windows sandbox, and the WSL bash launcher cannot reach the WSL service from it either. This also hits native commands that spawn Git Bash internally: git hooks and git/gh credential helpers can fail this way even though git and gh themselves run fine. Prefer Zero native tools (grep, read_file with offset/limit, list_directory, glob), cmd.exe findstr, or PowerShell Select-Object -First/-Last. If host-level execution is truly required, rerun with sandbox_permissions: \"require_escalated\" and a narrow justification." -// windowsMsysProneNames is the single source of truth for POSIX coreutil names -// that commonly resolve to a Git-for-Windows MSYS/Cygwin binary rather than a -// cmd.exe-native command, and so fail under the write-restricted Windows -// sandbox (#458). Every Windows MSYS-detection path (the preflight command -// scan below, the exported MsysProneCommandName, and the known-safe-segment -// guard in internal/agent/command_prefix.go) derives from this one set, so -// they cannot drift out of sync with each other. +// windowsMsysProneNames is the single source of truth for POSIX coreutil and +// shell names that commonly resolve to a Git-for-Windows MSYS/Cygwin binary +// rather than a cmd.exe-native command, and so fail under the write-restricted +// Windows sandbox (#458). Every Windows MSYS-detection path (the preflight +// command scan below, the exported MsysProneCommandName, and the +// known-safe-segment guard in internal/agent/command_prefix.go) derives from +// this one set, so they cannot drift out of sync with each other. var windowsMsysProneNames = map[string]bool{ "cat": true, "cut": true, "expr": true, "grep": true, "head": true, "id": true, "ls": true, "nl": true, "paste": true, "rev": true, "seq": true, "stat": true, "tail": true, "tr": true, "uname": true, "uniq": true, "wc": true, "which": true, "awk": true, "sed": true, "xargs": true, + // Shells. Git-for-Windows bash.exe/sh.exe are MSYS binaries and die during + // MSYS runtime init under the restricted token ("couldn't create signal + // pipe" or "CreateFileMapping .1", both Win32 error 5), and the + // System32 WSL bash launcher fails equivalently at a different layer (the + // restricted token cannot connect to the WSL service: + // Bash/Service/CreateInstance/E_ACCESSDENIED), so every executable a bare + // `bash` can resolve to fails under the sandbox. + "bash": true, "sh": true, } var ( @@ -255,6 +263,9 @@ func detectShellOutputIssue(output string, goos string) *shellIssue { if msysRuntimeFailedInOutput(lower) { return windowsMsysSandboxIssue("An MSYS/Cygwin runtime failed under Zero's Windows sandbox (ACCESS_DENIED during MSYS startup).") } + if wslServiceDeniedInOutput(lower) { + return windowsMsysSandboxIssue("WSL bash could not connect to the WSL service under Zero's Windows sandbox (Bash/Service/CreateInstance/E_ACCESSDENIED).") + } if strings.Contains(lower, "the syntax of the command is incorrect") || strings.Contains(lower, "is not recognized as an internal or external command") { return &shellIssue{ @@ -292,6 +303,15 @@ func msysRuntimeFailedInOutput(lower string) bool { strings.Contains(lower, "[main]") } +// wslServiceDeniedInOutput matches the WSL bash launcher's failure to open its +// service connection under the restricted token. The launcher writes UTF-16LE +// to its (piped, non-console) stderr, which the byte-based capture renders as +// ASCII interleaved with NUL bytes, so the NULs are stripped before matching. +func wslServiceDeniedInOutput(lower string) bool { + compact := strings.ReplaceAll(lower, "\x00", "") + return strings.Contains(compact, "bash/service/") && strings.Contains(compact, "e_accessdenied") +} + func appendShellIssueHint(output string, issue shellIssue) string { output = strings.TrimRight(output, "\r\n") hint := "[zero] shell issue: " + issue.Message diff --git a/internal/tools/shell_runtime_test.go b/internal/tools/shell_runtime_test.go index 6c3ac5da4..977306545 100644 --- a/internal/tools/shell_runtime_test.go +++ b/internal/tools/shell_runtime_test.go @@ -30,6 +30,35 @@ func TestDetectShellCommandIssueFlagsStandaloneCat(t *testing.T) { } } +// TestDetectShellCommandIssueFlagsShells covers bash/sh invocations: every +// executable a bare `bash` resolves to on Windows fails under the restricted +// token (Git-for-Windows MSYS bash dies during runtime init; the System32 WSL +// launcher is denied its WSL service connection), so both names are blocked +// upfront like the MSYS coreutils. +func TestDetectShellCommandIssueFlagsShells(t *testing.T) { + for _, command := range []string{ + `bash -c "make test"`, + `bash.exe -lc ls`, + `sh -c "echo hi"`, + `git status && bash -c "echo hi"`, + } { + issue := detectShellCommandIssue(command, "windows") + if issue == nil || issue.Kind != "windows_msys_sandbox" { + t.Fatalf("expected windows_msys_sandbox for %q, got %#v", command, issue) + } + } + + // Shell names inside quoted argument text are not invocations. + for _, command := range []string{ + `git commit -m "bash fails under the sandbox"`, + `gh pr comment --body "run sh -c manually"`, + } { + if issue := detectShellCommandIssue(command, "windows"); issue != nil { + t.Fatalf("expected quoted shell mention to pass for %q, got %#v", command, issue) + } + } +} + func TestDetectShellOutputIssueFlagsMsysCreateFileMappingError(t *testing.T) { output := `0 [main] head (3568) C:\Program Files\Git\usr\bin\head.exe: *** fatal error - CreateFileMapping S-1-5-21-3149109338-1484423945-518236903-1001.1, Win32 error 5. Terminating.` issue := detectShellOutputIssue(output, "windows") @@ -58,6 +87,25 @@ func TestDetectShellOutputIssueFlagsMsysTerminatingWithMsysMarker(t *testing.T) } } +// TestDetectShellOutputIssueFlagsWslServiceDenied pins detection of the WSL +// bash launcher's failure under the restricted token. The launcher writes +// UTF-16LE to a piped stderr, so the captured text carries a NUL byte after +// every ASCII character; the fixture reproduces that shape. +func TestDetectShellOutputIssueFlagsWslServiceDenied(t *testing.T) { + var utf16ish strings.Builder + for _, r := range "Access is denied.\r\nError code: Bash/Service/CreateInstance/E_ACCESSDENIED\r\n" { + utf16ish.WriteRune(r) + utf16ish.WriteByte(0) + } + issue := detectShellOutputIssue(utf16ish.String(), "windows") + if issue == nil || issue.Kind != "windows_msys_sandbox" { + t.Fatalf("expected WSL service-denied output issue, got %#v", issue) + } + if !strings.Contains(issue.Message, "WSL") { + t.Fatalf("expected WSL-specific message, got %#v", issue) + } +} + func TestDetectShellOutputIssueIgnoresNonMsysWin32Error5(t *testing.T) { output := `myapp.exe: unable to open service handle, Win32 error 5 (access denied). Terminating worker.` issue := detectShellOutputIssue(output, "windows") @@ -82,7 +130,7 @@ func TestShellIssueBlockResultMsysCommand(t *testing.T) { } func TestMsysProneCommandName(t *testing.T) { - if !MsysProneCommandName("HEAD") || MsysProneCommandName("echo") { + if !MsysProneCommandName("HEAD") || !MsysProneCommandName("bash") || MsysProneCommandName("echo") { t.Fatalf("unexpected MsysProneCommandName results") } } From d1c4a1f678180675d09b2c48e1ac13861ea78236 Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Wed, 8 Jul 2026 00:13:54 -0400 Subject: [PATCH 2/3] fix(tui): align picker_test switchProviderModel call with 4-value return --- internal/tui/picker_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/picker_test.go b/internal/tui/picker_test.go index 93f144b99..e9532ef54 100644 --- a/internal/tui/picker_test.go +++ b/internal/tui/picker_test.go @@ -768,7 +768,7 @@ func TestSwitchProviderModelRecordsRecentHistory(t *testing.T) { }, }) - next, status, _ := m.switchProviderModel("ollama", "kimi-k2.7-code:cloud") + next, status, _, _ := m.switchProviderModel("ollama", "kimi-k2.7-code:cloud") wantStatus := "Model\nSwitched to ollama ยท kimi-k2.7-code:cloud" if status != wantStatus { t.Fatalf("switchProviderModel() status = %q, want %q (a mismatch here means the switch itself failed, not the recentModels assertion below)", status, wantStatus) From ba234796408bdd4c6b1fa5ceddfb3422d5a522df Mon Sep 17 00:00:00 2001 From: euxaristia <25621994+euxaristia@users.noreply.github.com> Date: Wed, 8 Jul 2026 03:10:25 -0400 Subject: [PATCH 3/3] test(tools): cover sh.exe in Windows shell blocking test --- internal/tools/shell_runtime_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/tools/shell_runtime_test.go b/internal/tools/shell_runtime_test.go index 977306545..63bfef864 100644 --- a/internal/tools/shell_runtime_test.go +++ b/internal/tools/shell_runtime_test.go @@ -40,6 +40,7 @@ func TestDetectShellCommandIssueFlagsShells(t *testing.T) { `bash -c "make test"`, `bash.exe -lc ls`, `sh -c "echo hi"`, + `sh.exe -c "echo hi"`, `git status && bash -c "echo hi"`, } { issue := detectShellCommandIssue(command, "windows")