Skip to content

Commit f750e22

Browse files
Add case for exception flow
1 parent c8fc565 commit f750e22

File tree

4 files changed

+129
-89
lines changed

4 files changed

+129
-89
lines changed

python/ql/src/Resources/FileNotAlwaysClosed.ql

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,63 +13,13 @@
1313
*/
1414

1515
import python
16-
// import FileOpen
1716
import FileNotAlwaysClosedQuery
1817

19-
// /**
20-
// * Whether resource is opened and closed in in a matched pair of methods,
21-
// * either `__enter__` and `__exit__` or `__init__` and `__del__`
22-
// */
23-
// predicate opened_in_enter_closed_in_exit(ControlFlowNode open) {
24-
// file_not_closed_at_scope_exit(open) and
25-
// exists(FunctionValue entry, FunctionValue exit |
26-
// open.getScope() = entry.getScope() and
27-
// exists(ClassValue cls |
28-
// cls.declaredAttribute("__enter__") = entry and cls.declaredAttribute("__exit__") = exit
29-
// or
30-
// cls.declaredAttribute("__init__") = entry and cls.declaredAttribute("__del__") = exit
31-
// ) and
32-
// exists(AttrNode attr_open, AttrNode attrclose |
33-
// attr_open.getScope() = entry.getScope() and
34-
// attrclose.getScope() = exit.getScope() and
35-
// expr_is_open(attr_open.(DefinitionNode).getValue(), open) and
36-
// attr_open.getName() = attrclose.getName() and
37-
// close_method_call(_, attrclose)
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-
// predicate file_not_closed_at_exception_exit(ControlFlowNode open, ControlFlowNode exit) {
53-
// exists(EssaVariable v |
54-
// exit.(RaisingNode).viableExceptionalExit(_, _) and
55-
// not closes_arg(exit, v.getSourceVariable()) and
56-
// not close_method_call(exit, v.getAUse().(NameNode)) and
57-
// var_is_open(v, open) and
58-
// v.getAUse() = exit.getAChild*()
59-
// )
60-
// }
61-
/* Check to see if a file is opened but not closed or returned */
62-
// from ControlFlowNode defn, string message
63-
// where
64-
// not opened_in_enter_closed_in_exit(defn) and
65-
// (
66-
// file_not_closed_at_scope_exit(defn) and message = "File is opened but is not closed."
67-
// or
68-
// not file_not_closed_at_scope_exit(defn) and
69-
// file_not_closed_at_exception_exit(defn, _) and
70-
// message = "File may not be closed if an exception is raised."
71-
// )
72-
// select defn.getNode(), message
73-
from FileOpen fo
74-
where fileNotAlwaysClosed(fo)
75-
select fo, "File is opened but is not closed."
18+
from FileOpen fo, string msg
19+
where
20+
fileNotClosed(fo) and
21+
msg = "File is opened but is not closed."
22+
or
23+
fileMayNotBeClosedOnException(fo, _) and
24+
msg = "File may not be closed if an exception is raised"
25+
select fo.getLocalSource(), msg

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,133 @@ import python
44
import semmle.python.dataflow.new.internal.DataFlowDispatch
55
import semmle.python.ApiGraphs
66

7-
abstract class FileOpen extends DataFlow::CfgNode { }
7+
/** A CFG node where a file is opened. */
8+
abstract class FileOpenSource extends DataFlow::CfgNode { }
89

