Fix UnusedVariables false positive on result-referenced variables#297
Open
feiiiiii5 wants to merge 1 commit into
Open
Fix UnusedVariables false positive on result-referenced variables#297feiiiiii5 wants to merge 1 commit into
feiiiiii5 wants to merge 1 commit into
Conversation
`Interpreter.unused_assignments()` broke out of its loop upon encountering the `result = ...` assignment (produced by STOP) without walking the assignment's value for `ast.Name` references. This caused variables only referenced via the result expression to be falsely flagged as unused — e.g. scanpy pickles where `_varN` is referenced via BINGET inside the result tuple (trailofbits#226). The same class of bug also existed in four opcode implementations where `ast.Tuple`/`ast.Dict` were constructed with non-list field values (tuples, `reversed()` iterators). `ast.walk` only recurses into list-typed fields, so variables nested inside these nodes were invisible to `unused_assignments()` even after the result-walk fix: TupleOne/Two/Three, the variable-arity TUPLE (StackSliceOpcode), and the DICT opcode. This only affects the unused-variable heuristic's precision (false positives); it does not weaken any security check. Per SECURITY.md, UnusedVariables bypasses remain out of scope, but false positives on genuinely-used variables are a quality-of-output regression and are in scope. Fixes trailofbits#226.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #226.
fickling --check-safetyreports false-positiveUnusedVariableshits on variables that are used — specifically, variables referenced only inside theresult = ...expression (the final assignment produced bySTOP). The scanpy pickle in the issue shows_var16flagged as unused even though it is referenced viaBINGETinside the result tuple.Root cause
Two related bugs, same effect:
Interpreter.unused_assignments()skips the result value. Upon encountering theresult = ...assignment itbreaks out of its loop without walkingstatement.valueforast.Namereferences. So any variable only referenced via the result expression is invisible to the heuristic.Four opcode implementations construct
ast.Tuple/ast.Dictwith non-list field values.ast.walkonly recurses into list-typed fields, not tuples or iterators. So even after fixing (1), variables nested inside these AST nodes would still be missed:TupleOne/TupleTwo/TupleThree—eltswas atupleTuple(the variable-arityTUPLEopcode,StackSliceOpcode) —eltswastuple(stack_slice)Dict(theDICTopcode) —keys/valueswerereversed(...)iteratorsFix
unused_assignments()now walksstatement.valuewithast.walkand adds everyast.Nameid tousedbefore thebreak.listinstead oftuple/reversed().Scope
This is a false-positive fix, not a bypass fix. It sharpens the UnusedVariables heuristic's precision (fewer false alarms on legitimate pickles) without weakening any security guarantee: genuinely malicious eval/import calls are still flagged with the same severity. Per
SECURITY.md, UnusedVariables bypasses remain out of scope; reducing false positives on used variables is a quality-of-output improvement and is in scope.Testing
New regression tests in
test/test_benign_edge_cases.py(7 tests):test_var_used_in_result_not_flagged—result = _var0(simplest case, exercises fix Add NewObj_EX opcode #1)test_var_used_multiple_times_in_result_via_memo—result = (_var0, _var0)viaBINGET+TupleTwo(mirrors the scanpy structure from False positive for--check-safety(variable is actually used afterwards) #226; exercises fix Add TUPLE1, TUPLE2 and TUPLE3 #2)test_multiple_vars_used_in_result_not_flagged—result = (_var0, _var1)(guards against over-correction)test_var_used_in_tuple_stack_slice_not_flagged—result = (_var0, _var0, _var0, _var0)viaTUPLEopcode (exercises fix Add example showing the creation of a hello world Trojan on PyTorch e… #3)test_var_used_in_dict_keys_not_flagged—result = {_var0: "v1", _var0: "v2"}viaDICTopcode (exercises fix Carson/more poc #4)test_genuinely_unused_var_still_flagged—_var1is genuinely unused; ensures the fix does not over-correct and hide real unused variablestest_insert_python_eval_replacing_result_no_false_positive— end-to-end smoke test viacheck_safety(); asserts the eval is stillOVERTLY_MALICIOUSandUnusedVariablesno longer appears in detailed resultsUpdated
test/test_pickle.py::test_unused_variables— the old test useduse_output_as_unpickle_result=True, which makesresult = _var0(so_var0IS used) yet asserted_var0was unused. That assertion codified the bug. Switched torun_first=True, use_output_as_unpickle_result=False, where_var0is genuinely unused (resultis the original[1,2,3,4]). Severity remainsOVERTLY_MALICIOUS.Full suite:
118 passed(was 116; +2 new tests for the StackSlice TUPLE and DICT cases).ruff formatclean.ruff checkreports only one pre-existingB905intest_pickle.py:44(unrelated to this change).AI Disclosure
This contribution was developed with assistance from an AI coding agent (Claude / TRAE IDE). Specifically:
ast.walknon-list-field limitation), wrote the opcode-level regression tests using fickling'sop.*API, and ran a skeptical self-review pass.--check-safety(variable is actually used afterwards) #226; every fix point was manually traced against CPython'sast.walksemantics; the regression tests were empirically validated to fail without the fix and pass with it; the scope framing (false-positive fix vs. bypass) was manually checked againstSECURITY.md.I have reviewed every line of the diff and the tests, and I stand behind the correctness of this change.