sdk run: quote argv elements instead of joining them raw - #201
sdk run: quote argv elements instead of joining them raw#201mobileoverlord wants to merge 2 commits into
Conversation
`sdk run` spliced the user's argv into a shell script inside the container
with a bare `cmd.join(" ")`, so the container shell re-split it. This:
avocado sdk run -- bash -lc 'U=/opt/x; ls $U'
reached the container as:
bash -lc U=/opt/x; ls $U
which the shell reads as two commands: a throwaway `bash -lc U=/opt/x`,
then `ls $U` with `$U` expanded by the *outer* shell to nothing -- i.e.
`ls`. It silently listed the wrong directory instead of failing, which
makes it an actively misleading debugging tool.
Quote each element with the existing utils::runs_on::shell_escape (now
pub(crate)). Extract join_argv so the behavior is unit-testable without a
container.
There was a problem hiding this comment.
Pull request overview
This PR fixes avocado sdk run incorrectly re-splitting user-provided argv inside the container by quoting each argv element before splicing it into the container’s bash -c script, aligning behavior with typical docker run / kubectl exec argv semantics.
Changes:
- Promotes
utils::runs_on::shell_escapetopub(crate)so it can be reused for argv-safe quoting. - Adds
join_argvto shell-escape each argv element (instead ofjoin(" ")) when building the container command string. - Adds unit tests covering argv-boundary preservation and embedded single-quote escaping.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/utils/runs_on.rs | Makes shell_escape available within the crate to support safe argv quoting. |
| src/commands/sdk/run.rs | Uses per-argv-element shell escaping via join_argv and adds targeted unit tests. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
jetm
left a comment
There was a problem hiding this comment.
The fix is right and I verified the premise rather than taking it on faith: RunConfig.command really is spliced into bash -c at four sites in src/utils/container.rs (944/1153/1543/1841), and the --runs-on path does not double-escape, because build_docker_command already wraps the whole script in shell_escape. Both new tests assert the exact escaped strings rather than truthiness, and main.rs:3404 maps an empty argv to None, so join_argv never sees an empty slice. cargo fmt --all -- --check, cargo clippy --all-targets --all-features -- -D warnings and cargo test --lib join_argv all pass at e7806cb.
One thing I would add before merging.
This is a user-visible behavior change with no CHANGELOG entry. Anyone who was single-quoting a $VAR or a glob into sdk run and relying on the container shell to expand it loses that silently. I confirmed both halves in bash: bash -c "'bash' '-lc' 'X=1; echo $X'" now prints 1 (the fix), and bash -c "'echo' '\$HOME'" prints a literal $HOME (the loss). The repo keeps a populated Keep-a-Changelog Unreleased block, so there is a natural home for it — I would file it under Fixed with a line about the expansion change, since the people affected will not find it in the diff.
Three nits, all optional:
src/commands/sdk/run.rs:275— stale error text inside the function you touched: "You must either provide a --command (-c)".avocado sdk run --helpshows no such flag; the command is a trailing positional and-cis unassigned (-Cis--config). I checked withcargo run -- sdk run --help.src/commands/sdk/run.rs:22— layering: a generic shell-quoting helper now comes fromutils::runs_on, the remote-execution module, into a local non-remote path, and via an inlinecrate::utils::runs_on::shell_escape(..)rather than ause.utils::shellwould be the honest home.- The same untrusted-argv-into-
bash -csplice is still raw insdk/dnf.rs:115,ext/dnf.rs:358andruntime/dnf.rs:303(self.command.join(" ")). Out of scope here, but the class of bug is not closed by this PR.
…ll_escape - CHANGELOG entry under Fixed. The people this affects are the ones who were relying on the container shell to expand a $VAR or a glob out of a single argument, and that loss is invisible in the diff, so it says so explicitly and names the `bash -lc` form to move to. - The "You must either provide a --command (-c)" error names a flag that does not exist: the command is a trailing positional, and -c is unassigned (-C is --config). Reworded to "a command to run", and the test asserting on that string follows it. - shell_escape moves from utils::runs_on to a new utils::shell. A generic quoting helper living in the remote-execution module meant a local, non-remote path had to reach into runs_on for it. Its four tests move with it, and the import is a `use` rather than an inline path now.
|
All three taken, plus the fourth as a filed follow-up. Pushed in c785a69. CHANGELOG. Added under Stale flag text. Fixed. Worth noting it was wrong in a second way beyond the flag not existing: the message told you to provide Layering. Agreed, and done — The dnf sites. Confirmed all three are the identical
|
jetm
left a comment
There was a problem hiding this comment.
Re-reviewed the increment since e7806cb - the changelog/flag-text/shell_escape commit answering my last pass. The shell_escape move is byte-identical and every call site still resolves; the reworded flag text checks out against the actual flags. One blocking finding inline, on where the changelog files this change rather than on the code.
Six advisory notes were withheld rather than appended here, so the blocking one stays readable.
| that collapse is what let an unparseable version fall through as a pass. | ||
|
|
||
| ### Fixed | ||
| - **`sdk run` no longer lets the container shell re-split its argv.** The |
There was a problem hiding this comment.
User-visible CLI break filed under Fixed, not Breaking
This entry's own bold sentence concedes it "removes an expansion that some invocations relied on", yet it sits under ### Fixed. Meanwhile the ### Breaking section in this same ## [Unreleased] block (line 44) holds only a change the text itself scopes to "consumers of the avocado_cli lib target".
Failure path: a maintainer bumps the pinned avocado CLI version in CI and reads only ### Breaking for upgrade impact. It says lib-target consumers only, so they ship. A job running avocado sdk run -- '$BUILD_DIR/x' now passes a literal $BUILD_DIR instead of the expansion - no compile error, no warning, and the pre-change behaviour was already wrong-but-plausible output rather than a hard failure.
The asymmetry is what makes this worth changing: the entry that is filed under Breaking would have failed loudly as a compile error, while this one fails silently at runtime. Swapping which section each sits in would match the actual blast radius.
What
sdk runsplices the user's argv into a shell script inside the container using a barecmd.join(" "), so the container shell re-splits it. This:arrives in the container as:
The shell reads that as two commands — a throwaway
bash -lc U=/opt/x, thenls $Uwhere$Uis expanded by the outer shell to nothing, i.e. plainls.It does not error. It prints plausible output for a different directory. I hit this while debugging an SDK sysroot and it produced two confidently wrong readings before I noticed — a debugging tool that lies is worse than one that fails.
Fix
Quote each argv element with the existing
utils::runs_on::shell_escape(promoted topub(crate)— no new helper). Extractedjoin_argvso this is unit-testable without spinning a container.Behavior change
Argv is now treated as argv, matching
docker run/kubectl execconvention. Anyone currently passing a single string of shell code:now gets that string as one command word, and should use the explicit form:
which is what the flag combination already implies, and which only works correctly after this fix. Flagging it since it is user-visible.
Tests
Two unit tests in
src/commands/sdk/run.rs— argv-boundary preservation (the exactbash -lc 'X=1; echo $X'case) and embedded-single-quote escaping.cargo test --bin avocado sdk::run— 11 passed. fmt and clippy clean.Separate from #200; no overlap.