Python: Add jump steps for module attribute reads.

This is the quick-and-dirty solution, as discussed.

An even quicker-and-dirtier solution would have used
`ModuleValue::attr` and take the `getOrigin` of that as the source of
the jump step. However, this turns out to be a bad choice, since
`attr` might fail to have a value for the given attribute (for a
variety of reasons). Thus, we instead appeal to a helper predicate
that keeps track of which names are defined by which right-hand-sides
in a given module. (Observe that type tracking works correctly for `x`
in `mymodule.py`, even though `x` is never assigned a value in the
eyes of the Value API.)

This means that points-to is only used to actually figure out if the
object we're looking an attribute up on is a module or not. This is
the next thing to replace in order to eliminate the dependence on
points-to, but this will require some care to ensure that all module
lookups are handled correctly.

Only two test files needed to be changed for the tests to pass. The
first was the fixed false negative in the type tracker, and the other
was a bunch of missing flow in the regression test. I have manually
removed the `# Flow not found` annotations to make them consistent
with the output. Pay particular attention to the annotation on line
117 -- I believe it was misplaced and should have been on line 106
instead (where, indeed, we now have flow where none appeared before).
This commit is contained in:
Taus Brock-Nannestad
2020-10-19 19:13:32 +02:00
parent 60fcb5e7d5
commit 7755993dd3
4 changed files with 29 additions and 5 deletions

View File

@@ -504,6 +504,26 @@ predicate jumpStep(Node nodeFrom, Node nodeTo) {
or
// Module variable write
nodeFrom = nodeTo.(ModuleVariableNode).getAWrite()
or
// Read of module attribute:
exists(AttrRead r, ModuleValue mv |
r.getObject().asCfgNode().pointsTo(mv) and
module_export(mv.getScope(), r.getAttributeName(), nodeFrom) and
nodeTo = r
)
}
/**
* Holds if the module `m` defines a name `name` that by assigning `defn` to it. This is an
* overapproximation, as `name` may not in fact be exported (e.g. by defining an `__all__` that does
* not include `name`).
*/
private predicate module_export(Module m, string name, CfgNode defn) {
exists(EssaVariable v |
v.getName() = name and
v.getAUse() = m.getANormalExit() and
defn.getNode() = v.getDefinition().(AssignmentDefinition).getValue()
)
}
//--------

View File

@@ -1,3 +1,7 @@
| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:89:10:89:10 | ControlFlowNode for t |
| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:106:10:106:14 | ControlFlowNode for Attribute |
| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:111:10:111:12 | ControlFlowNode for Attribute |
| module.py:1:13:1:18 | ControlFlowNode for SOURCE | test.py:156:6:156:11 | ControlFlowNode for unsafe |
| module.py:6:12:6:17 | ControlFlowNode for SOURCE | test.py:101:10:101:10 | ControlFlowNode for t |
| test.py:3:10:3:15 | ControlFlowNode for SOURCE | test.py:3:10:3:15 | ControlFlowNode for SOURCE |
| test.py:6:9:6:14 | ControlFlowNode for SOURCE | test.py:7:10:7:10 | ControlFlowNode for s |

View File

@@ -86,7 +86,7 @@ import module
def test13():
t = module.dangerous
SINK(t) # Flow not found
SINK(t)
def test14():
t = module.safe
@@ -108,13 +108,13 @@ def x_sink(arg):
def test17():
t = C()
t.x = module.dangerous
SINK(t.x) # Flow not found
SINK(t.x)
def test18():
t = C()
t.x = module.dangerous
t = hub(t)
x_sink(t) # Flow not found
x_sink(t)
def test19():
t = CUSTOM_SOURCE
@@ -153,7 +153,7 @@ def test22(cond):
SINK(t)
from module import dangerous as unsafe
SINK(unsafe) # Flow not found
SINK(unsafe)
def test23():
with SOURCE as t:

View File

@@ -51,7 +51,7 @@ def global_var_write_test():
def test_import():
import mymodule
mymodule.x # $f-:tracked
mymodule.x # $tracked
y = mymodule.func() # $tracked
y # $tracked