diff --git a/VERSION b/VERSION index 6f84a79fe4133f..5378f3c07548b6 100644 --- a/VERSION +++ b/VERSION @@ -1,2 +1,2 @@ -go1.26.2 -time 2026-03-27T21:58:29Z +go1.26.3 +time 2026-05-04T20:36:18Z diff --git a/lib/fips140/certified.txt b/lib/fips140/certified.txt new file mode 100644 index 00000000000000..efd3caba85ba62 --- /dev/null +++ b/lib/fips140/certified.txt @@ -0,0 +1 @@ +v1.0.0-c2097c7c diff --git a/lib/fips140/inprocess.txt b/lib/fips140/inprocess.txt index efd3caba85ba62..426c2fb87f5d08 100644 --- a/lib/fips140/inprocess.txt +++ b/lib/fips140/inprocess.txt @@ -1 +1 @@ -v1.0.0-c2097c7c +v1.26.0 diff --git a/src/cmd/compile/internal/bloop/bloop.go b/src/cmd/compile/internal/bloop/bloop.go index b8ff1f0fbdf168..361c0a9a05ba88 100644 --- a/src/cmd/compile/internal/bloop/bloop.go +++ b/src/cmd/compile/internal/bloop/bloop.go @@ -139,6 +139,16 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) { ret = stmt switch n := stmt.(type) { case *ir.AssignStmt: + // If the left hand side is blank, we need to assign it to a temp + // so that it can be kept alive. + if ir.IsBlank(n.X) { + tmp := typecheck.TempAt(n.Pos(), curFn, n.Y.Type()) + n.X = tmp + n.Def = true + n.PtrInit().Append(typecheck.Stmt(ir.NewDecl(n.Pos(), ir.ODCL, tmp))) + stmt = typecheck.AssignExpr(n) + n = stmt.(*ir.AssignStmt) + } // Peel down struct and slice indexing to get the names name := getAddressableNameFromNode(n.X) if name != nil { @@ -154,7 +164,30 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) { } case *ir.AssignListStmt: ns := []ir.Node{} - for _, lhs := range n.Lhs { + hasBlank := false + for i, lhs := range n.Lhs { + if ir.IsBlank(lhs) { + // If the left hand side has blanks, we need to assign them to temps + // so that they can be kept alive. + var typ *types.Type + // AssignListStmt can have tuple or a list of expressions on the right hand side. + if len(n.Rhs) == 1 && n.Rhs[0].Type() != nil && + n.Rhs[0].Type().IsTuple() && + len(n.Lhs) == n.Rhs[0].Type().NumFields() { + typ = n.Rhs[0].Type().Field(i).Type + } else if len(n.Rhs) == len(n.Lhs) { + typ = n.Rhs[i].Type() + } else { + // Unrecognized shapes, skip? + base.WarnfAt(n.Pos(), "unrecognized shape for assign list stmt for blank assignment") + continue + } + tmp := typecheck.TempAt(n.Pos(), curFn, typ) + n.Lhs[i] = tmp + n.PtrInit().Append(typecheck.Stmt(ir.NewDecl(n.Pos(), ir.ODCL, tmp))) + hasBlank = true + lhs = tmp + } name := getAddressableNameFromNode(lhs) if name != nil { debugName(name, n.Pos()) @@ -168,6 +201,12 @@ func preserveStmt(curFn *ir.Func, stmt ir.Node) (ret ir.Node) { base.WarnfAt(n.Pos(), "expr is unknown to bloop pass") } } + if hasBlank { + // blank nodes are rewritten to temps, we need to typecheck the node again. + n.Def = true + stmt = typecheck.AssignExpr(n) + n = stmt.(*ir.AssignListStmt) + } ret = keepAliveAt(ns, n) case *ir.AssignOpStmt: name := getAddressableNameFromNode(n.X) diff --git a/src/cmd/compile/internal/devirtualize/devirtualize.go b/src/cmd/compile/internal/devirtualize/devirtualize.go index 5f1da236545ac4..59ddc2e5366cd6 100644 --- a/src/cmd/compile/internal/devirtualize/devirtualize.go +++ b/src/cmd/compile/internal/devirtualize/devirtualize.go @@ -293,9 +293,22 @@ func concreteType1(s *State, n ir.Node, seen map[*ir.Name]struct{}) (outT *types continue } } - if t == nil || (typ != nil && !types.Identical(typ, t)) { - return nil + if t == nil { + return nil // unknown concrete type } + + // Methods are only declared on named types, and each named type + // is represented by a unique [*types.Type], thus pointer comparison + // is fine here. + // + // The only scenario where [types.IdenticalStrict] could help here is with + // unnamed struct types that embed another type (e.g. foo = struct { Impl }{}). + // However, such patterns are uncommon and not worth the additional complexity + // in the devirtualizer. + if typ != nil && typ != t { + return nil // assigned with a different type + } + typ = t } diff --git a/src/cmd/compile/internal/loopvar/loopvar_test.go b/src/cmd/compile/internal/loopvar/loopvar_test.go index 0265dd06eec788..c4ff8f620dc5e6 100644 --- a/src/cmd/compile/internal/loopvar/loopvar_test.go +++ b/src/cmd/compile/internal/loopvar/loopvar_test.go @@ -6,6 +6,7 @@ package loopvar_test import ( "internal/testenv" + "os" "os/exec" "path/filepath" "regexp" @@ -381,3 +382,62 @@ func TestLoopVarVersionDisableGoBuild(t *testing.T) { t.Errorf("err=%v == nil", err) } } + +// TestLoopVarLineDirective tests that loopvar version detection works correctly +// with line directives. This is a regression test for a bug where FileBase() was +// used instead of Base(), causing incorrect version lookup when line directives +// were present. +func TestLoopVarLineDirective(t *testing.T) { + switch runtime.GOOS { + case "linux", "darwin": + default: + t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS) + } + switch runtime.GOARCH { + case "amd64", "arm64": + default: + t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH) + } + + testenv.MustHaveGoBuild(t) + gocmd := testenv.GoToolPath(t) + tmpdir := t.TempDir() + output := filepath.Join(tmpdir, "foo.exe") + + // Create a go.mod file with Go 1.21 to test compatibility behavior. + // When building with a higher Go compiler, the loopvar should be created per-loop. + gomodPath := filepath.Join(tmpdir, "go.mod") + if err := os.WriteFile(gomodPath, []byte("module test\n\ngo 1.21\n"), 0644); err != nil { + t.Fatal(err) + } + + // Copy the test file (with line directive) to the temporary module + testFile := "range_esc_closure_linedir.go" + srcPath := filepath.Join("testdata", testFile) + dstPath := filepath.Join(tmpdir, testFile) + src, err := os.ReadFile(srcPath) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dstPath, src, 0644); err != nil { + t.Fatal(err) + } + + // Build the module (not as a single file, so go.mod is respected) + cmd := testenv.Command(t, gocmd, "build", "-o", output, ".") + cmd.Dir = tmpdir + b, err := cmd.CombinedOutput() + if err != nil { + t.Logf("build output: %s", b) + t.Fatal(err) + } + t.Logf("build output: %s", b) + + cmd = testenv.Command(t, output) + b, err = cmd.CombinedOutput() + t.Logf("run output: %s", b) + + if err != nil { + t.Errorf("expected success (exit code 0), got: %v", err) + } +} diff --git a/src/cmd/compile/internal/loopvar/testdata/range_esc_closure_linedir.go b/src/cmd/compile/internal/loopvar/testdata/range_esc_closure_linedir.go new file mode 100644 index 00000000000000..24823e8990f29f --- /dev/null +++ b/src/cmd/compile/internal/loopvar/testdata/range_esc_closure_linedir.go @@ -0,0 +1,24 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//line range_esc_closure_linedir.go:5 +package main + +import "fmt" + +var is []func() int + +func main() { + var ints = []int{0, 0, 0} + for i := range ints { + is = append(is, func() int { return i }) + } + + for _, f := range is { + fmt.Println(f()) + if f() != 2 { + panic("loop variable i: expected shared per-loop, but got distinct per-iteration") + } + } +} diff --git a/src/cmd/compile/internal/noder/writer.go b/src/cmd/compile/internal/noder/writer.go index 0b5aa007bf9404..e772328b0a5b97 100644 --- a/src/cmd/compile/internal/noder/writer.go +++ b/src/cmd/compile/internal/noder/writer.go @@ -1557,7 +1557,7 @@ func (w *writer) forStmt(stmt *syntax.ForStmt) { func (w *writer) distinctVars(stmt *syntax.ForStmt) bool { lv := base.Debug.LoopVar - fileVersion := w.p.info.FileVersions[stmt.Pos().Base()] + fileVersion := w.p.info.FileVersions[stmt.Pos().FileBase()] is122 := fileVersion == "" || version.Compare(fileVersion, "go1.22") >= 0 // Turning off loopvar for 1.22 is only possible with loopvarhash=qn diff --git a/src/cmd/compile/internal/ssa/loopbce.go b/src/cmd/compile/internal/ssa/loopbce.go index a63a314043fe89..84811647cedcd1 100644 --- a/src/cmd/compile/internal/ssa/loopbce.go +++ b/src/cmd/compile/internal/ssa/loopbce.go @@ -145,6 +145,12 @@ func findIndVar(f *Func) []indVar { if step == 0 { continue } + // step == minInt64 cannot be safely negated below, because -step + // overflows back to minInt64. The later underflow checks need a + // positive magnitude, so reject this case here. + if step == minSignedValue(ind.Type) { + continue + } // startBody is the edge that eventually returns to the loop header. var startBody Edge diff --git a/src/cmd/compile/internal/ssa/prove.go b/src/cmd/compile/internal/ssa/prove.go index de16dfb3406eec..27afa8e33a628f 100644 --- a/src/cmd/compile/internal/ssa/prove.go +++ b/src/cmd/compile/internal/ssa/prove.go @@ -1517,124 +1517,6 @@ func prove(f *Func) { // Since this induction variable is not used for anything but counting the iterations, // no point in putting it into the facts table. } - - // try to rewrite to a downward counting loop checking against start if the - // loop body does not depend on ind or nxt and end is known before the loop. - // This reduces pressure on the register allocator because this does not need - // to use end on each iteration anymore. We compare against the start constant instead. - // That means this code: - // - // loop: - // ind = (Phi (Const [x]) nxt), - // if ind < end - // then goto enter_loop - // else goto exit_loop - // - // enter_loop: - // do something without using ind nor nxt - // nxt = inc + ind - // goto loop - // - // exit_loop: - // - // is rewritten to: - // - // loop: - // ind = (Phi end nxt) - // if (Const [x]) < ind - // then goto enter_loop - // else goto exit_loop - // - // enter_loop: - // do something without using ind nor nxt - // nxt = ind - inc - // goto loop - // - // exit_loop: - // - // this is better because it only requires to keep ind then nxt alive while looping, - // while the original form keeps ind then nxt and end alive - start, end := v.min, v.max - if v.flags&indVarCountDown != 0 { - start, end = end, start - } - - if !start.isGenericIntConst() { - // if start is not a constant we would be winning nothing from inverting the loop - continue - } - if end.isGenericIntConst() { - // TODO: if both start and end are constants we should rewrite such that the comparison - // is against zero and nxt is ++ or -- operation - // That means: - // for i := 2; i < 11; i += 2 { - // should be rewritten to: - // for i := 5; 0 < i; i-- { - continue - } - - if end.Block == ind.Block { - // we can't rewrite loops where the condition depends on the loop body - // this simple check is forced to work because if this is true a Phi in ind.Block must exist - continue - } - - check := ind.Block.Controls[0] - // invert the check - check.Args[0], check.Args[1] = check.Args[1], check.Args[0] - - // swap start and end in the loop - for i, v := range check.Args { - if v != end { - continue - } - - check.SetArg(i, start) - goto replacedEnd - } - panic(fmt.Sprintf("unreachable, ind: %v, start: %v, end: %v", ind, start, end)) - replacedEnd: - - for i, v := range ind.Args { - if v != start { - continue - } - - ind.SetArg(i, end) - goto replacedStart - } - panic(fmt.Sprintf("unreachable, ind: %v, start: %v, end: %v", ind, start, end)) - replacedStart: - - if nxt.Args[0] != ind { - // unlike additions subtractions are not commutative so be sure we get it right - nxt.Args[0], nxt.Args[1] = nxt.Args[1], nxt.Args[0] - } - - switch nxt.Op { - case OpAdd8: - nxt.Op = OpSub8 - case OpAdd16: - nxt.Op = OpSub16 - case OpAdd32: - nxt.Op = OpSub32 - case OpAdd64: - nxt.Op = OpSub64 - case OpSub8: - nxt.Op = OpAdd8 - case OpSub16: - nxt.Op = OpAdd16 - case OpSub32: - nxt.Op = OpAdd32 - case OpSub64: - nxt.Op = OpAdd64 - default: - panic("unreachable") - } - - if f.pass.debug > 0 { - f.Warnl(ind.Pos, "Inverted loop iteration") - } } ft := newFactsTable(f) diff --git a/src/cmd/compile/internal/types2/builtins.go b/src/cmd/compile/internal/types2/builtins.go index 549d94615bccec..c0073c5136b0c7 100644 --- a/src/cmd/compile/internal/types2/builtins.go +++ b/src/cmd/compile/internal/types2/builtins.go @@ -112,7 +112,7 @@ func (check *Checker) builtin(x *operand, call *syntax.CallExpr, id builtinId) ( for _, u := range typeset(y.typ) { if s, _ := u.(*Slice); s != nil && Identical(s.elem, universeByte) { // typeset ⊇ {[]byte} - } else if isString(u) { + } else if u != nil && isString(u) { // typeset ⊇ {string} hasString = true } else { @@ -375,7 +375,7 @@ func (check *Checker) builtin(x *operand, call *syntax.CallExpr, id builtinId) ( for _, u := range typeset(y.typ) { if s, _ := u.(*Slice); s != nil && Identical(s.elem, universeByte) { // typeset ⊇ {[]byte} - } else if isString(u) { + } else if u != nil && isString(u) { // typeset ⊇ {string} } else { special = false diff --git a/src/cmd/compile/internal/types2/issues_test.go b/src/cmd/compile/internal/types2/issues_test.go index 8ddb39987a5db0..d0b1f4e36c1451 100644 --- a/src/cmd/compile/internal/types2/issues_test.go +++ b/src/cmd/compile/internal/types2/issues_test.go @@ -631,9 +631,13 @@ func TestIssue55030(t *testing.T) { makeSig := func(typ Type, valid bool) { if !valid { defer func() { - if recover() == nil { + r := recover() + if r == nil { panic("NewSignatureType panic expected") } + if _, ok := r.(string); !ok { + panic("NewSignatureType string panic expected") + } }() } par := NewParam(nopos, nil, "", typ) diff --git a/src/cmd/compile/internal/types2/signature.go b/src/cmd/compile/internal/types2/signature.go index d569ba8013954a..13f99885ab5ce9 100644 --- a/src/cmd/compile/internal/types2/signature.go +++ b/src/cmd/compile/internal/types2/signature.go @@ -61,6 +61,9 @@ func NewSignatureType(recv *Var, recvTypeParams, typeParams []*TypeParam, params last := params.At(n - 1).typ var S *Slice for t := range typeset(last) { + if t == nil { + break + } var s *Slice if isString(t) { s = NewSlice(universeByte) diff --git a/src/cmd/compile/internal/types2/under.go b/src/cmd/compile/internal/types2/under.go index 98c62733c7cd99..15fca73522278a 100644 --- a/src/cmd/compile/internal/types2/under.go +++ b/src/cmd/compile/internal/types2/under.go @@ -26,12 +26,12 @@ func all(t Type, f func(t, u Type) bool) bool { // typeset is an iterator over the (type/underlying type) pairs of the // specific type terms of the type set implied by t. // If t is a type parameter, the implied type set is the type set of t's constraint. -// In that case, if there are no specific terms, typeset calls yield with (nil, nil). +// In this case, if there are no specific terms, the iterator produces (nil, nil). // If t is not a type parameter, the implied type set consists of just t. -// In any case, typeset is guaranteed to call yield at least once. +// In any case, typeset is guaranteed to produce at least one set of results. func typeset(t Type) iter.Seq2[Type, Type] { return func(yield func(t, u Type) bool) { - _ = all(t, yield) + all(t, yield) } } diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index 14020d4f96ffeb..5aad9432c8a388 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -507,7 +507,8 @@ // It supports these flags: // // -diff -// instead of applying each fix, print the patch as a unified diff +// instead of applying each fix, print the patch as a unified diff; +// exit with a non-zero status if the diff is not empty // // The -fixtool=prog flag selects a different analysis tool with // alternative or additional fixers; see the documentation for go vet's @@ -2046,7 +2047,8 @@ // -fix // instead of printing each diagnostic, apply its first fix (if any) // -diff -// instead of applying each fix, print the patch as a unified diff +// instead of applying each fix, print the patch as a unified diff; +// exit with a non-zero status if the diff is not empty // // The -vettool=prog flag selects a different analysis tool with // alternative or additional checks. For example, the 'shadow' analyzer diff --git a/src/cmd/go/internal/bug/bug.go b/src/cmd/go/internal/bug/bug.go index 749edc51cf1d03..f967e9a8ef5725 100644 --- a/src/cmd/go/internal/bug/bug.go +++ b/src/cmd/go/internal/bug/bug.go @@ -184,14 +184,14 @@ func firstLine(buf []byte) []byte { // printGlibcVersion prints information about the glibc version. // It ignores failures. func printGlibcVersion(w io.Writer) { - tempdir := os.TempDir() - if tempdir == "" { + tempdir, err := os.MkdirTemp("", "") + if err != nil { return } src := []byte(`int main() {}`) srcfile := filepath.Join(tempdir, "go-bug.c") outfile := filepath.Join(tempdir, "go-bug") - err := os.WriteFile(srcfile, src, 0644) + err = os.WriteFile(srcfile, src, 0644) if err != nil { return } diff --git a/src/cmd/go/internal/modfetch/fetch.go b/src/cmd/go/internal/modfetch/fetch.go index 7c9280f1d090bd..ed384c3c43fb75 100644 --- a/src/cmd/go/internal/modfetch/fetch.go +++ b/src/cmd/go/internal/modfetch/fetch.go @@ -870,7 +870,7 @@ func checkSumDB(mod module.Version, h string) error { return module.VersionError(modWithoutSuffix, fmt.Errorf("verifying %s: checksum mismatch\n\tdownloaded: %v\n\t%s: %v"+sumdbMismatch, noun, h, db, line[len(prefix)-len("h1:"):])) } } - return nil + return module.VersionError(modWithoutSuffix, fmt.Errorf("verifying %s: checksum missing from sumdb response"+sumdbAbsent, noun)) } // Sum returns the checksum for the downloaded copy of the given module, @@ -1080,6 +1080,19 @@ have intercepted the download attempt. For more information, see 'go help module-auth'. ` +const sumdbAbsent = ` + +SECURITY ERROR +This download does NOT match one reported by the checksum server. +The checksum server has provided checksums, but the checksums do +not contain an entry for the download. +The checksum server may be malfunctioning, or an attacker may have +intercepted the checksum request. +The download cannot be verified. + +For more information, see 'go help module-auth'. +` + const hashVersionMismatch = ` SECURITY WARNING diff --git a/src/cmd/go/internal/test/test.go b/src/cmd/go/internal/test/test.go index e55d98c5e4e49c..8d7987df26294b 100644 --- a/src/cmd/go/internal/test/test.go +++ b/src/cmd/go/internal/test/test.go @@ -1417,9 +1417,10 @@ type runTestActor struct { type runCache struct { disableCache bool // cache should be disabled for this run - buf *bytes.Buffer - id1 cache.ActionID - id2 cache.ActionID + buf *bytes.Buffer + id1 cache.ActionID + id2 cache.ActionID + covMeta cache.ActionID // Hash of writeCoverMetaAct dependencies, for invalidating coverage profiles } func coverProfTempFile(a *work.Action) string { @@ -1812,6 +1813,33 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo return false } + // If we are collecting coverage for out-of-band packages (-coverpkg), + // find the writeCoverMetaAct among the run action's dependencies and hash + // its deps to ensure the cache invalidates when covered packages change. + // Note: the run action's original deps may be wrapped inside a "test barrier" + // action, so we search both a.Deps and any barrier's deps. + if len(testCoverPkgs) != 0 { + searchDeps := a.Deps + for _, dep := range a.Deps { + if dep.Mode == "test barrier" { + searchDeps = dep.Deps + break + } + } + for _, dep := range searchDeps { + if dep.Mode == "write coverage meta-data file" { + h := cache.NewHash("covermeta") + for _, metaDep := range dep.Deps { + if aid := metaDep.BuildActionID(); aid != "" { + fmt.Fprintf(h, "dep %s\n", aid) + } + } + c.covMeta = h.Sum() + break + } + } + } + var cacheArgs []string for _, arg := range testArgs { i := strings.Index(arg, "=") @@ -1918,7 +1946,7 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo // Merge cached cover profile data to cover profile. if testCoverProfile != "" { // Specifically ignore entry as it will be the same as above. - cpData, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID)) + cpData, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID, c.covMeta)) if err != nil { if cache.DebugTest { fmt.Fprintf(os.Stderr, "testcache: %s: cached cover profile missing: %v\n", a.Package.ImportPath, err) @@ -1926,6 +1954,18 @@ func (c *runCache) tryCacheWithID(b *work.Builder, a *work.Action, id string) bo return false } mergeCoverProfile(cpData) + } else if c.covMeta != (cache.ActionID{}) { + // If we have a coverage metadata hash but no testCoverProfile, we're collecting + // coverage for out-of-band packages. Check if the coverage profile cache is still + // valid. If c.covMeta changed (meaning a covered package changed), the coverage + // profile cache will miss and we need to re-run the test. + _, _, err := cache.GetFile(cache.Default(), coverProfileAndInputKey(testID, testInputsID, c.covMeta)) + if err != nil { + if cache.DebugTest { + fmt.Fprintf(os.Stderr, "testcache: %s: coverage metadata changed, re-running test: %v\n", a.Package.ImportPath, err) + } + return false + } } if len(data) == 0 || data[len(data)-1] != '\n' { @@ -2116,9 +2156,15 @@ func testAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID { return cache.Subkey(testID, fmt.Sprintf("inputs:%x", testInputsID)) } -// coverProfileAndInputKey returns the "coverprofile" cache key for the pair (testID, testInputsID). -func coverProfileAndInputKey(testID, testInputsID cache.ActionID) cache.ActionID { - return cache.Subkey(testAndInputKey(testID, testInputsID), "coverprofile") +// coverProfileAndInputKey returns the "coverprofile" cache key. +// If covMetaID is non-zero, it is included in the hash to ensure coverage profiles are invalidated +// when the coverage metadata changes (e.g., when source files in covered packages are modified). +func coverProfileAndInputKey(testID, testInputsID, covMetaID cache.ActionID) cache.ActionID { + key := testAndInputKey(testID, testInputsID) + if covMetaID != (cache.ActionID{}) { + key = cache.Subkey(key, fmt.Sprintf("coverdeps:%x", covMetaID)) + } + return cache.Subkey(key, "coverprofile") } func (c *runCache) saveOutput(a *work.Action) { @@ -2159,7 +2205,11 @@ func (c *runCache) saveOutput(a *work.Action) { cache.PutNoVerify(cache.Default(), c.id1, bytes.NewReader(testlog)) cache.PutNoVerify(cache.Default(), testAndInputKey(c.id1, testInputsID), bytes.NewReader(a.TestOutput.Bytes())) if coverProfile != nil { - cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID), bytes.NewReader(coverProfile)) + cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID, c.covMeta), bytes.NewReader(coverProfile)) + } else if c.covMeta != (cache.ActionID{}) { + // Write a sentinel so the else-if branch in tryCacheWithID can verify + // that the covMeta hash has not changed since the last run. + cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id1, testInputsID, c.covMeta), bytes.NewReader(nil)) } } if c.id2 != (cache.ActionID{}) { @@ -2169,7 +2219,10 @@ func (c *runCache) saveOutput(a *work.Action) { cache.PutNoVerify(cache.Default(), c.id2, bytes.NewReader(testlog)) cache.PutNoVerify(cache.Default(), testAndInputKey(c.id2, testInputsID), bytes.NewReader(a.TestOutput.Bytes())) if coverProfile != nil { - cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID), bytes.NewReader(coverProfile)) + cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID, c.covMeta), bytes.NewReader(coverProfile)) + } else if c.covMeta != (cache.ActionID{}) { + // Sentinel for covMeta validity; see comment in id1 block above. + cache.PutNoVerify(cache.Default(), coverProfileAndInputKey(c.id2, testInputsID, c.covMeta), bytes.NewReader(nil)) } } } diff --git a/src/cmd/go/internal/vet/vet.go b/src/cmd/go/internal/vet/vet.go index e7d01782bf2f6c..68357f81b3c39a 100644 --- a/src/cmd/go/internal/vet/vet.go +++ b/src/cmd/go/internal/vet/vet.go @@ -44,7 +44,8 @@ It supports these flags: -fix instead of printing each diagnostic, apply its first fix (if any) -diff - instead of applying each fix, print the patch as a unified diff + instead of applying each fix, print the patch as a unified diff; + exit with a non-zero status if the diff is not empty The -vettool=prog flag selects a different analysis tool with alternative or additional checks. For example, the 'shadow' analyzer @@ -81,7 +82,8 @@ and applies suggested fixes. It supports these flags: -diff - instead of applying each fix, print the patch as a unified diff + instead of applying each fix, print the patch as a unified diff; + exit with a non-zero status if the diff is not empty The -fixtool=prog flag selects a different analysis tool with alternative or additional fixers; see the documentation for go vet's @@ -165,8 +167,8 @@ func run(ctx context.Context, cmd *base.Command, args []string) { // command args tool args // go vet => cmd/vet -json Parse stdout, print diagnostics to stderr. // go vet -json => cmd/vet -json Pass stdout through. - // go vet -fix [-diff] => cmd/vet -fix [-diff] Pass stdout through. - // go fix [-diff] => cmd/fix -fix [-diff] Pass stdout through. + // go vet -fix [-diff] => cmd/vet -fix [-diff] Pass stdout through (and exit 1 if diffs). + // go fix [-diff] => cmd/fix -fix [-diff] Pass stdout through (and exit 1 if diffs). // go fix -json => cmd/fix -json Pass stdout through. // // Notes: @@ -189,6 +191,10 @@ func run(ctx context.Context, cmd *base.Command, args []string) { toolFlags = append(toolFlags, "-fix") if diffFlag { toolFlags = append(toolFlags, "-diff") + // In -diff mode, the tool prints unified diffs to stdout. + // Copy stdout through and exit non-zero if diffs were printed, + // consistent with gofmt -d and go mod tidy -diff. + work.VetHandleStdout = copyAndDetectDiff } else { applyFixes = true } @@ -343,6 +349,24 @@ func readZip(zipfile string, out map[string][]byte) error { return nil } +// copyAndDetectDiff copies the tool's stdout to the go command's stdout +// and sets exit status 1 if any output was produced (meaning diffs exist). +// This is used in -diff mode to implement the convention that "go fix -diff" +// exits non-zero when the diff is not empty, consistent with gofmt -d +// and go mod tidy -diff. +func copyAndDetectDiff(r io.Reader) error { + stdouterrMu.Lock() + defer stdouterrMu.Unlock() + n, err := io.Copy(os.Stdout, r) + if err != nil { + return fmt.Errorf("copying diff output: %w", err) + } + if n > 0 { + base.SetExitStatus(1) + } + return nil +} + // printJSONDiagnostics parses JSON (from the tool's stdout) and // prints it (to stderr) in "file:line: message" form. // It also ensures that we exit nonzero if there were diagnostics. @@ -386,11 +410,11 @@ func printJSONDiagnostics(r io.Reader) error { return nil } -var stderrMu sync.Mutex // serializes concurrent writes to stdout +var stdouterrMu sync.Mutex // serializes concurrent writes to stdout and stderr func printJSONDiagnostic(analyzer string, diag jsonDiagnostic) { - stderrMu.Lock() - defer stderrMu.Unlock() + stdouterrMu.Lock() + defer stdouterrMu.Unlock() type posn struct { file string diff --git a/src/cmd/go/internal/work/cover.go b/src/cmd/go/internal/work/cover.go index fc96f67d6e8a0b..be090ce4c3b50b 100644 --- a/src/cmd/go/internal/work/cover.go +++ b/src/cmd/go/internal/work/cover.go @@ -24,7 +24,7 @@ import ( func (b *Builder) CovData(a *Action, cmdargs ...any) ([]byte, error) { cmdline := str.StringList(cmdargs...) args := append([]string{}, cfg.BuildToolexec...) - args = append(args, "go", "tool", "covdata") + args = append(args, filepath.Join(cfg.GOROOTbin, "go"), "tool", "covdata") args = append(args, cmdline...) return b.Shell(a).runOut(a.Objdir, nil, args) } diff --git a/src/cmd/go/proxy_test.go b/src/cmd/go/proxy_test.go index 5ff81361c9609a..2a8bacec186819 100644 --- a/src/cmd/go/proxy_test.go +++ b/src/cmd/go/proxy_test.go @@ -172,6 +172,23 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) { return } + // Request for $GOPROXY/sumdb-redirect/module@version:/lookup/... + // performs a lookup for module@version rather than the requested module. + if strings.HasPrefix(path, "sumdb-redirect/") { + redirect, rest, ok := strings.Cut(path[len("sumdb-redirect"):], ":") + if !ok { + w.WriteHeader(500) + return + } + if strings.HasPrefix(rest, "/lookup/") { + r.URL.Path = "/lookup" + redirect + } else { + r.URL.Path = rest + } + sumdbServer.ServeHTTP(w, r) + return + } + // Request for $GOPROXY/redirect//... goes to redirects. if strings.HasPrefix(path, "redirect/") { path = path[len("redirect/"):] diff --git a/src/cmd/go/testdata/script/cover_switch_toolchain.txt b/src/cmd/go/testdata/script/cover_switch_toolchain.txt new file mode 100644 index 00000000000000..dace804fb6cb27 --- /dev/null +++ b/src/cmd/go/testdata/script/cover_switch_toolchain.txt @@ -0,0 +1,12 @@ +go test -cover -n ./... +[!GOOS:windows] stderr $GOROOT'/bin/go tool covdata' +[GOOS:windows] stderr '\\\\bin\\\\go" tool covdata' + +-- go.mod -- +module example.com/m + +go 1.25.0 +-- m.go -- +package main + +func main() {} diff --git a/src/cmd/go/testdata/script/fipssnap.txt b/src/cmd/go/testdata/script/fipssnap.txt index 4d96aedf2a472d..4f2d486c61990f 100644 --- a/src/cmd/go/testdata/script/fipssnap.txt +++ b/src/cmd/go/testdata/script/fipssnap.txt @@ -1,4 +1,4 @@ -env snap=v1.0.0-c2097c7c +env snap=v1.26.0 env alias=inprocess env GOFIPS140=$snap diff --git a/src/cmd/go/testdata/script/fix_diff_exitcode.txt b/src/cmd/go/testdata/script/fix_diff_exitcode.txt new file mode 100644 index 00000000000000..2622c0265b6ca4 --- /dev/null +++ b/src/cmd/go/testdata/script/fix_diff_exitcode.txt @@ -0,0 +1,36 @@ +# Test that go fix -diff exits with non-zero status when diffs exist, +# and exits with zero status when no diffs are needed. +# This is consistent with gofmt -d (#46289) and go mod tidy -diff (#27005). + +# When the source needs fixes, go fix -diff should print the diff +# and exit with a non-zero code. +! go fix -diff example.com/needsfix +stdout 'net.JoinHostPort' + +# When the source is already clean, go fix -diff should print nothing +# and exit with a zero code. +go fix -diff example.com/clean +! stdout . + +-- go.mod -- +module example.com +go 1.26 + +-- needsfix/x.go -- +package needsfix + +import ( + "fmt" + "net" +) + +var s string +var _, _ = net.Dial("tcp", fmt.Sprintf("%s:%d", s, 80)) + +-- clean/x.go -- +package clean + +import "net" + +var s string +var _, _ = net.Dial("tcp", net.JoinHostPort(s, "80")) diff --git a/src/cmd/go/testdata/script/fix_suite.txt b/src/cmd/go/testdata/script/fix_suite.txt index 455629dc172f30..28ef96e4d1cacd 100644 --- a/src/cmd/go/testdata/script/fix_suite.txt +++ b/src/cmd/go/testdata/script/fix_suite.txt @@ -5,9 +5,9 @@ # Each assertion matches the expected diff. # # Tip: to see the actual stdout, -# temporarily prefix the go command with "! ". +# temporarily remove the "! " prefix from the go command. -go fix -diff example.com/x +! go fix -diff example.com/x # buildtag stdout '-// \+build go1.26' diff --git a/src/cmd/go/testdata/script/fix_vendor.txt b/src/cmd/go/testdata/script/fix_vendor.txt index f03f3269221a98..b1ee1975dbb9b3 100644 --- a/src/cmd/go/testdata/script/fix_vendor.txt +++ b/src/cmd/go/testdata/script/fix_vendor.txt @@ -7,8 +7,8 @@ go mod vendor # Show fixes on two packages, one in the main module # and one in a vendored dependency. -# Only the main one (a) is shown. -go fix -diff example.com/a example.com/b +# Only the main one (a) is shown. Diff => nonzero exit. +! go fix -diff example.com/a example.com/b stdout 'a[/\\]a.go' stdout '\-var _ interface\{\}' stdout '\+var _ any' diff --git a/src/cmd/go/testdata/script/mod_sum_absent.txt b/src/cmd/go/testdata/script/mod_sum_absent.txt new file mode 100644 index 00000000000000..c2dd814542d671 --- /dev/null +++ b/src/cmd/go/testdata/script/mod_sum_absent.txt @@ -0,0 +1,17 @@ +# When the sumdb returns a response which does not +# include a sum for the requested module, +# we should report an error. +# Verifies CVE-2026-42501. +env sumdb=$GOSUMDB +env proxy=$GOPROXY +env GOPROXY GONOPROXY GOSUMDB GONOSUMDB + +# /sumdb-redirect/ causes the sumdb to return /lookup/ responses +# for rsc.io/quote@v1.0.0, not for the requested module. +env GOSUMDB=$sumdb' '$proxy/sumdb-redirect/rsc.io/quote@v1.0.0: + +! go get rsc.io/fortune@v1.0.0 +stderr 'SECURITY ERROR' +! grep rsc.io go.sum +-- go.mod -- +module m diff --git a/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt b/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt new file mode 100644 index 00000000000000..71cd3b392050f8 --- /dev/null +++ b/src/cmd/go/testdata/script/test_cache_coverpkg_bug.txt @@ -0,0 +1,134 @@ +# Test for bug where cached coverage profiles with -coverpkg can contain +# outdated line references when source files are modified. +# This reproduces the issue where coverage data from cache may reference +# lines that no longer exist in the updated source files. + +[short] skip +[GODEBUG:gocacheverify=1] skip + +# We're testing cache behavior, so start with a clean GOCACHE. +env GOCACHE=$WORK/cache + +# Create a project structure with multiple packages +# proj/ +# some_func.go +# some_func_test.go +# sub/ +# sub.go +# sub_test.go +# sum/ +# sum.go + +# Switch to the proj directory +cd proj + +# Run tests with -coverpkg to collect coverage for all packages +go test -coverpkg=proj/... -coverprofile=cover1.out ./... +stdout 'coverage:' + +# Verify the first coverage profile exists and has expected content +exists cover1.out +grep -q 'proj/sub/sub.go:' cover1.out + +# Run again to ensure caching works +go test -coverpkg=proj/... -coverprofile=cover1_cached.out ./... +stdout '\(cached\)' +stdout 'coverage:' + +# Note: Due to the bug, cached coverage profiles may have duplicate entries. +# The duplicate entries are the entries for the previous file structure and the new file structure. + +# Now modify sub.go to change line structure - this will invalidate +# the cache for the sub package but not for the proj package. +cp ../sub_modified.go sub/sub.go + +# After modifying sub.go, we should not have both old and new line references. +go test -coverpkg=proj/... -coverprofile=cover2.out ./... +stdout 'coverage:' + +# With the bug present, we would see duplicate entries for the same lines. +# With the bug fixed, there should be no duplicate or stale entries in the coverage profile. +grep 'proj/sub/sub.go:' cover2.out +# The fix should ensure that only the new line format exists, not the old one +grep 'proj/sub/sub.go:3.24,4.35' cover2.out +# This should fail if the stale coverage line exists (the bug is present) +! grep 'proj/sub/sub.go:3.24,4.22' cover2.out + +-- proj/go.mod -- +module proj + +go 1.21 + +-- proj/some_func.go -- +package proj + +import "proj/sum" + +func SomeFunc(a, b int) int { + if a == 0 && b == 0 { + return 0 + } + return sum.Sum(a, b) +} + +-- proj/some_func_test.go -- +package proj + +import ( + "testing" +) + +func Test_SomeFunc(t *testing.T) { + t.Run("test1", func(t *testing.T) { + result := SomeFunc(1, 1) + if result != 2 { + t.Errorf("Expected 2, got %d", result) + } + }) +} + +-- proj/sub/sub.go -- +package sub + +func Sub(a, b int) int { + if a == 0 && b == 0 { + return 0 + } + return a - b +} + +-- proj/sub/sub_test.go -- +package sub + +import ( + "testing" +) + +func Test_Sub(t *testing.T) { + t.Run("test_sub1", func(t *testing.T) { + result := Sub(1, 1) + if result != 0 { + t.Errorf("Expected 0, got %d", result) + } + }) +} + +-- proj/sum/sum.go -- +package sum + +func Sum(a, b int) int { + if a == 0 { + return b + } + return a + b +} + +-- sub_modified.go -- +package sub + +func Sub(a, b int) int { + if a == 0 && b == 0 || a == -100 { + return 0 + } + return a - b +} diff --git a/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt b/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt new file mode 100644 index 00000000000000..263400a9e675ff --- /dev/null +++ b/src/cmd/go/testdata/script/test_cache_coverpkg_no_profile.txt @@ -0,0 +1,80 @@ +# Test that the test cache is invalidated when -coverpkg dependencies change, +# even when -coverprofile is not specified. This exercises the else-if branch +# in tryCacheWithID that checks covMeta without a cover profile file. + +[short] skip +[GODEBUG:gocacheverify=1] skip + +# Start with a clean GOCACHE. +env GOCACHE=$WORK/cache + +cd proj + +# Run tests with -cover and -coverpkg but without -coverprofile. +go test -cover -coverpkg=proj/... ./... +stdout 'coverage:' + +# Run again — should be served from cache. +go test -cover -coverpkg=proj/... ./... +stdout '\(cached\)' +stdout 'coverage:' + +# Modify a covered package that is not directly under test. +cp ../sub_modified.go sub/sub.go + +# Run again — the cache must be invalidated because a covered package changed. +go test -cover -coverpkg=proj/... ./... +! stdout '\(cached\)' +stdout 'coverage:' + +-- proj/go.mod -- +module proj + +go 1.21 + +-- proj/main.go -- +package proj + +import "proj/sub" + +func Add(a, b int) int { + return sub.Sub(a, -b) +} + +-- proj/main_test.go -- +package proj + +import "testing" + +func TestAdd(t *testing.T) { + if Add(1, 2) != 3 { + t.Fatal("expected 3") + } +} + +-- proj/sub/sub.go -- +package sub + +func Sub(a, b int) int { + return a - b +} + +-- proj/sub/sub_test.go -- +package sub + +import "testing" + +func TestSub(t *testing.T) { + if Sub(3, 1) != 2 { + t.Fatal("expected 2") + } +} + +-- sub_modified.go -- +package sub + +// Added a comment to change the source. + +func Sub(a, b int) int { + return a - b +} diff --git a/src/cmd/go/testdata/script/vet_basic.txt b/src/cmd/go/testdata/script/vet_basic.txt index a0dd3ae2d84d7f..faff586fcb07b2 100644 --- a/src/cmd/go/testdata/script/vet_basic.txt +++ b/src/cmd/go/testdata/script/vet_basic.txt @@ -28,8 +28,8 @@ stdout '"message": "address format .* does not work with IPv6",' stdout '"suggested_fixes":' stdout '"message": "Replace fmt.Sprintf with net.JoinHostPort",' -# vet -fix -diff displays a diff. -go vet -fix -diff example.com/x +# vet -fix -diff displays a diff. Diff => nonzero exit. +! go vet -fix -diff example.com/x stdout '\-var _ = fmt.Sprintf\(s\)' stdout '\+var _ = fmt.Sprintf\("%s", s\)' stdout '\-var _, _ = net.Dial\("tcp", fmt.Sprintf\("%s:%d", s, 80\)\)' @@ -53,8 +53,8 @@ grep 'net.JoinHostPort' x.go ! stderr . cp x.go.bak x.go -# Show diff of fixes from the fix suite. -go fix -diff example.com/x +# Show diff of fixes from the fix suite. Diff => nonzero exit. +! go fix -diff example.com/x ! stdout '\-var _ = fmt.Sprintf\(s\)' stdout '\-var _, _ = net.Dial\("tcp", fmt.Sprintf\("%s:%d", s, 80\)\)' stdout '\+var _, _ = net.Dial\("tcp", net.JoinHostPort\(s, "80"\)\)' diff --git a/src/cmd/link/internal/ld/lib.go b/src/cmd/link/internal/ld/lib.go index bcad5add4abe19..1427f9c7bc62dc 100644 --- a/src/cmd/link/internal/ld/lib.go +++ b/src/cmd/link/internal/ld/lib.go @@ -1700,22 +1700,48 @@ func (ctxt *Link) hostlink() { } if ctxt.Arch.InFamily(sys.ARM64) && buildcfg.GOOS == "linux" { - // On ARM64, the GNU linker will fail with - // -znocopyreloc if it thinks a COPY relocation is - // required. Switch to gold. + // On ARM64, the GNU linker had issues with -znocopyreloc + // and COPY relocations. This was fixed in GNU ld 2.36+. // https://sourceware.org/bugzilla/show_bug.cgi?id=19962 // https://go.dev/issue/22040 - altLinker = "gold" + // And newer gold is deprecated, may lack new features/flags, or even missing - // If gold is not installed, gcc will silently switch - // back to ld.bfd. So we parse the version information - // and provide a useful error if gold is missing. + // If the default linker is GNU ld 2.35 or older, use gold + useGold := false name, args := flagExtld[0], flagExtld[1:] - args = append(args, "-fuse-ld=gold", "-Wl,--version") + args = append(args, "-Wl,--version") cmd := exec.Command(name, args...) if out, err := cmd.CombinedOutput(); err == nil { - if !bytes.Contains(out, []byte("GNU gold")) { - log.Fatalf("ARM64 external linker must be gold (issue #15696, 22040), but is not: %s", out) + // Parse version from output like "GNU ld (GNU Binutils for Distro) 2.36.1" + for line := range strings.Lines(string(out)) { + if !strings.HasPrefix(line, "GNU ld ") { + continue + } + fields := strings.Fields(line[len("GNU ld "):]) + var major, minor int + if ret, err := fmt.Sscanf(fields[len(fields)-1], "%d.%d", &major, &minor); ret == 2 && err == nil { + if major == 2 && minor <= 35 { + useGold = true + } + break + } + } + } + + if useGold { + // Use gold for older linkers + altLinker = "gold" + + // If gold is not installed, gcc will silently switch + // back to ld.bfd. So we parse the version information + // and provide a useful error if gold is missing. + args = flagExtld[1:] + args = append(args, "-fuse-ld=gold", "-Wl,--version") + cmd = exec.Command(name, args...) + if out, err := cmd.CombinedOutput(); err == nil { + if !bytes.Contains(out, []byte("GNU gold")) { + log.Fatalf("ARM64 external linker must be ld>=2.36 or gold (issue #15696, 22040), but is not: %s", out) + } } } } diff --git a/src/cmd/pack/pack.go b/src/cmd/pack/pack.go index 4ac6ce995fe918..e9382990e3a059 100644 --- a/src/cmd/pack/pack.go +++ b/src/cmd/pack/pack.go @@ -139,6 +139,11 @@ func openArchive(name string, mode int, files []string) *Archive { if err != nil { log.Fatal(err) } + for _, f := range a.Entries { + if !filepath.IsLocal(f.Name) || filepath.Base(f.Name) != f.Name { + log.Fatalf("%q: invalid name", f.Name) + } + } return &Archive{ a: a, files: files, diff --git a/src/cmd/pack/pack_test.go b/src/cmd/pack/pack_test.go index 2922ada8e923cb..b46847bc1452d8 100644 --- a/src/cmd/pack/pack_test.go +++ b/src/cmd/pack/pack_test.go @@ -6,6 +6,7 @@ package main import ( "bufio" + "bytes" "cmd/internal/archive" "fmt" "internal/testenv" @@ -382,6 +383,49 @@ func TestRWithNonexistentFile(t *testing.T) { run(packPath(t), "r", "p.a", "p.o") // should succeed } +func TestOutputPathSanitization(t *testing.T) { + dir := t.TempDir() + + // Create pack.a containing a file named "longpathname". + // Note that "go tool pack" requires that all files be at least 8 bytes long. + const validPathName = "longpathname" + if err := os.WriteFile(dir+"/"+validPathName, make([]byte, 8), 0o666); err != nil { + t.Fatal(err) + } + doRun(t, dir, packPath(t), "grc", "pack.a", validPathName) + + // Create evil.a from pack.a, replacing "longpathname" with "out/pathname". + b, err := os.ReadFile(dir + "/pack.a") + if err != nil { + t.Fatal(err) + } + idx := bytes.Index(b, []byte(validPathName)) + if idx < 0 { + t.Fatalf("%v not found in pack.a", validPathName) + } + copy(b[idx:], "out/") + os.WriteFile(dir+"/evil.a", b, 0o666) + + // Extract evil.a. It should fail and not extract a file to /out. + os.Mkdir(dir+"/out", 0o777) + + cmd := testenv.Command(t, packPath(t), "x", "evil.a") + cmd.Dir = dir + _, err = cmd.CombinedOutput() + if err == nil { + t.Errorf("pack x evil.a: unexpected success") + } + + ents, err := os.ReadDir(dir + "/out") + if err != nil { + t.Error(err) + } + for _, e := range ents { + t.Errorf("unexpected file in /out: %q", e.Name()) + } + +} + // doRun runs a program in a directory and returns the output. func doRun(t *testing.T, dir string, args ...string) string { cmd := testenv.Command(t, args[0], args[1:]...) diff --git a/src/crypto/fips140/fips140.go b/src/crypto/fips140/fips140.go index f44f3b399b2f05..d3f63d3bf18fcb 100644 --- a/src/crypto/fips140/fips140.go +++ b/src/crypto/fips140/fips140.go @@ -2,6 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package fips140 provides information about the FIPS 140-3 Go Cryptographic +// Module and FIPS 140-3 mode. +// +// For more details, see the [FIPS 140-3 documentation]. +// +// [FIPS 140-3 documentation]: https://go.dev/doc/security/fips140 package fips140 import ( diff --git a/src/crypto/internal/fips140/drbg/entropy_fips140.go b/src/crypto/internal/fips140/drbg/entropy_fips140.go new file mode 100644 index 00000000000000..ba083e8c332f70 --- /dev/null +++ b/src/crypto/internal/fips140/drbg/entropy_fips140.go @@ -0,0 +1,97 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !wasm + +// This file contains reading from from entropy sources in FIPS-140 +// mode. It uses a scratch buffer in the BSS section (see below), +// which usually doesn't cost much, except on Wasm, due to the way +// the linear memory works. FIPS-140 mode is not supported on Wasm, +// so we just use a build tag to exclude it. (Could also exclude other +// platforms that does not support FIPS-140 mode, but as the BSS +// variable doesn't cost much, don't bother.) + +package drbg + +import ( + entropy "crypto/internal/entropy/v1.0.0" + "crypto/internal/sysrand" + "sync" + "sync/atomic" +) + +// memory is a scratch buffer that is accessed between samples by the entropy +// source to expose it to memory access timings. +// +// We reuse it and share it between Seed calls to avoid the significant (~500µs) +// cost of zeroing a new allocation every time. The entropy source accesses it +// using atomics (and doesn't care about its contents). +// +// It should end up in the .noptrbss section, and become backed by physical pages +// at first use. This ensures that programs that do not use the FIPS 140-3 module +// do not incur any memory use or initialization penalties. +var memory entropy.ScratchBuffer + +func getEntropy() *[SeedSize]byte { + var retries int + seed, err := entropy.Seed(&memory) + for err != nil { + // The CPU jitter-based SP 800-90B entropy source has a non-negligible + // chance of failing the startup health tests. + // + // Each time it does, it enters a permanent failure state, and we + // restart it anew. This is not expected to happen more than a few times + // in a row. + if retries++; retries > 100 { + panic("fips140/drbg: failed to obtain initial entropy") + } + seed, err = entropy.Seed(&memory) + } + return &seed +} + +// getEntropy is very slow (~500µs), so we don't want it on the hot path. +// We keep both a persistent DRBG instance and a pool of additional instances. +// Occasional uses will use drbgInstance, even if the pool was emptied since the +// last use. Frequent concurrent uses will fill the pool and use it. +var drbgInstance atomic.Pointer[Counter] +var drbgPool = sync.Pool{ + New: func() any { + return NewCounter(getEntropy()) + }, +} + +func readFromEntropy(b []byte) { + // At every read, 128 random bits from the operating system are mixed as + // additional input, to make the output as strong as non-FIPS randomness. + // This is not credited as entropy for FIPS purposes, as allowed by Section + // 8.7.2: "Note that a DRBG does not rely on additional input to provide + // entropy, even though entropy could be provided in the additional input". + additionalInput := new([SeedSize]byte) + sysrand.Read(additionalInput[:16]) + + drbg := drbgInstance.Swap(nil) + if drbg == nil { + drbg = drbgPool.Get().(*Counter) + } + defer func() { + if !drbgInstance.CompareAndSwap(nil, drbg) { + drbgPool.Put(drbg) + } + }() + + for len(b) > 0 { + size := min(len(b), maxRequestSize) + if reseedRequired := drbg.Generate(b[:size], additionalInput); reseedRequired { + // See SP 800-90A Rev. 1, Section 9.3.1, Steps 6-8, as explained in + // Section 9.3.2: if Generate reports a reseed is required, the + // additional input is passed to Reseed along with the entropy and + // then nulled before the next Generate call. + drbg.Reseed(getEntropy(), additionalInput) + additionalInput = nil + continue + } + b = b[size:] + } +} diff --git a/src/crypto/internal/fips140/drbg/entropy_wasm.go b/src/crypto/internal/fips140/drbg/entropy_wasm.go new file mode 100644 index 00000000000000..f2e4cc73b16d31 --- /dev/null +++ b/src/crypto/internal/fips140/drbg/entropy_wasm.go @@ -0,0 +1,11 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package drbg + +func readFromEntropy(b []byte) { + panic("FIPS-140 entropy generation is not supported on Wasm") +} diff --git a/src/crypto/internal/fips140/drbg/rand.go b/src/crypto/internal/fips140/drbg/rand.go index 949e74ac60c51b..d9e545f980b2fc 100644 --- a/src/crypto/internal/fips140/drbg/rand.go +++ b/src/crypto/internal/fips140/drbg/rand.go @@ -9,55 +9,11 @@ package drbg import ( - entropy "crypto/internal/entropy/v1.0.0" "crypto/internal/fips140" "crypto/internal/sysrand" "io" - "sync" - "sync/atomic" ) -// memory is a scratch buffer that is accessed between samples by the entropy -// source to expose it to memory access timings. -// -// We reuse it and share it between Seed calls to avoid the significant (~500µs) -// cost of zeroing a new allocation every time. The entropy source accesses it -// using atomics (and doesn't care about its contents). -// -// It should end up in the .noptrbss section, and become backed by physical pages -// at first use. This ensures that programs that do not use the FIPS 140-3 module -// do not incur any memory use or initialization penalties. -var memory entropy.ScratchBuffer - -func getEntropy() *[SeedSize]byte { - var retries int - seed, err := entropy.Seed(&memory) - for err != nil { - // The CPU jitter-based SP 800-90B entropy source has a non-negligible - // chance of failing the startup health tests. - // - // Each time it does, it enters a permanent failure state, and we - // restart it anew. This is not expected to happen more than a few times - // in a row. - if retries++; retries > 100 { - panic("fips140/drbg: failed to obtain initial entropy") - } - seed, err = entropy.Seed(&memory) - } - return &seed -} - -// getEntropy is very slow (~500µs), so we don't want it on the hot path. -// We keep both a persistent DRBG instance and a pool of additional instances. -// Occasional uses will use drbgInstance, even if the pool was emptied since the -// last use. Frequent concurrent uses will fill the pool and use it. -var drbgInstance atomic.Pointer[Counter] -var drbgPool = sync.Pool{ - New: func() any { - return NewCounter(getEntropy()) - }, -} - // Read fills b with cryptographically secure random bytes. In FIPS mode, it // uses an SP 800-90A Rev. 1 Deterministic Random Bit Generator (DRBG). // Otherwise, it uses the operating system's random number generator. @@ -76,37 +32,7 @@ func Read(b []byte) { return } - // At every read, 128 random bits from the operating system are mixed as - // additional input, to make the output as strong as non-FIPS randomness. - // This is not credited as entropy for FIPS purposes, as allowed by Section - // 8.7.2: "Note that a DRBG does not rely on additional input to provide - // entropy, even though entropy could be provided in the additional input". - additionalInput := new([SeedSize]byte) - sysrand.Read(additionalInput[:16]) - - drbg := drbgInstance.Swap(nil) - if drbg == nil { - drbg = drbgPool.Get().(*Counter) - } - defer func() { - if !drbgInstance.CompareAndSwap(nil, drbg) { - drbgPool.Put(drbg) - } - }() - - for len(b) > 0 { - size := min(len(b), maxRequestSize) - if reseedRequired := drbg.Generate(b[:size], additionalInput); reseedRequired { - // See SP 800-90A Rev. 1, Section 9.3.1, Steps 6-8, as explained in - // Section 9.3.2: if Generate reports a reseed is required, the - // additional input is passed to Reseed along with the entropy and - // then nulled before the next Generate call. - drbg.Reseed(getEntropy(), additionalInput) - additionalInput = nil - continue - } - b = b[size:] - } + readFromEntropy(b) } var testingReader io.Reader diff --git a/src/crypto/tls/fips140_test.go b/src/crypto/tls/fips140_test.go index 96273c0fe0ea2f..540d2e6ee7c9c8 100644 --- a/src/crypto/tls/fips140_test.go +++ b/src/crypto/tls/fips140_test.go @@ -7,7 +7,9 @@ package tls import ( "crypto/ecdsa" "crypto/elliptic" + "crypto/fips140" "crypto/internal/boring" + ifips140 "crypto/internal/fips140" "crypto/rand" "crypto/rsa" "crypto/x509" @@ -19,6 +21,7 @@ import ( "math/big" "net" "os" + "regexp" "runtime" "strings" "testing" @@ -54,12 +57,29 @@ func generateKeyShare(group CurveID) keyShare { return shares[0] } +func rerunWithFIPS140Enforced(t *testing.T) { + t.Helper() + if err := ifips140.Supported(); err != nil { + t.Skipf("test requires FIPS 140 mode: %v", err) + } + nameRegex := "^" + regexp.QuoteMeta(t.Name()) + "$" + cmd := testenv.Command(t, testenv.Executable(t), "-test.run="+nameRegex, "-test.v") + cmd.Env = append(cmd.Environ(), "GODEBUG=fips140=only") + out, err := cmd.CombinedOutput() + t.Logf("running with GODEBUG=fips140=only:\n%s", out) + if err != nil { + t.Errorf("fips140=only subprocess failed: %v", err) + } +} + +var testConfigFIPS140 *Config + func TestFIPSServerProtocolVersion(t *testing.T) { test := func(t *testing.T, name string, v uint16, msg string) { t.Run(name, func(t *testing.T) { - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.MinVersion = VersionSSL30 - clientConfig := testConfig.Clone() + clientConfig := testConfigFIPS140.Clone() clientConfig.MinVersion = v clientConfig.MaxVersion = v _, _, err := testHandshake(t, clientConfig, serverConfig) @@ -91,6 +111,10 @@ func TestFIPSServerProtocolVersion(t *testing.T) { test(t, "VersionTLS12", VersionTLS12, "") test(t, "VersionTLS13", VersionTLS13, "") }) + + if !fips140.Enforced() { + rerunWithFIPS140Enforced(t) + } } func isFIPSVersion(v uint16) bool { @@ -178,7 +202,7 @@ func isFIPSSignatureScheme(alg SignatureScheme) bool { } func TestFIPSServerCipherSuites(t *testing.T) { - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.Certificates = make([]Certificate, 1) for _, id := range allCipherSuitesIncludingTLS13() { @@ -219,16 +243,20 @@ func TestFIPSServerCipherSuites(t *testing.T) { }) }) } + + if !fips140.Enforced() { + rerunWithFIPS140Enforced(t) + } } func TestFIPSServerCurves(t *testing.T) { - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.CurvePreferences = nil serverConfig.BuildNameToCertificate() for _, curveid := range defaultCurvePreferences() { t.Run(fmt.Sprintf("curve=%v", curveid), func(t *testing.T) { - clientConfig := testConfig.Clone() + clientConfig := testConfigFIPS140.Clone() clientConfig.CurvePreferences = []CurveID{curveid} runWithFIPSDisabled(t, func(t *testing.T) { @@ -248,6 +276,10 @@ func TestFIPSServerCurves(t *testing.T) { }) }) } + + if !fips140.Enforced() { + rerunWithFIPS140Enforced(t) + } } func fipsHandshake(t *testing.T, clientConfig, serverConfig *Config) (clientErr, serverErr error) { @@ -276,7 +308,7 @@ func TestFIPSServerSignatureAndHash(t *testing.T) { for _, sigHash := range defaultSupportedSignatureAlgorithms() { t.Run(fmt.Sprintf("%v", sigHash), func(t *testing.T) { - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.Certificates = make([]Certificate, 1) testingOnlySupportedSignatureAlgorithms = []SignatureScheme{sigHash} @@ -302,7 +334,7 @@ func TestFIPSServerSignatureAndHash(t *testing.T) { serverConfig.MaxVersion = VersionTLS12 runWithFIPSDisabled(t, func(t *testing.T) { - clientErr, serverErr := fipsHandshake(t, testConfig, serverConfig) + clientErr, serverErr := fipsHandshake(t, testConfigFIPS140, serverConfig) if clientErr != nil { t.Fatalf("expected handshake with %v to succeed; client error: %v; server error: %v", sigHash, clientErr, serverErr) } @@ -310,7 +342,7 @@ func TestFIPSServerSignatureAndHash(t *testing.T) { // With fipstls forced, bad curves should be rejected. runWithFIPSEnabled(t, func(t *testing.T) { - clientErr, _ := fipsHandshake(t, testConfig, serverConfig) + clientErr, _ := fipsHandshake(t, testConfigFIPS140, serverConfig) if isFIPSSignatureScheme(sigHash) { if clientErr != nil { t.Fatalf("expected handshake with %v to succeed; err=%v", sigHash, clientErr) @@ -323,6 +355,10 @@ func TestFIPSServerSignatureAndHash(t *testing.T) { }) }) } + + if !fips140.Enforced() { + rerunWithFIPS140Enforced(t) + } } func TestFIPSClientHello(t *testing.T) { @@ -337,7 +373,7 @@ func testFIPSClientHello(t *testing.T) { defer c.Close() defer s.Close() - clientConfig := testConfig.Clone() + clientConfig := testConfigFIPS140.Clone() // All sorts of traps for the client to avoid. clientConfig.MinVersion = VersionSSL30 clientConfig.MaxVersion = VersionTLS13 @@ -345,7 +381,7 @@ func testFIPSClientHello(t *testing.T) { clientConfig.CurvePreferences = defaultCurvePreferences() go Client(c, clientConfig).Handshake() - srv := Server(s, testConfig) + srv := Server(s, testConfigFIPS140) msg, err := srv.readHandshake(nil) if err != nil { t.Fatal(err) @@ -409,12 +445,12 @@ func TestFIPSCertAlgs(t *testing.T) { // client verifying server cert testServerCert := func(t *testing.T, desc string, pool *x509.CertPool, key any, list [][]byte, ok bool) { - clientConfig := testConfig.Clone() + clientConfig := testConfigFIPS140.Clone() clientConfig.RootCAs = pool clientConfig.InsecureSkipVerify = false clientConfig.ServerName = "example.com" - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.Certificates = []Certificate{{Certificate: list, PrivateKey: key}} serverConfig.BuildNameToCertificate() @@ -437,11 +473,11 @@ func TestFIPSCertAlgs(t *testing.T) { // server verifying client cert testClientCert := func(t *testing.T, desc string, pool *x509.CertPool, key any, list [][]byte, ok bool) { - clientConfig := testConfig.Clone() + clientConfig := testConfigFIPS140.Clone() clientConfig.ServerName = "example.com" clientConfig.Certificates = []Certificate{{Certificate: list, PrivateKey: key}} - serverConfig := testConfig.Clone() + serverConfig := testConfigFIPS140.Clone() serverConfig.ClientCAs = pool serverConfig.ClientAuth = RequireAndVerifyClientCert diff --git a/src/crypto/tls/handshake_test.go b/src/crypto/tls/handshake_test.go index 6e00b4348e6038..7cf0ee2343b132 100644 --- a/src/crypto/tls/handshake_test.go +++ b/src/crypto/tls/handshake_test.go @@ -477,6 +477,15 @@ func runMain(m *testing.M) int { defer f.Close() } + testConfigFIPS140 = testConfig.Clone() + // Only crypto/rand.Reader is allowed in FIPS 140-only mode. + testConfigFIPS140.Rand = nil + // FIPS 140-only mode requires 2048-bit RSA keys. + testConfigFIPS140.Certificates = []Certificate{{ + Certificate: [][]byte{testRSA2048Certificate}, + PrivateKey: testRSA2048PrivateKey, + }} + return m.Run() } diff --git a/src/crypto/tls/key_schedule.go b/src/crypto/tls/key_schedule.go index bfa22449c87178..97f4993a179939 100644 --- a/src/crypto/tls/key_schedule.go +++ b/src/crypto/tls/key_schedule.go @@ -7,6 +7,7 @@ package tls import ( "crypto" "crypto/ecdh" + "crypto/fips140" "crypto/hmac" "crypto/internal/fips140/tls13" "crypto/mlkem" @@ -165,7 +166,14 @@ type hybridKeyExchange struct { } func (ke *hybridKeyExchange) keyShares(rand io.Reader) (*keySharePrivateKeys, []keyShare, error) { - priv, ecdhShares, err := ke.ecdh.keyShares(rand) + var ( + priv *keySharePrivateKeys + ecdhShares []keyShare + err error + ) + fips140.WithoutEnforcement(func() { // Hybrid of ML-KEM, which is Approved. + priv, ecdhShares, err = ke.ecdh.keyShares(rand) + }) if err != nil { return nil, nil, err } @@ -201,7 +209,14 @@ func (ke *hybridKeyExchange) serverSharedSecret(rand io.Reader, clientKeyShare [ ecdhShareData = clientKeyShare[:ke.ecdhElementSize] mlkemShareData = clientKeyShare[ke.ecdhElementSize:] } - ecdhSharedSecret, ks, err := ke.ecdh.serverSharedSecret(rand, ecdhShareData) + var ( + ecdhSharedSecret []byte + ks keyShare + err error + ) + fips140.WithoutEnforcement(func() { // Hybrid of ML-KEM, which is Approved. + ecdhSharedSecret, ks, err = ke.ecdh.serverSharedSecret(rand, ecdhShareData) + }) if err != nil { return nil, keyShare{}, err } @@ -234,7 +249,13 @@ func (ke *hybridKeyExchange) clientSharedSecret(priv *keySharePrivateKeys, serve ecdhShareData = serverKeyShare[:ke.ecdhElementSize] mlkemShareData = serverKeyShare[ke.ecdhElementSize:] } - ecdhSharedSecret, err := ke.ecdh.clientSharedSecret(priv, ecdhShareData) + var ( + ecdhSharedSecret []byte + err error + ) + fips140.WithoutEnforcement(func() { // Hybrid of ML-KEM, which is Approved. + ecdhSharedSecret, err = ke.ecdh.clientSharedSecret(priv, ecdhShareData) + }) if err != nil { return nil, err } diff --git a/src/crypto/tls/tls_test.go b/src/crypto/tls/tls_test.go index 4513008a5f930e..86322390862137 100644 --- a/src/crypto/tls/tls_test.go +++ b/src/crypto/tls/tls_test.go @@ -11,6 +11,7 @@ import ( "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" + "crypto/fips140" "crypto/internal/boring" "crypto/rand" "crypto/tls/internal/fips140tls" @@ -194,6 +195,13 @@ func runWithFIPSEnabled(t *testing.T, testFunc func(t *testing.T)) { } func runWithFIPSDisabled(t *testing.T, testFunc func(t *testing.T)) { + if fips140.Enforced() { + t.Run("no-fips140tls", func(t *testing.T) { + t.Skip("can't run no-fips140tls tests in fips140=only mode") + }) + return + } + originalFIPS := fips140tls.Required() defer func() { if originalFIPS { diff --git a/src/go.mod b/src/go.mod index efc07451b53448..b329ba18def47c 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,7 +4,7 @@ go 1.26 require ( golang.org/x/crypto v0.46.1-0.20251210140736-7dacc380ba00 - golang.org/x/net v0.47.1-0.20251128220604-7c360367ab7e + golang.org/x/net v0.47.1-0.20260417193450-705de46f8788 ) require ( diff --git a/src/go.sum b/src/go.sum index b6b841b44d8e38..37f908522c1926 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,7 +1,7 @@ golang.org/x/crypto v0.46.1-0.20251210140736-7dacc380ba00 h1:JgcPM1rzpSOZS8y69FQvnY0xN0ciHlpQqwTXJcuZIA4= golang.org/x/crypto v0.46.1-0.20251210140736-7dacc380ba00/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= -golang.org/x/net v0.47.1-0.20251128220604-7c360367ab7e h1:PAAT9cIDvIAIRQVz2txQvUFRt3jOlhiO84ihd8XMGlg= -golang.org/x/net v0.47.1-0.20251128220604-7c360367ab7e/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.47.1-0.20260417193450-705de46f8788 h1:fVWwoa/P68Bsajqy2FO4dha7TRBfgf09o932L4USeXI= +golang.org/x/net v0.47.1-0.20260417193450-705de46f8788/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= diff --git a/src/go/types/builtins.go b/src/go/types/builtins.go index 90a3b4a901f8e7..3da5a679e9bf08 100644 --- a/src/go/types/builtins.go +++ b/src/go/types/builtins.go @@ -115,7 +115,7 @@ func (check *Checker) builtin(x *operand, call *ast.CallExpr, id builtinId) (_ b for _, u := range typeset(y.typ) { if s, _ := u.(*Slice); s != nil && Identical(s.elem, universeByte) { // typeset ⊇ {[]byte} - } else if isString(u) { + } else if u != nil && isString(u) { // typeset ⊇ {string} hasString = true } else { @@ -378,7 +378,7 @@ func (check *Checker) builtin(x *operand, call *ast.CallExpr, id builtinId) (_ b for _, u := range typeset(y.typ) { if s, _ := u.(*Slice); s != nil && Identical(s.elem, universeByte) { // typeset ⊇ {[]byte} - } else if isString(u) { + } else if u != nil && isString(u) { // typeset ⊇ {string} } else { special = false diff --git a/src/go/types/issues_test.go b/src/go/types/issues_test.go index 6388dcf6876e75..a6ce660c2bc21f 100644 --- a/src/go/types/issues_test.go +++ b/src/go/types/issues_test.go @@ -641,9 +641,13 @@ func TestIssue55030(t *testing.T) { makeSig := func(typ Type, valid bool) { if !valid { defer func() { - if recover() == nil { + r := recover() + if r == nil { panic("NewSignatureType panic expected") } + if _, ok := r.(string); !ok { + panic("NewSignatureType string panic expected") + } }() } par := NewParam(nopos, nil, "", typ) diff --git a/src/go/types/signature.go b/src/go/types/signature.go index 13f60c07722b25..f89357b7d48149 100644 --- a/src/go/types/signature.go +++ b/src/go/types/signature.go @@ -74,6 +74,9 @@ func NewSignatureType(recv *Var, recvTypeParams, typeParams []*TypeParam, params last := params.At(n - 1).typ var S *Slice for t := range typeset(last) { + if t == nil { + break + } var s *Slice if isString(t) { s = NewSlice(universeByte) diff --git a/src/go/types/under.go b/src/go/types/under.go index 6056b2e4829d3b..1b304237419f23 100644 --- a/src/go/types/under.go +++ b/src/go/types/under.go @@ -29,12 +29,12 @@ func all(t Type, f func(t, u Type) bool) bool { // typeset is an iterator over the (type/underlying type) pairs of the // specific type terms of the type set implied by t. // If t is a type parameter, the implied type set is the type set of t's constraint. -// In that case, if there are no specific terms, typeset calls yield with (nil, nil). +// In this case, if there are no specific terms, the iterator produces (nil, nil). // If t is not a type parameter, the implied type set consists of just t. -// In any case, typeset is guaranteed to call yield at least once. +// In any case, typeset is guaranteed to produce at least one set of results. func typeset(t Type) iter.Seq2[Type, Type] { return func(yield func(t, u Type) bool) { - _ = all(t, yield) + all(t, yield) } } diff --git a/src/html/template/escape_test.go b/src/html/template/escape_test.go index 126dc22f330576..8992b787b2a9c4 100644 --- a/src/html/template/escape_test.go +++ b/src/html/template/escape_test.go @@ -231,6 +231,21 @@ func TestEscape(t *testing.T) { "", ``, }, + { + "scriptTypeSpace", + "", + "", + }, + { + "scriptTypeTab", + "", + "", + }, + { + "scriptTypeEmpty", + "", + "", + }, { "jsObjValueNotOverEscaped", "