Skip to content

Commit bd3fa7f

Browse files
Switch to dataflow check for guards exceptions
This reduces some confusing FPs, though appears to introduce another
1 parent 93f4721 commit bd3fa7f

File tree

7 files changed

+76
-63
lines changed

7 files changed

+76
-63
lines changed

python/ql/src/Resources/FileNotAlwaysClosed.ql

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@
1515

1616
import python
1717
import FileNotAlwaysClosedQuery
18+
import codeql.util.Option
1819

19-
from FileOpen fo, string msg
20+
from FileOpen fo, string msg, LocatableOption<Location, DataFlow::Node>::Option exec
2021
where
2122
fileNotClosed(fo) and
22-
msg = "File is opened but is not closed."
23+
msg = "File is opened but is not closed." and
24+
exec.isNone()
2325
or
24-
fileMayNotBeClosedOnException(fo, _) and
25-
msg = "File may not be closed if an exception is raised."
26-
select fo, msg
26+
fileMayNotBeClosedOnException(fo, exec.asSome()) and
27+
msg = "File may not be closed if $@ raises an exception."
28+
select fo, msg, exec, "this operation"

python/ql/src/Resources/FileNotAlwaysClosedQuery.qll

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class FileWrapperCall extends DataFlow::CallCfgNode {
3434
DataFlow::Node wrapped;
3535

3636
FileWrapperCall() {
37+
// Approximation: Treat any passing of a file object to a class constructor as potentially a wrapper
38+
// This could be made more precise by checking that the constructor writes the file to a field.
3739
wrapped = this.getArg(_).getALocalSource() and
3840
this.getFunction() = classTracker(_)
3941
or
@@ -51,21 +53,14 @@ class FileWrapperCall extends DataFlow::CallCfgNode {
5153
/** A node where a file is closed. */
5254
abstract class FileClose extends DataFlow::CfgNode {
5355
/** Holds if this file close will occur if an exception is raised at `raises`. */
54-
predicate guardsExceptions(DataFlow::CfgNode raises) {
56+
predicate guardsExceptions(DataFlow::CfgNode fileRaises) {
5557
// The close call occurs after an exception edge in the cfg (a catch or finally)
56-
bbReachableRefl(raises.asCfgNode().getBasicBlock().getAnExceptionalSuccessor(),
58+
bbReachableRefl(fileRaises.asCfgNode().getBasicBlock().getAnExceptionalSuccessor(),
5759
this.asCfgNode().getBasicBlock())
5860
or
5961
// 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())
62+
// A full cfg reachability check is not feasible for performance, instead we use local dataflow
63+
fileLocalFlow(this, fileRaises)
6964
}
7065
}
7166

@@ -93,13 +88,6 @@ class WithStatement extends FileClose {
9388
With w;
9489

9590
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-
}
10391
}
10492

10593
/** Holds if an exception may be raised at `raises` if `file` is a file object. */
@@ -131,7 +119,7 @@ private predicate fileLocalFlowHelper1(
131119

132120
/** Holds if data flows from `source` to `sink`, including file wrapper classes. */
133121
pragma[inline]
134-
private predicate fileLocalFlow(FileOpen source, DataFlow::Node sink) {
122+
private predicate fileLocalFlow(DataFlow::Node source, DataFlow::Node sink) {
135123
exists(DataFlow::LocalSourceNode mid | fileLocalFlowHelper1(source, mid) and mid.flowsTo(sink))
136124
}
137125

@@ -171,7 +159,7 @@ predicate fileMayNotBeClosedOnException(FileOpen fo, DataFlow::Node raises) {
171159
fileLocalFlow(fo, fileRaised) and
172160
not exists(FileClose fc |
173161
fileLocalFlow(fo, fc) and
174-
fc.guardsExceptions(raises)
162+
fc.guardsExceptions(fileRaised)
175163
)
176164
)
177165
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
| resources_test.py:4:10:4:25 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:5:5:5:33 | ControlFlowNode for Attribute() | this operation |
2+
| resources_test.py:9:10:9:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |
3+
| resources_test.py:108:11:108:20 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |
4+
| resources_test.py:112:11:112:28 | ControlFlowNode for opener_func2() | File may not be closed if $@ raises an exception. | resources_test.py:113:5:113:22 | ControlFlowNode for Attribute() | this operation |
5+
| resources_test.py:123:11:123:24 | ControlFlowNode for opener_func2() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |
6+
| resources_test.py:129:15:129:24 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:130:9:130:26 | ControlFlowNode for Attribute() | this operation |
7+
| resources_test.py:248:11:248:25 | ControlFlowNode for open() | File is opened but is not closed. | file://:0:0:0:0 | (none) | this operation |
8+
| resources_test.py:269:10:269:27 | ControlFlowNode for Attribute() | File may not be closed if $@ raises an exception. | resources_test.py:271:5:271:19 | ControlFlowNode for Attribute() | this operation |
9+
| resources_test.py:285:11:285:20 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:287:5:287:31 | ControlFlowNode for Attribute() | this operation |
10+
| resources_test.py:305:10:305:19 | ControlFlowNode for open() | File may not be closed if $@ raises an exception. | resources_test.py:308:5:308:24 | ControlFlowNode for Attribute() | this operation |
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
query: Resources/FileNotAlwaysClosed.ql
2+
postprocess: utils/test/InlineExpectationsTestQuery.ql

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

Lines changed: 49 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") # $ notClosedOnException
4+
f1 = open("filename") # $ Alert # not closed on exception
55
f1.write("Error could occur")
66
f1.close()
77

88
def not_close2():
9-
f2 = open("filename") # $ notClosed
9+
f2 = open("filename") # $ Alert
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") # $ MISSING:notClosedOnException
49+
f8 = open("filename") # $ MISSING:Alert # not closed on exception
5050
f8.write("Error could occur")
5151
finally:
5252
if f8 is None: # We don't precisely consider this condition, so this result is MISSING. However, this seems uncommon.
@@ -105,11 +105,11 @@ def opener_func2(name):
105105
return t1
106106

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

111111
def may_not_be_closed14(name):
112-
f14 = opener_func2(name) # $ notClosedOnException
112+
f14 = opener_func2(name) # $ Alert # not closed on exception
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() # $ SPURIOUS:notClosed
123+
f15 = opener_func2() # $ SPURIOUS:Alert
124124
closer2(f15) # We don't detect that this call closes the file, so this result is SPURIOUS.
125125

126126

127127
def may_not_be_closed16(name):
128128
try:
129-
f16 = open(name) # $ notClosedOnException
129+
f16 = open(name) # $ Alert # not closed on exception
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") # $ MISSING:notClosedOnException
141+
f17 = open("filename") # $ MISSING:Alert # not closed on exception
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") # $ MISSING:notClosedOnException
237+
f22 = open(path, "wb") # $ MISSING:Alert # not closed on exception
238238
try:
239239
f22.write(b"foo")
240240
may_raise()
@@ -245,7 +245,7 @@ def not_closed22(path):
245245
f22.close()
246246

247247
def not_closed23(path):
248-
f23 = open(path, "w") # $ notClosed
248+
f23 = open(path, "w") # $ Alert
249249
wr = FileWrapper(f23)
250250

251251
def closed24(path):
@@ -266,7 +266,7 @@ def closed26(path):
266266
os.close(fd)
267267

268268
def not_closed27(path):
269-
fd = os.open(path, "w") # $notClosedOnException
269+
fd = os.open(path, "w") # $Alert # not closed on exception
270270
f27 = os.fdopen(fd, "w")
271271
f27.write("hi")
272272
f27.close()
@@ -282,6 +282,42 @@ def closed28(path):
282282
def closed29(path):
283283
# 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.
284284
# We presume this case to be uncommon.
285-
f28 = open(path) # $SPURIOUS:notClosedOnException
285+
f28 = open(path) # $SPURIOUS:Alert # not closed on exception
286286
f28.close()
287-
f28.write("already closed")
287+
f28.write("already closed")
288+
289+
# False positive:
290+
291+
class NotWrapper:
292+
def __init__(self, fp):
293+
self.data = fp.read()
294+
fp.close()
295+
296+
def do_something():
297+
pass
298+
299+
def closed30(path):
300+
# Combination of approximations resulting in this FP:
301+
# - NotWrapper is treated as a wrapper class as a file handle is passed to it
302+
# - thing.do_something() is treated as a call that can raise an exception while a file is open
303+
# - this call is treated as occurring after the open but not as being guarded by the with statement, as it is in the same basic block
304+
305+
with open(path) as fp: # $SPURIOUS:Alert # not closed on exception
306+
thing = NotWrapper(fp)
307+
308+
thing.do_something()
309+
310+
311+
def closed31(path):
312+
with open(path) as fp:
313+
data = fp.readline()
314+
data2 = fp.readline()
315+
316+
317+
class FlowReader():
318+
def __init__(self, f):
319+
pass
320+
def test_cannot_convert(tdata):
321+
with open(tdata, "rb") as f:
322+
flow_reader = FlowReader(f)
323+
list(flow_reader.stream())

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

Whitespace-only changes.

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

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)