Skip to content

Commit f67d0ea

Browse files
committed
Go: Account for deferred Close in writable-file-close query
A deferred Close runs at function exit, but the CFG splices it in at the exit node where it can be reached along paths that never execute Sync. The previous dominance check therefore produced a false positive when a statement followed the if-block that registered the defer (e.g. deferredCloseWithSync2). For deferred closes, require instead that a handled Sync post-dominates the point where the defer is registered, which guarantees Sync runs before Close on every path on which Close is registered. Non-deferred closes keep the existing dominance check.
1 parent 5217ede commit f67d0ea

1 file changed

Lines changed: 38 additions & 9 deletions

File tree

go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,25 @@ predicate isWritableFileHandle(DataFlow::Node source, DataFlow::CallNode call) {
8686
}
8787

8888
/**
89-
* Holds if `os.File.Close` is called on `sink`.
89+
* Holds if `postDominator` post-dominates `node` in the control-flow graph. That is,
90+
* every path from `node` to the exit of the enclosing function passes through
91+
* `postDominator`.
92+
*/
93+
pragma[inline]
94+
predicate postDominatesNode(ControlFlow::Node postDominator, ControlFlow::Node node) {
95+
exists(ReachableBasicBlock pdbb, ReachableBasicBlock nbb, int i, int j |
96+
postDominator = pdbb.getNode(i) and node = nbb.getNode(j)
97+
|
98+
pdbb.strictlyPostDominates(nbb)
99+
or
100+
pdbb = nbb and i >= j
101+
)
102+
}
103+
104+
/**
105+
* Holds if `closeCall` is an unhandled call to `os.File.Close` on `sink` that is not
106+
* guaranteed to be preceded during execution by a handled call to `os.File.Sync` on the
107+
* same file handle.
90108
*/
91109
predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) {
92110
// find calls to the os.File.Close function
@@ -95,18 +113,29 @@ predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) {
95113
unhandledCall(closeCall) and
96114
// where the function is called on the sink
97115
closeCall.getReceiver() = sink and
98-
// and check that it is not dominated by a call to `os.File.Sync`.
99-
// TODO: fix this logic when `closeCall` is in a defer statement.
100-
not exists(IR::Instruction syncInstr, DataFlow::Node syncReceiver, DataFlow::CallNode syncCall |
101-
// match the instruction corresponding to an `os.File.Sync` call with the predecessor
102-
syncCall.asInstruction() = syncInstr and
116+
// and check that the call to `os.File.Close` is not guaranteed to be preceded during
117+
// execution by a handled call to `os.File.Sync` on the same file handle.
118+
not exists(DataFlow::Node syncReceiver, DataFlow::CallNode syncCall |
103119
// check that the call to `os.File.Sync` is handled
104120
isHandledSync(syncReceiver, syncCall) and
105-
// find a predecessor to `closeCall` in the control flow graph which dominates the call to
106-
// `os.File.Close`
107-
syncInstr.dominatesNode(closeCall.asInstruction()) and
108121
// check that `os.File.Sync` is called on the same object as `os.File.Close`
109122
exists(DataFlow::SsaNode ssa | ssa.getAUse() = sink and ssa.getAUse() = syncReceiver)
123+
|
124+
if exists(DeferStmt defer | defer.getCall() = closeCall.asExpr())
125+
then
126+
// When the call to `os.File.Close` is deferred it runs when the enclosing function
127+
// returns, but the receiver of the deferred call is evaluated where the `defer`
128+
// statement appears. It is therefore enough for the handled call to `os.File.Sync`
129+
// to post-dominate that point, since that guarantees `os.File.Sync` runs before the
130+
// deferred `os.File.Close` on every path on which the `os.File.Close` is registered.
131+
// We cannot reuse the domination check below because the control-flow graph splices
132+
// the deferred call in at the function exit, where it may be reachable along paths
133+
// that do not pass through the call to `os.File.Sync`.
134+
postDominatesNode(syncCall.asInstruction(), sink.asInstruction())
135+
else
136+
// Otherwise the call to `os.File.Close` is executed where it appears, so we require
137+
// the handled call to `os.File.Sync` to dominate it.
138+
syncCall.asInstruction().dominatesNode(closeCall.asInstruction())
110139
)
111140
}
112141

0 commit comments

Comments
 (0)