Repository navigation
fix(analyzer): stabilize TM1 window identity - #578
chrisknvidia wants to merge 30 commits into
Conversation
rng1995
left a comment
There was a problem hiding this comment.
Reviewed exact draft head d856d88c289c5ef3389060ef69651c9981aae38f. I found no additional blocker in the focused TM1 identity range (f2ae98f..d856d88): 220 focused tests and 392 current-main merged-tree regressions pass, and lint/format/diff checks are clean.
I am requesting changes because the current combined tree still contains both confirmed dependency blockers from #576 and #577. On this exact head, a three-row ledger cap drops the second distinct fatal fact, and shell=enabled still evades TM1 when a later argument expression is effectful. The PR is also still draft and its own review contract requires #576/#577 to land, a rebase onto current main, fresh CI, and current-head review.
Please propagate the dependency fixes and rebase. If the focused range remains semantically unchanged and CI stays green, I found it otherwise suitable for approval.
rng1995
left a comment
There was a problem hiding this comment.
The focused #578 cap/source-order work looks correct and its prior #576/#577 reproductions now pass, but this stacked head contains the exact #577 receiver-invalidation blocker: an effectful argument after shell= may replace the trusted subprocess receiver, yet later proxy calls are still classified as subprocess and receive false-positive TM1 findings. Please update the stack after #577 clears trusted_names on this effectful path and add the regression. The current head is otherwise conflict-free and its affected/integration suites are clean.
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Changes requested on exact head 1c03702961323bd8d322c89928480197282d1d0b for a confirmed remaining issue inherited from #577.
The earlier effectful-argument fix is present: direct subprocess calls now invalidate receiver trust after unsafe arguments, including expression, assignment, and annotated-assignment paths. However, an ordinary effectful function call can still replace the receiver without invalidating that trust; a later call through the replacement object is then reported as HIGH TM1. The inline comment provides the source-traced fixture and expected correction. Please propagate the #577 correction through this stack and cover this case.
Scope: I inspected the prior review history, current dependency fix, receiver-trust collector and both scan passes, its caller integration, tests for the direct-argument correction, and current checks. This is a focused blocking review, not approval or certification of the entire large combined stack. The remaining combined diff still needs complete current-head assessment after the dependency issue is addressed. Six hosted checks pass, but passing checks do not establish this untested receiver semantic.
No contributor-provided code or tests were executed locally.
2e94945 to
508e16a
Compare
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
508e16a to
0239f80
Compare
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Re-reviewed the previous receiver-invalidation finding on exact head 193bb90a3eed190562a9e97103b0aba5d29773b3, including its current implementation, forward/prepass effects, caller/ownership integration, all prior reviews/replies, and relevant regression tests.
The exact standalone-call examples and the earlier effectful-argument ordering fixes are present. However, the required receiver-safety correction is still incomplete: a generic effectful call nested in a tuple/list RHS leaves the receiver trusted, and a subsequent fresh true binding makes a proxy call a false-positive HIGH TM1. This is the same remaining defect in the current #577 implementation. The inline continuation describes the source-traced case and required regression coverage.
All six hosted checks pass, but their generic-call tests cover only outer ast.Call values. Please fix this in the shared implementation and propagate the correction through the stack.
Review scope remains a focused blocking re-review of the dependency and its integration, not full clearance of the nine-file, 6,064-added-line combined TM1/window stack. Complete current-head stack assessment remains required before approval after the blocker is fixed. No contributor code/tests were executed locally; no merge was performed.
…window-identity Signed-off-by: Christopher Kevin <christopherk@nvidia.com> # Conflicts: # src/skillspector/nodes/analyzers/static_runner.py # tests/nodes/analyzers/test_shared_python_ast.py
…window-identity Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @chrisknvidia, thank you for the persistence on the receiver-trust work and for adding the nested-container regressions!
Value and readiness: The nested tuple/list receiver finding is fixed. However, 68e22d0 makes the shared receiver-trust model conservative enough that TM1 shell=<name> findings main reports today are now silenced in ordinary script layouts. That reopens the issue #475 evasion class, so this needs a fix before the stack is ready. The TM1 window/identity work that earlier reviews assessed is unaffected by this update apart from merge resolutions, which look correct. #577 is still open with changes requested.
Previous findings:
- Nested tuple/list/annotated RHS receiver invalidation (last review, inline thread): Resolved. The first part landed in
7fee7afand68e22d0extended it._expression_preserves_receiver_trust(static_python_shell_truthiness.py:780-835) now rejects any eager expression that is not passive or a passive direct subprocess call. It recurses into tuples, lists and comprehensions, and both the forward scan and the_advance_trusted_namesprepass use it. Covered bytest_nested_generic_call_invalidates_receiver_trust[tuple-rhs|list-rhs|annotated-tuple-rhs],test_nested_generic_call_invalidates_receiver_trust_for_called_functionandtest_unsupported_eager_statement_invalidates_receiver_*. Real positives remain intest_direct_subprocess_call_remains_detected. - Earlier findings (inherited #576/#577 ledger-cap and later-effectful-argument blockers, effectful arguments after
shell=, standalone generic calls): Resolved in earlier rounds and still in place at this head. - Rebase and fresh CI: Resolved for main (main merged at
8831219, 6/6 checks green). #577 remains open.
On your inline reply: I confirmed the fix. Note that 7fee7af is a "Merge remote-tracking branch" commit that also carries the fix itself: +34/-11 lines in the truthiness module and +37 test lines, plus static_runner.py / test_shared_python_ast.py conflict resolutions. The resolutions adopt main's coordinate_view and include_source_offsets correctly. 912263f is a clean main merge. Please land fixes as their own commits so reviewers can isolate them.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/static_python_shell_truthiness.py:1135,:1565withstatic_patterns_tool_misuse.py:5480: TM1 detections that main reports are now lost (introduced by68e22d0)._reconcile_variable_shell_findingsdrops every same-scopex = True ... shell=xlexical finding (same_scopeor ownershipFalse) and leaves it to the companion. After68e22d0the companion declines in common code:_advance_trusted_namesnow clears trust for every unsupported statement (line 1135; before, it removed only explicit rebinds). A trailingif __name__ == "__main__":therefore records an invalidation after every earlierdef, and line 1519 scans those function bodies with no trust.- Once any generic call sets the new sticky
unknown_unsafe_bindings, every later import clears trust, includingimport subprocess(line 1565).
Source-traced failing inputs. Main reports TM1 HIGH for both via
_VARIABLE_SHELL_FLAG_RE, and7fee7afalso detected both through the companion. This head reports nothing:import subprocess def run(cmd): use_shell = True subprocess.run(cmd, shell=use_shell) if __name__ == "__main__": run("ls")
import os payload = input() import subprocess enabled = True subprocess.run(output, shell=enabled)
The second input is the original
test_preparsed_python_is_reused_by_all_ast_analyzersfixture.68e22d0reorders it to putimport subprocessfirst instead of keeping the detection. The same reconciliation also drops main's finding forreturn subprocess.run(cmd, shell=use_shell)becauseReturnis never inspected. That part predates this update.
Expected fix: when the companion cannot prove receiver identity or truthiness, keep main's lexical TM1 (reduced confidence is fine). Suppress it only when the receiver is explicitly rebound (store, import alias, attribute mutation) or the companion emits its own finding for that call. Fix this in the shared #577 module and propagate it through this PR and #579. Addnode()-level regressions for both inputs above and restore the original fixture order. -
[Non-blocking] Test parity across the stack: #579 carries about 600 more lines of tests for the byte-identical truthiness module, for example
test_unknown_unsafe_binding_blocks_receiver_reestablishmentand the class-body release cases. This PR ships the same code without them. Please sync the tests into the layer that introduces the code. -
[Non-blocking] The PR body is stale. It lists #576 as unmerged, cites #577 at
c3b0ff9, and its validation counts predate68e22d0. Please refresh it.
PIC tradeoffs: The receiver-trust model now treats any statement that could in theory rebind subprocess as if it did. That includes a later call, a compound statement, a class with any base, and a __del__ finalizer. This removes contrived proxy false positives but costs detections in ordinary scripts. The PIC should confirm that an uncertain receiver keeps the lexical TM1 finding rather than dropping it.
Verification and gaps: I read all four prior rng1995 reviews, both inline threads and the replies. I compared the PR diff against its merge base at 193bb90 and at 68e22d0 per file. I inspected git show --remerge-diff for 7fee7af (conflicts in static_runner.py and test_shared_python_ast.py, plus author edits) and 912263f (clean), and read all of 68e22d0. I traced the forward scan, both prepasses, _reconcile_variable_shell_findings and main's _VARIABLE_SHELL_FLAG_RE / _variable_shell_flag_same_scope path for the inputs above, and compared _advance_trusted_names at 193bb90 and 68e22d0. I did not re-assess the window/identity range beyond the merge resolutions. All six checks pass. Per policy, tests and contributor code were not executed locally.
Decision: Changes Requested (reviewed head 68e22d068437bd9b3e933fdd7f6a7e9047275a47)
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
Signed-off-by: Christopher Kevin <256191862+chrisknvidia@users.noreply.github.com>
rng1995
left a comment
There was a problem hiding this comment.
Re-reviewed exact head 481a8fbe57774fbb9e8311436d36e9ceed0ab511 against all earlier reviews and the full current diff. The reported original lexical-abstention cases now retain HIGH TM1, and focused analyzer/runner/end-to-end tests pass. Independent base-to-head reproductions confirm three P1 groups: untaken expression stores, identity-preserving receiver/flag stores, and an earlier compound-statement invocation hidden by a future module store. Each still executes the native subprocess call with shell=True but loses its HIGH finding on this head. Details and minimal reproductions are inline. The green hosted checks do not cover these regressions. Changes requested; do not merge until they are fixed. The three earlier threads whose exact examples are now fixed are resolved; #577 remains an open dependency.
Resolve two additive conflicts: keep both sides' imports in static_patterns_tool_misuse.py, and in static_runner.py keep the PR's deferred output-limit helper next to main's PythonStringClosers setup. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
Address the three remaining review findings on lexical TM1 reconciliation, where a bound shell=True call still ran natively but lost its HIGH finding: - Untaken expression stores: a walrus in a short-circuited BoolOp operand, an untaken conditional-expression arm, or a comprehension/generator body is no longer recorded as replacement evidence. Only constant operands and tests prove that the store executed. - Identity-preserving stores: unpacked targets are paired with their RHS elements. Self-stores are transparent, truthy constants keep the flag, and native receiver aliases (saved = subprocess, import subprocess as saved, Popen = subprocess.Popen) keep the native receiver. - Deferred bodies: an outer store only proves replacement when every invocation the owner scope can reach sees it. Any reference to the function, method or class (compound statements, main guards, callbacks, other bodies) can invoke it from that point on, and decorators or lambdas can run once defined. This also covers a re-import before a later invocation. The release-trigger test for a called function now matches its module-level twin: the companion abstains and the lexical HIGH remains. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's NVIDIA#784 test expects the tool-misuse ledger event for an invalid Python file to carry obfuscated_instruction_text. This PR makes tool misuse parse Python for TM1 reconciliation, so the runner's higher-precedence syntax_error now names that partial event. Keep the fail-closed contract: assert the marker reading through the lexical-only anti-refusal analyzer and keep tool misuse partial with syntax_error. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… proof Port the six true-prefixed TM1 regression tests from NVIDIA#577. This branch already carries test_true_direct_calls_on_one_line_keep_distinct_locations as test_true_direct_calls_around_safe_comprehension_keep_distinct_locations, so only the other five are added. Two of them reported the same call twice on this branch: - A variable window for a name spelled `true` (in any case) duplicated the case-insensitive direct `shell=True` owner at the same call. AST reconciliation removes that window only when the dataflow companion takes the call, so a file that does not parse, or a call after an unknown receiver effect, kept both findings. coalesce_path_findings now drops such a window when a direct lexical owner starts at the same call. A window for a call whose direct candidate was folded into an identical same-line call still reports that call. - A call-anchored window for a longer true-prefixed name (`true_value`) used its raw text as identity, so the raw and normalized security views of one call did not collapse, and reconciliation then moved both to the call. The window now takes its identity from the normalized view text, as the direct owner already does. Also cover both root causes with a test for a nested `true` call after an unknown receiver effect and a normalized `true_value` call in a file that does not parse. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eports The `\b`-bounded direct pattern does not match `shell=true_value`, so on this branch such a call was reported only through a variable window. That window pairs an assignment with a single call, skips an assignment inside an earlier window, and was dropped outside Python. Compared with NVIDIA#577 this lost: - calls in JavaScript and Markdown files, fenced or plain; - a later call that the AST companion abstains on, for example after an unknown receiver effect; - all but the last same-line call in a file that does not parse, and the second of two identical same-line `shell=true` calls outside Python, which the direct candidate key folds into one; - a call whose assignment sits inside the window of an earlier one. _variable_shell_matches now pairs every assignment of a true-prefixed name with each call its bounded window reaches, and the nearest assignment owns each call. Candidate generation, analysis and AST reconciliation share that list, and a window's candidate identity uses its full text, so windows to different calls do not collapse on a shared 200-character preview. Outside Python, call-anchored windows for true-prefixed names are kept, and coalescing still drops one when a direct owner reports the same call. With an AST, each added window passes the same visibility, counterevidence, cached replacement and companion checks as before. Windows for other names are unchanged. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-identity NVIDIA#577 landed on main as 5ee48ef. This branch carries the superset of its TM1 work: the same lexical-counterevidence fix, the window-identity changes, the five NVIDIA#577-only tests ported in 7b643ff, and a differential check showing every call NVIDIA#577 reports is still reported here. For the four files that conflict (static_patterns_tool_misuse.py, static_python_shell_truthiness.py, test_shared_python_ast.py, test_tool_misuse_python_ast.py) keep this branch's version. No other main commit touched them since the previous merge. The shared-AST fixture keeps the input()-before-import subprocess order the review asked for. The other NVIDIA#577 files merged to this branch's content unchanged. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @chrisknvidia, thank you for keeping the window-identity work in step with #577 through every round!
Value and readiness: Ready once CI passes on the new head. #577 is now on main, and this branch carries the TM1 window-identity changes on top of it. The three P1 findings from the last round are fixed. While stacking this branch on the merged #577, I found and fixed two gaps where this branch reported differently from #577.
Previous findings (review at 481a8fb)
- [P1] Untaken expression stores: Resolved in
15b4e58, the same change as #577. - [P1] Identity-preserving receiver and flag stores: Resolved in
15b4e58. - [P1] Future outer store hiding an earlier invocation: Resolved in
15b4e58. - Earlier rounds (main guard, input-before-import, lexical abstention): still resolved, and node-level regressions cover them.
What I changed on the branch (signed off)
-
9162964mergesmain(additive conflicts in imports andstatic_runner.py). -
15b4e58applies the lexical-counterevidence fix byte-identical to #577's. -
548b17cadapts main's #784 marker-ownership test, the same as #577. -
7b643ffDuplicate TM1 findings. This branch emitted two findings for one call where #577 emits one. Both cases are onmaintoo:true = True+shell=truein unparseable Python, or after an unknown effect, reported both the variable window and the direct owner;- the raw and normalized (
ff) views of atrue_valuewindow kept different identities.
Now there is one owner per call. It also ports five #577-only tests.
-
327591bDetection lost against #577. This branch's\b-bounded direct pattern no longer ownsshell=true_value. Sotrue_value = True+shell=true_valuein.jsor Markdown got 0 findings, while #577 reports 1 andmain2. A second call after an unknown effect, and two calls on one unparseable line, were also missed. True-prefixed names now get a window per call, and outside Python that window is kept. -
4031711merges currentmain(with #577). In the four files #577 also changed, this branch's superset version is kept; no othermaincommit touched them. The shared-AST fixture keeps theinput()-before-import subprocessorder you restored.
Differential check: I ran 2,400 generated inputs (names, call forms, ff prefixes, layouts, malformed Python, .py, .js and SKILL.md). Every call #577 reports is reported here, with no new duplicates. The only extra findings in #577 are its double reports of subprocess.Popen calls (at column 0 and at the Popen token).
Non-blocking notes
- Unlike #577, which matches on the name prefix alone, this branch uses the AST, so
shell=true_valuewith noTruebinding is no longer reported (a parameter,= False, or a shadowed name). That is the intended precision. - Already on
mainand on #577: duplicate windows for non-truenames with theffprefix or Cyrillic look-alikes.
Verification:
- Full local non-integration suite on
4031711: 11,310 passed, 0 failed. - The 20-layout TM1 probe matches #577.
ruffis clean.
Decision: Ready to merge once CI passes. A human maintainer needs to approve (reviewed head 4031711b9622e9ece1cb2a04c5a985fb43e0f7ba). This bot pushed commits to the branch, so it does not approve the PR itself.
…-surface-classification This branch already contains NVIDIA#578's changes, the same commits cherry-picked with the execution-surface work on top. Record NVIDIA#578's reviewed head as an ancestor so that once NVIDIA#578 lands with a merge commit, this branch merges main without conflicts. In the four overlapping files, keep this branch's version. The resulting tree is identical to a4131c9. Signed-off-by: Narendran Raghavan <nraghavan@nvidia.com>
TM1 reconciliation can drop lexical findings or collapse distinct long calls when displayed evidence collides. Preserve the lexical signal during companion abstention, use complete source identities for retained-owner matching, and retain source order and canonical metadata under bounded windows and output caps.
Review fixes keep uninvoked global/nonlocal stores, annotation-only declarations, unrelated attribute stores and same-slot assignments from suppressing lexical HIGH. Explicit called-slot replacement has a bounded lifetime: cached-module changes are tracked across ordinary imports, while unknown eager effects discard replacement certainty. Native callable captures, cross-API assignments and native values wrapped in RHS expressions cannot establish replacement proof. The legacy first-statement direct shadow control remains bounded to a single completed store with no native receiver/Popen reference and no later eager effect. Receiver binding, report ownership and cached slot state remain separate.
This branch includes the still-open #577 changes. #576 is merged.
Validation of this revision: 663 affected analyzer/graph/input cases passed; 51 fresh installed-wheel CLI scans passed across positive/negative source cases, real reports and strict exits. Ruff lint/format for src/tests and diff checks passed. Static verification uses real native analyzer/graph execution and an installed wheel with --no-llm; remote provider inference, production deployment and other operating systems were not exercised. Earlier broad-suite results were from older heads and do not establish a green full suite for this update. Hosted CI and human re-review remain required.