Skip to content

Commit 3707f10

Browse files
Fix tests + add more tests
1 parent 2c74ddb commit 3707f10

File tree

2 files changed

+55
-25
lines changed

2 files changed

+55
-25
lines changed

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,17 @@ private DataFlow::TypeTrackingNode fileOpenInstance(DataFlow::TypeTracker t) {
2121
exists(DataFlow::TypeTracker t2 | result = fileOpenInstance(t2).track(t2, t))
2222
}
2323

24-
/** A call that returns an instance of an open file object. */
24+
/**
25+
* A call that returns an instance of an open file object.
26+
* This includes calls to methods that transitively call `open` or similar.
27+
*/
2528
class FileOpen extends DataFlow::CallCfgNode {
2629
FileOpen() { fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) }
2730
}
2831

2932
/** A call that may wrap a file object in a wrapper class or `os.fdopen`. */
3033
class FileWrapperCall extends DataFlow::CallCfgNode {
31-
FileOpen wrapped;
34+
DataFlow::Node wrapped;
3235

3336
FileWrapperCall() {
3437
wrapped = this.getArg(_).getALocalSource() and
@@ -42,18 +45,18 @@ class FileWrapperCall extends DataFlow::CallCfgNode {
4245
}
4346

4447
/** Gets the file that this call wraps. */
45-
FileOpen getWrapped() { result = wrapped }
48+
DataFlow::Node getWrapped() { result = wrapped }
4649
}
4750

4851
/** A node where a file is closed. */
4952
abstract class FileClose extends DataFlow::CfgNode {
5053
/** Holds if this file close will occur if an exception is thrown at `e`. */
51-
predicate guardsExceptions(Expr e) {
52-
this.asCfgNode() =
53-
DataFlow::exprNode(e).asCfgNode().getAnExceptionalSuccessor().getASuccessor*()
54+
predicate guardsExceptions(DataFlow::CfgNode raises) {
55+
this.asCfgNode() = raises.asCfgNode().getAnExceptionalSuccessor().getASuccessor*()
5456
or
55-
// the expression is after the close call
56-
DataFlow::exprNode(e).asCfgNode() = this.asCfgNode().getASuccessor*()
57+
// The expression is after the close call.
58+
// This also covers the body of a `with` statement.
59+
raises.asCfgNode() = this.asCfgNode().getASuccessor*()
5760
}
5861
}
5962

@@ -72,20 +75,14 @@ class WithStatement extends FileClose {
7275
With w;
7376

7477
WithStatement() { this.asExpr() = w.getContextExpr() }
75-
76-
override predicate guardsExceptions(Expr e) {
77-
super.guardsExceptions(e)
78-
or
79-
e = w.getAStmt().getAChildNode*()
80-
}
8178
}
8279

83-
/** Holds if an exception may be raised at `node` if it is a file object. */
84-
private predicate mayRaiseWithFile(DataFlow::CfgNode node) {
80+
/** Holds if an exception may be raised at `raises` if `file` is a file object. */
81+
private predicate mayRaiseWithFile(DataFlow::CfgNode file, DataFlow::CfgNode raises) {
8582
// Currently just consider any method called on `node`; e.g. `file.write()`; as potentially raising an exception
86-
exists(DataFlow::MethodCallNode mc | node = mc.getObject()) and
87-
not node instanceof FileOpen and
88-
not node instanceof FileClose
83+
raises.(DataFlow::MethodCallNode).getObject() = file and
84+
not file instanceof FileOpen and
85+
not file instanceof FileClose
8986
}
9087

9188
/** Holds if data flows from `nodeFrom` to `nodeTo` in one step that also includes file wrapper classes. */
@@ -131,10 +128,12 @@ predicate fileNotClosed(FileOpen fo) {
131128

132129
predicate fileMayNotBeClosedOnException(FileOpen fo, DataFlow::Node raises) {
133130
fileIsClosed(fo) and
134-
mayRaiseWithFile(raises) and
135-
fileLocalFlow(fo, raises) and
136-
not exists(FileClose fc |
137-
DataFlow::localFlow(fo, fc) and
138-
fc.guardsExceptions(raises.asExpr())
131+
exists(DataFlow::CfgNode fileRaised |
132+
mayRaiseWithFile(fileRaised, raises) and
133+
fileLocalFlow(fo, fileRaised) and
134+
not exists(FileClose fc |
135+
fileLocalFlow(fo, fc) and
136+
fc.guardsExceptions(raises)
137+
)
139138
)
140139
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,35 @@ def not_closed22(path):
246246

247247
def not_closed23(path):
248248
f23 = open(path, "w") # $ notClosed
249-
wr = FileWrapper(f23)
249+
wr = FileWrapper(f23)
250+
251+
def closed24(path):
252+
f24 = open(path, "w")
253+
try:
254+
f24.write("hi")
255+
except:
256+
pass
257+
f24.close()
258+
259+
def closed25(path):
260+
from django.http import FileResponse
261+
return FileResponse(open(path))
262+
263+
import os
264+
def closed26(path):
265+
fd = os.open(path)
266+
os.close(fd)
267+
268+
def not_closed27(path):
269+
fd = os.open(path, "w") # $notClosedOnException
270+
f27 = os.fdopen(fd, "w")
271+
f27.write("hi")
272+
f27.close()
273+
274+
def closed28(path):
275+
fd = os.open(path, os.O_WRONLY)
276+
f28 = os.fdopen(fd, "w")
277+
try:
278+
f28.write("hi")
279+
finally:
280+
f28.close()

0 commit comments

Comments
 (0)