Skip to content

Commit ea7510b

Browse files
committed
Refactor ReExposedInstance logic into one place
1 parent 415857c commit ea7510b

3 files changed

Lines changed: 57 additions & 36 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Provides a parameterized module for identifying values that instance-attribute type tracking
3+
* has re-exposed (laundered) out of an instance attribute, rather than freshly created.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
9+
/** Holds if `node` should be treated as an instance source. */
10+
signature predicate instanceNodeSig(DataFlow::Node node);
11+
12+
/**
13+
* Provides a predicate for identifying values that instance-attribute type tracking has
14+
* re-exposed (laundered) out of an instance attribute, rather than freshly created.
15+
*
16+
* Instance-attribute type tracking can flow a value into an instance attribute and back out at
17+
* a later attribute read, for example `BufferedRWPair.reader` or `FileIO.fileno` returning
18+
* `self._fd`. Such a re-exposed value is owned by the enclosing instance and is not a fresh
19+
* resource; queries that reason about resource creation or lifetime should not treat it as one.
20+
*
21+
* The parameter `isInstance` defines which nodes count as instance sources (typically the result
22+
* of a class- or resource-instance type tracker).
23+
*/
24+
module ReExposedInstance<instanceNodeSig/1 isInstance> {
25+
/**
26+
* Holds if `read` is an attribute read that re-exposes an instance held in an instance
27+
* attribute.
28+
*/
29+
private predicate launderedAttrRead(DataFlow::AttrRead read) { isInstance(read) }
30+
31+
/** Type tracking forward from an attribute read that re-exposes an instance held in a field. */
32+
private DataFlow::TypeTrackingNode launderedInstance(DataFlow::TypeTracker t) {
33+
t.start() and
34+
launderedAttrRead(result)
35+
or
36+
exists(DataFlow::TypeTracker t2 | result = launderedInstance(t2).track(t2, t))
37+
}
38+
39+
/**
40+
* Holds if `node` is a value that has been re-exposed (laundered) out of an instance attribute,
41+
* rather than being a freshly created instance.
42+
*/
43+
predicate isReExposed(DataFlow::Node node) {
44+
launderedInstance(DataFlow::TypeTracker::end()).flowsTo(node)
45+
}
46+
}

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import python
44
import semmle.python.dataflow.new.internal.DataFlowDispatch
55
import semmle.python.ApiGraphs
6+
private import semmle.python.dataflow.new.internal.ReExposedInstance
67

78
/** A CFG node where a file is opened. */
89
abstract class FileOpenSource extends DataFlow::CfgNode { }
@@ -22,24 +23,13 @@ private DataFlow::TypeTrackingNode fileOpenInstance(DataFlow::TypeTracker t) {
2223
}
2324

2425
/**
25-
* Holds if `read` is an attribute read that re-exposes an already-open file held in an
26-
* instance attribute, for example `FileIO.fileno` returning `self._fd`.
27-
*
28-
* Instance-attribute type tracking can launder an open file out of such an accessor, which
29-
* would otherwise be mistaken for a fresh file open. The underlying open is tracked, and its
30-
* lifetime handled, separately at its real creation site.
26+
* Holds if `node` is tracked to be an instance of an open file object.
3127
*/
32-
private predicate launderedAttrRead(DataFlow::AttrRead read) {
33-
fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(read)
28+
private predicate fileInstanceNode(DataFlow::Node node) {
29+
fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(node)
3430
}
3531

36-
/** Type tracking forward from an attribute read that re-exposes a file held in a field. */
37-
private DataFlow::TypeTrackingNode launderedFileInstance(DataFlow::TypeTracker t) {
38-
t.start() and
39-
launderedAttrRead(result)
40-
or
41-
exists(DataFlow::TypeTracker t2 | result = launderedFileInstance(t2).track(t2, t))
42-
}
32+
private module FileReExposed = ReExposedInstance<fileInstanceNode/1>;
4333

4434
/**
4535
* A call that returns an instance of an open file object.
@@ -52,7 +42,7 @@ class FileOpen extends DataFlow::CallCfgNode {
5242
// (e.g. `FileIO.fileno` returning `self._fd`) as opening a new file. Such flow is
5343
// introduced by instance-attribute type tracking; the underlying open is tracked at its
5444
// real creation site.
55-
not launderedFileInstance(DataFlow::TypeTracker::end()).flowsTo(this)
45+
not FileReExposed::isReExposed(this)
5646
}
5747
}
5848

python/ql/src/Statements/ShouldUseWithStatement.ql

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import python
1616
private import semmle.python.dataflow.new.DataFlow
1717
private import semmle.python.dataflow.new.internal.DataFlowDispatch
18+
private import semmle.python.dataflow.new.internal.ReExposedInstance
1819

1920
predicate calls_close(Call c) { exists(Attribute a | c.getFunc() = a and a.getName() = "close") }
2021

@@ -24,26 +25,10 @@ predicate only_stmt_in_finally(Try t, Call c) {
2425
)
2526
}
2627

27-
/**
28-
* Holds if `read` is an attribute read that re-exposes an instance of `cls` held in an
29-
* instance attribute, for example `BufferedRWPair.reader`.
30-
*
31-
* Instance-attribute type tracking can launder such an instance out of a field. The object
32-
* is owned by the enclosing instance, so its lifetime spans that instance and cannot be
33-
* expressed with a `with` statement; closing it in a `finally` block is therefore not a
34-
* candidate for refactoring.
35-
*/
36-
private predicate launderedAttrRead(Class cls, DataFlow::AttrRead read) {
37-
read = classInstanceTracker(cls)
38-
}
28+
/** Holds if `node` is tracked to be an instance of some class. */
29+
private predicate classInstanceNode(DataFlow::Node node) { node = classInstanceTracker(_) }
3930

40-
/** Type tracking forward from an attribute read that re-exposes an instance held in a field. */
41-
private DataFlow::TypeTrackingNode launderedInstance(Class cls, DataFlow::TypeTracker t) {
42-
t.start() and
43-
launderedAttrRead(cls, result)
44-
or
45-
exists(DataFlow::TypeTracker t2 | result = launderedInstance(cls, t2).track(t2, t))
46-
}
31+
private module ClassReExposed = ReExposedInstance<classInstanceNode/1>;
4732

4833
from Call close, Try t, Class cls, DataFlow::Node closeTarget
4934
where
@@ -54,7 +39,7 @@ where
5439
// Don't report closing a resource that is held in an instance attribute (e.g. `self.reader`).
5540
// Such flow is introduced by instance-attribute type tracking; the object's lifetime is tied
5641
// to the enclosing instance and cannot be expressed with a `with` statement.
57-
not launderedInstance(cls, DataFlow::TypeTracker::end()).flowsTo(closeTarget) and
42+
not ClassReExposed::isReExposed(closeTarget) and
5843
DuckTyping::isContextManager(cls)
5944
select close,
6045
"Instance of context-manager class $@ is closed in a finally block. Consider using 'with' statement.",

0 commit comments

Comments
 (0)