Skip to content

Commit 4356766

Browse files
Merge pull request #18845 from joefarebrother/python-qual-file-not-closed
Python: Modernize File Not Always Closed query
2 parents 96f7dc7 + 2fd9b16 commit 4356766

13 files changed

+275
-253
lines changed

python/ql/src/Resources/FileNotAlwaysClosed.py

-15
This file was deleted.

python/ql/src/Resources/FileNotAlwaysClosed.qhelp

+13-15
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,30 @@
44
<qhelp>
55

66
<overview>
7-
<p> If a file is opened then it should always be closed again, even if an
8-
exception is raised.
9-
Failing to ensure that all files are closed may result in failure due to too
10-
many open files.</p>
11-
7+
<p>When a file is opened, it should always be closed.
8+
</p>
9+
<p>A file opened for writing that is not closed when the application exits may result in data loss, where not all of the data written may be saved to the file.
10+
A file opened for reading or writing that is not closed may also use up file descriptors, which is a resource leak that in long running applications could lead to a failure to open additional files.
11+
</p>
1212
</overview>
1313
<recommendation>
1414

15-
<p>Ensure that if you open a file it is always closed on exiting the method.
16-
Wrap the code between the <code>open()</code> and <code>close()</code>
17-
functions in a <code>with</code> statement or use a <code>try...finally</code>
18-
statement. Using a <code>with</code> statement is preferred as it is shorter
19-
and more readable.</p>
15+
<p>Ensure that opened files are always closed, including when an exception could be raised.
16+
The best practice is often to use a <code>with</code> statement to automatically clean up resources.
17+
Otherwise, ensure that <code>.close()</code> is called in a <code>try...except</code> or <code>try...finally</code>
18+
block to handle any possible exceptions.
19+
</p>
2020

2121
</recommendation>
2222
<example>
23-
<p>The following code shows examples of different ways of closing a file. In the first example, the
24-
file is closed only if the method is exited successfully. In the other examples, the file is always
25-
closed on exiting the method.</p>
23+
<p>In the following examples, in the case marked BAD, the file may not be closed if an exception is raised. In the cases marked GOOD, the file is always closed.</p>
2624

27-
<sample src="FileNotAlwaysClosed.py" />
25+
<sample src="examples/FileNotAlwaysClosed.py" />
2826

2927
</example>
3028
<references>
3129

32-
30+
<li>Python Documentation: <a href="https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files">Reading and writing files</a>.</li>
3331
<li>Python Language Reference: <a href="http://docs.python.org/reference/compound_stmts.html#the-with-statement">The with statement</a>,
3432
<a href="http://docs.python.org/reference/compound_stmts.html#the-try-statement">The try statement</a>.</li>
3533
<li>Python PEP 343: <a href="http://www.python.org/dev/peps/pep-0343">The "with" Statement</a>.</li>
+11-59
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,26 @@
11
/**
22
* @name File is not always closed
3-
* @description Opening a file without ensuring that it is always closed may cause resource leaks.
3+
* @description Opening a file without ensuring that it is always closed may lead to data loss or resource leaks.
44
* @kind problem
55
* @tags efficiency
66
* correctness
77
* resources
8+
* quality
89
* external/cwe/cwe-772
910
* @problem.severity warning
1011
* @sub-severity high
11-
* @precision medium
12+
* @precision high
1213
* @id py/file-not-closed
1314
*/
1415

1516
import python
16-
import FileOpen
17+
import FileNotAlwaysClosedQuery
1718

18-
/**
19-
* Whether resource is opened and closed in in a matched pair of methods,
20-
* either `__enter__` and `__exit__` or `__init__` and `__del__`
21-
*/
22-
predicate opened_in_enter_closed_in_exit(ControlFlowNode open) {
23-
file_not_closed_at_scope_exit(open) and
24-
exists(FunctionValue entry, FunctionValue exit |
25-
open.getScope() = entry.getScope() and
26-
exists(ClassValue cls |
27-
cls.declaredAttribute("__enter__") = entry and cls.declaredAttribute("__exit__") = exit
28-
or
29-
cls.declaredAttribute("__init__") = entry and cls.declaredAttribute("__del__") = exit
30-
) and
31-
exists(AttrNode attr_open, AttrNode attrclose |
32-
attr_open.getScope() = entry.getScope() and
33-
attrclose.getScope() = exit.getScope() and
34-
expr_is_open(attr_open.(DefinitionNode).getValue(), open) and
35-
attr_open.getName() = attrclose.getName() and
36-
close_method_call(_, attrclose)
37-
)
38-
)
39-
}
40-
41-
predicate file_not_closed_at_scope_exit(ControlFlowNode open) {
42-
exists(EssaVariable v |
43-
BaseFlow::reaches_exit(v) and
44-
var_is_open(v, open) and
45-
not file_is_returned(v, open)
46-
)
47-
or
48-
call_to_open(open) and
49-
not exists(AssignmentDefinition def | def.getValue() = open) and
50-
not exists(Return r | r.getValue() = open.getNode())
51-
}
52-
53-
predicate file_not_closed_at_exception_exit(ControlFlowNode open, ControlFlowNode exit) {
54-
exists(EssaVariable v |
55-
exit.(RaisingNode).viableExceptionalExit(_, _) and
56-
not closes_arg(exit, v.getSourceVariable()) and
57-
not close_method_call(exit, v.getAUse().(NameNode)) and
58-
var_is_open(v, open) and
59-
v.getAUse() = exit.getAChild*()
60-
)
61-
}
62-
63-
/* Check to see if a file is opened but not closed or returned */
64-
from ControlFlowNode defn, string message
19+
from FileOpen fo, string msg
6520
where
66-
not opened_in_enter_closed_in_exit(defn) and
67-
(
68-
file_not_closed_at_scope_exit(defn) and message = "File is opened but is not closed."
69-
or
70-
not file_not_closed_at_scope_exit(defn) and
71-
file_not_closed_at_exception_exit(defn, _) and
72-
message = "File may not be closed if an exception is raised."
73-
)
74-
select defn.getNode(), message
21+
fileNotClosed(fo) and
22+
msg = "File is opened but is not closed."
23+
or
24+
fileMayNotBeClosedOnException(fo, _) and
25+
msg = "File may not be closed if an exception is raised."
26+
select fo, msg
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/** Definitions for reasoning about whether files are closed. */
2+
3+
import python
4+
import semmle.python.dataflow.new.internal.DataFlowDispatch
5+
import semmle.python.ApiGraphs
6+
7+
/** A CFG node where a file is opened. */
8+
abstract class FileOpenSource extends DataFlow::CfgNode { }
9+
10+
/** A call to the builtin `open` or `os.open`. */
11+
class FileOpenCall extends FileOpenSource {
12+
FileOpenCall() {
13+
this = [API::builtin("open").getACall(), API::moduleImport("os").getMember("open").getACall()]
14+
}
15+
}
16+
17+
private DataFlow::TypeTrackingNode fileOpenInstance(DataFlow::TypeTracker t) {
18+
t.start() and
19+
result instanceof FileOpenSource
20+
or
21+
exists(DataFlow::TypeTracker t2 | result = fileOpenInstance(t2).track(t2, t))
22+
}
23+
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+
*/
28+
class FileOpen extends DataFlow::CallCfgNode {
29+
FileOpen() { fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) }
30+
}
31+
32+
/** A call that may wrap a file object in a wrapper class or `os.fdopen`. */
33+
class FileWrapperCall extends DataFlow::CallCfgNode {
34+
DataFlow::Node wrapped;
35+
36+
FileWrapperCall() {
37+
wrapped = this.getArg(_).getALocalSource() and
38+
this.getFunction() = classTracker(_)
39+
or
40+
wrapped = this.getArg(0) and
41+
this = API::moduleImport("os").getMember("fdopen").getACall()
42+
or
43+
wrapped = this.getArg(0) and
44+
this = API::moduleImport("django").getMember("http").getMember("FileResponse").getACall()
45+
}
46+
47+
/** Gets the file that this call wraps. */
48+
DataFlow::Node getWrapped() { result = wrapped }
49+
}
50+
51+
/** A node where a file is closed. */
52+
abstract class FileClose extends DataFlow::CfgNode {
53+
/** Holds if this file close will occur if an exception is thrown at `raises`. */
54+
predicate guardsExceptions(DataFlow::CfgNode raises) {
55+
this.asCfgNode() = raises.asCfgNode().getAnExceptionalSuccessor().getASuccessor*()
56+
or
57+
// The expression is after the close call.
58+
// This also covers the body of a `with` statement.
59+
raises.asCfgNode() = this.asCfgNode().getASuccessor*()
60+
}
61+
}
62+
63+
/** A call to the `.close()` method of a file object. */
64+
class FileCloseCall extends FileClose {
65+
FileCloseCall() { exists(DataFlow::MethodCallNode mc | mc.calls(this, "close")) }
66+
}
67+
68+
/** A call to `os.close`. */
69+
class OsCloseCall extends FileClose {
70+
OsCloseCall() { this = API::moduleImport("os").getMember("close").getACall().getArg(0) }
71+
}
72+
73+
/** A `with` statement. */
74+
class WithStatement extends FileClose {
75+
WithStatement() { this.asExpr() = any(With w).getContextExpr() }
76+
}
77+
78+
/** Holds if an exception may be raised at `raises` if `file` is a file object. */
79+
private predicate mayRaiseWithFile(DataFlow::CfgNode file, DataFlow::CfgNode raises) {
80+
// Currently just consider any method called on `file`; e.g. `file.write()`; as potentially raising an exception
81+
raises.(DataFlow::MethodCallNode).getObject() = file and
82+
not file instanceof FileOpen and
83+
not file instanceof FileClose
84+
}
85+
86+
/** Holds if data flows from `nodeFrom` to `nodeTo` in one step that also includes file wrapper classes. */
87+
private predicate fileAdditionalLocalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
88+
exists(FileWrapperCall fw | nodeFrom = fw.getWrapped() and nodeTo = fw)
89+
}
90+
91+
private predicate fileLocalFlowHelper0(
92+
DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
93+
) {
94+
exists(DataFlow::Node nodeMid |
95+
nodeFrom.flowsTo(nodeMid) and fileAdditionalLocalFlowStep(nodeMid, nodeTo)
96+
)
97+
}
98+
99+
private predicate fileLocalFlowHelper1(
100+
DataFlow::LocalSourceNode nodeFrom, DataFlow::LocalSourceNode nodeTo
101+
) {
102+
fileLocalFlowHelper0*(nodeFrom, nodeTo)
103+
}
104+
105+
/** Holds if data flows from `source` to `sink`, including file wrapper classes. */
106+
pragma[inline]
107+
private predicate fileLocalFlow(FileOpen source, DataFlow::Node sink) {
108+
exists(DataFlow::LocalSourceNode mid | fileLocalFlowHelper1(source, mid) and mid.flowsTo(sink))
109+
}
110+
111+
/** Holds if the file opened at `fo` is closed. */
112+
predicate fileIsClosed(FileOpen fo) { exists(FileClose fc | fileLocalFlow(fo, fc)) }
113+
114+
/** Holds if the file opened at `fo` is returned to the caller. This makes the caller responsible for closing the file. */
115+
predicate fileIsReturned(FileOpen fo) {
116+
exists(Return ret, Expr retVal |
117+
(
118+
retVal = ret.getValue()
119+
or
120+
retVal = ret.getValue().(List).getAnElt()
121+
or
122+
retVal = ret.getValue().(Tuple).getAnElt()
123+
) and
124+
fileLocalFlow(fo, DataFlow::exprNode(retVal))
125+
)
126+
}
127+
128+
/** Holds if the file opened at `fo` is stored in a field. We assume that another method is then responsible for closing the file. */
129+
predicate fileIsStoredInField(FileOpen fo) {
130+
exists(DataFlow::AttrWrite aw | fileLocalFlow(fo, aw.getValue()))
131+
}
132+
133+
/** Holds if the file opened at `fo` is not closed, and is expected to be closed. */
134+
predicate fileNotClosed(FileOpen fo) {
135+
not fileIsClosed(fo) and
136+
not fileIsReturned(fo) and
137+
not fileIsStoredInField(fo)
138+
}
139+
140+
predicate fileMayNotBeClosedOnException(FileOpen fo, DataFlow::Node raises) {
141+
fileIsClosed(fo) and
142+
exists(DataFlow::CfgNode fileRaised |
143+
mayRaiseWithFile(fileRaised, raises) and
144+
fileLocalFlow(fo, fileRaised) and
145+
not exists(FileClose fc |
146+
fileLocalFlow(fo, fc) and
147+
fc.guardsExceptions(raises)
148+
)
149+
)
150+
}

python/ql/src/Resources/FileOpen.qll

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
/** Contains predicates concerning when and where files are opened and closed. */
1+
/**
2+
* DEPRECATED: Use FileNotAlwaysClosedQuery instead.
3+
* Contains predicates concerning when and where files are opened and closed.
4+
*/
5+
deprecated module;
26

37
import python
48
import semmle.python.pointsto.Filters
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
def bad():
2+
f = open("filename", "w")
3+
f.write("could raise exception") # BAD: This call could raise an exception, leading to the file not being closed.
4+
f.close()
5+
6+
7+
def good1():
8+
with open("filename", "w") as f:
9+
f.write("always closed") # GOOD: The `with` statement ensures the file is always closed.
10+
11+
def good2():
12+
f = open("filename", "w")
13+
try:
14+
f.write("always closed")
15+
finally:
16+
f.close() # GOOD: The `finally` block always ensures the file is closed.
17+

0 commit comments

Comments
 (0)