Skip to content

Commit 4998a48

Browse files
committed
Python: Fix simple guards
1 parent 913990b commit 4998a48

File tree

3 files changed

+78
-31
lines changed

3 files changed

+78
-31
lines changed

python/ql/src/semmle/python/functions/ModificationOfParameterWithDefault.qll

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,28 @@ module ModificationOfParameterWithDefault {
1919
* A data-flow configuration for detecting modifications of a parameters default value.
2020
*/
2121
class Configuration extends DataFlow::Configuration {
22-
boolean nonEmpty;
22+
/** Record whether the default value being tracked is non-empty. */
23+
boolean nonEmptyDefault;
2324

2425
Configuration() {
25-
nonEmpty in [true, false] and
26-
this = "ModificationOfParameterWithDefault:" + nonEmpty.toString()
26+
nonEmptyDefault in [true, false] and
27+
this = "ModificationOfParameterWithDefault:" + nonEmptyDefault.toString()
2728
}
2829

29-
override predicate isSource(DataFlow::Node source) { source.(Source).isNonEmpty() = nonEmpty }
30+
override predicate isSource(DataFlow::Node source) {
31+
source.(Source).isNonEmpty() = nonEmptyDefault
32+
}
3033

3134
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
3235

3336
override predicate isBarrier(DataFlow::Node node) { node instanceof Barrier }
3437

3538
override predicate isBarrierGuard(DataFlow::BarrierGuard guard) {
36-
guard.(BarrierGuard).blocksNonEmpty() = nonEmpty
39+
// if we are tracking a emmpty default, then we should not modify falsy values
40+
nonEmptyDefault = false and guard instanceof BlocksFalsey
41+
or
42+
// if we are tracking a non-empty default, then we should not modify truthy values
43+
nonEmptyDefault = true and guard instanceof BlocksTruthy
3744
}
3845
}
3946
}

python/ql/src/semmle/python/functions/ModificationOfParameterWithDefaultCustomizations.qll

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,24 @@ module ModificationOfParameterWithDefault {
3232
abstract class Barrier extends DataFlow::Node { }
3333

3434
/**
35-
* A sanitizer guard for detecting modifications of a parameters default value.
35+
* A sanitizer guard that does not let a truthy value flow to the true branch.
36+
*
37+
* Since guards with different behaviour cannot exist on the same node,
38+
* we let all guards have the same behaviour, in the sense that they all check
39+
* the true branch. Instead, we partition guards into those that block
40+
* truthy values and those that block falsy values.
3641
*/
37-
abstract class BarrierGuard extends DataFlow::BarrierGuard {
38-
/** Result is true if this guard blocks non-empty values and false if it blocks empty values. */
39-
abstract boolean blocksNonEmpty();
40-
}
42+
abstract class BlocksTruthy extends DataFlow::BarrierGuard { }
43+
44+
/**
45+
* A sanitizer guard that does not let a falsy value flow to the true branch.
46+
*
47+
* Since guards with different behaviour cannot exist on the same node,
48+
* we let all guards have the same behaviour, in the sense that they all check
49+
* the true branch. Instead, we partition guards into those that block
50+
* truthy values and those that block falsy values.
51+
*/
52+
abstract class BlocksFalsey extends DataFlow::BarrierGuard { }
4153

4254
/** Gets the truthiness (non emptyness) of the default of `p` if that value is mutable */
4355
private boolean mutableDefaultValue(Parameter p) {
@@ -147,30 +159,46 @@ module ModificationOfParameterWithDefault {
147159
boolean isInverted() { result = inverted }
148160
}
149161

150-
/**
151-
* A check for the value being truthy or falsy can guard against modifying the default value.
152-
*/
153-
class IdentityGuard extends BarrierGuard {
154-
ControlFlowNode checked_node;
155-
boolean safe_branch;
156-
boolean nonEmpty;
162+
boolean isIdentityGuard(DataFlow::GuardNode guard, ControlFlowNode guarded) {
163+
exists(IdentityGuarded ig |
164+
ig instanceof Name and
165+
// In `not l`, the `ControlFlowNode` for `l` is not an instance of `GuardNode`.
166+
// TODO: This is slightly naive, we should change it when we have a proper guards library.
167+
guard.getNode().getAChildNode*() = ig and
168+
result = ig.isInverted() and
169+
guarded.getNode() = ig
170+
)
171+
}
157172

158-
IdentityGuard() {
159-
nonEmpty in [true, false] and
160-
exists(IdentityGuarded ig |
161-
this.getNode() = ig and
162-
checked_node = this and
163-
// The raw guard is true if the value is non-empty.
164-
// So we are safe either if we are looking for a non-empty value
165-
// or if we are looking for an empty value and the guard is inverted.
166-
safe_branch = ig.isInverted().booleanXor(nonEmpty)
167-
)
173+
class BlocksTruthyGuard extends BlocksTruthy {
174+
ControlFlowNode guarded;
175+
176+
BlocksTruthyGuard() {
177+
// The raw guard is true if the value is non-empty.
178+
// We wish to send truthy falues to the false branch,
179+
// se we are looking for inverted guards.
180+
isIdentityGuard(this, guarded) = true
168181
}
169182

170183
override predicate checks(ControlFlowNode node, boolean branch) {
171-
node = checked_node and branch = safe_branch
184+
node = guarded and
185+
branch = true
172186
}
187+
}
188+
189+
class BlocksFalseyGuard extends BlocksFalsey {
190+
ControlFlowNode guarded;
173191

174-
override boolean blocksNonEmpty() { result = nonEmpty }
192+
BlocksFalseyGuard() {
193+
// The raw guard is true if the value is non-empty.
194+
// We wish to send falsy falues to the false branch,
195+
// se we are looking for guards that are not inverted.
196+
isIdentityGuard(this, guarded) = false
197+
}
198+
199+
override predicate checks(ControlFlowNode node, boolean branch) {
200+
node = guarded and
201+
branch = true
202+
}
175203
}
176204
}

python/ql/test/query-tests/Functions/ModificationOfParameterWithDefault/test.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,23 @@ def dict_update_op_nochange(d = {}):
124124
# OK
125125
def sanitizer(l = []):
126126
if l:
127-
l.append(1) #$ modification=l
127+
l.append(1)
128128
return l
129129

130130
# OK
131131
def sanitizer_negated(l = [1]):
132132
if not l:
133-
l.append(1) #$ SPURIOUS: modification=l
133+
l.append(1)
134+
return l
135+
136+
# Not OK
137+
def sanitizer(l = []):
138+
if not l:
139+
l.append(1) #$ modification=l
140+
return l
141+
142+
# Not OK
143+
def sanitizer_negated(l = [1]):
144+
if l:
145+
l.append(1) #$ modification=l
134146
return l

0 commit comments

Comments
 (0)