fix(analyzer): avoid false parse limits on quoted shell assignments - #634
Conversation
Signed-off-by: Deepak Jain <deepujain@gmail.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @deepujain, thank you for your contribution to SkillSpector — we really appreciate the time you put into this! A few items need attention before it can be merged; details below.
This PR makes each successfully parsed candidate word record the positions of its closing quotes and of any $ inside double quotes; later candidates at those positions are skipped. It also parses a quote directly after = as a word. This removes the #628 triggers: a quoted $(...), or a (/| inside an assignment string, made the command-word sweep re-enter a closing quote and run to EOF.
I statically traced the head against main through _has_shell_command_word_exhaustion, _parse_shell_command_word, _bounded_shell_tokens, _shell_command_strings and _destructive_command_words. The issue's forms are fixed, and failed parses correctly claim nothing. However, owning the $ of a quoted $(...) also removes a fail-closed signal that main deliberately keeps. I did not execute contributor code, so the repros below come from static tracing and should be added as regression tests.
Findings
-
[Blocking] Runtime-selected destructive commands inside double quotes are now reported as complete (
static_patterns_tool_misuse.py:1456-1457, skipped at:1745).Why main flags them. Main keeps a
$(...)start as an independent candidate ("Only executable nested substitutions retain independent command positions"), and_bounded_shell_tokenstreats the preceding quote as a wrapper. A quoted runtime command with root operands therefore returns exhaustion (static_parse_limit, partial).What changes here. The
$is owned by the enclosing string's parse and never evaluated, and nothing else covers it:- the enclosing word's own token check starts after its closing quote;
_destructive_command_wordsskips dynamic words;_shell_command_stringsdoesn't recognize the clause.
Static traces:
- Markdown.
SKILL.mdcontainingRun "$(resolve_tool) -rf /". On main, the$candidate yields tokens-rf /up to the wrapper quote,_has_destructive_root_pathfires, and the result is PARTIAL. On this head the result is complete, with no findings, i.e. SAFE. - Shell script:
On main, the stray comment quote's parse contains
# see "notes $(resolve_tool) -rf / # "
$(, soparsed_throughdoes not advance and line 2 is still evaluated (PARTIAL). On this head, that mis-aligned comment-quote parse claims line 2's$as a "quoted expansion start", and the result is complete.
Why CI stays green. The
$(printf ...),$($(resolve_tool)/printf ...)and"$(...)" -rf /variants still exhaust, either vialimitedor via tokens after the closing quote. Non-printf runtime helpers with operands inside the quotes do not.Expected fix.
- Don't take ownership of
$(/backtick starts inside double quotes. Instead, stop an interior candidate's word parse at its wrapper quote, as_bounded_shell_tokensalready does, so it can't run past the closing quote. Alternatively, apply main's runtime-command operand check to the quoted span before claiming it. - Don't let a candidate that began inside a comment or another word's quoted literal grant ownership.
- Add both repros as regression tests asserting PARTIAL /
STATIC_PARSE_LIMIT.
-
[Non-blocking]
assignment_quotechanges behavior outside shell (:1752). Every=-adjacent quote in every file type (Markdown, Python, HTML, …) is now parsed as a shell word. A mis-paired quote after=, such as Pythonx='It\'s'or a prose typo likename="value, can now report exhaustion where main returned False; the new test already assertsx="unclosed"plus padding is partial. That is defensible fail-closed behavior, but please cover it in a non-shell file type and mention it in the description. -
[Non-blocking] Test coverage. All new tests call
has_bounded_parse_exhaustionwith the defaultfile_type="shell". Please add:- Markdown/
SKILL.mdcases; - an end-to-end repro of #628 (
SKILL.mdplus a paddedscripts/run.sh) asserting complete coverage and no AE1; - the issue's comment form,
# use "$(cmd)" hereplus padding; - the repros from finding 1.
- Markdown/
-
Overlap. These are complementary PRs, not duplicates.
- #627 (approved) fixes a different root cause: Markdown contractions, and confining exhausted-span evidence to its command. Both PRs edit
_has_shell_command_word_exhaustion, in separate hunks (roughly main lines 1728-1791 here vs 1810-1821 there), so they should merge cleanly. Whichever lands second should be rebased and re-run against the parse-limit suites. - #465 changes
_next_shell_invocation_word/printf-invocation handling for bare$ARGUMENTS(#464) and does not touch this code. It is already conflicting with main: itssentinel collides with main's_RUNTIME_SHELL_PARAMETER_SENTINEL.
- #627 (approved) fixes a different root cause: Markdown contractions, and confining exhausted-span evidence to its command. Both PRs edit
-
FYI, pre-existing and out of scope. On main, a stray
"in a comment already hides the parameter form$CMD -rf /. The mis-aligned dynamic word advancesparsed_through, so# see "notes/$CMD -rf //# "is reported complete on main. This deserves its own issue. The fix for finding 1 should not extend that gap to$(...)forms, which main currently handles.
Tests/CI: CI is green at e2080b2f (lint, test-unit, DCO, docker-smoke, OpenCode TS). According to the PR description, the positive tests fail on main.
Decision: Changes Requested (reviewed head e2080b2ff7438f56389bbb55bd58677e3d01df60)
Signed-off-by: Deepak Jain <deepujain@gmail.com>
Keep review finding 1 closed for trailing comments and prose: a word that
starts in a comment, or at a quote after `=`, can claim quotes only on its
own line, and a quote that opens a runtime word (`"$(...)`, `"${...}`,
`` "`...` ``) is never claimed. Before this, `ls # see "notes` could hide a
following `"$(resolve_tool)" -rf /` that main reports as partial.
Let comment-origin words claim same-line closing quotes, so the NVIDIA#628
comment form `# use "$(cmd)" here` no longer consumes following padding.
A quote after `=` names an assignment value, never a command, and main never
parsed that position. It now only claims ownership, which restores main's
result for `x="$(printf '%s' "$X")"`. When a backtick or quote directly
encloses the assignment, as in a documented `` `X="${X:-default}"` ``, the
word stops at that delimiter and owns the closing backtick instead of
opening a new substitution that runs to the end of the file.
Add the reviewer-requested coverage: non-shell file types, a Markdown fence,
the NVIDIA#628 comment form and an end-to-end NVIDIA#628 bundle, plus regressions for
the trailing-comment, prose and host-language ownership cases.
Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @deepujain, thank you for the quick turnaround on the first review and for sticking with this one. The quote-ownership approach is the right fix for #628, and it also unblocks a customer skill we were chasing internally. I re-reviewed 20bb62d against the earlier findings and pushed one maintainer commit, 11d1629, to close the gaps below. Please take a look and tell me if anything in it doesn't match your intent.
Status of the earlier findings on 20bb62d
- [Blocking] quoted runtime commands. Mostly addressed.
$/backtick starts are no longer owned, interior words stop at their wrapper quote, and both repros are PARTIAL with ledger assertions. However, ownership was only withheld from lines that start with#, and the new quote-after-=candidates could also claim a quote on a later line. Each of these is complete on20bb62dbut PARTIAL onmain:- trailing comment:
ls # see "notes/"$(resolve_tool)" -rf //# " - Markdown prose:
Set name="value in prose.followed by a fenced"$(resolve_tool)" -rf /and a later" - host source:
x = f(name="value)/"$(resolve_tool)" -rf //" - prose parameter:
Set name="value/$T -rf //" - same line:
name="a "$(resolve_tool)" -rf /"
- trailing comment:
- [Non-blocking] non-shell behavior. The description note is there, but the new test used the default
file_type="shell". - [Non-blocking] coverage. The Markdown ledger case is there. The end-to-end #628 bundle and the
# use "$(cmd)" herecomment form were missing. The comment form was also still partial, because comment lines could claim nothing at all.
Two more regressions against main, found while testing a customer skill
x="$(printf '%s' "$X")"andmodel_count="$(printf '%s\n' "$j" | jq -r 'length')"are complete onmainbut partial here. The quote after=became a candidate and was then checked as a possible runtime-built command.- A documented
`X="${X:-default}"`in a Python docstring: the quote-after-=word treated the span's closing backtick as a new substitution and consumed the rest of the file.
What 11d1629 changes (static_patterns_tool_misuse.py)
- A word that starts in a comment (line-start or trailing
#), or at a quote after=, may only claim quotes on its own line. Same-line claims are allowed, which fixes the #628 comment form. - A quote followed by
$or a backtick is never claimed, since it opens a runtime-selected word, so a mis-paired claim can't hide its operands. - A quote-after-
=candidate only claims ownership. It skips the printf and outer-operand checks, because an assignment value never names the command andmainnever parsed that position. A multi-line one also doesn't advanceparsed_through. - When a backtick or quote directly encloses the assignment name, the word stops at that delimiter and owns the closing backtick.
- The blanket "line-start comments claim nothing" rule is replaced by the rules above.
Tests added (test_shell_assignment_quotes.py, now 31 tests): the unclosed-assignment test across shell, Markdown and Python; the #628 comment form, a Markdown fence and both printf assignment values with padding; three backtick-enclosed assignment cases with apostrophe padding; the five mis-paired ownership cases above; and an end-to-end #628 CLI bundle asserting complete coverage, no ledger exceptions and no AE1. The 11 targeted tests fail on 20bb62d and pass on 11d1629.
Validation
-
This head merged with current
main(ec6633e): full non-integration suite8434 passed, 14 skipped, 4 xfailed. Ruff lint and format pass. -
Fail-closed parity: every probe above, plus the original review repros, matches
main's PARTIAL result. One intentional difference is inherited from20bb62d:echo "$(resolve_tool)" " -rf /"is now complete, which is shell-correct, because" -rf /"is a single quoted argument.mainonly flagged it through quote mis-pairing. -
Customer skill (
skills/vss-build-vision-aifrom NVIDIA-AI-Blueprints/video-search-and-summarization#2386,--no-llm):Build Partial files AE1 main16 35 20bb62d9 5 11d16298 4 11d1629+main+ #6865 0 The last 4 AE1 on this PR alone come from
${VAR:-default}inside backticks, which #686 fixes. The two PRs merge without conflicts and their test files pass together.
Non-blocking, out of scope
- The 5 remaining partial files come from prose apostrophes and non-shell quoting in YAML, logstash and Python string literals. They are tracked separately.
- Pre-existing and unchanged from
main: a trailing-comment quote followed by"/usr/bin/${T}" -rf /is complete on both. - The branch is 50 commits behind
mainbut has no conflicts.
Decision: Approve (reviewed head 11d1629cc394c9fd67c108fdb3021e41047cb70b)
Fixes #628.
A valid quoted shell assignment near the start of a longer file could make a benign skill report
static_parse_limit: the sweep revisited its closing quote as a new command and consumed unrelated trailing content.Successfully parsed words now own their closing quotes, including assignment values. Executable substitutions remain independent candidates, and a substitution's interior word stops at its surrounding quote. Comment-origin candidates cannot grant ownership to a later command. This preserves partial coverage for runtime-selected commands with destructive operands, including the two reviewer examples.
Validation: both reviewer examples fail on the previous head and pass after repair, including the analyzer ledger's PARTIAL/static_parse_limit outcome. The shell-assignment, documentation-reconstruction and adjacent static-pattern suites pass 462 tests. The original benign-padding, nested-command and failed-parse controls remain covered. Ruff lint and formatting pass. No shell fixture is executed and no provider-backed integration is claimed.
The conservative assignment check also applies to non-shell text: an unclosed
name="valuefollowed by enough content is partial, now pinned by a regression.