Skip to content

Commit 2e9d548

Browse files
authored
Merge pull request #14706 from RasmusWL/class-attribute-flow
Python: Add basic flow for class attributes
2 parents 14e5162 + 5fc8a00 commit 2e9d548

File tree

6 files changed

+146
-11
lines changed

6 files changed

+146
-11
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added basic flow for attributes defined on classes, when the attribute lookup is on a direct reference to that class (so not instance, cls parameter, or self parameter). Example: class definition `class Foo: my_tuples = (dangerous, safe)` and usage `SINK(Foo.my_tuples[0])`.

python/ql/lib/semmle/python/dataflow/new/internal/DataFlowPrivate.qll

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -846,10 +846,27 @@ predicate comprehensionStoreStep(CfgNode nodeFrom, Content c, CfgNode nodeTo) {
846846
* ```
847847
* data flows from `x` to the attribute `foo` of (the post-update node for) `obj`.
848848
*/
849-
predicate attributeStoreStep(Node nodeFrom, AttributeContent c, PostUpdateNode nodeTo) {
850-
exists(AttrWrite write |
851-
write.accesses(nodeTo.getPreUpdateNode(), c.getAttribute()) and
852-
nodeFrom = write.getValue()
849+
predicate attributeStoreStep(Node nodeFrom, AttributeContent c, Node nodeTo) {
850+
exists(Node object |
851+
// Normally we target a PostUpdateNode. However, for class definitions the class
852+
// is only constructed after evaluating its' entire scope, so in terms of python
853+
// evaluations there is no post or pre update nodes, just one node for the class
854+
// expression. Therefore we target the class expression directly.
855+
//
856+
// Note: Due to the way we handle decorators, using a class decorator will result in
857+
// there being a post-update node for the class (argument to the decorator). We do
858+
// not want to differentiate between these two cases, so still target the class
859+
// expression directly.
860+
object = nodeTo.(PostUpdateNode).getPreUpdateNode() and
861+
not object.asExpr() instanceof ClassExpr
862+
or
863+
object = nodeTo and
864+
object.asExpr() instanceof ClassExpr
865+
|
866+
exists(AttrWrite write |
867+
write.accesses(object, c.getAttribute()) and
868+
nodeFrom = write.getValue()
869+
)
853870
)
854871
}
855872

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,9 @@ class C:
338338

339339
@expects(2)
340340
def test_attribute_reference():
341-
SINK(C.a) #$ MISSING:flow="SOURCE, l:-4 -> C.a"
341+
SINK(C.a) # $ flow="SOURCE, l:-5 -> C.a"
342342
c = C()
343-
SINK(c.a) #$ MISSING:flow="SOURCE, l:-6 -> c.a"
343+
SINK(c.a) # $ MISSING: flow="SOURCE, l:-7 -> c.a"
344344

345345
# overriding __getattr__ should be tested by the class coverage tests
346346

python/ql/test/experimental/dataflow/fieldflow/test.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,124 @@ def set_attr(obj):
237237
SINK(y.attr) # $ MISSING: flow
238238
SINK_F(z.attr) # $ MISSING: flow
239239

240+
# ------------------------------------------------------------------------------
241+
# Content in class attribute
242+
# ------------------------------------------------------------------------------
243+
244+
class WithTuple:
245+
my_tuple = (SOURCE, NONSOURCE)
246+
247+
def test_inst(self):
248+
SINK(self.my_tuple[0]) # $ MISSING: flow
249+
SINK_F(self.my_tuple[1])
250+
251+
def test_inst_no_call(self):
252+
SINK(self.my_tuple[0]) # $ MISSING: flow
253+
SINK_F(self.my_tuple[1])
254+
255+
@classmethod
256+
def test_cm(cls):
257+
SINK(cls.my_tuple[0]) # $ flow="SOURCE, l:-12 -> cls.my_tuple[0]"
258+
SINK_F(cls.my_tuple[1])
259+
260+
@classmethod
261+
def test_cm_no_call(cls):
262+
SINK(cls.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-8 -> cls.my_tuple[0]"
263+
SINK_F(cls.my_tuple[1])
264+
265+
266+
@expects(2*4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
267+
def test_WithTuple():
268+
SINK(WithTuple.my_tuple[0]) # $ flow="SOURCE, l:-23 -> WithTuple.my_tuple[0]"
269+
SINK_F(WithTuple.my_tuple[1])
270+
271+
WithTuple.test_cm()
272+
273+
inst = WithTuple()
274+
inst.test_inst()
275+
276+
SINK(inst.my_tuple[0]) # $ MISSING: flow
277+
SINK_F(inst.my_tuple[1])
278+
279+
280+
@expects(4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
281+
def test_inst_override():
282+
inst = WithTuple()
283+
284+
# setting attribute on instance does not override class attribute, it's only on the
285+
# instance!
286+
inst.my_tuple = (NONSOURCE, SOURCE)
287+
288+
SINK_F(inst.my_tuple[0])
289+
SINK(inst.my_tuple[1]) # $ flow="SOURCE, l:-3 -> inst.my_tuple[1]"
290+
291+
SINK(WithTuple.my_tuple[0]) # $ flow="SOURCE, l:-46 -> WithTuple.my_tuple[0]"
292+
SINK_F(WithTuple.my_tuple[1])
293+
294+
295+
class WithTuple2:
296+
my_tuple = (NONSOURCE,)
297+
298+
def set_to_source():
299+
WithTuple2.my_tuple = (SOURCE,)
300+
301+
@expects(4) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
302+
def test_global_flow_to_class_attribute():
303+
inst = WithTuple2()
304+
SINK_F(WithTuple2.my_tuple[0])
305+
SINK_F(inst.my_tuple[0])
306+
307+
set_to_source()
308+
309+
SINK(WithTuple2.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-10 -> WithTuple2.my_tuple[0]"
310+
SINK(inst.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-11 -> inst.my_tuple[0]"
311+
312+
313+
class Outer:
314+
src = SOURCE
315+
class Inner:
316+
src = SOURCE
317+
318+
@expects(2) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
319+
def test_nested_class():
320+
SINK(Outer.src) # $ flow="SOURCE, l:-6 -> Outer.src"
321+
SINK(Outer.Inner.src) # $ flow="SOURCE, l:-5 -> Outer.Inner.src"
322+
323+
# --------------------------------------
324+
# unique classes from functions
325+
# --------------------------------------
326+
def make_class():
327+
# a fresh class is returned each time this function is called
328+
class C:
329+
my_tuple = (NONSOURCE,)
330+
return C
331+
332+
@expects(8) # $ unresolved_call=expects(..) unresolved_call=expects(..)(..)
333+
def test_unique_class():
334+
# This test highlights that if we use the _ClassExpr_ itself as the target/source
335+
# for jumpsteps, we will end up with spurious flow (that is, we will think that
336+
# x_cls and y_cls are the same, so by updating .my_tuple on x_cls we might propagate
337+
# that to y_cls as well -- it might not matter too much in reality, but certainly an
338+
# interesting corner case)
339+
x_cls = make_class()
340+
y_cls = make_class()
341+
342+
assert x_cls != y_cls
343+
344+
x_inst = x_cls()
345+
y_inst = y_cls()
346+
347+
SINK_F(x_cls.my_tuple[0])
348+
SINK_F(x_inst.my_tuple[0])
349+
SINK_F(y_cls.my_tuple[0])
350+
SINK_F(y_inst.my_tuple[0])
351+
352+
x_cls.my_tuple = (SOURCE,)
353+
SINK(x_cls.my_tuple[0]) # $ flow="SOURCE, l:-1 -> x_cls.my_tuple[0]"
354+
SINK(x_inst.my_tuple[0]) # $ MISSING: flow="SOURCE, l:-2 -> x_inst.my_tuple[0]"
355+
SINK_F(y_cls.my_tuple[0])
356+
SINK_F(y_inst.my_tuple[0])
357+
240358
# ------------------------------------------------------------------------------
241359
# Crosstalk test -- using different function based on conditional
242360
# ------------------------------------------------------------------------------

python/ql/test/experimental/dataflow/match/test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class Unsafe:
6464
def test_value_pattern():
6565
match SOURCE:
6666
case Unsafe.VALUE as x:
67-
SINK(x) #$ flow="SOURCE, l:-2 -> x" MISSING: flow="SOURCE, l:-5 -> x"
67+
SINK(x) #$ flow="SOURCE, l:-2 -> x" flow="SOURCE, l:-5 -> x"
6868

6969
@expects(2)
7070
def test_sequence_pattern_tuple():

python/ql/test/library-tests/PointsTo/new/ImpliesDataflow.expected

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,5 @@
33
| code/l_calls.py:12:1:12:20 | ControlFlowNode for ClassExpr | code/l_calls.py:16:16:16:18 | ControlFlowNode for cls |
44
| code/l_calls.py:12:1:12:20 | ControlFlowNode for ClassExpr | code/l_calls.py:24:13:24:22 | ControlFlowNode for Attribute() |
55
| code/l_calls.py:12:1:12:20 | ControlFlowNode for ClassExpr | code/l_calls.py:25:16:25:16 | ControlFlowNode for a |
6-
| code/l_calls.py:33:5:33:23 | ControlFlowNode for FunctionExpr | code/l_calls.py:39:1:39:3 | ControlFlowNode for Attribute |
7-
| code/l_calls.py:48:5:48:30 | ControlFlowNode for FunctionExpr | code/l_calls.py:53:1:53:3 | ControlFlowNode for Attribute |
8-
| code/q_super.py:48:5:48:17 | ControlFlowNode for ClassExpr | code/q_super.py:51:25:51:29 | ControlFlowNode for Attribute |
9-
| code/q_super.py:63:5:63:17 | ControlFlowNode for ClassExpr | code/q_super.py:66:19:66:23 | ControlFlowNode for Attribute |
106
| code/t_type.py:3:1:3:16 | ControlFlowNode for ClassExpr | code/t_type.py:6:1:6:9 | ControlFlowNode for type() |
117
| code/t_type.py:3:1:3:16 | ControlFlowNode for ClassExpr | code/t_type.py:13:5:13:13 | ControlFlowNode for type() |

0 commit comments

Comments
 (0)