Skip to content

Commit 58285c0

Browse files
authored
Merge pull request #1391 from markshannon/python-points-to-varargs
Python points to varargs
2 parents 703fbc6 + 03d296a commit 58285c0

File tree

13 files changed

+376
-21
lines changed

13 files changed

+376
-21
lines changed

python/ql/src/semmle/python/Exprs.qll

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,12 @@ class Call extends Call_ {
168168

169169
override CallNode getAFlowNode() { result = super.getAFlowNode() }
170170

171-
/** Gets a tuple (*) argument of this class definition. */
171+
/** Gets a tuple (*) argument of this call. */
172172
Expr getStarargs() {
173173
result = this.getAPositionalArg().(Starred).getValue()
174174
}
175175

176-
/** Gets a dictionary (**) argument of this class definition. */
176+
/** Gets a dictionary (**) argument of this call. */
177177
Expr getKwargs() {
178178
result = this.getANamedArg().(DictUnpacking).getValue()
179179
}
@@ -227,6 +227,18 @@ class Call extends Call_ {
227227
result = this.getKwargs().(Dict).getAKey().(StrConst).getText()
228228
}
229229

230+
/** Gets the positional argument count of this call, provided there is no more than one tuple (*) argument. */
231+
int getPositionalArgumentCount() {
232+
count(this.getStarargs()) < 2 and
233+
result = count(Expr arg | arg = this.getAPositionalArg() and not arg instanceof Starred)
234+
}
235+
236+
/** Gets the tuple (*) argument of this call, provided there is exactly one. */
237+
Expr getStarArg() {
238+
count(this.getStarargs()) < 2 and
239+
result = getStarargs()
240+
}
241+
230242
}
231243

232244
/** A conditional expression such as, `body if test else orelse` */

python/ql/src/semmle/python/Flow.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,12 @@ class CallNode extends ControlFlowNode {
481481
)
482482
}
483483

484+
/** Gets the tuple (*) argument of this call, provided there is exactly one. */
485+
ControlFlowNode getStarArg() {
486+
result.getNode() = this.getNode().getStarArg() and
487+
result.getBasicBlock().dominates(this.getBasicBlock())
488+
}
489+
484490
}
485491

486492
/** A control flow corresponding to an attribute expression, such as `value.attr` */

python/ql/src/semmle/python/objects/ObjectInternal.qll

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,10 @@ module ObjectInternal {
481481
result = TBuiltinClassObject(Builtin::special("ClassType"))
482482
}
483483

484+
ObjectInternal emptyTuple() {
485+
result.(BuiltinTupleObjectInternal).length() = 0
486+
}
487+
484488
}
485489

486490
/** Helper for boolean predicates returning both `true` and `false` */

python/ql/src/semmle/python/objects/Sequences.qll

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ abstract class TupleObjectInternal extends SequenceObjectInternal {
3939
}
4040

4141
private string contents(int n) {
42-
n = this.length() and result = ""
42+
n < 4 and n = this.length() and result = ""
43+
or
44+
n = 3 and this.length() > 3 and result = (this.length()-3).toString() + " more..."
4345
or
4446
result = this.getItem(n).toString() + ", " + this.contents(n+1)
4547
}
@@ -88,6 +90,7 @@ abstract class TupleObjectInternal extends SequenceObjectInternal {
8890

8991
}
9092

