From 59908124c136a32ea18cf2b09d5a3cf0b4487931 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 12:21:38 +0100 Subject: [PATCH 01/12] Add test showing limits of DeferStmt in CFG There are paths to the exit of the function which go through the defer statement and paths which don't, so we add an optional call to the deferred function. This causes FPs in the query as it stands. --- .../UnhandledCloseWritableHandle.expected | 26 +++++++++++-------- .../UnhandledCloseWritableHandle/tests.go | 15 +++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected index 41034c55796..8649b21354f 100644 --- a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected +++ b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected @@ -5,9 +5,10 @@ | tests.go:15:3:15:3 | f | tests.go:46:5:46:76 | ... := ...[0] | tests.go:15:3:15: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:46:15:46:76 | call to OpenFile | call to OpenFile | | tests.go:57:3:57:3 | f | tests.go:55:5:55:78 | ... := ...[0] | tests.go:57:3:57: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:55:15:55:78 | call to OpenFile | call to OpenFile | | tests.go:69:3:69:3 | f | tests.go:67:5:67:76 | ... := ...[0] | tests.go:69:3:69: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:67:15:67:76 | call to OpenFile | call to OpenFile | -| tests.go:111:9:111:9 | f | tests.go:109:5:109:78 | ... := ...[0] | tests.go:111:9:111:9 | 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:109:15:109:78 | call to OpenFile | call to OpenFile | -| tests.go:130:3:130:3 | f | tests.go:126:5:126:78 | ... := ...[0] | tests.go:130:3:130: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:126:15:126:78 | call to OpenFile | call to OpenFile | -| tests.go:151:8:151:8 | f | tests.go:147:2:147:74 | ... := ...[0] | tests.go:151:8:151: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:147:12:147:74 | call to OpenFile | call to OpenFile | +| tests.go:112:9:112:9 | f | tests.go:109:5:109:78 | ... := ...[0] | tests.go:112:9:112:9 | 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:109:15:109:78 | call to OpenFile | call to OpenFile | +| tests.go:126:9:126:9 | f | tests.go:124:5:124:78 | ... := ...[0] | tests.go:126:9:126:9 | 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:124:15:124:78 | call to OpenFile | call to OpenFile | +| tests.go:145:3:145:3 | f | tests.go:141:5:141:78 | ... := ...[0] | tests.go:145:3:145: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:141:15:141:78 | call to OpenFile | call to OpenFile | +| tests.go:166:8:166:8 | f | tests.go:162:2:162:74 | ... := ...[0] | tests.go:166:8:166: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:162:12:162:74 | call to OpenFile | call to OpenFile | edges | tests.go:9:24:9:24 | definition of f | tests.go:10:8:10:8 | f | provenance | | | tests.go:13:32:13:32 | definition of f | tests.go:14:13:16:2 | capture variable f | provenance | | @@ -22,9 +23,10 @@ edges | tests.go:48:29:48:29 | f | tests.go:13:32:13:32 | definition of f | provenance | | | tests.go:55:5:55:78 | ... := ...[0] | tests.go:57:3:57:3 | f | provenance | Src:MaD:1 | | tests.go:67:5:67:76 | ... := ...[0] | tests.go:69:3:69:3 | f | provenance | Src:MaD:1 | -| tests.go:109:5:109:78 | ... := ...[0] | tests.go:111:9:111:9 | f | provenance | Src:MaD:1 | -| tests.go:126:5:126:78 | ... := ...[0] | tests.go:130:3:130:3 | f | provenance | Src:MaD:1 | -| tests.go:147:2:147:74 | ... := ...[0] | tests.go:151:8:151:8 | f | provenance | Src:MaD:1 | +| tests.go:109:5:109:78 | ... := ...[0] | tests.go:112:9:112:9 | f | provenance | Src:MaD:1 | +| tests.go:124:5:124:78 | ... := ...[0] | tests.go:126:9:126:9 | f | provenance | Src:MaD:1 | +| tests.go:141:5:141:78 | ... := ...[0] | tests.go:145:3:145:3 | f | provenance | Src:MaD:1 | +| tests.go:162:2:162:74 | ... := ...[0] | tests.go:166:8:166:8 | f | provenance | Src:MaD:1 | models | 1 | Source: os; ; false; OpenFile; ; ; ReturnValue[0]; file; manual | nodes @@ -44,9 +46,11 @@ nodes | tests.go:67:5:67:76 | ... := ...[0] | semmle.label | ... := ...[0] | | tests.go:69:3:69:3 | f | semmle.label | f | | tests.go:109:5:109:78 | ... := ...[0] | semmle.label | ... := ...[0] | -| tests.go:111:9:111:9 | f | semmle.label | f | -| tests.go:126:5:126:78 | ... := ...[0] | semmle.label | ... := ...[0] | -| tests.go:130:3:130:3 | f | semmle.label | f | -| tests.go:147:2:147:74 | ... := ...[0] | semmle.label | ... := ...[0] | -| tests.go:151:8:151:8 | f | semmle.label | f | +| tests.go:112:9:112:9 | f | semmle.label | f | +| tests.go:124:5:124:78 | ... := ...[0] | semmle.label | ... := ...[0] | +| tests.go:126:9:126:9 | f | semmle.label | f | +| tests.go:141:5:141:78 | ... := ...[0] | semmle.label | ... := ...[0] | +| tests.go:145:3:145:3 | f | semmle.label | f | +| tests.go:162:2:162:74 | ... := ...[0] | semmle.label | ... := ...[0] | +| tests.go:166:8:166:8 | f | semmle.label | f | subpaths diff --git a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go index ec74b12e5a3..5068f69eeab 100644 --- a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go +++ b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go @@ -104,6 +104,21 @@ func deferredCloseWithSync() { } } +func deferredCloseWithSync2() { + // open file for writing + if f, err := os.OpenFile("foo.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666); err != nil { // $ SPURIOUS: Source + // a call to `Close` is deferred, but we have a call to `Sync` later which + // precedes the call to `Close` during execution + defer f.Close() // $ SPURIOUS: Alert + + if err := f.Sync(); err != nil { + log.Fatal(err) + } + } + var a int + _ = a +} + func deferredCloseWithSyncEarlyReturn(n int) { // open file for writing if f, err := os.OpenFile("foo.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666); err != nil { // $ Source From 5217ede621b7a4ca5665d4dcf87c7b274666a830 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:05:59 +0100 Subject: [PATCH 02/12] Go: Tidy up comments in writable-file-close query Correct the doc for unhandledCall (it also matches expression statements where the result is discarded) and remove a stale commented-out line in isWritableFileHandle. --- go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql index 25b1c8ae8fc..72096872a62 100644 --- a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql +++ b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql @@ -55,7 +55,7 @@ class SyncFileFun extends Method { /** * Holds if a `call` to a function is "unhandled". That is, it is either - * deferred or its result is not assigned to anything. + * deferred or used as an expression statement, so that its result is discarded. * * TODO: maybe we should check that something is actually done with the result */ @@ -77,7 +77,6 @@ predicate isWritableFileHandle(DataFlow::Node source, DataFlow::CallNode call) { // get the flags expression used for opening the file call.getArgument(1) = flags and // extract individual flags from the argument - // flag = flag.getAChild*() and flag = getConstants(flags.asExpr()) and // check for one which signals that the handle will be writable // note that we are underestimating here, since the flags may be From f67d0ea9611c857fa49934615fdf7c24c9faaa1c Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:06:34 +0100 Subject: [PATCH 03/12] 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. --- .../UnhandledCloseWritableHandle.ql | 47 +++++++++++++++---- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql index 72096872a62..dd1d357a311 100644 --- a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql +++ b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql @@ -86,7 +86,25 @@ predicate isWritableFileHandle(DataFlow::Node source, DataFlow::CallNode call) { } /** - * Holds if `os.File.Close` is called on `sink`. + * Holds if `postDominator` post-dominates `node` in the control-flow graph. That is, + * every path from `node` to the exit of the enclosing function passes through + * `postDominator`. + */ +pragma[inline] +predicate postDominatesNode(ControlFlow::Node postDominator, ControlFlow::Node node) { + exists(ReachableBasicBlock pdbb, ReachableBasicBlock nbb, int i, int j | + postDominator = pdbb.getNode(i) and node = nbb.getNode(j) + | + pdbb.strictlyPostDominates(nbb) + or + pdbb = nbb and i >= j + ) +} + +/** + * Holds if `closeCall` is an unhandled call to `os.File.Close` on `sink` that is not + * guaranteed to be preceded during execution by a handled call to `os.File.Sync` on the + * same file handle. */ predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) { // find calls to the os.File.Close function @@ -95,18 +113,29 @@ predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) { unhandledCall(closeCall) and // where the function is called on the sink closeCall.getReceiver() = sink and - // and check that it is not dominated by a call to `os.File.Sync`. - // TODO: fix this logic when `closeCall` is in a defer statement. - not exists(IR::Instruction syncInstr, DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | - // match the instruction corresponding to an `os.File.Sync` call with the predecessor - syncCall.asInstruction() = syncInstr and + // and check that the call to `os.File.Close` is not guaranteed to be preceded during + // execution by a handled call to `os.File.Sync` on the same file handle. + not exists(DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | // check that the call to `os.File.Sync` is handled isHandledSync(syncReceiver, syncCall) and - // find a predecessor to `closeCall` in the control flow graph which dominates the call to - // `os.File.Close` - syncInstr.dominatesNode(closeCall.asInstruction()) and // check that `os.File.Sync` is called on the same object as `os.File.Close` exists(DataFlow::SsaNode ssa | ssa.getAUse() = sink and ssa.getAUse() = syncReceiver) + | + if exists(DeferStmt defer | defer.getCall() = closeCall.asExpr()) + then + // When the call to `os.File.Close` is deferred it runs when the enclosing function + // returns, but the receiver of the deferred call is evaluated where the `defer` + // statement appears. It is therefore enough for the handled call to `os.File.Sync` + // to post-dominate that point, since that guarantees `os.File.Sync` runs before the + // deferred `os.File.Close` on every path on which the `os.File.Close` is registered. + // We cannot reuse the domination check below because the control-flow graph splices + // the deferred call in at the function exit, where it may be reachable along paths + // that do not pass through the call to `os.File.Sync`. + postDominatesNode(syncCall.asInstruction(), sink.asInstruction()) + else + // Otherwise the call to `os.File.Close` is executed where it appears, so we require + // the handled call to `os.File.Sync` to dominate it. + syncCall.asInstruction().dominatesNode(closeCall.asInstruction()) ) } From 05e21adc53b3e08ba5253fda9be249f7d5902598 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:07:14 +0100 Subject: [PATCH 04/12] Accept test changes --- .../UnhandledCloseWritableHandle.expected | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected index 8649b21354f..4bab888f956 100644 --- a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected +++ b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected @@ -5,7 +5,6 @@ | tests.go:15:3:15:3 | f | tests.go:46:5:46:76 | ... := ...[0] | tests.go:15:3:15: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:46:15:46:76 | call to OpenFile | call to OpenFile | | tests.go:57:3:57:3 | f | tests.go:55:5:55:78 | ... := ...[0] | tests.go:57:3:57: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:55:15:55:78 | call to OpenFile | call to OpenFile | | tests.go:69:3:69:3 | f | tests.go:67:5:67:76 | ... := ...[0] | tests.go:69:3:69: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:67:15:67:76 | call to OpenFile | call to OpenFile | -| tests.go:112:9:112:9 | f | tests.go:109:5:109:78 | ... := ...[0] | tests.go:112:9:112:9 | 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:109:15:109:78 | call to OpenFile | call to OpenFile | | tests.go:126:9:126:9 | f | tests.go:124:5:124:78 | ... := ...[0] | tests.go:126:9:126:9 | 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:124:15:124:78 | call to OpenFile | call to OpenFile | | tests.go:145:3:145:3 | f | tests.go:141:5:141:78 | ... := ...[0] | tests.go:145:3:145: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:141:15:141:78 | call to OpenFile | call to OpenFile | | tests.go:166:8:166:8 | f | tests.go:162:2:162:74 | ... := ...[0] | tests.go:166:8:166: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:162:12:162:74 | call to OpenFile | call to OpenFile | @@ -23,7 +22,6 @@ edges | tests.go:48:29:48:29 | f | tests.go:13:32:13:32 | definition of f | provenance | | | tests.go:55:5:55:78 | ... := ...[0] | tests.go:57:3:57:3 | f | provenance | Src:MaD:1 | | tests.go:67:5:67:76 | ... := ...[0] | tests.go:69:3:69:3 | f | provenance | Src:MaD:1 | -| tests.go:109:5:109:78 | ... := ...[0] | tests.go:112:9:112:9 | f | provenance | Src:MaD:1 | | tests.go:124:5:124:78 | ... := ...[0] | tests.go:126:9:126:9 | f | provenance | Src:MaD:1 | | tests.go:141:5:141:78 | ... := ...[0] | tests.go:145:3:145:3 | f | provenance | Src:MaD:1 | | tests.go:162:2:162:74 | ... := ...[0] | tests.go:166:8:166:8 | f | provenance | Src:MaD:1 | @@ -45,8 +43,6 @@ nodes | tests.go:57:3:57:3 | f | semmle.label | f | | tests.go:67:5:67:76 | ... := ...[0] | semmle.label | ... := ...[0] | | tests.go:69:3:69:3 | f | semmle.label | f | -| tests.go:109:5:109:78 | ... := ...[0] | semmle.label | ... := ...[0] | -| tests.go:112:9:112:9 | f | semmle.label | f | | tests.go:124:5:124:78 | ... := ...[0] | semmle.label | ... := ...[0] | | tests.go:126:9:126:9 | f | semmle.label | f | | tests.go:141:5:141:78 | ... := ...[0] | semmle.label | ... := ...[0] | @@ -54,3 +50,6 @@ nodes | tests.go:162:2:162:74 | ... := ...[0] | semmle.label | ... := ...[0] | | tests.go:166:8:166:8 | f | semmle.label | f | subpaths +testFailures +| tests.go:109:94:109:114 | comment | Fixed spurious result: Source | +| tests.go:112:19:112:38 | comment | Fixed spurious result: Alert | From c87bfd5f285581859d1c07ecfdcd64f5d3c0db31 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 12:59:03 +0100 Subject: [PATCH 05/12] Remove redundant call to `isCloseSink` --- go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql index dd1d357a311..92ba8b9d695 100644 --- a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql +++ b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql @@ -176,14 +176,12 @@ import UnhandledFileCloseFlow::PathGraph from UnhandledFileCloseFlow::PathNode source, DataFlow::CallNode openCall, - UnhandledFileCloseFlow::PathNode sink, DataFlow::CallNode closeCall + UnhandledFileCloseFlow::PathNode sink where // find data flow from an `os.OpenFile` call to an `os.File.Close` call // where the handle is writable UnhandledFileCloseFlow::flowPath(source, sink) and - isWritableFileHandle(source.getNode(), openCall) and - // get the `CallNode` corresponding to the sink - isCloseSink(sink.getNode(), closeCall) + isWritableFileHandle(source.getNode(), openCall) select sink, source, sink, "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.", openCall, openCall.toString() From 101812310cad17097bdf1d4ab11f2d032e0537ee Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:39:24 +0100 Subject: [PATCH 06/12] Inline `isCloseCall` into `isSink` --- .../UnhandledCloseWritableHandle.ql | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql index 92ba8b9d695..c37d33bc9ee 100644 --- a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql +++ b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql @@ -101,44 +101,6 @@ predicate postDominatesNode(ControlFlow::Node postDominator, ControlFlow::Node n ) } -/** - * Holds if `closeCall` is an unhandled call to `os.File.Close` on `sink` that is not - * guaranteed to be preceded during execution by a handled call to `os.File.Sync` on the - * same file handle. - */ -predicate isCloseSink(DataFlow::Node sink, DataFlow::CallNode closeCall) { - // find calls to the os.File.Close function - closeCall = any(CloseFileFun f).getACall() and - // that are unhandled - unhandledCall(closeCall) and - // where the function is called on the sink - closeCall.getReceiver() = sink and - // and check that the call to `os.File.Close` is not guaranteed to be preceded during - // execution by a handled call to `os.File.Sync` on the same file handle. - not exists(DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | - // check that the call to `os.File.Sync` is handled - isHandledSync(syncReceiver, syncCall) and - // check that `os.File.Sync` is called on the same object as `os.File.Close` - exists(DataFlow::SsaNode ssa | ssa.getAUse() = sink and ssa.getAUse() = syncReceiver) - | - if exists(DeferStmt defer | defer.getCall() = closeCall.asExpr()) - then - // When the call to `os.File.Close` is deferred it runs when the enclosing function - // returns, but the receiver of the deferred call is evaluated where the `defer` - // statement appears. It is therefore enough for the handled call to `os.File.Sync` - // to post-dominate that point, since that guarantees `os.File.Sync` runs before the - // deferred `os.File.Close` on every path on which the `os.File.Close` is registered. - // We cannot reuse the domination check below because the control-flow graph splices - // the deferred call in at the function exit, where it may be reachable along paths - // that do not pass through the call to `os.File.Sync`. - postDominatesNode(syncCall.asInstruction(), sink.asInstruction()) - else - // Otherwise the call to `os.File.Close` is executed where it appears, so we require - // the handled call to `os.File.Sync` to dominate it. - syncCall.asInstruction().dominatesNode(closeCall.asInstruction()) - ) -} - /** * Holds if `os.File.Sync` is called on `sink` and the result of the call is neither * deferred nor discarded. @@ -155,7 +117,43 @@ predicate isHandledSync(DataFlow::Node sink, DataFlow::CallNode syncCall) { module UnhandledFileCloseConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { isWritableFileHandle(source, _) } - predicate isSink(DataFlow::Node sink) { isCloseSink(sink, _) } + predicate isSink(DataFlow::Node sink) { + // `closeCall` is an unhandled call to `os.File.Close` on `sink` that is not + // guaranteed to be preceded during execution by a handled call to `os.File.Sync` on the + // same file handle. + exists(DataFlow::CallNode closeCall | + // find calls to the os.File.Close function + closeCall = any(CloseFileFun f).getACall() and + // that are unhandled + unhandledCall(closeCall) and + // where the function is called on the sink + closeCall.getReceiver() = sink and + // and check that the call to `os.File.Close` is not guaranteed to be preceded during + // execution by a handled call to `os.File.Sync` on the same file handle. + not exists(DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | + // check that the call to `os.File.Sync` is handled + isHandledSync(syncReceiver, syncCall) and + // check that `os.File.Sync` is called on the same object as `os.File.Close` + exists(DataFlow::SsaNode ssa | ssa.getAUse() = sink and ssa.getAUse() = syncReceiver) + | + if exists(DeferStmt defer | defer.getCall() = closeCall.asExpr()) + then + // When the call to `os.File.Close` is deferred it runs when the enclosing function + // returns, but the receiver of the deferred call is evaluated where the `defer` + // statement appears. It is therefore enough for the handled call to `os.File.Sync` + // to post-dominate that point, since that guarantees `os.File.Sync` runs before the + // deferred `os.File.Close` on every path on which the `os.File.Close` is registered. + // We cannot reuse the domination check below because the control-flow graph splices + // the deferred call in at the function exit, where it may be reachable along paths + // that do not pass through the call to `os.File.Sync`. + postDominatesNode(syncCall.asInstruction(), sink.asInstruction()) + else + // Otherwise the call to `os.File.Close` is executed where it appears, so we require + // the handled call to `os.File.Sync` to dominate it. + syncCall.asInstruction().dominatesNode(closeCall.asInstruction()) + ) + ) + } predicate observeDiffInformedIncrementalMode() { any() } From 50e03549113cb37a5309594b5f2c66ddf2593a0a Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:14:58 +0100 Subject: [PATCH 07/12] Tidy up comments in `isSink` --- .../InconsistentCode/UnhandledCloseWritableHandle.ql | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql index c37d33bc9ee..778d90a82ae 100644 --- a/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql +++ b/go/ql/src/InconsistentCode/UnhandledCloseWritableHandle.ql @@ -118,17 +118,13 @@ module UnhandledFileCloseConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { isWritableFileHandle(source, _) } predicate isSink(DataFlow::Node sink) { - // `closeCall` is an unhandled call to `os.File.Close` on `sink` that is not - // guaranteed to be preceded during execution by a handled call to `os.File.Sync` on the - // same file handle. exists(DataFlow::CallNode closeCall | - // find calls to the os.File.Close function + // `closeCall` is an unhandled call to `os.File.Close` on `sink` closeCall = any(CloseFileFun f).getACall() and - // that are unhandled unhandledCall(closeCall) and - // where the function is called on the sink - closeCall.getReceiver() = sink and - // and check that the call to `os.File.Close` is not guaranteed to be preceded during + closeCall.getReceiver() = sink + | + // `closeCall` is not guaranteed to be preceded during // execution by a handled call to `os.File.Sync` on the same file handle. not exists(DataFlow::Node syncReceiver, DataFlow::CallNode syncCall | // check that the call to `os.File.Sync` is handled From 14e3ee2fb0b482451a9208a388c247a4735eb606 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:24:31 +0100 Subject: [PATCH 08/12] Add change note --- .../change-notes/2026-06-04-unhandled-writable-file-close.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 go/ql/src/change-notes/2026-06-04-unhandled-writable-file-close.md diff --git a/go/ql/src/change-notes/2026-06-04-unhandled-writable-file-close.md b/go/ql/src/change-notes/2026-06-04-unhandled-writable-file-close.md new file mode 100644 index 00000000000..f2da5d217f8 --- /dev/null +++ b/go/ql/src/change-notes/2026-06-04-unhandled-writable-file-close.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The query `go/unhandled-writable-file-close` ("Writable file handle closed without error handling") now produces fewer false positives. A deferred call to `Close` that is preceded on every execution path by a handled call to `Sync` on the same file handle is no longer flagged. From c170002fb1e31311a7bf9a4f041d9ae8ad30c362 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 4 Jun 2026 13:52:05 +0100 Subject: [PATCH 09/12] Update test output --- .../UnhandledCloseWritableHandle.expected | 3 --- .../InconsistentCode/UnhandledCloseWritableHandle/tests.go | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected index 4bab888f956..faf2702011f 100644 --- a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected +++ b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/UnhandledCloseWritableHandle.expected @@ -50,6 +50,3 @@ nodes | tests.go:162:2:162:74 | ... := ...[0] | semmle.label | ... := ...[0] | | tests.go:166:8:166:8 | f | semmle.label | f | subpaths -testFailures -| tests.go:109:94:109:114 | comment | Fixed spurious result: Source | -| tests.go:112:19:112:38 | comment | Fixed spurious result: Alert | diff --git a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go index 5068f69eeab..94027e18d9d 100644 --- a/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go +++ b/go/ql/test/query-tests/InconsistentCode/UnhandledCloseWritableHandle/tests.go @@ -106,10 +106,10 @@ func deferredCloseWithSync() { func deferredCloseWithSync2() { // open file for writing - if f, err := os.OpenFile("foo.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666); err != nil { // $ SPURIOUS: Source + if f, err := os.OpenFile("foo.txt", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666); err != nil { // a call to `Close` is deferred, but we have a call to `Sync` later which // precedes the call to `Close` during execution - defer f.Close() // $ SPURIOUS: Alert + defer f.Close() if err := f.Sync(); err != nil { log.Fatal(err) From 17b9a668957cfa164cad085cd16b4ba0fcc77311 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 12 Jun 2026 10:17:12 +0200 Subject: [PATCH 10/12] Make alert coverage team the code owners for `/actions/` --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 131fb1e767d..f7b622c2709 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,7 +2,7 @@ * @github/code-scanning-alert-coverage # CodeQL language libraries -/actions/ @github/codeql-dynamic +/actions/ @github/code-scanning-alert-coverage /cpp/ @github/codeql-c-analysis /csharp/ @github/codeql-csharp /csharp/autobuilder/Semmle.Autobuild.Cpp @github/codeql-c-extractor @github/code-scanning-language-coverage From ef67311af296a07932ffeb6dd22b355d746adffc Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Tue, 16 Jun 2026 10:56:32 +0200 Subject: [PATCH 11/12] Swift: Filter more clang options not recognized by off-the-shelf clang --- swift/tools/tracing-config.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/swift/tools/tracing-config.lua b/swift/tools/tracing-config.lua index a29e7b3b953..22ce71b2e78 100644 --- a/swift/tools/tracing-config.lua +++ b/swift/tools/tracing-config.lua @@ -54,6 +54,8 @@ function RegisterExtractorPack(id) strip_unsupported_arg(args, '-experimental-skip-non-inlinable-function-bodies-without-types', 0) strip_unsupported_clang_arg(args, '-ivfsstatcache', 1) strip_unsupported_clang_arg(args, '-fno-odr-hash-protocols', 0) + strip_unsupported_clang_arg(args, '-fobjc-msgsend-selector-stubs', 0) + strip_unsupported_clang_arg(args, '-fstack-check', 0) strip_unsupported_clang_arg(args, '-clang-vendor-feature=+disableNonDependentMemberExprInCurrentInstantiation', 0) strip_unsupported_clang_arg(args, '-clang-vendor-feature=+enableAggressiveVLAFolding', 0) strip_unsupported_clang_arg(args, '-clang-vendor-feature=+revert09abecef7bbf', 0) From 2eb9c54456c204232896e9f051489667d717f8dd Mon Sep 17 00:00:00 2001 From: Jeroen Ketema Date: Tue, 16 Jun 2026 15:38:41 +0200 Subject: [PATCH 12/12] Swift: Update test to ensure stabilitry across Xcode versions --- swift/ql/integration-tests/osx/hello-xcode/Files.ql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/swift/ql/integration-tests/osx/hello-xcode/Files.ql b/swift/ql/integration-tests/osx/hello-xcode/Files.ql index 0ef87821564..1151ff0bb9b 100644 --- a/swift/ql/integration-tests/osx/hello-xcode/Files.ql +++ b/swift/ql/integration-tests/osx/hello-xcode/Files.ql @@ -1,5 +1,7 @@ import swift from File f -where exists(f.getRelativePath()) or f instanceof UnknownFile +where + (exists(f.getRelativePath()) or f instanceof UnknownFile) and + not f.getBaseName() = "" select f