Skip to content

fix(win): escape spawned command-line args so the index worker gets valid argv#881

Merged
DeusData merged 1 commit into
DeusData:mainfrom
Flipper1994:fix/win-index-worker-cmdline-quoting
Jul 5, 2026
Merged

fix(win): escape spawned command-line args so the index worker gets valid argv#881
DeusData merged 1 commit into
DeusData:mainfrom
Flipper1994:fix/win-index-worker-cmdline-quoting

Conversation

@Flipper1994

@Flipper1994 Flipper1994 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes the Windows-only smoke-windows (standard + ui) failure in the final release dry run, where index_repository reported outcome=exit_nonzero and "Indexing worker crashed on a file" on big_templated.hpp.

The failure is not the #424 allocator class and not big_templated.hpp. It is a command-line quoting bug in the Windows index-worker spawn. The file parses fine; the worker never reaches it.

Relationship to #838

This is the Windows residual of #838. #838 fixed the CLI parser to accept the supervisor's raw-JSON-positional + --response-out flag layout — verified there with a PowerShell repro, where PowerShell passes the JSON as one intact argument. On current main a correctly-delivered JSON arg does parse (confirmed: running the worker directly indexes clean).

But the real supervisor spawns the worker via CreateProcessA, not PowerShell. Its command-line re-parse strips the JSON's quotes, so on Windows the now-fixed parser still receives corrupted JSON. #838 was closed against the parser; this PR fixes the spawn-side quoting that #838's repro never exercised. (See also #423 — the open Windows "repo_path is required, path is specified" report.)

Root cause

The supervisor spawns:

<self> cli --index-worker index_repository <args_json> --response-out <file>

args_json is the raw tool JSON, e.g. {"repo_path":"C:/r"} — it contains double-quotes. On Windows, cbm_run_win built the CreateProcess command line by wrapping each argv element in bare quotes without escaping:

snprintf(cmdline, ..., "%s\"%s\"", (i ? " " : ""), argv[i]);  // no escaping

Windows re-parses that single string back into argv and strips the inner quotes, so the child received {repo_path:C:/r} — invalid JSON → repo_path is required → exit 1. The supervisor classified exit_nonzero and blamed the last-marked file (big_templated.hpp), producing the misleading crash message.

Proof: a standalone CreateProcessA reproducer built with the exact cbm_run_win construction made an argv-printer receive argv[4]=<{repo_path:C:/r}>; feeding the worker that exact mangled JSON reproduced the failure signature byte-for-byte.

Why POSIX is unaffected: cbm_run_posix passes the argv array straight to execv — no command-line re-parse. That is why Linux/macOS index the same fixture fine and only smoke-windows was red.

Fix

cbm_build_win_cmdline() applies the Microsoft C runtime quoting rules (quote-wrap + escape embedded quotes and their preceding backslash runs) so the child re-parses byte-identical argv. It is defined unconditionally (pure string logic, no Windows headers) so the quoting contract is unit-tested on Linux/macOS CI too, and both spawn sites share one tested implementation.

Also in this PR

  • ui/http_server.c — the UI index spawn had a byte-for-byte duplicate of the same bug (its own "%s" cmdline for the JSON arg). Now routed through cbm_build_win_cmdline.
  • mcp/index_supervisor.c (observability) — log the worker's raw exit_code and keep its log on any failure (delete only on a clean run). Previously the log was always deleted and only outcome+signal were logged, so a non-zero worker left nothing to diagnose — the CI blind spot that hid this bug.

Scope

Kept atomic to the CreateProcess argv-quoting bug class. A Windows quoting audit of the tree also found pipeline/artifact.c's two single-quoted, unvalidated git -C '%s' shell-outs — but that is a different bug class (shell/popen command-string quoting, not CreateProcess argv re-parse), so per review it is split into a follow-up PR with its own guard test. The other CreateProcess sites are safe (argv-array escaping via cbm_exec_no_shell, or "-rejecting validation).

Test

tests/test_subprocess.c gains a Layer-3 guard for cbm_build_win_cmdline, verified by round-trip: a reference implementation of the Windows CommandLineToArgvW rules (the inverse of the builder) must re-parse the emitted line back into the exact original argv. Cases cover the failing JSON, spaces/tabs, embedded quotes, backslash runs, trailing backslashes, backslash-before-quote, real Windows paths, and overflow. Testing the invariant (not a hand-computed escaped string) keeps the guard honest and runs on every platform.

Verification (CLANG64 — the toolchain CI ships)

  • Before: supervised big_templated.hppindex.supervisor.reap outcome=exit_nonzero, exit 1.
  • After: index.supervisor.reap outcome=clean exit_code=0 signal=0, status:indexed (9005 nodes), exit 0.
  • Also confirmed on the native Windows smoke-invariants leg: PASS: index-cli (nodes=29, rc=0) — the supervised index_repository that returns nodes=0 without the fix.
  • subprocess suite: 11 passed / 5 skipped (POSIX-only spawn tests).

Refs #838, #423.

@Flipper1994 Flipper1994 requested a review from DeusData as a code owner July 5, 2026 11:13
@Flipper1994 Flipper1994 marked this pull request as draft July 5, 2026 11:29
@Flipper1994 Flipper1994 force-pushed the fix/win-index-worker-cmdline-quoting branch from 1699938 to 7187500 Compare July 5, 2026 11:37
@DeusData

DeusData commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Thank you — this is genuinely excellent work. The diagnosis is spot-on (spawn-side CreateProcessA quoting, not the allocator/big_templated.hpp), the cbm_build_win_cmdline escaping matches the canonical MS-CRT rules exactly (and lines up byte-for-byte with our existing cbm_build_cmdline), both vulnerable spawn sites are covered, and the round-trip test is a real, cross-platform guard. We want to land it.

Two small things before we merge, then it's good to go:

1. Please drop the AI attribution from the commit. Our public repos keep commit messages human-authored — no Co-Authored-By: Claude … / claude co-author. Could you git commit --amend to remove that trailer (keep your own Signed-off-by: — DCO needs it, and it already matches your author email 👍), then git push --force-with-lease?

2. Please split out the artifact.c change into its own follow-up PR. It's a good hardening, but it's a different bug class — shell/popen command-string quoting rather than the CreateProcess argv re-parse this PR fixes — and it lands without a test. We keep PRs atomic + reproduce-first, so pulling it into a separate PR (with a small guard test) keeps this one focused on the argv-quoting fix. Happy to fast-track that follow-up. The subprocess.c + http_server.c fix, the test, and the index_supervisor.c observability are all great to keep here.

Once those two are done we'll merge it. Heads up: we're landing it right alongside a smoke-CI fix that makes the native windows-latest smoke leg actually run and gate (it wasn't, due to a matrix bug) — so your fix will get validated on native Windows in CI, not just the emulated leg. Really appreciate you jumping on this. 🙏

…alid argv

Fixes the Windows-only smoke-windows (standard + ui) failure in the release dry
run, where index_repository reported outcome=exit_nonzero and "Indexing worker
crashed on a file" on big_templated.hpp. It is not the DeusData#424 allocator class and
not big_templated.hpp — the file parses fine; the worker never reaches it.

This is the Windows residual of DeusData#838. That issue fixed the CLI parser to accept
the supervisor's raw-JSON-positional + --response-out flag layout, verified with a
PowerShell repro where the JSON argument arrives intact. But the real supervisor
spawns the worker via CreateProcessA, whose command-line re-parse strips the JSON's
quotes, so on Windows the now-fixed parser still receives corrupted JSON.

On Windows the index supervisor spawns the worker as
`<self> cli --index-worker index_repository <args_json> ...`, but cbm_run_win
built the CreateProcess command line by wrapping each argv element in bare quotes
with no escaping. Windows re-parses that single string back into argv and strips
the inner quotes, so the JSON argument {"repo_path":"..."} arrived at the child as
{repo_path:...} — invalid JSON. The worker exited non-zero at arg parse
("repo_path is required"); the supervisor classified exit_nonzero and blamed the
last-marked file (big_templated.hpp), producing the misleading crash message.
POSIX is unaffected: cbm_run_posix passes the argv array straight to execv with no
re-parse, which is why only smoke-windows was red while Linux/macOS indexed the
same fixture fine.

Fix: cbm_build_win_cmdline() applies the Microsoft C runtime quoting rules
(quote-wrap + escape embedded quotes and their preceding backslash runs) so the
child re-parses byte-identical argv. It is defined unconditionally (pure string
logic, no Windows headers) and unit-tested on every platform via a round-trip
against a reference CommandLineToArgvW parser.

Also in this change:
- ui/http_server.c: the UI index spawn had a byte-for-byte duplicate of the same
  bug (its own "%s" cmdline). Route it through cbm_build_win_cmdline too.
- mcp/index_supervisor.c: log the worker's raw exit_code and KEEP its log on any
  failure (delete only on a clean run). Previously the log was always deleted and
  only outcome+signal were logged, so a non-zero worker left nothing to diagnose —
  the CI blind spot that hid this bug behind a generic message.

A separate hardening of pipeline/artifact.c's single-quoted git shell-outs (a
different bug class — popen command-string quoting, not CreateProcess argv) is
split into a follow-up PR with its own guard test, per review.

Verified on a CLANG64 build: the supervised big_templated.hpp fixture now indexes
clean (reap outcome=clean exit_code=0); subprocess suite 11/11, affected suites
(git_context, index_resilience, ui, httpd, mcp, security, str_util) green.

Refs: DeusData#838, DeusData#423
Signed-off-by: Flipper <jacobphilipp@ymail.com>
@Flipper1994 Flipper1994 force-pushed the fix/win-index-worker-cmdline-quoting branch from 7187500 to 982916b Compare July 5, 2026 11:50
@Flipper1994 Flipper1994 marked this pull request as ready for review July 5, 2026 11:59
@DeusData DeusData enabled auto-merge July 5, 2026 12:48
@DeusData DeusData disabled auto-merge July 5, 2026 12:50
Flipper1994 added a commit to Flipper1994/codebase-memory-mcp that referenced this pull request Jul 5, 2026
…ed repo paths

artifact.c shells out to git twice via cbm_popen with the repo path interpolated
straight into the command:

    git -C '%s' rev-parse HEAD 2>/dev/null                  (git_head_hash)
    git -C '%s' config merge.ours.driver true 2>/dev/null   (ensure_gitattributes)

Both used SINGLE quotes with NO validation of repo_path. cmd.exe does not honor
single quotes, so on Windows a repo path containing a space broke argument grouping
(the artifact commit hash and merge-driver config silently failed for any spaced repo
path), and an embedded quote or shell metacharacter could break out of the intended
argument. This was the only unvalidated git shell interpolation left in the tree.

Different bug class from the CreateProcess argv-quoting fix (which re-parses a single
command STRING back into argv); this is popen/shell command-string quoting — hence a
separate, atomic PR per review.

Fix: validate repo_path with cbm_artifact_repo_path_is_shell_safe() and switch to
double quotes + a per-platform null device, matching git_context.c (the best-hardened
git shell-out): cbm_validate_shell_arg rejects quote / backslash / substitution
metacharacters, and on Windows we also reject the cmd.exe expansion metacharacters
% ! ^. Double quotes are honored by both POSIX sh and cmd.exe, so a legitimate spaced
repo path now works — the concrete regression the single-quote form caused.

The validator is exposed (artifact.h) and guarded by a new test_artifact.c suite:
plain + spaced paths accepted; quote / ; / $() / backtick / pipe injection rejected;
the cmd.exe metacharacters % ! ^ rejected on Windows (allowed on POSIX, where they are
literal inside double quotes).

Refs: DeusData#881
Signed-off-by: Flipper <jacobphilipp@ymail.com>
@DeusData DeusData merged commit 3714266 into DeusData:main Jul 5, 2026
15 checks passed
@DeusData

DeusData commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Merged — thank you, @Flipper1994. Sharp diagnosis and a clean fix: the crash was never the allocator or big_templated.hpp at all, but the Windows CreateProcessA spawn wrapping the JSON arg in bare quotes, so the re-parse stripped them and the worker got invalid JSON. The MS-CRT quoting in cbm_build_win_cmdline (shared across both spawn sites), the round-trip test, and the exit-code observability are all excellent, and the artifact.c split into #886 was exactly right. This unblocks the release. Really appreciate you jumping on it. 🙏

@Flipper1994

Copy link
Copy Markdown
Contributor Author

Thanks again for the fast merge! 🙏

One honest post-review note: during a self-review after this was already merged, I spotted a small contract inaccuracy in cbm_build_win_cmdline — on the overflow path it return falses without NUL-terminating buf, although the header comment says "buf then holds a truncated string". No live bug — both call sites (cbm_run_win, the http_server spawn) ignore buf when the function returns false — but a future caller trusting the doc could OOB-read. I have a tiny tidy ready that adds buf[0] = '\0' on that path, corrects that comment plus one stale comment on cbm_cmdline_put, and bounds-guards the reference parser in the round-trip test.

Happy to open it as a small atomic follow-up, or leave main as-is if it's not worth the round-trip — your call. It's pure polish, not a blocker.

@DeusData

DeusData commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Thanks for the honest post-merge self-review, @Flipper1994 — I verified it and you're right on all three points. subprocess.h promises "buf then holds a truncated string" on overflow, but the overflow path (if (ovf) return false;) skips the buf[pos] = '\0' that only runs on the success path, so buf is left unterminated. Both call sites (cbm_run_win, http_server index spawn) ignore buf on false, so it's latent — no live bug — but the contract is wrong and a future caller could OOB-read.

Yes please, open it as a small atomic follow-up. Two asks to keep it in line with our conventions:

  • Make the overflow path NUL-terminatebuf[0] = '\0' (an empty truncated string is safest, since pos can reach cap), so the code matches the doc rather than loosening the doc to "contents unspecified".
  • Include a reproduce-first test: one that is RED on current main (feed an argv that overflows a small cap, then assert buf is NUL-terminated / the reference round-trip parser stays in bounds) and turns green with the fix. The bounds-guarded round-trip test you mentioned sounds exactly right.

Keep the two comment corrections (cbm_build_win_cmdline + cbm_cmdline_put) in the same PR — they're part of the same contract fix. Appreciate the diligence 🙏

Flipper1994 added a commit to Flipper1994/codebase-memory-mcp that referenced this pull request Jul 5, 2026
Follow-up to DeusData#881. cbm_build_win_cmdline() returned false from inside its loop on
buffer overflow without NUL-terminating buf, although its header contract states the
buffer holds a string. No live bug — both call sites (cbm_run_win and the UI
http_server index spawn) ignore buf when the function returns false — but a future
caller trusting the documented contract could read an unterminated (possibly
uninitialized) buffer. Set buf[0]='\0' on the overflow path so it is always a valid
string, and correct the header comment to match.

Reproduce-first: the new win_cmdline_overflow_leaves_empty_string test feeds an argv
that overflows a small cap and asserts buf[0]=='\0'. It is RED on the pre-fix code
(buf[0] holds the first quoted byte '"' = 0x22) and GREEN with the fix.

Also tidies two review nits in the same files:
- fix the stale cbm_cmdline_put comment (it returns pos UNCHANGED on overflow;
  the overflow is signalled via *ovf, not an advancing pos),
- bounds-guard the reference CommandLineToArgvW parser in the round-trip test.

Refs: DeusData#881
Signed-off-by: Flipper <jacobphilipp@ymail.com>
DeusData added a commit that referenced this pull request Jul 5, 2026
…low-contract

fix(subprocess): NUL-terminate cbm_build_win_cmdline buf on overflow (post-review, follow-up to #881)
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.

2 participants