Skip to content

Python: Add ModuleVariableNode to keep track of global reads and writes#4265

Merged
yoff merged 19 commits into
github:mainfrom
tausbn:python-add-global-flow-steps
Sep 21, 2020
Merged

Python: Add ModuleVariableNode to keep track of global reads and writes#4265
yoff merged 19 commits into
github:mainfrom
tausbn:python-add-global-flow-steps

Conversation

@tausbn
Copy link
Copy Markdown
Contributor

@tausbn tausbn commented Sep 14, 2020

Still partly a WIP. A few remaining issues need to be addressed:

  • Picking a suitable location and enclosing callable for these nodes. Since the location should be unique, and global variables may be defined in multiple places, I feel like the only reasonable choice here is to pick the module itself as the location (but I welcome any other suggestions). PR now uses the module location as the location of the node. This does present a small problem with regard to inline test expectations (where do you put a module-level comment?) but for now I'm just excluding these nodes from the test output.
  • Currently, the changes in this PR affect the behaviour of local flow at the top level (e.g. between global SSA variables in the same basic block). I'm not sure what's causing this, but it seems to be removing flow that we would actually want to have. Local flow at the top level now seems to work as intended.
  • Flow across module boundaries is not currently handled (and will probably not be handled in this PR).

The missing `global g` annotation meant `g = x` was interpreted as a
local assignment.
Some of the places where flow has disappeared look a bit suspect, so I
don't consider this to be the final word on these tests.
@tausbn tausbn requested a review from a team as a code owner September 14, 2020 16:23
Copy link
Copy Markdown
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.

Overall looks really nice 👍 I think we need to restore the global flow, but I also think I figured out the problem (see inline comment below).

Picking a suitable location and enclosing callable for these nodes. Since the location should be unique, and global variables may be defined in multiple places, I feel like the only reasonable choice here is to pick the module itself as the location (but I welcome any other suggestions).

I agree with using the module for the location 👍

