Python: Add ModuleVariableNode to keep track of global reads and writes#4265
Conversation
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.
RasmusWL
left a comment
There was a problem hiding this comment.
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 👍
| 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 | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = SOURCENow, 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).
|
Hooking up the |
|
I'm confused why the fix to |
|
Yeah, I'm not entirely sure. |
Co-authored-by: Rasmus Wriedt Larsen <rasmuswriedtlarsen@gmail.com>
RasmusWL
left a comment
There was a problem hiding this comment.
@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 😅
|
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. |
RasmusWL
left a comment
There was a problem hiding this comment.
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).
This is possibly because they happen in different contexts. Note that |
yoff
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Why are these moved out of the quantifier? A post-update node cannot be a ModuleVariableNode.
(I concede that you could move out nodeTo.)
There was a problem hiding this comment.
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).
yoff
left a comment
There was a problem hiding this comment.
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 tojumpSteps that happen at run-time.
thus onlyModuleVariables 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?
I'm not quite sure what "this" is referring to in the first sentence above. 😕 |
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.