Thanks to visit codestin.com
Credit goes to github.com

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

7 files changed

Lines changed: 76 additions & 63 deletions

File tree

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)