93+
/** A tuple built-in to the interpreter, including the empty tuple. */
9194
class BuiltinTupleObjectInternal extends TBuiltinTuple, TupleObjectInternal {
9295

9396
override predicate introducedAt(ControlFlowNode node, PointsToContext context) {
@@ -114,7 +117,7 @@ class BuiltinTupleObjectInternal extends TBuiltinTuple, TupleObjectInternal {
114117
}
115118
}
116119

117-
120+
/** A tuple declared by a tuple expression in the Python source code */
118121
class PythonTupleObjectInternal extends TPythonTuple, TupleObjectInternal {
119122

120123
override predicate introducedAt(ControlFlowNode node, PointsToContext context) {
@@ -145,6 +148,35 @@ class PythonTupleObjectInternal extends TPythonTuple, TupleObjectInternal {
145148

146149
}
147150

151+
/** A tuple created by a `*` parameter */
152+
class VarargsTupleObjectInternal extends TVarargsTuple, TupleObjectInternal {
153+
154+
override predicate introducedAt(ControlFlowNode node, PointsToContext context) {
155+
none()
156+
}
157+
158+
override Builtin getBuiltin() {
159+
none()
160+
}
161+
162+
override ControlFlowNode getOrigin() {
163+
none()
164+
}
165+
166+
override ObjectInternal getItem(int n) {
167+
exists(CallNode call, PointsToContext context, int offset, int length |
168+
this = TVarargsTuple(call, context, offset, length) and
169+
n < length and
170+
InterProceduralPointsTo::positional_argument_points_to(call, offset+n, context, result, _)
171+
)
172+
}
173+
174+
override int length() {
175+
this = TVarargsTuple(_, _, _, result)
176+
}
177+
}
178+
179+
148180
/** The `sys.version_info` object. We treat this specially to prevent premature pruning and
149181
* false positives when we are unsure of the actual version of Python that the code is expecting.
150182
*/

python/ql/src/semmle/python/objects/TObject.qll

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ cached newtype TObject =
179179
context.appliesTo(origin)
180180
}
181181
or
182+
/* Varargs tuple */
183+
TVarargsTuple(CallNode call, PointsToContext context, int offset, int length) {
184+
InterProceduralPointsTo::varargs_tuple(call, context, _, _, offset, length)
185+
}
186+
or
182187
/* `type` */
183188
TType()
184189
or

python/ql/src/semmle/python/pointsto/PointsTo.qll

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -844,11 +844,13 @@ module InterProceduralPointsTo {
844844
private predicate normal_parameter_points_to(ParameterDefinition def, PointsToContext context, ObjectInternal value, ControlFlowNode origin) {
845845
exists(PointsToContext caller, ControlFlowNode arg |
846846
PointsToInternal::pointsTo(arg, caller, value, origin) and
847-
callsite_argument_transfer(arg, caller, def, context)
847+
named_argument_transfer(arg, caller, def, context)
848848
)
849849
or
850850
not def.isSelf() and not def.isVarargs() and not def.isKwargs() and
851851
context.isRuntime() and value = ObjectInternal::unknown() and origin = def.getDefiningNode()
852+
or
853+
positional_parameter_points_to(def, context, value, origin)
852854
}
853855

854856
pragma [noinline]
@@ -898,44 +900,107 @@ module InterProceduralPointsTo {
898900
/** Helper for parameter_points_to */
899901
pragma [noinline]
900902
private predicate special_parameter_points_to(ParameterDefinition def, PointsToContext context, ObjectInternal value, ControlFlowNode origin) {
903+
/* Runtime: Just an unknown tuple (or dict for `**` args) */
901904
special_parameter_value(def, value) and
902-
(
903-
context.isRuntime()
904-
or
905-
exists(PointsToContext caller, CallNode call |
906-
context.fromCall(call, caller) and
907-
context.appliesToScope(def.getScope()) and
908-
not exists(call.getArg(def.getParameter().getPosition())) and
909-
not exists(call.getArgByName(def.getParameter().getName()))
910-
)
905+
context.isRuntime() and
906+
origin = def.getDefiningNode()
907+
or
908+
/* A tuple constructed from positional arguments for a `*` parameter. */
909+
def.isVarargs() and
910+
exists(CallNode call, Function scope, PointsToContext caller, int offset, int length |
911+
varargs_tuple(call, caller, scope, context, offset, length) and
912+
value = TVarargsTuple(call, caller, offset, length) and
913+
def.getScope() = scope
914+
) and
915+
origin = def.getDefiningNode()
916+
or
917+
/* A `*` parameter with no surplus positional arguments; an empty tuple */
918+
def.isVarargs() and
919+
exists(Function scope |
920+
varargs_empty_tuple(scope, context) and
921+
value = ObjectInternal::emptyTuple() and
922+
def.getScope() = scope
911923
) and
912924
origin = def.getDefiningNode()
913925
}
914926

927+
/** Holds if `call` in context `caller` calls into the function scope `scope` in context `callee` and
928+
* that the number of position arguments (including expansion of `*` argument) exceeds the number of positional arguments by
929+
* `length` and that the excess arguments start at `start`.
930+
*/
931+
predicate varargs_tuple(CallNode call, PointsToContext caller, Function scope, PointsToContext callee, int start, int length) {
932+
exists(int parameter_offset |
933+
callsite_calls_function(call, caller, scope, callee, parameter_offset) and
934+
start = scope.getPositionalParameterCount() - parameter_offset and
935+
length = positional_argument_count(call, caller) - start and
936+
length > 0
937+
)
938+
}
939+
940+
/** Holds if for function scope `func` in context `callee` the `*` parameter will hold the empty tuple. */
941+
predicate varargs_empty_tuple(Function func, PointsToContext callee) {
942+
exists(CallNode call, PointsToContext caller, int parameter_offset |
943+
callsite_calls_function(call, caller, func, callee, parameter_offset) and
944+
func.getPositionalParameterCount() - parameter_offset >= positional_argument_count(call, caller)
945+
)
946+
}
947+
915948
/** Helper predicate for special_parameter_points_to */
916949
private predicate special_parameter_value(ParameterDefinition p, ObjectInternal value) {
917950
p.isVarargs() and value = TUnknownInstance(ObjectInternal::builtin("tuple"))
918951
or
919952
p.isKwargs() and value = TUnknownInstance(ObjectInternal::builtin("dict"))
920953
}
921954

922-
/** Holds if the `(argument, caller)` pair matches up with `(param, callee)` pair across call. */
923-
cached predicate callsite_argument_transfer(ControlFlowNode argument, PointsToContext caller, ParameterDefinition param, PointsToContext callee) {
955+
/** Holds if the `n`th argument in call `call` with context `caller` points-to `value` from `origin`, including values in tuples
956+
* expanded by a `*` argument. For example, for the call `f('a', *(`x`,`y`))` the arguments are `('a', 'x', y')`
957+
*/
958+
predicate positional_argument_points_to(CallNode call, int n, PointsToContext caller, ObjectInternal value, ControlFlowNode origin) {
959+
PointsToInternal::pointsTo(call.getArg(n), caller, value, origin)
960+
or
961+
exists(SequenceObjectInternal arg, int pos |
962+
pos = call.getNode().getPositionalArgumentCount() and
963+
PointsToInternal::pointsTo(origin, caller, arg, _) and
964+
value = arg.getItem(n-pos) and
965+
origin = call.getStarArg()
966+
)
967+
}
968+
969+
/** Gets the number of positional arguments including values in tuples expanded by a `*` argument.*/
970+
private int positional_argument_count(CallNode call, PointsToContext caller) {
971+
result = call.getNode().getPositionalArgumentCount() and not exists(call.getStarArg()) and caller.appliesTo(call)
972+
or
973+
exists(SequenceObjectInternal arg, int pos |
974+
pos = call.getNode().getPositionalArgumentCount() and
975+
PointsToInternal::pointsTo(call.getStarArg(), caller, arg, _) and
976+
result = pos + arg.length()
977+
)
978+
}
979+
980+
/** Holds if the parameter definition `def` points-to `value` from `origin` given the context `context` */
981+
predicate positional_parameter_points_to(ParameterDefinition def, PointsToContext context, ObjectInternal value, ControlFlowNode origin) {
982+
exists(CallNode call, int argument, PointsToContext caller, Function func, int offset |
983+
positional_argument_points_to(call, argument, caller, value, origin) and
984+
callsite_calls_function(call, caller, func, context, offset) and
985+
def.getParameter() = func.getArg(argument+offset)
986+
)
987+
}
988+
989+
/** Holds if the named `argument` given the context `caller` is transferred to the parameter `param` with conntext `callee` by a call. */
990+
cached predicate named_argument_transfer(ControlFlowNode argument, PointsToContext caller, ParameterDefinition param, PointsToContext callee) {
924991
exists(CallNode call, Function func, int offset |
925992
callsite_calls_function(call, caller, func, callee, offset)
926993
|
927-
exists(int n |
928-
argument = call.getArg(n) and
929-
param.getParameter() = func.getArg(n+offset)
930-
)
931-
or
932994
exists(string name |
933995
argument = call.getArgByName(name) and
934996
param.getParameter() = func.getArgByName(name)
935997
)
936998
)
937999
}
9381000

1001+
/** Holds if the `call` with context `caller` calls the function `scope` in context `callee`
1002+
* and the offset from argument to parameter is `parameter_offset`
1003+
*/
9391004
cached predicate callsite_calls_function(CallNode call, PointsToContext caller, Function scope, PointsToContext callee, int parameter_offset) {
9401005
exists(ObjectInternal func |
9411006
callWithContext(call, caller, func, callee) and

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@
1616
| l_calls.py:10 | ControlFlowNode for bar() | bar |
1717
| l_calls.py:24 | ControlFlowNode for Attribute() | Owner.cm |
1818
| l_calls.py:25 | ControlFlowNode for Attribute() | Owner.cm2 |
19+
| l_calls.py:37 | ControlFlowNode for f() | f |
20+
| l_calls.py:38 | ControlFlowNode for Attribute() | E.m |
21+
| l_calls.py:39 | ControlFlowNode for Attribute() | E.m |
22+
| l_calls.py:42 | ControlFlowNode for f() | f |
23+
| l_calls.py:51 | ControlFlowNode for g() | g |
24+
| l_calls.py:52 | ControlFlowNode for Attribute() | F.m |
25+
| l_calls.py:53 | ControlFlowNode for Attribute() | F.m |
1926
| q_super.py:4 | ControlFlowNode for Attribute() | object.__init__ |
2027
| q_super.py:12 | ControlFlowNode for Attribute() | Base2.__init__ |
2128
| q_super.py:22 | ControlFlowNode for Attribute() | Base1.meth |

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,12 +112,19 @@
112112
| k_getsetattr.py:0 | Module code.k_getsetattr | k | Function k |
113113
| k_getsetattr.py:4 | Class C | meth1 | Function meth1 |
114114
| k_getsetattr.py:4 | Class C | meth2 | Function meth2 |
115+
| l_calls.py:0 | Module code.l_calls | E | class E |
116+
| l_calls.py:0 | Module code.l_calls | F | class F |
115117
| l_calls.py:0 | Module code.l_calls | Owner | class Owner |
116118
| l_calls.py:0 | Module code.l_calls | bar | Function bar |
119+
| l_calls.py:0 | Module code.l_calls | f | Function f |
117120
| l_calls.py:0 | Module code.l_calls | foo | Function foo |
121+
| l_calls.py:0 | Module code.l_calls | g | Function g |
122+
| l_calls.py:0 | Module code.l_calls | t | Tuple |
118123
| l_calls.py:12 | Class Owner | cm | classmethod() |
119124
| l_calls.py:12 | Class Owner | cm2 | classmethod() |
120125
| l_calls.py:12 | Class Owner | m | Function m |
126+
| l_calls.py:32 | Class E | m | Function m |
127+
| l_calls.py:47 | Class F | m | Function m |
121128
| o_no_returns.py:0 | Module code.o_no_returns | bar | Function bar |
122129
| o_no_returns.py:0 | Module code.o_no_returns | fail | Function fail |
123130
| o_no_returns.py:0 | Module code.o_no_returns | foo | Function foo |

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
| k_getsetattr.py:15 | ControlFlowNode for Attribute() | 6 |
7676
| l_calls.py:4 | ControlFlowNode for Attribute() | 4 |
7777
| l_calls.py:9 | ControlFlowNode for foo() | 4 |
78+
| l_calls.py:48 | ControlFlowNode for None | 48 |
7879
| m_attributes.py:12 | ControlFlowNode for Attribute() | 8 |
7980
| m_attributes.py:13 | ControlFlowNode for Attribute() | 8 |
8081
| o_no_returns.py:7 | ControlFlowNode for fail() | 10 |

0 commit comments

Comments
 (0)