Comment thread python/ql/src/experimental/dataflow/internal/DataFlowPublic.qll Outdated
Comment on lines 397 to 404
predicate jumpStep(Node nodeFrom, Node nodeTo) {
// As we have ESSA variables for global variables,
// we include ESSA flow steps involving global variables.
(
nodeFrom.(EssaNode).getVar() instanceof GlobalSsaVariable
or
nodeTo.(EssaNode).getVar() instanceof GlobalSsaVariable
) and
EssaFlow::essaFlowStep(nodeFrom, nodeTo) and
(
EssaFlow::essaFlowStep(nodeFrom, nodeTo)
nodeFrom instanceof ModuleVariableNode
or
// As jump steps do not respect chronology,
// we add jump steps for each def-use pair.
nodeFrom.asVar() instanceof GlobalSsaVariable and
nodeTo.asCfgNode() = nodeFrom.asVar().getASourceUse()
nodeTo instanceof ModuleVariableNode
)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The fact that we only allow jumpStep with a global variable that escapes (ModuleVariableNode) seems to be a problem, and what is causing us to loose flow.

python/ql/test/experimental/dataflow/basic/test.py contains:

def obfuscated_id(x):
  y = x
  z = y
  return z

a = 42
b = obfuscated_id(a)

And used to have flow from a -> x, a -> y, a -> z, a -> b. But since a doesn't escape, we won't include it anymore.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with your assessment, but I don't think allowing non-escaping variables is the solution. Because jump steps ignore chronology, something like the following would have flow from source to sink if foo has a ModuleVariableNode:

foo = NON_SOURCE
SINK(foo)
foo = SOURCE

Now, I was hoping that the necessary flow in the above would fall out from just the regular SSA flow, but it seems it doesn't.

Note that if foo escapes, then we do get the bad flow above anyway, and this is in some sense a bit spurious again (since the second line will never execute a second time with foo containing SOURCE). One solution would be to not consider all top-level reads and writes to global variables, but only the ones that ultimately define the variable in question.

Here's another problematic test example:

foo = SOURCE
foo = NON_SOURCE
SINK(foo)

Currently we would have flow from source to sink if foo is considered to be escaping.

Either way, I think we need to have "local" Global SSA flow for modelling assignments during module initialisation, and flow to and from module variable nodes to model steps via reads and writes of globals outside of initialisation.

I've named this `DataFlowModuleScope` since it's not really a
callable (and all of the relevant methods are empty anyway).
@tausbn
Copy link
Copy Markdown
Contributor Author

tausbn commented Sep 15, 2020

Hooking up the getEnclosingCallable machinery made most of the missing global flow reappear. 🎉
This is now looking closer to being mergeable, but I still would like to add test cases for (and possibly prevent) some of the kinds of bad global flow I outlined above.

@RasmusWL
Copy link
Copy Markdown
Member

I'm confused why the fix to getEnclosingCallable broguth back flow for python/ql/test/experimental/dataflow/basic/test.py 😕 at least my explanation of only considering jumpSteps for global variables that escapes doesn't seem to hold true any longer 😞 😕

@tausbn
Copy link
Copy Markdown
Contributor Author

tausbn commented Sep 15, 2020

Yeah, I'm not entirely sure.
I think for that test file, the only missing flow was from the definition of a to its use. The rest is handled locally in obfuscated_id and through the parameter passing and return steps, none of which should have been affected by this PR. I'll debug it for a bit to see if I can figure out what's going on with the flow.

@tausbn tausbn requested review from RasmusWL and yoff September 16, 2020 12:47
Copy link
Copy Markdown
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.

Nice 👍 a few minor things, and we're good to go


Unrelated to this PR: @yoff do you know why all the results in python/ql/test/experimental/dataflow/basic/globalStep.expected are duplicated? 😕

Comment thread python/ql/test/experimental/dataflow/global-flow/test.py Outdated
Comment thread python/ql/test/experimental/dataflow/global-flow/test.py Outdated
Comment thread python/ql/test/experimental/dataflow/tainttracking/basic/test.py Outdated
tausbn and others added 2 commits September 16, 2020 18:21
Co-authored-by: Rasmus Wriedt Larsen <rasmuswriedtlarsen@gmail.com>
@tausbn tausbn requested a review from RasmusWL September 16, 2020 17:05
Copy link
Copy Markdown
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.

@tausbn seems like you forgot to add the new tests to a commit?

The test cases are removed in ee76d9b#diff-7d9bc8520abc0d5daba111c8d9d5b922L9-L26, but I can't see them anywhere else 😅

@tausbn
Copy link
Copy Markdown
Contributor Author

tausbn commented Sep 17, 2020

This is what happens when I go fast and break things. Sometimes I get too used to just looking at the "unstaged changes", and I forget about "untracked files" entirely.

Comment thread python/ql/test/experimental/dataflow/tainttracking/unwanted-global-flow/test.py Outdated
RasmusWL
RasmusWL previously approved these changes Sep 17, 2020
Copy link
Copy Markdown
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.

Nice 👍

I did not look deeply into the .expected changes of the (non taint realted) dataflow tests, I simply didn't know what to look for. -- hopefully @yoff can sign off on those 😊

I'm using the QL code from this PR locally right now, so I'm already unblocked (and getting this PR merged RIGHT NOW, wouldn't change that).

@yoff
Copy link
Copy Markdown
Contributor

yoff commented Sep 17, 2020

Unrelated to this PR: @yoff do you know why all the results in python/ql/test/experimental/dataflow/basic/globalStep.expected are duplicated? 😕

This is possibly because they happen in different contexts. Note that globalStep.ql references PathNodes which are context sensitive.

Copy link
Copy Markdown
Contributor

@yoff yoff left a comment

Choose a reason for hiding this comment

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

I am a bit confused about the strategy here. The PR leaves GlobalSsaVariables in the dataflow graph and reenables flow between them during local flow nut removes them from jump steps (I think this accounts for many of the test output changes). It then adds dataflow nodes for module variables and add jump steps for those. Could you clarify, which mechanisms should be in play for modelling which flow? 🙏

I love the clean output in the consistency check, but I actually do not understand how it is achieved 🤨

* excludes SSA flow through instance fields.
*/
predicate simpleLocalFlowStep(Node nodeFrom, Node nodeTo) {
not nodeFrom instanceof ModuleVariableNode and
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are these moved out of the quantifier? A post-update node cannot be a ModuleVariableNode.
(I concede that you could move out nodeTo.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, now we allow GlobalSsaVariables is that intended?

Removes steps from `ModuleVariableNode`s from `essaFlowStep`, and
instead puts them only in `jumpStep`. This cleans up the logic a bit.

This slightly broke the type tracker implementation (as it relied on
`essaFlowStep` being fairly liberal), so I have rewritten it to
explicitly rely on just familiar predicates for local and jump steps.

Additionally, we disallow Essa-to-Essa steps where exactly one of the
two nodes corresponds to a global variable (i.e. only local-local and
global-global steps).
Copy link
Copy Markdown
Contributor

@yoff yoff left a comment

Choose a reason for hiding this comment

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

This looks good for now. Might I just ask that you write down in a comment somewhere:
Global SSA variables and ModuleVariables both refer to variables in module scope, but flow to and from

  • Global SSA variables correspond to "module-local flow" that happens at module initialisation time.
  • ModuleVariables correspond to jumpSteps that happen at run-time.
    thus only ModuleVariables are allowed to mix with local variables.

I guess this should not be restricted to just the variables but extended to their def-use chains? Do we do this correctly?

@tausbn
Copy link
Copy Markdown
Contributor Author

tausbn commented Sep 21, 2020

I guess this should not be restricted to just the variables but extended to their def-use chains? Do we do this correctly?

I'm not quite sure what "this" is referring to in the first sentence above. 😕

Copy link
Copy Markdown
Contributor

@yoff yoff left a comment

Choose a reason for hiding this comment

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

Good documentation 👍

@yoff yoff merged commit 557db33 into github:main Sep 21, 2020
@tausbn tausbn deleted the python-add-global-flow-steps branch February 12, 2021 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants