Skip to content

Commit 4cbaeb1

Browse files
Merge pull request #19641 from joefarebrother/python-qual-file-not-closed
Python: Improve performance of FileNotClosed query by using basic block reachability
2 parents ec09d36 + 38072c7 commit 4cbaeb1

File tree

2 files changed

+37
-18
lines changed

2 files changed

+37
-18
lines changed

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,29 +50,32 @@ class FileWrapperCall extends DataFlow::CallCfgNode {
5050

5151
/** A node where a file is closed. */
5252
abstract class FileClose extends DataFlow::CfgNode {
53-
/** Holds if this file close will occur if an exception is thrown at `raises`. */
53+
/** Holds if this file close will occur if an exception is raised at `raises`. */
5454
predicate guardsExceptions(DataFlow::CfgNode raises) {
55-
cfgGetASuccessorStar(raises.asCfgNode().getAnExceptionalSuccessor(), this.asCfgNode())
55+
// The close call occurs after an exception edge in the cfg (a catch or finally)
56+
bbReachableRefl(raises.asCfgNode().getBasicBlock().getAnExceptionalSuccessor(),
57+
this.asCfgNode().getBasicBlock())
5658
or
57-
// The expression is after the close call.
58-
// This also covers the body of a `with` statement.
59-
cfgGetASuccessorStar(this.asCfgNode(), raises.asCfgNode())
59+
// The exception is after the close call.
60+
// A full cfg reachability check is not in general feasible for performance, so we approximate it with:
61+
// - A basic block reachability check (here) that works if the expression and close call are in different basic blocks
62+
// - A check (in the `WithStatement` override of `guardsExceptions`) for the case where the exception call
63+
// is lexically contained in the body of a `with` statement that closes the file.
64+
// This may cause FPs in a case such as:
65+
// f.close()
66+
// f.write("...")
67+
// We presume this to not be very common.
68+
bbReachableStrict(this.asCfgNode().getBasicBlock(), raises.asCfgNode().getBasicBlock())
6069
}
6170
}
6271

63-
private predicate cfgGetASuccessor(ControlFlowNode src, ControlFlowNode sink) {
64-
sink = src.getASuccessor()
65-
}
72+
private predicate bbSuccessor(BasicBlock src, BasicBlock sink) { sink = src.getASuccessor() }
6673

67-
pragma[inline]
68-
private predicate cfgGetASuccessorPlus(ControlFlowNode src, ControlFlowNode sink) =
69-
fastTC(cfgGetASuccessor/2)(src, sink)
74+
private predicate bbReachableStrict(BasicBlock src, BasicBlock sink) =
75+
fastTC(bbSuccessor/2)(src, sink)
7076

71-
pragma[inline]
72-
private predicate cfgGetASuccessorStar(ControlFlowNode src, ControlFlowNode sink) {
73-
src = sink
74-
or
75-
cfgGetASuccessorPlus(src, sink)
77+
private predicate bbReachableRefl(BasicBlock src, BasicBlock sink) {
78+
bbReachableStrict(src, sink) or src = sink
7679
}
7780

7881
/** A call to the `.close()` method of a file object. */
@@ -87,7 +90,16 @@ class OsCloseCall extends FileClose {
8790

8891
/** A `with` statement. */
8992
class WithStatement extends FileClose {
90-
WithStatement() { this.asExpr() = any(With w).getContextExpr() }
93+
With w;
94+
95+
WithStatement() { this.asExpr() = w.getContextExpr() }
96+
97+
override predicate guardsExceptions(DataFlow::CfgNode raises) {
98+
super.guardsExceptions(raises)
99+
or
100+
// Check whether the exception is raised in the body of the with statement.
101+
raises.asExpr().getParent*() = w.getBody().getAnItem()
102+
}
91103
}
92104

93105
/** Holds if an exception may be raised at `raises` if `file` is a file object. */

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,11 @@ def closed28(path):
277277
try:
278278
f28.write("hi")
279279
finally:
280-
f28.close()
280+
f28.close()
281+
282+
def closed29(path):
283+
# Due to an approximation in CFG reachability for performance, it is not detected that the `write` call that may raise occurs after the file has already been closed.
284+
# We presume this case to be uncommon.
285+
f28 = open(path) # $SPURIOUS:notClosedOnException
286+
f28.close()
287+
f28.write("already closed")

0 commit comments

Comments
 (0)