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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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 f4dc86e645ff20f7c42ff35ec28f2bbd3f8b3009 Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Thu, 4 Jun 2026 18:18:08 +0100 Subject: [PATCH 10/21] Correct query metadata for `actions/untrusted-checkout/medium` --- .../CWE-829/UntrustedCheckoutMedium.ql | 10 +++++----- ...6-04-untrusted-checkout-medium-metadata.md | 4 ++++ .../CWE-829/UntrustedCheckoutMedium.expected | 20 +++++++++---------- 3 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 actions/ql/src/change-notes/2026-06-04-untrusted-checkout-medium-metadata.md diff --git a/actions/ql/src/Security/CWE-829/UntrustedCheckoutMedium.ql b/actions/ql/src/Security/CWE-829/UntrustedCheckoutMedium.ql index ca68c7fffd1..fc4b8b11257 100644 --- a/actions/ql/src/Security/CWE-829/UntrustedCheckoutMedium.ql +++ b/actions/ql/src/Security/CWE-829/UntrustedCheckoutMedium.ql @@ -1,8 +1,8 @@ /** - * @name Checkout of untrusted code in a trusted context - * @description Privileged workflows have read/write access to the base repository and access to secrets. - * By explicitly checking out and running the build script from a fork the untrusted code is running in an environment - * that is able to push to the base repository and to access secrets. + * @name Checkout of untrusted code in a non-privileged context + * @description Checking out and running the build script from a fork executes untrusted code. Even in a + * non-privileged workflow, this can be abused, for example to compromise self-hosted runners + * or to poison caches and artifacts that are later consumed by privileged workflows. * @kind problem * @problem.severity warning * @precision medium @@ -20,4 +20,4 @@ from PRHeadCheckoutStep checkout where // the checkout occurs in a non-privileged context inNonPrivilegedContext(checkout) -select checkout, "Potential unsafe checkout of untrusted pull request on privileged workflow." +select checkout, "Potential unsafe checkout of untrusted pull request on non-privileged workflow." diff --git a/actions/ql/src/change-notes/2026-06-04-untrusted-checkout-medium-metadata.md b/actions/ql/src/change-notes/2026-06-04-untrusted-checkout-medium-metadata.md new file mode 100644 index 00000000000..cb082fc63a5 --- /dev/null +++ b/actions/ql/src/change-notes/2026-06-04-untrusted-checkout-medium-metadata.md @@ -0,0 +1,4 @@ +--- +category: queryMetadata +--- +* The name, description, and alert message of `actions/untrusted-checkout/medium` have been corrected to describe a non-privileged context. diff --git a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected index 2b9bf3f2b79..cb5e652d560 100644 --- a/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected +++ b/actions/ql/test/query-tests/Security/CWE-829/UntrustedCheckoutMedium.expected @@ -1,10 +1,10 @@ -| .github/workflows/artifactpoisoning81.yml:11:9:14:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/dependabot2.yml:33:9:38:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/mend.yml:22:9:29:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/poc3.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/poc.yml:30:9:36:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/priv_pull_request_checkout.yml:14:9:20:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/test3.yml:28:9:33:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/test4.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/test8.yml:20:9:26:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | -| .github/workflows/test9.yml:11:9:16:6 | Uses Step | Potential unsafe checkout of untrusted pull request on privileged workflow. | +| .github/workflows/artifactpoisoning81.yml:11:9:14:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/dependabot2.yml:33:9:38:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/mend.yml:22:9:29:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/poc3.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/poc.yml:30:9:36:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/priv_pull_request_checkout.yml:14:9:20:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/test3.yml:28:9:33:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/test4.yml:18:7:25:4 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/test8.yml:20:9:26:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | +| .github/workflows/test9.yml:11:9:16:6 | Uses Step | Potential unsafe checkout of untrusted pull request on non-privileged workflow. | From c241049384a6399ff447ccdf5c16e609ebb37e89 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 15:26:19 +0100 Subject: [PATCH 11/21] Add control flow test for result read steps --- .../ControlFlowNode_getASuccessor.expected | 252 ++++++++++-------- .../go/controlflow/ControlFlowGraph/main.go | 10 + 2 files changed, 148 insertions(+), 114 deletions(-) diff --git a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/ControlFlowNode_getASuccessor.expected b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/ControlFlowNode_getASuccessor.expected index f3d27b4bf38..f14d1319795 100644 --- a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/ControlFlowNode_getASuccessor.expected +++ b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/ControlFlowNode_getASuccessor.expected @@ -735,129 +735,153 @@ | main.go:48:11:48:12 | 42 | main.go:48:2:48:7 | assignment to result | | main.go:49:2:49:7 | return statement | main.go:47:13:47:18 | implicit read of result | | main.go:52:1:54:1 | entry | main.go:52:14:52:19 | zero value for result | -| main.go:52:1:54:1 | function declaration | main.go:56:6:56:10 | skip | +| main.go:52:1:54:1 | function declaration | main.go:56:6:56:9 | skip | | main.go:52:6:52:9 | skip | main.go:52:1:54:1 | function declaration | | main.go:52:14:52:19 | implicit read of result | main.go:52:1:54:1 | exit | | main.go:52:14:52:19 | initialization of result | main.go:53:2:53:7 | return statement | | main.go:52:14:52:19 | zero value for result | main.go:52:14:52:19 | initialization of result | | main.go:53:2:53:7 | return statement | main.go:52:14:52:19 | implicit read of result | -| main.go:56:1:80:1 | entry | main.go:57:6:57:6 | skip | -| main.go:56:1:80:1 | function declaration | main.go:82:6:82:13 | skip | -| main.go:56:6:56:10 | skip | main.go:56:1:80:1 | function declaration | -| main.go:57:6:57:6 | assignment to x | main.go:58:6:58:9 | cond | -| main.go:57:6:57:6 | skip | main.go:57:6:57:6 | zero value for x | -| main.go:57:6:57:6 | zero value for x | main.go:57:6:57:6 | assignment to x | -| main.go:58:6:58:9 | cond | main.go:58:6:58:11 | call to cond | -| main.go:58:6:58:11 | call to cond | main.go:56:1:80:1 | exit | -| main.go:58:6:58:11 | call to cond | main.go:58:6:58:11 | call to cond is false | -| main.go:58:6:58:11 | call to cond | main.go:58:6:58:11 | call to cond is true | -| main.go:58:6:58:11 | call to cond is false | main.go:61:2:61:10 | selection of Print | -| main.go:58:6:58:11 | call to cond is true | main.go:59:3:59:3 | skip | -| main.go:59:3:59:3 | assignment to x | main.go:58:6:58:9 | cond | -| main.go:59:3:59:3 | skip | main.go:59:7:59:7 | 2 | -| main.go:59:7:59:7 | 2 | main.go:59:3:59:3 | assignment to x | -| main.go:61:2:61:10 | selection of Print | main.go:61:12:61:12 | x | -| main.go:61:2:61:13 | call to Print | main.go:56:1:80:1 | exit | -| main.go:61:2:61:13 | call to Print | main.go:63:2:63:2 | skip | -| main.go:61:12:61:12 | x | main.go:61:2:61:13 | call to Print | -| main.go:63:2:63:2 | assignment to y | main.go:64:6:64:6 | skip | -| main.go:63:2:63:2 | skip | main.go:63:7:63:7 | 1 | -| main.go:63:7:63:7 | 1 | main.go:63:2:63:2 | assignment to y | -| main.go:64:6:64:6 | assignment to i | main.go:65:6:65:9 | cond | -| main.go:64:6:64:6 | skip | main.go:64:11:64:11 | 0 | -| main.go:64:11:64:11 | 0 | main.go:64:6:64:6 | assignment to i | -| main.go:64:16:64:16 | i | main.go:64:16:64:18 | 1 | -| main.go:64:16:64:18 | 1 | main.go:64:16:64:18 | rhs of increment statement | -| main.go:64:16:64:18 | increment statement | main.go:65:6:65:9 | cond | -| main.go:64:16:64:18 | rhs of increment statement | main.go:64:16:64:18 | increment statement | -| main.go:65:6:65:9 | cond | main.go:65:6:65:11 | call to cond | -| main.go:65:6:65:11 | call to cond | main.go:56:1:80:1 | exit | -| main.go:65:6:65:11 | call to cond | main.go:65:6:65:11 | call to cond is false | -| main.go:65:6:65:11 | call to cond | main.go:65:6:65:11 | call to cond is true | -| main.go:65:6:65:11 | call to cond is false | main.go:68:3:68:3 | skip | -| main.go:65:6:65:11 | call to cond is true | main.go:66:4:66:8 | skip | -| main.go:66:4:66:8 | skip | main.go:70:2:70:10 | selection of Print | -| main.go:68:3:68:3 | assignment to y | main.go:64:16:64:16 | i | -| main.go:68:3:68:3 | skip | main.go:68:7:68:7 | 2 | -| main.go:68:7:68:7 | 2 | main.go:68:3:68:3 | assignment to y | -| main.go:70:2:70:10 | selection of Print | main.go:70:12:70:12 | y | -| main.go:70:2:70:13 | call to Print | main.go:56:1:80:1 | exit | -| main.go:70:2:70:13 | call to Print | main.go:72:2:72:2 | skip | -| main.go:70:12:70:12 | y | main.go:70:2:70:13 | call to Print | -| main.go:72:2:72:2 | assignment to z | main.go:73:6:73:6 | skip | -| main.go:72:2:72:2 | skip | main.go:72:7:72:7 | 1 | -| main.go:72:7:72:7 | 1 | main.go:72:2:72:2 | assignment to z | -| main.go:73:6:73:6 | assignment to i | main.go:74:3:74:3 | skip | -| main.go:73:6:73:6 | skip | main.go:73:11:73:11 | 0 | -| main.go:73:11:73:11 | 0 | main.go:73:6:73:6 | assignment to i | -| main.go:73:16:73:16 | i | main.go:73:16:73:18 | 1 | -| main.go:73:16:73:18 | 1 | main.go:73:16:73:18 | rhs of increment statement | -| main.go:73:16:73:18 | increment statement | main.go:74:3:74:3 | skip | -| main.go:73:16:73:18 | rhs of increment statement | main.go:73:16:73:18 | increment statement | -| main.go:74:3:74:3 | assignment to z | main.go:75:6:75:9 | cond | -| main.go:74:3:74:3 | skip | main.go:74:7:74:7 | 2 | -| main.go:74:7:74:7 | 2 | main.go:74:3:74:3 | assignment to z | +| main.go:56:1:64:1 | entry | main.go:56:11:56:18 | argument corresponding to selector | +| main.go:56:1:64:1 | function declaration | main.go:66:6:66:10 | skip | +| main.go:56:6:56:9 | skip | main.go:56:1:64:1 | function declaration | +| main.go:56:11:56:18 | argument corresponding to selector | main.go:56:11:56:18 | initialization of selector | +| main.go:56:11:56:18 | initialization of selector | main.go:56:26:56:31 | zero value for result | +| main.go:56:26:56:31 | implicit read of result | main.go:56:1:64:1 | exit | +| main.go:56:26:56:31 | initialization of result | main.go:57:2:57:7 | skip | +| main.go:56:26:56:31 | zero value for result | main.go:56:26:56:31 | initialization of result | +| main.go:57:2:57:7 | assignment to result | main.go:58:5:58:12 | selector | +| main.go:57:2:57:7 | skip | main.go:57:11:57:11 | 0 | +| main.go:57:11:57:11 | 0 | main.go:57:2:57:7 | assignment to result | +| main.go:58:5:58:12 | selector | main.go:58:17:58:17 | 1 | +| main.go:58:5:58:17 | ...==... | main.go:58:5:58:17 | ...==... is false | +| main.go:58:5:58:17 | ...==... | main.go:58:5:58:17 | ...==... is true | +| main.go:58:5:58:17 | ...==... is false | main.go:61:3:61:8 | skip | +| main.go:58:5:58:17 | ...==... is true | main.go:59:10:59:10 | 1 | +| main.go:58:17:58:17 | 1 | main.go:58:5:58:17 | ...==... | +| main.go:59:3:59:10 | return statement | main.go:56:26:56:31 | implicit read of result | +| main.go:59:10:59:10 | 1 | main.go:59:10:59:10 | implicit write of result | +| main.go:59:10:59:10 | implicit write of result | main.go:59:3:59:10 | return statement | +| main.go:61:3:61:8 | assignment to result | main.go:63:2:63:7 | return statement | +| main.go:61:3:61:8 | skip | main.go:61:12:61:12 | 2 | +| main.go:61:12:61:12 | 2 | main.go:61:3:61:8 | assignment to result | +| main.go:63:2:63:7 | return statement | main.go:56:26:56:31 | implicit read of result | +| main.go:66:1:90:1 | entry | main.go:67:6:67:6 | skip | +| main.go:66:1:90:1 | function declaration | main.go:92:6:92:13 | skip | +| main.go:66:6:66:10 | skip | main.go:66:1:90:1 | function declaration | +| main.go:67:6:67:6 | assignment to x | main.go:68:6:68:9 | cond | +| main.go:67:6:67:6 | skip | main.go:67:6:67:6 | zero value for x | +| main.go:67:6:67:6 | zero value for x | main.go:67:6:67:6 | assignment to x | +| main.go:68:6:68:9 | cond | main.go:68:6:68:11 | call to cond | +| main.go:68:6:68:11 | call to cond | main.go:66:1:90:1 | exit | +| main.go:68:6:68:11 | call to cond | main.go:68:6:68:11 | call to cond is false | +| main.go:68:6:68:11 | call to cond | main.go:68:6:68:11 | call to cond is true | +| main.go:68:6:68:11 | call to cond is false | main.go:71:2:71:10 | selection of Print | +| main.go:68:6:68:11 | call to cond is true | main.go:69:3:69:3 | skip | +| main.go:69:3:69:3 | assignment to x | main.go:68:6:68:9 | cond | +| main.go:69:3:69:3 | skip | main.go:69:7:69:7 | 2 | +| main.go:69:7:69:7 | 2 | main.go:69:3:69:3 | assignment to x | +| main.go:71:2:71:10 | selection of Print | main.go:71:12:71:12 | x | +| main.go:71:2:71:13 | call to Print | main.go:66:1:90:1 | exit | +| main.go:71:2:71:13 | call to Print | main.go:73:2:73:2 | skip | +| main.go:71:12:71:12 | x | main.go:71:2:71:13 | call to Print | +| main.go:73:2:73:2 | assignment to y | main.go:74:6:74:6 | skip | +| main.go:73:2:73:2 | skip | main.go:73:7:73:7 | 1 | +| main.go:73:7:73:7 | 1 | main.go:73:2:73:2 | assignment to y | +| main.go:74:6:74:6 | assignment to i | main.go:75:6:75:9 | cond | +| main.go:74:6:74:6 | skip | main.go:74:11:74:11 | 0 | +| main.go:74:11:74:11 | 0 | main.go:74:6:74:6 | assignment to i | +| main.go:74:16:74:16 | i | main.go:74:16:74:18 | 1 | +| main.go:74:16:74:18 | 1 | main.go:74:16:74:18 | rhs of increment statement | +| main.go:74:16:74:18 | increment statement | main.go:75:6:75:9 | cond | +| main.go:74:16:74:18 | rhs of increment statement | main.go:74:16:74:18 | increment statement | | main.go:75:6:75:9 | cond | main.go:75:6:75:11 | call to cond | -| main.go:75:6:75:11 | call to cond | main.go:56:1:80:1 | exit | +| main.go:75:6:75:11 | call to cond | main.go:66:1:90:1 | exit | | main.go:75:6:75:11 | call to cond | main.go:75:6:75:11 | call to cond is false | | main.go:75:6:75:11 | call to cond | main.go:75:6:75:11 | call to cond is true | -| main.go:75:6:75:11 | call to cond is false | main.go:73:16:73:16 | i | +| main.go:75:6:75:11 | call to cond is false | main.go:78:3:78:3 | skip | | main.go:75:6:75:11 | call to cond is true | main.go:76:4:76:8 | skip | -| main.go:76:4:76:8 | skip | main.go:79:2:79:10 | selection of Print | -| main.go:79:2:79:10 | selection of Print | main.go:79:12:79:12 | z | -| main.go:79:2:79:13 | call to Print | main.go:56:1:80:1 | exit | -| main.go:79:12:79:12 | z | main.go:79:2:79:13 | call to Print | -| main.go:82:1:86:1 | entry | main.go:82:18:82:18 | zero value for a | -| main.go:82:1:86:1 | function declaration | main.go:88:6:88:23 | skip | -| main.go:82:6:82:13 | skip | main.go:82:1:86:1 | function declaration | -| main.go:82:18:82:18 | implicit read of a | main.go:82:25:82:25 | implicit read of b | -| main.go:82:18:82:18 | initialization of a | main.go:82:25:82:25 | zero value for b | -| main.go:82:18:82:18 | zero value for a | main.go:82:18:82:18 | initialization of a | -| main.go:82:25:82:25 | implicit read of b | main.go:82:1:86:1 | exit | -| main.go:82:25:82:25 | initialization of b | main.go:83:2:83:2 | skip | -| main.go:82:25:82:25 | zero value for b | main.go:82:25:82:25 | initialization of b | -| main.go:83:2:83:2 | assignment to x | main.go:84:2:84:2 | skip | -| main.go:83:2:83:2 | skip | main.go:83:7:83:8 | 23 | -| main.go:83:7:83:8 | 23 | main.go:83:2:83:2 | assignment to x | -| main.go:84:2:84:2 | assignment to x | main.go:84:5:84:5 | assignment to a | -| main.go:84:2:84:2 | skip | main.go:84:5:84:5 | skip | -| main.go:84:5:84:5 | assignment to a | main.go:85:2:85:7 | return statement | -| main.go:84:5:84:5 | skip | main.go:84:9:84:9 | x | -| main.go:84:9:84:9 | x | main.go:84:11:84:12 | 19 | -| main.go:84:9:84:12 | ...+... | main.go:84:15:84:15 | x | -| main.go:84:11:84:12 | 19 | main.go:84:9:84:12 | ...+... | -| main.go:84:15:84:15 | x | main.go:84:2:84:2 | assignment to x | -| main.go:85:2:85:7 | return statement | main.go:82:18:82:18 | implicit read of a | -| main.go:88:1:96:1 | entry | main.go:88:25:88:25 | argument corresponding to x | -| main.go:88:1:96:1 | function declaration | main.go:0:0:0:0 | exit | -| main.go:88:6:88:23 | skip | main.go:88:1:96:1 | function declaration | -| main.go:88:25:88:25 | argument corresponding to x | main.go:88:25:88:25 | initialization of x | -| main.go:88:25:88:25 | initialization of x | main.go:89:2:89:2 | skip | -| main.go:89:2:89:2 | assignment to a | main.go:89:5:89:5 | assignment to b | -| main.go:89:2:89:2 | skip | main.go:89:5:89:5 | skip | -| main.go:89:5:89:5 | assignment to b | main.go:90:5:90:8 | cond | -| main.go:89:5:89:5 | skip | main.go:89:10:89:10 | x | -| main.go:89:10:89:10 | x | main.go:89:13:89:13 | 0 | -| main.go:89:13:89:13 | 0 | main.go:89:2:89:2 | assignment to a | -| main.go:90:5:90:8 | cond | main.go:90:5:90:10 | call to cond | -| main.go:90:5:90:10 | call to cond | main.go:88:1:96:1 | exit | -| main.go:90:5:90:10 | call to cond | main.go:90:5:90:10 | call to cond is false | -| main.go:90:5:90:10 | call to cond | main.go:90:5:90:10 | call to cond is true | -| main.go:90:5:90:10 | call to cond is false | main.go:93:3:93:3 | skip | -| main.go:90:5:90:10 | call to cond is true | main.go:91:3:91:3 | skip | -| main.go:91:3:91:3 | assignment to a | main.go:95:9:95:9 | a | -| main.go:91:3:91:3 | skip | main.go:91:6:91:6 | skip | -| main.go:91:6:91:6 | skip | main.go:91:10:91:10 | b | -| main.go:91:10:91:10 | b | main.go:91:13:91:13 | a | -| main.go:91:13:91:13 | a | main.go:91:3:91:3 | assignment to a | -| main.go:93:3:93:3 | skip | main.go:93:6:93:6 | skip | -| main.go:93:6:93:6 | assignment to b | main.go:95:9:95:9 | a | -| main.go:93:6:93:6 | skip | main.go:93:10:93:10 | b | -| main.go:93:10:93:10 | b | main.go:93:13:93:13 | a | -| main.go:93:13:93:13 | a | main.go:93:6:93:6 | assignment to b | -| main.go:95:2:95:12 | return statement | main.go:88:1:96:1 | exit | -| main.go:95:9:95:9 | a | main.go:95:12:95:12 | b | -| main.go:95:12:95:12 | b | main.go:95:2:95:12 | return statement | +| main.go:76:4:76:8 | skip | main.go:80:2:80:10 | selection of Print | +| main.go:78:3:78:3 | assignment to y | main.go:74:16:74:16 | i | +| main.go:78:3:78:3 | skip | main.go:78:7:78:7 | 2 | +| main.go:78:7:78:7 | 2 | main.go:78:3:78:3 | assignment to y | +| main.go:80:2:80:10 | selection of Print | main.go:80:12:80:12 | y | +| main.go:80:2:80:13 | call to Print | main.go:66:1:90:1 | exit | +| main.go:80:2:80:13 | call to Print | main.go:82:2:82:2 | skip | +| main.go:80:12:80:12 | y | main.go:80:2:80:13 | call to Print | +| main.go:82:2:82:2 | assignment to z | main.go:83:6:83:6 | skip | +| main.go:82:2:82:2 | skip | main.go:82:7:82:7 | 1 | +| main.go:82:7:82:7 | 1 | main.go:82:2:82:2 | assignment to z | +| main.go:83:6:83:6 | assignment to i | main.go:84:3:84:3 | skip | +| main.go:83:6:83:6 | skip | main.go:83:11:83:11 | 0 | +| main.go:83:11:83:11 | 0 | main.go:83:6:83:6 | assignment to i | +| main.go:83:16:83:16 | i | main.go:83:16:83:18 | 1 | +| main.go:83:16:83:18 | 1 | main.go:83:16:83:18 | rhs of increment statement | +| main.go:83:16:83:18 | increment statement | main.go:84:3:84:3 | skip | +| main.go:83:16:83:18 | rhs of increment statement | main.go:83:16:83:18 | increment statement | +| main.go:84:3:84:3 | assignment to z | main.go:85:6:85:9 | cond | +| main.go:84:3:84:3 | skip | main.go:84:7:84:7 | 2 | +| main.go:84:7:84:7 | 2 | main.go:84:3:84:3 | assignment to z | +| main.go:85:6:85:9 | cond | main.go:85:6:85:11 | call to cond | +| main.go:85:6:85:11 | call to cond | main.go:66:1:90:1 | exit | +| main.go:85:6:85:11 | call to cond | main.go:85:6:85:11 | call to cond is false | +| main.go:85:6:85:11 | call to cond | main.go:85:6:85:11 | call to cond is true | +| main.go:85:6:85:11 | call to cond is false | main.go:83:16:83:16 | i | +| main.go:85:6:85:11 | call to cond is true | main.go:86:4:86:8 | skip | +| main.go:86:4:86:8 | skip | main.go:89:2:89:10 | selection of Print | +| main.go:89:2:89:10 | selection of Print | main.go:89:12:89:12 | z | +| main.go:89:2:89:13 | call to Print | main.go:66:1:90:1 | exit | +| main.go:89:12:89:12 | z | main.go:89:2:89:13 | call to Print | +| main.go:92:1:96:1 | entry | main.go:92:18:92:18 | zero value for a | +| main.go:92:1:96:1 | function declaration | main.go:98:6:98:23 | skip | +| main.go:92:6:92:13 | skip | main.go:92:1:96:1 | function declaration | +| main.go:92:18:92:18 | implicit read of a | main.go:92:25:92:25 | implicit read of b | +| main.go:92:18:92:18 | initialization of a | main.go:92:25:92:25 | zero value for b | +| main.go:92:18:92:18 | zero value for a | main.go:92:18:92:18 | initialization of a | +| main.go:92:25:92:25 | implicit read of b | main.go:92:1:96:1 | exit | +| main.go:92:25:92:25 | initialization of b | main.go:93:2:93:2 | skip | +| main.go:92:25:92:25 | zero value for b | main.go:92:25:92:25 | initialization of b | +| main.go:93:2:93:2 | assignment to x | main.go:94:2:94:2 | skip | +| main.go:93:2:93:2 | skip | main.go:93:7:93:8 | 23 | +| main.go:93:7:93:8 | 23 | main.go:93:2:93:2 | assignment to x | +| main.go:94:2:94:2 | assignment to x | main.go:94:5:94:5 | assignment to a | +| main.go:94:2:94:2 | skip | main.go:94:5:94:5 | skip | +| main.go:94:5:94:5 | assignment to a | main.go:95:2:95:7 | return statement | +| main.go:94:5:94:5 | skip | main.go:94:9:94:9 | x | +| main.go:94:9:94:9 | x | main.go:94:11:94:12 | 19 | +| main.go:94:9:94:12 | ...+... | main.go:94:15:94:15 | x | +| main.go:94:11:94:12 | 19 | main.go:94:9:94:12 | ...+... | +| main.go:94:15:94:15 | x | main.go:94:2:94:2 | assignment to x | +| main.go:95:2:95:7 | return statement | main.go:92:18:92:18 | implicit read of a | +| main.go:98:1:106:1 | entry | main.go:98:25:98:25 | argument corresponding to x | +| main.go:98:1:106:1 | function declaration | main.go:0:0:0:0 | exit | +| main.go:98:6:98:23 | skip | main.go:98:1:106:1 | function declaration | +| main.go:98:25:98:25 | argument corresponding to x | main.go:98:25:98:25 | initialization of x | +| main.go:98:25:98:25 | initialization of x | main.go:99:2:99:2 | skip | +| main.go:99:2:99:2 | assignment to a | main.go:99:5:99:5 | assignment to b | +| main.go:99:2:99:2 | skip | main.go:99:5:99:5 | skip | +| main.go:99:5:99:5 | assignment to b | main.go:100:5:100:8 | cond | +| main.go:99:5:99:5 | skip | main.go:99:10:99:10 | x | +| main.go:99:10:99:10 | x | main.go:99:13:99:13 | 0 | +| main.go:99:13:99:13 | 0 | main.go:99:2:99:2 | assignment to a | +| main.go:100:5:100:8 | cond | main.go:100:5:100:10 | call to cond | +| main.go:100:5:100:10 | call to cond | main.go:98:1:106:1 | exit | +| main.go:100:5:100:10 | call to cond | main.go:100:5:100:10 | call to cond is false | +| main.go:100:5:100:10 | call to cond | main.go:100:5:100:10 | call to cond is true | +| main.go:100:5:100:10 | call to cond is false | main.go:103:3:103:3 | skip | +| main.go:100:5:100:10 | call to cond is true | main.go:101:3:101:3 | skip | +| main.go:101:3:101:3 | assignment to a | main.go:105:9:105:9 | a | +| main.go:101:3:101:3 | skip | main.go:101:6:101:6 | skip | +| main.go:101:6:101:6 | skip | main.go:101:10:101:10 | b | +| main.go:101:10:101:10 | b | main.go:101:13:101:13 | a | +| main.go:101:13:101:13 | a | main.go:101:3:101:3 | assignment to a | +| main.go:103:3:103:3 | skip | main.go:103:6:103:6 | skip | +| main.go:103:6:103:6 | assignment to b | main.go:105:9:105:9 | a | +| main.go:103:6:103:6 | skip | main.go:103:10:103:10 | b | +| main.go:103:10:103:10 | b | main.go:103:13:103:13 | a | +| main.go:103:13:103:13 | a | main.go:103:6:103:6 | assignment to b | +| main.go:105:2:105:12 | return statement | main.go:98:1:106:1 | exit | +| main.go:105:9:105:9 | a | main.go:105:12:105:12 | b | +| main.go:105:12:105:12 | b | main.go:105:2:105:12 | return statement | | noretfunctions.go:0:0:0:0 | entry | noretfunctions.go:3:1:6:1 | skip | | noretfunctions.go:3:1:6:1 | skip | noretfunctions.go:8:6:8:12 | skip | | noretfunctions.go:8:1:10:1 | entry | noretfunctions.go:9:2:9:8 | selection of Exit | diff --git a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/main.go b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/main.go index 7345560670b..9bef6a909ea 100644 --- a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/main.go +++ b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/main.go @@ -53,6 +53,16 @@ func baz2() (result int) { return } +func baz3(selector int) (result int) { + result = 0 + if selector == 1 { + return 1 + } else { + result = 2 + } + return +} + func loops() { var x int for cond() { From 1c47084479ea040ecf3920019a3718542fa1a7a6 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 15:43:58 +0100 Subject: [PATCH 12/21] Add result node test with SPURIOUS result --- .../go/dataflow/Nodes/ResultNode.expected | 9 +++++++ .../semmle/go/dataflow/Nodes/ResultNode.ql | 9 +++++++ .../semmle/go/dataflow/Nodes/ResultNode.qlref | 2 ++ .../semmle/go/dataflow/Nodes/main.go | 2 +- .../go/dataflow/Nodes/resultParameters.go | 27 +++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.ql create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.qlref create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected new file mode 100644 index 00000000000..ae728dd5939 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected @@ -0,0 +1,9 @@ +| main.go:21:9:21:10 | 23 | Result node with index 0 | +| main.go:21:13:21:14 | 42 | Result node with index 1 | +| resultParameters.go:5:10:5:10 | 0 | Result node with index 0 | +| resultParameters.go:9:10:9:10 | 1 | Result node with index 0 | +| resultParameters.go:11:10:11:10 | 2 | Result node with index 0 | +| resultParameters.go:13:9:13:9 | 3 | Result node with index 0 | +| resultParameters.go:16:26:16:26 | implicit read of r | Result node with index 0 | +| resultParameters.go:21:38:21:38 | implicit read of r | Result node with index 0 | +| resultParameters.go:24:10:24:10 | 1 | Result node with index 0 | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.ql b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.ql new file mode 100644 index 00000000000..f2afd3ea430 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.ql @@ -0,0 +1,9 @@ +/** + * @kind problem + * @id result-node + */ + +import go + +from DataFlow::ResultNode r +select r, "Result node with index " + r.getIndex() diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.qlref b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.qlref new file mode 100644 index 00000000000..effcf9ce1d9 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.qlref @@ -0,0 +1,2 @@ +query: ResultNode.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/main.go b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/main.go index 1fb3466820c..dcfb9fb8c04 100644 --- a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/main.go +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/main.go @@ -18,5 +18,5 @@ func f() { } func test() (int, int) { - return 23, 42 + return 23, 42 // $ Alert[result-node] } diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go new file mode 100644 index 00000000000..af48b1be540 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go @@ -0,0 +1,27 @@ +package main + +func multipleReturns(selector int) int { + if selector == 0 { + return 0 // $ Alert[result-node] + } + switch selector { + case 1: + return 1 // $ Alert[result-node] + case 2: + return 2 // $ Alert[result-node] + } + return 3 // $ Alert[result-node] +} + +func resultParameter1() (r int) { // $ Alert[result-node] // implicit reads of result parameters are located at the result parameter declaration + r = 0 + return +} + +func resultParameter2(selector int) (r int) { // $ Alert[result-node] // implicit reads of result parameters are located at the result parameter declaration + r = 0 + if selector == 1 { + return 1 // $ SPURIOUS: Alert[result-node] + } + return +} From f4f17b01c168c77b1a48f8321d628acb4624a826 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 15:54:27 +0100 Subject: [PATCH 13/21] Fix result node and remove SPURIOUS test result --- go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll | 4 ++++ .../semmle/go/dataflow/Nodes/BinaryOperationNodes.expected | 2 ++ .../semmle/go/dataflow/Nodes/ResultNode.expected | 1 - .../semmle/go/dataflow/Nodes/resultParameters.go | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll index 603da6364df..90c21654556 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll @@ -932,6 +932,10 @@ module Public { ResultNode() { exists(FuncDef fd | + // When a function has any (named) result variables, then the + // `ReadResultInstruction`s at the end of the function are the correct + // result nodes. + not exists(fd.getAResultVar()) and exists(IR::ReturnInstruction ret | ret.getRoot() = fd | insn = ret.getResult(i)) or insn.(IR::ReadResultInstruction).reads(fd.getResultVar(i)) diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/BinaryOperationNodes.expected b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/BinaryOperationNodes.expected index 9d996bdd020..9315b269125 100644 --- a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/BinaryOperationNodes.expected +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/BinaryOperationNodes.expected @@ -2,3 +2,5 @@ | main.go:7:19:7:23 | ...+... | + | main.go:7:19:7:19 | y | main.go:7:23:7:23 | z | | main.go:10:14:10:18 | ...+... | + | main.go:10:14:10:14 | x | main.go:10:18:10:18 | y | | main.go:17:2:17:13 | ... += ... | + | main.go:17:2:17:6 | index expression | main.go:17:11:17:13 | "!" | +| resultParameters.go:4:5:4:17 | ...==... | == | resultParameters.go:4:5:4:12 | selector | resultParameters.go:4:17:4:17 | 0 | +| resultParameters.go:23:5:23:17 | ...==... | == | resultParameters.go:23:5:23:12 | selector | resultParameters.go:23:17:23:17 | 1 | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected index ae728dd5939..093fcdbdae1 100644 --- a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/ResultNode.expected @@ -6,4 +6,3 @@ | resultParameters.go:13:9:13:9 | 3 | Result node with index 0 | | resultParameters.go:16:26:16:26 | implicit read of r | Result node with index 0 | | resultParameters.go:21:38:21:38 | implicit read of r | Result node with index 0 | -| resultParameters.go:24:10:24:10 | 1 | Result node with index 0 | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go index af48b1be540..c404b819914 100644 --- a/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go +++ b/go/ql/test/library-tests/semmle/go/dataflow/Nodes/resultParameters.go @@ -21,7 +21,7 @@ func resultParameter1() (r int) { // $ Alert[result-node] // implicit reads of r func resultParameter2(selector int) (r int) { // $ Alert[result-node] // implicit reads of result parameters are located at the result parameter declaration r = 0 if selector == 1 { - return 1 // $ SPURIOUS: Alert[result-node] + return 1 } return } From da777a455d50bc9e334f4148e80b9c9c0eeedecf Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 23:09:45 +0200 Subject: [PATCH 14/21] Improve QLDoc --- go/ql/lib/semmle/go/Expr.qll | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/go/ql/lib/semmle/go/Expr.qll b/go/ql/lib/semmle/go/Expr.qll index 0dcc707b19d..d50456411fd 100644 --- a/go/ql/lib/semmle/go/Expr.qll +++ b/go/ql/lib/semmle/go/Expr.qll @@ -1049,16 +1049,28 @@ class FuncTypeExpr extends @functypeexpr, TypeExpr, ScopeNode, FieldParent { */ int getNumParameter() { result = count(this.getAParameterDecl().getANameExpr()) } - /** Gets the `i`th result of this function type (0-based). */ + /** + * Gets the `i`th result declaration of this function type (0-based). + * + * Note: `x, y int` is a single `ResultVariableDecl`. + */ ResultVariableDecl getResultDecl(int i) { result = this.getField(-(i + 1)) } - /** Gets a result of this function type. */ + /** + * Gets a result declaration of this function type. + * + * Note: `x, y int` is a single `ResultVariableDecl`. + */ ResultVariableDecl getAResultDecl() { result = this.getResultDecl(_) } /** Gets the number of results of this function type. */ int getNumResult() { result = count(this.getAResultDecl()) } - /** Gets the result of this function type, if there is only one. */ + /** + * Gets the result of this function type, if there is only one. + * + * Note: `x, y int` is a single `ResultVariableDecl`. + */ ResultVariableDecl getResultDecl() { this.getNumResult() = 1 and result = this.getAResultDecl() } override string toString() { result = "function type" } From 8ce543bf4ddc2127abf249c7042347a55314c296 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 23:15:55 +0200 Subject: [PATCH 15/21] Fix: `getNumResult()` was wrong in some cases It was the number of result declarations, which is different from the number of results when one result declaration declares more than one variable, as in `x, y int`. --- go/ql/lib/semmle/go/Expr.qll | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/ql/lib/semmle/go/Expr.qll b/go/ql/lib/semmle/go/Expr.qll index d50456411fd..0d3955a80df 100644 --- a/go/ql/lib/semmle/go/Expr.qll +++ b/go/ql/lib/semmle/go/Expr.qll @@ -1063,8 +1063,8 @@ class FuncTypeExpr extends @functypeexpr, TypeExpr, ScopeNode, FieldParent { */ ResultVariableDecl getAResultDecl() { result = this.getResultDecl(_) } - /** Gets the number of results of this function type. */ - int getNumResult() { result = count(this.getAResultDecl()) } + /** Gets the number of result parameters of this function type. */ + int getNumResult() { result = count(this.getAResultDecl().getANameExpr()) } /** * Gets the result of this function type, if there is only one. From a92349683e1f32f02dd9ef2c2aaaac45023d77a9 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 23:18:37 +0200 Subject: [PATCH 16/21] Deprecate `FuncTypeExpr.getResultDecl()` It is unused in this library. It could easily be used incorrectly and silently omit results when `getNumResult() > 1`. --- go/ql/lib/semmle/go/Expr.qll | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/go/ql/lib/semmle/go/Expr.qll b/go/ql/lib/semmle/go/Expr.qll index 0d3955a80df..9a8481a2dcc 100644 --- a/go/ql/lib/semmle/go/Expr.qll +++ b/go/ql/lib/semmle/go/Expr.qll @@ -1067,11 +1067,11 @@ class FuncTypeExpr extends @functypeexpr, TypeExpr, ScopeNode, FieldParent { int getNumResult() { result = count(this.getAResultDecl().getANameExpr()) } /** - * Gets the result of this function type, if there is only one. - * - * Note: `x, y int` is a single `ResultVariableDecl`. + * DEPRECATED: Use `getResultDecl(int i)` instead. */ - ResultVariableDecl getResultDecl() { this.getNumResult() = 1 and result = this.getAResultDecl() } + deprecated ResultVariableDecl getResultDecl() { + this.getNumResult() = 1 and result = this.getAResultDecl() + } override string toString() { result = "function type" } From 071a0e3d7d5185c3367361c847c51813fb9234de Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Mon, 8 Jun 2026 23:28:46 +0200 Subject: [PATCH 17/21] Add change notes --- .../2026-06-08-deprecate-functypeexpr-getresultdecl.md | 4 ++++ go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md | 4 ++++ .../lib/change-notes/2026-06-08-functypeexpr-getnumresult.md | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 go/ql/lib/change-notes/2026-06-08-deprecate-functypeexpr-getresultdecl.md create mode 100644 go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md create mode 100644 go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md diff --git a/go/ql/lib/change-notes/2026-06-08-deprecate-functypeexpr-getresultdecl.md b/go/ql/lib/change-notes/2026-06-08-deprecate-functypeexpr-getresultdecl.md new file mode 100644 index 00000000000..157fa33bf6a --- /dev/null +++ b/go/ql/lib/change-notes/2026-06-08-deprecate-functypeexpr-getresultdecl.md @@ -0,0 +1,4 @@ +--- +category: deprecated +--- +* `FuncTypeExpr.getResultDecl()` has been deprecated. Use `FuncTypeExpr.getResultDecl(int i)` instead. diff --git a/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md b/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md new file mode 100644 index 00000000000..bc7aed56261 --- /dev/null +++ b/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* `DataFlow::ResultNode`s are no longer created for returned expressions in functions with named result parameters. In this case there are already result nodes corresponding to `IR::ResultReadInstruction`s at the end of the function body. diff --git a/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md b/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md new file mode 100644 index 00000000000..974f604a041 --- /dev/null +++ b/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* `FuncTypeExpr.getNumResults()` now gets the number of result parameters. It previously got the number of result declarations, which is different when one result declaration declares more than one variable, as in `x, y int`. All uses of it expected the number of result parameters. Its QLDoc has been updated. From e22f9fadd7852d67d45e16764ea23e6aacfbd2ca Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan <62447351+owen-mc@users.noreply.github.com> Date: Tue, 9 Jun 2026 05:26:56 +0100 Subject: [PATCH 18/21] Fix mistakes in change notes Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md | 2 +- go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md b/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md index bc7aed56261..a567dd4edda 100644 --- a/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md +++ b/go/ql/lib/change-notes/2026-06-08-fix-result-nodes.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* `DataFlow::ResultNode`s are no longer created for returned expressions in functions with named result parameters. In this case there are already result nodes corresponding to `IR::ResultReadInstruction`s at the end of the function body. +* `DataFlow::ResultNode`s are no longer created for returned expressions in functions with named result parameters. In this case there are already result nodes corresponding to `IR::ReadResultInstruction`s at the end of the function body. diff --git a/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md b/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md index 974f604a041..70564beef11 100644 --- a/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md +++ b/go/ql/lib/change-notes/2026-06-08-functypeexpr-getnumresult.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* `FuncTypeExpr.getNumResults()` now gets the number of result parameters. It previously got the number of result declarations, which is different when one result declaration declares more than one variable, as in `x, y int`. All uses of it expected the number of result parameters. Its QLDoc has been updated. +* `FuncTypeExpr.getNumResult()` now gets the number of result parameters. It previously got the number of result declarations, which is different when one result declaration declares more than one variable, as in `x, y int`. All uses of it expected the number of result parameters. Its QLDoc has been updated. From 990913519da651573d726c649a70c47d824e62e9 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Tue, 9 Jun 2026 06:29:45 +0200 Subject: [PATCH 19/21] Make comment clearer --- go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll index 90c21654556..9a26beb5b31 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll @@ -932,9 +932,10 @@ module Public { ResultNode() { exists(FuncDef fd | - // When a function has any (named) result variables, then the - // `ReadResultInstruction`s at the end of the function are the correct - // result nodes. + // If the function has named result variables, then the + // `IR::ReadResultInstruction` nodes at the end of the function are + // the correct result nodes. Otherwise, the returned expressions are + // the result nodes. not exists(fd.getAResultVar()) and exists(IR::ReturnInstruction ret | ret.getRoot() = fd | insn = ret.getResult(i)) or From 17b9a668957cfa164cad085cd16b4ba0fcc77311 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 12 Jun 2026 10:17:12 +0200 Subject: [PATCH 20/21] 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 0a065c93dea71d7b5e896a38c3f164c7f8039cd0 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Wed, 17 Jun 2026 11:03:23 +0100 Subject: [PATCH 21/21] Update QLDoc for ResultNode --- go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll index 9a26beb5b31..7069cf36bd0 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll @@ -923,9 +923,9 @@ module Public { /** * A node whose value is returned as a result from a function. * - * This can either be a node corresponding to an expression in a return statement, - * or a node representing the current value of a named result variable at the exit - * of the function. + * If the function declares named result variables, this is a node representing + * the current value of one of those variables at function exit. Otherwise, this + * is a node corresponding to an expression in a return statement. */ class ResultNode extends InstructionNode { int i;