Skip to content

Fix cmd.exe /S quote desync and gh-stub EINVAL on Windows - #20

Merged
erwin-wee merged 2 commits into
mainfrom
fix/windows-cmd-quoting-gh-stub-einval
Sep 18, 2026
Merged

erwin-wee merged 2 commits into
mainfrom
fix/windows-cmd-quoting-gh-stub-einval

Conversation

@erwin-wee

Copy link
Copy Markdown
Owner

Fixes #3 (partially — see scope note below).

Root causes fixed

buildWindowsCmdInvocation / cmdEscapeArgument (src/main/exec.ts) — the actual reason the .cmd spawn bug survived #2. The remainder passed to cmd.exe /d /s /c was a sequence of separately quoted-and-caret-escaped segments ("file" "arg1" "arg2"...) with no outer wrapper. Per cmd /?, /S strips the first character (if a quote) and the last quote character found anywhere in the remainder — not the last character of the string. With no wrapper, that second strip landed on the closing quote of the last argument instead of a deliberate boundary, desyncing every argument's quote pairing — exactly reproducing & was unexpected at this time. Fix: wrap the whole escaped command in one more outer quote pair, matching the pattern already used correctly elsewhere in this file (startOnWindows) and in check-runner.ts's buildCheckInvocation.

gh-stub.test.ts spawnSync ... EINVAL — that test drives gh.cmd directly via execFileSync, bypassing exec()'s cmd.exe indirection entirely (as the issue notes: "This is spawnSync, not exec(), so it never went through the fix in #2 at all"). Node refuses to spawn .cmd/.bat without shell: true (the CVE-2024-27980 mitigation) and throws EINVAL synchronously — matching the report exactly. Added shell: true on Windows for the three direct launcher invocations.

test/exec-windows-cmd.test.ts now simulates the /S stripping rule itself and asserts the round-trip back to the correctly escaped inner command. The previous test actually asserted the unwrapped shape was correct — that assertion was the bug.

Not touched

worktrees.test.ts (4), repo-health.test.ts (1), and signing.test.ts's real-GPG case remain unexplained per the issue thread ("likely path or line-ending handling... needs to be worked out against a real Windows shell rather than reasoned about — two attempts have now been wrong"). I don't have a Windows host to verify a fix against, so I left these alone rather than guess a third time. Also left the if: runner.os == 'Linux' CI guards in place — removing them before those are fixed would just turn CI red.

Verification

  • npx vitest run test/exec-windows-cmd.test.ts test/fixture/gh-stub.test.ts test/check-runner.test.ts — 24/24 pass.
  • npx tsc --noEmit — clean.
  • The /S-stripping simulation added to exec-windows-cmd.test.ts is the strongest proof available without an actual Windows runner; recommend re-measuring the fixture suite on Windows CI against this branch.

erwin-wee and others added 2 commits September 19, 2026 03:24
buildWindowsCmdInvocation joined per-argument cmd-escaped segments
("file" "arg1" "arg2"...) without an outer wrapping quote pair. cmd's
/S rule strips the first character (if a quote) and the *last quote
character anywhere in the remainder*, not the last character of the
string -- with no wrapper, that second strip landed on the closing
quote of the last argument instead of a deliberate boundary, desyncing
every argument's quoting and reproducing the reported
"& was unexpected at this time". Wrap the whole escaped command in one
more outer quote pair, matching the pattern already used correctly by
startOnWindows and check-runner.ts's buildCheckInvocation in this same
codebase.

gh-stub.test.ts drove gh.cmd directly via execFileSync, bypassing
exec()'s cmd.exe indirection entirely. Node refuses to spawn .cmd/.bat
files without shell: true (the CVE-2024-27980 mitigation) and throws
EINVAL synchronously, matching the reported
"spawnSync ... gh.cmd EINVAL". Add shell: true on Windows for those
three direct launcher invocations.

test/exec-windows-cmd.test.ts now simulates the /S stripping rule and
asserts the round-trip back to the correctly escaped inner command;
the previous test asserted the unwrapped shape, which was the bug.

worktrees.test.ts, repo-health.test.ts and signing.test.ts's gpg case
remain unexplained per the issue thread and are not touched here --
verifying a fix needs a real Windows host, and two prior attempts at
reasoning about them from source were wrong.

Fixes #3
@erwin-wee
erwin-wee merged commit 472667c into main Sep 18, 2026
2 checks passed
@erwin-wee
erwin-wee deleted the fix/windows-cmd-quoting-gh-stub-einval branch September 19, 2026 15:36
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.

Fixture and smoke suites are Linux-only in CI; 16 fixture tests fail on Windows

1 participant