Skip to content

Commit 199fd86

Browse files
committed
Fix FP for py/file-not-closed
1 parent 8909694 commit 199fd86

3 files changed

Lines changed: 33 additions & 7 deletions

File tree

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,39 @@ private DataFlow::TypeTrackingNode fileOpenInstance(DataFlow::TypeTracker t) {
2121
exists(DataFlow::TypeTracker t2 | result = fileOpenInstance(t2).track(t2, t))
2222
}
2323

24+
/**
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.
31+
*/
32+
private predicate launderedAttrRead(DataFlow::AttrRead read) {
33+
fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(read)
34+
}
35+
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+
}
43+
2444
/**
2545
* A call that returns an instance of an open file object.
2646
* This includes calls to methods that transitively call `open` or similar.
2747
*/
2848
class FileOpen extends DataFlow::CallCfgNode {
29-
FileOpen() { fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) }
49+
FileOpen() {
50+
fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) and
51+
// Don't treat an accessor that merely re-exposes a file held in an instance attribute
52+
// (e.g. `FileIO.fileno` returning `self._fd`) as opening a new file. Such flow is
53+
// introduced by instance-attribute type tracking; the underlying open is tracked at its
54+
// real creation site.
55+
not launderedFileInstance(DataFlow::TypeTracker::end()).flowsTo(this)
56+
}
3057
}
3158

3259
/** A call that may wrap a file object in a wrapper class or `os.fdopen`. */

python/ql/test/query-tests/Resources/FileNotAlwaysClosed/FileNotAlwaysClosed.expected

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,3 @@
77
| resources_test.py:250:11:250:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |
88
| resources_test.py:271:10:271:27 | ControlFlowNode for Attribute() | File may not be closed if $@ raises an exception. | resources_test.py:273:5:273:19 | ControlFlowNode for Attribute() | this operation |
99
| resources_test.py:287:11:287:20 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:289:5:289:31 | ControlFlowNode for Attribute() | this operation |
10-
| resources_test.py:361:13:361:27 | ControlFlowNode for Attribute() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |

python/ql/test/query-tests/Resources/FileNotAlwaysClosed/resources_test.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,14 +350,14 @@ def close(self):
350350
os.close(self._fd)
351351

352352
def closed33(path):
353-
# False positive mirroring CPython's `_pyio.open`.
353+
# Regression test mirroring CPython's `_pyio.open`.
354354
# `holder.fileno()` merely returns the existing file descriptor; it does not
355355
# open a new resource. With instance-attribute type tracking, the `os.open`
356-
# source flows through `self._fd` and back out of `fileno()`, so the call
357-
# `holder.fileno()` is wrongly treated as a fresh file-open whose result is
358-
# never closed. The descriptor is in fact owned and closed by `holder.close()`.
356+
# source flows through `self._fd` and back out of `fileno()`. The query must
357+
# not treat that re-exposed descriptor as a fresh file-open whose result is
358+
# never closed. The descriptor is owned and closed by `holder.close()`.
359359
holder = FdHolder33(path)
360360
try:
361-
n = holder.fileno() # $ SPURIOUS: Alert
361+
n = holder.fileno() # No alert: this re-exposes an existing descriptor, not a new open.
362362
finally:
363363
holder.close()

0 commit comments

Comments
 (0)