9-
class FileOpenCall extends FileOpen {
10-
FileOpenCall() { this = [API::builtin("open").getACall()] }
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+
/** A call that returns an instance of an open file object. */
25+
class FileOpen extends DataFlow::CallCfgNode {
26+
FileOpen() { fileOpenInstance(DataFlow::TypeTracker::end()).flowsTo(this) }
27+
28+
/** Gets the local source of this file object, through any wrapper calls. */
29+
FileOpen getLocalSource() {
30+
if this instanceof FileWrapperCall
31+
then result = this.(FileWrapperCall).getWrapped().getLocalSource()
32+
else result = this
33+
}
1134
}
1235

13-
class FileWrapperClassCall extends FileOpen, DataFlow::CallCfgNode {
36+
/** A call that may wrap a file object in a wrapper class or `os.fdopen`. */
37+
class FileWrapperCall extends FileOpenSource, DataFlow::CallCfgNode {
1438
FileOpen wrapped;
1539

16-
FileWrapperClassCall() {
40+
FileWrapperCall() {
1741
wrapped = this.getArg(_).getALocalSource() and
1842
this.getFunction() = classTracker(_)
43+
or
44+
wrapped = this.getArg(0) and
45+
this = API::moduleImport("os").getMember("fdopen").getACall()
1946
}
2047

48+
/** Gets the file that this call wraps. */
2149
FileOpen getWrapped() { result = wrapped }
2250
}
2351

24-
abstract class FileClose extends DataFlow::CfgNode { }
52+
/** A node where a file is closed. */
53+
abstract class FileClose extends DataFlow::CfgNode {
54+
/** Holds if this file close will occur if an exception is thrown at `e`. */
55+
predicate guardsExceptions(Expr e) {
56+
exists(Try try |
57+
e = try.getAStmt().getAChildNode*() and
58+
(
59+
this.asExpr() = try.getAHandler().getAChildNode*()
60+
or
61+
this.asExpr() = try.getAFinalstmt().getAChildNode*()
62+
)
63+
)
64+
}
65+
}
2566

67+
/** A call to the `.close()` method of a file object. */
2668
class FileCloseCall extends FileClose {
2769
FileCloseCall() { exists(DataFlow::MethodCallNode mc | mc.calls(this, "close")) }
2870
}
2971

72+
/** A call to `os.close`. */
3073
class OsCloseCall extends FileClose {
3174
OsCloseCall() { this = API::moduleImport("os").getMember("close").getACall().getArg(0) }
3275
}
3376

77+
/** A `with` statement. */
3478
class WithStatement extends FileClose {
35-
WithStatement() { exists(With w | this.asExpr() = w.getContextExpr()) }
79+
With w;
80+
81+
WithStatement() { this.asExpr() = w.getContextExpr() }
82+
83+
override predicate guardsExceptions(Expr e) {
84+
super.guardsExceptions(e)
85+
or
86+
e = w.getAStmt().getAChildNode*()
87+
}
3688
}
3789

90+
/** Holds if an exception may be raised at `node` if it is a file object. */
91+
private predicate mayRaiseWithFile(DataFlow::CfgNode node) {
92+
// Currently just consider any method called on `node`; e.g. `file.write()`; as potentially raising an exception
93+
exists(DataFlow::MethodCallNode mc | node = mc.getObject()) and
94+
not node instanceof FileOpen and
95+
not node instanceof FileClose
96+
}
97+
98+
/** Holds if the file opened at `fo` is closed. */
3899
predicate fileIsClosed(FileOpen fo) { exists(FileClose fc | DataFlow::localFlow(fo, fc)) }
39100

101+
/** Holds if the file opened at `fo` is returned to the caller. This makes the caller responsible for closing the file. */
40102
predicate fileIsReturned(FileOpen fo) {
41-
exists(Return ret | DataFlow::localFlow(fo, DataFlow::exprNode(ret.getValue())))
103+
exists(Return ret, Expr retVal |
104+
(
105+
retVal = ret.getValue()
106+
or
107+
retVal = ret.getValue().(List).getAnElt()
108+
or
109+
retVal = ret.getValue().(Tuple).getAnElt()
110+
) and
111+
DataFlow::localFlow(fo, DataFlow::exprNode(retVal))
112+
)
42113
}
43114

115+
/** Holds if the file opened at `fo` is stored in a field. We assume that another method is then responsible for closing the file. */
44116
predicate fileIsStoredInField(FileOpen fo) {
45117
exists(DataFlow::AttrWrite aw | DataFlow::localFlow(fo, aw.getValue()))
46118
}
47119

48-
predicate fileNotAlwaysClosed(FileOpen fo) {
120+
/** Holds if the file opened at `fo` is not closed, and is expected to be closed. */
121+
predicate fileNotClosed(FileOpen fo) {
49122
not fileIsClosed(fo) and
50123
not fileIsReturned(fo) and
51124
not fileIsStoredInField(fo) and
52-
not exists(FileWrapperClassCall fwc | fo = fwc.getWrapped())
125+
not exists(FileWrapperCall fwc | fo = fwc.getWrapped())
126+
}
127+
128+
predicate fileMayNotBeClosedOnException(FileOpen fo, DataFlow::Node raises) {
129+
fileIsClosed(fo) and
130+
mayRaiseWithFile(raises) and
131+
DataFlow::localFlow(fo, raises) and
132+
not exists(FileClose fc |
133+
DataFlow::localFlow(fo, fc) and
134+
fc.guardsExceptions(raises.asExpr())
135+
)
53136
}

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

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#File not always closed
22

33
def not_close1():
4-
f1 = open("filename")
4+
f1 = open("filename") # $ notClosedOnException
55
f1.write("Error could occur")
6-
f1.close() # $ notAlwaysClosed
6+
f1.close()
77

88
def not_close2():
9-
f2 = open("filename") # $ notAlwaysClosed
9+
f2 = open("filename") # $ notClosed
1010

1111
def closed3():
1212
f3 = open("filename")
@@ -46,7 +46,7 @@ def closed7():
4646
def not_closed8():
4747
f8 = None
4848
try:
49-
f8 = open("filename") # $ notAlwaysClosed
49+
f8 = open("filename") # $ MISSING:notClosedOnException
5050
f8.write("Error could occur")
5151
finally:
5252
if f8 is None:
@@ -55,7 +55,7 @@ def not_closed8():
5555
def not_closed9():
5656
f9 = None
5757
try:
58-
f9 = open("filename") # $ notAlwaysClosed
58+
f9 = open("filename") # $ MISSING:notAlwaysClosed
5959
f9.write("Error could occur")
6060
finally:
6161
if not f9:
@@ -76,7 +76,7 @@ def closed10():
7676

7777
#Not closed by handling the wrong exception
7878
def not_closed11():
79-
f11 = open("filename") # $ notAlwaysClosed
79+
f11 = open("filename") # $ MISSING:notAlwaysClosed
8080
try:
8181
f11.write("IOError could occur")
8282
f11.write("IOError could occur")
@@ -88,7 +88,7 @@ def doesnt_raise(*args):
8888
pass
8989

9090
def mostly_closed12():
91-
f12 = open("filename") # $ SPURIOUS:notAlwaysClosed
91+
f12 = open("filename")
9292
try:
9393
f12.write("IOError could occur")
9494
f12.write("IOError could occur")
@@ -105,11 +105,11 @@ def opener_func2(name):
105105
return t1
106106

107107
def not_closed13(name):
108-
f13 = open(name) # $ notAlwaysClosed
108+
f13 = open(name) # $ notClosed
109109
f13.write("Hello")
110110

111111
def may_not_be_closed14(name):
112-
f14 = opener_func2(name) # $ notAlwaysClosed
112+
f14 = opener_func2(name) # $ notClosedOnException
113113
f14.write("Hello")
114114
f14.close()
115115

@@ -120,13 +120,13 @@ def closer2(t3):
120120
closer1(t3)
121121

122122
def closed15():
123-
f15 = opener_func2()
123+
f15 = opener_func2() # $ SPURIOUS:notClosed
124124
closer2(f15)
125125

126126

127127
def may_not_be_closed16(name):
128128
try:
129-
f16 = open(name) # $ notAlwaysClosed
129+
f16 = open(name) # $ notClosedOnException
130130
f16.write("Hello")
131131
f16.close()
132132
except IOError:
@@ -138,7 +138,7 @@ def may_raise():
138138

139139
#Not handling all exceptions, but we'll tolerate the false negative
140140
def not_closed17():
141-
f17 = open("filename") # $ notAlwaysClosed
141+
f17 = open("filename") # $ MISSING:notClosedOnException
142142
try:
143143
f17.write("IOError could occur")
144144
f17.write("IOError could occur")
@@ -234,7 +234,7 @@ def closed21(path):
234234

235235

236236
def not_closed22(path):
237-
f22 = open(path, "wb") # $ notAlwaysClosed
237+
f22 = open(path, "wb") # $ MISSING:notClosedOnException
238238
try:
239239
f22.write(b"foo")
240240
may_raise()
@@ -244,3 +244,6 @@ def not_closed22(path):
244244
if f22.closed: # Wrong sense
245245
f22.close()
246246

247+
def not_closed23(path):
248+
f23 = open(path, "w") # $ notClosed
249+
wr = FileWrapper(f23)
Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
1-
import python
1+
import python
22
import Resources.FileNotAlwaysClosedQuery
33
import utils.test.InlineExpectationsTest
44

55
module MethodArgTest implements TestSig {
6-
string getARelevantTag() { result = "notAlwaysClosed" }
6+
string getARelevantTag() { result = ["notClosed", "notClosedOnException"] }
77

88
predicate hasActualResult(Location location, string element, string tag, string value) {
9-
exists(DataFlow::CfgNode f |
10-
element = f.toString() and
11-
location = f.getLocation() and
9+
exists(DataFlow::CfgNode el, FileOpen fo |
10+
el = fo.getLocalSource() and
11+
element = el.toString() and
12+
location = el.getLocation() and
1213
value = "" and
1314
(
14-
fileNotAlwaysClosed(f) and
15-
tag = "notAlwaysClosed"
15+
fileNotClosed(fo) and
16+
tag = "notClosed"
17+
or
18+
fileMayNotBeClosedOnException(fo, _) and
19+
tag = "notClosedOnException"
1620
)
1721
)
1822
}
1923
}
2024

21-
import MakeTest<MethodArgTest>
25+
import MakeTest<MethodArgTest>

0 commit comments

Comments
 (0)