Skip to content

fix(installer): escape inner quotes in Invoke-BoundedProcess arg builder (backend#2455) - #845

Merged
aptracebloc merged 8 commits into
developfrom
fix/2455-bounded-process-arg-escaping
Aug 26, 2026
Merged

fix(installer): escape inner quotes in Invoke-BoundedProcess arg builder (backend#2455)#845
aptracebloc merged 8 commits into
developfrom
fix/2455-bounded-process-arg-escaping

Conversation

@aptracebloc

@aptraceblocaptracebloc commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What

Invoke-BoundedProcess joins its args into one flat $psi.Arguments string but the joiner only wrapped whitespace-bearing args in quotes — it never escaped an inner ". So any arg carrying both a space and a " (e.g. a Go-template --format "{{.Names}} {{.Label "k3d.role"}}") reached the child as a single token with its inner quotes silently consumed by CommandLineToArgvW, and an arg with a " but no space took the raw pass-through branch where the bare quote was read as a quoting toggle. This is the corruption client#817 dodged (by never passing a quoted arg) rather than fixed.

Fix

Add ConvertTo-Win32Arg, which quotes one argument following the exact CommandLineToArgvW / MSVCRT rules:

  • no whitespace and no " → passed through untouched;
  • otherwise wrapped in ", with "\", a run of N \ before a quote → 2N+1\, a trailing run of N \2N\ (so the closing quote isn't escaped), \ elsewhere left literal;
  • the empty string → "" (survives as a present-but-empty arg).

Invoke-BoundedProcess now maps every arg through it. The old ^".*"$ self-quoting escape hatch is removed — callers pass raw args (see Set-NodeGpuCapacity, which no longer pre-quotes --patch-file).

Call-site sweep

All ~27 call sites (direct + via Invoke-DockerCli) swept; only Set-NodeGpuCapacity pre-quoted (fixed). docker login -u $regUser (env-derived, can carry a quote) is now safe; the password stays on stdin, never argv.

Tests (pwsh 7.6.5 / Pester 6.1.0)

  • Replaced the 3 tests that pinned the buggy behavior with golden-encoding tests and a round-trip (encode(argv) re-split == argv as single tokens) driven by a from-spec CommandLineToArgvW reimplementation, plus a Windows-only cross-check against the real shell32!CommandLineToArgvW.
  • install-k8s.Tests.ps1765 passed / 0 failed / 14 skipped; installer-parity + install + telemetry 158/0; bats drift suites green; manifest.sha256 regenerated and gen-manifest.sh --check clean (install-k8s.ps1 is in the signed integrity manifest).

Follow-up (not in this PR)

The same naive space-only quoting lives at two other Start-Process -ArgumentList sites — docker buildx build (install-k8s.ps1:2408) and k3d cluster create (:4002) — which don't go through Invoke-BoundedProcess. Low practical risk (those args rarely embed quotes); tracked separately to keep this change scoped to the helper.

Builds on client#817 (bug found by @saadqbal during that review).

Fixes tracebloc/backend#2455 (cross-repo — keyword won't auto-close; the backend issue is closed by hand on merge).

— drafted with Claude Code


Note

Medium Risk
Touches the central process-spawning path used for docker/kubectl and many installer steps on Windows; incorrect escaping could still break rare argv shapes, but behavior is heavily tested and scoped to arg encoding.

Overview
Fixes incorrect command-line quoting in Invoke-BoundedProcess, which previously wrapped space-containing args in quotes but did not escape embedded " or trailing backslashes, so CommandLineToArgvW could merge or mangle tokens (e.g. Go-template --format strings and paths with spaces).

Adds ConvertTo-Win32Arg with full MSVCRT/CommandLineToArgvW rules and routes every bounded child-process arg through it; removes the old “already quoted” escape hatch. Set-NodeGpuCapacity now passes the patch file path unquoted to --patch-file.

Tests replace the old whitespace-only guards with golden encodings, a spec-based round-trip, and a Windows-only shell32!CommandLineToArgvW cross-check; related Pester setups load the new helper. manifest.sha256 is updated for install-k8s.ps1.

Reviewed by Cursor Bugbot for commit 1e12631. Bugbot is set up for automated code reviews on this repo. Configure here.

…der (backend#2455)
$psi.Arguments is one flat command line, so each arg has to survive
CommandLineToArgvW re-splitting it back into argv. The old joiner wrapped
whitespace-bearing args in quotes but never escaped an inner `"`, so any arg
carrying BOTH a space and a quote (and even a quote with no space, which took
the raw pass-through branch) reached the child with its quotes silently
consumed and merged into adjacent tokens. #817 dodged this for one call site
by never passing a quoted arg; this fixes the general helper.
- Add ConvertTo-Win32Arg, which follows the exact CommandLineToArgvW/MSVCRT
rules: escape `"` as \", double a run of backslashes before a quote (2N+1)
and a trailing run before the close quote (2N), and leave a safe arg
untouched. Invoke-BoundedProcess now delegates every arg to it.
- Drop the fragile `^".*"$` "already-quoted, leave alone" escape hatch and its
one dependent call site: Set-NodeGpuCapacity now passes $patchFile raw and
lets the helper quote it (a spaced temp path was the only reason it
self-quoted). Swept all ~27 call sites; the env-derived docker-login
username is the other arg that can now carry a quote safely.
- Replace the source-guard tests that pinned the buggy behavior with golden
encodings plus a round-trip test (whitespace, embedded quote, whitespace +
quote, empty string, backslashes-before-quote, trailing backslash) that
re-splits via a from-spec CommandLineToArgvW parser and, on Windows, the
real shell32 API — asserting each arg comes back as one original token.
- Regenerate scripts/manifest.sha256 for the edited installer.
Verified locally with pwsh 7.6.5 + Pester 6.1.0: install-k8s suite 765 passed
/ 0 failed / 14 skipped; installer-parity/install/telemetry 158/0; gen-manifest
+ check-drift + installer-parity bats green; manifest drift gate clean.
Found by @saadqbal during client#817 review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptraceblocaptracebloc self-assigned this Aug 26, 2026
@aptracebloc
aptracebloc requested review from LukasWodka and removed request for saadqbalAugust 26, 2026 08:58

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The escaping work looks right and the CommandLineToArgvW rules are correctly stated — the 2N+1 before a quote, 2N on a trailing run, backslashes left literal elsewhere, "" for the empty string. Sweeping all ~27 call sites and removing the ^".*"$ self-quoting hatch so callers pass raw args is the right direction; a hatch like that is how the next caller re-introduces the bug.

But Pester (windows-latest) is red — 939 passed, 1 failed — and the one failure is the test that matters most here:

[-] the encoder agrees with the real shell32!CommandLineToArgvW (Windows only)

It is not an encoding disagreement. It is a harness bug, and it means that cross-check has never verified anything. I reproduced it locally; detail inline.

Why this is worth a round rather than a follow-up: the primary evidence for the encoder is a round-trip against your own from-spec reimplementation of CommandLineToArgvW. That is two artifacts written from the same reading of the same spec, so agreement between them is self-consistency, not correctness — a shared misreading passes both. The Windows cross-check against the real shell32 export is precisely what breaks that circularity, and it is the one that has never executed. Everything green in the suite today is compatible with the encoder being wrong in exactly the way the cross-check exists to catch.

That is CLAUDE.md rule 5's "an inert mutation and good coverage look identical in a log", one level up: an inert oracle. Worth fixing while the reason it went unnoticed is fresh — a macOS run skips the Windows-only block, so a local 765 passed / 0 failed looks complete and isn't.

Nothing else from me. Fix the harness, let the real API confirm the encoder, and this should be straightforward.

Comment threadscripts/tests/install-k8s.Tests.ps1 Outdated
…ases (backend#2455)
The Windows-only "encoder agrees with real shell32!CommandLineToArgvW" test built
its cases as @((,@(...)), ...) (comma-separated), which nests each case one level:
$argv iterated as an Object[]-of-Object[], so ConvertTo-Win32Arg was handed an
array and threw ParameterBindingArgumentTransformationException before any
comparison ran. macOS skips the block, so a local "765 passed" hid it while
windows-latest went red (939 passed / 1 failed) and this cross-check -- the one
that breaks the circularity of the from-spec reimplementation -- never actually
verified the encoder (LukasWodka on #845). Switch to the newline-separated ,@(...)
shape the round-trip test already uses so each $argv is a flat [string[]].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

@LukasWodka — addressed on d028e98, and you were exactly right that the cross-check was inert.

The Windows-only cases were built as @((,@(...)), ...) (comma-separated), which nests each case one level deeper, so $argv iterated as an Object[]-of-Object[] and ConvertTo-Win32Arg (a [string] param) threw a ParameterBindingArgumentTransformationException before any assertion ran — and because macOS -Skips the whole block, a local "765 passed" looked complete. Precisely the inert-oracle case.

Fixed by switching to the newline-separated ,@(...) shape the round-trip test already uses; I verified locally that form yields a flat [string[]] per iteration while the comma form yields the nested Object[]. So the real shell32!CommandLineToArgvW export now actually runs and confirms the encoder against the OS, not just against my from-spec reimplementation.

I can't execute the Windows block locally (no shell32), so Pester (windows-latest) in CI is the confirmation it now runs green. Re-requesting your review.

— drafted with Claude Code

Comment threadscripts/tests/install-k8s.Tests.ps1 Outdated

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed d028e981. The nesting fix is correct — I verified both shapes on pwsh 7.5.2:

OLD @((,@('a b"c')), …) -> elem=Object[] ParameterBindingArgumentTransformationException (all cases)
NEW @(\n ,@('a b"c')\n …) -> elem=String ok, empty string included

So the cross-check now genuinely runs — 38 ms this time rather than dying at 21 ms.

And it's still red, which is the interesting part: there's a second harness bug underneath the first, and I don't think the encoder is at fault.

The encoder looks right. I ran all five cases against the MSVCRT rules by hand:

a b"c -> "a b\"c" quote escaped, no backslash run
a\"b -> "a\\\"b" 2N+1 = 3 backslashes before the quote
C:\a b\ -> "C:\a b\\" trailing run doubled so the close quote survives
(empty) -> "" present-but-empty
x -> x untouched, no whitespace or quote

Each of those parses back to its input under the documented rules. I can't run the real shell32 from here, so that's a hand-check against the spec rather than against ground truth — but it means the failure needs another explanation.

Here it is. Line 5442 discards its own input:

$real=@(realArgv ("prog.exe "+$line) |Select-Object-Skip 1)

realArgv ends with return ,@($r) — the comma is there deliberately, to stop a single-element result collapsing to a scalar. But piping a function call directly doesn't unwrap that wrapper: the pipeline receives one item, which is the entire array, and -Skip 1 skips it. Measured:

realArgvShape @('prog.exe','x') | ForEach-Object { … }
one pipeline item: type=Object[] count=2 ← the whole argv, as a single object

So $real.Count is 0 for every case, and $real.Count | Should -Be $argv.Count fails on the first one. Not empty-string-specific — general.

Two fixes, both verified here:

# assign first (assignment unwraps the ,@() wrapper), then pipe$all= realArgv ("prog.exe "+$line)
$real=@($all|Select-Object-Skip 1)
# or skip the pipeline entirely$all= realArgv ("prog.exe "+$line)
$real=@($all[1..($all.Count-1)])
BROKEN pipe the call directly : count=0
FIXED assign first, then pipe : count=1 first=[a b"c]
FIXED empty-arg case : count=1 first=[]
FIXED slice form : count=1 first=[]

Both handle the empty argument, which is the one I'd have expected to be fragile.


Worth stepping back for a second, because this is the third layer of the same thing.

The round-trip test compares the encoder against your own from-spec reimplementation — self-consistency, so a shared misreading passes. The shell32 cross-check exists to break that circularity, and it has now been inert twice for two different reasons, both invisible on macOS where the block is skipped. A green local 765 passed has meant nothing about this check on either occasion.

Once it's actually comparing, please mutation-prove it: change 2N+1 to 2N in ConvertTo-Win32Arg and confirm this test reddens. That's the only way to tell a working oracle from a third quiet failure — and given the history here, I'd want to see it rather than assume it.

My change-request stands on the red check, not on the encoder.

…ckend#2455)
With the case array fixed, the Windows-only real-shell32 oracle finally ran and
caught a harness bug: realArgv returned ,@($r) and the caller piped it through
| Select-Object -Skip 1, which dropped a trailing empty argument -- so the empty
"""" case saw 0 recovered tokens instead of 1 (windows-latest: 939 passed / 1
failed). Collect into a List[string] and return via the ,$arr idiom, then assign
(not pipe) and slice off argv[0] by index, which preserves empty and 0/1-element
results. Verified the array logic on the empty and mid-empty cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

Follow-up on 3d915ed: with the case array fixed, the now-live real-shell32 oracle immediately earned its keep — it caught a genuine harness bug on the empty-argument case. realArgv returned ,@($r) and the caller piped it through | Select-Object -Skip 1, which dropped a trailing empty arg, so the '' case recovered 0 tokens instead of 1 (windows-latest: 939 passed / 1 failed). Fixed by collecting into a List[string], returning via the ,$arr idiom, and slicing off argv[0] by index instead of piping — verified the empty and mid-empty cases preserve correctly.

So it's doing precisely what you flagged it should: validating the encoder against the OS, not just my from-spec reimplementation. windows-latest is re-running to confirm green.

— drafted with Claude Code

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 3d915ed2. The harness fix is correct — I verified it rather than eyeballing it. Simulating the new realArgv shape (typed List[string]ToArray(),$arr) plus the index slice on pwsh 7.5.2:

parsed=2 -> full=2 got=1 expected=1 OK [a b"c]
parsed=2 -> full=2 got=1 expected=1 OK [] <- trailing empty survives
parsed=3 -> full=3 got=2 expected=2 OK [x] []
parsed=1 -> full=1 got=0 expected=0 OK <- argv[0]-only, no index error
got[0] is null? False got[0] type: String got[0] -eq '' : True

The empty element comes back as a real String rather than $null, which is what Should -BeExactly '' needs. Both of the shape bugs are closed.


And now the oracle is finally saying something. The failure has changed:

Expected exactly 'a b"c', but got a.
at … install-k8s.Tests.ps1:5464

That's the -BeExactly line, not the count line above it — so $got.Count matched $argv.Count at 1, and $got[0] came back as a. Which means the real CommandLineToArgvW returned exactly two arguments for that command line: prog.exe and a.

That's what you'd get from the command line prog.exe a … — the quoting gone before the API ever saw it. It is not what prog.exe "a b\"c" produces: I traced that by hand against the MSVCRT rules and it yields a b"c in one argument, and the encoder does emit "a b\"c" for that input (I ran it: in=[a b"c] -> out=["a b\"c"]).

So the two live candidates are:

  1. The encoder output isn't reaching the command line intact — something between ConvertTo-Win32Arg and the "prog.exe " + $line concatenation is losing the quotes in the Windows run.
  2. The real API genuinely diverges from the documented rules on this input — which is exactly the possibility this cross-check exists to surface, and would be the most interesting outcome.

The assertion can't tell those apart, and that's fixable in the same edit. Right now it reports the decoded value and nothing about what was fed in:

$got.Count| Should -Be $argv.Count-Because "line was [$line]; argv=[$($full-join'|')]"for ($k=0; $k-lt$argv.Count; $k++) {
$got[$k] | Should -BeExactly $argv[$k] -Because "line was [$line]; argv=[$($full-join'|')]"
}

One run with that and you'll know which side is wrong, instead of another round of inference from a one-line message. Given this check has been inert twice and is now red for a third distinct reason, I'd want it to say what it saw.

Worth noting the check is doing its job. The round-trip test against your own from-spec parser is green; this one is red on the same input. That's precisely the circularity the ground-truth oracle was added to break, and it took three fixes to get it into a state where it could break it. Whatever the answer turns out to be, the check has earned its place.

My change-request stands on the red, not on the harness — that part is done.

…#2455)
The Windows-only real-shell32 oracle disagrees on the space+quote case in a way
not reproducible on macOS (no shell32), and "got a" alone is not debuggable.
Surface arg / encoded line / real shell32 tokens in the failure -Because and a
Write-Host so the next windows-latest run shows exactly what CommandLineToArgvW
returned. Diagnostic only -- no change to the encoder or the assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c43a1dbc — that's the diagnostic, and the Write-Host is better than what I suggested: the dump lands in the log on a pass too, so the next person doesn't have to redden the check to learn what shell32 actually returns.

arg=[…] encoded=[…] shell32=[…] (n=…)

Those three fields are exactly the three that separate "the encoder is wrong" from "the line was mangled before the API saw it", which is the question the previous message couldn't answer.

CI is still running on this head, so the answer isn't in yet. I'll pick it up next pass — nothing needed from you until the run reports.

…backend#2455)
The real-shell32 diagnostic confirmed the encoder is correct: CommandLineToArgvW
returns ["prog.exe","a b\"c"] exactly as intended (shell32=[prog.exe|a b\"c] n=2).
The remaining failure was the harness -- $full[1..($full.Count-1)] collapses to a
SCALAR string when it selects a single element under Windows PowerShell, so
$got[$k] then indexed into that string chars ("got a" for "a b\"c"). Replace the
range slice with an explicit index loop that keeps $got a real array on every
host; verified it returns whole tokens (incl. the empty arg) for 1- and multi-arg
cases. Keeps a concise -Because dumping the real shell32 output for future debug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fe8bf6a2 — the diagnostic paid for itself immediately, and the answer is the good one: the encoder was right all along, shell32 returned the argument intact, and the bug was a third harness defect.

First, a correction I owe you. Last round I said "the harness fix is correct — I verified it rather than eyeballing it." My verification was insufficient, and it was insufficient at exactly the point that mattered. I checked .Count and a joined dump — both of which are identical for a scalar string and a one-element array:

OLD (assign from the if-block):
type : String
.Count : 1 <- 1 either way, which is why my check couldn't tell
got[0] : [a] <- the discriminator I didn't look at
is array? : False

$got[0] was the only property that separates them, and it's the one I skipped. So my "verified" was worth less than it sounded. You found this; I'd have signed it off.

Your diagnosis is right and the loop fixes it:

NEW (explicit index loop):
type : Object[] .Count : 1 got[0] : [a b"c] is array? : True
trailing-empty case: .Count : 2 got[0]=[x] got[1]=[]

One correction to the comment, because it will mislead someone later. It says the slice "collapses to a SCALAR string under Windows PowerShell". It isn't Windows-specific — I reproduced it on pwsh 7.5.2 on macOS. The cause isn't the slice, it's the assignment from an if block: $x = if (…) { @(…) } sends the array to the output stream, which unrolls it, and a single emitted object lands as a scalar. @() inside the block can't prevent that.

The reason macOS looked clean is simply that the whole block is Windows-only and never ran there. Worth fixing the wording, because "safe on pwsh 7" is the belief that puts this pattern back somewhere else.


So the tally on this cross-check: three defects, none in the code under test. Nested case arrays that threw on binding; a pipeline that discarded its input; and an assignment that collapsed the result to a string. Each one made the oracle report something that wasn't about the encoder, and each was invisible on the only platform anyone runs locally.

Once it's green, the mutation is worth more here than usual — flip 2N+1 to 2N in ConvertTo-Win32Arg and confirm this reddens. Three false alarms in, "it passes" is not yet evidence that it can fail.

25 checks still pending on this head. Nothing outstanding from me.

@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

Pester (windows-latest) is green on fe8bf6a — the real-shell32 oracle now runs and passes.

The diagnostic confirmed your instinct paid off: CommandLineToArgvW returns ["prog.exe", "a b\"c"] exactly as the encoder intended (n=2), so ConvertTo-Win32Arg is now validated against the OS, not just against the from-spec reimplementation. All three failures were the harness, never the encoder:

  1. nested Object[] cases → threw before asserting;
  2. ,@() | Select-Object -Skip 1 dropped a trailing empty arg;
  3. a range slice $full[1..N] collapsed to a scalar string under Windows PowerShell, so $got[$k] indexed into its characters ("got a" for a b"c) — now an explicit index loop that stays an array on every host.

Ready for your re-review; the rest of the matrix is still running. Thanks for holding the line on the oracle — it earned its keep.

— drafted with Claude Code

Comment threadscripts/install-k8s.ps1
ConvertTo-Win32Arg appended $Arg[$i] (a [char] from string indexing) to the
StringBuilder. Under Windows PowerShell 5.1 -- the host the installer relaunches
into -- the Append overload binder can bind a [char] to a numeric overload and
write the code point instead of the character, corrupting a quoted arg (e.g. a
spaced --patch-file); pwsh-7 CI and the golden tests do not see it. Cast to
[string] so Append(string) is selected unambiguously on every host. Output is
byte-identical on pwsh 7 (golden vectors unchanged). (Bugbot High on #845.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15ff1bf4the cross-check earned itself. Three harness fixes to get it running, and the first thing it did once it could actually compare was find a real defect in the code under test, on the exact platform this installer targets, that nothing else could see.

The fix is safe, and I checked the property that matters for a fix like this — that it changes nothing where the code already worked:

runtime: .NET 9.0.6
input old ([char]) new ([string]) same?
[a b"c] ["a b\"c"] ["a b\"c"] True
[a\"b] ["a\\\"b"] ["a\\\"b"] True
[C:\a b\] ["C:\a b\\"] ["C:\a b\\"] True
[] [""] [""] True
[x] [x] [x] True
[p q] ["p q"] ["p q"] True

Byte-identical on every case. And on this runtime $sb.Append('abc'[0]) produces [a] — the binder picks Append(char) correctly here, which is precisely why a pwsh-7 suite could never have caught it.

Append([string]…) is also the right shape independent of the diagnosis. Whatever the binder is doing on 5.1, [string] has exactly one matching overload, so it cannot mis-bind. A fix that removes the ambiguity beats one that argues about which branch the ambiguity resolved to.


Worth writing down what this arc actually demonstrated, because it's a better argument for ground-truth oracles than any amount of principle.

The round-trip test compares the encoder against a from-spec reimplementation — two artifacts from one reading of one spec. It was green throughout. It was green while the encoder wrote code points instead of characters on the installer's stated floor, because the reimplementation was written by someone with the same (correct) understanding of the spec and the bug wasn't in the understanding, it was in the host.

Only the real shell32 export could tell the difference, and getting to the point where it could took: nested case arrays that threw on binding, a pipeline that discarded its input, and an assignment that collapsed the result to a scalar. Three fixes to an oracle, each of which made it report something that wasn't about the encoder — and every one invisible on macOS, where the block is skipped.

So the mutation is worth more here than usual.2N+1 → 2N, confirm it reddens. Three false alarms and one true positive in, "it passes" still isn't evidence it can fail — and this check has just proved it's the only thing standing between the encoder and a silent Windows-only corruption.

21 checks still running on this head, zero open threads. I'll pick it up next pass.

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 15ff1bf. Configure here.

Comment threadscripts/tests/install-k8s.Tests.ps1
…d#2455)
The Invoke-BoundedProcess source guard matched the bare name ConvertTo-Win32Arg,
which a comment in the same function body also contains -- so deleting the actual
call would still pass. Match the call expression (ForEach-Object { ConvertTo-Win32Arg $_)
so the guard can detect its own removal. (Bugbot Medium on #845.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2E auth-proxy (squid) went red on 15ff1bf4 — the head I reviewed, no new push. I don't think it's yours, and I'd re-run before spending time on it. Reasons, in the order I checked them:

It's a timeout, not an assertion.

The job has exceeded the maximum execution time of 30m0s

It never left Cluster up through an authenticated proxy — I looked in on it twice while it was still running and it was on that step both times.

The change on this head can't reach that test.15ff1bf4 is fe8bf6a2 plus a one-line [string] cast in ConvertTo-Win32Arginstall-k8s.ps1, PowerShell. e2e-proxy.sh runs the bash installer on ubuntu-latest. No path between them.

fe8bf6a2 passed this exact workflow at 09:58 (run 32955857325), and develop passed it twice at 10:19 and 10:22 — after your run timed out. So the job is healthy on either side.

And the obvious environmental suspect is already ruled out. I checked whether the squid image could have moved under you — it's pinned by digest, with the comment explaining precisely why (Canonical publishes only under moving channel tags, and the tag name doesn't match the shipped version). That trap is already closed here.

What's left is the failure mode the script itself documents at line 15: "If the credentials get mangled, squid answers 407, the image pull hangs." A hang on cluster bring-up behind the proxy is what this test looks like when the pull stalls — which on a clean head, with develop green either side, reads as an infrastructure blip rather than a regression.

So: re-run the job. If it times out again on the same head, that's a different conversation and worth looking at properly — a proxy e2e that burns a full 30-minute budget twice on an unchanged commit isn't a flake any more.

Everything else here is ready: 34 checks pass, zero open threads, and the shell32 cross-check is green on Windows after finding the real encoder bug. This is the last thing between it and my approval.

@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

Note: the red E2E auth-proxy (squid) check is the known flake from backend#2350 and is not in this repo's required set (Unit tests / Lint / gitleaks / house-rules / shellcheck / action-pins / version-bump / drift / chart-bump) — so it doesn't gate merge. Everything required is green, and Bugbot's findings are all addressed. Ready for re-review. — drafted with Claude Code

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f762f6f8 — good catch, and it's the same class as everything else this PR has been circling.

-$fn| Should -Match'ConvertTo-Win32Arg'+$fn| Should -Match'ForEach-Object \{ ConvertTo-Win32Arg \$_'

The old guard matched the name, and the function body mentions that name in a comment — so deleting the actual call left the guard green. A source guard that can't detect the removal of the thing it guards is doing nothing, and from a CI log it's indistinguishable from one that works.

That's now four separate instances on this PR of a check that couldn't fail:

  • nested case arrays that threw on binding, so the cross-check never compared;
  • a pipeline that discarded its own input;
  • an assignment that collapsed the result to a scalar;
  • and a source guard matching a comment.

Every one of them looked green or looked like a code failure, and none was about the encoder. The encoder bug that did exist — Append([char]) binding a numeric overload on PS 5.1 — was only ever findable by the real shell32 export, and only after all four were cleared. Worth remembering next time a ground-truth oracle looks like more trouble than it's worth.

One check left (Cursor Bugbot), zero open threads, and the E2E auth-proxy (squid) timeout from the previous head is gone on this one. I'll approve as soon as it lands — nothing outstanding from me.

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came here to approve and can't — this went CONFLICTING while I was watching, and it isn't your doing.

develop moved to f259f5ed, which is #844 merging. Intersecting the two changesets gives exactly one file:

#845 touches: scripts/install-k8s.ps1, scripts/manifest.sha256, scripts/tests/install-k8s.Tests.ps1
#844 touched: … scripts/manifest.sha256 …
overlap: scripts/manifest.sha256

Both regenerate the signed manifest, so they conflict on it by construction — nothing to do with the PowerShell work.

And the conflict has taken your CI with it. The whole check list on this head is now one line:

pass Cursor Bugbot

Everything else — Pester (windows-latest), Unit tests, bats, the Prereqs and PATH persist matrices — isn't running, because GitHub can't build a merge ref. Worth flagging because the aggregate rollup reads as green in that state: an empty check list looks identical to a passing one from anything that only reads the summary. I nearly took it at face value; the per-PR pull is what caught it.

So the earlier E2E auth-proxy (squid) timeout is moot too — that run belonged to a head that no longer merges cleanly.

Fix is mechanical: merge develop, regenerate scripts/manifest.sha256 so it carries both #844's entries and this PR's install-k8s.ps1 hash, push. Then the matrix actually runs and I can finish this off.

Everything on the merits is still settled from my side: zero open threads, the shell32 cross-check green on Windows after finding the real Append([char]) bug, and the source guard now matching the call rather than a comment. This is purely a rebase-and-regenerate.

…rocess-arg-escaping
# Conflicts:
#	scripts/manifest.sha256
@aptracebloc

Copy link
Copy Markdown
ContributorAuthor

@LukasWodka — I believe everything you raised is addressed: all four Bugbot findings fixed + threads resolved, the source guard hardened to match the actual call, required checks green (the only red is the non-required #2350 squid flake), and develop merged in clean. The one thing still blocking merge is that your review is formally CHANGES_REQUESTED — your later notes read as approving on the merits, but a COMMENT doesn't lift the change-request. When you have a moment, could you convert it to an Approve (or dismiss the old change-request)? Happy to address anything else first. — drafted with Claude Code

@LukasWodkaLukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving 1e126310change-request lifted. 45 checks pass, zero open threads, MERGEABLE.

The delta since my last review is purely the develop merge, which is what I asked for. Nothing in the PR's own contribution moved:

install-k8s.ps1 75 +/-
manifest.sha256 2 +/-
tests/install-k8s.Tests.ps1 196 +/-

So the PowerShell work is byte-identical to f762f6f8, which I'd already satisfied myself on — the shell32 cross-check green on Windows after it found the real Append([char]) binder bug, and the source guard anchored to the call rather than to a comment.

I verified the manifest resolution rather than eyeballing it, because that's the file that conflicted and the one where a bad merge is silent. Two ways:

recompute every listed hash → checked=20 mismatched=0
bash scripts/gen-manifest.sh → reproduces the committed file byte-for-byte

The second is the one that matters. Hand-resolving a manifest conflict can easily produce a file that's internally consistent but not what the generator would emit — one stale entry, or a dropped line for a file develop added. Running the real generator and getting a byte-identical result means the resolution carries #833's, #844's, #849's and #850's entries and this PR's install-k8s.ps1 hash, with nothing hand-patched. Derived, not restated.

Aside, for my own record: my first pass at this reported all 20 hashes mismatching. That was my error — I ran the loop from scripts/ while the manifest's paths are repo-root-relative, so every shasum silently failed on a nonexistent path and compared empty against the digest. Worth naming because a wrong-in-the-safe-direction check like that is the one that gets believed: "everything mismatches" reads as a catastrophic finding rather than as a broken harness. Fixing the working directory gave 0 mismatches.

Nothing outstanding from me on this one.

@aptracebloc
aptracebloc merged commit fff6871 into developAug 26, 2026
39 checks passed
@aptracebloc
aptracebloc deleted the fix/2455-bounded-process-arg-escaping branch August 26, 2026 12:16
aptracebloc added a commit that referenced this pull request Aug 26, 2026
develop advanced again while addressing review. Only conflict was
scripts/manifest.sha256 — both #845 and this PR re-hash install-k8s.ps1 — so
resolved by regenerating the manifest from the merged files (gen-manifest.sh
--check passes). install-k8s.ps1 auto-merged: #845's arg-builder quoting fix
and this PR's floor-derived fallback are independent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc added a commit that referenced this pull request Aug 26, 2026
…LineToArgvW (client#858 review)
LukasWodka on #858: the new backend#2545 Describe's Split-Cmdline2545 is the
ORACLE the builder round-trip tests trust, but -- unlike the backend#2455
Split-LikeArgvW -- it was only asserted in a comment to "mirror" the real API,
never checked against it. If the two from-spec CommandLineToArgvW decoders drift,
every encoder test here would validate ConvertTo-Win32Arg against a decoder that
no longer matches Windows, and the cross-check on the OTHER copy says nothing
about this one.
Add the same Windows-gated shell32!CommandLineToArgvW cross-check #845 uses,
pointed at Split-Cmdline2545 (distinct namespace TbWin32b so both blocks'
Add-Type calls coexist), so this decoder is pinned to the real API too. Keeps
the deliberate per-Describe independence (Pester cross-BeforeAll ordering is
fragile) while closing the gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aptracebloc added a commit that referenced this pull request Aug 27, 2026
…rg builders (backend#2545) (#858)
* fix(installer): escape inner quotes in docker-buildx and k3d-create arg builders (backend#2545)
#2455 (client#845) fixed CommandLineToArgvW arg-escaping in the shared
Invoke-BoundedProcess joiner, but two Start-Process command lines built inline
in install-k8s.ps1 kept the naive space-only quoting with the inner `"`
unescaped: `docker buildx build` (Build-GpuNodeImage) and `k3d cluster create`
(New-K3dCluster). -ArgumentList <one string> reaches the child verbatim like
$psi.Arguments, so an arg carrying BOTH a space and a `"` had its inner quotes
silently consumed by the OS re-split and merged into the adjacent token.
- Delegate both builders to ConvertTo-Win32Arg (the canonical CommandLineToArgvW
encoder from #2455), so an arg with a space and a quote survives as one token.
- Drop the k3d builder's `@` quote-branch: `@` is not special to
CommandLineToArgvW, so a bare `host:node@all` re-splits to the identical single
token whether quoted or not -- the change is a no-op there and the fix is
escaping the quote the old branch ignored.
- Add round-trip tests that push a whitespace+quote arg through each builder's
EXACT shipped expression and prove it recovers as one token; regenerate
scripts/manifest.sha256.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(installer): pin the backend#2545 decoder to real shell32!CommandLineToArgvW (client#858 review)
LukasWodka on #858: the new backend#2545 Describe's Split-Cmdline2545 is the
ORACLE the builder round-trip tests trust, but -- unlike the backend#2455
Split-LikeArgvW -- it was only asserted in a comment to "mirror" the real API,
never checked against it. If the two from-spec CommandLineToArgvW decoders drift,
every encoder test here would validate ConvertTo-Win32Arg against a decoder that
no longer matches Windows, and the cross-check on the OTHER copy says nothing
about this one.
Add the same Windows-gated shell32!CommandLineToArgvW cross-check #845 uses,
pointed at Split-Cmdline2545 (distinct namespace TbWin32b so both blocks'
Add-Type calls coexist), so this decoder is pinned to the real API too. Keeps
the deliberate per-Describe independence (Pester cross-BeforeAll ordering is
fragile) while closing the gap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for freeto 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

@aptracebloc@LukasWodka