Skip to content

Go: Use shared guards library - #22365

Open
owen-mc wants to merge 11 commits into
github:mainfrom
owen-mc:go/shared-guards
Open

owen-mc wants to merge 11 commits into
github:mainfrom
owen-mc:go/shared-guards

Conversation

@owen-mc

@owen-mc owen-mc commented Aug 17, 2026 •

Copy link
Copy Markdown
Contributor

Instantiate the shared guards library for Go, port over all uses of the Go-specific ConditionGuardNode and deprecate it.

Go-specific issues encountered:

  • functions with multiple return values.
  • that the syntactic receiver and the semantic receiver of a method call may be different, e.g. in case of a promoted method or an implicit dereference.
  • functions with named result parameters may implicitly return values, rather than explicitly as part of a return statement.

Comment thread go/ql/lib/semmle/go/controlflow/Guards.qll Fixed
Comment thread go/ql/lib/semmle/go/controlflow/Guards.qll Fixed
Comment thread go/ql/lib/semmle/go/dataflow/internal/DataFlowUtil.qll Fixed

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@owen-mc
owen-mc force-pushed the go/shared-guards branch 2 times, most recently from def5d61 to 888eae3 Compare August 18, 2026 11:45
Comment thread go/ql/lib/semmle/go/controlflow/Guards.qll Fixed
Comment thread go/ql/lib/semmle/go/controlflow/Guards.qll Fixed
Comment thread go/ql/lib/semmle/go/dataflow/internal/DataFlowUtil.qll Fixed
Comment thread go/ql/src/InconsistentCode/LengthComparisonOffByOne.ql Fixed
Comment thread go/ql/src/InconsistentCode/LengthComparisonOffByOne.ql Fixed
Comment thread go/ql/src/InconsistentCode/LengthComparisonOffByOne.ql Fixed
Comment thread go/ql/lib/semmle/go/controlflow/Guards.qll Fixed
@owen-mc

owen-mc commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

"Run QL for QL" is failing for all PRs at the moment. I don't think it is related to this PR.

@owen-mc
owen-mc marked this pull request as ready for review October 6, 2026 16:28
Copilot AI balanced review requested due to automatic review settings October 6, 2026 16:28
@owen-mc
owen-mc requested review from a team as code owners October 6, 2026 16:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The deprecation change note incorrectly states that the legacy class has no instances.

Review effort: Balanced
Findings: 1 Low severity

Open (1)
What changed in this PR

Migrates Go guard reasoning to the shared control-flow guards library and updates dependent analyses, models, and tests.

Changes:

  • Adds the Go Guard/GuardValue implementation and shared-library extension points.
  • Migrates guard consumers and barrier models to the new APIs.
  • Deprecates the legacy guard API and expands regression coverage.
File Description
shared/​controlflow/​codeql/​controlflow/​Guards.qll Extends shared guard capabilities.
java/​ql/​lib/​semmle/​code/​java/​controlflow/​Guards.qll Adapts Java to the extended interface.
go/​ql/​test/​query-tests/​Security/​CWE-295/​DisabledCertificateCheck/​main.go Adds switch-based flag coverage.
go/​ql/​test/​query-tests/​Security/​CWE-209/​test.go Adds switch-flow coverage.
go/​ql/​test/​query-tests/​Security/​CWE-209/​StackTraceExposure.expected Updates expected findings.
go/​ql/​test/​library-tests/​semmle/​go/​dataflow/​GuardingFunctions/​test.go Expands wrapper-guard tests.
go/​ql/​test/​library-tests/​semmle/​go/​controlflow/​Guards/​Guards.ql Adds guard API test queries.
go/​ql/​test/​library-tests/​semmle/​go/​controlflow/​Guards/​guards.go Adds comprehensive guard fixtures.
go/​ql/​test/​library-tests/​semmle/​go/​controlflow/​Guards/​Guards.ext.yml Adds barrier-guard models.
go/​ql/​test/​library-tests/​semmle/​go/​controlflow/​Guards/​Guards.expected Records expected guard results.
go/​ql/​test/​experimental/​CWE-942/​CorsMisconfiguration.go Adds switch-based CORS coverage.
go/​ql/​test/​experimental/​CWE-942/​CorsMisconfiguration.expected Updates expected CORS findings.
go/​ql/​test/​experimental/​CWE-942/​CONSISTENCY/​DataFlowConsistency.expected Updates consistency expectations.
go/​ql/​src/​Security/​CWE-327/​InsecureTLS.ql Uses shared flag-control reasoning.
go/​ql/​src/​Security/​CWE-295/​DisabledCertificateCheck.ql Uses shared flag-control reasoning.
go/​ql/​src/​Security/​CWE-209/​StackTraceExposure.ql Migrates debug-flag barriers.
go/​ql/​src/​Security/​CWE-020/​IncompleteHostnameRegexp.ql Migrates regexp guard reasoning.
go/​ql/​src/​InconsistentCode/​LengthComparisonOffByOne.ql Migrates comparison guards.
go/​ql/​src/​InconsistentCode/​ConstantLengthComparison.ql Migrates length guards.
go/​ql/​src/​experimental/​IntegerOverflow/​RangeAnalysis.qll Uses shared range guards.
go/​ql/​src/​experimental/​CWE-942/​CorsMisconfiguration.ql Migrates CORS guard logic.
go/​ql/​src/​experimental/​CWE-807/​SensitiveConditionBypass.ql Migrates sensitive-condition guards.
go/​ql/​lib/​semmle/​go/​security/​InsecureFeatureFlag.qll Adds flag-control abstraction.
go/​ql/​lib/​semmle/​go/​dataflow/​internal/​DataFlowUtil.qll Migrates barrier-guard infrastructure.
go/​ql/​lib/​semmle/​go/​dataflow/​internal/​DataFlowPrivate.qll Migrates unreachable-region guards.
go/​ql/​lib/​semmle/​go/​dataflow/​ExternalFlow.qll Supports all modeled guard values.
go/​ql/​lib/​semmle/​go/​controlflow/​IR.qll Clarifies conditional IR handling.
go/​ql/​lib/​semmle/​go/​controlflow/​Guards.qll Instantiates shared guards for Go.
go/​ql/​lib/​semmle/​go/​controlflow/​ControlFlowGraph.qll Deprecates legacy guard APIs.
go/​ql/​lib/​change-notes/​2026-08-17-shared-guards.md Announces the shared guard API.
go/​ql/​lib/​change-notes/​2026-08-17-deprecate-condition-guard-node.md Announces legacy API deprecation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread go/ql/lib/change-notes/2026-08-17-deprecate-condition-guard-node.md Outdated
@owen-mc
owen-mc requested a review from aschackmull October 9, 2026 15:51
@owen-mc

owen-mc commented Oct 10, 2026

Copy link
Copy Markdown
Contributor Author

There seems to be a performance problem which is causing some DCA jobs to time out. I'll investigate on Tuesday.

@owen-mc
owen-mc force-pushed the go/shared-guards branch 3 times, most recently from c4929fe to dfff37f Compare October 13, 2026 19:20
owen-mc and others added 7 commits October 13, 2026 23:45
Add the complete guard regression suite and fold the shared hooks and Go-specific correctness fixes into the initial adapter.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move simple data-flow and security consumers to shared guards, including value-aware barriers and the corrected feature-flag semantics.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@owen-mc

owen-mc commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

@aschackmull This is now ready to review.

I've resolved the big performance issues. The latest DCA run now shows and average 5% increase in analysis time. I investigated this in a few cases and wasn't able to easily reduce it - I think it is acceptable because we are now doing a more thorough analysis that we used to, e.g. conditions being propagated through variables and phi nodes.

The test failures seem spurious.


class Expr extends G::Expr {
/** Gets the associated control flow node. */
CfgImpl::Cfg::ControlFlowNode getControlFlowNode() { result = IR::evalExprInstruction(this) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intended to be result.injects(this). AFAICS the only difference is for defers, is that a relevant distinction or could we use injects here?

Comment on lines +108 to +109
* This supports implicit equality tests that do not have a corresponding source expression,
* such as comparisons between a switch expression and a case expression.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should already work without an additional input predicate.

@owen-mc owen-mc Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing Case abstraction only handles a single constant case expression, assuming the switch expression is the value being constrained. Go has two additional cases:

  1. The constant may be the switch expression, for example switch false { case x: ... }. Here we need to infer x == false, but asConstantCase() cannot expose x as the compared expression.
  2. A clause may contain multiple expressions, for example case 1, 2. Taking its non-match path implies both value != 1 and value != 2, while taking its match path does not identify which equality succeeded.

I suggest replacing asConstantCase() with an outcome-sensitive relation such as:

predicate constantComparison(
  boolean caseOutcome,
  Expr comparedExpr,
  ConstantExpr constant,
  boolean equal
);

This would model constants on either side and the asymmetric implications of multi-expression cases in the shared library, avoiding the lower-level equalityBranchEdge input.

}
}

predicate caseOutcomeBlock(Case guard, CfgImpl::Cfg::BasicBlock outcomeBlock, boolean branch) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this signature extension? Could you elaborate on what didn't work without it? It seems quite narrow as it's restricted to default cases, so I wonder what hole it's trying to plug.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, this can be removed. It was trying to preserve the old Go situation where we had a fact for "default matched", but that isn't actually needed. I'll push a commit to remove it.

Comment on lines +81 to +87
/**
* Holds if `outcomeBlock` contains a control flow node indicating that
* `guard` evaluated to `branch`.
*/
default predicate booleanOutcomeBlock(Expr guard, BasicBlock outcomeBlock, boolean branch) {
none()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm aware that the Guards library likely needs an extension for covering Guards with after-value nodes that are fully determined by prior branches. I.e. a disjunction in a test position like if (a || b) is currently not working as a guard - only the individual a and b are guards here. Is it correctly understood that it is this particular corner-case that you're trying to handle here? I'd much prefer if such an extension was the subject of its own PR instead. Really, this ties into a potentially larger Guards refactor to better take into account the structure of the new CFG.

@owen-mc owen-mc Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's right. I'll remove it from this PR.

Comment on lines +634 to +640
* Holds if `def` defines the implicit return value represented by `method`, or is an SSA
* definition traversed while determining that value.
*/
default predicate relevantImplicitReturnSsaDefinition(
NonOverridableMethod method, SsaDefinition def
) {
any()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoah! The default is a huge cartesian product?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes, that default is too broad. It was intended as an unrestrictive fallback, with evaluation seeded by implicitReturnDefinition, but it is still conceptually a Cartesian product and is unsafe if used unbound. I’ll change the default to none() and require languages supporting implicit returns to provide a bounded implementation.

private module LogicInput implements GuardsImpl::LogicInputSig {
final private class FinalSsaDefinition = GoSsa::SsaDefinition;

class SsaDefinition extends FinalSsaDefinition {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: If Go used the MakeSsa module exposed by the shared SSA lib to build its SSA classes then the result should be directly pluggable with a one-line import here. (Just something to consider for potential followup).

* This supports languages where a return statement can return a named result
* without containing a return expression.
*/
default predicate implicitReturnDefinition(NonOverridableMethod method, SsaDefinition def) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems very Go-specific. I wonder whether we could instead just include any expressions being written to def as the return expressions instead? That way we'd stay within the existing signature and not need this extension.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with that is that result parameters are implicitly initialized to a zero value, without a corresponding expression, just a synthetic SSA definition. See for example the test guardMultiNamed. (This is probably quite a common pattern in real Go code, to have an Error result parameter, implicitly initialized to nil, and only change its value on branches where there is an error.)

Other possible approaches:

  1. use SSA definitions/writes as return-value sources;
  2. introduce a shared ReturnValue abstraction that can wrap either an expression or a synthetic SSA value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For approach 1, I note that another input predicate I've added, additionalSsaDefinitionValue, could be part of the solution, e.g. if we generalize implicitReturnDefinition to something like:

/**
 * Holds if `def` is the SSA value returned as the result represented by `method`.
 */
default predicate returnValueDefinition(
  NonOverridableMethod method,
  SsaDefinition def
) {
  none()
}

Then the shared library can trace possible values back and additionalSsaDefinitionValue can be used for implicit initializations.

Comment on lines +422 to +427
predicate additionalSsaDefinitionValue(SsaDefinition def, GuardValue value) {
exists(IR::EvalImplicitInitInstruction init |
def.(GoSsa::SsaExplicitDefinition).getInstruction() = init and
value.asBooleanValue() = init.getBoolValue()
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition here looks reasonable, but perhaps somewhat narrow. Was this found to make a difference? Is there a test exercising this case?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relevant test is conditionalNamedResultIsNotNil:

func conditionalNamedResultIsNotNil(pointer *int) (valid bool) {
	if pointer == nil {
		return
	}
	valid = true
	return
}

At the merge point, valid is a phi over the explicit true and the implicit false.

Comment on lines +1258 to +1262
* Holds if `def` evaluating to `defVal` implies that `guard` evaluates to `guardVal`.
*/
predicate ssaImpliesGuard(
NonOverridableMethod method, SsaDefinition def, GuardValue defVal, Guard guard,
GuardValue guardVal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you walk me through what this addition is covering?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers naked returns of named Go results, where there is no return expression to seed the existing analysis. It walks backwards from the SSA definition read in the synthetic return epilogue, through any phis, to recover the guard outcome implied by the returned value. For example, if the result is a phi of implicit false on p == nil and explicit true otherwise, observing true implies p != nil.

owen-mc and others added 3 commits October 19, 2026 11:07
Align Go with Java and C# by deriving useful default-branch facts from the preceding non-match edges rather than treating the default clause itself as a true guard.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep support for compound guards represented by CFG after-value nodes out of the Go shared-guards migration. This behavior can be reconsidered as part of a broader Guards/CFG integration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use an empty default for relevantImplicitReturnSsaDefinition instead of the Cartesian product. Languages supporting implicit returns must provide a bounded implementation; Go already does.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants