diff --git a/api/next/73088.txt b/api/next/73088.txt new file mode 100644 index 0000000000..2d15b83816 --- /dev/null +++ b/api/next/73088.txt @@ -0,0 +1,7 @@ +pkg go/ast, const FilterFuncDuplicates //deprecated #73088 +pkg go/ast, const FilterImportDuplicates //deprecated #73088 +pkg go/ast, const FilterUnassociatedComments //deprecated #73088 +pkg go/ast, func FilterPackage //deprecated #73088 +pkg go/ast, func MergePackageFiles //deprecated #73088 +pkg go/ast, func PackageExports //deprecated #73088 +pkg go/ast, type MergeMode //deprecated #73088 diff --git a/doc/next/6-stdlib/99-minor/go/ast/73088.md b/doc/next/6-stdlib/99-minor/go/ast/73088.md new file mode 100644 index 0000000000..e7035a7047 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/go/ast/73088.md @@ -0,0 +1,4 @@ +The [ast.FilterPackage], [ast.PackageExports], and +[ast.MergePackageFiles] functions, and the [MergeMode] type and its +constants, are all deprecated, as they are for use only with the +long-deprecated [ast.Object] and [ast.Package] machinery. diff --git a/doc/next/9-todo.md b/doc/next/9-todo.md index a6861d176b..fa1c71084f 100644 --- a/doc/next/9-todo.md +++ b/doc/next/9-todo.md @@ -1,6 +1,10 @@ +### TODO + +**Please turn these into proper release notes** + all: implement plugin build mode for riscv64 diff --git a/src/cmd/compile/internal/ir/expr.go b/src/cmd/compile/internal/ir/expr.go index cf56515a2c..8f7df4b458 100644 --- a/src/cmd/compile/internal/ir/expr.go +++ b/src/cmd/compile/internal/ir/expr.go @@ -853,6 +853,10 @@ func IsAddressable(n Node) bool { // // calling StaticValue on the "int(y)" expression returns the outer // "g()" expression. +// +// NOTE: StaticValue can return a result with a different type than +// n's type because it can traverse through OCONVNOP operations. +// TODO: consider reapplying OCONVNOP operations to the result. See https://go.dev/cl/676517. func StaticValue(n Node) Node { for { switch n1 := n.(type) { diff --git a/src/cmd/compile/internal/walk/order.go b/src/cmd/compile/internal/walk/order.go index 77322286c7..8ba8dd96cc 100644 --- a/src/cmd/compile/internal/walk/order.go +++ b/src/cmd/compile/internal/walk/order.go @@ -249,14 +249,14 @@ func (o *orderState) addrTemp(n ir.Node) ir.Node { if (v.Op() == ir.OSTRUCTLIT || v.Op() == ir.OARRAYLIT) && !base.Ctxt.IsFIPS() { if ir.IsZero(v) && 0 < v.Type().Size() && v.Type().Size() <= abi.ZeroValSize { // This zero value can be represented by the read-only zeroVal. - zeroVal := ir.NewLinksymExpr(v.Pos(), ir.Syms.ZeroVal, v.Type()) + zeroVal := ir.NewLinksymExpr(v.Pos(), ir.Syms.ZeroVal, n.Type()) vstat := typecheck.Expr(zeroVal).(*ir.LinksymOffsetExpr) return vstat } if isStaticCompositeLiteral(v) { // v can be directly represented in the read-only data section. lit := v.(*ir.CompLitExpr) - vstat := readonlystaticname(lit.Type()) + vstat := readonlystaticname(n.Type()) fixedlit(inInitFunction, initKindStatic, lit, vstat, nil) // nil init vstat = typecheck.Expr(vstat).(*ir.Name) return vstat diff --git a/src/cmd/go/internal/modget/get.go b/src/cmd/go/internal/modget/get.go index 6867bdaa36..31e9244e2d 100644 --- a/src/cmd/go/internal/modget/get.go +++ b/src/cmd/go/internal/modget/get.go @@ -337,6 +337,7 @@ func runGet(ctx context.Context, cmd *base.Command, args []string) { r.performLocalQueries(ctx) r.performPathQueries(ctx) r.performToolQueries(ctx) + r.performWorkQueries(ctx) for { r.performWildcardQueries(ctx) @@ -513,6 +514,7 @@ type resolver struct { pathQueries []*query // package path literal queries in original order wildcardQueries []*query // path wildcard queries in original order patternAllQueries []*query // queries with the pattern "all" + workQueries []*query // queries with the pattern "work" toolQueries []*query // queries with the pattern "tool" // Indexed "none" queries. These are also included in the slices above; @@ -578,6 +580,8 @@ func newResolver(ctx context.Context, queries []*query) *resolver { for _, q := range queries { if q.pattern == "all" { r.patternAllQueries = append(r.patternAllQueries, q) + } else if q.pattern == "work" { + r.workQueries = append(r.workQueries, q) } else if q.pattern == "tool" { r.toolQueries = append(r.toolQueries, q) } else if q.patternIsLocal { @@ -1070,6 +1074,37 @@ func (r *resolver) performToolQueries(ctx context.Context) { } } +// performWorkQueries populates the candidates for each query whose pattern is "work". +// The candidate module to resolve the work pattern is exactly the single main module. +func (r *resolver) performWorkQueries(ctx context.Context) { + for _, q := range r.workQueries { + q.pathOnce(q.pattern, func() pathSet { + // TODO(matloob): Maybe export MainModules.mustGetSingleMainModule and call that. + // There are a few other places outside the modload package where we expect + // a single main module. + if len(modload.MainModules.Versions()) != 1 { + panic("internal error: number of main modules is not exactly one in resolution phase of go get") + } + mainModule := modload.MainModules.Versions()[0] + + // We know what the result is going to be, assuming the main module is not + // empty, (it's the main module itself) but first check to see that there + // are packages in the main module, so that if there aren't any, we can + // return the expected warning that the pattern matched no packages. + match := modload.MatchInModule(ctx, q.pattern, mainModule, imports.AnyTags()) + if len(match.Errs) > 0 { + return pathSet{err: match.Errs[0]} + } + if len(match.Pkgs) == 0 { + search.WarnUnmatched([]*search.Match{match}) + return pathSet{} // There are no packages in the main module, so the main module isn't needed to resolve them. + } + + return pathSet{pkgMods: []module.Version{mainModule}} + }) + } +} + // performPatternAllQueries populates the candidates for each query whose // pattern is "all". // diff --git a/src/cmd/go/testdata/script/mod_get_nopkgs.txt b/src/cmd/go/testdata/script/mod_get_nopkgs.txt index 14176a7dc8..e2bfdf30a8 100644 --- a/src/cmd/go/testdata/script/mod_get_nopkgs.txt +++ b/src/cmd/go/testdata/script/mod_get_nopkgs.txt @@ -29,6 +29,10 @@ stderr '^go: example\.net/emptysubdir/subdir/\.\.\.: module example\.net/emptysu ! go get builtin/... # in GOROOT/src, but contains no packages stderr '^go: builtin/...: malformed module path "builtin": missing dot in first path element$' +cd ../subdirmod +go get work +stderr -count=1 'matched no packages' + -- go.mod -- module example.net/emptysubdir @@ -38,3 +42,7 @@ go 1.16 package emptysubdir -- subdir/README.txt -- This module intentionally does not contain any p +-- subdirmod/go.mod -- +module example.net/emptysubdir/subdirmod + +go 1.16 diff --git a/src/cmd/go/testdata/script/mod_get_work.txt b/src/cmd/go/testdata/script/mod_get_work.txt new file mode 100644 index 0000000000..39c7ea6beb --- /dev/null +++ b/src/cmd/go/testdata/script/mod_get_work.txt @@ -0,0 +1,46 @@ +# Test go get with the work pattern. + +# go get work gets dependencies to satisfy missing imports in the +# main modules' package graph. Before the 'work' pattern existed, users +# would have to run './...' in the root of the work (main) module. +cp go.mod go.mod.orig +go get work +cmp go.mod go.mod.want + +# 'go get work' and 'go get all' behave very differently. Because +# 'all' evaluates to work packages but also to their dependencies, +# 'go get all' will run the 'get' logic on all the dependency module +# packages, bumping all their modules to the latest versions. +cp go.mod.orig go.mod +go get all +cmp go.mod go.mod.all.want +-- go.mod -- +module example.com/a + +go 1.25 +-- go.mod.want -- +module example.com/a + +go 1.25 + +require rsc.io/quote v1.5.2 + +require ( + golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect + rsc.io/sampler v1.3.0 // indirect +) +-- go.mod.all.want -- +module example.com/a + +go 1.25 + +require rsc.io/quote v1.5.2 + +require ( + golang.org/x/text v0.3.0 // indirect + rsc.io/sampler v1.99.99 // indirect +) +-- a.go -- +package a + +import _ "rsc.io/quote" diff --git a/src/cmd/go/testdata/script/mod_get_work_incomplete.txt b/src/cmd/go/testdata/script/mod_get_workspace_incomplete.txt similarity index 98% rename from src/cmd/go/testdata/script/mod_get_work_incomplete.txt rename to src/cmd/go/testdata/script/mod_get_workspace_incomplete.txt index ada2ae50f1..89340ffb57 100644 --- a/src/cmd/go/testdata/script/mod_get_work_incomplete.txt +++ b/src/cmd/go/testdata/script/mod_get_workspace_incomplete.txt @@ -20,6 +20,13 @@ go get ./... cmp go.mod go.mod.want cmp go.sum go.sum.want +# Test go get with an incomplete module using a "work" query. +cp go.mod.orig go.mod +rm go.sum +go get work +cmp go.mod go.mod.want +cmp go.sum go.sum.want + # Test go get with an incomplete module using a path query that can be resolved. cp go.mod.orig go.mod rm go.sum diff --git a/src/crypto/tls/auth.go b/src/crypto/tls/auth.go index 2d0596689f..f5de7b3069 100644 --- a/src/crypto/tls/auth.go +++ b/src/crypto/tls/auth.go @@ -149,20 +149,18 @@ func legacyTypeAndHashFromPublicKey(pub crypto.PublicKey) (sigType uint8, hash c var rsaSignatureSchemes = []struct { scheme SignatureScheme minModulusBytes int - maxVersion uint16 }{ // RSA-PSS is used with PSSSaltLengthEqualsHash, and requires // emLen >= hLen + sLen + 2 - {PSSWithSHA256, crypto.SHA256.Size()*2 + 2, VersionTLS13}, - {PSSWithSHA384, crypto.SHA384.Size()*2 + 2, VersionTLS13}, - {PSSWithSHA512, crypto.SHA512.Size()*2 + 2, VersionTLS13}, + {PSSWithSHA256, crypto.SHA256.Size()*2 + 2}, + {PSSWithSHA384, crypto.SHA384.Size()*2 + 2}, + {PSSWithSHA512, crypto.SHA512.Size()*2 + 2}, // PKCS #1 v1.5 uses prefixes from hashPrefixes in crypto/rsa, and requires // emLen >= len(prefix) + hLen + 11 - // TLS 1.3 dropped support for PKCS #1 v1.5 in favor of RSA-PSS. - {PKCS1WithSHA256, 19 + crypto.SHA256.Size() + 11, VersionTLS12}, - {PKCS1WithSHA384, 19 + crypto.SHA384.Size() + 11, VersionTLS12}, - {PKCS1WithSHA512, 19 + crypto.SHA512.Size() + 11, VersionTLS12}, - {PKCS1WithSHA1, 15 + crypto.SHA1.Size() + 11, VersionTLS12}, + {PKCS1WithSHA256, 19 + crypto.SHA256.Size() + 11}, + {PKCS1WithSHA384, 19 + crypto.SHA384.Size() + 11}, + {PKCS1WithSHA512, 19 + crypto.SHA512.Size() + 11}, + {PKCS1WithSHA1, 15 + crypto.SHA1.Size() + 11}, } // signatureSchemesForCertificate returns the list of supported SignatureSchemes @@ -202,7 +200,7 @@ func signatureSchemesForCertificate(version uint16, cert *Certificate) []Signatu size := pub.Size() sigAlgs = make([]SignatureScheme, 0, len(rsaSignatureSchemes)) for _, candidate := range rsaSignatureSchemes { - if size >= candidate.minModulusBytes && version <= candidate.maxVersion { + if size >= candidate.minModulusBytes { sigAlgs = append(sigAlgs, candidate.scheme) } } @@ -219,10 +217,9 @@ func signatureSchemesForCertificate(version uint16, cert *Certificate) []Signatu } // Filter out any unsupported signature algorithms, for example due to - // FIPS 140-3 policy, tlssha1=0, or any downstream changes to defaults.go. - supportedAlgs := supportedSignatureAlgorithms(version) + // FIPS 140-3 policy, tlssha1=0, or protocol version. sigAlgs = slices.DeleteFunc(sigAlgs, func(sigAlg SignatureScheme) bool { - return !isSupportedSignatureAlgorithm(sigAlg, supportedAlgs) + return isDisabledSignatureAlgorithm(version, sigAlg, false) }) return sigAlgs diff --git a/src/crypto/tls/bogo_config.json b/src/crypto/tls/bogo_config.json index 1bc647ce60..9e3990ecb5 100644 --- a/src/crypto/tls/bogo_config.json +++ b/src/crypto/tls/bogo_config.json @@ -20,8 +20,6 @@ "TLS-ECH-Client-Reject-NoChannelID-TLS13": "We don't support sending channel ID", "TLS-ECH-Client-Reject-NoChannelID-TLS12": "We don't support sending channel ID", - "ServerAuth-SHA1-Fallback*": "We don't ever support SHA-1 in TLS 1.2, so we fail if there are no signature_algorithms", - "TLS-ECH-Client-GREASE-IgnoreHRRExtension": "We don't support ECH GREASE because we don't fallback to plaintext", "TLS-ECH-Client-NoSupportedConfigs-GREASE": "We don't support ECH GREASE because we don't fallback to plaintext", "TLS-ECH-Client-GREASEExtensions": "We don't support ECH GREASE because we don't fallback to plaintext", @@ -40,7 +38,19 @@ "PostQuantumNotEnabledByDefaultInClients": "We do enable it by default!", "*-Kyber-TLS13": "We don't support Kyber, only ML-KEM (BoGo bug ignoring AllCurves?)", - "*-SignDefault-*": "TODO, partially it encodes BoringSSL defaults, partially we might be missing some implicit behavior of a missing flag", + "*-RSA_PKCS1_SHA256_LEGACY-TLS13": "We don't support the legacy PKCS#1 v1.5 codepoint for TLS 1.3", + "*-Verify-RSA_PKCS1_SHA256_LEGACY-TLS12": "Likewise, we don't know how to handle it in TLS 1.2, so we send the wrong alert", + "*-VerifyDefault-*": "Our signature algorithms are not configurable, so there is no difference between default and supported", + "Ed25519DefaultDisable-*": "We support Ed25519 by default", + "NoCommonSignatureAlgorithms-TLS12-Fallback": "We don't support the legacy RSA exchange (without tlsrsakex=1)", + + "*_SHA1-TLS12": "We don't support SHA-1 in TLS 1.2 (without tlssha1=1)", + "Agree-Digest-SHA1": "We don't support SHA-1 in TLS 1.2 (without tlssha1=1)", + "ServerAuth-SHA1-Fallback*": "We don't support SHA-1 in TLS 1.2 (without tlssha1=1), so we fail if there are no signature_algorithms", + + "Agree-Digest-SHA256": "We select signature algorithms in peer preference order. We should consider changing this.", + "ECDSACurveMismatch-Verify-TLS13": "We don't enforce the curve when verifying. This is a bug. We need to fix this.", + "*-Verify-ECDSA_P224_SHA256-TLS13": "Side effect of the bug above. BoGo sends a P-256 sigAlg with a P-224 key, and we allow it.", "V2ClientHello-*": "We don't support SSLv2", "SendV2ClientHello*": "We don't support SSLv2", @@ -62,8 +72,10 @@ "CurveID-Resume*": "unexposed curveID is not stored in the ticket yet", "BadRSAClientKeyExchange-4": "crypto/tls doesn't check the version number in the premaster secret - see processClientKeyExchange comment", "BadRSAClientKeyExchange-5": "crypto/tls doesn't check the version number in the premaster secret - see processClientKeyExchange comment", - "CheckLeafCurve": "TODO: first pass, this should be fixed", "SupportTicketsWithSessionID": "We don't support session ID resumption", + "ResumeTLS12SessionID-TLS13": "We don't support session ID resumption", + + "CheckLeafCurve": "TODO: first pass, this should be fixed", "KeyUpdate-RequestACK": "TODO: first pass, this should be fixed", "SupportedVersionSelection-TLS12": "TODO: first pass, this should be fixed", "UnsolicitedServerNameAck-TLS-TLS1": "TODO: first pass, this should be fixed", @@ -88,19 +100,6 @@ "Resume-Server-OmitPSKsOnSecondClientHello": "TODO: first pass, this should be fixed", "Renegotiate-Server-Forbidden": "TODO: first pass, this should be fixed", "Renegotiate-Client-Forbidden-1": "TODO: first pass, this should be fixed", - "Client-Sign-RSA_PKCS1_SHA1-TLS13": "TODO: first pass, this should be fixed", - "Client-Sign-RSA_PKCS1_SHA256-TLS13": "TODO: first pass, this should be fixed", - "Client-Sign-RSA_PKCS1_SHA384-TLS13": "TODO: first pass, this should be fixed", - "Client-Sign-RSA_PKCS1_SHA512-TLS13": "TODO: first pass, this should be fixed", - "Client-Sign-ECDSA_SHA1-TLS13": "TODO: first pass, this should be fixed", - "Client-Sign-ECDSA_P224_SHA256-TLS13": "TODO: first pass, this should be fixed", - "ClientAuth-NoFallback-TLS13": "TODO: first pass, this should be fixed", - "ClientAuth-NoFallback-ECDSA": "TODO: first pass, this should be fixed", - "ClientAuth-NoFallback-RSA": "TODO: first pass, this should be fixed", - "ECDSACurveMismatch-Verify-TLS13": "TODO: first pass, this should be fixed", - "Ed25519DefaultDisable-NoAdvertise": "TODO: first pass, this should be fixed", - "Ed25519DefaultDisable-NoAccept": "TODO: first pass, this should be fixed", - "NoCommonSignatureAlgorithms-TLS12-Fallback": "TODO: first pass, this should be fixed", "UnknownExtension-Client": "TODO: first pass, this should be fixed", "UnknownUnencryptedExtension-Client-TLS13": "TODO: first pass, this should be fixed", "UnofferedExtension-Client-TLS13": "TODO: first pass, this should be fixed", @@ -153,7 +152,6 @@ "TrailingMessageData-TLS13-ClientCertificate-TLS": "TODO: first pass, this should be fixed", "TrailingMessageData-TLS13-ClientCertificateVerify-TLS": "TODO: first pass, this should be fixed", "TrailingMessageData-TLS13-ServerCertificate-TLS": "TODO: first pass, this should be fixed", - "ResumeTLS12SessionID-TLS13": "We don't support session ID resumption", "SkipEarlyData-TLS13": "TODO: first pass, this should be fixed", "DuplicateKeyShares-TLS13": "TODO: first pass, this should be fixed", "Server-TooLongSessionID-TLS13": "TODO: first pass, this should be fixed", diff --git a/src/crypto/tls/bogo_shim_test.go b/src/crypto/tls/bogo_shim_test.go index fff276979e..2e88d539c4 100644 --- a/src/crypto/tls/bogo_shim_test.go +++ b/src/crypto/tls/bogo_shim_test.go @@ -35,8 +35,10 @@ var ( isHandshakerSupported = flag.Bool("is-handshaker-supported", false, "") - keyfile = flag.String("key-file", "", "") - certfile = flag.String("cert-file", "", "") + keyfile = flag.String("key-file", "", "") + certfile = flag.String("cert-file", "", "") + ocspResponse = flagBase64("ocsp-response", "") + signingPrefs = flagIntSlice("signing-prefs", "") trustCert = flag.String("trust-cert", "", "") @@ -55,13 +57,17 @@ var ( resumeCount = flag.Int("resume-count", 0, "") - curves = flagStringSlice("curves", "") + curves = flagIntSlice("curves", "") expectedCurve = flag.String("expect-curve-id", "", "") + verifyPrefs = flagIntSlice("verify-prefs", "") + expectedSigAlg = flag.String("expect-peer-signature-algorithm", "", "") + expectedPeerSigAlg = flagIntSlice("expect-peer-verify-pref", "") + shimID = flag.Uint64("shim-id", 0, "") _ = flag.Bool("ipv6", false, "") - echConfigListB64 = flag.String("ech-config-list", "", "") + echConfigList = flagBase64("ech-config-list", "") expectECHAccepted = flag.Bool("expect-ech-accept", false, "") expectHRR = flag.Bool("expect-hrr", false, "") expectNoHRR = flag.Bool("expect-no-hrr", false, "") @@ -71,7 +77,7 @@ var ( _ = flag.Bool("expect-no-ech-name-override", false, "") _ = flag.String("expect-ech-name-override", "", "") _ = flag.Bool("reverify-on-resume", false, "") - onResumeECHConfigListB64 = flag.String("on-resume-ech-config-list", "", "") + onResumeECHConfigList = flagBase64("on-resume-ech-config-list", "") _ = flag.Bool("on-resume-expect-reject-early-data", false, "") onResumeExpectECHAccepted = flag.Bool("on-resume-expect-ech-accept", false, "") _ = flag.Bool("on-resume-expect-no-ech-name-override", false, "") @@ -105,7 +111,7 @@ var ( type stringSlice []string func flagStringSlice(name, usage string) *stringSlice { - f := &stringSlice{} + f := new(stringSlice) flag.Var(f, name, usage) return f } @@ -119,12 +125,59 @@ func (saf *stringSlice) Set(s string) error { return nil } +type intSlice []int64 + +func flagIntSlice(name, usage string) *intSlice { + f := new(intSlice) + flag.Var(f, name, usage) + return f +} + +func (sf *intSlice) String() string { + return strings.Join(strings.Split(fmt.Sprint(*sf), " "), ",") +} + +func (sf *intSlice) Set(s string) error { + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + *sf = append(*sf, i) + return nil +} + +type base64Flag []byte + +func flagBase64(name, usage string) *base64Flag { + f := new(base64Flag) + flag.Var(f, name, usage) + return f +} + +func (f *base64Flag) String() string { + return base64.StdEncoding.EncodeToString(*f) +} + +func (f *base64Flag) Set(s string) error { + if *f != nil { + return fmt.Errorf("multiple base64 values not supported") + } + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + *f = b + return nil +} + func bogoShim() { if *isHandshakerSupported { fmt.Println("No") return } + fmt.Printf("BoGo shim flags: %q", os.Args[1:]) + // Test with both the default and insecure cipher suites. var ciphersuites []uint16 for _, s := range append(CipherSuites(), InsecureCipherSuites()...) { @@ -218,7 +271,39 @@ func bogoShim() { if err != nil { log.Fatalf("load key-file err: %s", err) } - cfg.Certificates = []Certificate{pair} + for _, id := range *signingPrefs { + pair.SupportedSignatureAlgorithms = append(pair.SupportedSignatureAlgorithms, SignatureScheme(id)) + } + pair.OCSPStaple = *ocspResponse + // Use Get[Client]Certificate to force the use of the certificate, which + // more closely matches the BoGo expectations (e.g. handshake failure if + // no client certificates are compatible). + cfg.GetCertificate = func(chi *ClientHelloInfo) (*Certificate, error) { + if *expectedPeerSigAlg != nil { + if len(chi.SignatureSchemes) != len(*expectedPeerSigAlg) { + return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg) + } + for i := range *expectedPeerSigAlg { + if chi.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) { + return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg) + } + } + } + return &pair, nil + } + cfg.GetClientCertificate = func(cri *CertificateRequestInfo) (*Certificate, error) { + if *expectedPeerSigAlg != nil { + if len(cri.SignatureSchemes) != len(*expectedPeerSigAlg) { + return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg) + } + for i := range *expectedPeerSigAlg { + if cri.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) { + return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg) + } + } + } + return &pair, nil + } } if *trustCert != "" { pool := x509.NewCertPool() @@ -242,26 +327,24 @@ func bogoShim() { cfg.ClientAuth = VerifyClientCertIfGiven } - if *echConfigListB64 != "" { - echConfigList, err := base64.StdEncoding.DecodeString(*echConfigListB64) - if err != nil { - log.Fatalf("parse ech-config-list err: %s", err) - } - cfg.EncryptedClientHelloConfigList = echConfigList + if *echConfigList != nil { + cfg.EncryptedClientHelloConfigList = *echConfigList cfg.MinVersion = VersionTLS13 } - if len(*curves) != 0 { - for _, curveStr := range *curves { - id, err := strconv.Atoi(curveStr) - if err != nil { - log.Fatalf("failed to parse curve id %q: %s", curveStr, err) - } + if *curves != nil { + for _, id := range *curves { cfg.CurvePreferences = append(cfg.CurvePreferences, CurveID(id)) } } - if len(*echServerConfig) != 0 { + if *verifyPrefs != nil { + for _, id := range *verifyPrefs { + testingOnlySupportedSignatureAlgorithms = append(testingOnlySupportedSignatureAlgorithms, SignatureScheme(id)) + } + } + + if *echServerConfig != nil { if len(*echServerConfig) != len(*echServerKey) || len(*echServerConfig) != len(*echServerRetryConfig) { log.Fatal("-ech-server-config, -ech-server-key, and -ech-is-retry-config mismatch") } @@ -285,12 +368,8 @@ func bogoShim() { } for i := 0; i < *resumeCount+1; i++ { - if i > 0 && (*onResumeECHConfigListB64 != "") { - echConfigList, err := base64.StdEncoding.DecodeString(*onResumeECHConfigListB64) - if err != nil { - log.Fatalf("parse ech-config-list err: %s", err) - } - cfg.EncryptedClientHelloConfigList = echConfigList + if i > 0 && *onResumeECHConfigList != nil { + cfg.EncryptedClientHelloConfigList = *onResumeECHConfigList } conn, err := net.Dial("tcp", net.JoinHostPort("localhost", *port)) @@ -343,7 +422,7 @@ func bogoShim() { if err != io.EOF { retryErr, ok := err.(*ECHRejectionError) if !ok { - log.Fatalf("unexpected error type returned: %v", err) + log.Fatal(err) } if *expectNoECHRetryConfigs && len(retryErr.RetryConfigList) > 0 { log.Fatalf("expected no ECH retry configs, got some") @@ -408,10 +487,21 @@ func bogoShim() { if err != nil { log.Fatalf("failed to parse -expect-curve-id: %s", err) } - if tlsConn.curveID != CurveID(expectedCurveID) { + if cs.CurveID != CurveID(expectedCurveID) { log.Fatalf("unexpected curve id: want %d, got %d", expectedCurveID, tlsConn.curveID) } } + + // TODO: implement testingOnlyPeerSignatureAlgorithm on resumption. + if *expectedSigAlg != "" && !cs.DidResume { + expectedSigAlgID, err := strconv.Atoi(*expectedSigAlg) + if err != nil { + log.Fatalf("failed to parse -expect-peer-signature-algorithm: %s", err) + } + if cs.testingOnlyPeerSignatureAlgorithm != SignatureScheme(expectedSigAlgID) { + log.Fatalf("unexpected peer signature algorithm: want %s, got %s", SignatureScheme(expectedSigAlgID), cs.testingOnlyPeerSignatureAlgorithm) + } + } } } @@ -491,20 +581,36 @@ func TestBogoSuite(t *testing.T) { assertResults := map[string]string{ "CurveTest-Client-MLKEM-TLS13": "PASS", "CurveTest-Server-MLKEM-TLS13": "PASS", + + // Various signature algorithm tests checking that we enforce our + // preferences on the peer. + "ClientAuth-Enforced": "PASS", + "ServerAuth-Enforced": "PASS", + "ClientAuth-Enforced-TLS13": "PASS", + "ServerAuth-Enforced-TLS13": "PASS", + "VerifyPreferences-Advertised": "PASS", + "VerifyPreferences-Enforced": "PASS", + "Client-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS", + "Server-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS", + "Client-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS", + "Server-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS", } for name, result := range results.Tests { // This is not really the intended way to do this... but... it works? t.Run(name, func(t *testing.T) { if result.Actual == "FAIL" && result.IsUnexpected { - t.Fatal(result.Error) + t.Fail() } - if expectedResult, ok := assertResults[name]; ok && expectedResult != result.Actual { - t.Fatalf("unexpected result: got %s, want %s", result.Actual, assertResults[name]) + if result.Error != "" { + t.Log(result.Error) + } + if exp, ok := assertResults[name]; ok && exp != result.Actual { + t.Errorf("unexpected result: got %s, want %s", result.Actual, exp) } delete(assertResults, name) if result.Actual == "SKIP" { - t.Skip() + t.SkipNow() } }) } diff --git a/src/crypto/tls/common.go b/src/crypto/tls/common.go index 6a1c53fe9c..6fe6f34cd2 100644 --- a/src/crypto/tls/common.go +++ b/src/crypto/tls/common.go @@ -309,6 +309,10 @@ type ConnectionState struct { // testingOnlyDidHRR is true if a HelloRetryRequest was sent/received. testingOnlyDidHRR bool + + // testingOnlyPeerSignatureAlgorithm is the signature algorithm used by the + // peer to sign the handshake. It is not set for resumed connections. + testingOnlyPeerSignatureAlgorithm SignatureScheme } // ExportKeyingMaterial returns length bytes of exported key material in a new @@ -1684,35 +1688,62 @@ func unexpectedMessageError(wanted, got any) error { return fmt.Errorf("tls: received unexpected handshake message of type %T when waiting for %T", got, wanted) } +var testingOnlySupportedSignatureAlgorithms []SignatureScheme + // supportedSignatureAlgorithms returns the supported signature algorithms for // the given minimum TLS version, to advertise in ClientHello and // CertificateRequest messages. func supportedSignatureAlgorithms(minVers uint16) []SignatureScheme { sigAlgs := defaultSupportedSignatureAlgorithms() - if fips140tls.Required() { - sigAlgs = slices.DeleteFunc(sigAlgs, func(s SignatureScheme) bool { - return !slices.Contains(allowedSignatureAlgorithmsFIPS, s) - }) + if testingOnlySupportedSignatureAlgorithms != nil { + sigAlgs = slices.Clone(testingOnlySupportedSignatureAlgorithms) } - if minVers > VersionTLS12 { - sigAlgs = slices.DeleteFunc(sigAlgs, func(s SignatureScheme) bool { - sigType, sigHash, _ := typeAndHashFromSignatureScheme(s) - return sigType == signaturePKCS1v15 || sigHash == crypto.SHA1 - }) + return slices.DeleteFunc(sigAlgs, func(s SignatureScheme) bool { + return isDisabledSignatureAlgorithm(minVers, s, false) + }) +} + +var tlssha1 = godebug.New("tlssha1") + +func isDisabledSignatureAlgorithm(version uint16, s SignatureScheme, isCert bool) bool { + if fips140tls.Required() && !slices.Contains(allowedSignatureAlgorithmsFIPS, s) { + return true } - return sigAlgs + + // For the _cert extension we include all algorithms, including SHA-1 and + // PKCS#1 v1.5, because it's more likely that something on our side will be + // willing to accept a *-with-SHA1 certificate (e.g. with a custom + // VerifyConnection or by a direct match with the CertPool), than that the + // peer would have a better certificate but is just choosing not to send it. + // crypto/x509 will refuse to verify important SHA-1 signatures anyway. + if isCert { + return false + } + + // TLS 1.3 removed support for PKCS#1 v1.5 and SHA-1 signatures, + // and Go 1.25 removed support for SHA-1 signatures in TLS 1.2. + if version > VersionTLS12 { + sigType, sigHash, _ := typeAndHashFromSignatureScheme(s) + if sigType == signaturePKCS1v15 || sigHash == crypto.SHA1 { + return true + } + } else if tlssha1.Value() != "1" { + _, sigHash, _ := typeAndHashFromSignatureScheme(s) + if sigHash == crypto.SHA1 { + return true + } + } + + return false } // supportedSignatureAlgorithmsCert returns the supported algorithms for // signatures in certificates. func supportedSignatureAlgorithmsCert() []SignatureScheme { - sigAlgs := defaultSupportedSignatureAlgorithmsCert() - if fips140tls.Required() { - sigAlgs = slices.DeleteFunc(sigAlgs, func(s SignatureScheme) bool { - return !slices.Contains(allowedSignatureAlgorithmsFIPS, s) - }) - } - return sigAlgs + sigAlgs := defaultSupportedSignatureAlgorithms() + return slices.DeleteFunc(sigAlgs, func(s SignatureScheme) bool { + return isDisabledSignatureAlgorithm(0, s, true) + }) } func isSupportedSignatureAlgorithm(sigAlg SignatureScheme, supportedSignatureAlgorithms []SignatureScheme) bool { diff --git a/src/crypto/tls/conn.go b/src/crypto/tls/conn.go index cd9b9778fd..b36fcaa648 100644 --- a/src/crypto/tls/conn.go +++ b/src/crypto/tls/conn.go @@ -51,6 +51,7 @@ type Conn struct { didHRR bool // whether a HelloRetryRequest was sent/received cipherSuite uint16 curveID CurveID + peerSigAlg SignatureScheme ocspResponse []byte // stapled OCSP response scts [][]byte // signed certificate timestamps from server peerCertificates []*x509.Certificate @@ -1630,6 +1631,7 @@ func (c *Conn) connectionStateLocked() ConnectionState { state.NegotiatedProtocol = c.clientProtocol state.DidResume = c.didResume state.testingOnlyDidHRR = c.didHRR + state.testingOnlyPeerSignatureAlgorithm = c.peerSigAlg state.CurveID = c.curveID state.NegotiatedProtocolIsMutual = true state.ServerName = c.serverName diff --git a/src/crypto/tls/defaults.go b/src/crypto/tls/defaults.go index 3aa1bc2e4c..489a2750df 100644 --- a/src/crypto/tls/defaults.go +++ b/src/crypto/tls/defaults.go @@ -24,53 +24,11 @@ func defaultCurvePreferences() []CurveID { return []CurveID{X25519MLKEM768, X25519, CurveP256, CurveP384, CurveP521} } -var tlssha1 = godebug.New("tlssha1") - // defaultSupportedSignatureAlgorithms returns the signature and hash algorithms that // the code advertises and supports in a TLS 1.2+ ClientHello and in a TLS 1.2+ // CertificateRequest. The two fields are merged to match with TLS 1.3. // Note that in TLS 1.2, the ECDSA algorithms are not constrained to P-256, etc. func defaultSupportedSignatureAlgorithms() []SignatureScheme { - if tlssha1.Value() == "1" { - return []SignatureScheme{ - PSSWithSHA256, - ECDSAWithP256AndSHA256, - Ed25519, - PSSWithSHA384, - PSSWithSHA512, - PKCS1WithSHA256, - PKCS1WithSHA384, - PKCS1WithSHA512, - ECDSAWithP384AndSHA384, - ECDSAWithP521AndSHA512, - PKCS1WithSHA1, - ECDSAWithSHA1, - } - } - return []SignatureScheme{ - PSSWithSHA256, - ECDSAWithP256AndSHA256, - Ed25519, - PSSWithSHA384, - PSSWithSHA512, - PKCS1WithSHA256, - PKCS1WithSHA384, - PKCS1WithSHA512, - ECDSAWithP384AndSHA384, - ECDSAWithP521AndSHA512, - } -} - -// defaultSupportedSignatureAlgorithmsCert returns the signature algorithms that -// the code advertises as supported for signatures in certificates. -// -// We include all algorithms, including SHA-1 and PKCS#1 v1.5, because it's more -// likely that something on our side will be willing to accept a *-with-SHA1 -// certificate (e.g. with a custom VerifyConnection or by a direct match with -// the CertPool), than that the peer would have a better certificate but is just -// choosing not to send it. crypto/x509 will refuse to verify important SHA-1 -// signatures anyway. -func defaultSupportedSignatureAlgorithmsCert() []SignatureScheme { return []SignatureScheme{ PSSWithSHA256, ECDSAWithP256AndSHA256, diff --git a/src/crypto/tls/fips140_test.go b/src/crypto/tls/fips140_test.go index 46d3076864..d3fa61dc97 100644 --- a/src/crypto/tls/fips140_test.go +++ b/src/crypto/tls/fips140_test.go @@ -18,6 +18,7 @@ import ( "internal/testenv" "math/big" "net" + "os" "runtime" "strings" "testing" @@ -262,15 +263,19 @@ func fipsHandshake(t *testing.T, clientConfig, serverConfig *Config) (clientErr, func TestFIPSServerSignatureAndHash(t *testing.T) { defer func() { - testingOnlyForceClientHelloSignatureAlgorithms = nil + testingOnlySupportedSignatureAlgorithms = nil }() + defer func(godebug string) { + os.Setenv("GODEBUG", godebug) + }(os.Getenv("GODEBUG")) + os.Setenv("GODEBUG", "tlssha1=1") for _, sigHash := range defaultSupportedSignatureAlgorithms() { t.Run(fmt.Sprintf("%v", sigHash), func(t *testing.T) { serverConfig := testConfig.Clone() serverConfig.Certificates = make([]Certificate, 1) - testingOnlyForceClientHelloSignatureAlgorithms = []SignatureScheme{sigHash} + testingOnlySupportedSignatureAlgorithms = []SignatureScheme{sigHash} sigType, _, _ := typeAndHashFromSignatureScheme(sigHash) switch sigType { diff --git a/src/crypto/tls/handshake_client.go b/src/crypto/tls/handshake_client.go index 2d3a2ef25b..90c5bdacd8 100644 --- a/src/crypto/tls/handshake_client.go +++ b/src/crypto/tls/handshake_client.go @@ -20,7 +20,6 @@ import ( "errors" "fmt" "hash" - "internal/byteorder" "internal/godebug" "io" "net" @@ -42,8 +41,6 @@ type clientHandshakeState struct { ticket []byte // a fresh ticket received during this handshake } -var testingOnlyForceClientHelloSignatureAlgorithms []SignatureScheme - func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echClientContext, error) { config := c.config if len(config.ServerName) == 0 && !config.InsecureSkipVerify { @@ -126,9 +123,6 @@ func (c *Conn) makeClientHello() (*clientHelloMsg, *keySharePrivateKeys, *echCli hello.supportedSignatureAlgorithms = supportedSignatureAlgorithms(minVersion) hello.supportedSignatureAlgorithmsCert = supportedSignatureAlgorithmsCert() } - if testingOnlyForceClientHelloSignatureAlgorithms != nil { - hello.supportedSignatureAlgorithms = testingOnlyForceClientHelloSignatureAlgorithms - } var keyShareKeys *keySharePrivateKeys if maxVersion >= VersionTLS13 { @@ -732,8 +726,9 @@ func (hs *clientHandshakeState) doFullHandshake() error { c.sendAlert(alertIllegalParameter) return err } - if len(skx.key) >= 3 && skx.key[0] == 3 /* named curve */ { - c.curveID = CurveID(byteorder.BEUint16(skx.key[1:])) + if keyAgreement, ok := keyAgreement.(*ecdheKeyAgreement); ok { + c.curveID = keyAgreement.curveID + c.peerSigAlg = keyAgreement.signatureAlgorithm } msg, err = c.readHandshake(&hs.finishedHash) @@ -819,7 +814,7 @@ func (hs *clientHandshakeState) doFullHandshake() error { if c.vers >= VersionTLS12 { signatureAlgorithm, err := selectSignatureScheme(c.vers, chainToSend, certReq.supportedSignatureAlgorithms) if err != nil { - c.sendAlert(alertIllegalParameter) + c.sendAlert(alertHandshakeFailure) return err } sigType, sigHash, err = typeAndHashFromSignatureScheme(signatureAlgorithm) diff --git a/src/crypto/tls/handshake_client_tls13.go b/src/crypto/tls/handshake_client_tls13.go index bf3cab97b8..4f4966904f 100644 --- a/src/crypto/tls/handshake_client_tls13.go +++ b/src/crypto/tls/handshake_client_tls13.go @@ -694,6 +694,7 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error { c.sendAlert(alertDecryptError) return errors.New("tls: invalid signature by the server certificate: " + err.Error()) } + c.peerSigAlg = certVerify.signatureAlgorithm if err := transcriptMsg(certVerify, hs.transcript); err != nil { return err diff --git a/src/crypto/tls/handshake_server.go b/src/crypto/tls/handshake_server.go index 6848407e74..8240e6afae 100644 --- a/src/crypto/tls/handshake_server.go +++ b/src/crypto/tls/handshake_server.go @@ -16,7 +16,6 @@ import ( "errors" "fmt" "hash" - "internal/byteorder" "io" "time" ) @@ -632,8 +631,9 @@ func (hs *serverHandshakeState) doFullHandshake() error { return err } if skx != nil { - if len(skx.key) >= 3 && skx.key[0] == 3 /* named curve */ { - c.curveID = CurveID(byteorder.BEUint16(skx.key[1:])) + if keyAgreement, ok := keyAgreement.(*ecdheKeyAgreement); ok { + c.curveID = keyAgreement.curveID + c.peerSigAlg = keyAgreement.signatureAlgorithm } if _, err := hs.c.writeHandshakeRecord(skx, &hs.finishedHash); err != nil { return err @@ -789,6 +789,7 @@ func (hs *serverHandshakeState) doFullHandshake() error { c.sendAlert(alertDecryptError) return errors.New("tls: invalid signature by the client certificate: " + err.Error()) } + c.peerSigAlg = certVerify.signatureAlgorithm if err := transcriptMsg(certVerify, &hs.finishedHash); err != nil { return err diff --git a/src/crypto/tls/handshake_server_tls13.go b/src/crypto/tls/handshake_server_tls13.go index 090ff67fb7..dbd6ff2c4f 100644 --- a/src/crypto/tls/handshake_server_tls13.go +++ b/src/crypto/tls/handshake_server_tls13.go @@ -1115,6 +1115,7 @@ func (hs *serverHandshakeStateTLS13) readClientCertificate() error { c.sendAlert(alertDecryptError) return errors.New("tls: invalid signature by the client certificate: " + err.Error()) } + c.peerSigAlg = certVerify.signatureAlgorithm if err := transcriptMsg(certVerify, hs.transcript); err != nil { return err diff --git a/src/crypto/tls/key_agreement.go b/src/crypto/tls/key_agreement.go index 3daa1aa40b..88116f941e 100644 --- a/src/crypto/tls/key_agreement.go +++ b/src/crypto/tls/key_agreement.go @@ -165,25 +165,29 @@ type ecdheKeyAgreement struct { // and returned in generateClientKeyExchange. ckx *clientKeyExchangeMsg preMasterSecret []byte + + // curveID and signatureAlgorithm are set by processServerKeyExchange and + // generateServerKeyExchange. + curveID CurveID + signatureAlgorithm SignatureScheme } func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Certificate, clientHello *clientHelloMsg, hello *serverHelloMsg) (*serverKeyExchangeMsg, error) { - var curveID CurveID for _, c := range clientHello.supportedCurves { if config.supportsCurve(ka.version, c) { - curveID = c + ka.curveID = c break } } - if curveID == 0 { + if ka.curveID == 0 { return nil, errors.New("tls: no supported elliptic curves offered") } - if _, ok := curveForCurveID(curveID); !ok { + if _, ok := curveForCurveID(ka.curveID); !ok { return nil, errors.New("tls: CurvePreferences includes unsupported curve") } - key, err := generateECDHEKey(config.rand(), curveID) + key, err := generateECDHEKey(config.rand(), ka.curveID) if err != nil { return nil, err } @@ -193,8 +197,8 @@ func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Cer ecdhePublic := key.PublicKey().Bytes() serverECDHEParams := make([]byte, 1+2+1+len(ecdhePublic)) serverECDHEParams[0] = 3 // named curve - serverECDHEParams[1] = byte(curveID >> 8) - serverECDHEParams[2] = byte(curveID) + serverECDHEParams[1] = byte(ka.curveID >> 8) + serverECDHEParams[2] = byte(ka.curveID) serverECDHEParams[3] = byte(len(ecdhePublic)) copy(serverECDHEParams[4:], ecdhePublic) @@ -203,15 +207,14 @@ func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Cer return nil, fmt.Errorf("tls: certificate private key of type %T does not implement crypto.Signer", cert.PrivateKey) } - var signatureAlgorithm SignatureScheme var sigType uint8 var sigHash crypto.Hash if ka.version >= VersionTLS12 { - signatureAlgorithm, err = selectSignatureScheme(ka.version, cert, clientHello.supportedSignatureAlgorithms) + ka.signatureAlgorithm, err = selectSignatureScheme(ka.version, cert, clientHello.supportedSignatureAlgorithms) if err != nil { return nil, err } - sigType, sigHash, err = typeAndHashFromSignatureScheme(signatureAlgorithm) + sigType, sigHash, err = typeAndHashFromSignatureScheme(ka.signatureAlgorithm) if err != nil { return nil, err } @@ -249,8 +252,8 @@ func (ka *ecdheKeyAgreement) generateServerKeyExchange(config *Config, cert *Cer copy(skx.key, serverECDHEParams) k := skx.key[len(serverECDHEParams):] if ka.version >= VersionTLS12 { - k[0] = byte(signatureAlgorithm >> 8) - k[1] = byte(signatureAlgorithm) + k[0] = byte(ka.signatureAlgorithm >> 8) + k[1] = byte(ka.signatureAlgorithm) k = k[2:] } k[0] = byte(len(sig) >> 8) @@ -284,7 +287,7 @@ func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHell if skx.key[0] != 3 { // named curve return errors.New("tls: server selected unsupported curve") } - curveID := CurveID(skx.key[1])<<8 | CurveID(skx.key[2]) + ka.curveID = CurveID(skx.key[1])<<8 | CurveID(skx.key[2]) publicLen := int(skx.key[3]) if publicLen+4 > len(skx.key) { @@ -298,15 +301,15 @@ func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHell return errServerKeyExchange } - if !slices.Contains(clientHello.supportedCurves, curveID) { + if !slices.Contains(clientHello.supportedCurves, ka.curveID) { return errors.New("tls: server selected unoffered curve") } - if _, ok := curveForCurveID(curveID); !ok { + if _, ok := curveForCurveID(ka.curveID); !ok { return errors.New("tls: server selected unsupported curve") } - key, err := generateECDHEKey(config.rand(), curveID) + key, err := generateECDHEKey(config.rand(), ka.curveID) if err != nil { return err } @@ -330,16 +333,16 @@ func (ka *ecdheKeyAgreement) processServerKeyExchange(config *Config, clientHell var sigType uint8 var sigHash crypto.Hash if ka.version >= VersionTLS12 { - signatureAlgorithm := SignatureScheme(sig[0])<<8 | SignatureScheme(sig[1]) + ka.signatureAlgorithm = SignatureScheme(sig[0])<<8 | SignatureScheme(sig[1]) sig = sig[2:] if len(sig) < 2 { return errServerKeyExchange } - if !isSupportedSignatureAlgorithm(signatureAlgorithm, clientHello.supportedSignatureAlgorithms) { + if !isSupportedSignatureAlgorithm(ka.signatureAlgorithm, clientHello.supportedSignatureAlgorithms) { return errors.New("tls: certificate used with invalid signature algorithm") } - sigType, sigHash, err = typeAndHashFromSignatureScheme(signatureAlgorithm) + sigType, sigHash, err = typeAndHashFromSignatureScheme(ka.signatureAlgorithm) if err != nil { return err } diff --git a/src/errors/example_test.go b/src/errors/example_test.go index 1976f05afa..278df8c7da 100644 --- a/src/errors/example_test.go +++ b/src/errors/example_test.go @@ -66,11 +66,13 @@ func ExampleJoin() { if errors.Is(err, err2) { fmt.Println("err is err2") } + fmt.Println(err.(interface{ Unwrap() []error }).Unwrap()) // Output: // err1 // err2 // err is err1 // err is err2 + // [err1 err2] } func ExampleIs() { diff --git a/src/go/ast/filter.go b/src/go/ast/filter.go index 89682846df..7a0a402037 100644 --- a/src/go/ast/filter.go +++ b/src/go/ast/filter.go @@ -34,6 +34,9 @@ func FileExports(src *File) bool { // // PackageExports reports whether there are exported declarations; // it returns false otherwise. +// +// Deprecated: use the type checker [go/types] instead of [Package]; +// see [Object]. Alternatively, use [FileExports]. func PackageExports(pkg *Package) bool { return filterPackage(pkg, exportFilter, true) } @@ -276,6 +279,9 @@ func filterFile(src *File, f Filter, export bool) bool { // // FilterPackage reports whether there are any top-level declarations // left after filtering. +// +// Deprecated: use the type checker [go/types] instead of [Package]; +// see [Object]. Alternatively, use [FilterFile]. func FilterPackage(pkg *Package, f Filter) bool { return filterPackage(pkg, f, false) } @@ -294,8 +300,13 @@ func filterPackage(pkg *Package, f Filter, export bool) bool { // Merging of package files // The MergeMode flags control the behavior of [MergePackageFiles]. +// +// Deprecated: use the type checker [go/types] instead of [Package]; +// see [Object]. type MergeMode uint +// Deprecated: use the type checker [go/types] instead of [Package]; +// see [Object]. const ( // If set, duplicate function declarations are excluded. FilterFuncDuplicates MergeMode = 1 << iota @@ -332,6 +343,9 @@ var separator = &Comment{token.NoPos, "//"} // MergePackageFiles creates a file AST by merging the ASTs of the // files belonging to a package. The mode flags control merging behavior. +// +// Deprecated: this function is poorly specified and has unfixable +// bugs; also [Package] is deprecated. func MergePackageFiles(pkg *Package, mode MergeMode) *File { // Count the number of package docs, comments and declarations across // all package files. Also, compute sorted list of filenames, so that diff --git a/src/go/token/export_test.go b/src/go/token/export_test.go new file mode 100644 index 0000000000..b1bd26958d --- /dev/null +++ b/src/go/token/export_test.go @@ -0,0 +1,9 @@ +// Copyright 2025 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. + +package token + +// exports for tests + +func SearchInts(a []int, x int) int { return searchInts(a, x) } diff --git a/src/go/token/position_bench_test.go b/src/go/token/position_bench_test.go index 41be7285b7..7bb9de8946 100644 --- a/src/go/token/position_bench_test.go +++ b/src/go/token/position_bench_test.go @@ -2,9 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package token +package token_test import ( + "go/build" + "go/token" + "math/rand/v2" + "os" + "path/filepath" "testing" ) @@ -14,11 +19,103 @@ func BenchmarkSearchInts(b *testing.B) { data[i] = i } const x = 8 - if r := searchInts(data, x); r != x { + if r := token.SearchInts(data, x); r != x { b.Errorf("got index = %d; want %d", r, x) } b.ResetTimer() for i := 0; i < b.N; i++ { - searchInts(data, x) + token.SearchInts(data, x) } } + +func BenchmarkFileSet_Position(b *testing.B) { + rng := rand.New(rand.NewPCG(rand.Uint64(), rand.Uint64())) + + // Create a FileSet based on the files of net/http, + // a single large package. + netHTTPFset := token.NewFileSet() + pkg, err := build.Default.Import("net/http", "", 0) + if err != nil { + b.Fatal(err) + } + for _, filename := range pkg.GoFiles { + filename = filepath.Join(pkg.Dir, filename) + fi, err := os.Stat(filename) + if err != nil { + b.Fatal(err) + } + netHTTPFset.AddFile(filename, -1, int(fi.Size())) + } + + // Measure randomly distributed Pos values across net/http. + b.Run("random", func(b *testing.B) { + base := netHTTPFset.Base() + for b.Loop() { + pos := token.Pos(rng.IntN(base)) + _ = netHTTPFset.Position(pos) + } + }) + + // Measure random lookups within the same file of net/http. + // (It's faster because of the "last file" cache.) + b.Run("file", func(b *testing.B) { + var file *token.File + for file = range netHTTPFset.Iterate { + break + } + base, size := file.Base(), file.Size() + for b.Loop() { + _ = netHTTPFset.Position(token.Pos(base + rng.IntN(size))) + } + }) + + // Measure random lookups on a FileSet with a great many files. + b.Run("manyfiles", func(b *testing.B) { + fset := token.NewFileSet() + for range 25000 { + fset.AddFile("", -1, 10000) + } + base := fset.Base() + for b.Loop() { + pos := token.Pos(rng.IntN(base)) + _ = fset.Position(pos) + } + }) +} + +func BenchmarkFileSet_AddExistingFiles(b *testing.B) { + // Create the "universe" of files. + fset := token.NewFileSet() + var files []*token.File + for range 25000 { + files = append(files, fset.AddFile("", -1, 10000)) + } + rand.Shuffle(len(files), func(i, j int) { + files[i], files[j] = files[j], files[i] + }) + + // choose returns n random files. + choose := func(n int) []*token.File { + res := make([]*token.File, n) + for i := range res { + res[i] = files[rand.IntN(n)] + } + return files[:n] + } + + // Measure the cost of creating a FileSet with a large number + // of files added in small handfuls, with some overlap. + // This case is critical to gopls. + b.Run("sequence", func(b *testing.B) { + for b.Loop() { + b.StopTimer() + fset2 := token.NewFileSet() + fset2.AddExistingFiles(files[:10000]...) + b.StartTimer() + + for range 1000 { + fset2.AddExistingFiles(choose(10)...) // about one package + } + } + }) +} diff --git a/src/internal/synctest/synctest_test.go b/src/internal/synctest/synctest_test.go index e46040e048..7f71df1710 100644 --- a/src/internal/synctest/synctest_test.go +++ b/src/internal/synctest/synctest_test.go @@ -9,11 +9,13 @@ import ( "internal/synctest" "iter" "reflect" + "runtime" "slices" "strconv" "sync" "testing" "time" + "weak" ) func TestNow(t *testing.T) { @@ -625,6 +627,17 @@ func TestHappensBefore(t *testing.T) { } } +// https://go.dev/issue/73817 +func TestWeak(t *testing.T) { + synctest.Run(func() { + for range 100 { + runtime.GC() + b := make([]byte, 1024) + weak.Make(&b) + } + }) +} + func wantPanic(t *testing.T, want string) { if e := recover(); e != nil { if got := fmt.Sprint(e); got != want { diff --git a/src/io/fs/example_test.go b/src/io/fs/example_test.go index c9027034c4..e9ad2a3d83 100644 --- a/src/io/fs/example_test.go +++ b/src/io/fs/example_test.go @@ -9,8 +9,89 @@ import ( "io/fs" "log" "os" + "testing/fstest" ) +func ExampleGlob() { + fsys := fstest.MapFS{ + "file.txt": {}, + "file.go": {}, + "dir/file.txt": {}, + "dir/file.go": {}, + "dir/subdir/x.go": {}, + } + + patterns := []string{ + "*.txt", + "*.go", + "dir/*.go", + "dir/*/x.go", + } + + for _, pattern := range patterns { + matches, err := fs.Glob(fsys, pattern) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%q matches: %v\n", pattern, matches) + } + + // Output: + // "*.txt" matches: [file.txt] + // "*.go" matches: [file.go] + // "dir/*.go" matches: [dir/file.go] + // "dir/*/x.go" matches: [dir/subdir/x.go] +} + +func ExampleReadFile() { + fsys := fstest.MapFS{ + "hello.txt": { + Data: []byte("Hello, World!\n"), + }, + } + + data, err := fs.ReadFile(fsys, "hello.txt") + if err != nil { + log.Fatal(err) + } + + fmt.Print(string(data)) + + // Output: + // Hello, World! +} + +func ExampleValidPath() { + paths := []string{ + ".", + "x", + "x/y/z", + "", + "..", + "/x", + "x/", + "x//y", + "x/./y", + "x/../y", + } + + for _, path := range paths { + fmt.Printf("ValidPath(%q) = %t\n", path, fs.ValidPath(path)) + } + + // Output: + // ValidPath(".") = true + // ValidPath("x") = true + // ValidPath("x/y/z") = true + // ValidPath("") = false + // ValidPath("..") = false + // ValidPath("/x") = false + // ValidPath("x/") = false + // ValidPath("x//y") = false + // ValidPath("x/./y") = false + // ValidPath("x/../y") = false +} + func ExampleWalkDir() { root := "/usr/local/go/bin" fileSystem := os.DirFS(root) diff --git a/src/log/slog/logger_test.go b/src/log/slog/logger_test.go index 63595504fe..bf645d9c4c 100644 --- a/src/log/slog/logger_test.go +++ b/src/log/slog/logger_test.go @@ -315,7 +315,7 @@ func TestCallDepthConnection(t *testing.T) { got := string(firstLine) want := fmt.Sprintf( - `source=:0 msg="logger_test.go:%d: %s"`, + `msg="logger_test.go:%d: %s"`, line+i, tt.name, ) if got != want { diff --git a/src/os/os_test.go b/src/os/os_test.go index 281f13c7c9..9f6eb13e1f 100644 --- a/src/os/os_test.go +++ b/src/os/os_test.go @@ -2309,9 +2309,9 @@ func TestOpenFileCreateExclDanglingSymlink(t *testing.T) { var f *File var err error if r == nil { - f, err = OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o666) + f, err = OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o444) } else { - f, err = r.OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o666) + f, err = r.OpenFile(link, O_WRONLY|O_CREATE|O_EXCL, 0o444) } if err == nil { f.Close() diff --git a/src/runtime/lockrank.go b/src/runtime/lockrank.go index 456f2b75e6..7f32e6397b 100644 --- a/src/runtime/lockrank.go +++ b/src/runtime/lockrank.go @@ -204,7 +204,7 @@ var lockPartialOrder [][]lockRank = [][]lockRank{ lockRankRoot: {}, lockRankItab: {}, lockRankReflectOffs: {lockRankItab}, - lockRankSynctest: {lockRankSysmon, lockRankScavenge, lockRankSweep, lockRankTestR, lockRankTimerSend, lockRankPollDesc, lockRankWakeableSleep, lockRankHchan, lockRankNotifyList, lockRankTimers, lockRankTimer, lockRankRoot, lockRankItab, lockRankReflectOffs}, + lockRankSynctest: {lockRankSysmon, lockRankScavenge, lockRankSweepWaiters, lockRankStrongFromWeakQueue, lockRankSweep, lockRankTestR, lockRankTimerSend, lockRankPollDesc, lockRankWakeableSleep, lockRankHchan, lockRankNotifyList, lockRankTimers, lockRankTimer, lockRankRoot, lockRankItab, lockRankReflectOffs}, lockRankUserArenaState: {}, lockRankTraceBuf: {lockRankSysmon, lockRankScavenge}, lockRankTraceStrings: {lockRankSysmon, lockRankScavenge, lockRankTraceBuf}, diff --git a/src/runtime/mcleanup.go b/src/runtime/mcleanup.go index 5cbae156ba..c368730c57 100644 --- a/src/runtime/mcleanup.go +++ b/src/runtime/mcleanup.go @@ -457,6 +457,13 @@ func (q *cleanupQueue) flush() { // new cleanup goroutines. var cb *cleanupBlock for _, pp := range allp { + if pp == nil { + // This function is reachable via mallocgc in the + // middle of procresize, when allp has been resized, + // but the new Ps not allocated yet. + missing++ + continue + } b := pp.cleanups if b == nil { missing++ diff --git a/src/runtime/mklockrank.go b/src/runtime/mklockrank.go index 6cccece9b5..2e3375331a 100644 --- a/src/runtime/mklockrank.go +++ b/src/runtime/mklockrank.go @@ -99,7 +99,15 @@ NONE < reflectOffs; # Synctest -hchan, root, timers, timer, notifyList, reflectOffs < synctest; +hchan, + notifyList, + reflectOffs, + root, + strongFromWeakQueue, + sweepWaiters, + timer, + timers +< synctest; # User arena state NONE < userArenaState; diff --git a/src/runtime/testdata/testsynctest/main.go b/src/runtime/testdata/testsynctest/main.go index d2cbc99258..b47e3fcfc9 100644 --- a/src/runtime/testdata/testsynctest/main.go +++ b/src/runtime/testdata/testsynctest/main.go @@ -8,6 +8,7 @@ import ( "internal/synctest" "runtime" "runtime/metrics" + "sync/atomic" ) // This program ensures system goroutines (GC workers, finalizer goroutine) @@ -27,11 +28,24 @@ func numGCCycles() uint64 { } func main() { + // Channels created by a finalizer and cleanup func registered within the bubble. + var ( + finalizerCh atomic.Pointer[chan struct{}] + cleanupCh atomic.Pointer[chan struct{}] + ) synctest.Run(func() { - // Start the finalizer goroutine. - p := new(int) - runtime.SetFinalizer(p, func(*int) {}) - + // Start the finalizer and cleanup goroutines. + { + p := new(int) + runtime.SetFinalizer(p, func(*int) { + ch := make(chan struct{}) + finalizerCh.Store(&ch) + }) + runtime.AddCleanup(p, func(struct{}) { + ch := make(chan struct{}) + cleanupCh.Store(&ch) + }, struct{}{}) + } startingCycles := numGCCycles() ch1 := make(chan *int) ch2 := make(chan *int) @@ -55,13 +69,18 @@ func main() { // If we've improperly put a GC goroutine into the synctest group, // this Wait is going to hang. - synctest.Wait() + //synctest.Wait() // End the test after a couple of GC cycles have passed. - if numGCCycles()-startingCycles > 1 { + if numGCCycles()-startingCycles > 1 && finalizerCh.Load() != nil && cleanupCh.Load() != nil { break } } }) + // Close the channels created by the finalizer and cleanup func. + // If the funcs improperly ran inside the bubble, these channels are bubbled + // and trying to close them will panic. + close(*finalizerCh.Load()) + close(*cleanupCh.Load()) println("success") } diff --git a/src/runtime/trace/subscribe.go b/src/runtime/trace/subscribe.go index 45320cee36..7e22b6abdb 100644 --- a/src/runtime/trace/subscribe.go +++ b/src/runtime/trace/subscribe.go @@ -155,6 +155,14 @@ func (t *traceMultiplexer) startLocked() error { t.subscribersMu.Unlock() go func() { + header := runtime_readTrace() + if traceStartWriter != nil { + traceStartWriter.Write(header) + } + if flightRecorder != nil { + flightRecorder.Write(header) + } + for { data := runtime_readTrace() if data == nil { @@ -167,9 +175,18 @@ func (t *traceMultiplexer) startLocked() error { // Pick up any changes. t.subscribersMu.Lock() + frIsNew := flightRecorder != t.flightRecorder && t.flightRecorder != nil + trIsNew := traceStartWriter != t.traceStartWriter && t.traceStartWriter != nil flightRecorder = t.flightRecorder traceStartWriter = t.traceStartWriter t.subscribersMu.Unlock() + + if trIsNew { + traceStartWriter.Write(header) + } + if frIsNew { + flightRecorder.Write(header) + } } else { if traceStartWriter != nil { traceStartWriter.Write(data) diff --git a/src/runtime/trace/subscribe_test.go b/src/runtime/trace/subscribe_test.go new file mode 100644 index 0000000000..0e6c57cbc6 --- /dev/null +++ b/src/runtime/trace/subscribe_test.go @@ -0,0 +1,153 @@ +// Copyright 2025 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. + +package trace_test + +import ( + "bytes" + inttrace "internal/trace" + "internal/trace/testtrace" + "io" + "runtime" + "runtime/trace" + "slices" + "testing" +) + +func TestSubscribers(t *testing.T) { + validate := func(t *testing.T, source string, tr io.Reader) { + // Prepare to read the trace snapshot. + r, err := inttrace.NewReader(tr) + if err != nil { + t.Fatalf("unexpected error creating trace reader for %s: %v", source, err) + return + } + + v := testtrace.NewValidator() + // These platforms can't guarantee a monotonically increasing clock reading in a short trace. + if runtime.GOOS == "windows" || runtime.GOARCH == "wasm" { + v.SkipClockSnapshotChecks() + } + // Make sure there are Sync events: at the start and end. + var syncs []int + evs := 0 + for { + ev, err := r.ReadEvent() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("unexpected error reading trace for %s: %v", source, err) + } + if err := v.Event(ev); err != nil { + t.Fatalf("event validation failed: %s", err) + } + if ev.Kind() == inttrace.EventSync { + syncs = append(syncs, evs) + } + evs++ + } + ends := []int{syncs[0], syncs[len(syncs)-1]} + if wantEnds := []int{0, evs - 1}; !slices.Equal(wantEnds, ends) { + t.Errorf("expected a sync event at each end of the trace, found sync events at %d instead of %d for %s", + ends, wantEnds, source) + } + } + + validateTraces := func(t *testing.T, tReader, frReader io.Reader) { + validate(t, "tracer", tReader) + validate(t, "flightRecorder", frReader) + } + startFlightRecorder := func(t *testing.T) *trace.FlightRecorder { + fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{}) + if err := fr.Start(); err != nil { + t.Fatalf("unexpected error creating flight recorder: %v", err) + } + return fr + } + startTrace := func(t *testing.T, w io.Writer) { + if err := trace.Start(w); err != nil { + t.Fatalf("unexpected error starting flight recorder: %v", err) + } + } + stopFlightRecorder := func(t *testing.T, fr *trace.FlightRecorder, w io.Writer) { + if _, err := fr.WriteTo(w); err != nil { + t.Fatalf("unexpected error writing trace from flight recorder: %v", err) + } + fr.Stop() + } + stopTrace := func() { + trace.Stop() + } + t.Run("start(flight)_start(trace)_stop(trace)_stop(flight)", func(t *testing.T) { + if trace.IsEnabled() { + t.Skip("skipping because trace is already enabled") + } + frBuf := new(bytes.Buffer) + tBuf := new(bytes.Buffer) + fr := startFlightRecorder(t) + defer fr.Stop() + startTrace(t, tBuf) + defer trace.Stop() + stopTrace() + stopFlightRecorder(t, fr, frBuf) + validateTraces(t, tBuf, frBuf) + }) + t.Run("start(trace)_start(flight)_stop(trace)_stop(flight)", func(t *testing.T) { + if trace.IsEnabled() { + t.Skip("skipping because trace is already enabled") + } + frBuf := new(bytes.Buffer) + tBuf := new(bytes.Buffer) + startTrace(t, tBuf) + defer trace.Stop() + fr := startFlightRecorder(t) + defer fr.Stop() + stopTrace() + stopFlightRecorder(t, fr, frBuf) + validateTraces(t, tBuf, frBuf) + }) + t.Run("start(flight)_stop(flight)_start(trace)_stop(trace)", func(t *testing.T) { + if trace.IsEnabled() { + t.Skip("skipping because trace is already enabled") + } + frBuf := new(bytes.Buffer) + tBuf := new(bytes.Buffer) + fr := startFlightRecorder(t) + defer fr.Stop() + stopFlightRecorder(t, fr, frBuf) + startTrace(t, tBuf) + defer trace.Stop() + stopTrace() + validateTraces(t, tBuf, frBuf) + }) + t.Run("start(flight)_stop(flight)_start(trace)_stop(trace)", func(t *testing.T) { + if trace.IsEnabled() { + t.Skip("skipping because trace is already enabled") + } + frBuf := new(bytes.Buffer) + tBuf := new(bytes.Buffer) + fr := startFlightRecorder(t) + defer fr.Stop() + stopFlightRecorder(t, fr, frBuf) + startTrace(t, tBuf) + defer trace.Stop() + stopTrace() + validateTraces(t, tBuf, frBuf) + }) + t.Run("start(flight)_start(trace)_stop(flight)_stop(trace)", func(t *testing.T) { + if trace.IsEnabled() { + t.Skip("skipping because trace is already enabled") + } + frBuf := new(bytes.Buffer) + tBuf := new(bytes.Buffer) + fr := startFlightRecorder(t) + defer fr.Stop() + startTrace(t, tBuf) + defer trace.Stop() + stopFlightRecorder(t, fr, frBuf) + stopTrace() + validateTraces(t, tBuf, frBuf) + }) +} diff --git a/src/syscall/syscall_windows.go b/src/syscall/syscall_windows.go index 653e20f496..01c039cf28 100644 --- a/src/syscall/syscall_windows.go +++ b/src/syscall/syscall_windows.go @@ -398,22 +398,7 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { if flag&O_CLOEXEC == 0 { sa = makeInheritSa() } - // We don't use CREATE_ALWAYS, because when opening a file with - // FILE_ATTRIBUTE_READONLY these will replace an existing file - // with a new, read-only one. See https://go.dev/issue/38225. - // - // Instead, we ftruncate the file after opening when O_TRUNC is set. - var createmode uint32 var attrs uint32 = FILE_ATTRIBUTE_NORMAL - switch { - case flag&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL): - createmode = CREATE_NEW - attrs |= FILE_FLAG_OPEN_REPARSE_POINT // don't follow symlinks - case flag&O_CREAT == O_CREAT: - createmode = OPEN_ALWAYS - default: - createmode = OPEN_EXISTING - } if perm&S_IWRITE == 0 { attrs = FILE_ATTRIBUTE_READONLY } @@ -433,6 +418,21 @@ func Open(name string, flag int, perm uint32) (fd Handle, err error) { const _FILE_FLAG_WRITE_THROUGH = 0x80000000 attrs |= _FILE_FLAG_WRITE_THROUGH } + // We don't use CREATE_ALWAYS, because when opening a file with + // FILE_ATTRIBUTE_READONLY these will replace an existing file + // with a new, read-only one. See https://go.dev/issue/38225. + // + // Instead, we ftruncate the file after opening when O_TRUNC is set. + var createmode uint32 + switch { + case flag&(O_CREAT|O_EXCL) == (O_CREAT | O_EXCL): + createmode = CREATE_NEW + attrs |= FILE_FLAG_OPEN_REPARSE_POINT // don't follow symlinks + case flag&O_CREAT == O_CREAT: + createmode = OPEN_ALWAYS + default: + createmode = OPEN_EXISTING + } h, err := createFile(namep, access, sharemode, sa, createmode, attrs, 0) if h == InvalidHandle { if err == ERROR_ACCESS_DENIED && (attrs&FILE_FLAG_BACKUP_SEMANTICS == 0) { diff --git a/src/testing/synctest/synctest.go b/src/testing/synctest/synctest.go index aeac8c4b43..c7e93b2201 100644 --- a/src/testing/synctest/synctest.go +++ b/src/testing/synctest/synctest.go @@ -83,6 +83,10 @@ // is associated with it. Operating on a bubbled channel, timer, or // ticker from outside the bubble panics. // +// Cleanup functions and finalizers registered with +// [runtime.AddCleanup] and [runtime.SetFinalizer] +// run outside of any bubble. +// // # Example: Context.AfterFunc // // This example demonstrates testing the [context.AfterFunc] function. diff --git a/test/fixedbugs/issue73888.go b/test/fixedbugs/issue73888.go new file mode 100644 index 0000000000..b3c1ff768f --- /dev/null +++ b/test/fixedbugs/issue73888.go @@ -0,0 +1,34 @@ +// run + +// Copyright 2025 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. + +package main + +type SourceRange struct { + x, y int +} + +func (r *SourceRange) String() string { + return "hello" +} + +type SourceNode interface { + SourceRange() +} + +type testNode SourceRange + +func (tn testNode) SourceRange() { +} + +func main() { + n := testNode(SourceRange{}) // zero value + Errorf(n) +} + +//go:noinline +func Errorf(n SourceNode) { + n.SourceRange() +} diff --git a/test/fixedbugs/issue73888b.go b/test/fixedbugs/issue73888b.go new file mode 100644 index 0000000000..b6e0289cc4 --- /dev/null +++ b/test/fixedbugs/issue73888b.go @@ -0,0 +1,34 @@ +// run + +// Copyright 2025 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. + +package main + +type SourceRange struct { + x, y int +} + +func (r *SourceRange) String() string { + return "hello" +} + +type SourceNode interface { + SourceRange() +} + +type testNode SourceRange + +func (tn testNode) SourceRange() { +} + +func main() { + n := testNode(SourceRange{1, 1}) // not zero value + Errorf(n) +} + +//go:noinline +func Errorf(n SourceNode) { + n.SourceRange() +}