Skip to content

Commit aa4345a

Browse files
authored
Merge pull request github#4710 from yoff/python-dataflow-variable-capture
Python: Dataflow, variable capture
2 parents 8c68463 + 215986b commit aa4345a

File tree

9 files changed

+318
-7
lines changed

9 files changed

+318
-7
lines changed

python/ql/test/experimental/dataflow/coverage/argumentPassing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def with_multiple_kw_args(a, b, c):
112112
SINK3(c)
113113

114114

115-
@expects(9)
115+
@expects(12)
116116
def test_multiple_kw_args():
117117
with_multiple_kw_args(b=arg2, c=arg3, a=arg1)
118118
with_multiple_kw_args(arg1, *(arg2,), arg3)

python/ql/test/experimental/dataflow/coverage/classesCallGraph.expected

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
| classes.py:45:16:45:35 | ControlFlowNode for Attribute() | classes.py:45:16:45:35 | ControlFlowNode for Attribute() |
22
| classes.py:60:17:60:27 | [pre objCreate] ControlFlowNode for With_init() | classes.py:54:18:54:21 | ControlFlowNode for self |
3-
| classes.py:242:9:242:24 | ControlFlowNode for set() | classes.py:242:9:242:24 | ControlFlowNode for set() |
4-
| classes.py:247:9:247:30 | ControlFlowNode for frozenset() | classes.py:247:9:247:30 | ControlFlowNode for frozenset() |
5-
| classes.py:252:9:252:28 | ControlFlowNode for dict() | classes.py:252:9:252:28 | ControlFlowNode for dict() |
63
| classes.py:565:5:565:16 | ControlFlowNode for with_getitem | classes.py:555:21:555:24 | ControlFlowNode for self |
74
| classes.py:565:18:565:21 | ControlFlowNode for arg2 | classes.py:555:27:555:29 | ControlFlowNode for key |
85
| classes.py:581:5:581:16 | ControlFlowNode for with_setitem | classes.py:570:21:570:24 | ControlFlowNode for self |

python/ql/test/experimental/dataflow/coverage/validTest.py renamed to python/ql/test/experimental/dataflow/validTest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ def check_tests_valid(testFile):
5050

5151

5252
if __name__ == "__main__":
53-
check_tests_valid("classes")
54-
check_tests_valid("test")
55-
check_tests_valid("argumentPassing")
53+
check_tests_valid("coverage.classes")
54+
check_tests_valid("coverage.test")
55+
check_tests_valid("coverage.argumentPassing")
56+
check_tests_valid("variable-capture.in")
57+
check_tests_valid("variable-capture.nonlocal")
58+
check_tests_valid("variable-capture.dict")

python/ql/test/experimental/dataflow/variable-capture/CaptureTest.expected

Whitespace-only changes.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import TestUtilities.InlineExpectationsTest
4+
import experimental.dataflow.testConfig
5+
6+
class CaptureTest extends InlineExpectationsTest {
7+
CaptureTest() { this = "CaptureTest" }
8+
9+
override string getARelevantTag() { result = "captured" }
10+
11+
override predicate hasActualResult(Location ___location, string element, string tag, string value) {
12+
exists(DataFlow::Node source, DataFlow::Node sink |
13+
exists(TestConfiguration cfg | cfg.hasFlow(source, sink))
14+
|
15+
___location = sink.getLocation() and
16+
tag = "captured" and
17+
value = "" and
18+
element = sink.toString()
19+
)
20+
}
21+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Here we test writing to a captured variable via a dictionary (see `out`).
2+
# We also test reading one captured variable and writing the value to another (see `through`).
3+
4+
# All functions starting with "test_" should run and execute `print("OK")` exactly once.
5+
# This can be checked by running validTest.py.
6+
7+
import sys
8+
import os
9+
10+
sys.path.append(os.path.dirname(os.path.dirname((__file__))))
11+
from testlib import *
12+
13+
# These are defined so that we can evaluate the test code.
14+
NONSOURCE = "not a source"
15+
SOURCE = "source"
16+
17+
def is_source(x):
18+
return x == "source" or x == b"source" or x == 42 or x == 42.0 or x == 42j
19+
20+
21+
def SINK(x):
22+
if is_source(x):
23+
print("OK")
24+
else:
25+
print("Unexpected flow", x)
26+
27+
28+
def SINK_F(x):
29+
if is_source(x):
30+
print("Unexpected flow", x)
31+
else:
32+
print("OK")
33+
34+
35+
def out():
36+
sinkO1 = { "x": "" }
37+
def captureOut1():
38+
sinkO1["x"] = SOURCE
39+
captureOut1()
40+
SINK(sinkO1["x"]) #$ MISSING:captured
41+
42+
sinkO2 = { "x": "" }
43+
def captureOut2():
44+
def m():
45+
sinkO2["x"] = SOURCE
46+
m()
47+
captureOut2()
48+
SINK(sinkO2["x"]) #$ MISSING:captured
49+
50+
nonSink0 = { "x": "" }
51+
def captureOut1NotCalled():
52+
nonSink0["x"] = SOURCE
53+
SINK_F(nonSink0["x"])
54+
55+
def captureOut2NotCalled():
56+
def m():
57+
nonSink0["x"] = SOURCE
58+
captureOut2NotCalled()
59+
SINK_F(nonSink0["x"])
60+
61+
@expects(4)
62+
def test_out():
63+
out()
64+
65+
def through(tainted):
66+
sinkO1 = { "x": "" }
67+
def captureOut1():
68+
sinkO1["x"] = tainted
69+
captureOut1()
70+
SINK(sinkO1["x"]) #$ MISSING:captured
71+
72+
sinkO2 = { "x": "" }
73+
def captureOut2():
74+
def m():
75+
sinkO2["x"] = tainted
76+
m()
77+
captureOut2()
78+
SINK(sinkO2["x"]) #$ MISSING:captured
79+
80+
nonSink0 = { "x": "" }
81+
def captureOut1NotCalled():
82+
nonSink0["x"] = tainted
83+
SINK_F(nonSink0["x"])
84+
85+
def captureOut2NotCalled():
86+
def m():
87+
nonSink0["x"] = tainted
88+
captureOut2NotCalled()
89+
SINK_F(nonSink0["x"])
90+
91+
@expects(4)
92+
def test_through():
93+
through(SOURCE)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Here we test the case where a captured variable is being read.
2+
3+
# All functions starting with "test_" should run and execute `print("OK")` exactly once.
4+
# This can be checked by running validTest.py.
5+
6+
import sys
7+
import os
8+
9+
sys.path.append(os.path.dirname(os.path.dirname((__file__))))
10+
from testlib import *
11+
12+
# These are defined so that we can evaluate the test code.
13+
NONSOURCE = "not a source"
14+
SOURCE = "source"
15+
16+
def is_source(x):
17+
return x == "source" or x == b"source" or x == 42 or x == 42.0 or x == 42j
18+
19+
20+
def SINK(x):
21+
if is_source(x):
22+
print("OK")
23+
else:
24+
print("Unexpected flow", x)
25+
26+
27+
def SINK_F(x):
28+
if is_source(x):
29+
print("Unexpected flow", x)
30+
else:
31+
print("OK")
32+
33+
# Capture the parameter of an outer function.
34+
def inParam(tainted):
35+
def captureIn1():
36+
sinkI1 = tainted
37+
SINK(sinkI1) #$ MISSING:captured
38+
captureIn1()
39+
40+
def captureIn2():
41+
def m():
42+
sinkI2 = tainted
43+
SINK(sinkI2) #$ MISSING:captured
44+
m()
45+
captureIn2()
46+
47+
captureIn3 = lambda arg: SINK(tainted)
48+
captureIn3("")
49+
50+
def captureIn1NotCalled():
51+
nonSink0 = tainted
52+
SINK_F(nonSink0)
53+
54+
def captureIn2NotCalled():
55+
def m():
56+
nonSink0 = tainted
57+
SINK_F(nonSink0)
58+
captureIn2NotCalled()
59+
60+
@expects(3)
61+
def test_inParam():
62+
inParam(SOURCE)
63+
64+
# Capture the local variable of an outer function.
65+
def inLocal():
66+
tainted = SOURCE
67+
68+
def captureIn1():
69+
sinkI1 = tainted
70+
SINK(sinkI1) #$ MISSING:captured
71+
captureIn1()
72+
73+
def captureIn2():
74+
def m():
75+
sinkI2 = tainted
76+
SINK(sinkI2) #$ MISSING:captured
77+
m()
78+
captureIn2()
79+
80+
captureIn3 = lambda arg: SINK(tainted)
81+
captureIn3("")
82+
83+
def captureIn1NotCalled():
84+
nonSink0 = tainted
85+
SINK_F(nonSink0)
86+
87+
def captureIn2NotCalled():
88+
def m():
89+
nonSink0 = tainted
90+
SINK_F(nonSink0)
91+
captureIn2NotCalled()
92+
93+
@expects(3)
94+
def test_inLocal():
95+
inLocal()
96+
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Here we test writing to a captured variable via the `nonlocal` keyword (see `out`).
2+
# We also test reading one captured variable and writing the value to another (see `through`).
3+
4+
# All functions starting with "test_" should run and execute `print("OK")` exactly once.
5+
# This can be checked by running validTest.py.
6+
7+
import sys
8+
import os
9+
10+
sys.path.append(os.path.dirname(os.path.dirname((__file__))))
11+
from testlib import *
12+
13+
# These are defined so that we can evaluate the test code.
14+
NONSOURCE = "not a source"
15+
SOURCE = "source"
16+
17+
def is_source(x):
18+
return x == "source" or x == b"source" or x == 42 or x == 42.0 or x == 42j
19+
20+
21+
def SINK(x):
22+
if is_source(x):
23+
print("OK")
24+
else:
25+
print("Unexpected flow", x)
26+
27+
28+
def SINK_F(x):
29+
if is_source(x):
30+
print("Unexpected flow", x)
31+
else:
32+
print("OK")
33+
34+
35+
def out():
36+
sinkO1 = ""
37+
def captureOut1():
38+
nonlocal sinkO1
39+
sinkO1 = SOURCE
40+
captureOut1()
41+
SINK(sinkO1) #$ MISSING:captured
42+
43+
sinkO2 = ""
44+
def captureOut2():
45+
def m():
46+
nonlocal sinkO2
47+
sinkO2 = SOURCE
48+
m()
49+
captureOut2()
50+
SINK(sinkO2) #$ MISSING:captured
51+
52+
nonSink0 = ""
53+
def captureOut1NotCalled():
54+
nonlocal nonSink0
55+
nonSink0 = SOURCE
56+
SINK_F(nonSink0)
57+
58+
def captureOut2NotCalled():
59+
def m():
60+
nonlocal nonSink0
61+
nonSink0 = SOURCE
62+
captureOut2NotCalled()
63+
SINK_F(nonSink0)
64+
65+
@expects(4)
66+
def test_out():
67+
out()
68+
69+
def through(tainted):
70+
sinkO1 = ""
71+
def captureOut1():
72+
nonlocal sinkO1
73+
sinkO1 = tainted
74+
captureOut1()
75+
SINK(sinkO1) #$ MISSING:captured
76+
77+
sinkO2 = ""
78+
def captureOut2():
79+
def m():
80+
nonlocal sinkO2
81+
sinkO2 = tainted
82+
m()
83+
captureOut2()
84+
SINK(sinkO2) #$ MISSING:captured
85+
86+
nonSink0 = ""
87+
def captureOut1NotCalled():
88+
nonlocal nonSink0
89+
nonSink0 = tainted
90+
SINK_F(nonSink0)
91+
92+
def captureOut2NotCalled():
93+
def m():
94+
nonlocal nonSink0
95+
nonSink0 = tainted
96+
captureOut2NotCalled()
97+
SINK_F(nonSink0)
98+
99+
@expects(4)
100+
def test_through():
101+
through(SOURCE)

0 commit comments

Comments
 (0)