From 726ff3dbcf2a6afcd6ad47fe4a376103c002fc33 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:33:59 +0000 Subject: [PATCH 1/9] Test Go interface dispatch through captures and loops Record exact call targets and end-to-end value flow before the DataFlowIntegration migration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dataflow/InterfaceDispatch/Flows.expected | 0 .../go/dataflow/InterfaceDispatch/Flows.ql | 3 ++ .../InterfaceDispatch/Targets.expected | 10 ++++ .../go/dataflow/InterfaceDispatch/Targets.ql | 9 ++++ .../go/dataflow/InterfaceDispatch/main.go | 48 +++++++++++++++++++ 5 files changed, 70 insertions(+) create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.ql create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.ql create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/main.go diff --git a/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.expected b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.ql b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.ql new file mode 100644 index 000000000000..c43143b5d96c --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Flows.ql @@ -0,0 +1,3 @@ +import go +import utils.test.InlineFlowTest +import ValueFlowTest diff --git a/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.expected b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.expected new file mode 100644 index 000000000000..2377dd912add --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.expected @@ -0,0 +1,10 @@ +| capturedOverwrite | main.go:20:7:20:15 | call to Greet | main.go:3:6:3:12 | Greeter | +| capturedOverwrite | main.go:20:7:20:15 | call to Greet | main.go:7:6:7:16 | SafeGreeter | +| capturedOverwrite | main.go:20:7:20:15 | call to Greet | main.go:8:6:8:19 | TaintedGreeter | +| overwriteLoop | main.go:38:7:38:15 | call to Greet | main.go:7:6:7:16 | SafeGreeter | +| overwriteLoop | main.go:38:7:38:15 | call to Greet | main.go:8:6:8:19 | TaintedGreeter | +| readOnlyLoop | main.go:27:8:27:16 | call to Greet | main.go:7:6:7:16 | SafeGreeter | +| readOnlyLoop | main.go:30:7:30:15 | call to Greet | main.go:7:6:7:16 | SafeGreeter | +| unknownReceiver | main.go:45:7:45:15 | call to Greet | main.go:3:6:3:12 | Greeter | +| unknownReceiver | main.go:45:7:45:15 | call to Greet | main.go:7:6:7:16 | SafeGreeter | +| unknownReceiver | main.go:45:7:45:15 | call to Greet | main.go:8:6:8:19 | TaintedGreeter | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.ql b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.ql new file mode 100644 index 000000000000..327515c5689b --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/Targets.ql @@ -0,0 +1,9 @@ +import go +import semmle.go.dataflow.internal.DataFlowDispatch + +from DataFlow::CallNode call, Callable target +where + call.getTarget().getName() = "Greet" and + target = viableCallable(call.asExpr()).asCallable() +select call.getEnclosingCallable().getName(), call, + target.asFunction().(Method).getReceiverBaseType() diff --git a/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/main.go b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/main.go new file mode 100644 index 000000000000..08edfee3aa79 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/InterfaceDispatch/main.go @@ -0,0 +1,48 @@ +package main + +type Greeter interface { + Greet() string +} + +type SafeGreeter struct{} +type TaintedGreeter struct{} + +func source() string { return "tainted" } +func sink(string) {} + +func (SafeGreeter) Greet() string { return "safe" } +func (TaintedGreeter) Greet() string { return source() } + +func capturedOverwrite() { + var g Greeter = SafeGreeter{} + set := func() { g = TaintedGreeter{} } + set() + sink(g.Greet()) // $ hasValueFlow="call to Greet" +} + +func readOnlyLoop(b bool) { + var g Greeter = SafeGreeter{} + for b { + x := g + sink(x.Greet()) + } + x := g + sink(x.Greet()) +} + +func overwriteLoop(b bool) { + var g Greeter = SafeGreeter{} + for b { + g = TaintedGreeter{} + } + sink(g.Greet()) // $ hasValueFlow="call to Greet" +} + +func unknownReceiver(g Greeter, b bool) { + for b { + g = SafeGreeter{} + } + sink(g.Greet()) // $ hasValueFlow="call to Greet" +} + +func main() {} From 2a5157754c5f378e99f6d8c173e2ba7a582c48b0 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:45:03 +0000 Subject: [PATCH 2/9] Test Go interface nil checks through captures and loops Record pre-integration behavior for captured overwrites, loop-carried interface values, typed nil pointers, and unknown origins. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ImpossibleInterfaceNilCheck.expected | 4 ++ .../ImpossibleInterfaceNilCheck/ssa.go | 57 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ssa.go diff --git a/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ImpossibleInterfaceNilCheck.expected b/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ImpossibleInterfaceNilCheck.expected index e1294f702e28..a0b95d63dcdb 100644 --- a/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ImpossibleInterfaceNilCheck.expected +++ b/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ImpossibleInterfaceNilCheck.expected @@ -1,2 +1,6 @@ | ImpossibleInterfaceNilCheck.go:9:5:9:5 | e | This value can never be nil, since it is a wrapped interface value. | +| ssa.go:15:6:15:6 | x | This value can never be nil, since it is a wrapped interface value. | +| ssa.go:18:5:18:9 | value | This value can never be nil, since it is a wrapped interface value. | +| ssa.go:44:6:44:6 | x | This value can never be nil, since it is a wrapped interface value. | +| ssa.go:47:5:47:9 | value | This value can never be nil, since it is a wrapped interface value. | | tst.go:10:14:10:14 | y | This value can never be nil, since it is a wrapped interface value. | diff --git a/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ssa.go b/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ssa.go new file mode 100644 index 000000000000..d3fd94004fdd --- /dev/null +++ b/go/ql/test/query-tests/RedundantCode/ImpossibleInterfaceNilCheck/ssa.go @@ -0,0 +1,57 @@ +package main + +func capturedNil() { + var value interface{} = 42 + clear := func() { value = nil } + clear() + if value == nil { + } +} + +func readOnlyLoop(b bool) { + var value interface{} = 42 + for b { + x := value + if x == nil { // $ Alert + } + } + if value == nil { // $ Alert + } +} + +func nilLoop(b bool) { + var value interface{} + for b { + value = 42 + } + if value == nil { + } +} + +func unknownLoop(value interface{}, b bool) { + for b { + value = 42 + } + if value == nil { + } +} + +func typedNilLoop(b bool) { + var pointer *int + var value interface{} = pointer + for b { + x := value + if x == nil { // $ Alert + } + } + if value == nil { // $ Alert + } +} + +func capturedConcrete() { + var value interface{} = 42 + set := func() { value = 43 } + set() + if value == nil { // Captured writes remain unknown to the local analysis. + } +} From 0ca35acfd88618f2aa4ddc509131d1d6a3088dfb Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 12 Nov 2026 15:34:21 +0000 Subject: [PATCH 3/9] Use DataFlowIntegration for local SSA flow We do not use closure flow or barrier guards. --- go/ql/lib/semmle/go/dataflow/SsaImpl.qll | 39 +++++- .../go/dataflow/internal/DataFlowNodes.qll | 52 ++++++++ .../go/dataflow/internal/DataFlowPrivate.qll | 30 +---- .../SSADataFlowIntegration/Flows.expected | 0 .../dataflow/SSADataFlowIntegration/Flows.ql | 22 +++ .../dataflow/SSADataFlowIntegration/main.go | 126 ++++++++++++++++++ 6 files changed, 242 insertions(+), 27 deletions(-) create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.ql create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/main.go diff --git a/go/ql/lib/semmle/go/dataflow/SsaImpl.qll b/go/ql/lib/semmle/go/dataflow/SsaImpl.qll index 9415b78bf08a..8aa04971f26a 100644 --- a/go/ql/lib/semmle/go/dataflow/SsaImpl.qll +++ b/go/ql/lib/semmle/go/dataflow/SsaImpl.qll @@ -125,7 +125,9 @@ private module Internal { } cached - predicate ssaCachedStageRevRef() { Ssa::ssaCachedStageRevRef1() } + predicate ssaCachedStageRevRef() { + Ssa::ssaCachedStageRevRef1() and DataFlowIntegrationImpl::ssaCachedStageRevRef2() + } } } @@ -157,6 +159,41 @@ private module PublicSsaInput implements Impl::SsaInputSig { module Ssa = Impl::MakeSsa; +private module DataFlowIntegrationInput implements Impl::DataFlowIntegrationInputSig { + private import codeql.util.Void + + predicate ssaDefHasSource(Impl::WriteDefinition def) { def instanceof Ssa::SsaExplicitWrite } + + predicate allowFlowIntoUncertainDef(Impl::UncertainWriteDefinition def) { any() } + + // Keep Go's field-sensitive barrier guards rather than adding definition-level barriers. + class GuardValue = Void; + + class Guard extends Void { + string toString() { none() } + + predicate hasValueBranchEdge(BasicBlock bb1, BasicBlock bb2, GuardValue val) { none() } + + predicate valueControlsBranchEdge(BasicBlock bb1, BasicBlock bb2, GuardValue val) { none() } + } + + predicate guardDirectlyControlsBlock(Guard guard, BasicBlock bb, GuardValue val) { none() } + + predicate supportBarrierGuardsOnPhiEdges() { none() } +} + +private module DataFlowIntegrationImpl = Impl::DataFlowIntegration; + +cached +module DataFlowIntegration { + import DataFlowIntegrationImpl + + cached + predicate localFlowStep(SsaSourceVariable v, Node nodeFrom, Node nodeTo, boolean isUseStep) { + DataFlowIntegrationImpl::localFlowStep(v, nodeFrom, nodeTo, isUseStep) + } +} + /** Gets the control-flow root containing `def`. */ ControlFlow::Root getSsaDefinitionRoot(Ssa::SsaDefinition def) { result = def.getBasicBlock().getScope() diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll index d3f965578c8d..803355f1330b 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowNodes.qll @@ -9,10 +9,13 @@ private import DataFlowPrivate private import FlowSummaryImpl as FlowSummaryImpl private import semmle.go.dataflow.ExternalFlow +private module SsaFlow = SsaImpl::DataFlowIntegration; + cached private newtype TNode = MkInstructionNode(IR::Instruction insn) or MkSsaNode(SsaDefinition ssa) or + MkSsaSynthReadNode(SsaFlow::SsaSynthReadNode ssa) or MkGlobalFunctionNode(Function f) or MkImplicitVarargsSlice(IR::EvalInstruction ins) { // We only use CallExprs with an EvalInstruction to guarantee reachability. @@ -26,6 +29,55 @@ private newtype TNode = module Private { private import DataFlowDispatch + /** A synthetic SSA read at a control-flow merge. */ + class SsaSynthReadNode extends Node, MkSsaSynthReadNode { + private SsaFlow::SsaSynthReadNode ssa; + + SsaSynthReadNode() { this = MkSsaSynthReadNode(ssa) } + + SsaFlow::SsaSynthReadNode getSsaNode() { result = ssa } + + override ControlFlow::Root getRoot() { result = ssa.getBasicBlock().getScope() } + + override BasicBlock getBasicBlock() { result = ssa.getBasicBlock() } + + override Type getType() { result = ssa.getSourceVariable().getType() } + + override string getNodeKind() { result = "SSA synthetic read" } + + override string toString() { result = ssa.toString() } + + override Location getLocation() { result = ssa.getLocation() } + } + + /** Gets the Go data-flow node corresponding to `n`. */ + Node getNodeFromSsaNode(SsaFlow::Node n) { + result.(SsaNode).getDefinition() = n.(SsaFlow::SsaDefinitionNode).getDefinition() + or + result.(SsaSynthReadNode).getSsaNode() = n + or + exists(IR::Instruction insn, SsaSourceVariable v | + n.(SsaFlow::ReadNode).readsAt(insn, v) and + insn.reads(v) and + result.asInstruction() = insn + ) + or + exists(IR::Instruction insn, SsaSourceVariable v | + n.(SsaFlow::PostUpdateReadNode).readsAt(insn, v) and + insn.reads(v) and + result.(PostUpdateNode).getPreUpdateNode().asInstruction() = insn + ) + or + exists(IR::Instruction rhs | + rhs = + n.(SsaFlow::WriteDefSourceNode).getDefinition().(SsaExplicitWrite).getDefinition().getRhs() + | + result.asInstruction() = rhs + or + result.(PostUpdateNode).getPreUpdateNode().asInstruction() = rhs + ) + } + /** Gets the callable in which this node occurs. */ DataFlowCallable nodeGetEnclosingCallable(Node n) { result.asCallable() = n.getEnclosingCallable() diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll index fbbbe0172a9a..cf3d13a6847a 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll @@ -70,32 +70,10 @@ predicate basicLocalFlowStep(Node nodeFrom, Node nodeTo) { else nodeTo.asInstruction() = evalAssert ) or - // Instruction -> SSA defn - exists(IR::Instruction pred, SsaExplicitWrite succ | - succ.getDefinition().getRhs() = pred and - ( - nodeFrom = instructionNode(pred) or - nodeFrom.(PostUpdateNode).getPreUpdateNode() = instructionNode(pred) - ) and - nodeTo = ssaNode(succ) - ) - or - // SSA defn -> first SSA use - exists(SsaDefinition pred, IR::Instruction succ | SsaImpl::firstUse(pred, succ) | - (pred instanceof SsaExplicitWrite or pred instanceof SsaUncertainWrite) and - nodeFrom = ssaNode(pred) and - nodeTo = instructionNode(succ) - ) - or - // SSA use -> successive SSA use - // Note this case includes Phi node traversal - exists(IR::Instruction pred, IR::Instruction succ | succ = getAnAdjacentUse(pred) | - ( - nodeFrom = instructionNode(pred) or - nodeFrom.(PostUpdateNode).getPreUpdateNode() = instructionNode(pred) - ) and - nodeTo = instructionNode(succ) and - nodeTo != nodeFrom + exists(SsaImpl::DataFlowIntegration::Node pred, SsaImpl::DataFlowIntegration::Node succ | + SsaImpl::DataFlowIntegration::localFlowStep(_, pred, succ, _) and + nodeFrom = getNodeFromSsaNode(pred) and + nodeTo = getNodeFromSsaNode(succ) ) } diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.expected b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.ql b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.ql new file mode 100644 index 000000000000..d759975414c1 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/Flows.ql @@ -0,0 +1,22 @@ +import go +import utils.test.InlineFlowTest + +predicate isBad(DataFlow::Node g, Expr e, boolean branch) { + g.(DataFlow::CallNode).getTarget().getName() = "isBad" and + e = g.(DataFlow::CallNode).getArgument(0).asExpr() and + branch = false +} + +module Config implements DataFlow::ConfigSig { + predicate isSource = DefaultFlowConfig::isSource/1; + + predicate isSink = DefaultFlowConfig::isSink/1; + + predicate fieldFlowBranchLimit = DefaultFlowConfig::fieldFlowBranchLimit/0; + + predicate isBarrier(DataFlow::Node node) { + node = DataFlow::BarrierGuard::getABarrierNode() + } +} + +import ValueFlowTest diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/main.go b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/main.go new file mode 100644 index 000000000000..f26e7e640e94 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/main.go @@ -0,0 +1,126 @@ +package main + +func source() string { return "tainted" } +func sink(string) {} +func isBad(string) bool { + return false +} + +func readMerge(b, c bool) { + value := source() + if b { + sink(value) // $ hasValueFlow="value" + } else { + sink(value) // $ hasValueFlow="value" + } + if c { + sink(value) // $ hasValueFlow="value" + } else { + sink(value) // $ hasValueFlow="value" + } +} + +func writeMerge(b, c bool) { + value := "safe" + if b { + value = source() + } + if c { + sink(value) // $ hasValueFlow="value" + } else { + sink(value) // $ hasValueFlow="value" + } + value = "safe" + sink(value) +} + +func loop(b bool) { + value := source() + for b { + sink(value) // $ hasValueFlow="value" + value = "safe" + } + sink(value) // $ hasValueFlow="value" +} + +func namedResult() (value string) { + value = source() + return +} + +func implicitRead() { + sink(namedResult()) // $ hasValueFlow="call to namedResult" +} + +func capturedWrite() { + value := "safe" + set := func() { + value = source() + } + set() + sink(value) // $ hasValueFlow="value" +} + +func capturedRead() { + value := source() + read := func() { + sink(value) // $ hasValueFlow="value" + } + sink(value) // $ hasValueFlow="value" + read() +} + +func uncertainUpdate(b bool) { + value := source() + update := func() { + if b { + value = "safe" + } + } + update() + sink(value) // $ hasValueFlow="value" +} + +func postUpdate(b, c bool) { + value := []string{"safe"} + update := func(values []string) { + values[0] = source() + } + update(value) + if b { + sink(value[0]) // $ hasValueFlow="index expression" + } + if c { + sink(value[0]) // $ hasValueFlow="index expression" + } else { + sink(value[0]) // $ hasValueFlow="index expression" + } +} + +func guardedMerge(b bool) { + value := source() + if isBad(value) { + return + } + if b { + sink(value) + } else { + sink(value) + } +} + +type record struct { + value string +} + +func guardedFieldMerge(b bool) { + item := record{source()} + if isBad(item.value) { + return + } + if b { + sink(item.value) + } else { + sink(item.value) + } +} From d34c8438f71d9180274523153032b11892778217 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:06:13 +0000 Subject: [PATCH 4/9] Add SSA consistency checks --- go/ql/consistency-queries/SsaConsistency.ql | 4 ++ .../CONSISTENCY/SsaConsistency.expected | 4 ++ .../CONSISTENCY/SsaConsistency.expected | 10 ++++ .../CONSISTENCY/SsaConsistency.expected | 8 +++ .../Twirp/CONSISTENCY/SsaConsistency.expected | 54 +++++++++++++++++++ .../CONSISTENCY/SsaConsistency.expected | 4 ++ 6 files changed, 84 insertions(+) create mode 100644 go/ql/consistency-queries/SsaConsistency.ql create mode 100644 go/ql/test/experimental/CWE-942/CONSISTENCY/SsaConsistency.expected create mode 100644 go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/CONSISTENCY/SsaConsistency.expected create mode 100644 go/ql/test/library-tests/semmle/go/frameworks/Twirp/CONSISTENCY/SsaConsistency.expected create mode 100644 go/ql/test/query-tests/Security/CWE-643/CONSISTENCY/SsaConsistency.expected diff --git a/go/ql/consistency-queries/SsaConsistency.ql b/go/ql/consistency-queries/SsaConsistency.ql new file mode 100644 index 000000000000..daf7cb71f35e --- /dev/null +++ b/go/ql/consistency-queries/SsaConsistency.ql @@ -0,0 +1,4 @@ +import go +import semmle.go.dataflow.SsaImpl +import Consistency +import DataFlowIntegration::DfConsistency diff --git a/go/ql/test/experimental/CWE-942/CONSISTENCY/SsaConsistency.expected b/go/ql/test/experimental/CWE-942/CONSISTENCY/SsaConsistency.expected new file mode 100644 index 000000000000..e0aed12c69e4 --- /dev/null +++ b/go/ql/test/experimental/CWE-942/CONSISTENCY/SsaConsistency.expected @@ -0,0 +1,4 @@ +uselessPhiNode +| CorsMisconfiguration.go:13:1:229:1 | SSA phi(allowedOrigins) | 1 | +phiWithoutTwoPriorRefs +| CorsMisconfiguration.go:13:1:229:1 | SSA phi(allowedOrigins) | CorsMisconfiguration.go:13:1:229:1 | Exceptional Exit | CorsMisconfiguration.go:94:3:94:16 | allowedOrigins | 1 | diff --git a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected new file mode 100644 index 000000000000..7a742decbacf --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected @@ -0,0 +1,10 @@ +readWithoutDef +| stmts2.go:18:7:18:7 | x | stmts2.go:18:2:19:10 | comm clause | 4 | +| stmts2.go:20:10:20:10 | y | stmts2.go:20:2:24:10 | comm clause | 5 | +uselessPhiNode +| epilogues.go:89:3:91:3 | SSA phi(result) | 1 | +readWithoutPriorRef +| stmts2.go:18:7:18:7 | x | stmts2.go:18:2:19:10 | comm clause | 4 | +| stmts2.go:20:10:20:10 | y | stmts2.go:20:2:24:10 | comm clause | 5 | +phiWithoutTwoPriorRefs +| epilogues.go:89:3:91:3 | SSA phi(result) | epilogues.go:89:3:91:3 | After if statement | epilogues.go:87:30:87:35 | result | 1 | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/CONSISTENCY/SsaConsistency.expected b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/CONSISTENCY/SsaConsistency.expected new file mode 100644 index 000000000000..59855fa55900 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/CONSISTENCY/SsaConsistency.expected @@ -0,0 +1,8 @@ +uselessPhiNode +| main.go:57:9:59:2 | SSA phi(value) | 1 | +| main.go:64:1:71:1 | SSA phi(value) | 1 | +| main.go:76:3:78:3 | SSA phi(value) | 1 | +phiWithoutTwoPriorRefs +| main.go:57:9:59:2 | SSA phi(value) | main.go:57:9:59:2 | Exit | main.go:56:2:56:6 | value | 1 | +| main.go:64:1:71:1 | SSA phi(value) | main.go:64:1:71:1 | Exceptional Exit | main.go:65:2:65:6 | value | 1 | +| main.go:76:3:78:3 | SSA phi(value) | main.go:76:3:78:3 | After if statement | main.go:74:2:74:6 | value | 1 | diff --git a/go/ql/test/library-tests/semmle/go/frameworks/Twirp/CONSISTENCY/SsaConsistency.expected b/go/ql/test/library-tests/semmle/go/frameworks/Twirp/CONSISTENCY/SsaConsistency.expected new file mode 100644 index 000000000000..440d65afa578 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/frameworks/Twirp/CONSISTENCY/SsaConsistency.expected @@ -0,0 +1,54 @@ +uselessPhiNode +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:494:9:494:48 | SSA phi(err) | 1 | +| rpc/notes/service.twirp.go:494:9:494:48 | SSA phi(respContent) | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:575:9:575:48 | SSA phi(err) | 1 | +| rpc/notes/service.twirp.go:575:9:575:48 | SSA phi(respContent) | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:674:9:674:48 | SSA phi(err) | 1 | +| rpc/notes/service.twirp.go:674:9:674:48 | SSA phi(respContent) | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(handler) | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(reqContent) | 1 | +| rpc/notes/service.twirp.go:755:9:755:48 | SSA phi(err) | 1 | +| rpc/notes/service.twirp.go:755:9:755:48 | SSA phi(respContent) | 1 | +| rpc/notes/service.twirp.go:1203:1:1249:1 | SSA phi(resp) | 1 | +| rpc/notes/service.twirp.go:1203:156:1249:1 | SSA phi(resp) | 1 | +| rpc/notes/service.twirp.go:1252:1:1305:1 | SSA phi(resp) | 1 | +| rpc/notes/service.twirp.go:1252:152:1305:1 | SSA phi(resp) | 1 | +phiWithoutTwoPriorRefs +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(handler) | rpc/notes/service.twirp.go:446:1:527:1 | Exceptional Exit | rpc/notes/service.twirp.go:468:2:468:8 | handler | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(handler) | rpc/notes/service.twirp.go:446:1:527:1 | Normal Exit | rpc/notes/service.twirp.go:468:2:468:8 | handler | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:446:1:527:1 | Exceptional Exit | rpc/notes/service.twirp.go:461:2:461:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:446:1:527:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:446:1:527:1 | Normal Exit | rpc/notes/service.twirp.go:461:2:461:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:494:9:494:48 | SSA phi(err) | rpc/notes/service.twirp.go:494:9:494:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:447:6:447:8 | err | 1 | +| rpc/notes/service.twirp.go:494:9:494:48 | SSA phi(respContent) | rpc/notes/service.twirp.go:494:9:494:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:492:6:492:16 | respContent | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(handler) | rpc/notes/service.twirp.go:529:1:606:1 | Exceptional Exit | rpc/notes/service.twirp.go:549:2:549:8 | handler | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(handler) | rpc/notes/service.twirp.go:529:1:606:1 | Normal Exit | rpc/notes/service.twirp.go:549:2:549:8 | handler | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:529:1:606:1 | Exceptional Exit | rpc/notes/service.twirp.go:543:2:543:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:529:1:606:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:529:1:606:1 | Normal Exit | rpc/notes/service.twirp.go:543:2:543:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:575:9:575:48 | SSA phi(err) | rpc/notes/service.twirp.go:575:9:575:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:530:6:530:8 | err | 1 | +| rpc/notes/service.twirp.go:575:9:575:48 | SSA phi(respContent) | rpc/notes/service.twirp.go:575:9:575:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:573:6:573:16 | respContent | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(handler) | rpc/notes/service.twirp.go:626:1:707:1 | Exceptional Exit | rpc/notes/service.twirp.go:648:2:648:8 | handler | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(handler) | rpc/notes/service.twirp.go:626:1:707:1 | Normal Exit | rpc/notes/service.twirp.go:648:2:648:8 | handler | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:626:1:707:1 | Exceptional Exit | rpc/notes/service.twirp.go:641:2:641:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:626:1:707:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:626:1:707:1 | Normal Exit | rpc/notes/service.twirp.go:641:2:641:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:674:9:674:48 | SSA phi(err) | rpc/notes/service.twirp.go:674:9:674:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:627:6:627:8 | err | 1 | +| rpc/notes/service.twirp.go:674:9:674:48 | SSA phi(respContent) | rpc/notes/service.twirp.go:674:9:674:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:672:6:672:16 | respContent | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(handler) | rpc/notes/service.twirp.go:709:1:786:1 | Exceptional Exit | rpc/notes/service.twirp.go:729:2:729:8 | handler | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(handler) | rpc/notes/service.twirp.go:709:1:786:1 | Normal Exit | rpc/notes/service.twirp.go:729:2:729:8 | handler | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:709:1:786:1 | Exceptional Exit | rpc/notes/service.twirp.go:723:2:723:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:709:1:786:1 | SSA phi(reqContent) | rpc/notes/service.twirp.go:709:1:786:1 | Normal Exit | rpc/notes/service.twirp.go:723:2:723:11 | reqContent | 1 | +| rpc/notes/service.twirp.go:755:9:755:48 | SSA phi(err) | rpc/notes/service.twirp.go:755:9:755:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:710:6:710:8 | err | 1 | +| rpc/notes/service.twirp.go:755:9:755:48 | SSA phi(respContent) | rpc/notes/service.twirp.go:755:9:755:48 | defer-invoke call to ensurePanicResponses | rpc/notes/service.twirp.go:753:6:753:16 | respContent | 1 | diff --git a/go/ql/test/query-tests/Security/CWE-643/CONSISTENCY/SsaConsistency.expected b/go/ql/test/query-tests/Security/CWE-643/CONSISTENCY/SsaConsistency.expected new file mode 100644 index 000000000000..403dd165c0b9 --- /dev/null +++ b/go/ql/test/query-tests/Security/CWE-643/CONSISTENCY/SsaConsistency.expected @@ -0,0 +1,4 @@ +uselessPhiNode +| XPathInjection.go:11:1:28:1 | SSA phi(username) | 1 | +phiWithoutTwoPriorRefs +| XPathInjection.go:11:1:28:1 | SSA phi(username) | XPathInjection.go:11:1:28:1 | Exceptional Exit | XPathInjection.go:13:2:13:9 | username | 1 | From 331c6c97e58253b0a00e552ac4772db6e72ebfee Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:19:55 +0000 Subject: [PATCH 5/9] Hide internal Go SSA merge nodes from flow paths Keep synthetic read and phi nodes in the analysis while omitting them from path explanations. Add regression coverage for state-changing steps and neverSkip configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../go/dataflow/internal/DataFlowPrivate.qll | 8 +- .../HiddenNodes.expected | 2 + .../SSADataFlowIntegration/HiddenNodes.ql | 78 +++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.expected create mode 100644 go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.ql diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll index cf3d13a6847a..23ee7bc64af9 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowPrivate.qll @@ -426,7 +426,13 @@ Node getArgument(CallNode c, int i) { } /** Holds if `n` should be hidden from path explanations. */ -predicate nodeIsHidden(Node n) { n instanceof FlowSummaryNode } +predicate nodeIsHidden(Node n) { + n instanceof FlowSummaryNode + or + n instanceof SsaSynthReadNode + or + n.(SsaNode).getDefinition() instanceof SsaImpl::PhiNode +} class LambdaCallKind = Unit; diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.expected b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.expected new file mode 100644 index 000000000000..34e311036d72 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.expected @@ -0,0 +1,2 @@ +| neverSkip | 8 | 0 | 0 | +| stateful | 8 | 0 | 0 | diff --git a/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.ql b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.ql new file mode 100644 index 000000000000..fc3d3add9369 --- /dev/null +++ b/go/ql/test/library-tests/semmle/go/dataflow/SSADataFlowIntegration/HiddenNodes.ql @@ -0,0 +1,78 @@ +import go +import semmle.go.dataflow.internal.DataFlowNodes as Nodes +import semmle.go.dataflow.SsaImpl + +private predicate sourceNode(DataFlow::Node node) { + node.(DataFlow::CallNode).getTarget().getName() = "source" and + node.getEnclosingCallable().getName() = ["readMerge", "writeMerge", "loop"] +} + +private predicate sinkNode(DataFlow::Node node) { + node = any(DataFlow::CallNode call | call.getTarget().getName() = "sink").getArgument(0) and + node.getEnclosingCallable().getName() = ["readMerge", "writeMerge", "loop"] +} + +module NeverSkipConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node node) { sourceNode(node) } + + predicate isSink(DataFlow::Node node) { sinkNode(node) } + + predicate neverSkip(DataFlow::Node node) { any() } +} + +module NeverSkipFlow = DataFlow::Global; + +module StatefulConfig implements DataFlow::StateConfigSig { + class FlowState extends string { + FlowState() { this = ["initial", "marked"] } + } + + predicate isSource(DataFlow::Node node, FlowState state) { + sourceNode(node) and state = "initial" + } + + predicate isSink(DataFlow::Node node, FlowState state) { sinkNode(node) and state = "marked" } + + predicate isAdditionalFlowStep( + DataFlow::Node node1, FlowState state1, DataFlow::Node node2, FlowState state2 + ) { + node1 = node2 and + node1.getType().getUnderlyingType() instanceof StringType and + state1 = "initial" and + state2 = "marked" + } +} + +module StatefulFlow = DataFlow::GlobalWithState; + +private predicate pathNode(string mode, DataFlow::Node node) { + exists(NeverSkipFlow::PathNode p | + ( + NeverSkipFlow::PathGraph::edges(p, _, _, _) or + NeverSkipFlow::PathGraph::edges(_, p, _, _) + ) and + mode = "neverSkip" and + node = p.getNode() + ) + or + exists(StatefulFlow::PathNode p | + ( + StatefulFlow::PathGraph::edges(p, _, _, _) or + StatefulFlow::PathGraph::edges(_, p, _, _) + ) and + mode = "stateful" and + node = p.getNode() + ) +} + +private predicate flow(string mode, DataFlow::Node source, DataFlow::Node sink) { + mode = "neverSkip" and NeverSkipFlow::flow(source, sink) + or + mode = "stateful" and StatefulFlow::flow(source, sink) +} + +from string mode +where mode = ["neverSkip", "stateful"] +select mode, count(DataFlow::Node source, DataFlow::Node sink | flow(mode, source, sink)) as flows, + count(DataFlow::Node n | pathNode(mode, n) and n instanceof Nodes::Private::SsaSynthReadNode) as syntheticReads, + count(DataFlow::SsaNode n | pathNode(mode, n) and n.getDefinition() instanceof PhiNode) as phis From 0ed40ebfe28fc69233009035ffc17bb82548f641 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:37:15 +0000 Subject: [PATCH 6/9] Preserve Go interface dispatch through shared SSA flow Propagate unknown origins instead of recursively proving every predecessor concrete. Treat uncertain writes conservatively and require a concrete origin, allowing known types to pass through SSA cycles. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../go/dataflow/internal/DataFlowDispatch.qll | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/go/ql/lib/semmle/go/dataflow/internal/DataFlowDispatch.qll b/go/ql/lib/semmle/go/dataflow/internal/DataFlowDispatch.qll index 10455f84edba..80f9a8db0a1a 100644 --- a/go/ql/lib/semmle/go/dataflow/internal/DataFlowDispatch.qll +++ b/go/ql/lib/semmle/go/dataflow/internal/DataFlowDispatch.qll @@ -31,18 +31,34 @@ private Type getConcreteType(DataFlow::Node nd) { } /** - * Holds if all concrete (that is, non-interface) types of `nd` concrete types can be determined by - * local reasoning. + * Holds if local flow into `nd` may originate from a value whose concrete type is unknown. + * Values with concrete static types stop propagation. + */ +private predicate hasUnknownSource(DataFlow::Node nd) { + nd = getInterfaceCallReceiverSource(_) and + not exists(getConcreteType(nd)) and + ( + not basicLocalFlowStep(_, nd) + or + // Uncertain writes may also receive values through non-local capture flow. + nd.(DataFlow::SsaNode).getDefinition() instanceof SsaUncertainWrite + or + exists(DataFlow::Node pred | basicLocalFlowStep(pred, nd) and hasUnknownSource(pred)) + ) +} + +/** + * Holds if all concrete (that is, non-interface) types of `nd` can be determined by local reasoning. + * Checking for unknown origins rather than recursively proving every predecessor handles SSA cycles. * * `nd` is restricted to nodes that flow into the receiver value of an interface call, since that is * all we are ultimately interested in. */ private predicate isConcreteValue(DataFlow::Node nd) { nd = getInterfaceCallReceiverSource(_) and - ( - exists(getConcreteType(nd)) - or - forex(DataFlow::Node pred | basicLocalFlowStep(pred, nd) | isConcreteValue(pred)) + not hasUnknownSource(nd) and + exists(DataFlow::Node source | + exists(getConcreteType(source)) and basicLocalFlowStep*(source, nd) ) } From 7fb1225710774504b34faedcd5030bcfd5d54814 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 15:46:16 +0000 Subject: [PATCH 7/9] Preserve Go interface nil checks through shared SSA flow Track possibly nil origins instead of recursively proving every predecessor non-nil. Treat captured writes conservatively and handle loop-carried values while retaining typed-nil and post-update semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ImpossibleInterfaceNilCheck.ql | 48 +++++++++++++++---- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/go/ql/src/RedundantCode/ImpossibleInterfaceNilCheck.ql b/go/ql/src/RedundantCode/ImpossibleInterfaceNilCheck.ql index c8ff9c345f69..8675ef4dcaa9 100644 --- a/go/ql/src/RedundantCode/ImpossibleInterfaceNilCheck.ql +++ b/go/ql/src/RedundantCode/ImpossibleInterfaceNilCheck.ql @@ -30,21 +30,49 @@ predicate flowsToInterfaceNilCheck(DataFlow::Node nd) { flowsToInterfaceNilCheck(nd.getASuccessor()) } +/** + * Holds if wrapping `nd` in an interface produces a non-nil interface value. + */ +private predicate isNonNilWrapperSource(DataFlow::Node nd) { + exists(Type tp | tp = nd.getType().getUnderlyingType() | + not tp instanceof InterfaceType and + not tp instanceof NilLiteralType and + not tp instanceof InvalidType + ) +} + +private DataFlow::Node getWrapperPredecessor(DataFlow::Node nd) { + result = nd.getAPredecessor() and not result instanceof DataFlow::PostUpdateNode +} + +/** + * Holds if local flow into `nd` may originate from a nil or unknown interface + * value. + */ +private predicate hasPossiblyNilSource(DataFlow::Node nd) { + flowsToInterfaceNilCheck(nd) and + not isNonNilWrapperSource(nd) and + ( + not exists(getWrapperPredecessor(nd)) + or + // Uncertain writes may receive replacement values through non-local capture flow. + nd.(DataFlow::SsaNode).getDefinition() instanceof SsaUncertainWrite + or + hasPossiblyNilSource(getWrapperPredecessor(nd)) + ) +} + /** * Holds if `nd` is a non-nil interface value. + * + * A concrete origin and no possibly nil origins establish this even through + * SSA cycles. */ predicate nonNilWrapper(DataFlow::Node nd) { flowsToInterfaceNilCheck(nd) and - forex(DataFlow::Node pred | - pred = nd.getAPredecessor() and not pred instanceof DataFlow::PostUpdateNode - | - exists(Type predtp | predtp = pred.getType().getUnderlyingType() | - not predtp instanceof InterfaceType and - not predtp instanceof NilLiteralType and - not predtp instanceof InvalidType - ) - or - nonNilWrapper(pred) + not hasPossiblyNilSource(nd) and + exists(DataFlow::Node source | + source = getWrapperPredecessor*(nd) and isNonNilWrapperSource(source) ) } From ed554f236cf659c0e87386b1eca4adf0a26b1206 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 16:34:57 +0000 Subject: [PATCH 8/9] Reclassify some reads, to fix SSA consistency --- go/ql/lib/semmle/go/controlflow/IR.qll | 5 ++++- .../ControlFlowGraph/CONSISTENCY/SsaConsistency.expected | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/go/ql/lib/semmle/go/controlflow/IR.qll b/go/ql/lib/semmle/go/controlflow/IR.qll index f74a23e1155c..786fdbe7380c 100644 --- a/go/ql/lib/semmle/go/controlflow/IR.qll +++ b/go/ql/lib/semmle/go/controlflow/IR.qll @@ -245,7 +245,10 @@ module IR { /** Gets the expression underlying this instruction. */ Expr getExpr() { result = e } - override predicate reads(ValueEntity v) { e = v.getAReference() } + override predicate reads(ValueEntity v) { + e = v.getAReference() and + e.(ReferenceExpr).isRvalue() + } override Type getResultType() { result = e.getType() } diff --git a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected index 7a742decbacf..a810e3bd97cf 100644 --- a/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected +++ b/go/ql/test/library-tests/semmle/go/controlflow/ControlFlowGraph/CONSISTENCY/SsaConsistency.expected @@ -1,10 +1,4 @@ -readWithoutDef -| stmts2.go:18:7:18:7 | x | stmts2.go:18:2:19:10 | comm clause | 4 | -| stmts2.go:20:10:20:10 | y | stmts2.go:20:2:24:10 | comm clause | 5 | uselessPhiNode | epilogues.go:89:3:91:3 | SSA phi(result) | 1 | -readWithoutPriorRef -| stmts2.go:18:7:18:7 | x | stmts2.go:18:2:19:10 | comm clause | 4 | -| stmts2.go:20:10:20:10 | y | stmts2.go:20:2:24:10 | comm clause | 5 | phiWithoutTwoPriorRefs | epilogues.go:89:3:91:3 | SSA phi(result) | epilogues.go:89:3:91:3 | After if statement | epilogues.go:87:30:87:35 | result | 1 | From 135d102ecac848ee89e2bbb6e4b20473bd27c65f Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 13 Nov 2026 16:47:55 +0000 Subject: [PATCH 9/9] Add missing QLDoc --- go/ql/lib/semmle/go/dataflow/SsaImpl.qll | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/go/ql/lib/semmle/go/dataflow/SsaImpl.qll b/go/ql/lib/semmle/go/dataflow/SsaImpl.qll index 8aa04971f26a..e8d0ad6ed9f3 100644 --- a/go/ql/lib/semmle/go/dataflow/SsaImpl.qll +++ b/go/ql/lib/semmle/go/dataflow/SsaImpl.qll @@ -184,10 +184,17 @@ private module DataFlowIntegrationInput implements Impl::DataFlowIntegrationInpu private module DataFlowIntegrationImpl = Impl::DataFlowIntegration; +/** Provides shared SSA nodes and local flow steps for Go data-flow analysis. */ cached module DataFlowIntegration { import DataFlowIntegrationImpl + /** + * Holds if the value of `v` flows from `nodeFrom` to `nodeTo` in one local SSA step. + * + * `isUseStep` is `true` when `nodeFrom` is a (post-update) read node and + * `nodeTo` is a read node or phi (read) node. + */ cached predicate localFlowStep(SsaSourceVariable v, Node nodeFrom, Node nodeTo, boolean isUseStep) { DataFlowIntegrationImpl::localFlowStep(v, nodeFrom, nodeTo, isUseStep)