Skip to content

Commit 7e9617f

Browse files
committed
Detect lack of error handling for os.File.Close
1 parent c03fe70 commit 7e9617f

File tree

8 files changed

+386
-0
lines changed

8 files changed

+386
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"os"
5+
)
6+
7+
func example() error {
8+
f, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE, 0666)
9+
10+
if err != nil {
11+
return err
12+
}
13+
14+
defer f.Close()
15+
16+
if _, err := f.WriteString("Hello"); err != nil {
17+
return err
18+
}
19+
20+
return nil
21+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Data written to a file handle may not immediately be flushed to the underlying storage medium by
9+
the operating system. Often, the data may be cached in memory until the handle is closed, or
10+
until a later point after that. Only calling <code>os.File.Sync</code> gives a reasonable guarantee
11+
that the data is flushed. Therefore, write errors may not occur until <code>os.File.Close</code>
12+
or <code>os.File.Sync</code> are called. If either is called and any errors returned by them are
13+
discarded, then the program may be unaware that data loss occurred.
14+
</p>
15+
</overview>
16+
17+
<recommendation>
18+
<p>
19+
Always check whether <code>os.File.Close</code> returned an error and handle it appropriately.
20+
</p>
21+
</recommendation>
22+
23+
<example>
24+
<p>
25+
In the following example, a call to <code>os.File.Close</code> is deferred with the intention of
26+
closing the file after all work on it has been done. However, while errors that may arise during
27+
the call to <code>os.File.WriteString</code> are handled, any errors arising from
28+
<code>os.File.Close</code> are discarded silently:
29+
</p>
30+
31+
<sample src="UnhandledCloseWritableHandle.go" />
32+
33+
<p>
34+
To correct this issue, handle errors arising from calls to <code>os.File.Close</code> explicitly:
35+
</p>
36+
37+
<sample src="UnhandledCloseWritableHandleGood.go" />
38+
39+
</example>
40+
41+
<references>
42+
<li>The Go Programming Language Specification: <a href="https://go.dev/ref/spec#Defer_statements">Defer statements</a>.</li>
43+
<li>The Go Programming Language Specification: <a href="https://go.dev/ref/spec#Errors">Errors</a>.</li>
44+
</references>
45+
</qhelp>
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* @name Writable file handled closed without error handling
3+
* @description Errors which occur when closing a writable file handle may result in data loss
4+
* if the data could not be successfully flushed. Such errors should be handled
5+
* explicitly.
6+
* @kind problem
7+
* @problem.severity warning
8+
* @precision medium
9+
* @id go/unhandled-writable-file-close
10+
* @tags maintainability
11+
* correctness
12+
* call
13+
* defer
14+
*/
15+
16+
import go
17+
18+
/**
19+
* Determines whether a flag for use with `os.OpenFile` implies that the resulting
20+
* file handle will be writable.
21+
*/
22+
predicate isWritable(Entity flag) {
23+
flag.hasQualifiedName("os", "O_WRONLY") or
24+
flag.hasQualifiedName("os", "O_RDWR")
25+
}
26+
27+
/**
28+
* Recursively extracts constant names from an expression.
29+
*/
30+
QualifiedName getConstants(ValueExpr expr) {
31+
result = expr or
32+
result = getConstants(expr.getAChild())
33+
}
34+
35+
/**
36+
* Matches the `os.OpenFile` function.
37+
*/
38+
class OpenFileFun extends Function {
39+
OpenFileFun() { this.hasQualifiedName("os", "OpenFile") }
40+
}
41+
42+
/**
43+
* Matches the `os.File.Close` function.
44+
*/
45+
class CloseFileFun extends Function {
46+
CloseFileFun() { this.hasQualifiedName("os.File", "Close") }
47+
}
48+
49+
/**
50+
* Matches the `os.File.Sync` function.
51+
*/
52+
class SyncFileFun extends Function {
53+
SyncFileFun() { this.hasQualifiedName("os.File", "Sync") }
54+
}
55+
56+
/**
57+
* Determines whether a call to a function is "unhandled". That is, it is either
58+
* deferred or its result is not assigned to anything.
59+
* TODO: maybe we should check that something is actually done with the result
60+
*/
61+
predicate unhandledCall(DataFlow::CallNode call) {
62+
exists(DeferStmt defer | defer.getCall() = call.asExpr()) or
63+
exists(ExprStmt stmt | stmt.getExpr() = call.asExpr())
64+
}
65+
66+
/**
67+
* Determines whether `source` is a writable file handle returned by a `call` to the
68+
* `os.OpenFile` function.
69+
*/
70+
predicate isWritableFileHandle(DataFlow::Node source, DataFlow::CallNode call) {
71+
exists(OpenFileFun f, DataFlow::Node flags, QualifiedName flag |
72+
// check that the source is a result of the call
73+
source = call.getAResult() and
74+
// find a call to the os.OpenFile function
75+
f.getACall() = call and
76+
// get the flags expression used for opening the file
77+
call.getArgument(1) = flags and
78+
// extract individual flags from the argument
79+
// flag = flag.getAChild*() and
80+
flag = getConstants(flags.asExpr()) and
81+
// check for one which signals that the handle will be writable
82+
// note that we are underestimating here, since the flags may be
83+
// specified elsewhere
84+
isWritable(flag.getTarget())
85+
)
86+
}
87+
88+
/**
89+
* Determines whether `os.File.Close` is called on `sink`.
90+
*/
91+
predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode call) {
92+
exists(CloseFileFun f |
93+
// find calls to the os.File.Close function
94+
f.getACall() = call and
95+
// that are deferred
96+
unhandledCall(call) and
97+
// where the function is called on the sink
98+
call.getReceiver() = sink
99+
)
100+
}
101+
102+
/**
103+
* Determines whether `os.File.Sync` is called on `sink` and the result of the call is neither
104+
* deferred nor discarded.
105+
*/
106+
predicate isHandledSync(DataFlow::Node sink, DataFlow::CallNode syncCall) {
107+
// find a call of the `os.File.Sync` function
108+
syncCall = any(SyncFileFun f).getACall() and
109+
// match the sink with the object on which the method is called
110+
syncCall.getReceiver() = sink and
111+
// check that the result is neither deferred nor discarded
112+
not unhandledCall(syncCall)
113+
}
114+
115+
/**
116+
* A data flow configuration which traces writable file handles resulting from calls to
117+
* `os.OpenFile` to `os.File.Close` calls on them.
118+
*/
119+
class UnhandledFileCloseDataFlowConfiguration extends DataFlow::Configuration {
120+
UnhandledFileCloseDataFlowConfiguration() { this = "UnhandledCloseWritableHandle" }
121+
122+
override predicate isSource(DataFlow::Node source) { isWritableFileHandle(source, _) }
123+
124+
override predicate isSink(DataFlow::Node sink) { isCloseSink(sink, _) }
125+
}
126+
127+
/**
128+
* Determines whether a `DataFlow::CallNode` is preceded by a call to `os.File.Sync`.
129+
*/
130+
predicate precededBySync(DataFlow::Node close, DataFlow::CallNode closeCall) {
131+
// using the control flow graph, try to find a call to a handled call to `os.File.Sync`
132+
// which precedes `closeCall`.
133+
exists(IR::Instruction instr, DataFlow::Node syncReceiver, DataFlow::CallNode syncCall |
134+
// find a predecessor to `closeCall` in the control flow graph
135+
instr = closeCall.asInstruction().getAPredecessor*() and
136+
// match the instruction corresponding to an `os.File.Sync` call with the predecessor
137+
syncCall.asInstruction() = instr and
138+
// check that the call to `os.File.Sync` is handled
139+
isHandledSync(syncReceiver, syncCall) and
140+
// check that `os.File.Sync` is called on the same object as `os.File.Close`
141+
exists(DataFlow::SsaNode ssa | ssa.getAUse() = close and ssa.getAUse() = syncReceiver)
142+
)
143+
}
144+
145+
from
146+
UnhandledFileCloseDataFlowConfiguration cfg, DataFlow::Node source, DataFlow::CallNode openCall,
147+
DataFlow::Node close, DataFlow::CallNode closeCall
148+
where
149+
// find data flow from an `os.OpenFile` call to an `os.File.Close` call
150+
// where the handle is writable
151+
cfg.hasFlow(source, close) and
152+
isWritableFileHandle(source, openCall) and
153+
// get the `CallNode` corresponding to the sink
154+
isCloseSink(close, closeCall) and
155+
// check that the call to `os.File.Close` is not preceded by a checked call to
156+
// `os.File.Sync`
157+
not precededBySync(close, closeCall)
158+
select close,
159+
"File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly.",
160+
openCall, openCall.toString()
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"os"
5+
)
6+
7+
func example() (exampleError error) {
8+
f, err := os.OpenFile("file.txt", os.O_WRONLY|os.O_CREATE, 0666)
9+
10+
if err != nil {
11+
return err
12+
}
13+
14+
defer func() {
15+
// try to close the file; if an error occurs, set the error returned by `example`
16+
// to that error, but only if `WriteString` didn't already set it to something first
17+
if err := f.Close(); exampleError == nil && err != nil {
18+
exampleError = err
19+
}
20+
}()
21+
22+
if _, err := f.WriteString("Hello"); err != nil {
23+
exampleError = err
24+
}
25+
26+
return
27+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: newQuery
3+
---
4+
* Added a new query, `go/unhandled-writable-file-close`, to detect instances where writable file handles are closed without appropriate checks for errors.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
| tests.go:22:8:22:8 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:9:12:9:74 | call to OpenFile | call to OpenFile |
2+
| tests.go:22:8:22:8 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:18:9:18:69 | call to OpenFile | call to OpenFile |
3+
| tests.go:27:3:27:3 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:9:12:9:74 | call to OpenFile | call to OpenFile |
4+
| tests.go:27:3:27:3 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:18:9:18:69 | call to OpenFile | call to OpenFile |
5+
| tests.go:53:3:53:3 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:9:12:9:74 | call to OpenFile | call to OpenFile |
6+
| tests.go:63:3:63:3 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:18:9:18:69 | call to OpenFile | call to OpenFile |
7+
| tests.go:119:3:119:3 | f | File handle may be writable as a result of data flow from a $@ and closing it may result in data loss upon failure, which is not handled explicitly. | tests.go:9:12:9:74 | call to OpenFile | call to OpenFile |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
InconsistentCode/UnhandledCloseWritableHandle.ql
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package test
2+
3+
import (
4+
"log"
5+
"os"
6+
)
7+
8+
func openFileWrite(filename string) (*os.File, error) {
9+
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
10+
return f, err
11+
}
12+
13+
func openFileRead(filename string) (*os.File, error) {
14+
return os.OpenFile(filename, os.O_RDONLY|os.O_CREATE, 0666)
15+
}
16+
17+
func openFileReadWrite(filename string) (*os.File, error) {
18+
return os.OpenFile(filename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0666)
19+
}
20+
21+
func closeFileDeferred(f *os.File) {
22+
defer f.Close() // NOT OK, if `f` is writable
23+
}
24+
25+
func closeFileDeferredIndirect(f *os.File) {
26+
var cont = func() {
27+
f.Close() // NOT OK, if `f` is writable
28+
}
29+
30+
defer cont()
31+
}
32+
33+
func deferredCalls() {
34+
if f, err := openFileWrite("foo.txt"); err != nil {
35+
closeFileDeferred(f) // NOT OK
36+
closeFileDeferredIndirect(f) // NOT OK
37+
}
38+
39+
if f, err := openFileRead("foo.txt"); err != nil {
40+
closeFileDeferred(f) // OK
41+
closeFileDeferredIndirect(f) // OK
42+
}
43+
44+
if f, err := openFileReadWrite("foo.txt"); err != nil {
45+
closeFileDeferred(f) // NOT OK
46+
closeFileDeferredIndirect(f) // NOT OK
47+
}
48+
}
49+
50+
func notDeferred() {
51+
if f, err := openFileWrite("foo.txt"); err != nil {
52+
// the handle is write-only and we don't check if `Close` succeeds
53+
f.Close() // NOT OK
54+
}
55+
56+
if f, err := openFileRead("foo.txt"); err != nil {
57+
// the handle is read-only, so this is ok
58+
f.Close() // OK
59+
}
60+
61+
if f, err := openFileReadWrite("foo.txt"); err != nil {
62+
// the handle is read-write and we don't check if `Close` succeeds
63+
f.Close() // NOT OK
64+
}
65+
}
66+
67+
func foo() error {
68+
if f, err := openFileWrite("foo.txt"); err != nil {
69+
// the result of the call to `Close` is returned to the caller
70+
return f.Close() // OK
71+
}
72+
73+
return nil
74+
}
75+
76+
func isSyncedFirst() {
77+
if f, err := openFileWrite("foo.txt"); err != nil {
78+
// we have a call to `Sync` and check whether it was successful before proceeding
79+
if err := f.Sync(); err != nil {
80+
f.Close() // OK
81+
}
82+
f.Close() // OK
83+
}
84+
}
85+
86+
func deferredCloseWithSync() {
87+
if f, err := openFileWrite("foo.txt"); err != nil {
88+
// a call to `Close` is deferred, but we have a call to `Sync` later which
89+
// precedes the call to `Close` during execution
90+
defer f.Close() // OK
91+
92+
if err := f.Sync(); err != nil {
93+
log.Fatal(err)
94+
}
95+
}
96+
}
97+
98+
func deferredCloseWithSyncEarlyReturn(n int) {
99+
if f, err := openFileWrite("foo.txt"); err != nil {
100+
// a call to `Close` is deferred
101+
defer f.Close() // NOT OK - false negative
102+
103+
if n > 100 {
104+
return
105+
}
106+
107+
// we have a call to `Sync` here, but it might not get executed if n <= 100
108+
if err := f.Sync(); err != nil {
109+
log.Fatal(err)
110+
}
111+
}
112+
}
113+
114+
func unhandledSync() {
115+
if f, err := openFileWrite("foo.txt"); err != nil {
116+
// we have a call to `Sync` which precedes the call to `Close`, but there is no check
117+
// to see if `Sync` may have failed
118+
f.Sync()
119+
f.Close() // NOT OK
120+
}
121+
}

0 commit comments

Comments
 (0)