Skip to content

fix(G602): report constant index equal to the length asserted by an equality guard - #1746

Open
askalf wants to merge 3 commits into
securego:masterfrom
askalf:fix/g602-equality-guard-index-bounds
Open

askalf wants to merge 3 commits into
securego:masterfrom
askalf:fix/g602-equality-guard-index-bounds

Conversation

@askalf

@askalf askalf commented Sep 14, 2026

Copy link
Copy Markdown

Addresses part of #1727 (the equality-guard case only; see "Not fixed here").

What was wrong

Under an if len(s) == N guard, the bounded / *ssa.IndexAddr arm in analyzers/slice_bounds.go treated a constant index equal to N as in-bounds and deleted the finding. For a slice of length N the valid indices are 0..N-1, so s[N] is the one index that is always out of range: the analyzer dropped exactly the case it exists to catch.

s := make([]int, 0)
if len(s) == 3 {
    fmt.Println(s[3]) // no G602 on master
}

The change

One arm, three small parts:

  • Use the predicate the file already has. isSliceIndexInsideBounds(h, index) is what the sibling upperBounded arm uses a few lines above; the bounded arm had a hand-rolled int(indexValue) == value instead.
  • assertedLen recovers the asserted length when the comparison carries a constant offset, so if len(s)-1 == 1 { s[1] } is still understood as a length of 2 and stays clean. Without it the tighter predicate would have turned that safe code into a false positive.
  • Gate the suppression on the then successor (i == 0). invBound maps bounded to itself, so both successors of the if are walked with the same bound and value; entering the else branch only proves the length is not N, so nothing may be cleared there. Without this gate the tighter predicate would have cleared every constant index in [0, N) inside the else branch, a broader false negative than the one being fixed. That regression was reproduced against an earlier version of this change before the gate was added.

Alternatives considered and rejected: fixing invBound (feeds every bound consumer in the file, including the taint path) and teaching extractBinOpBound to return the offset (four call sites); both widen the blast radius for a one-arm bug.

Tests

New samples in testutils/g602_samples.go, following the >= 2 positive / >= 2 negative convention: the then branch (index equal to the asserted length, offset guards, zero-length guard), the else branch (must keep reporting), reversed operands (N == len(s)), and nested equality guards. Each sample was checked to discriminate on its own: the positive ones fail on master, the negative ones stay green on master and on this branch.

$ go test -p 1 -count=1 ./analyzers/ -args -ginkgo.focus="out of bounds slice access"   # master + the new samples
  [FAILED] Expected <[]*issue.Issue | len:0, cap:16>: [] to have length 1
FAIL	github.com/securego/gosec/v2/analyzers	12.165s

$ go test -p 1 -count=1 ./analyzers/ -args -ginkgo.focus="out of bounds slice access"   # this branch
ok  	github.com/securego/gosec/v2/analyzers	15.174s

$ go test -count=1 ./analyzers/                                                          # full analyzers package, this branch
ok  	github.com/securego/gosec/v2/analyzers	179.608s

gofmt is clean on both touched files. Not run here: go test ./... and golangci-lint (the sandbox this was developed in runs out of memory on the full module build); CI covers both.

Not fixed here

#1727 reports three things. This PR claims only the equality-guard gap. The append case (s = append(s, 10); if len(s) == 3 { s[3] }) still reports nothing with this change applied, so tracking is being lost across append in a different part of the pass, and the >= vs == inconsistency is not touched either. Both are separate defects and belong in their own PRs. While testing this, one pre-existing false positive was also noticed in the neighbouring *ssa.Slice arm (s[0:3] under len(s) == 3); it is not caused or changed by this PR and is left alone.

AI assistance: the bug was found and the fix and samples were drafted with AI tooling in my workflow; the test runs above were executed as pasted. I'm responsible for the change and will handle review feedback.

…quality guard

Under an "if len(s) == N" guard the *ssa.IndexAddr arm of the bounded
case suppressed the issue when the constant index equalled N. Valid
indices of a length-N slice are 0..N-1, so index == N is the one value
that is always out of range, and G602 silently dropped it.

Use isSliceIndexInsideBounds, as the sibling upperBounded arm already
does, and recover the asserted length when the compared expression
carries a constant offset ("len(s)-1 == 1" asserts a length of 2).

Fixes part of securego#1727
invBound maps bounded to itself, so both successors of "if len(s) == N"
are walked with bound=bounded and value=N. Entering the else branch only
proves the length is not N, yet the *ssa.IndexAddr arm cleared every
constant index in [0, N) there.

Restrict the suppression to the then successor (i == 0) and cover the
inverted branch with samples.

Fixes part of securego#1727
…y guards

Adds samples exercising the code paths the equality-guard fix has to hold on
but the existing table did not reach: the constant on the left of the guard,
a positive constant offset on the compared expression, an "else if" chain and
a nested guard inside the outer "else" branch.
@askalf
askalf deployed to security-review September 14, 2026 15:29 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant