Sitelet https://web.archive.org/web/20250501213549/https://github.com/github/codeql/pull/12537
Skip to content

Python: Captured variables for type tracking and the API graph #12537

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 9, 2023

Conversation

yoff
Copy link
Contributor

@yoff yoff commented Mar 15, 2023 •

We should perhaps check more thoroughly that the jump step is always correct.
From all the fixed results, it looks good, though...

  • In TypeTrackerSpecific.qll we add a jump step

    • to every scope entry definition
    • from the value of any defining DefinitionNode
      (In our example, the definition is the class name, Users,
      while the assigned value is the class definition, and it is
      the latter which receives flow in this case.)
  • In LocalSources.qll we allow scope entry definitions as local sources.

    • This feels natural enough, as they are a local source for the value, they represent.
      It is perhaps a bit funne to see an Ssa variable here,
      rather than a control flow node.
    • This is necessary in order for type tracking to see the local flow
      from the scope entry definition.
  • In ApiGraphs.qll we no longer restrict the result of trackUseNode
    to be an ExprNode. To keep the positive formulation, we do not
    prohibit module variable nodes explicitly. Instead we restrict to the new
    LocalSourceNodeNotModule which has a restricted charpred.

  • Adjusted tracked.ql, the type tracking test:

    • no need to annotate results on line 0
      this could happen for global SSA variables
    • no need to annotate scope entry definitons
      they look a bit weird, as the annotation goes on the
      line of the function definition.

@yoff yoff added the Awaiting evaluation Do not merge yet, this PR is waiting for an evaluation to finish label Mar 15, 2023
@yoff yoff force-pushed the python/captured-variables-for-typetracking branch from 73829e8 to 0723a05 Compare March 16, 2023 09:54
@yoff
Copy link
Contributor Author

yoff commented Mar 16, 2023

Evaluation showed a modest slow-down. This is expected, since we compute a bigger graph.
It also showed a more dramatic slow-down of py/jump-to-definition in one case. I suppose this is not really surprising.

yoff added 4 commits March 16, 2023 12:53
This is a condensed versio of the user reported example
found [here](https://github.com/dsp-testing/apictf/blob/eb377d5918bb4ac316a32361e5e0c082e61036d6/app.py#L278)
The `MISSING` annotation indicates where our API graph falls short.
type tracking and the API graph.

- In `TypeTrackerSpecific.qll` we add a jump step
  - to every scope entry definition
  - from the value of any defining `DefinitionNode`
    (In our example, the definition is the class name, `Users`,
     while the assigned value is the class definition, and it is
     the latter which receives flow in this case.)
- In `LocalSources.qll` we allow scope entry definitions as local sources.
  - This feels natural enough, as they are a local source for the value, they represent.
    It is perhaps a bit funne to see an Ssa variable here,
    rather than a control flow node.
 - This is necessary in order for type tracking to see the local flow
    from the scope entry definition.
- In `ApiGraphs.qll` we no longer restrict the result of `trackUseNode`
  to be an `ExprNode`. To keep the positive formulation, we do not
  prohibit module variable nodes. Instead we restrict to the new
  `LocalSourceNodeNotModule` which avoids those cases.
Adjusted `tracked.ql`
- no need to annotate results on line 0
  this could happen for global SSA variables
- no need to annotate scope entry definitons
  they look a bit weird, as the annotation goes on the
  line of the function definition.
@yoff yoff force-pushed the python/captured-variables-for-typetracking branch from 66d83fb to f9bffb5 Compare March 16, 2023 11:56
@yoff yoff marked this pull request as ready for review March 16, 2023 11:57
@yoff yoff requested a review from a team as a code owner March 16, 2023 11:57
Copy link
Member

@RasmusWL RasmusWL left a comment

Choose a reason for hiding this comment

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

I have not looked at the QL code yet, just skimmed the examples added.

I would like you to add more examples of edge cases, and things we still don't quite handle correctly even with this PR.

I would assume we handle Foo/Bar correctly in the examples below, but not so sure about Baz (can differentiate on whether a subclass of A or B reaches the print statement).

def func()
    if <cond>:
        class Foo(A): pass
    else:
        class Foo(B): pass

    class Bar(A): pass
    class Bar(B): pass

    class Baz(A): pass
    
    def other_func():
        print(Foo)
        print(Bar)
        print(Baz)

    class Baz(B): pass

    other_func()

I guess there are also cases where a reference to a global variables is captured, and the variables is altered in a function (using global modifier) -- but this might be covered by our tests already?

Lastly, do we have tests that cover capturing by value, such as the code below?

def func():
    a = 1
    def inner(a_val=a):
        print(a_val, a)
    a = 2
    inner()

@yoff
Copy link
Contributor Author

yoff commented Apr 26, 2023

Nice suggestions for missing tests. Some of them are more dataflow than type-tracking, but good to have so I added them anyway 👍

@yoff yoff requested a review from RasmusWL April 26, 2023 13:13
@yoff yoff removed the Awaiting evaluation Do not merge yet, this PR is waiting for an evaluation to finish label Apr 27, 2023
yoff and others added 2 commits April 27, 2023 17:32
Instead of reusing `nonSink0` for both captureOut1NotCalled and
captureOut2NotCalled tests (I used 1/2 naming scheme to match things up
nicely).

I also added a comment highlighting that `m` is the function that is not
called (since I overlooked that initially :O)
Copy link
Member

@RasmusWL RasmusWL left a comment

Choose a reason for hiding this comment

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

I made a small set of changes locally to use fresh variables names in some tests, and instead of requesting that you click buttons to apply these changes, I simply pushed the changes on the PR myself.

However, there are a few minor things I don't feel as confident about, so doing this in normal review style 😊

But overall this looks really good, and we have a really nice set of improvements to our results 💪 🎉

We should perhaps check more thoroughly that the jump step is always correct.

In the PR description you said the above, so just want to check whether this is something we need to look at more thoroughly? (to me it looks ok)


def other_func():
print(Foo) #$ use=moduleImport("foo").getMember("A").getASubclass() use=moduleImport("foo").getMember("B").getASubclass()
print(Bar) #$ use=moduleImport("foo").getMember("B").getASubclass() MISSING: use=moduleImport("foo").getMember("A").getASubclass() The MISSING here is documenting correct behaviour
Copy link
Member

Choose a reason for hiding this comment

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

I disagree. There is no path where the Bar printed on this line can be a subclass of A.

Suggested change
print(Bar) #$ use=moduleImport("foo").getMember("B").getASubclass() MISSING: use=moduleImport("foo").getMember("A").getASubclass() The MISSING here is documenting correct behaviour
print(Bar) #$ use=moduleImport("foo").getMember("B").getASubclass()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no path where the Bar printed on this line can be a subclass of A.

On this we agree, but if I simply leave out the annotation, we do not know if it finds such a path or not. This because we are in the context of "admissible annotations", so it would not complain if it sees something but does not find an annotation. I tried to express that the MISSING annotation is used to document the fact that no path is, correctly, found.

@yoff yoff requested a review from RasmusWL May 3, 2023 16:24
@yoff
Copy link
Contributor Author

yoff commented May 4, 2023

We should perhaps check more thoroughly that the jump step is always correct.

In the PR description you said the above, so just want to check whether this is something we need to look at more thoroughly? (to me it looks ok)

I think it is ok :-)

RasmusWL
RasmusWL previously approved these changes May 4, 2023
Copy link
Member

@RasmusWL RasmusWL left a comment

Choose a reason for hiding this comment

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

Besides a small nit, this looks ready to be merged for me 👍

@yoff
Copy link
Contributor Author

yoff commented May 4, 2023

Interesting question from @calumgrant: Would a DCA run show, via some meta query, that we find new edges in the call graph?

@RasmusWL
Copy link
Member

RasmusWL commented May 4, 2023

Interesting question from @calumgrant: Would a DCA run show, via some meta query, that we find new edges in the call graph?

yes, if you run this meta query: https://github.com/github/codeql/blob/main/python/ql/src/meta/analysis-quality/CallGraph.ql

@yoff
Copy link
Contributor Author

yoff commented May 9, 2023

yes, if you run this meta query: https://github.com/github/codeql/blob/main/python/ql/src/meta/analysis-quality/CallGraph.ql

Thanks, I ran that and a few more. It seems to have made a positive difference on the call graph :-)

@yoff yoff requested a review from RasmusWL May 9, 2023 09:02
Copy link
Member

@RasmusWL RasmusWL left a comment

Choose a reason for hiding this comment

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

From my sampling of the new call-edges, they all look good 👍

@yoff yoff merged commit 1a57f81 into github:main May 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants