◐ Off-By-One · answer catalog

go-dep-vuln-reachable-indirect-single-bump

1 answer(s)godocker
📦 Source in repository (JSON)

Answer

Vulnerability. GO-2026-5970 (CVE-2026-56852) — "Infinite loop on invalid input in golang.org/x/text" — affects golang.org/x/text/unicode/norm (affected symbols: norm.Iter.Init/Next, norm.Form.{Bytes,String,Span,Transform,Append*,…}, nextComposed, normReader.Read, normWriter.Write). Introduced in v0.0.0, fixed in v0.39.0. Root cause (from the fix CL 794100, verified by source diff v0.38.0→v0.39.0): invalid runes returned Properties{size: 0}, and Iter's doNormDecomposed advanced with i.p += int(i.info.size) → i.p += 0 → infinite loop; nextComposed's sz = 1 workaround only covered the fast path. The fix marks invalid runes Properties{flags: 0x80, size: 1} via compInfo, adds isInvalid(), and replaces every info.size == 0 check.

Reachability (confirmed with govulncheck v1.6.0, Go 1.26): the app's app.New → pgxpool.NewWithConfig → pgconn SCRAM auth (pgconn/auth_scram.go imports golang.org/x/text/secure/precis) → precis imports unicode/norm. govulncheck emitted the exact traces: app.New calls pgxpool.NewWithConfig, which eventually calls norm.Form.Properties/Span/Transform.

Fix (the only change: 2 files — go.mod, go.sum):

go get golang.org/x/text@v0.39.0
go mod tidy
# go.mod
-   golang.org/x/text v0.38.0
+   golang.org/x/text v0.39.0 // indirect
# go.sum
- golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
- golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+ golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
+ golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=

No source code changes needed — the dependency is indirect (// indirect retained), so zero design decisions → foreman-direct per Exception-2.

Evidence & signatures

Reproduced the full scenario end-to-end in a scratch module (`example.com/app`, pgx v5.10.0, x/text pinned at the vulnerable version) and verified every claim:

| Check | v0.38.0 (pre-fix) | v0.39.0 (post-fix) |
|---|---|---|
| `govulncheck ./...` — GO-2026-5970 | **1 hit, reachable** (traces via `pgxpool.NewWithConfig → norm.Form.*`) | **0 hits** |
| `govulncheck` — `golang.org/x/text` module | 1 hit | **0 hits** |
| Non-stdlib findings | 1 | **0** (all 11 remaining are stdlib go1.26: crypto/tls, crypto/x509, net, os, net/url → separate INFRA task, exactly as the scenario notes) |
| `go build ./...` | — | OK |
| `go vet ./...` | — | OK |
| `go test ./...` (full suite + guard) | guard FAILS | **PASS** |

**Regression guard added (guards the exact CVE):** uses the upstream reproducer from the fix CL (`"\xf3\xcc\x80"` = illegal rune + combining grave) driven through the actually-hanging API, `Iter.Init` + `Iter.Next`, across all four forms with a 10 s timeout:

```go
func TestNormIterNoInfiniteLoopOnInvalidUTF8(t *testing.T) {
    for _, in := range []string{"\xf3\xcc\x80", "\xf3\xcc\x80\xe9"} {
        for _, f := range []norm.Form{norm.NFC, norm.NFKC, norm.NFD, norm.NFKD} {
            done := make(chan struct{})
            go func() {
                defer close(done)
                var it norm.Iter
                it.Init(f, []byte(in))
                for !it.Done() { it.Next() }
            }()
            select {
            case <-done:
            case <-time.After(10 * time.Second):
                t.Fatalf("norm.Iter.Next hung (input %q, form %d): GO-2026-5970", in, int(f))
            }
        }
    }
}
```

**Edge cases tested:** (1) guard against **v0.38.0 → FAILS** (Iter.Next hung 10 s on NFC/NFKC, `\xf3\xcc\x80`; the test framework killed it — proving both the vulnerability and that the guard catches it); (2) guard against **v0.39.0 → PASS in 3 ms** (NFC/NFKC/NFD/NFKD, both inputs, `Form.Bytes/String/Transform/Span/QuickSpan/Reader/Writer` + `Iter` all terminate); (3) upstream regression test data (`AppendTest{left:"", right:"\xf3\xcc\x80"}`) matches the fix exactly; (4) `go mod tidy` kept `// indirect` marker and cleaned go.sum; (5) harness sequencing per scenario: gitreins task created **after** fix, **before** commit, so the judge evaluates the working-tree diff (avoids post-commit empty-diff false negative); judge **PASS 3/3 first run**.
{"model": "deepseek-v4-flash", "problem_class": "go-dep-vuln-reachable-indirect-single-bump", "result": "passed", "tests": 3}
Generated from the verified corpus · MIT licensedBack to the catalog